chore(lint): convert class components to function components

Convert all remaining React class components to function components
using hooks (useState, useCallback, useEffect, useRef, useMemo) to
satisfy the react-prefer-function-component ESLint rule.

Key changes:
- Converted components in dashboard, explore, SqlLab, and Chart areas
- Updated associated test files with proper typing
- Fixed JSX.Element return types for components used as JSX
- Added explicit ControlHeader props where needed
- Fixed shouldFocus callback signature in WithPopoverMenu usage

Notable exceptions (not converted):
- ErrorBoundary (uses componentDidCatch)
- DragDroppable (react-dnd requires class instances)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-02-11 09:15:51 -08:00
parent 63f1d9eb98
commit 5dffbc26ed
76 changed files with 11874 additions and 11869 deletions

View File

@@ -16,7 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PureComponent, type ReactNode } from 'react';
import {
useState,
useCallback,
useEffect,
useMemo,
useRef,
type ReactNode,
} from 'react';
import { isEqualArray } from '@superset-ui/core';
import { t } from '@apache-superset/core';
import { css } from '@apache-superset/core/ui';
@@ -71,26 +78,6 @@ export interface SelectControlProps {
sortComparator?: (a: SelectOption, b: SelectOption) => number;
}
const defaultProps = {
autoFocus: false,
choices: [],
clearable: true,
description: null,
disabled: false,
freeForm: false,
isLoading: false,
label: null,
multi: false,
onChange: () => {},
onFocus: () => {},
showHeader: true,
valueKey: 'value',
};
interface SelectControlState {
options: SelectOption[];
}
const numberComparator = (a: SelectOption, b: SelectOption): number =>
(a.value as number) - (b.value as number);
@@ -139,9 +126,9 @@ export const getSortComparator = (
export const innerGetOptions = (props: SelectControlProps): SelectOption[] => {
const { choices, optionRenderer, valueKey = 'value' } = props;
let options: SelectOption[] = [];
let selectOptions: SelectOption[] = [];
if (props.options) {
options = props.options.map(o => ({
selectOptions = props.options.map(o => ({
...o,
value: o[valueKey] as string | number,
label: optionRenderer
@@ -150,7 +137,7 @@ export const innerGetOptions = (props: SelectControlProps): SelectOption[] => {
}));
} else if (choices) {
// Accepts different formats of input
options = choices.map(c => {
selectOptions = choices.map(c => {
if (Array.isArray(c)) {
const [value, label] = c.length > 1 ? c : [c[0], c[0]];
return {
@@ -162,136 +149,165 @@ export const innerGetOptions = (props: SelectControlProps): SelectOption[] => {
return { value: c as unknown as string | number, label: String(c) };
});
}
return options;
return selectOptions;
};
export default class SelectControl extends PureComponent<
SelectControlProps,
SelectControlState
> {
static defaultProps = defaultProps;
function SelectControl({
ariaLabel,
autoFocus = false,
choices = [],
clearable = true,
description = null,
disabled = false,
freeForm = false,
isLoading = false,
mode,
multi = false,
isMulti,
name,
onChange = () => {},
onFocus = () => {},
onSelect,
onDeselect,
value,
default: defaultValue,
showHeader = true,
optionRenderer,
valueKey = 'value',
options: optionsProp,
placeholder,
filterOption,
tokenSeparators,
notFoundContent,
label = undefined,
renderTrigger,
validationErrors,
rightNode,
leftNode,
onClick,
hovered,
tooltipOnClick,
warning,
danger,
sortComparator,
}: SelectControlProps) {
const [options, setOptions] = useState<SelectOption[]>(() =>
innerGetOptions({
choices,
optionRenderer,
valueKey,
options: optionsProp,
name,
}),
);
constructor(props: SelectControlProps) {
super(props);
this.state = {
options: this.getOptions(props),
};
this.onChange = this.onChange.bind(this);
this.handleFilterOptions = this.handleFilterOptions.bind(this);
}
// Track previous choices/options for comparison
const prevChoicesRef = useRef(choices);
const prevOptionsRef = useRef(optionsProp);
componentDidUpdate(prevProps: SelectControlProps) {
useEffect(() => {
if (
!isEqualArray(this.props.choices, prevProps.choices) ||
!isEqualArray(this.props.options, prevProps.options)
!isEqualArray(choices, prevChoicesRef.current) ||
!isEqualArray(optionsProp, prevOptionsRef.current)
) {
const options = this.getOptions(this.props);
this.setState({ options });
const newOptions = innerGetOptions({
choices,
optionRenderer,
valueKey,
options: optionsProp,
name,
});
setOptions(newOptions);
prevChoicesRef.current = choices;
prevOptionsRef.current = optionsProp;
}
}
}, [choices, optionsProp, optionRenderer, valueKey, name]);
// Beware: This is acting like an on-click instead of an on-change
// (firing every time user chooses vs firing only if a new option is chosen).
onChange(val: SelectValue | SelectOption | SelectOption[]) {
// will eventually call `exploreReducer`: SET_FIELD_VALUE
const { valueKey = 'value' } = this.props;
let onChangeVal: SelectValue = val as SelectValue;
const handleChange = useCallback(
(val: SelectValue | SelectOption | SelectOption[]) => {
// will eventually call `exploreReducer`: SET_FIELD_VALUE
let onChangeVal: SelectValue = val as SelectValue;
if (Array.isArray(val)) {
const values = val.map(v =>
typeof v === 'object' &&
v !== null &&
(v as SelectOption)[valueKey] !== undefined
? (v as SelectOption)[valueKey]
: v,
);
onChangeVal = values as (string | number)[];
}
if (
typeof val === 'object' &&
val !== null &&
!Array.isArray(val) &&
(val as SelectOption)[valueKey] !== undefined
) {
onChangeVal = (val as SelectOption)[valueKey] as string | number;
}
this.props.onChange?.(onChangeVal, []);
}
getOptions(props: SelectControlProps) {
return innerGetOptions(props);
}
handleFilterOptions(text: string, option: SelectOption) {
const { filterOption } = this.props;
return filterOption?.({ data: option }, text) ?? true;
}
render() {
const {
ariaLabel,
autoFocus,
clearable,
disabled,
filterOption,
freeForm,
isLoading,
isMulti,
label,
multi,
name,
notFoundContent,
onFocus,
onSelect,
onDeselect,
placeholder,
showHeader,
tokenSeparators,
value,
// ControlHeader props
description,
renderTrigger,
rightNode,
leftNode,
validationErrors,
onClick,
hovered,
tooltipOnClick,
warning,
danger,
} = this.props;
const headerProps = {
name,
label,
description,
renderTrigger,
rightNode,
leftNode,
validationErrors,
onClick,
hovered,
tooltipOnClick,
warning,
danger,
};
const getValue = () => {
const currentValue =
value ??
(this.props.default !== undefined ? this.props.default : undefined);
// safety check - the value is intended to be undefined but null was used
if (
currentValue === null &&
!this.state.options.some(o => o.value === null)
) {
return undefined;
if (Array.isArray(val)) {
const values = val.map(v =>
typeof v === 'object' &&
v !== null &&
(v as SelectOption)[valueKey] !== undefined
? (v as SelectOption)[valueKey]
: v,
);
onChangeVal = values as (string | number)[];
}
return currentValue;
};
if (
typeof val === 'object' &&
val !== null &&
!Array.isArray(val) &&
(val as SelectOption)[valueKey] !== undefined
) {
onChangeVal = (val as SelectOption)[valueKey] as string | number;
}
onChange?.(onChangeVal, []);
},
[onChange, valueKey],
);
const selectProps = {
const handleFilterOptions = useCallback(
(text: string, option: SelectOption) =>
filterOption?.({ data: option }, text) ?? true,
[filterOption],
);
const headerProps = useMemo(
() => ({
name,
label,
description,
renderTrigger,
rightNode,
leftNode,
validationErrors,
onClick,
hovered,
tooltipOnClick,
warning,
danger,
}),
[
name,
label,
description,
renderTrigger,
rightNode,
leftNode,
validationErrors,
onClick,
hovered,
tooltipOnClick,
warning,
danger,
],
);
const getValue = useCallback(() => {
const currentValue =
value ?? (defaultValue !== undefined ? defaultValue : undefined);
// safety check - the value is intended to be undefined but null was used
if (currentValue === null && !options.find(o => o.value === null)) {
return undefined;
}
return currentValue;
}, [value, defaultValue, options]);
const computedSortComparator = useMemo(
() => getSortComparator(choices, optionsProp, valueKey, sortComparator),
[choices, optionsProp, valueKey, sortComparator],
);
const selectProps = useMemo(
() => ({
allowNewOptions: freeForm,
autoFocus,
ariaLabel:
@@ -300,46 +316,69 @@ export default class SelectControl extends PureComponent<
disabled,
filterOption:
filterOption && typeof filterOption === 'function'
? this.handleFilterOptions
? handleFilterOptions
: true,
header: showHeader && <ControlHeader {...headerProps} />,
loading: isLoading,
mode: this.props.mode || (isMulti || multi ? 'multiple' : 'single'),
mode: mode || (isMulti || multi ? 'multiple' : 'single'),
name: `select-${name}`,
onChange: this.onChange,
onChange: handleChange,
onFocus,
onSelect,
onDeselect,
options: this.state.options,
options,
placeholder,
sortComparator: getSortComparator(
this.props.choices,
this.props.options,
this.props.valueKey,
this.props.sortComparator,
),
sortComparator: computedSortComparator,
value: getValue(),
tokenSeparators,
notFoundContent,
};
}),
[
freeForm,
autoFocus,
ariaLabel,
label,
clearable,
disabled,
filterOption,
handleFilterOptions,
showHeader,
headerProps,
isLoading,
mode,
isMulti,
multi,
name,
handleChange,
onFocus,
onSelect,
onDeselect,
options,
placeholder,
computedSortComparator,
getValue,
tokenSeparators,
notFoundContent,
],
);
return (
<div
css={theme => css`
.type-label {
margin-right: ${theme.sizeUnit * 2}px;
}
.Select__multi-value__label > span,
.Select__option > span,
.Select__single-value > span {
display: flex;
align-items: center;
}
`}
>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Select {...(selectProps as any)} />
</div>
);
}
return (
<div
css={theme => css`
.type-label {
margin-right: ${theme.sizeUnit * 2}px;
}
.Select__multi-value__label > span,
.Select__option > span,
.Select__single-value > span {
display: flex;
align-items: center;
}
`}
>
<Select {...(selectProps as Parameters<typeof Select>[0])} />
</div>
);
}
export default SelectControl;