Compare commits

...

3 Commits

Author SHA1 Message Date
Evan
cab71798dd chore(superset-ui-core): apply prettier formatting to Select.tsx 2026-06-08 07:25:28 -07:00
Claude Code
86a9bacc06 chore(superset-ui-core): address review on TS6 forward-compat fixes
Per @bito's review on #39535:

- TimezoneSelector: replace direct `antd/es/select` LabeledValue import
  with the re-export from `@superset-ui/core/components` (per CLAUDE.md
  rule against direct Ant Design imports).

- ModalTrigger: handle callback refs in addition to object refs. The
  previous `if (ref && typeof ref !== 'function')` branch silently
  no-op'd whenever a parent passed a function ref, so they couldn't
  access close/open/showModal.

Not applying the AsyncEsmComponent ref-type suggestion: the existing
`@ts-expect-error` directive above the call site is the deliberate
workaround for the PropsWithoutRef incompatibility, and changing the
ref type would make that directive unused and warn.
2026-06-08 07:25:28 -07:00
Evan Rusackas
defa5fc627 chore(superset-ui-core): forward-compat fixes for TypeScript 6.0
Scoped to packages/superset-ui-core. All changes compile cleanly on
TypeScript 5.4.5 (current CI) and eliminate TS 6.0 errors in this package.

Key fixes:

- forwardRef callbacks use ForwardedRef<T> instead of RefObject<T>
  (AsyncEsmComponent, DropdownContainer, ModalTrigger). ModalTrigger
  also narrows object-ref access with a typeof-function guard.
- StatefulChart: cast onRenderFailure to the shared HandlerFunction type
  so the stricter 6.0 inference matches the SuperChart prop.
- AntdEnhanced: drop the wider IconType.component from the spread so
  {...rest} can no longer override the explicit <BaseIconComponent
  component={...}> binding under 6.0's stricter JSX attribute checking.
- Select / AsyncSelect: widen antd sorter/filter/handler call-sites with
  targeted `as unknown as ...` casts against antd's BaseOptionType /
  DefaultOptionType / SelectHandler shapes. Behaviour unchanged; only
  the type boundary moves.
- VirtualTable: accept width?: number from react-resize-detector's 6.0
  callback signature, defaulting missing width to 0.
- TableCollection: cast columns / rowClassName / onChange to antd's
  generic ColumnsType<object> and TableProps<object> variants.
- TimezoneSelector: cast our comparator to antd's LabeledValue-based
  sorter signature; our comparator only reads fields that always exist
  on TimezoneOption, so the broader shape is safe at runtime.
- connection/types: widen FetchRetryOptions retryDelay/retryOn to
  accept `Error | null` / `Response | null`, matching fetch-retry.
- fetchTimeRange: rename catch variable and narrow it via the
  getClientErrorObject parameter type, handling TS 6.0's `unknown`
  default for caught values.
- lruCache: guard Map iterator .value before Map#delete (TS 6.0 types
  IteratorResult.value as `string | undefined`).
- InteractiveTableUtils: initialise columnRef to null for strict
  property initialization.
- types/assets.d.ts: add `declare module '*.css'` for CSS side-effect
  imports under 6.0's stricter module resolution.

Part of the TypeScript 5.4 -> 6.0 migration, split per-package to keep
reviews small. No runtime behaviour changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 07:25:27 -07:00
15 changed files with 207 additions and 48 deletions

View File

@@ -28,6 +28,7 @@ import {
RequestConfig,
getClientErrorObject,
} from '../..';
import type { HandlerFunction } from '../types/Base';
import { Loading } from '../../components/Loading';
import ChartClient from '../clients/ChartClient';
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
@@ -482,7 +483,7 @@ export default function StatefulChart(props: StatefulChartProps) {
enableNoResults={enableNoResults}
noResults={NoDataComponent && <NoDataComponent />}
onRenderSuccess={onRenderSuccess}
onRenderFailure={onRenderFailure}
onRenderFailure={onRenderFailure as HandlerFunction | undefined}
hooks={hooks}
/>
);

View File

