chore: Upgrade to React 18 (#38563)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
This commit is contained in:
Mehmet Salih Yavuz
2026-05-04 19:19:36 +03:00
committed by GitHub
parent 28239c18d4
commit 41a22d7918
183 changed files with 5035 additions and 7225 deletions

View File

@@ -16,17 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { Component } from 'react';
import React, { useCallback, useMemo } from 'react';
import { IconTooltip, List } from '@superset-ui/core/components';
import { nanoid } from 'nanoid';
import { t } from '@apache-superset/core/translation';
import { withTheme, type SupersetTheme } from '@apache-superset/core/theme';
import { useTheme, type SupersetTheme } from '@apache-superset/core/theme';
import {
SortableContainer,
SortableHandle,
SortableElement,
DndContext,
closestCenter,
useSensor,
useSensors,
PointerSensor,
type DragEndEvent,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
useSortable,
arrayMove,
} from 'react-sortable-hoc';
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Icons } from '@superset-ui/core/components/Icons';
import {
HeaderContainer,
@@ -54,161 +63,243 @@ interface CollectionControlProps {
isFloat?: boolean;
isInt?: boolean;
controlName: string;
theme: SupersetTheme;
}
const defaultProps: Partial<CollectionControlProps> = {
label: null,
description: null,
onChange: () => {},
placeholder: t('Empty collection'),
itemGenerator: () => ({ key: nanoid(11) }),
keyAccessor: (o: CollectionItem) => o.key ?? '',
value: [],
addTooltip: t('Add an item'),
};
const SortableListItem = SortableElement(CustomListItem);
const SortableList = SortableContainer(List);
const SortableDragger = SortableHandle(() => (
<Icons.MenuOutlined
role="img"
aria-label={t('Drag to reorder')}
className="text-primary"
style={{ cursor: 'ns-resize' }}
/>
));
function DragHandle() {
return (
<Icons.MenuOutlined
role="img"
aria-label={t('Drag to reorder')}
className="text-primary"
style={{ cursor: 'ns-resize' }}
/>
);
}
class CollectionControl extends Component<CollectionControlProps> {
static defaultProps = defaultProps;
interface SortableItemProps {
id: string;
index: number;
item: CollectionItem;
controlProps: Omit<CollectionControlProps, 'label'>;
onChangeItem: (index: number, value: CollectionItem) => void;
onRemoveItem: (index: number) => void;
}
constructor(props: CollectionControlProps) {
super(props);
this.onAdd = this.onAdd.bind(this);
}
function SortableItem({
id,
index,
item,
controlProps,
onChangeItem,
onRemoveItem,
}: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition ?? undefined,
};
const Control = (controlMap as Record<string, React.ComponentType<any>>)[
controlProps.controlName
];
onChange(i: number, value: CollectionItem) {
const currentValue = this.props.value ?? [];
const newValue = [...currentValue];
newValue[i] = { ...currentValue[i], ...value };
this.props.onChange?.(newValue);
}
onAdd() {
const currentValue = this.props.value ?? [];
const newItem = this.props.itemGenerator?.();
// Cast needed: original JS allowed undefined items from itemGenerator
this.props.onChange?.(
currentValue.concat([newItem] as unknown as CollectionItem[]),
);
}
onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
const currentValue = this.props.value ?? [];
this.props.onChange?.(arrayMove(currentValue, oldIndex, newIndex));
}
removeItem(i: number) {
const currentValue = this.props.value ?? [];
this.props.onChange?.(currentValue.filter((o, ix) => i !== ix));
}
renderList() {
const currentValue = this.props.value ?? [];
if (currentValue.length === 0) {
return <div className="text-muted">{this.props.placeholder}</div>;
}
const Control = (controlMap as Record<string, React.ComponentType<any>>)[
this.props.controlName
];
const keyAccessor =
this.props.keyAccessor ?? ((o: CollectionItem) => o.key ?? '');
return (
<SortableList
useDragHandle
lockAxis="y"
onSortEnd={this.onSortEnd.bind(this)}
bordered
return (
<CustomListItem
ref={setNodeRef}
style={style}
selectable={false}
className="clearfix"
css={(theme: SupersetTheme) => ({
alignItems: 'center',
justifyContent: 'flex-start',
display: 'flex',
paddingInline: theme.sizeUnit * 6,
})}
>
<span {...attributes} {...listeners}>
<DragHandle />
</span>
<div
css={(theme: SupersetTheme) => ({
borderRadius: theme.borderRadius,
flex: 1,
marginLeft: theme.sizeUnit * 2,
marginRight: theme.sizeUnit * 2,
})}
>
{currentValue.map((o: CollectionItem, i: number) => {
// label relevant only for header, not here
const { label, theme, ...commonProps } = this.props;
return (
<SortableListItem
selectable={false}
className="clearfix"
css={(theme: SupersetTheme) => ({
alignItems: 'center',
justifyContent: 'flex-start',
display: 'flex',
paddingInline: theme.sizeUnit * 6,
})}
key={keyAccessor(o)}
index={i}
>
<SortableDragger />
<div
css={(theme: SupersetTheme) => ({
flex: 1,
marginLeft: theme.sizeUnit * 2,
marginRight: theme.sizeUnit * 2,
})}
>
<Control
{...commonProps}
{...o}
onChange={this.onChange.bind(this, i)}
/>
</div>
<IconTooltip
className="pointer"
placement="right"
onClick={this.removeItem.bind(this, i)}
tooltip={t('Remove item')}
mouseEnterDelay={0}
mouseLeaveDelay={0}
css={(theme: SupersetTheme) => ({
padding: 0,
minWidth: 'auto',
height: 'auto',
lineHeight: 1,
cursor: 'pointer',
'& svg path': {
fill: theme.colorIcon,
transition: `fill ${theme.motionDurationMid} ease-out`,
},
'&:hover svg path': {
fill: theme.colorError,
},
})}
>
<Icons.CloseOutlined iconSize="s" />
</IconTooltip>
</SortableListItem>
);
})}
</SortableList>
);
}
render() {
return (
<div data-test="CollectionControl" className="CollectionControl">
<HeaderContainer>
<ControlHeader {...this.props} />
<AddIconButton onClick={this.onAdd}>
<Icons.PlusOutlined
iconSize="s"
iconColor={this.props.theme.colorTextLightSolid}
/>
</AddIconButton>
</HeaderContainer>
{this.renderList()}
<Control
{...controlProps}
{...item}
onChange={(value: CollectionItem) => onChangeItem(index, value)}
/>
</div>
);
}
<IconTooltip
className="pointer"
placement="right"
onClick={() => onRemoveItem(index)}
tooltip={t('Remove item')}
mouseEnterDelay={0}
mouseLeaveDelay={0}
css={(theme: SupersetTheme) => ({
padding: 0,
minWidth: 'auto',
height: 'auto',
lineHeight: 1,
cursor: 'pointer',
'& svg path': {
fill: theme.colorIcon,
transition: `fill ${theme.motionDurationMid} ease-out`,
},
'&:hover svg path': {
fill: theme.colorError,
},
})}
>
<Icons.CloseOutlined iconSize="s" />
</IconTooltip>
</CustomListItem>
);
}
export default withTheme(CollectionControl);
const defaultKeyAccessor = (o: CollectionItem) => o.key ?? '';
const defaultItemGenerator = () => ({ key: nanoid(11) });
function CollectionControl({
name,
label = null,
description = null,
placeholder = t('Empty collection'),
addTooltip = t('Add an item'),
itemGenerator = defaultItemGenerator,
keyAccessor = defaultKeyAccessor,
onChange,
value = [],
isFloat,
isInt,
controlName,
}: CollectionControlProps) {
const theme = useTheme();
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 5 },
}),
);
const itemIds = useMemo(
() => value.map((item, i) => keyAccessor(item) || String(i)),
[value, keyAccessor],
);
const onAdd = useCallback(() => {
const newItem = itemGenerator();
onChange?.(value.concat([newItem] as unknown as CollectionItem[]));
}, [value, itemGenerator, onChange]);
const onChangeItem = useCallback(
(i: number, itemValue: CollectionItem) => {
const newValue = [...value];
newValue[i] = { ...value[i], ...itemValue };
onChange?.(newValue);
},
[value, onChange],
);
const onRemoveItem = useCallback(
(i: number) => {
onChange?.(value.filter((_, ix) => i !== ix));
},
[value, onChange],
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = itemIds.indexOf(String(active.id));
const newIndex = itemIds.indexOf(String(over.id));
onChange?.(arrayMove(value, oldIndex, newIndex));
}
},
[value, itemIds, onChange],
);
const controlProps = useMemo(
() => ({
name,
description,
placeholder,
addTooltip,
itemGenerator,
keyAccessor,
onChange,
value,
isFloat,
isInt,
controlName,
}),
[
name,
description,
placeholder,
addTooltip,
itemGenerator,
keyAccessor,
onChange,
value,
isFloat,
isInt,
controlName,
],
);
const renderList = () => {
if (value.length === 0) {
return <div className="text-muted">{placeholder}</div>;
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
<List
bordered
css={(theme: SupersetTheme) => ({
borderRadius: theme.borderRadius,
})}
>
{value.map((item, i) => (
<SortableItem
key={itemIds[i]}
id={itemIds[i]}
index={i}
item={item}
controlProps={controlProps}
onChangeItem={onChangeItem}
onRemoveItem={onRemoveItem}
/>
))}
</List>
</SortableContext>
</DndContext>
);
};
return (
<div data-test="CollectionControl" className="CollectionControl">
<HeaderContainer>
<ControlHeader name={name} label={label} description={description} />
<AddIconButton onClick={onAdd}>
<Icons.PlusOutlined
iconSize="s"
iconColor={theme.colorTextLightSolid}
/>
</AddIconButton>
</HeaderContainer>
{renderList()}
</div>
);
}
export default CollectionControl;