Skip to content

Commit a8f4389

Browse files
rusackasclaude
andauthored
chore(superset-ui-core): forward-compat fixes for TypeScript 6.0 (Phase B) (#39535)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 4bf203e commit a8f4389

16 files changed

Lines changed: 210 additions & 46 deletions

File tree

UPDATING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ The git SHA and build number surfaced in the "About" section, the bootstrap payl
4444

4545
The pivot table chart's `First` and `Last` aggregations now return the first and last value in data (query result) order, instead of effectively returning the minimum and maximum. Existing pivot tables that use these aggregations for totals/subtotals may show different values after upgrading. For deterministic results, ensure the underlying query has a stable sort order.
4646

47+
### `FetchRetryOptions` callback parameters widened to allow `null`
48+
49+
The `error` and `response` parameters of the `retryDelay` and `retryOn` callbacks in `FetchRetryOptions` (exported from `@superset-ui/core`) are now typed `Error | null` and `Response | null` to match the actual call-site signature provided by `fetch-retry`. Because these parameter types are contravariant, consumers who typed their callbacks with the non-nullable `(attempt: number, error: Error, response: Response) => number` will get a TypeScript compile error. Widen your callback signatures to accept `Error | null` / `Response | null`.
50+
4751
### `thumbnail_url` removed from dashboard list API response
4852

4953
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:

superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
RequestConfig,
2929
getClientErrorObject,
3030
} from '../..';
31+
import type { HandlerFunction } from '../types/Base';
3132
import { Loading } from '../../components/Loading';
3233
import ChartClient from '../clients/ChartClient';
3334
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
@@ -482,7 +483,7 @@ export default function StatefulChart(props: StatefulChartProps) {
482483
enableNoResults={enableNoResults}
483484
noResults={NoDataComponent && <NoDataComponent />}
484485
onRenderSuccess={onRenderSuccess}
485-
onRenderFailure={onRenderFailure}
486+
onRenderFailure={onRenderFailure as HandlerFunction | undefined}
486487
hooks={hooks}
487488
/>
488489
);

superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import React, { useEffect, useState, forwardRef, ComponentType } from 'react';
19+
import React, {
20+
useEffect,
21+
useState,
22+
forwardRef,
23+
ComponentType,
24+
ForwardedRef,
25+
} from 'react';
2026