@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState, forwardRef, ComponentType } from 'react';
import React, {
useEffect,
useState,
forwardRef,
ComponentType,
ForwardedRef,
} from 'react';
import { Loading } from '../Loading';
import type { PlaceholderProps } from './types';
@@ -93,7 +99,7 @@ export function AsyncEsmComponent<
// @ts-expect-error -- generic forwardRef has PropsWithoutRef incompatibility with FullProps
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: FullProps,
ref,
ref: ForwardedRef<ComponentType<FullProps>>,
) {
const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => {

View File

@@ -19,7 +19,7 @@
import {
cloneElement,
forwardRef,
RefObject,
ForwardedRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
@@ -54,7 +54,7 @@ export const DropdownContainer = forwardRef(
forceRender,
style,
}: DropdownContainerProps,
outerRef: RefObject<DropdownRef>,
outerRef: ForwardedRef<DropdownRef>,
) => {
const theme = useTheme();
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();

View File

@@ -328,11 +328,17 @@ export const antdEnhancedIcons: Record<
.filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded)))
.reduce(
(acc, key) => {
acc[key as AntdIconNames] = (props: IconType) => (
acc[key as AntdIconNames] = ({
// Forward-compat: TS 6.0 treats IconComponentProps.component as a
// different shape than BaseIconProps.component; strip it from spread
// props so our own component binding is authoritative.
component: _ignoredComponent,
...rest
}: IconType) => (
<BaseIconComponent
component={AntdIcons[key as AntdIconNames]}
fileName={key}
{...props}
{...rest}
/>
);
return acc;

View File

@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef, useState, ReactNode, MouseEvent } from 'react';
import {
forwardRef,
ForwardedRef,
useState,
ReactNode,
MouseEvent,
} from 'react';
import { Button } from '../Button';
import { Modal } from '../Modal';
@@ -51,7 +57,7 @@ export interface ModalTriggerRef {
}
export const ModalTrigger = forwardRef(
(props: ModalTriggerProps, ref: ModalTriggerRef | null) => {
(props: ModalTriggerProps, ref: ForwardedRef<ModalTriggerRef['current']>) => {
const [showModal, setShowModal] = useState(false);
const {
beforeOpen = () => {},
@@ -84,8 +90,14 @@ export const ModalTrigger = forwardRef(
setShowModal(true);
};
if (ref) {
ref.current = { close, open, showModal }; // eslint-disable-line
// Forward both callback refs (e.g. `(value) => setRef(value)`) and
// object refs. Without the callback-ref branch, parents that pass a
// function ref get silently no-op'd and can't call close/open/showModal.
const refValue = { close, open, showModal };
if (typeof ref === 'function') {
ref(refValue);
} else if (ref) {
ref.current = refValue; // eslint-disable-line
}
/* eslint-disable jsx-a11y/interactive-supports-focus */

View File

@@ -18,6 +18,7 @@
*/
import {
forwardRef,
ForwardedRef,
FocusEvent,
ReactElement,
RefObject,
@@ -38,6 +39,8 @@ import {
getClientErrorObject,
} from '@superset-ui/core';
import {
BaseOptionType,
DefaultOptionType,
LabeledValue as AntdLabeledValue,
RefSelectProps,
} from 'antd/es/select';
@@ -146,7 +149,7 @@ const AsyncSelect = forwardRef(
maxTagCount: propsMaxTagCount,
...props
}: AsyncSelectProps,
ref: RefObject<AsyncSelectRef>,
ref: ForwardedRef<AsyncSelectRef>,
) => {
const isSingleMode = mode === 'single';
const [selectValue, setSelectValue] = useState(value);
@@ -318,7 +321,14 @@ const AsyncSelect = forwardRef(
mergedData = prevOptions
.filter(previousOption => !dataValues.has(previousOption.value))
.concat(data)
.sort(sortComparatorForNoSearch);
// Forward-compat: TS 6.0 infers stricter antd option types; widen
// the comparator to accept the broader DefaultOptionType shape.
.sort(
sortComparatorForNoSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
);
return mergedData;
});
}
@@ -503,7 +513,13 @@ const AsyncSelect = forwardRef(
if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
const sortedOptions = selectOptions
.slice()
.sort(sortComparatorForNoSearch);
// Forward-compat: see note in mergeData above.
.sort(
sortComparatorForNoSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
);
if (!isEqual(sortedOptions, selectOptions)) {
setSelectOptions(sortedOptions);
}
@@ -626,14 +642,16 @@ const AsyncSelect = forwardRef(
setAllValuesLoaded(false);
};
useImperativeHandle(
ref,
() => ({
...(ref.current as RefSelectProps),
useImperativeHandle(ref, () => {
const current =
ref && typeof ref !== 'function' && ref.current
? (ref.current as RefSelectProps)
: ({} as RefSelectProps);
return {
...current,
clearCache,
}),
[ref],
);
};
}, [ref]);
const getPastedTextValue = useCallback(
async (text: string) => {
@@ -699,8 +717,21 @@ const AsyncSelect = forwardRef(
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
popupRender={popupRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
// Forward-compat: TS 6.0 infers stricter antd option types; local
// helpers typed against AntdLabeledValue are behaviorally compatible
// with the broader BaseOptionType/DefaultOptionType antd expects.
filterOption={
handleFilterOption as unknown as (
search: string,
option?: BaseOptionType | DefaultOptionType,
) => boolean
}
filterSort={
sortComparatorWithSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number
}
getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode)
}
@@ -710,13 +741,26 @@ const AsyncSelect = forwardRef(
mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
// Forward-compat: TS 6.0 narrows the Select value type handed to
// SelectHandler; our local handlers already accept the broader union.
onDeselect={
handleOnDeselect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onOpenChange={handleOnDropdownVisibleChange}
// @ts-expect-error
// @ts-expect-error antd Select does not declare onPaste on its prop
// surface, but the underlying input accepts it and we rely on that.
onPaste={onPaste}
onPopupScroll={handlePagination}
onSearch={showSearch ? handleOnSearch : undefined}
onSelect={handleOnSelect}
onSelect={
handleOnSelect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onClear={handleClear}
options={fullSelectOptions}
optionRender={option => <Space>{option.label || option.value}</Space>}

View File

@@ -34,6 +34,8 @@ import { t } from '@apache-superset/core/translation';
import { ensureIsArray, formatNumber, usePrevious } from '@superset-ui/core';
import { Constants } from '@superset-ui/core/components';
import {
BaseOptionType,
DefaultOptionType,
LabeledValue as AntdLabeledValue,
RefSelectProps,
} from 'antd/es/select';
@@ -211,7 +213,17 @@ const Select = forwardRef(
);
const initialOptionsSorted = useMemo(
() => initialOptions.slice().sort(sortSelectedFirst),
() =>
initialOptions
.slice()
// Forward-compat: TS 6.0 infers stricter antd option types; widen the
// comparator to accept the broader DefaultOptionType shape.
.sort(
sortSelectedFirst as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
),
[initialOptions, sortSelectedFirst],
);
@@ -239,7 +251,17 @@ const Select = forwardRef(
missingValues.length > 0
? missingValues.concat(selectOptions)
: selectOptions;
return result.slice().sort(sortSelectedFirst);
return (
result
.slice()
// Forward-compat: see note on initialOptionsSorted.
.sort(
sortSelectedFirst as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number,
)
);
}, [selectOptions, selectValue, sortSelectedFirst]);
const enabledOptions = useMemo(
@@ -519,7 +541,8 @@ const Select = forwardRef(
handleSelectAll();
}}
>
{t('Select all')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
{t('Select all')}{' '}
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
</Button>
<Button
type="link"
@@ -536,7 +559,8 @@ const Select = forwardRef(
handleDeselectAll();
}}
>
{t('Clear')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
{t('Clear')}{' '}
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
</Button>
</StyledBulkActionsContainer>
),
@@ -751,8 +775,21 @@ const Select = forwardRef(
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
popupRender={popupRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
// Forward-compat: TS 6.0 infers stricter antd option types; local
// helpers typed against AntdLabeledValue are behaviorally compatible
// with the broader BaseOptionType/DefaultOptionType antd expects.
filterOption={
handleFilterOption as unknown as (
search: string,
option?: BaseOptionType | DefaultOptionType,
) => boolean
}
filterSort={
sortComparatorWithSearch as unknown as (
a: BaseOptionType | DefaultOptionType,
b: BaseOptionType | DefaultOptionType,
) => number
}
getPopupContainer={
getPopupContainer || (triggerNode => triggerNode.parentNode)
}
@@ -763,13 +800,26 @@ const Select = forwardRef(
mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
// Forward-compat: TS 6.0 narrows the Select value type handed to
// SelectHandler; our local handlers already accept the broader union.
onDeselect={
handleOnDeselect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onOpenChange={handleOnDropdownVisibleChange}
// @ts-expect-error
// @ts-expect-error antd Select does not declare onPaste on its prop
// surface, but the underlying input accepts it and we rely on that.
onPaste={onPaste}
onPopupScroll={undefined}
onSearch={shouldShowSearch ? handleOnSearch : undefined}
onSelect={handleOnSelect}
onSelect={
handleOnSelect as unknown as (
value: unknown,
option: BaseOptionType | DefaultOptionType,
) => void
}
onClear={handleClear}
placeholder={placeholder}
tokenSeparators={tokenSeparators}

View File

@@ -84,8 +84,8 @@ const VirtualTable = <RecordType extends object>(
allowHTML = false,
} = props;
const [tableWidth, setTableWidth] = useState<number>(0);
const onResize = useCallback((width: number) => {
setTableWidth(width);
const onResize = useCallback((width?: number) => {
setTableWidth(width ?? 0);
}, []);
const { ref } = useResizeDetector({ onResize });
const theme = useTheme();

View File

@@ -29,7 +29,7 @@ interface IInteractiveColumn extends HTMLElement {
export default class InteractiveTableUtils {
tableRef: HTMLTableElement | null;
columnRef: IInteractiveColumn | null;
columnRef: IInteractiveColumn | null = null;
setDerivedColumns: Function;

View File

@@ -27,7 +27,12 @@ import {
} from 'react-table';
import { styled } from '@apache-superset/core/theme';
import { Table, TableSize } from '@superset-ui/core/components/Table';
import { TableRowSelection, SorterResult } from 'antd/es/table/interface';
import {
ColumnsType,
TableRowSelection,
SorterResult,
} from 'antd/es/table/interface';
import type { TableProps } from 'antd/es/table';
import { mapColumns, mapRows } from './utils';
export interface TableCollectionProps<T extends object> {
@@ -303,7 +308,10 @@ function TableCollection<T extends object>({
<StyledTable
loading={loading}
sticky={sticky ?? false}
columns={mappedColumns}
// Forward-compat: TS 6.0 tightens antd Table's generic inference so our
// typed-against-react-table mapped columns must be widened to the antd
// ColumnsType<object> surface the Table expects here.
columns={mappedColumns as unknown as ColumnsType<object>}
data={mappedRows}
size={size}
data-test="listview-table"
@@ -316,7 +324,9 @@ function TableCollection<T extends object>({
sortDirections={['ascend', 'descend', 'ascend']}
isPaginationSticky={isPaginationSticky}
showRowCount={showRowCount}
rowClassName={getRowClassName}
rowClassName={
getRowClassName as unknown as TableProps<object>['rowClassName']
}
expandable={expandable}
components={{
header: {
@@ -342,7 +352,7 @@ function TableCollection<T extends object>({
),
},
}}
onChange={handleTableChange}
onChange={handleTableChange as unknown as TableProps<object>['onChange']}
/>
);
}

View File

@@ -20,6 +20,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { t } from '@apache-superset/core/translation';
import { Select } from '@superset-ui/core/components';
import type { LabeledValue } from '@superset-ui/core/components';
import { extendedDayjs } from '../../utils/dates';
import {
timezoneOptionsCache,
@@ -156,7 +157,16 @@ export default function TimezoneSelector({
onOpenChange={handleOpenChange}
value={selectValue}
options={timezoneOptions || []}
sortComparator={sortComparator}
// Forward-compat: TS 6.0 resolves sortComparator against antd's
// LabeledValue; our comparator only reads properties that always exist
// on TimezoneOption, so the broader shape is safe at runtime.
sortComparator={
sortComparator as unknown as (
a: LabeledValue,
b: LabeledValue,
search?: string,
) => number
}
loading={isLoadingOptions}
placeholder={isLoadingOptions ? t('Loading timezones...') : placeholder}
{...{ placement: 'topLeft', ...rest }}

View File

@@ -26,10 +26,18 @@ export type FetchRetryOptions = {
retries?: number;
retryDelay?:
| number
| ((attempt: number, error: Error, response: Response) => number);
| ((
attempt: number,
error: Error | null,
response: Response | null,
) => number);
retryOn?:
| number[]
| ((attempt: number, error: Error, response: Response) => boolean);
| ((
attempt: number,
error: Error | null,
response: Response | null,
) => boolean);
};
export type Headers = { [k: string]: string };
export type Host = string;

View File

@@ -103,10 +103,16 @@ export const fetchTimeRange = async (
),
),
};
} catch (response) {
} catch (caught) {
// Forward-compat: TS 6.0 types caught values as `unknown`; cast to the
// shape getClientErrorObject accepts and narrow for statusText access.
const response = caught as Parameters<typeof getClientErrorObject>[0];
const clientError = await getClientErrorObject(response);
return {
error: clientError.message || clientError.error || response.statusText,
error:
clientError.message ||
clientError.error ||
(response as { statusText?: string }).statusText,
};
}
};

View File

@@ -55,7 +55,12 @@ class LRUCache<T> {
throw new TypeError('The LRUCache key must be string.');
}
if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
// Forward-compat: TS 6.0 types IteratorResult.value as `string | undefined`
// when not explicitly checked; guard before passing to Map#delete.
const oldestKey = this.cache.keys().next().value;
if (oldestKey !== undefined) {
this.cache.delete(oldestKey);
}
}
this.cache.set(key, value);
}

View File

@@ -21,3 +21,4 @@ declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.css';