mirror of
https://github.com/apache/superset.git
synced 2026-07-26 16:42:32 +00:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user