mirror of
https://github.com/apache/superset.git
synced 2026-05-29 20:29:34 +00:00
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:
committed by
GitHub
parent
28239c18d4
commit
41a22d7918
@@ -37,6 +37,8 @@ export type ControlHeaderProps = {
|
||||
tooltipOnClick?: () => void;
|
||||
warning?: string;
|
||||
danger?: string;
|
||||
// Allow extra props from control spread patterns (e.g. {...this.props})
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const iconStyles = css`
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useFilteredTableData } from '.';
|
||||
|
||||
const data = [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Constants } from '@superset-ui/core/components';
|
||||
import { useTableColumns } from '.';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { QueryFormData, JsonObject } from '@superset-ui/core';
|
||||
@@ -100,7 +100,7 @@ const additionalItemsStyles = (theme: SupersetTheme) => css`
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExploreChartHeader: FC<ExploreChartHeaderProps> = ({
|
||||
const ExploreChartHeader: FC<ExploreChartHeaderProps> = ({
|
||||
dashboardId,
|
||||
colorScheme: dashboardColorScheme,
|
||||
slice,
|
||||
@@ -270,77 +270,112 @@ export const ExploreChartHeader: FC<ExploreChartHeaderProps> = ({
|
||||
}
|
||||
}, [showUnsavedChangesModal, shouldForceCloseModal]);
|
||||
|
||||
const editableTitleProps = useMemo(
|
||||
() => ({
|
||||
title: sliceName ?? '',
|
||||
canEdit:
|
||||
!slice ||
|
||||
canOverwrite ||
|
||||
(user?.userId !== undefined &&
|
||||
(slice?.owners || []).includes(user.userId)),
|
||||
onSave: actions.updateChartTitle,
|
||||
placeholder: t('Add the name of the chart'),
|
||||
label: t('Chart title'),
|
||||
}),
|
||||
[actions.updateChartTitle, canOverwrite, slice, sliceName, user?.userId],
|
||||
);
|
||||
|
||||
const certificatiedBadgeProps = useMemo(
|
||||
() => ({
|
||||
certifiedBy: slice?.certified_by,
|
||||
details: slice?.certification_details,
|
||||
}),
|
||||
[slice?.certified_by, slice?.certification_details],
|
||||
);
|
||||
|
||||
const faveStarProps = useMemo(
|
||||
() => ({
|
||||
itemId: slice?.slice_id ?? 0,
|
||||
fetchFaveStar: actions.fetchFaveStar,
|
||||
saveFaveStar: actions.saveFaveStar,
|
||||
isStarred,
|
||||
showTooltip: true,
|
||||
}),
|
||||
[actions.fetchFaveStar, actions.saveFaveStar, isStarred, slice?.slice_id],
|
||||
);
|
||||
|
||||
const titlePanelAdditionalItems = useMemo(
|
||||
() => (
|
||||
<div css={additionalItemsStyles}>
|
||||
{sliceFormData ? (
|
||||
<AlteredSliceTag
|
||||
className="altered"
|
||||
diffs={formDiffs}
|
||||
origFormData={originalFormData as QueryFormData}
|
||||
currentFormData={currentFormData as QueryFormData}
|
||||
/>
|
||||
) : null}
|
||||
{formData && isMatrixifyEnabled(formData as MatrixifyFormData) && (
|
||||
<Tag name="Matrixified" color="purple" />
|
||||
)}
|
||||
{metadataBar}
|
||||
</div>
|
||||
),
|
||||
[
|
||||
currentFormData,
|
||||
formData,
|
||||
formDiffs,
|
||||
metadataBar,
|
||||
originalFormData,
|
||||
sliceFormData,
|
||||
],
|
||||
);
|
||||
|
||||
const rightPanelAdditionalItems = useMemo(
|
||||
() => (
|
||||
<Tooltip
|
||||
title={
|
||||
saveDisabled ? t('Add required control values to save chart') : null
|
||||
}
|
||||
>
|
||||
{/* needed to wrap button in a div - antd tooltip doesn't work with disabled button */}
|
||||
<div>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={showModal}
|
||||
disabled={saveDisabled}
|
||||
data-test="query-save-button"
|
||||
css={saveButtonStyles}
|
||||
icon={<Icons.SaveOutlined />}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
),
|
||||
[saveDisabled, showModal],
|
||||
);
|
||||
|
||||
const menuDropdownProps = useMemo(
|
||||
() => ({
|
||||
open: isDropdownVisible,
|
||||
onOpenChange: setIsDropdownVisible,
|
||||
}),
|
||||
[isDropdownVisible, setIsDropdownVisible],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeaderWithActions
|
||||
editableTitleProps={{
|
||||
title: sliceName ?? '',
|
||||
canEdit:
|
||||
!slice ||
|
||||
canOverwrite ||
|
||||
(user?.userId !== undefined &&
|
||||
(slice?.owners || []).includes(user.userId)),
|
||||
onSave: actions.updateChartTitle,
|
||||
placeholder: t('Add the name of the chart'),
|
||||
label: t('Chart title'),
|
||||
}}
|
||||
editableTitleProps={editableTitleProps}
|
||||
showTitlePanelItems={!!slice}
|
||||
certificatiedBadgeProps={{
|
||||
certifiedBy: slice?.certified_by,
|
||||
details: slice?.certification_details,
|
||||
}}
|
||||
certificatiedBadgeProps={certificatiedBadgeProps}
|
||||
showFaveStar={!!user?.userId && slice?.slice_id !== undefined}
|
||||
faveStarProps={{
|
||||
itemId: slice?.slice_id ?? 0,
|
||||
fetchFaveStar: actions.fetchFaveStar,
|
||||
saveFaveStar: actions.saveFaveStar,
|
||||
isStarred,
|
||||
showTooltip: true,
|
||||
}}
|
||||
titlePanelAdditionalItems={
|
||||
<div css={additionalItemsStyles}>
|
||||
{sliceFormData ? (
|
||||
<AlteredSliceTag
|
||||
className="altered"
|
||||
diffs={formDiffs}
|
||||
origFormData={originalFormData as QueryFormData}
|
||||
currentFormData={currentFormData as QueryFormData}
|
||||
/>
|
||||
) : null}
|
||||
{formData && isMatrixifyEnabled(formData as MatrixifyFormData) && (
|
||||
<Tag name="Matrixified" color="purple" />
|
||||
)}
|
||||
{metadataBar}
|
||||
</div>
|
||||
}
|
||||
rightPanelAdditionalItems={
|
||||
<Tooltip
|
||||
title={
|
||||
saveDisabled
|
||||
? t('Add required control values to save chart')
|
||||
: null
|
||||
}
|
||||
>
|
||||
{/* needed to wrap button in a div - antd tooltip doesn't work with disabled button */}
|
||||
<div>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={showModal}
|
||||
disabled={saveDisabled}
|
||||
data-test="query-save-button"
|
||||
css={saveButtonStyles}
|
||||
icon={<Icons.SaveOutlined />}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
faveStarProps={faveStarProps}
|
||||
titlePanelAdditionalItems={titlePanelAdditionalItems}
|
||||
rightPanelAdditionalItems={rightPanelAdditionalItems}
|
||||
additionalActionsMenu={menu}
|
||||
menuDropdownProps={{
|
||||
open: isDropdownVisible,
|
||||
onOpenChange: setIsDropdownVisible,
|
||||
}}
|
||||
menuDropdownProps={menuDropdownProps}
|
||||
/>
|
||||
{isPropertiesModalOpen && (
|
||||
<PropertiesModal
|
||||
@@ -398,4 +433,4 @@ export const ExploreChartHeader: FC<ExploreChartHeaderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreChartHeader;
|
||||
export default memo(ExploreChartHeader);
|
||||
|
||||
@@ -58,7 +58,9 @@ import { ExploreAlert } from '../ExploreAlert';
|
||||
import useResizeDetectorByObserver from './useResizeDetectorByObserver';
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
const DefaultHeader: React.FC = ({ children }) => <>{children}</>;
|
||||
const DefaultHeader: React.FC<{ children?: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => <>{children}</>;
|
||||
|
||||
export interface ExploreChartPanelProps {
|
||||
actions: {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
useState,
|
||||
Dispatch,
|
||||
FC,
|
||||
ReactNode,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
|
||||
@@ -60,7 +61,7 @@ const reducer = (state: DropzoneSet = {}, action: Action) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
const ExploreContainer: FC<{}> = ({ children }) => {
|
||||
const ExploreContainer: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
const dragDropManager = useDragDropManager();
|
||||
const [dragging, setDragging] = useState(
|
||||
dragDropManager.getMonitor().isDragging(),
|
||||
|
||||
@@ -413,7 +413,10 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
||||
);
|
||||
|
||||
const addHistory = useCallback(
|
||||
async ({ isReplace = false, title } = {}) => {
|
||||
async ({
|
||||
isReplace = false,
|
||||
title,
|
||||
}: { isReplace?: boolean; title?: string } = {}) => {
|
||||
const formData = props.dashboardId
|
||||
? {
|
||||
...props.form_data,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, FC } from 'react';
|
||||
import { useEffect, FC, ReactNode } from 'react';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setStashFormData } from 'src/explore/actions/exploreActions';
|
||||
@@ -25,6 +25,7 @@ import useEffectEvent from 'src/hooks/useEffectEvent';
|
||||
type Props = {
|
||||
shouldStash: boolean;
|
||||
fieldNames: ReadonlyArray<string>;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const StashFormDataContainer: FC<Props> = ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,39 +16,48 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef, type ReactNode } from 'react';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import { List, type ListItemProps } from '@superset-ui/core/components';
|
||||
|
||||
export interface CustomListItemProps extends ListItemProps {
|
||||
selectable: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export default function CustomListItem(props: CustomListItemProps) {
|
||||
const { selectable, children, ...rest } = props;
|
||||
const theme = useTheme();
|
||||
const css: Record<string, Record<string, Record<string, number> | string>> = {
|
||||
'&.ant-list-item': {
|
||||
':first-of-type': {
|
||||
borderTopLeftRadius: theme.borderRadius,
|
||||
borderTopRightRadius: theme.borderRadius,
|
||||
const CustomListItem = forwardRef<HTMLDivElement, CustomListItemProps>(
|
||||
function CustomListItem(props, ref) {
|
||||
const { selectable, children, ...rest } = props;
|
||||
const theme = useTheme();
|
||||
const css: Record<
|
||||
string,
|
||||
Record<string, Record<string, number> | string>
|
||||
> = {
|
||||
'&.ant-list-item': {
|
||||
':first-of-type': {
|
||||
borderTopLeftRadius: theme.borderRadius,
|
||||
borderTopRightRadius: theme.borderRadius,
|
||||
},
|
||||
':last-of-type': {
|
||||
borderBottomLeftRadius: theme.borderRadius,
|
||||
borderBottomRightRadius: theme.borderRadius,
|
||||
},
|
||||
},
|
||||
':last-of-type': {
|
||||
borderBottomLeftRadius: theme.borderRadius,
|
||||
borderBottomRightRadius: theme.borderRadius,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (selectable) {
|
||||
css['&:hover'] = {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.colorFillSecondary,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Item {...rest} css={css}>
|
||||
{children}
|
||||
</List.Item>
|
||||
);
|
||||
}
|
||||
if (selectable) {
|
||||
css['&:hover'] = {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.colorFillSecondary,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Item ref={ref} {...rest} css={css}>
|
||||
{children}
|
||||
</List.Item>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default CustomListItem;
|
||||
|
||||
@@ -203,7 +203,7 @@ const ColumnSelectPopover = ({
|
||||
);
|
||||
|
||||
const onSqlExpressionChange = useCallback(
|
||||
sqlExpression => {
|
||||
(sqlExpression: string) => {
|
||||
setAdhocColumn({ label, sqlExpression, expressionType: 'SQL' });
|
||||
setSelectedSimpleColumn(undefined);
|
||||
setSelectedCalculatedColumn(undefined);
|
||||
@@ -213,7 +213,7 @@ const ColumnSelectPopover = ({
|
||||
);
|
||||
|
||||
const onCalculatedColumnChange = useCallback(
|
||||
selectedColumnName => {
|
||||
(selectedColumnName: string) => {
|
||||
const selectedColumn = calculatedColumns.find(
|
||||
col => col.column_name === selectedColumnName,
|
||||
);
|
||||
@@ -229,7 +229,7 @@ const ColumnSelectPopover = ({
|
||||
);
|
||||
|
||||
const onSimpleColumnChange = useCallback(
|
||||
selectedColumnName => {
|
||||
(selectedColumnName: string) => {
|
||||
const selectedColumn = simpleColumns.find(
|
||||
col => col.column_name === selectedColumnName,
|
||||
);
|
||||
@@ -245,7 +245,7 @@ const ColumnSelectPopover = ({
|
||||
);
|
||||
|
||||
const onSimpleMetricChange = useCallback(
|
||||
selectedMetricName => {
|
||||
(selectedMetricName: string) => {
|
||||
const selectedMetric = availableMetrics.find(
|
||||
metric => metric.metric_name === selectedMetricName,
|
||||
);
|
||||
@@ -261,7 +261,7 @@ const ColumnSelectPopover = ({
|
||||
);
|
||||
|
||||
const onSimpleItemChange = useCallback(
|
||||
selectedValue => {
|
||||
(selectedValue: string) => {
|
||||
const selectedColumn = columnMap[selectedValue];
|
||||
if (selectedColumn) {
|
||||
onSimpleColumnChange(selectedValue);
|
||||
@@ -349,7 +349,7 @@ const ColumnSelectPopover = ({
|
||||
]);
|
||||
|
||||
const onTabChange = useCallback(
|
||||
tab => {
|
||||
(tab: string) => {
|
||||
getCurrentTab(tab);
|
||||
setSelectedTab(tab);
|
||||
sqlEditorRef.current?.focus();
|
||||
|
||||
@@ -63,7 +63,7 @@ export const DndColumnSelectPopoverTitle = ({
|
||||
}, []);
|
||||
|
||||
const onInputBlur = useCallback(
|
||||
e => {
|
||||
(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (e.target.value === '') {
|
||||
onChange(e);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ const DndMetricSelect = (props: any) => {
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
opts => {
|
||||
(opts: ValueType | ValueType[] | null) => {
|
||||
// if clear out options
|
||||
if (opts === null) {
|
||||
onChange(null);
|
||||
@@ -150,7 +150,11 @@ const DndMetricSelect = (props: any) => {
|
||||
const optionValues = transformedOpts
|
||||
.map(option => {
|
||||
// pre-defined metric
|
||||
if (option.metric_name) {
|
||||
if (
|
||||
typeof option === 'object' &&
|
||||
'metric_name' in option &&
|
||||
option.metric_name
|
||||
) {
|
||||
return option.metric_name;
|
||||
}
|
||||
return option;
|
||||
@@ -263,7 +267,7 @@ const DndMetricSelect = (props: any) => {
|
||||
);
|
||||
|
||||
const getSavedMetricOptionsForMetric = useCallback(
|
||||
index =>
|
||||
(index: number) =>
|
||||
getOptionsForSavedMetrics(
|
||||
props.savedMetrics,
|
||||
props.value,
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function Option({
|
||||
}: OptionProps) {
|
||||
const theme = useTheme();
|
||||
const onClickClose = useCallback(
|
||||
e => {
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
clickClose(index);
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PureComponent } from 'react';
|
||||
import { PureComponent, ReactNode } from 'react';
|
||||
import { OptionSortType } from 'src/explore/types';
|
||||
import AdhocFilterEditPopover from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopover';
|
||||
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
@@ -37,6 +37,7 @@ interface AdhocFilterPopoverTriggerProps {
|
||||
togglePopover?: (visible: boolean) => void;
|
||||
closePopover?: () => void;
|
||||
requireSave?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface AdhocFilterPopoverTriggerState {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook, cleanup } from '@testing-library/react-hooks';
|
||||
import { renderHook, cleanup } from '@testing-library/react';
|
||||
import { TestDataset } from '@superset-ui/chart-controls';
|
||||
import { useDatePickerInAdhocFilter } from './useDatePickerInAdhocFilter';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { NO_TIME_RANGE, fetchTimeRange } from '@superset-ui/core';
|
||||
import { Operators } from 'src/explore/constants';
|
||||
import { useGetTimeRangeLabel } from './useGetTimeRangeLabel';
|
||||
@@ -80,10 +80,12 @@ test('should get actualTimeRange and title', async () => {
|
||||
clause: Clauses.Where,
|
||||
});
|
||||
|
||||
const { result } = await renderHook(() => useGetTimeRangeLabel(adhocFilter));
|
||||
expect(result.current).toEqual({
|
||||
actualTimeRange: 'MOCK TIME',
|
||||
title: 'Last week',
|
||||
const { result } = renderHook(() => useGetTimeRangeLabel(adhocFilter));
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
actualTimeRange: 'MOCK TIME',
|
||||
title: 'Last week',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,9 +100,11 @@ test('should get actualTimeRange and title when gets an error', async () => {
|
||||
clause: Clauses.Where,
|
||||
});
|
||||
|
||||
const { result } = await renderHook(() => useGetTimeRangeLabel(adhocFilter));
|
||||
expect(result.current).toEqual({
|
||||
actualTimeRange: 'temporal column (Last week)',
|
||||
title: 'MOCK ERROR',
|
||||
const { result } = renderHook(() => useGetTimeRangeLabel(adhocFilter));
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
actualTimeRange: 'temporal column (Last week)',
|
||||
title: 'MOCK ERROR',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ test('accepts an edited metric from an AdhocMetricEditPopover', async () => {
|
||||
userEvent.click(metricLabel);
|
||||
|
||||
await screen.findByText('aggregate');
|
||||
selectOption('AVG', 'Select aggregate options');
|
||||
await selectOption('AVG', 'Select aggregate options');
|
||||
|
||||
await screen.findByText('AVG(value)');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user