2127
import { Loading } from '../Loading';
2228
import type { PlaceholderProps } from './types';
@@ -93,7 +99,7 @@ export function AsyncEsmComponent<
9399
// @ts-expect-error -- generic forwardRef has PropsWithoutRef incompatibility with FullProps
94100
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
95101
props: FullProps,
96-
ref,
102+
ref: ForwardedRef<ComponentType<FullProps>>,
97103
) {
98104
const [loaded, setLoaded] = useState(component !== undefined);
99105
useEffect(() => {

superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import {
2020
cloneElement,
2121
forwardRef,
22-
RefObject,
22+
ForwardedRef,
2323
useEffect,
2424
useImperativeHandle,
2525
useLayoutEffect,
@@ -54,7 +54,7 @@ export const DropdownContainer = forwardRef(
5454
forceRender,
5555
style,
5656
}: DropdownContainerProps,
57-
outerRef: RefObject<DropdownRef>,
57+
outerRef: ForwardedRef<DropdownRef>,
5858
) => {
5959
const theme = useTheme();
6060
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();

superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,21 @@ export const antdEnhancedIcons: Record<
331331
.reduce(
332332
(acc, key) => {
333333
acc[key as AntdIconNames] = forwardRef<HTMLSpanElement, IconType>(
334-
(props, ref) => (
334+
(
335+
{
336+
// Forward-compat: TS 6.0 treats IconComponentProps.component as a
337+
// different shape than BaseIconProps.component; strip it from spread
338+
// props so our own component binding is authoritative.
339+
component: _ignoredComponent,
340+
...rest
341+
},
342+
ref,
343+
) => (
335344
<BaseIconComponent
336345
ref={ref}
337346
component={AntdIcons[key as AntdIconNames]}
338347
fileName={key}
339-
{...props}
348+
{...rest}
340349
/>
341350
),
342351
);

superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { forwardRef, useState, ReactNode, MouseEvent } from 'react';
19+
import {
20+
forwardRef,
21+
ForwardedRef,
22+
useState,
23+
ReactNode,
24+
MouseEvent,
25+
} from 'react';
2026

2127
import { Button } from '../Button';
2228
import { Modal } from '../Modal';
@@ -54,7 +60,7 @@ export interface ModalTriggerRef {
5460
}
5561

5662
export const ModalTrigger = forwardRef(
57-
(props: ModalTriggerProps, ref: ModalTriggerRef | null) => {
63+
(props: ModalTriggerProps, ref: ForwardedRef<ModalTriggerRef['current']>) => {
5864
const [showModal, setShowModal] = useState(false);
5965
const {
6066
beforeOpen = () => {},
@@ -87,8 +93,14 @@ export const ModalTrigger = forwardRef(
8793
setShowModal(true);
8894
};
8995

90-
if (ref) {
91-
ref.current = { close, open, showModal }; // eslint-disable-line
96+
// Forward both callback refs (e.g. `(value) => setRef(value)`) and
97+
// object refs. Without the callback-ref branch, parents that pass a
98+
// function ref get silently no-op'd and can't call close/open/showModal.
99+
const refValue = { close, open, showModal };
100+
if (typeof ref === 'function') {
101+
ref(refValue);
102+
} else if (ref) {
103+
ref.current = refValue; // eslint-disable-line
92104
}
93105

94106
/* eslint-disable jsx-a11y/interactive-supports-focus */

superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
import {
2020
forwardRef,
21+
ForwardedRef,
2122
FocusEvent,
2223
ReactElement,
2324
RefObject,
@@ -38,6 +39,8 @@ import {
3839
getClientErrorObject,
3940
} from '@superset-ui/core';
4041
import {
42+
BaseOptionType,
43+
DefaultOptionType,
4144
LabeledValue as AntdLabeledValue,
4245
RefSelectProps,
4346
} from 'antd/es/select';
@@ -146,7 +149,7 @@ const AsyncSelect = forwardRef(
146149
maxTagCount: propsMaxTagCount,
147150
...props
148151
}: AsyncSelectProps,
149-
ref: RefObject<AsyncSelectRef>,
152+
ref: ForwardedRef<AsyncSelectRef>,
150153
) => {
151154
const isSingleMode = mode === 'single';
152155
const [selectValue, setSelectValue] = useState(value);
@@ -324,7 +327,14 @@ const AsyncSelect = forwardRef(
324327
mergedData = prevOptions
325328
.filter(previousOption => !dataValues.has(previousOption.value))
326329
.concat(data)
327-
.sort(sortComparatorForNoSearch);
330+
// Forward-compat: TS 6.0 infers stricter antd option types; widen
331+
// the comparator to accept the broader DefaultOptionType shape.
332+
.sort(
333+
sortComparatorForNoSearch as unknown as (
334+
a: BaseOptionType | DefaultOptionType,
335+
b: BaseOptionType | DefaultOptionType,
336+
) => number,
337+
);
328338
return mergedData;
329339
});
330340
}
@@ -509,7 +519,13 @@ const AsyncSelect = forwardRef(
509519
if (isDropdownVisible && !inputValue && selectOptions.length > 1) {
510520
const sortedOptions = selectOptions
511521
.slice()
512-
.sort(sortComparatorForNoSearch);
522+
// Forward-compat: see note in mergeData above.
523+
.sort(
524+
sortComparatorForNoSearch as unknown as (
525+
a: BaseOptionType | DefaultOptionType,
526+
b: BaseOptionType | DefaultOptionType,
527+
) => number,
528+
);
513529
if (!isEqual(sortedOptions, selectOptions)) {
514530
setSelectOptions(sortedOptions);
515531
}
@@ -632,14 +648,16 @@ const AsyncSelect = forwardRef(
632648
setAllValuesLoaded(false);
633649
};
634650

635-
useImperativeHandle(
636-
ref,
637-
() => ({
638-
...(ref.current as RefSelectProps),
651+
useImperativeHandle(ref, () => {
652+
const current =
653+
ref && typeof ref !== 'function' && ref.current
654+
? (ref.current as RefSelectProps)
655+
: ({} as RefSelectProps);
656+
return {
657+
...current,
639658
clearCache,
640-
}),
641-
[ref],
642-
);
659+
};
660+
}, [ref]);
643661

644662
const getPastedTextValue = useCallback(
645663
async (text: string) => {
@@ -705,8 +723,21 @@ const AsyncSelect = forwardRef(
705723
data-test={ariaLabel || name}
706724
autoClearSearchValue={autoClearSearchValue}
707725
popupRender={popupRender}
708-
filterOption={handleFilterOption}
709-
filterSort={sortComparatorWithSearch}
726+
// Forward-compat: TS 6.0 infers stricter antd option types; local
727+
// helpers typed against AntdLabeledValue are behaviorally compatible
728+
// with the broader BaseOptionType/DefaultOptionType antd expects.
729+
filterOption={
730+
handleFilterOption as unknown as (
731+
search: string,
732+
option?: BaseOptionType | DefaultOptionType,
733+
) => boolean
734+
}
735+
filterSort={
736+
sortComparatorWithSearch as unknown as (
737+
a: BaseOptionType | DefaultOptionType,
738+
b: BaseOptionType | DefaultOptionType,
739+
) => number
740+
}
710741
getPopupContainer={
711742
getPopupContainer || (triggerNode => triggerNode.parentNode)
712743
}
@@ -716,13 +747,26 @@ const AsyncSelect = forwardRef(
716747
mode={mappedMode}
717748
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
718749
onBlur={handleOnBlur}
719-
onDeselect={handleOnDeselect}
750+
// Forward-compat: TS 6.0 narrows the Select value type handed to
751+
// SelectHandler; our local handlers already accept the broader union.
752+
onDeselect={
753+
handleOnDeselect as unknown as (
754+
value: unknown,
755+
option: BaseOptionType | DefaultOptionType,
756+
) => void
757+
}
720758
onOpenChange={handleOnDropdownVisibleChange}
721-
// @ts-expect-error
759+
// @ts-expect-error antd Select does not declare onPaste on its prop
760+
// surface, but the underlying input accepts it and we rely on that.
722761
onPaste={onPaste}
723762
onPopupScroll={handlePagination}
724763
onSearch={showSearch ? handleOnSearch : undefined}
725-
onSelect={handleOnSelect}
764+
onSelect={
765+
handleOnSelect as unknown as (
766+
value: unknown,
767+
option: BaseOptionType | DefaultOptionType,
768+
) => void
769+
}
726770
onClear={handleClear}
727771
options={fullSelectOptions}
728772
optionRender={option => <Space>{option.label || option.value}</Space>}

superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { t } from '@apache-superset/core/translation';
3434
import { ensureIsArray, formatNumber, usePrevious } from '@superset-ui/core';
3535
import { Constants } from '@superset-ui/core/components';
3636
import {
37+
BaseOptionType,
38+
DefaultOptionType,
3739
LabeledValue as AntdLabeledValue,
3840
RefSelectProps,
3941
} from 'antd/es/select';
@@ -212,7 +214,17 @@ const Select = forwardRef(
212214
);
213215

214216
const initialOptionsSorted = useMemo(
215-
() => initialOptions.slice().sort(sortSelectedFirst),
217+
() =>
218+
initialOptions
219+
.slice()
220+
// Forward-compat: TS 6.0 infers stricter antd option types; widen the
221+
// comparator to accept the broader DefaultOptionType shape.
222+
.sort(
223+
sortSelectedFirst as unknown as (
224+
a: BaseOptionType | DefaultOptionType,
225+
b: BaseOptionType | DefaultOptionType,
226+
) => number,
227+
),
216228
[initialOptions, sortSelectedFirst],
217229
);
218230

@@ -240,7 +252,17 @@ const Select = forwardRef(
240252
missingValues.length > 0
241253
? missingValues.concat(selectOptions)
242254
: selectOptions;
243-
return result.slice().sort(sortSelectedFirst);
255+
return (
256+
result
257+
.slice()
258+
// Forward-compat: see note on initialOptionsSorted.
259+
.sort(
260+
sortSelectedFirst as unknown as (
261+
a: BaseOptionType | DefaultOptionType,
262+
b: BaseOptionType | DefaultOptionType,
263+
) => number,
264+
)
265+
);
244266
}, [selectOptions, selectValue, sortSelectedFirst]);
245267

246268
const enabledOptions = useMemo(
@@ -773,8 +795,21 @@ const Select = forwardRef(
773795
data-test={ariaLabel || name}
774796
autoClearSearchValue={autoClearSearchValue}
775797
popupRender={popupRender}
776-
filterOption={handleFilterOption}
777-
filterSort={sortComparatorWithSearch}
798+
// Forward-compat: TS 6.0 infers stricter antd option types; local
799+
// helpers typed against AntdLabeledValue are behaviorally compatible
800+
// with the broader BaseOptionType/DefaultOptionType antd expects.
801+
filterOption={
802+
handleFilterOption as unknown as (
803+
search: string,
804+
option?: BaseOptionType | DefaultOptionType,
805+
) => boolean
806+
}
807+
filterSort={
808+
sortComparatorWithSearch as unknown as (
809+
a: BaseOptionType | DefaultOptionType,
810+
b: BaseOptionType | DefaultOptionType,
811+
) => number
812+
}
778813
getPopupContainer={
779814
getPopupContainer || (triggerNode => triggerNode.parentNode)
780815
}
@@ -785,13 +820,26 @@ const Select = forwardRef(
785820
mode={mappedMode}
786821
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
787822
onBlur={handleOnBlur}
788-
onDeselect={handleOnDeselect}
823+
// Forward-compat: TS 6.0 narrows the Select value type handed to
824+
// SelectHandler; our local handlers already accept the broader union.
825+
onDeselect={
826+
handleOnDeselect as unknown as (
827+
value: unknown,
828+
option: BaseOptionType | DefaultOptionType,
829+
) => void
830+
}
789831
onOpenChange={handleOnDropdownVisibleChange}
790-
// @ts-expect-error
832+
// @ts-expect-error antd Select does not declare onPaste on its prop
833+
// surface, but the underlying input accepts it and we rely on that.
791834
onPaste={onPaste}
792835
onPopupScroll={undefined}
793836
onSearch={shouldShowSearch ? handleOnSearch : undefined}
794-
onSelect={handleOnSelect}
837+
onSelect={
838+
handleOnSelect as unknown as (
839+
value: unknown,
840+
option: BaseOptionType | DefaultOptionType,
841+
) => void
842+
}
795843
onClear={handleClear}
796844
placeholder={placeholder}
797845
tokenSeparators={tokenSeparators}

superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ const VirtualTable = <RecordType extends object>(
8484
allowHTML = false,
8585
} = props;
8686
const [tableWidth, setTableWidth] = useState<number>(0);
87-
const onResize = useCallback((width: number) => {
88-
setTableWidth(width);
87+
const onResize = useCallback((width?: number) => {
88+
setTableWidth(width ?? 0);
8989
}, []);
9090
const { ref } = useResizeDetector({ onResize });
9191
const theme = useTheme();

superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ interface IInteractiveColumn extends HTMLElement {
2929
export default class InteractiveTableUtils {
3030
tableRef: HTMLTableElement | null;
3131

32-
columnRef: IInteractiveColumn | null;
32+
columnRef: IInteractiveColumn | null = null;
3333

3434
setDerivedColumns: Function;
3535

0 commit comments

Comments
 (0)