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


##########
superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx:
##########
@@ -883,602 +835,693 @@ const mapStateToProps = (state: RootState) => ({
 const connector = connect(mapStateToProps, mapDispatchToProps);
 type PropsFromRedux = ConnectedProps<typeof connector>;
 
-type DatasourceEditorProps = DatasourceEditorOwnProps &
-  PropsFromRedux & {
-    theme?: SupersetTheme;
-  };
-
-class DatasourceEditor extends PureComponent<
-  DatasourceEditorProps,
-  DatasourceEditorState
-> {
-  private isComponentMounted: boolean;
+type DatasourceEditorProps = DatasourceEditorOwnProps & PropsFromRedux;
+
+function DatasourceEditor({
+  datasource: propsDatasource,
+  onChange = () => {},
+  addSuccessToast,
+  addDangerToast,
+  setIsEditing = () => {},
+  database,
+  runQuery,
+  resetQuery,
+  formatQuery: formatQueryAction,
+}: DatasourceEditorProps) {
+  const theme = useTheme();
+  const isComponentMounted = useRef(false);
+  const isInitialMount = useRef(true);
+  const prevPropsDatasourceRef = useRef(propsDatasource);
+  const isSyncingColumnsFromProps = useRef(false);
+  const abortControllers = useRef<AbortControllers>({
+    formatQuery: null,
+    formatSql: null,
+    syncMetadata: null,
+    fetchUsageData: null,
+  });
+
+  // Initialize datasource state with transformed owners and metrics
+  const [datasource, setDatasource] = useState<DatasourceObject>(() => ({
+    ...propsDatasource,
+    owners: propsDatasource.owners.map(owner => ({
+      value: owner.value || owner.id,
+      label: owner.label || `${owner.first_name} ${owner.last_name}`,
+    })),
+    metrics: propsDatasource.metrics?.map(metric => {
+      const {
+        certified_by: certifiedByMetric,
+        certification_details: certificationDetails,
+      } = metric;
+      const {
+        certification: {
+          details = undefined,
+          certified_by: certifiedBy = undefined,
+        } = {},
+        warning_markdown: warningMarkdown,
+      } = JSON.parse(metric.extra || '{}') || {};
+      return {
+        ...metric,
+        certification_details: certificationDetails || details,
+        warning_markdown: warningMarkdown || '',
+        certified_by: certifiedBy || certifiedByMetric,
+      };
+    }),
+  }));
 
-  private abortControllers: AbortControllers;
+  const [errors, setErrors] = useState<string[]>([]);
+  const [isSqla] = useState(
+    propsDatasource.datasource_type === 'table' ||
+      propsDatasource.type === 'table',
+  );
+  const [isEditMode, setIsEditMode] = useState(false);
+  const [databaseColumns, setDatabaseColumns] = useState<Column[]>(
+    propsDatasource.columns.filter(col => !col.expression),
+  );
+  const [calculatedColumns, setCalculatedColumns] = useState<Column[]>(
+    propsDatasource.columns.filter(col => !!col.expression),
+  );
+  const [folders, setFolders] = useState<DatasourceFolder[]>(
+    propsDatasource.folders || [],
+  );
+  const [folderCount, setFolderCount] = useState(() => {
+    const savedFolders = propsDatasource.folders || [];
+    const savedCount = countAllFolders(savedFolders);
+    const hasDefaultsSaved = savedFolders.some(f => isDefaultFolder(f.uuid));
+    return savedCount + (hasDefaultsSaved ? 0 : DEFAULT_FOLDERS_COUNT);
+  });
+  const [metadataLoading, setMetadataLoading] = useState(false);
+  const [activeTabKey, setActiveTabKey] = useState(TABS_KEYS.SOURCE);
+  const [datasourceType, setDatasourceType] = useState(
+    propsDatasource.sql
+      ? DATASOURCE_TYPES.virtual.key
+      : DATASOURCE_TYPES.physical.key,
+  );
+  const [usageCharts, setUsageCharts] = useState<ChartUsageData[]>([]);
+  const [usageChartsCount, setUsageChartsCount] = useState(0);
+  const [metricSearchTerm, setMetricSearchTerm] = useState('');
+  const [columnSearchTerm, setColumnSearchTerm] = useState('');
+  const [calculatedColumnSearchTerm, setCalculatedColumnSearchTerm] =
+    useState('');
+
+  const findDuplicates = useCallback(
+    <T,>(arr: T[], accessor: (obj: T) => string): string[] => {
+      const seen: Record<string, null> = {};
+      const dups: string[] = [];
+      arr.forEach((obj: T) => {
+        const item = accessor(obj);
+        if (item in seen) {
+          dups.push(item);
+        } else {
+          seen[item] = null;
+        }
+      });
+      return dups;
+    },
+    [],
+  );
 
-  static defaultProps = {
-    onChange: () => {},
-    setIsEditing: () => {},
-  };
+  const validate = useCallback(
+    (callback: (validationErrors: string[]) => void) => {
+      let validationErrors: string[] = [];
+      let dups: string[];
 
-  constructor(props: DatasourceEditorProps) {
-    super(props);
-    this.state = {
-      datasource: {
-        ...props.datasource,
-        owners: props.datasource.owners.map(owner => {
-          const ownerName =
-            owner.label || `${owner.first_name} ${owner.last_name}`;
-          return {
-            value: owner.value || owner.id,
-            label: OwnerSelectLabel({
-              name: typeof ownerName === 'string' ? ownerName : '',
-              email: owner.email,
-            }),
-            [OWNER_TEXT_LABEL_PROP]:
-              typeof ownerName === 'string' ? ownerName : '',
-            [OWNER_EMAIL_PROP]: owner.email ?? '',
-          };
-        }),
-        metrics: props.datasource.metrics?.map(metric => {
-          const {
-            certified_by: certifiedByMetric,
-            certification_details: certificationDetails,
-          } = metric;
-          const {
-            certification: {
-              details = undefined,
-              certified_by: certifiedBy = undefined,
-            } = {},
-            warning_markdown: warningMarkdown,
-          } = JSON.parse(metric.extra || '{}') || {};
-          return {
-            ...metric,
-            certification_details: certificationDetails || details,
-            warning_markdown: warningMarkdown || '',
-            certified_by: certifiedBy || certifiedByMetric,
-          };
-        }),
-      },
-      errors: [],
-      isSqla:
-        props.datasource.datasource_type === 'table' ||
-        props.datasource.type === 'table',
-      isEditMode: false,
-      databaseColumns: props.datasource.columns.filter(col => !col.expression),
-      calculatedColumns: props.datasource.columns.filter(
-        col => !!col.expression,
-      ),
-      folders: props.datasource.folders || [],
-      folderCount: (() => {
-        const savedFolders = props.datasource.folders || [];
-        const savedCount = countAllFolders(savedFolders);
-        const hasDefaultsSaved = savedFolders.some(f =>
-          isDefaultFolder(f.uuid),
-        );
-        return savedCount + (hasDefaultsSaved ? 0 : DEFAULT_FOLDERS_COUNT);
-      })(),
-      metadataLoading: false,
-      activeTabKey: TABS_KEYS.SOURCE,
-      datasourceType: props.datasource.sql
-        ? DATASOURCE_TYPES.virtual.key
-        : DATASOURCE_TYPES.physical.key,
-      usageCharts: [],
-      usageChartsCount: 0,
-      metricSearchTerm: '',
-      columnSearchTerm: '',
-      calculatedColumnSearchTerm: '',
-    };
+      // Looking for duplicate column_name
+      dups = findDuplicates(datasource.columns, obj => obj.column_name);
+      validationErrors = validationErrors.concat(
+        dups.map(name => t('Column name [%s] is duplicated', name)),
+      );
 
-    this.isComponentMounted = false;
-    this.abortControllers = {
-      formatQuery: null,
-      formatSql: null,
-      syncMetadata: null,
-      fetchUsageData: null,
-    };
+      // Looking for duplicate metric_name
+      dups = findDuplicates(datasource.metrics ?? [], obj => obj.metric_name);
+      validationErrors = validationErrors.concat(
+        dups.map(name => t('Metric name [%s] is duplicated', name)),
+      );
 
-    this.onChange = this.onChange.bind(this);
-    this.onChangeEditMode = this.onChangeEditMode.bind(this);
-    this.onDatasourcePropChange = this.onDatasourcePropChange.bind(this);
-    this.onDatasourceChange = this.onDatasourceChange.bind(this);
-    this.tableChangeAndSyncMetadata =
-      this.tableChangeAndSyncMetadata.bind(this);
-    this.syncMetadata = this.syncMetadata.bind(this);
-    this.setColumns = this.setColumns.bind(this);
-    this.validateAndChange = this.validateAndChange.bind(this);
-    this.handleTabSelect = this.handleTabSelect.bind(this);
-    this.formatSql = this.formatSql.bind(this);
-    this.fetchUsageData = this.fetchUsageData.bind(this);
-    this.handleFoldersChange = this.handleFoldersChange.bind(this);
-  }
+      // Making sure calculatedColumns have an expression defined
+      const noFilterCalcCols = calculatedColumns.filter(
+        col => !col.expression && !col.json,
+      );
+      validationErrors = validationErrors.concat(
+        noFilterCalcCols.map(col =>
+          t('Calculated column [%s] requires an expression', col.column_name),
+        ),
+      );
 
-  onChange() {
-    // Emptying SQL if "Physical" radio button is selected
-    // Currently the logic to know whether the source is
-    // physical or virtual is based on whether SQL is empty or not.
-    const { datasourceType, datasource } = this.state;
-    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);
+      // validate currency code (skip 'AUTO' - it's a placeholder for 
auto-detection)
+      try {
+        datasource.metrics?.forEach(
+          metric =>
+            metric.currency?.symbol &&
+            metric.currency.symbol !== 'AUTO' &&
+            new Intl.NumberFormat('en-US', {
+              style: 'currency',
+              currency: metric.currency.symbol,
+            }),
+        );
+      } catch {
+        validationErrors = validationErrors.concat([
+          t('Invalid currency code in saved metrics'),
+        ]);
+      }
 
-    const newDatasource = {
-      ...this.state.datasource,
-      sql,
-      columns,
-      folders,
-    };
+      // Validate folders
+      if (folders?.length > 0) {
+        const folderValidation = validateFolders(folders);
+        validationErrors = validationErrors.concat(folderValidation.errors);
+      }
 
-    this.props.onChange?.(newDatasource, this.state.errors);
-  }
+      setErrors(validationErrors);
+      callback(validationErrors);
+    },
+    [datasource, calculatedColumns, folders, findDuplicates],
+  );
 
-  onChangeEditMode() {
-    this.props.setIsEditing?.(!this.state.isEditMode);
-    this.setState(prevState => ({ isEditMode: !prevState.isEditMode }));
-  }
+  const onChangeInternal = useCallback(
+    (validationErrors: string[] = errors) => {
+      // Emptying SQL if "Physical" radio button is selected
+      const sql =
+        datasourceType === DATASOURCE_TYPES.physical.key ? '' : datasource.sql;
 
-  onDatasourceChange(
-    datasource: DatasourceObject,
-    callback: () => void = this.validateAndChange,
-  ) {
-    this.setState({ datasource }, callback);
-  }
+      const columns = [...databaseColumns, ...calculatedColumns];
 
-  onDatasourcePropChange(attr: string, value: unknown) {
-    if (value === undefined) return; // if value is undefined do not update 
state
-    const datasource = { ...this.state.datasource, [attr]: value };
-    this.setState(
-      prevState => ({
-        datasource: { ...prevState.datasource, [attr]: value },
-      }),
-      () =>
-        attr === 'table_name'
-          ? this.onDatasourceChange(datasource, 
this.tableChangeAndSyncMetadata)
-          : this.onDatasourceChange(datasource, this.validateAndChange),
-    );
-  }
+      // 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 filteredFolders = filterFoldersByValidUuids(folders, validUuids);
 
-  onDatasourceTypeChange(datasourceType: string) {
-    // Call onChange after setting datasourceType to ensure
-    // SQL is cleared when switching to a physical dataset
-    this.setState({ datasourceType }, this.onChange);
-  }
+      const newDatasource = {
+        ...datasource,
+        sql,
+        columns,
+        folders: filteredFolders,
+      };
 
-  handleFoldersChange(folders: DatasourceFolder[]) {
-    const folderCount = countAllFolders(folders);
-    this.setState({ folders, folderCount }, () => {
-      this.onDatasourceChange({
-        ...this.state.datasource,
-        folders,
-      });
-    });
-  }
+      onChange(newDatasource, validationErrors);
+    },
+    [
+      datasource,
+      datasourceType,
+      databaseColumns,
+      calculatedColumns,
+      folders,
+      errors,
+      onChange,
+    ],
+  );
 
-  setColumns(
-    obj: { databaseColumns?: Column[] } | { calculatedColumns?: Column[] },
-  ) {
-    // update calculatedColumns or databaseColumns
-    this.setState(
-      obj as Pick<
-        DatasourceEditorState,
-        'databaseColumns' | 'calculatedColumns'
-      >,
-      this.validateAndChange,
-    );
-  }
+  const validateAndChange = useCallback(() => {
+    validate(onChangeInternal);
+  }, [validate, onChangeInternal]);
 
-  validateAndChange() {
-    this.validate(this.onChange);
-  }
+  const onDatasourceChange = useCallback((newDatasource: DatasourceObject) => {
+    setDatasource(newDatasource);
+  }, []);
 
-  async onQueryRun() {
-    const databaseId = this.state.datasource.database?.id;
-    const { sql } = this.state.datasource;
-    if (!databaseId || !sql) {
-      return;
-    }
-    this.props.runQuery({
-      client_id: this.props.database?.clientId,
-      database_id: databaseId,
-      runAsync: false,
-      catalog: this.state.datasource.catalog,
-      schema: this.state.datasource.schema,
-      sql,
-      tmp_table_name: '',
-      select_as_cta: false,
-      ctas_method: 'TABLE',
-      queryLimit: 25,
-      expand_data: true,
+  const onDatasourcePropChange = useCallback((attr: string, value: unknown) => 
{
+    if (value === undefined) return;
+    setDatasource(prev => {
+      const newDatasource = { ...prev, [attr]: value };
+      return newDatasource;
     });
-  }
+  }, []);
 
-  /**
-   * Formats SQL query using the formatQuery action.
-   * Aborts any pending format requests before starting a new one.
-   */
-  async onQueryFormat() {
-    const { datasource } = this.state;
-    if (!datasource.sql || !this.state.isEditMode) {
+  // Effect to trigger validation after datasource changes (skip initial mount)
+  useEffect(() => {
+    if (isInitialMount.current) {
       return;
     }
-
-    // Abort previous formatQuery if still pending
-    if (this.abortControllers.formatQuery) {
-      this.abortControllers.formatQuery.abort();
+    if (isComponentMounted.current) {
+      validateAndChange();
     }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [datasource]);
 
-    this.abortControllers.formatQuery = new AbortController();
-    const { signal } = this.abortControllers.formatQuery;
+  const onChangeEditMode = useCallback(() => {
+    setIsEditing(!isEditMode);
+    setIsEditMode(prev => !prev);
+  }, [isEditMode, setIsEditing]);
 
-    try {
-      const response = await this.props.formatQuery(datasource.sql, { signal 
});
+  const onDatasourceTypeChange = useCallback((newDatasourceType: string) => {
+    setDatasourceType(newDatasourceType);
+  }, []);
 
-      this.onDatasourcePropChange('sql', response.json.result);
-      this.props.addSuccessToast(t('SQL was formatted'));
-    } catch (error) {
-      if (error.name === 'AbortError') return;
+  // Effect to call onChange after datasourceType changes (skip initial mount)
+  useEffect(() => {
+    if (isInitialMount.current) {
+      return;
+    }
+    if (isComponentMounted.current) {
+      onChangeInternal();
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [datasourceType]);
+
+  const handleFoldersChange = useCallback((newFolders: DatasourceFolder[]) => {
+    const userMadeFolders = newFolders.filter(
+      f =>
+        f.uuid !== DEFAULT_METRICS_FOLDER_UUID &&
+        f.uuid !== DEFAULT_COLUMNS_FOLDER_UUID &&
+        (f.children?.length ?? 0) > 0,
+    );
+    setFolders(userMadeFolders);
+    setFolderCount(countAllFolders(userMadeFolders));
+    setDatasource(prev => ({ ...prev, folders: userMadeFolders }));
+  }, []);
 
-      const { error: clientError, statusText } =
-        await getClientErrorObject(error);
+  const setColumns = useCallback(
+    (
+      obj: { databaseColumns?: Column[] } | { calculatedColumns?: Column[] },
+    ) => {
+      if ('databaseColumns' in obj && obj.databaseColumns) {
+        setDatabaseColumns(obj.databaseColumns);
+      }
+      if ('calculatedColumns' in obj && obj.calculatedColumns) {
+        setCalculatedColumns(obj.calculatedColumns);
+      }
+    },
+    [],
+  );
 
-      this.props.addDangerToast(
-        clientError ||
-          statusText ||
-          t('An error occurred while formatting SQL'),
-      );
-    } finally {
-      this.abortControllers.formatQuery = null;
+  // Effect to trigger validation after user-initiated column changes
+  // Skips initial mount and prop-sync updates (which don't need validation)
+  useEffect(() => {
+    if (isInitialMount.current || isSyncingColumnsFromProps.current) {
+      isSyncingColumnsFromProps.current = false;
+      return;
     }
-  }
+    if (isComponentMounted.current) {
+      validateAndChange();
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [databaseColumns, calculatedColumns]);
 
-  getSQLLabUrl() {
+  const getSQLLabUrl = useCallback(() => {
     const queryParams = new URLSearchParams({
-      dbid: String(this.state.datasource.database?.id ?? ''),
-      sql: this.state.datasource.sql ?? '',
-      name: this.state.datasource.datasource_name ?? '',
-      schema: this.state.datasource.schema ?? '',
+      dbid: String(datasource.database?.id ?? ''),
+      sql: datasource.sql ?? '',
+      name: datasource.datasource_name ?? '',
+      schema: datasource.schema ?? '',
       autorun: 'true',
       isDataset: 'true',
     });
     return makeUrl(`/sqllab/?${queryParams.toString()}`);
-  }
+  }, [datasource]);
 
-  openOnSqlLab() {
-    window.open(this.getSQLLabUrl(), '_blank', 'noopener,noreferrer');
-  }
+  const openOnSqlLab = useCallback(() => {
+    window.open(getSQLLabUrl(), '_blank', 'noopener,noreferrer');
+  }, [getSQLLabUrl]);
 
-  tableChangeAndSyncMetadata() {
-    this.validate(() => {
-      this.syncMetadata();
-      this.onChange();
+  const onQueryRun = useCallback(async () => {
+    const databaseId = datasource.database?.id;
+    const { sql } = datasource;
+    if (!databaseId || !sql) {
+      return;
+    }
+    runQuery({
+      client_id: database?.clientId,
+      database_id: databaseId,
+      runAsync: false,
+      catalog: datasource.catalog,
+      schema: datasource.schema,
+      sql,
+      tmp_table_name: '',
+      select_as_cta: false,
+      ctas_method: 'TABLE',
+      queryLimit: 25,
+      expand_data: true,
     });
-  }
+  }, [datasource, database?.clientId, runQuery]);
 
-  /**
-   * Formats SQL query using the SQL format API endpoint.
-   * Aborts any pending format requests before starting a new one.
-   */
-  async formatSql() {
-    const { datasource } = this.state;
-    if (!datasource.sql) {
+  const onQueryFormat = useCallback(async () => {
+    if (!datasource.sql || !isEditMode) {
       return;
     }
 
-    // Abort previous formatSql if still pending
-    if (this.abortControllers.formatSql) {
-      this.abortControllers.formatSql.abort();
+    // Abort previous formatQuery if still pending
+    if (abortControllers.current.formatQuery) {
+      abortControllers.current.formatQuery.abort();
     }
 
-    this.abortControllers.formatSql = new AbortController();
-    const { signal } = this.abortControllers.formatSql;
+    abortControllers.current.formatQuery = new AbortController();
+    const { signal } = abortControllers.current.formatQuery;
 
     try {
-      const response = await SupersetClient.post({
-        endpoint: '/api/v1/sql/format',
-        body: JSON.stringify({ sql: datasource.sql }),
-        headers: { 'Content-Type': 'application/json' },
-        signal,
-      });
+      const response = await formatQueryAction(datasource.sql, { signal });
 
-      this.onDatasourcePropChange('sql', response.json.result);
-      this.props.addSuccessToast(t('SQL was formatted'));
-    } catch (error) {
-      if (error.name === 'AbortError') return;
+      onDatasourcePropChange('sql', response.json.result);
+      addSuccessToast(t('SQL was formatted'));
+    } catch (error: unknown) {
+      if ((error as Error).name === 'AbortError') return;
 
-      const { error: clientError, statusText } =
-        await getClientErrorObject(error);
+      const { error: clientError, statusText } = await getClientErrorObject(
+        error as Response,
+      );
 
-      this.props.addDangerToast(
+      addDangerToast(
         clientError ||
           statusText ||
           t('An error occurred while formatting SQL'),
       );
     } finally {
-      this.abortControllers.formatSql = null;
+      abortControllers.current.formatQuery = null;
     }
-  }
-
-  /**
-   * Syncs dataset columns with the database schema.
-   * Fetches column metadata from the underlying table/view and updates the 
dataset.
-   * Aborts any pending sync requests before starting a new one.
-   */
-  async syncMetadata() {
-    const { datasource } = this.state;
-
+  }, [
+    datasource.sql,
+    isEditMode,
+    formatQueryAction,
+    onDatasourcePropChange,
+    addSuccessToast,
+    addDangerToast,
+  ]);
+
+  const syncMetadata = useCallback(async () => {
     // Abort previous syncMetadata if still pending
-    if (this.abortControllers.syncMetadata) {
-      this.abortControllers.syncMetadata.abort();
+    if (abortControllers.current.syncMetadata) {
+      abortControllers.current.syncMetadata.abort();
     }
 
-    this.abortControllers.syncMetadata = new AbortController();
-    const { signal } = this.abortControllers.syncMetadata;
+    abortControllers.current.syncMetadata = new AbortController();
+    const { signal } = abortControllers.current.syncMetadata;
 
-    this.setState({ metadataLoading: true });
+    setMetadataLoading(true);
 
     try {
       const newCols = await fetchSyncedColumns(datasource, signal);
 
       const columnChanges = updateColumns(
         datasource.columns,
         newCols,
-        this.props.addSuccessToast,
+        addSuccessToast,
       );
-      this.setColumns({
+      setColumns({
         databaseColumns: columnChanges.finalColumns.filter(
-          col => !col.expression, // remove calculated columns
+          col => !col.expression,
         ) as Column[],
       });
 
       if (datasource.id !== undefined) {
         clearDatasetCache(datasource.id);
       }
 
-      this.props.addSuccessToast(t('Metadata has been synced'));
-      this.setState({ metadataLoading: false });
-    } catch (error) {
-      if (error.name === 'AbortError') {
-        // Only update state if still mounted (abort may happen during unmount)
-        if (this.isComponentMounted) {
-          this.setState({ metadataLoading: false });
+      addSuccessToast(t('Metadata has been synced'));
+      setMetadataLoading(false);
+    } catch (error: unknown) {
+      if ((error as Error).name === 'AbortError') {
+        if (isComponentMounted.current) {
+          setMetadataLoading(false);
         }
         return;
       }
 
-      const { error: clientError, statusText } =
-        await getClientErrorObject(error);
-
-      this.props.addDangerToast(
-        clientError || statusText || t('An error has occurred'),
+      const { error: clientError, statusText } = await getClientErrorObject(
+        error as Response,
       );
-      this.setState({ metadataLoading: false });
-    } finally {
-      this.abortControllers.syncMetadata = null;
-    }
-  }
 
-  /**
-   * Fetches chart usage data for this dataset (which charts use this dataset).
-   * Aborts any pending fetch requests before starting a new one.
-   *
-   * @param {number} page - Page number (1-indexed)
-   * @param {number} pageSize - Number of results per page
-   * @param {string} sortColumn - Column to sort by
-   * @param {string} sortDirection - Sort direction ('asc' or 'desc')
-   * @returns {Promise<{charts: Array, count: number, ids: Array}>} Chart 
usage data
-   */
-  async fetchUsageData(
-    page = 1,
-    pageSize = 25,
-    sortColumn = 'changed_on_delta_humanized',
-    sortDirection = 'desc',
-  ) {
-    const { datasource } = this.state;
-
-    // Abort previous fetchUsageData if still pending
-    if (this.abortControllers.fetchUsageData) {
-      this.abortControllers.fetchUsageData.abort();
+      addDangerToast(clientError || statusText || t('An error has occurred'));
+      setMetadataLoading(false);
+    } finally {
+      abortControllers.current.syncMetadata = null;
     }
+  }, [datasource, addSuccessToast, addDangerToast, setColumns]);
+
+  const fetchUsageData = useCallback(
+    async (
+      page = 1,
+      pageSize = 25,
+      sortColumn = 'changed_on_delta_humanized',
+      sortDirection = 'desc',
+    ) => {
+      // Abort previous fetchUsageData if still pending
+      if (abortControllers.current.fetchUsageData) {
+        abortControllers.current.fetchUsageData.abort();
+      }
 
-    this.abortControllers.fetchUsageData = new AbortController();
-    const { signal } = this.abortControllers.fetchUsageData;
+      abortControllers.current.fetchUsageData = new AbortController();
+      const { signal } = abortControllers.current.fetchUsageData;
+
+      try {
+        const queryParams = rison.encode({
+          columns: [
+            'slice_name',
+            'url',
+            'certified_by',
+            'certification_details',
+            'description',
+            'owners.first_name',
+            'owners.last_name',
+            'owners.id',
+            'changed_on_delta_humanized',
+            'changed_on',
+            'changed_by.first_name',
+            'changed_by.last_name',
+            'changed_by.id',
+            'dashboards.id',
+            'dashboards.dashboard_title',
+            'dashboards.url',
+          ],
+          filters: [
+            {
+              col: 'datasource_id',
+              opr: 'eq',
+              value: datasource.id,
+            },
+          ],
+          order_column: sortColumn,
+          order_direction: sortDirection,
+          page: page - 1,
+          page_size: pageSize,
+        });
 
-    try {
-      const queryParams = rison.encode({
-        columns: [
-          'slice_name',
-          'url',
-          'certified_by',
-          'certification_details',
-          'description',
-          'owners.first_name',
-          'owners.last_name',
-          'owners.id',
-          'changed_on_delta_humanized',
-          'changed_on',
-          'changed_by.first_name',
-          'changed_by.last_name',
-          'changed_by.id',
-          'dashboards.id',
-          'dashboards.dashboard_title',
-          'dashboards.url',
-        ],
-        filters: [
-          {
-            col: 'datasource_id',
-            opr: 'eq',
-            value: datasource.id,
-          },
-        ],
-        order_column: sortColumn,
-        order_direction: sortDirection,
-        page: page - 1,
-        page_size: pageSize,
-      });
+        const { json = {} } = await SupersetClient.get({
+          endpoint: `/api/v1/chart/?q=${queryParams}`,
+          signal,
+        });
 
-      const { json = {} } = await SupersetClient.get({
-        endpoint: `/api/v1/chart/?q=${queryParams}`,
-        signal,
-      });
+        const charts = json?.result || [];
+        const ids = json?.ids || [];
 
-      const charts = json?.result || [];
-      const ids = json?.ids || [];
+        const chartsWithIds = charts.map(
+          (chart: Omit<ChartUsageData, 'id'>, index: number) => ({
+            ...chart,
+            id: ids[index],
+          }),
+        );
 
-      // Map chart IDs to chart objects
-      const chartsWithIds = charts.map(
-        (chart: Omit<ChartUsageData, 'id'>, index: number) => ({
-          ...chart,
-          id: ids[index],
-        }),
-      );
+        if (!signal.aborted && isComponentMounted.current) {
+          setUsageCharts(chartsWithIds);
+          setUsageChartsCount(json?.count || 0);
+        }
 
-      // Only update state if not aborted and component still mounted
-      if (!signal.aborted && this.isComponentMounted) {
-        this.setState({
-          usageCharts: chartsWithIds,
-          usageChartsCount: json?.count || 0,
-        });
-      }
+        return {
+          charts: chartsWithIds,
+          count: json?.count || 0,
+          ids,
+        };
+      } catch (error: unknown) {
+        if ((error as Error).name === 'AbortError') throw error;
 
-      return {
-        charts: chartsWithIds,
-        count: json?.count || 0,
-        ids,
-      };
-    } catch (error) {
-      // Rethrow AbortError so callers can handle gracefully
-      if (error.name === 'AbortError') throw error;
+        const { error: clientError, statusText } = await getClientErrorObject(
+          error as Response,
+        );
 
-      const { error: clientError, statusText } =
-        await getClientErrorObject(error);
+        addDangerToast(
+          clientError ||
+            statusText ||
+            t('An error occurred while fetching usage data'),
+        );
+        setUsageCharts([]);
+        setUsageChartsCount(0);
+
+        return {
+          charts: [],
+          count: 0,
+          ids: [],
+        };
+      } finally {
+        abortControllers.current.fetchUsageData = null;
+      }
+    },
+    [datasource.id, addDangerToast],
+  );
 
-      this.props.addDangerToast(
-        clientError ||
-          statusText ||
-          t('An error occurred while fetching usage data'),
-      );
-      this.setState({
-        usageCharts: [],
-        usageChartsCount: 0,
-      });
+  const handleTabSelect = useCallback((key: string) => {
+    setActiveTabKey(key);
+  }, []);
 
-      return {
-        charts: [],
-        count: 0,
-        ids: [],
-      };
-    } finally {
-      this.abortControllers.fetchUsageData = null;
-    }
-  }
+  const sortMetrics = useCallback(
+    (metrics: Metric[]) =>
+      [...metrics].sort(
+        ({ id: a }: { id?: number }, { id: b }: { id?: number }) =>
+          (b ?? 0) - (a ?? 0),
+      ),
+    [],
+  );
 
-  findDuplicates<T>(arr: T[], accessor: (obj: T) => string): string[] {
-    const seen: Record<string, null> = {};
-    const dups: string[] = [];
-    arr.forEach((obj: T) => {
-      const item = accessor(obj);
-      if (item in seen) {
-        dups.push(item);
-      } else {
-        seen[item] = null;
+  // Keep the refs read by the (one-shot) Mousetrap handler up to date.
+  const isEditModeRef = useRef(isEditMode);
+  const onQueryFormatRef = useRef(onQueryFormat);
+  useEffect(() => {
+    isEditModeRef.current = isEditMode;
+    onQueryFormatRef.current = onQueryFormat;
+  }, [isEditMode, onQueryFormat]);
+
+  // componentDidMount
+  useEffect(() => {
+    isComponentMounted.current = true;
+    // Mark initial mount as complete after first render cycle
+    // This prevents useEffect hooks from firing on mount
+    isInitialMount.current = false;
+    // Bind ctrl+shift+f once on mount and route through refs so we don't
+    // unbind/rebind on every SQL-editor keystroke (onQueryFormat's identity
+    // changes with datasource.sql).
+    Mousetrap.bind('ctrl+shift+f', e => {
+      e.preventDefault();
+      if (isEditModeRef.current) {
+        onQueryFormatRef.current?.();
       }
+      return false;
+    });
+    fetchUsageData().catch(error => {
+      if (error?.name !== 'AbortError') throw error;
     });
-    return dups;
-  }
 
-  validate(callback: () => void) {
-    let errors: string[] = [];
-    let dups: string[];
-    const { datasource } = this.state;
+    // componentWillUnmount
+    return () => {
+      isComponentMounted.current = false;
 
-    // Looking for duplicate column_name
-    dups = this.findDuplicates(datasource.columns, obj => obj.column_name);
-    errors = errors.concat(
-      dups.map(name => t('Column name [%s] is duplicated', name)),
-    );
+      // Abort all pending requests
+      Object.values(abortControllers.current).forEach(controller => {
+        if (controller) controller.abort();
+      });
 
-    // Looking for duplicate metric_name
-    dups = this.findDuplicates(
-      datasource.metrics ?? [],
-      obj => obj.metric_name,
-    );
-    errors = errors.concat(
-      dups.map(name => t('Metric name [%s] is duplicated', name)),
-    );
+      Mousetrap.unbind('ctrl+shift+f');
+      resetQuery();
+    };
+  }, []);
 
-    // Making sure calculatedColumns have an expression defined
-    const noFilterCalcCols = this.state.calculatedColumns.filter(
-      col => !col.expression && !col.json,
-    );
-    errors = errors.concat(
-      noFilterCalcCols.map(col =>
-        t('Calculated column [%s] requires an expression', col.column_name),
-      ),
+  // componentDidUpdate for props.datasource changes
+  // Only run when the props.datasource reference actually changes from parent
+  useEffect(() => {
+    if (!isComponentMounted.current) return;
+    if (prevPropsDatasourceRef.current === propsDatasource) return;
+    prevPropsDatasourceRef.current = propsDatasource;
+
+    const newCalculatedColumns = propsDatasource.columns.filter(
+      col => !!col.expression,
     );
 
-    // validate currency code (skip 'AUTO' - it's a placeholder for 
auto-detection)
-    try {
-      this.state.datasource.metrics?.forEach(
-        metric =>
-          metric.currency?.symbol &&
-          metric.currency.symbol !== 'AUTO' &&
-          new Intl.NumberFormat('en-US', {
-            style: 'currency',
-            currency: metric.currency.symbol,
-          }),
-      );
-    } catch {
-      errors = errors.concat([t('Invalid currency code in saved metrics')]);
-    }
+    if (newCalculatedColumns.length === calculatedColumns.length) {
+      const orderedCalculatedColumns: Column[] = [];
+      const usedIds = new Set<string | number>();
 
-    // Validate folders
-    if (this.state.folders?.length > 0) {
-      const folderValidation = validateFolders(this.state.folders);
-      errors = errors.concat(folderValidation.errors);
-    }
+      calculatedColumns.forEach(currentCol => {
+        const id = currentCol.id || currentCol.column_name;
+        const updatedCol = newCalculatedColumns.find(
+          newCol => (newCol.id || newCol.column_name) === id,
+        );
+        if (updatedCol) {
+          orderedCalculatedColumns.push(updatedCol);
+          usedIds.add(id);
+        }
+      });
 
-    this.setState({ errors }, callback);
-  }
+      newCalculatedColumns.forEach(newCol => {
+        const id = newCol.id || newCol.column_name;
+        if (!usedIds.has(id)) {

Review Comment:
   **Suggestion:** Prop synchronization only runs when calculated-column counts 
are equal, so parent-provided datasource updates that add/remove calculated 
columns are ignored, leaving local state inconsistent with props. Remove this 
length gate and always reconcile local column state from the incoming 
datasource. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Column tabs can show stale schema after datasource change.
   - ⚠️ Validation and sync may use incorrect column definitions.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. `DatasourceEditor` receives its datasource from `DatasourceModal`'s 
`currentDatasource`
   prop at 
`superset-frontend/src/components/Datasource/DatasourceModal/index.tsx:90-100`,
   and `DatasourceModal` itself can be re-rendered with a different 
`datasource` object
   (e.g., editing a different dataset or receiving a refreshed dataset from the 
backend).
   
   2. When `propsDatasource` changes, the `useEffect` at 
`DatasourceEditor.tsx:1409-1448`
   runs (guarded only by `prevPropsDatasourceRef.current === propsDatasource`), 
computes
   `newCalculatedColumns = propsDatasource.columns.filter(col => 
!!col.expression)` at lines
   1414-1416, and compares `newCalculatedColumns.length` with the existing
   `calculatedColumns.length` at line 1418.
   
   3. If the new datasource has a different number of calculated columns than 
the previous
   local state (e.g., backend added or removed a calculated column, or a 
different dataset
   with more or fewer calculated columns is passed in), the length check fails 
and the body
   of the `if` is skipped, so neither 
`setCalculatedColumns(orderedCalculatedColumns)` nor
   `setDatabaseColumns(propsDatasource.columns.filter(...))` at lines 1432-1435 
is executed.
   
   4. In this situation, other props (such as table name and metrics) reflect 
the new
   datasource provided by `DatasourceModal`, but the local `databaseColumns` and
   `calculatedColumns` state inside `DatasourceEditor` remain pointing at the 
old dataset's
   columns, so the Columns and Calculated Columns tabs (configured at
   `DatasourceEditor.tsx:2373-2467`) show stale schema information and all 
subsequent
   validation, sync, and save operations act on this inconsistent local column 
state.
   ```
   </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=d4e053abc7aa4ef08f49afbfb0698b2d&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=d4e053abc7aa4ef08f49afbfb0698b2d&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/DatasourceEditor/DatasourceEditor.tsx
   **Line:** 1418:1435
   **Comment:**
        *Logic Error: Prop synchronization only runs when calculated-column 
counts are equal, so parent-provided datasource updates that add/remove 
calculated columns are ignored, leaving local state inconsistent with props. 
Remove this length gate and always reconcile local column state from the 
incoming datasource.
   
   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=c48e15807e682e8f9225ad7bfeb8876760f4e62ccf0e277538aad6e9db56a2fa&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=c48e15807e682e8f9225ad7bfeb8876760f4e62ccf0e277538aad6e9db56a2fa&reaction=dislike'>👎</a>



##########
superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx:
##########
@@ -883,602 +835,693 @@ const mapStateToProps = (state: RootState) => ({
 const connector = connect(mapStateToProps, mapDispatchToProps);
 type PropsFromRedux = ConnectedProps<typeof connector>;
 
-type DatasourceEditorProps = DatasourceEditorOwnProps &
-  PropsFromRedux & {
-    theme?: SupersetTheme;
-  };
-
-class DatasourceEditor extends PureComponent<
-  DatasourceEditorProps,
-  DatasourceEditorState
-> {
-  private isComponentMounted: boolean;
+type DatasourceEditorProps = DatasourceEditorOwnProps & PropsFromRedux;
+
+function DatasourceEditor({
+  datasource: propsDatasource,
+  onChange = () => {},
+  addSuccessToast,
+  addDangerToast,
+  setIsEditing = () => {},
+  database,
+  runQuery,
+  resetQuery,
+  formatQuery: formatQueryAction,
+}: DatasourceEditorProps) {
+  const theme = useTheme();
+  const isComponentMounted = useRef(false);
+  const isInitialMount = useRef(true);
+  const prevPropsDatasourceRef = useRef(propsDatasource);
+  const isSyncingColumnsFromProps = useRef(false);
+  const abortControllers = useRef<AbortControllers>({
+    formatQuery: null,
+    formatSql: null,
+    syncMetadata: null,
+    fetchUsageData: null,
+  });
+
+  // Initialize datasource state with transformed owners and metrics
+  const [datasource, setDatasource] = useState<DatasourceObject>(() => ({
+    ...propsDatasource,
+    owners: propsDatasource.owners.map(owner => ({
+      value: owner.value || owner.id,
+      label: owner.label || `${owner.first_name} ${owner.last_name}`,
+    })),
+    metrics: propsDatasource.metrics?.map(metric => {
+      const {
+        certified_by: certifiedByMetric,
+        certification_details: certificationDetails,
+      } = metric;
+      const {
+        certification: {
+          details = undefined,
+          certified_by: certifiedBy = undefined,
+        } = {},
+        warning_markdown: warningMarkdown,
+      } = JSON.parse(metric.extra || '{}') || {};
+      return {
+        ...metric,
+        certification_details: certificationDetails || details,
+        warning_markdown: warningMarkdown || '',
+        certified_by: certifiedBy || certifiedByMetric,
+      };
+    }),
+  }));
 
-  private abortControllers: AbortControllers;
+  const [errors, setErrors] = useState<string[]>([]);
+  const [isSqla] = useState(
+    propsDatasource.datasource_type === 'table' ||
+      propsDatasource.type === 'table',
+  );
+  const [isEditMode, setIsEditMode] = useState(false);
+  const [databaseColumns, setDatabaseColumns] = useState<Column[]>(
+    propsDatasource.columns.filter(col => !col.expression),
+  );
+  const [calculatedColumns, setCalculatedColumns] = useState<Column[]>(
+    propsDatasource.columns.filter(col => !!col.expression),
+  );
+  const [folders, setFolders] = useState<DatasourceFolder[]>(
+    propsDatasource.folders || [],
+  );
+  const [folderCount, setFolderCount] = useState(() => {
+    const savedFolders = propsDatasource.folders || [];
+    const savedCount = countAllFolders(savedFolders);
+    const hasDefaultsSaved = savedFolders.some(f => isDefaultFolder(f.uuid));
+    return savedCount + (hasDefaultsSaved ? 0 : DEFAULT_FOLDERS_COUNT);
+  });
+  const [metadataLoading, setMetadataLoading] = useState(false);
+  const [activeTabKey, setActiveTabKey] = useState(TABS_KEYS.SOURCE);
+  const [datasourceType, setDatasourceType] = useState(
+    propsDatasource.sql
+      ? DATASOURCE_TYPES.virtual.key
+      : DATASOURCE_TYPES.physical.key,
+  );
+  const [usageCharts, setUsageCharts] = useState<ChartUsageData[]>([]);
+  const [usageChartsCount, setUsageChartsCount] = useState(0);
+  const [metricSearchTerm, setMetricSearchTerm] = useState('');
+  const [columnSearchTerm, setColumnSearchTerm] = useState('');
+  const [calculatedColumnSearchTerm, setCalculatedColumnSearchTerm] =
+    useState('');
+
+  const findDuplicates = useCallback(
+    <T,>(arr: T[], accessor: (obj: T) => string): string[] => {
+      const seen: Record<string, null> = {};
+      const dups: string[] = [];
+      arr.forEach((obj: T) => {
+        const item = accessor(obj);
+        if (item in seen) {
+          dups.push(item);
+        } else {
+          seen[item] = null;
+        }
+      });
+      return dups;
+    },
+    [],
+  );
 
-  static defaultProps = {
-    onChange: () => {},
-    setIsEditing: () => {},
-  };
+  const validate = useCallback(
+    (callback: (validationErrors: string[]) => void) => {
+      let validationErrors: string[] = [];
+      let dups: string[];
 
-  constructor(props: DatasourceEditorProps) {
-    super(props);
-    this.state = {
-      datasource: {
-        ...props.datasource,
-        owners: props.datasource.owners.map(owner => {
-          const ownerName =
-            owner.label || `${owner.first_name} ${owner.last_name}`;
-          return {
-            value: owner.value || owner.id,
-            label: OwnerSelectLabel({
-              name: typeof ownerName === 'string' ? ownerName : '',
-              email: owner.email,
-            }),
-            [OWNER_TEXT_LABEL_PROP]:
-              typeof ownerName === 'string' ? ownerName : '',
-            [OWNER_EMAIL_PROP]: owner.email ?? '',
-          };
-        }),
-        metrics: props.datasource.metrics?.map(metric => {
-          const {
-            certified_by: certifiedByMetric,
-            certification_details: certificationDetails,
-          } = metric;
-          const {
-            certification: {
-              details = undefined,
-              certified_by: certifiedBy = undefined,
-            } = {},
-            warning_markdown: warningMarkdown,
-          } = JSON.parse(metric.extra || '{}') || {};
-          return {
-            ...metric,
-            certification_details: certificationDetails || details,
-            warning_markdown: warningMarkdown || '',
-            certified_by: certifiedBy || certifiedByMetric,
-          };
-        }),
-      },
-      errors: [],
-      isSqla:
-        props.datasource.datasource_type === 'table' ||
-        props.datasource.type === 'table',
-      isEditMode: false,
-      databaseColumns: props.datasource.columns.filter(col => !col.expression),
-      calculatedColumns: props.datasource.columns.filter(
-        col => !!col.expression,
-      ),
-      folders: props.datasource.folders || [],
-      folderCount: (() => {
-        const savedFolders = props.datasource.folders || [];
-        const savedCount = countAllFolders(savedFolders);
-        const hasDefaultsSaved = savedFolders.some(f =>
-          isDefaultFolder(f.uuid),
-        );
-        return savedCount + (hasDefaultsSaved ? 0 : DEFAULT_FOLDERS_COUNT);
-      })(),
-      metadataLoading: false,
-      activeTabKey: TABS_KEYS.SOURCE,
-      datasourceType: props.datasource.sql
-        ? DATASOURCE_TYPES.virtual.key
-        : DATASOURCE_TYPES.physical.key,
-      usageCharts: [],
-      usageChartsCount: 0,
-      metricSearchTerm: '',
-      columnSearchTerm: '',
-      calculatedColumnSearchTerm: '',
-    };
+      // Looking for duplicate column_name
+      dups = findDuplicates(datasource.columns, obj => obj.column_name);
+      validationErrors = validationErrors.concat(
+        dups.map(name => t('Column name [%s] is duplicated', name)),
+      );
 
-    this.isComponentMounted = false;
-    this.abortControllers = {
-      formatQuery: null,
-      formatSql: null,
-      syncMetadata: null,
-      fetchUsageData: null,
-    };
+      // Looking for duplicate metric_name
+      dups = findDuplicates(datasource.metrics ?? [], obj => obj.metric_name);
+      validationErrors = validationErrors.concat(
+        dups.map(name => t('Metric name [%s] is duplicated', name)),
+      );
 
-    this.onChange = this.onChange.bind(this);
-    this.onChangeEditMode = this.onChangeEditMode.bind(this);
-    this.onDatasourcePropChange = this.onDatasourcePropChange.bind(this);
-    this.onDatasourceChange = this.onDatasourceChange.bind(this);
-    this.tableChangeAndSyncMetadata =
-      this.tableChangeAndSyncMetadata.bind(this);
-    this.syncMetadata = this.syncMetadata.bind(this);
-    this.setColumns = this.setColumns.bind(this);
-    this.validateAndChange = this.validateAndChange.bind(this);
-    this.handleTabSelect = this.handleTabSelect.bind(this);
-    this.formatSql = this.formatSql.bind(this);
-    this.fetchUsageData = this.fetchUsageData.bind(this);
-    this.handleFoldersChange = this.handleFoldersChange.bind(this);
-  }
+      // Making sure calculatedColumns have an expression defined
+      const noFilterCalcCols = calculatedColumns.filter(
+        col => !col.expression && !col.json,
+      );
+      validationErrors = validationErrors.concat(
+        noFilterCalcCols.map(col =>
+          t('Calculated column [%s] requires an expression', col.column_name),
+        ),
+      );
 
-  onChange() {
-    // Emptying SQL if "Physical" radio button is selected
-    // Currently the logic to know whether the source is
-    // physical or virtual is based on whether SQL is empty or not.
-    const { datasourceType, datasource } = this.state;
-    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);
+      // validate currency code (skip 'AUTO' - it's a placeholder for 
auto-detection)
+      try {
+        datasource.metrics?.forEach(
+          metric =>
+            metric.currency?.symbol &&
+            metric.currency.symbol !== 'AUTO' &&
+            new Intl.NumberFormat('en-US', {
+              style: 'currency',
+              currency: metric.currency.symbol,
+            }),
+        );
+      } catch {
+        validationErrors = validationErrors.concat([
+          t('Invalid currency code in saved metrics'),
+        ]);
+      }
 
-    const newDatasource = {
-      ...this.state.datasource,
-      sql,
-      columns,
-      folders,
-    };
+      // Validate folders
+      if (folders?.length > 0) {
+        const folderValidation = validateFolders(folders);
+        validationErrors = validationErrors.concat(folderValidation.errors);
+      }
 
-    this.props.onChange?.(newDatasource, this.state.errors);
-  }
+      setErrors(validationErrors);
+      callback(validationErrors);
+    },
+    [datasource, calculatedColumns, folders, findDuplicates],
+  );
 
-  onChangeEditMode() {
-    this.props.setIsEditing?.(!this.state.isEditMode);
-    this.setState(prevState => ({ isEditMode: !prevState.isEditMode }));
-  }
+  const onChangeInternal = useCallback(
+    (validationErrors: string[] = errors) => {
+      // Emptying SQL if "Physical" radio button is selected
+      const sql =
+        datasourceType === DATASOURCE_TYPES.physical.key ? '' : datasource.sql;
 
-  onDatasourceChange(
-    datasource: DatasourceObject,
-    callback: () => void = this.validateAndChange,
-  ) {
-    this.setState({ datasource }, callback);
-  }
+      const columns = [...databaseColumns, ...calculatedColumns];
 
-  onDatasourcePropChange(attr: string, value: unknown) {
-    if (value === undefined) return; // if value is undefined do not update 
state
-    const datasource = { ...this.state.datasource, [attr]: value };
-    this.setState(
-      prevState => ({
-        datasource: { ...prevState.datasource, [attr]: value },
-      }),
-      () =>
-        attr === 'table_name'
-          ? this.onDatasourceChange(datasource, 
this.tableChangeAndSyncMetadata)
-          : this.onDatasourceChange(datasource, this.validateAndChange),
-    );
-  }
+      // 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 filteredFolders = filterFoldersByValidUuids(folders, validUuids);
 
-  onDatasourceTypeChange(datasourceType: string) {
-    // Call onChange after setting datasourceType to ensure
-    // SQL is cleared when switching to a physical dataset
-    this.setState({ datasourceType }, this.onChange);
-  }
+      const newDatasource = {
+        ...datasource,
+        sql,
+        columns,
+        folders: filteredFolders,
+      };
 
-  handleFoldersChange(folders: DatasourceFolder[]) {
-    const folderCount = countAllFolders(folders);
-    this.setState({ folders, folderCount }, () => {
-      this.onDatasourceChange({
-        ...this.state.datasource,
-        folders,
-      });
-    });
-  }
+      onChange(newDatasource, validationErrors);
+    },
+    [
+      datasource,
+      datasourceType,
+      databaseColumns,
+      calculatedColumns,
+      folders,
+      errors,
+      onChange,
+    ],
+  );
 
-  setColumns(
-    obj: { databaseColumns?: Column[] } | { calculatedColumns?: Column[] },
-  ) {
-    // update calculatedColumns or databaseColumns
-    this.setState(
-      obj as Pick<
-        DatasourceEditorState,
-        'databaseColumns' | 'calculatedColumns'
-      >,
-      this.validateAndChange,
-    );
-  }
+  const validateAndChange = useCallback(() => {
+    validate(onChangeInternal);
+  }, [validate, onChangeInternal]);
 
-  validateAndChange() {
-    this.validate(this.onChange);
-  }
+  const onDatasourceChange = useCallback((newDatasource: DatasourceObject) => {
+    setDatasource(newDatasource);
+  }, []);
 
-  async onQueryRun() {
-    const databaseId = this.state.datasource.database?.id;
-    const { sql } = this.state.datasource;
-    if (!databaseId || !sql) {
-      return;
-    }
-    this.props.runQuery({
-      client_id: this.props.database?.clientId,
-      database_id: databaseId,
-      runAsync: false,
-      catalog: this.state.datasource.catalog,
-      schema: this.state.datasource.schema,
-      sql,
-      tmp_table_name: '',
-      select_as_cta: false,
-      ctas_method: 'TABLE',
-      queryLimit: 25,
-      expand_data: true,
+  const onDatasourcePropChange = useCallback((attr: string, value: unknown) => 
{
+    if (value === undefined) return;
+    setDatasource(prev => {
+      const newDatasource = { ...prev, [attr]: value };
+      return newDatasource;
     });
-  }
+  }, []);
 
-  /**
-   * Formats SQL query using the formatQuery action.
-   * Aborts any pending format requests before starting a new one.
-   */
-  async onQueryFormat() {
-    const { datasource } = this.state;
-    if (!datasource.sql || !this.state.isEditMode) {
+  // Effect to trigger validation after datasource changes (skip initial mount)
+  useEffect(() => {
+    if (isInitialMount.current) {
       return;
     }
-
-    // Abort previous formatQuery if still pending
-    if (this.abortControllers.formatQuery) {
-      this.abortControllers.formatQuery.abort();
+    if (isComponentMounted.current) {
+      validateAndChange();
     }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [datasource]);
 
-    this.abortControllers.formatQuery = new AbortController();
-    const { signal } = this.abortControllers.formatQuery;
+  const onChangeEditMode = useCallback(() => {
+    setIsEditing(!isEditMode);
+    setIsEditMode(prev => !prev);
+  }, [isEditMode, setIsEditing]);
 
-    try {
-      const response = await this.props.formatQuery(datasource.sql, { signal 
});
+  const onDatasourceTypeChange = useCallback((newDatasourceType: string) => {
+    setDatasourceType(newDatasourceType);
+  }, []);
 
-      this.onDatasourcePropChange('sql', response.json.result);
-      this.props.addSuccessToast(t('SQL was formatted'));
-    } catch (error) {
-      if (error.name === 'AbortError') return;
+  // Effect to call onChange after datasourceType changes (skip initial mount)
+  useEffect(() => {
+    if (isInitialMount.current) {
+      return;
+    }
+    if (isComponentMounted.current) {
+      onChangeInternal();
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [datasourceType]);
+
+  const handleFoldersChange = useCallback((newFolders: DatasourceFolder[]) => {
+    const userMadeFolders = newFolders.filter(
+      f =>
+        f.uuid !== DEFAULT_METRICS_FOLDER_UUID &&
+        f.uuid !== DEFAULT_COLUMNS_FOLDER_UUID &&
+        (f.children?.length ?? 0) > 0,
+    );
+    setFolders(userMadeFolders);
+    setFolderCount(countAllFolders(userMadeFolders));
+    setDatasource(prev => ({ ...prev, folders: userMadeFolders }));
+  }, []);
 
-      const { error: clientError, statusText } =
-        await getClientErrorObject(error);
+  const setColumns = useCallback(
+    (
+      obj: { databaseColumns?: Column[] } | { calculatedColumns?: Column[] },
+    ) => {
+      if ('databaseColumns' in obj && obj.databaseColumns) {
+        setDatabaseColumns(obj.databaseColumns);
+      }
+      if ('calculatedColumns' in obj && obj.calculatedColumns) {
+        setCalculatedColumns(obj.calculatedColumns);
+      }
+    },
+    [],
+  );
 
-      this.props.addDangerToast(
-        clientError ||
-          statusText ||
-          t('An error occurred while formatting SQL'),
-      );
-    } finally {
-      this.abortControllers.formatQuery = null;
+  // Effect to trigger validation after user-initiated column changes
+  // Skips initial mount and prop-sync updates (which don't need validation)
+  useEffect(() => {
+    if (isInitialMount.current || isSyncingColumnsFromProps.current) {
+      isSyncingColumnsFromProps.current = false;
+      return;
     }
-  }
+    if (isComponentMounted.current) {
+      validateAndChange();
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [databaseColumns, calculatedColumns]);
 
-  getSQLLabUrl() {
+  const getSQLLabUrl = useCallback(() => {
     const queryParams = new URLSearchParams({
-      dbid: String(this.state.datasource.database?.id ?? ''),
-      sql: this.state.datasource.sql ?? '',
-      name: this.state.datasource.datasource_name ?? '',
-      schema: this.state.datasource.schema ?? '',
+      dbid: String(datasource.database?.id ?? ''),
+      sql: datasource.sql ?? '',
+      name: datasource.datasource_name ?? '',
+      schema: datasource.schema ?? '',
       autorun: 'true',
       isDataset: 'true',
     });
     return makeUrl(`/sqllab/?${queryParams.toString()}`);
-  }
+  }, [datasource]);
 
-  openOnSqlLab() {
-    window.open(this.getSQLLabUrl(), '_blank', 'noopener,noreferrer');
-  }
+  const openOnSqlLab = useCallback(() => {
+    window.open(getSQLLabUrl(), '_blank', 'noopener,noreferrer');
+  }, [getSQLLabUrl]);
 
-  tableChangeAndSyncMetadata() {
-    this.validate(() => {
-      this.syncMetadata();
-      this.onChange();
+  const onQueryRun = useCallback(async () => {
+    const databaseId = datasource.database?.id;
+    const { sql } = datasource;
+    if (!databaseId || !sql) {
+      return;
+    }
+    runQuery({
+      client_id: database?.clientId,
+      database_id: databaseId,
+      runAsync: false,
+      catalog: datasource.catalog,
+      schema: datasource.schema,
+      sql,
+      tmp_table_name: '',
+      select_as_cta: false,
+      ctas_method: 'TABLE',
+      queryLimit: 25,
+      expand_data: true,
     });
-  }
+  }, [datasource, database?.clientId, runQuery]);
 
-  /**
-   * Formats SQL query using the SQL format API endpoint.
-   * Aborts any pending format requests before starting a new one.
-   */
-  async formatSql() {
-    const { datasource } = this.state;
-    if (!datasource.sql) {
+  const onQueryFormat = useCallback(async () => {
+    if (!datasource.sql || !isEditMode) {
       return;
     }
 
-    // Abort previous formatSql if still pending
-    if (this.abortControllers.formatSql) {
-      this.abortControllers.formatSql.abort();
+    // Abort previous formatQuery if still pending
+    if (abortControllers.current.formatQuery) {
+      abortControllers.current.formatQuery.abort();
     }
 
-    this.abortControllers.formatSql = new AbortController();
-    const { signal } = this.abortControllers.formatSql;
+    abortControllers.current.formatQuery = new AbortController();
+    const { signal } = abortControllers.current.formatQuery;
 
     try {
-      const response = await SupersetClient.post({
-        endpoint: '/api/v1/sql/format',
-        body: JSON.stringify({ sql: datasource.sql }),
-        headers: { 'Content-Type': 'application/json' },
-        signal,
-      });
+      const response = await formatQueryAction(datasource.sql, { signal });
 
-      this.onDatasourcePropChange('sql', response.json.result);
-      this.props.addSuccessToast(t('SQL was formatted'));
-    } catch (error) {
-      if (error.name === 'AbortError') return;
+      onDatasourcePropChange('sql', response.json.result);
+      addSuccessToast(t('SQL was formatted'));
+    } catch (error: unknown) {
+      if ((error as Error).name === 'AbortError') return;
 
-      const { error: clientError, statusText } =
-        await getClientErrorObject(error);
+      const { error: clientError, statusText } = await getClientErrorObject(
+        error as Response,
+      );
 
-      this.props.addDangerToast(
+      addDangerToast(
         clientError ||
           statusText ||
           t('An error occurred while formatting SQL'),
       );
     } finally {
-      this.abortControllers.formatSql = null;
+      abortControllers.current.formatQuery = null;
     }
-  }
-
-  /**
-   * Syncs dataset columns with the database schema.
-   * Fetches column metadata from the underlying table/view and updates the 
dataset.
-   * Aborts any pending sync requests before starting a new one.
-   */
-  async syncMetadata() {
-    const { datasource } = this.state;
-
+  }, [
+    datasource.sql,
+    isEditMode,
+    formatQueryAction,
+    onDatasourcePropChange,
+    addSuccessToast,
+    addDangerToast,
+  ]);
+
+  const syncMetadata = useCallback(async () => {
     // Abort previous syncMetadata if still pending
-    if (this.abortControllers.syncMetadata) {
-      this.abortControllers.syncMetadata.abort();
+    if (abortControllers.current.syncMetadata) {
+      abortControllers.current.syncMetadata.abort();
     }
 
-    this.abortControllers.syncMetadata = new AbortController();
-    const { signal } = this.abortControllers.syncMetadata;
+    abortControllers.current.syncMetadata = new AbortController();
+    const { signal } = abortControllers.current.syncMetadata;
 
-    this.setState({ metadataLoading: true });
+    setMetadataLoading(true);
 
     try {
       const newCols = await fetchSyncedColumns(datasource, signal);
 
       const columnChanges = updateColumns(
         datasource.columns,
         newCols,
-        this.props.addSuccessToast,
+        addSuccessToast,
       );

Review Comment:
   **Suggestion:** Metadata sync diffs against `datasource.columns`, which can 
be stale after in-session column edits, so sync can compute incorrect changes 
and overwrite recent edits. Use the live merged columns (`databaseColumns` + 
`calculatedColumns`) as the source of truth for `updateColumns`. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ⚠️ Syncing metadata can silently discard column edits.
   - ⚠️ Dataset column settings revert unexpectedly after sync.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open a dataset in the editor through `DatasourceModal`, which passes
   `currentDatasource` into `<DatasourceEditor>` at
   
`superset-frontend/src/components/Datasource/DatasourceModal/index.tsx:90-100` 
and wires
   `onDatasourceChange` at lines 240-249.
   
   2. In the Columns tab, modify a column's metadata (e.g., label or type) 
using the
   `ColumnCollectionTable` at `DatasourceEditor.tsx:2408-2416`; these edits 
update the
   `databaseColumns` state via `setColumns({ databaseColumns: cols })` but do 
not immediately
   update `datasource.columns`.
   
   3. Click the "Sync columns from source" button defined in the Columns tab at
   `DatasourceEditor.tsx:2386-2392`; this calls `syncMetadata` at
   `DatasourceEditor.tsx:1202-1250`, which fetches fresh column metadata with
   `fetchSyncedColumns(datasource, signal)` at line 1214.
   
   4. `syncMetadata` then computes diffs via `const columnChanges =
   updateColumns(datasource.columns, newCols, addSuccessToast);` at lines 
1216-1220, passing
   the stale `datasource.columns` array into `updateColumns` (implemented in
   `superset-frontend/src/components/Datasource/utils/index.ts:122-183`), and 
uses
   `setColumns({ databaseColumns: columnChanges.finalColumns.filter(...) })` at 
lines
   1221-1225, which overwrites `databaseColumns` with a version that omits the 
user's
   in-session edits from step 2, causing silently lost column changes after a 
sync.
   ```
   </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=f1f20bfa3b934395ab32e1331c25211b&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=f1f20bfa3b934395ab32e1331c25211b&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/DatasourceEditor/DatasourceEditor.tsx
   **Line:** 1216:1220
   **Comment:**
        *Logic Error: Metadata sync diffs against `datasource.columns`, which 
can be stale after in-session column edits, so sync can compute incorrect 
changes and overwrite recent edits. Use the live merged columns 
(`databaseColumns` + `calculatedColumns`) as the source of truth for 
`updateColumns`.
   
   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=f0f609b76e8bb868c28db7284eb6d8865603ac3c8e4e2eeabd54dd4b11f9d459&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=f0f609b76e8bb868c28db7284eb6d8865603ac3c8e4e2eeabd54dd4b11f9d459&reaction=dislike'>👎</a>



##########
superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx:
##########
@@ -2345,299 +2300,289 @@ class DatasourceEditor extends PureComponent<
         />
       </div>
     );
-  }
+  }, [datasource, sortMetrics, onDatasourcePropChange, metricSearchTerm]);
 
-  render() {
-    const { datasource, activeTabKey } = this.state;
-    const { metrics } = datasource;
-    const sortedMetrics = metrics?.length ? this.sortMetrics(metrics) : [];
+  const sortedMetrics = useMemo(
+    () => (datasource.metrics?.length ? sortMetrics(datasource.metrics) : []),
+    [datasource.metrics, sortMetrics],
+  );
 
-    return (
-      <DatasourceContainer data-test="datasource-editor">
-        {this.renderErrors()}
-        <Alert
-          css={theme => ({ marginBottom: theme.sizeUnit * 4 })}
-          type="warning"
-          message={
-            <>
-              {' '}
-              <strong>{t('Be careful.')} </strong>
-              {t(
-                'Changing these settings will affect all charts using this 
dataset, including charts owned by other people.',
-              )}
-            </>
-          }
+  // Retained to mirror the canonical (class-based) component on master: the
+  // Spatial tab definition is kept available even though it is not wired into
+  // the rendered tab list. Removing it would also drop its translatable 
strings
+  // and regress existing translations. It is referenced in the `tabItems`
+  // dependency list below so it is not reported as an unused local.
+  const renderSpatialTab = useCallback(() => {
+    const { spatials, all_cols: allCols } = datasource;
+
+    return {
+      key: TABS_KEYS.SPATIAL,
+      label: <CollectionTabTitle collection={spatials} title={t('Spatial')} />,
+      children: (
+        <CollectionTable
+          tableColumns={['name', 'config']}
+          sortColumns={['name']}
+          onChange={value => onDatasourcePropChange('spatials', value)}
+          itemGenerator={() => ({
+            name: t('<new spatial>'),
+            type: t('<no type>'),
+            config: null,
+          })}
+          collection={spatials ?? []}
+          allowDeletes
+          itemRenderers={{
+            name: (d, onChange) => (
+              <EditableTitle
+                canEdit
+                title={d as string}
+                onSaveTitle={onChange}
+              />
+            ),
+            config: (v, onChange) => (
+              <SpatialControl
+                value={
+                  v as {
+                    type: 'latlong' | 'delimited' | 'geohash';
+                  }
+                }
+                onChange={onChange}
+                choices={allCols?.map(col => [col, col] as [string, string])}
+              />
+            ),
+          }}
         />
-        <StyledTableTabs
-          id="table-tabs"
-          data-test="edit-dataset-tabs"
-          onChange={this.handleTabSelect}
-          defaultActiveKey={activeTabKey}
-          items={[
-            {
-              key: TABS_KEYS.SOURCE,
-              label: t('Source'),
-              children: this.renderSourceFieldset(),
-            },
-            {
-              key: TABS_KEYS.METRICS,
-              label: (
-                <CollectionTabTitle
-                  collection={sortedMetrics}
-                  title={t('Metrics')}
-                />
-              ),
-              children: this.renderMetricCollection(),
-            },
-            {
-              key: TABS_KEYS.COLUMNS,
-              label: (
-                <CollectionTabTitle
-                  collection={this.state.databaseColumns}
-                  title={t('Columns')}
-                />
-              ),
-              children: (
-                <StyledTableTabWrapper>
-                  {this.renderDefaultColumnSettings()}
-                  <ColumnButtonWrapper>
-                    <StyledButtonWrapper>
-                      <Button
-                        buttonSize="small"
-                        buttonStyle="tertiary"
-                        onClick={this.syncMetadata}
-                        className="sync-from-source"
-                        disabled={this.state.isEditMode}
-                      >
-                        <Icons.DatabaseOutlined iconSize="m" />
-                        {t('Sync columns from source')}
-                      </Button>
-                    </StyledButtonWrapper>
-                  </ColumnButtonWrapper>
-                  <Input.Search
-                    placeholder={t('Search columns by name')}
-                    value={this.state.columnSearchTerm}
-                    onChange={e =>
-                      this.setState({ columnSearchTerm: e.target.value })
-                    }
-                    style={{ marginBottom: 16, width: 300 }}
-                    allowClear
-                  />
-                  <ColumnCollectionTable
-                    className="columns-table"
-                    columns={this.state.databaseColumns}
-                    filterTerm={this.state.columnSearchTerm}
-                    filterFields={['column_name']}
-                    datasource={datasource}
-                    onColumnsChange={databaseColumns =>
-                      this.setColumns({ databaseColumns })
-                    }
-                    onDatasourceChange={this.onDatasourceChange}
-                  />
-                  {this.state.metadataLoading && <Loading />}
-                </StyledTableTabWrapper>
-              ),
-            },
+      ),
+    };
+  }, [datasource, onDatasourcePropChange]);
+
+  const tabItems = useMemo(
+    () => [
+      {
+        key: TABS_KEYS.SOURCE,
+        label: t('Source'),
+        children: renderSourceFieldset(),
+      },
+      {
+        key: TABS_KEYS.METRICS,
+        label: (
+          <CollectionTabTitle collection={sortedMetrics} title={t('Metrics')} 
/>
+        ),
+        children: renderMetricCollection(),
+      },
+      {
+        key: TABS_KEYS.COLUMNS,
+        label: (
+          <CollectionTabTitle
+            collection={databaseColumns}
+            title={t('Columns')}
+          />
+        ),
+        children: (
+          <StyledTableTabWrapper>
+            {renderDefaultColumnSettings()}
+            <DefaultColumnSettingsTitle>
+              {t('Column Settings')}
+            </DefaultColumnSettingsTitle>
+            <ColumnButtonWrapper>
+              <StyledButtonWrapper>
+                <Button
+                  buttonSize="small"
+                  buttonStyle="tertiary"
+                  onClick={syncMetadata}
+                  className="sync-from-source"
+                  disabled={isEditMode}

Review Comment:
   **Suggestion:** The sync button disable condition is inverted: it is 
disabled while edit mode is enabled, which blocks metadata sync exactly when 
users are allowed to edit. Flip this condition so syncing is disabled only when 
the editor is locked. [incorrect condition logic]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Column metadata sync blocked while editor is unlocked.
   - ⚠️ Users must relock editor to sync columns.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Open the dataset edit modal, which renders `DatasourceModal` at
   
`superset-frontend/src/components/Datasource/DatasourceModal/index.tsx:90-100` 
and mounts
   `<DatasourceEditor datasource={currentDatasource} ... />` at lines 90-99 and 
323-332.
   
   2. In the DatasourceEditor, unlock editing by clicking the lock icon, which 
toggles
   `isEditMode` via `onChangeEditMode` at `DatasourceEditor.tsx:1761-1767` 
(sets local
   `isEditMode` true and calls `setIsEditing(true)` in the parent).
   
   3. Navigate to the "Columns" tab; the "Sync columns from source" button is 
rendered in the
   Columns tab definition at `DatasourceEditor.tsx:2386-2392`, with 
`disabled={isEditMode}`
   at line 2393.
   
   4. With the editor unlocked (`isEditMode === true`), observe that the sync 
button is
   disabled and cannot be clicked, even though the `syncMetadata` handler at
   `DatasourceEditor.tsx:1202-1250` is only reachable when the button is 
enabled; re-locking
   the editor (setting `isEditMode` back to false) re-enables sync, 
demonstrating the
   inverted condition.
   ```
   </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=1e89e284d3b149dda82bacd29d3f7ddb&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=1e89e284d3b149dda82bacd29d3f7ddb&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/DatasourceEditor/DatasourceEditor.tsx
   **Line:** 2393:2393
   **Comment:**
        *Incorrect Condition Logic: The sync button disable condition is 
inverted: it is disabled while edit mode is enabled, which blocks metadata sync 
exactly when users are allowed to edit. Flip this condition so syncing is 
disabled only when the editor is locked.
   
   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=1b89457bf2ce48a2338268092f2a911d9292396e5c5d98ee25ab2e70d584b235&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F39461&comment_hash=1b89457bf2ce48a2338268092f2a911d9292396e5c5d98ee25ab2e70d584b235&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