chore(ts): Migrate Row.jsx to Row.tsx (#36347)

Co-authored-by: Amy Li <amym1734@gmail.com>
This commit is contained in:
Shu Ying Wan
2025-12-05 18:40:34 -05:00
committed by GitHub
parent a18b62cf6b
commit 1127374edd
6 changed files with 196 additions and 131 deletions

View File

@@ -16,17 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
import { fireEvent, render } from 'spec/helpers/testing-library';
import React from 'react';
import {
fireEvent,
render,
RenderResult,
screen,
} from 'spec/helpers/testing-library';
import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown';
import IconButton from 'src/dashboard/components/IconButton';
import { DASHBOARD_GRID_ID } from 'src/dashboard/util/constants';
import { getMockStore } from 'spec/fixtures/mockStore';
import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
import { initialState } from 'src/SqlLab/fixtures';
import Row from './Row';
interface MockIntersectionObserverEntry {
isIntersecting: boolean;
}
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
isFeatureEnabled: jest.fn(() => true),
@@ -44,47 +51,84 @@ jest.mock('src/dashboard/util/isEmbedded', () => ({
}));
jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
Draggable: ({ children }) => (
<div data-test="mock-draggable">{children({})}</div>
),
Droppable: ({ children, depth }) => (
<div data-test="mock-droppable" depth={depth}>
Draggable: ({
children,
}: {
children: (args: object) => React.ReactNode;
}) => <div data-test="mock-draggable">{children({})}</div>,
Droppable: ({
children,
depth,
}: {
children: (args: object) => React.ReactNode;
depth: number;
}) => (
<div data-test="mock-droppable" data-depth={depth}>
{children({})}
</div>
),
}));
jest.mock(
'src/dashboard/containers/DashboardComponent',
() =>
({ availableColumnCount, depth }) => (
<div data-test="mock-dashboard-component" depth={depth}>
{availableColumnCount}
</div>
),
);
jest.mock(
'src/dashboard/components/menu/WithPopoverMenu',
() =>
({ children }) => <div data-test="mock-with-popover-menu">{children}</div>,
);
jest.mock('src/dashboard/containers/DashboardComponent', () => {
return ({
availableColumnCount,
depth,
}: {
availableColumnCount: number;
depth: number;
}) => (
<div data-test="mock-dashboard-component" data-depth={depth}>
{availableColumnCount}
</div>
);
});
jest.mock(
'src/dashboard/components/DeleteComponentButton',
() =>
({ onDelete }) => (
<button
type="button"
data-test="mock-delete-component-button"
onClick={onDelete}
>
Delete
</button>
),
);
jest.mock('src/dashboard/components/menu/WithPopoverMenu', () => {
return ({ children }: { children: React.ReactNode }) => (
<div data-test="mock-with-popover-menu">{children}</div>
);
});
const rowWithoutChildren = { ...mockLayout.present.ROW_ID, children: [] };
const props = {
jest.mock('src/dashboard/components/DeleteComponentButton', () => {
return ({ onDelete }: { onDelete: () => void }) => (
<button
type="button"
data-test="mock-delete-component-button"
onClick={onDelete}
>
Delete
</button>
);
});
const rowWithoutChildren = {
...mockLayout.present.ROW_ID,
children: [],
};
interface RowTestProps {
id: string;
parentId: string;
component: typeof mockLayout.present.ROW_ID;
parentComponent: (typeof mockLayout.present)[typeof DASHBOARD_GRID_ID];
index: number;
depth: number;
editMode: boolean;
availableColumnCount: number;
columnWidth: number;
occupiedColumnCount: number;
onResizeStart: () => void;
onResize: () => void;
onResizeStop: () => void;
handleComponentDrop: () => void;
deleteComponent: () => void;
updateComponents: () => void;
isComponentVisible: boolean;
maxChildrenHeight: number;
onChangeTab: () => void;
}
const props: RowTestProps = {
id: 'ROW_ID',
parentId: DASHBOARD_GRID_ID,
component: mockLayout.present.ROW_ID,
@@ -95,15 +139,18 @@ const props = {
availableColumnCount: 12,
columnWidth: 50,
occupiedColumnCount: 6,
onResizeStart() {},
onResize() {},
onResizeStop() {},
handleComponentDrop() {},
deleteComponent() {},
updateComponents() {},
onResizeStart: () => {},
onResize: () => {},
onResizeStop: () => {},
handleComponentDrop: () => {},
deleteComponent: () => {},
updateComponents: () => {},
isComponentVisible: true,
maxChildrenHeight: 0,
onChangeTab: () => {},
};
function setup(overrideProps) {
function setup(overrideProps: Partial<RowTestProps> = {}): RenderResult {
// We have to wrap provide DragDropContext for the underlying DragDroppable
// otherwise we cannot assert on DragDroppable children
const mockStore = getMockStore({
@@ -153,7 +200,7 @@ test('should render a HoverMenu in editMode', () => {
// pass the same depth of its droppable area
expect(getByTestId('mock-droppable')).toHaveAttribute(
'depth',
'data-depth',
`${props.depth}`,
);
});
@@ -167,17 +214,16 @@ test('should render a DeleteComponentButton in editMode', () => {
});
test.skip('should render a BackgroundStyleDropdown when focused', () => {
let wrapper = setup({ component: rowWithoutChildren });
expect(wrapper.find(BackgroundStyleDropdown)).toBeFalsy();
let { rerender } = setup({ component: rowWithoutChildren });
expect(screen.queryByTestId('background-style-dropdown')).toBeFalsy();
// we cannot set props on the Row because of the WithDragDropContext wrapper
wrapper = setup({ component: rowWithoutChildren, editMode: true });
wrapper
.find(IconButton)
.at(1) // first one is delete button
.simulate('click');
rerender(<Row {...props} component={rowWithoutChildren} editMode={true} />);
const buttons = screen.getAllByRole('button');
const settingsButton = buttons[1];
fireEvent.click(settingsButton);
expect(wrapper.find(BackgroundStyleDropdown)).toBeTruthy();
expect(screen.queryByTestId('background-style-dropdown')).toBeTruthy();
});
test('should call deleteComponent when deleted', () => {
@@ -190,14 +236,14 @@ test('should call deleteComponent when deleted', () => {
test('should pass appropriate availableColumnCount to children', () => {
const { getByTestId } = setup();
expect(getByTestId('mock-dashboard-component')).toHaveTextContent(
props.availableColumnCount - props.occupiedColumnCount,
`${props.availableColumnCount - props.occupiedColumnCount}`,
);
});
test('should increment the depth of its children', () => {
const { getByTestId } = setup();
expect(getByTestId('mock-dashboard-component')).toHaveAttribute(
'depth',
'data-depth',
`${props.depth + 1}`,
);
});
@@ -222,7 +268,7 @@ describe('visibility handling for intersection observers', () => {
});
afterAll(() => {
delete window.IntersectionObserver;
delete (window as any).IntersectionObserver;
});
test('should handle visibility prop changes without crashing', () => {
@@ -261,7 +307,7 @@ describe('visibility handling for intersection observers', () => {
});
test('intersection observer callbacks handle entries without errors', () => {
const callback = ([entry]) => {
const callback = ([entry]: [MockIntersectionObserverEntry]) => {
if (entry.isIntersecting) return true;
return false;

View File

@@ -24,13 +24,17 @@ import {
useEffect,
useMemo,
memo,
RefObject,
} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import { css, styled } from '@apache-superset/core/ui';
import {
FeatureFlag,
isFeatureEnabled,
t,
JsonObject,
} from '@superset-ui/core';
import { css, styled, SupersetTheme } from '@apache-superset/core/ui';
import { Icons, Constants } from '@superset-ui/core/components';
import {
Draggable,
Droppable,
@@ -42,7 +46,6 @@ import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
import IconButton from 'src/dashboard/components/IconButton';
import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown';
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import { componentShape } from 'src/dashboard/util/propShapes';
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
@@ -50,31 +53,36 @@ import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
import { isCurrentUserBot } from 'src/utils/isBot';
import { useDebouncedEffect } from '../../../../explore/exploreUtils';
const propTypes = {
id: PropTypes.string.isRequired,
parentId: PropTypes.string.isRequired,
component: componentShape.isRequired,
parentComponent: componentShape.isRequired,
index: PropTypes.number.isRequired,
depth: PropTypes.number.isRequired,
editMode: PropTypes.bool.isRequired,
export type RowProps = {
id: string;
parentId: string;
component: JsonObject;
parentComponent: JsonObject;
index: number;
depth: number;
editMode: boolean;
// grid related
availableColumnCount: PropTypes.number.isRequired,
columnWidth: PropTypes.number.isRequired,
occupiedColumnCount: PropTypes.number.isRequired,
onResizeStart: PropTypes.func.isRequired,
onResize: PropTypes.func.isRequired,
onResizeStop: PropTypes.func.isRequired,
maxChildrenHeight: PropTypes.number.isRequired,
availableColumnCount: number;
columnWidth: number;
occupiedColumnCount: number;
maxChildrenHeight: number;
onResizeStart: (e: unknown, direction: unknown) => void;
onResize: (e: unknown, direction: unknown, ref: HTMLElement) => void;
onResizeStop: (e: unknown, direction: unknown, ref: HTMLElement) => void;
// dnd
handleComponentDrop: PropTypes.func.isRequired,
deleteComponent: PropTypes.func.isRequired,
updateComponents: PropTypes.func.isRequired,
handleComponentDrop: (dropResult: unknown) => void;
deleteComponent: (id: string, parentId: string) => void;
updateComponents: (updates: Record<string, JsonObject>) => void;
// visibility
isComponentVisible: boolean;
onChangeTab: (tabId: string) => void;
};
const GridRow = styled.div`
const GridRow = styled.div<{ editMode: boolean }>`
${({ theme, editMode }) => css`
position: relative;
display: flex;
@@ -119,7 +127,7 @@ const GridRow = styled.div`
`}
`;
const emptyRowContentStyles = theme => css`
const emptyRowContentStyles = (theme: SupersetTheme) => css`
position: absolute;
width: 100%;
height: 100%;
@@ -129,7 +137,7 @@ const emptyRowContentStyles = theme => css`
color: ${theme.colorTextLabel};
`;
const Row = props => {
const Row = memo((props: RowProps) => {
const {
component: rowComponent,
parentComponent,
@@ -153,8 +161,8 @@ const Row = props => {
const [isFocused, setIsFocused] = useState(false);
const [isInView, setIsInView] = useState(false);
const [hoverMenuHovered, setHoverMenuHovered] = useState(false);
const [containerHeight, setContainerHeight] = useState(null);
const containerRef = useRef();
const [containerHeight, setContainerHeight] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const isComponentVisibleRef = useRef(isComponentVisible);
useEffect(() => {
@@ -164,8 +172,8 @@ const Row = props => {
// if chart not rendered - render it if it's less than 1 view height away from current viewport
// if chart rendered - remove it if it's more than 4 view heights away from current viewport
useEffect(() => {
let observerEnabler;
let observerDisabler;
let observerEnabler: IntersectionObserver | undefined;
let observerDisabler: IntersectionObserver | undefined;
if (
isFeatureEnabled(FeatureFlag.DashboardVirtualization) &&
@@ -219,23 +227,23 @@ const Row = props => {
containerRef.current &&
updatedHeight !== containerHeight
) {
setContainerHeight(updatedHeight);
setContainerHeight(updatedHeight ?? null);
}
},
Constants.FAST_DEBOUNCE,
[editMode, containerHeight],
);
const handleChangeFocus = useCallback(nextFocus => {
const handleChangeFocus = useCallback((nextFocus: boolean) => {
setIsFocused(Boolean(nextFocus));
}, []);
const handleChangeBackground = useCallback(
nextValue => {
(nextValue: string) => {
const metaKey = 'background';
if (nextValue && rowComponent.meta[metaKey] !== nextValue) {
if (nextValue && rowComponent.meta?.[metaKey] !== nextValue) {
updateComponents({
[rowComponent.id]: {
[rowComponent.id as string]: {
...rowComponent,
meta: {
...rowComponent.meta,
@@ -249,26 +257,30 @@ const Row = props => {
);
const handleDeleteComponent = useCallback(() => {
deleteComponent(rowComponent.id, parentId);
deleteComponent(rowComponent.id as string, parentId);
}, [deleteComponent, rowComponent, parentId]);
const handleMenuHover = useCallback(hovered => {
const { isHovered } = hovered;
setHoverMenuHovered(isHovered);
const handleMenuHover = useCallback((hover: { isHovered: boolean }) => {
setHoverMenuHovered(hover.isHovered);
}, []);
const rowItems = useMemo(
() => rowComponent.children || [],
const rowItems: string[] = useMemo(
() =>
Array.isArray(rowComponent.children)
? (rowComponent.children as string[])
: [],
[rowComponent.children],
);
const backgroundStyle = backgroundStyleOptions.find(
opt =>
opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
);
const backgroundStyle =
backgroundStyleOptions.find(
opt =>
opt.value === (rowComponent.meta?.background ?? BACKGROUND_TRANSPARENT),
) ?? backgroundStyleOptions[0];
const remainColumnCount = availableColumnCount - occupiedColumnCount;
const renderChild = useCallback(
({ dragSourceRef }) => (
({ dragSourceRef }: { dragSourceRef: RefObject<HTMLDivElement> }) => (
<WithPopoverMenu
isFocused={isFocused}
onChangeFocus={handleChangeFocus}
@@ -291,7 +303,7 @@ const Row = props => {
<DragHandle position="left" />
<DeleteComponentButton onDelete={handleDeleteComponent} />
<IconButton
onClick={handleChangeFocus}
onClick={() => handleChangeFocus(true)}
icon={<Icons.SettingOutlined iconSize="l" />}
/>
</HoverMenu>
@@ -334,13 +346,13 @@ const Row = props => {
...(rowItems.length > 0 && { width: 16 }),
}}
>
{({ dropIndicatorProps }) =>
{({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) =>
dropIndicatorProps && <div {...dropIndicatorProps} />
}
</Droppable>
)}
{rowItems.length === 0 && (
<div css={emptyRowContentStyles}>{t('Empty row')}</div>
<div css={emptyRowContentStyles as any}>{t('Empty row')}</div>
)}
{rowItems.length > 0 &&
rowItems.map((componentId, itemIndex) => (
@@ -348,7 +360,7 @@ const Row = props => {
<DashboardComponent
key={componentId}
id={componentId}
parentId={rowComponent.id}
parentId={rowComponent.id as string}
depth={depth + 1}
index={itemIndex}
availableColumnCount={remainColumnCount}
@@ -382,9 +394,11 @@ const Row = props => {
itemIndex === rowItems.length - 1 && { width: 16 }),
}}
>
{({ dropIndicatorProps }) =>
dropIndicatorProps && <div {...dropIndicatorProps} />
}
{({
dropIndicatorProps,
}: {
dropIndicatorProps: JsonObject;
}) => dropIndicatorProps && <div {...dropIndicatorProps} />}
</Droppable>
)}
</Fragment>
@@ -431,8 +445,6 @@ const Row = props => {
{renderChild}
</Draggable>
);
};
});
Row.propTypes = propTypes;
export default memo(Row);
export default Row;

View File

@@ -16,6 +16,4 @@
* specific language governing permissions and limitations
* under the License.
*/
import Row from './Row';
export default Row;
export { default } from './Row';

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render } from 'spec/helpers/testing-library';
import { render, RenderResult } from 'spec/helpers/testing-library';
import NewRow from 'src/dashboard/components/gridComponents/new/NewRow';
@@ -26,12 +26,12 @@ import { ROW_TYPE } from 'src/dashboard/util/componentTypes';
jest.mock(
'src/dashboard/components/gridComponents/new/DraggableNewComponent',
() =>
({ type, id }) => (
({ type, id }: { type: string; id: string }) => (
<div data-test="mock-draggable-new-component">{`${type}:${id}`}</div>
),
);
function setup() {
function setup(): RenderResult {
return render(<NewRow />);
}

View File

@@ -22,14 +22,17 @@ import { Icons } from '@superset-ui/core/components';
import { ROW_TYPE } from '../../../util/componentTypes';
import { NEW_ROW_ID } from '../../../util/constants';
import DraggableNewComponent from './DraggableNewComponent';
import { FC } from 'react';
export default function DraggableNewRow() {
return (
<DraggableNewComponent
id={NEW_ROW_ID}
type={ROW_TYPE}
label={t('Row')}
IconComponent={Icons.ColumnHeightOutlined}
/>
);
}
type DraggableNewRowProps = {};
const DraggableNewRow: FC<DraggableNewRowProps> = () => (
<DraggableNewComponent
id={NEW_ROW_ID}
type={ROW_TYPE}
label={t('Row')}
IconComponent={Icons.ColumnHeightOutlined}
/>
);
export default DraggableNewRow;

View File

@@ -49,6 +49,12 @@ const propTypes = {
directPathToChild: PropTypes.arrayOf(PropTypes.string),
directPathLastUpdated: PropTypes.number,
isComponentVisible: PropTypes.bool,
availableColumnCount: PropTypes.number,
columnWidth: PropTypes.number,
onResizeStart: PropTypes.func,
onResize: PropTypes.func,
onResizeStop: PropTypes.func,
isInView: PropTypes.bool,
};
const DashboardComponent = props => {