You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2024/03/04 16:37:35 UTC
(superset) 03/28: fix(dashboard): drag and drop indicator UX (#26699)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 4.0
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 3440a301baf1dc205b87f3f4f8f4502a48846b44
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Wed Feb 21 11:29:34 2024 -0800
fix(dashboard): drag and drop indicator UX (#26699)
(cherry picked from commit ac8c283df04b6c4cbc24a5ae625e05a8f2679802)
---
.../DashboardBuilder/DashboardBuilder.tsx | 53 ++++---
.../DashboardBuilder/DashboardWrapper.test.tsx | 18 ++-
.../DashboardBuilder/DashboardWrapper.tsx | 33 +++-
.../src/dashboard/components/DashboardGrid.jsx | 115 ++++++++------
.../src/dashboard/components/dnd/DragDroppable.jsx | 56 ++-----
.../src/dashboard/components/dnd/handleDrop.js | 32 ++--
.../components/gridComponents/ChartHolder.tsx | 9 +-
.../dashboard/components/gridComponents/Column.jsx | 118 +++++++++++---
.../components/gridComponents/Column.test.jsx | 23 ++-
.../components/gridComponents/Divider.jsx | 11 +-
.../components/gridComponents/Divider.test.jsx | 6 +-
.../components/gridComponents/DynamicComponent.tsx | 9 +-
.../dashboard/components/gridComponents/Header.jsx | 10 +-
.../components/gridComponents/Header.test.jsx | 6 +-
.../components/gridComponents/Markdown.jsx | 9 +-
.../components/gridComponents/Markdown.test.jsx | 8 +-
.../dashboard/components/gridComponents/Row.jsx | 175 +++++++++++++++++----
.../components/gridComponents/Row.test.jsx | 23 ++-
.../dashboard/components/gridComponents/Tab.jsx | 93 ++++++-----
.../components/gridComponents/Tab.test.tsx | 59 ++++---
.../dashboard/components/gridComponents/Tabs.jsx | 19 +--
.../components/gridComponents/Tabs.test.jsx | 14 +-
.../components/gridComponents/Tabs.test.tsx | 12 +-
superset-frontend/src/dashboard/constants.ts | 1 +
.../src/dashboard/util/getDropPosition.js | 3 +-
.../src/dashboard/util/getDropPosition.test.js | 5 +-
26 files changed, 585 insertions(+), 335 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 2756e71359..e86d924b21 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -44,7 +44,7 @@ import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'
import DashboardHeader from 'src/dashboard/containers/DashboardHeader';
import Icons from 'src/components/Icons';
import IconButton from 'src/dashboard/components/IconButton';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Droppable } from 'src/dashboard/components/dnd/DragDroppable';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex';
@@ -81,6 +81,7 @@ import {
MAIN_HEADER_HEIGHT,
OPEN_FILTER_BAR_MAX_WIDTH,
OPEN_FILTER_BAR_WIDTH,
+ EMPTY_CONTAINER_Z_INDEX,
} from 'src/dashboard/constants';
import { getRootLevelTabsComponent, shouldFocusTabs } from './utils';
import DashboardContainer from './DashboardContainer';
@@ -107,12 +108,27 @@ const StickyPanel = styled.div<{ width: number }>`
// @z-index-above-dashboard-popovers (99) + 1 = 100
const StyledHeader = styled.div`
- grid-column: 2;
- grid-row: 1;
- position: sticky;
- top: 0;
- z-index: 100;
- max-width: 100vw;
+ ${({ theme }) => css`
+ grid-column: 2;
+ grid-row: 1;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ max-width: 100vw;
+
+ .empty-droptarget:before {
+ position: absolute;
+ content: '';
+ display: none;
+ width: calc(100% - ${theme.gridUnit * 2}px);
+ height: calc(100% - ${theme.gridUnit * 2}px);
+ left: ${theme.gridUnit}px;
+ top: ${theme.gridUnit}px;
+ border: 1px dashed transparent;
+ border-radius: ${theme.gridUnit}px;
+ opacity: 0.5;
+ }
+ `}
`;
const StyledContent = styled.div<{
@@ -211,13 +227,9 @@ const DashboardContentWrapper = styled.div`
/* provide hit area in case row contents is edge to edge */
.dashboard-component-tabs-content {
- .dragdroppable-row {
+ > .dragdroppable-row {
padding-top: ${theme.gridUnit * 4}px;
}
-
- & > div:not(:last-child):not(.empty-droptarget) {
- margin-bottom: ${theme.gridUnit * 4}px;
- }
}
.dashboard-component-chart-holder {
@@ -250,25 +262,21 @@ const DashboardContentWrapper = styled.div`
}
& > .empty-droptarget {
+ z-index: ${EMPTY_CONTAINER_Z_INDEX};
position: absolute;
width: 100%;
}
& > .empty-droptarget:first-child:not(.empty-droptarget--full) {
height: ${theme.gridUnit * 4}px;
- top: -2px;
- z-index: 10;
+ top: 0;
}
& > .empty-droptarget:last-child {
- height: ${theme.gridUnit * 3}px;
- bottom: 0;
+ height: ${theme.gridUnit * 4}px;
+ bottom: ${-theme.gridUnit * 4}px;
}
}
-
- .empty-droptarget:first-child .drop-indicator--bottom {
- top: ${theme.gridUnit * 6}px;
- }
`}
`;
@@ -616,8 +624,9 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
)}
<StyledHeader ref={headerRef}>
{/* @ts-ignore */}
- <DragDroppable
+ <Droppable
data-test="top-level-tabs"
+ className={cx(!topLevelTabs && editMode && 'empty-droptarget')}
component={dashboardRoot}
parentComponent={null}
depth={DASHBOARD_ROOT_DEPTH}
@@ -630,7 +639,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
style={draggableStyle}
>
{renderDraggableContent}
- </DragDroppable>
+ </Droppable>
</StyledHeader>
<StyledContent fullSizeChartId={fullSizeChartId}>
<Global
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx
index fb913b4627..add3b482fd 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx
@@ -17,11 +17,19 @@
* under the License.
*/
import React from 'react';
-import { fireEvent, render } from 'spec/helpers/testing-library';
+import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
import { OptionControlLabel } from 'src/explore/components/controls/OptionControls';
import DashboardWrapper from './DashboardWrapper';
+beforeAll(() => {
+ jest.useFakeTimers();
+});
+
+afterAll(() => {
+ jest.useRealTimers();
+});
+
test('should render children', () => {
const { getByTestId } = render(
<DashboardWrapper>
@@ -32,7 +40,7 @@ test('should render children', () => {
expect(getByTestId('mock-children')).toBeInTheDocument();
});
-test('should update the style on dragging state', () => {
+test('should update the style on dragging state', async () => {
const defaultProps = {
label: <span>Test label</span>,
tooltipTitle: 'This is a tooltip title',
@@ -69,7 +77,13 @@ test('should update the style on dragging state', () => {
container.getElementsByClassName('dragdroppable--dragging'),
).toHaveLength(0);
fireEvent.dragStart(getByText('Label 1'));
+ await waitFor(() => jest.runAllTimers());
expect(
container.getElementsByClassName('dragdroppable--dragging'),
).toHaveLength(1);
+ fireEvent.dragEnd(getByText('Label 1'));
+ // immediately discards dragging state after dragEnd
+ expect(
+ container.getElementsByClassName('dragdroppable--dragging'),
+ ).toHaveLength(0);
});
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
index 5bb193de1b..2bcdc5b9cd 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
@@ -17,11 +17,12 @@
* under the License.
*/
import React, { useEffect } from 'react';
-import { css, styled } from '@superset-ui/core';
+import { FAST_DEBOUNCE, css, styled } from '@superset-ui/core';
import { RootState } from 'src/dashboard/types';
import { useSelector } from 'react-redux';
import { useDragDropManager } from 'react-dnd';
import classNames from 'classnames';
+import { debounce } from 'lodash';
const StyledDiv = styled.div`
${({ theme }) => css`
@@ -32,10 +33,20 @@ const StyledDiv = styled.div`
flex: 1;
/* Special cases */
- &.dragdroppable--dragging
- .dashboard-component-tabs-content
- > .empty-droptarget.empty-droptarget--full {
- height: 100%;
+ &.dragdroppable--dragging {
+ &
+ .dashboard-component-tabs-content
+ > .empty-droptarget.empty-droptarget--full {
+ height: 100%;
+ }
+ & .empty-droptarget:before {
+ display: block;
+ border-color: ${theme.colors.primary.light1};
+ background-color: ${theme.colors.primary.light3};
+ }
+ & .grid-row:after {
+ border-style: hidden;
+ }
}
/* A row within a column has inset hover menu */
@@ -106,12 +117,22 @@ const DashboardWrapper: React.FC<Props> = ({ children }) => {
useEffect(() => {
const monitor = dragDropManager.getMonitor();
+ const debouncedSetIsDragged = debounce(setIsDragged, FAST_DEBOUNCE);
const unsub = monitor.subscribeToStateChange(() => {
- setIsDragged(monitor.isDragging());
+ const isDragging = monitor.isDragging();
+ if (isDragging) {
+ // set a debounced function to prevent HTML5 drag source
+ // from interfering with the drop zone highlighting
+ debouncedSetIsDragged(true);
+ } else {
+ debouncedSetIsDragged.cancel();
+ setIsDragged(false);
+ }
});
return () => {
unsub();
+ debouncedSetIsDragged.cancel();
};
}, [dragDropManager]);
diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
index 70cf65218f..93444c7423 100644
--- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx
+++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
@@ -23,7 +23,7 @@ import { addAlpha, css, styled, t } from '@superset-ui/core';
import { EmptyStateBig } from 'src/components/EmptyState';
import { componentShape } from '../util/propShapes';
import DashboardComponent from '../containers/DashboardComponent';
-import DragDroppable from './dnd/DragDroppable';
+import { Droppable } from './dnd/DragDroppable';
import { GRID_GUTTER_SIZE, GRID_COLUMN_COUNT } from '../util/constants';
import { TAB_TYPE } from '../util/componentTypes';
@@ -41,15 +41,8 @@ const propTypes = {
const defaultProps = {};
-const renderDraggableContentBottom = dropProps =>
- dropProps.dropIndicatorProps && (
- <div className="drop-indicator drop-indicator--bottom" />
- );
-
-const renderDraggableContentTop = dropProps =>
- dropProps.dropIndicatorProps && (
- <div className="drop-indicator drop-indicator--top" />
- );
+const renderDraggableContent = dropProps =>
+ dropProps.dropIndicatorProps && <div {...dropProps.dropIndicatorProps} />;
const DashboardEmptyStateContainer = styled.div`
position: absolute;
@@ -60,28 +53,42 @@ const DashboardEmptyStateContainer = styled.div`
`;
const GridContent = styled.div`
- ${({ theme }) => css`
+ ${({ theme, editMode }) => css`
display: flex;
flex-direction: column;
/* gutters between rows */
& > div:not(:last-child):not(.empty-droptarget) {
- margin-bottom: ${theme.gridUnit * 4}px;
+ ${!editMode && `margin-bottom: ${theme.gridUnit * 4}px`};
}
- & > .empty-droptarget {
+ .empty-droptarget {
width: 100%;
- height: 100%;
+ height: ${theme.gridUnit * 4}px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: ${theme.gridUnit}px;
+ overflow: hidden;
+
+ &:before {
+ content: '';
+ display: block;
+ width: calc(100% - ${theme.gridUnit * 2}px);
+ height: calc(100% - ${theme.gridUnit * 2}px);
+ border: 1px dashed transparent;
+ border-radius: ${theme.gridUnit}px;
+ opacity: 0.5;
+ }
}
& > .empty-droptarget:first-child {
- height: ${theme.gridUnit * 12}px;
- margin-top: ${theme.gridUnit * -6}px;
+ height: ${theme.gridUnit * 4}px;
+ margin-top: ${theme.gridUnit * -4}px;
}
& > .empty-droptarget:last-child {
- height: ${theme.gridUnit * 12}px;
- margin-top: ${theme.gridUnit * -6}px;
+ height: ${theme.gridUnit * 24}px;
}
& > .empty-droptarget.empty-droptarget--full:only-child {
@@ -265,10 +272,14 @@ class DashboardGrid extends React.PureComponent {
</DashboardEmptyStateContainer>
)}
<div className="dashboard-grid" ref={this.setGridRef}>
- <GridContent className="grid-content" data-test="grid-content">
+ <GridContent
+ className="grid-content"
+ data-test="grid-content"
+ editMode={editMode}
+ >
{/* make the area above components droppable */}
{editMode && (
- <DragDroppable
+ <Droppable
component={gridComponent}
depth={depth}
parentComponent={null}
@@ -281,41 +292,43 @@ class DashboardGrid extends React.PureComponent {
gridComponent?.children?.length === 0,
})}
editMode
+ dropToChild={gridComponent?.children?.length === 0}
>
- {renderDraggableContentTop}
- </DragDroppable>
+ {renderDraggableContent}
+ </Droppable>
)}
{gridComponent?.children?.map((id, index) => (
- <DashboardComponent
- key={id}
- id={id}
- parentId={gridComponent.id}
- depth={depth + 1}
- index={index}
- availableColumnCount={GRID_COLUMN_COUNT}
- columnWidth={columnWidth}
- isComponentVisible={isComponentVisible}
- onResizeStart={this.handleResizeStart}
- onResize={this.handleResize}
- onResizeStop={this.handleResizeStop}
- onChangeTab={this.handleChangeTab}
- />
+ <React.Fragment key={id}>
+ <DashboardComponent
+ id={id}
+ parentId={gridComponent.id}
+ depth={depth + 1}
+ index={index}
+ availableColumnCount={GRID_COLUMN_COUNT}
+ columnWidth={columnWidth}
+ isComponentVisible={isComponentVisible}
+ onResizeStart={this.handleResizeStart}
+ onResize={this.handleResize}
+ onResizeStop={this.handleResizeStop}
+ onChangeTab={this.handleChangeTab}
+ />
+ {/* make the area below components droppable */}
+ {editMode && (
+ <Droppable
+ component={gridComponent}
+ depth={depth}
+ parentComponent={null}
+ index={index + 1}
+ orientation="column"
+ onDrop={handleComponentDrop}
+ className="empty-droptarget"
+ editMode
+ >
+ {renderDraggableContent}
+ </Droppable>
+ )}
+ </React.Fragment>
))}
- {/* make the area below components droppable */}
- {editMode && gridComponent?.children?.length > 0 && (
- <DragDroppable
- component={gridComponent}
- depth={depth}
- parentComponent={null}
- index={gridComponent.children.length}
- orientation="column"
- onDrop={handleComponentDrop}
- className="empty-droptarget"
- editMode
- >
- {renderDraggableContentBottom}
- </DragDroppable>
- )}
{isResizing &&
Array(GRID_COLUMN_COUNT)
.fill(null)
diff --git a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx
index 6a49f98875..185b4c08c6 100644
--- a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx
+++ b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx
@@ -25,12 +25,7 @@ import { css, styled } from '@superset-ui/core';
import { componentShape } from '../../util/propShapes';
import { dragConfig, dropConfig } from './dragDroppableConfig';
-import {
- DROP_TOP,
- DROP_RIGHT,
- DROP_BOTTOM,
- DROP_LEFT,
-} from '../../util/getDropPosition';
+import { DROP_FORBIDDEN } from '../../util/getDropPosition';
const propTypes = {
children: PropTypes.func,
@@ -39,6 +34,7 @@ const propTypes = {
parentComponent: componentShape,
depth: PropTypes.number.isRequired,
disableDragDrop: PropTypes.bool,
+ dropToChild: PropTypes.bool,
orientation: PropTypes.oneOf(['row', 'column']),
index: PropTypes.number.isRequired,
style: PropTypes.object,
@@ -61,6 +57,7 @@ const defaultProps = {
style: null,
parentComponent: null,
disableDragDrop: false,
+ dropToChild: false,
children() {},
onDrop() {},
onHover() {},
@@ -90,49 +87,18 @@ const DragDroppableStyles = styled.div`
z-index: 10;
}
- &.empty-droptarget--full > .drop-indicator--top {
- height: 100%;
- opacity: 0.3;
- }
-
& {
.drop-indicator {
display: block;
background-color: ${theme.colors.primary.base};
position: absolute;
z-index: 10;
- }
-
- .drop-indicator--top {
- top: ${-theme.gridUnit - 2}px;
- left: 0;
- height: ${theme.gridUnit}px;
- width: 100%;
- min-width: ${theme.gridUnit * 4}px;
- }
-
- .drop-indicator--bottom {
- bottom: ${-theme.gridUnit - 2}px;
- left: 0;
- height: ${theme.gridUnit}px;
+ opacity: 0.3;
width: 100%;
- min-width: ${theme.gridUnit * 4}px;
- }
-
- .drop-indicator--right {
- top: 0;
- left: calc(100% - ${theme.gridUnit}px);
height: 100%;
- width: ${theme.gridUnit}px;
- min-height: ${theme.gridUnit * 4}px;
- }
-
- .drop-indicator--left {
- top: 0;
- left: 0;
- height: 100%;
- width: ${theme.gridUnit}px;
- min-height: ${theme.gridUnit * 4}px;
+ &.drop-indicator--forbidden {
+ background-color: ${theme.colors.error.light1};
+ }
}
}
`};
@@ -189,10 +155,7 @@ export class UnwrappedDragDroppable extends React.PureComponent {
? {
className: cx(
'drop-indicator',
- dropIndicator === DROP_TOP && 'drop-indicator--top',
- dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
- dropIndicator === DROP_LEFT && 'drop-indicator--left',
- dropIndicator === DROP_RIGHT && 'drop-indicator--right',
+ dropIndicator === DROP_FORBIDDEN && 'drop-indicator--forbidden',
),
}
: null;
@@ -226,6 +189,9 @@ export class UnwrappedDragDroppable extends React.PureComponent {
UnwrappedDragDroppable.propTypes = propTypes;
UnwrappedDragDroppable.defaultProps = defaultProps;
+export const Draggable = DragSource(...dragConfig)(UnwrappedDragDroppable);
+export const Droppable = DropTarget(...dropConfig)(UnwrappedDragDroppable);
+
// note that the composition order here determines using
// component.method() vs decoratedComponentInstance.method() in the drag/drop config
export default DragSource(...dragConfig)(
diff --git a/superset-frontend/src/dashboard/components/dnd/handleDrop.js b/superset-frontend/src/dashboard/components/dnd/handleDrop.js
index 450a60867c..6d50d39c45 100644
--- a/superset-frontend/src/dashboard/components/dnd/handleDrop.js
+++ b/superset-frontend/src/dashboard/components/dnd/handleDrop.js
@@ -18,10 +18,7 @@
*/
import getDropPosition, {
clearDropCache,
- DROP_TOP,
- DROP_RIGHT,
- DROP_BOTTOM,
- DROP_LEFT,
+ DROP_FORBIDDEN,
} from '../../util/getDropPosition';
export default function handleDrop(props, monitor, Component) {
@@ -31,7 +28,7 @@ export default function handleDrop(props, monitor, Component) {
Component.setState(() => ({ dropIndicator: null }));
const dropPosition = getDropPosition(monitor, Component);
- if (!dropPosition) {
+ if (!dropPosition || dropPosition === DROP_FORBIDDEN) {
return undefined;
}
@@ -40,19 +37,11 @@ export default function handleDrop(props, monitor, Component) {
component,
index: componentIndex,
onDrop,
- orientation,
+ dropToChild,
} = Component.props;
const draggingItem = monitor.getItem();
- const dropAsChildOrSibling =
- (orientation === 'row' &&
- (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) ||
- (orientation === 'column' &&
- (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT))
- ? 'sibling'
- : 'child';
-
const dropResult = {
source: {
id: draggingItem.parentId,
@@ -67,12 +56,18 @@ export default function handleDrop(props, monitor, Component) {
};
// simplest case, append as child
- if (dropAsChildOrSibling === 'child') {
+ if (dropToChild) {
dropResult.destination = {
id: component.id,
type: component.type,
index: component.children.length,
};
+ } else if (!parentComponent) {
+ dropResult.destination = {
+ id: component.id,
+ type: component.type,
+ index: componentIndex,
+ };
} else {
// if the item is in the same list with a smaller index, you must account for the
// "missing" index upon movement within the list
@@ -81,10 +76,9 @@ export default function handleDrop(props, monitor, Component) {
const sameParentLowerIndex =
sameParent && draggingItem.index < componentIndex;
- let nextIndex = sameParentLowerIndex ? componentIndex - 1 : componentIndex;
- if (dropPosition === DROP_BOTTOM || dropPosition === DROP_RIGHT) {
- nextIndex += 1;
- }
+ const nextIndex = sameParentLowerIndex
+ ? componentIndex - 1
+ : componentIndex;
dropResult.destination = {
id: parentComponent.id,
diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
index a93f3b0b8d..bcc58d0691 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
@@ -25,7 +25,7 @@ import { LayoutItem, RootState } from 'src/dashboard/types';
import AnchorLink from 'src/dashboard/components/AnchorLink';
import Chart from 'src/dashboard/containers/Chart';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
import ResizableContainer from 'src/dashboard/components/resizable/ResizableContainer';
import getChartAndLabelComponentIdFromPath from 'src/dashboard/util/getChartAndLabelComponentIdFromPath';
@@ -243,7 +243,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
}, []);
return (
- <DragDroppable
+ <Draggable
component={component}
parentComponent={parentComponent}
orientation={parentComponent.type === ROW_TYPE ? 'column' : 'row'}
@@ -253,7 +253,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
disableDragDrop={false}
editMode={editMode}
>
- {({ dropIndicatorProps, dragSourceRef }) => (
+ {({ dragSourceRef }) => (
<ResizableContainer
id={component.id}
adjustableWidth={parentComponent.type === ROW_TYPE}
@@ -324,10 +324,9 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
</HoverMenu>
)}
</div>
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
</ResizableContainer>
)}
- </DragDroppable>
+ </Draggable>
);
};
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx
index 1883531404..3511c54a99 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx
@@ -23,7 +23,10 @@ import { css, styled, t } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import {
+ Draggable,
+ Droppable,
+} from 'src/dashboard/components/dnd/DragDroppable';
import DragHandle from 'src/dashboard/components/dnd/DragHandle';
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
import IconButton from 'src/dashboard/components/IconButton';
@@ -33,6 +36,7 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import { componentShape } from 'src/dashboard/util/propShapes';
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
+import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
const propTypes = {
id: PropTypes.string.isRequired,
@@ -60,13 +64,13 @@ const propTypes = {
const defaultProps = {};
const ColumnStyles = styled.div`
- ${({ theme }) => css`
+ ${({ theme, editMode }) => css`
&.grid-column {
width: 100%;
position: relative;
& > :not(.hover-menu):not(:last-child) {
- margin-bottom: ${theme.gridUnit * 4}px;
+ ${!editMode && `margin-bottom: ${theme.gridUnit * 4}px;`}
}
}
@@ -86,6 +90,25 @@ const ColumnStyles = styled.div`
border: 1px dashed ${theme.colors.primary.base};
z-index: 2;
}
+
+ & .empty-droptarget {
+ &.droptarget-edge {
+ position: absolute;
+ z-index: ${EMPTY_CONTAINER_Z_INDEX};
+ &:first-child {
+ inset-block-start: 0;
+ }
+ &:last-child {
+ inset-block-end: 0;
+ }
+ }
+ &:first-child:not(.droptarget-edge) {
+ position: absolute;
+ z-index: ${EMPTY_CONTAINER_Z_INDEX};
+ width: 100%;
+ height: 100%;
+ }
+ }
`}
`;
@@ -163,7 +186,7 @@ class Column extends React.PureComponent {
);
return (
- <DragDroppable
+ <Draggable
component={columnComponent}
parentComponent={parentComponent}
orientation="column"
@@ -172,7 +195,7 @@ class Column extends React.PureComponent {
onDrop={handleComponentDrop}
editMode={editMode}
>
- {({ dropIndicatorProps, dragSourceRef }) => (
+ {({ dragSourceRef }) => (
<ResizableContainer
id={columnComponent.id}
adjustableWidth
@@ -215,34 +238,85 @@ class Column extends React.PureComponent {
)}
<ColumnStyles
className={cx('grid-column', backgroundStyle.className)}
+ editMode={editMode}
>
+ {editMode && (
+ <Droppable
+ component={columnComponent}
+ parentComponent={parentComponent}
+ {...(columnItems.length === 0
+ ? {
+ component: columnComponent,
+ parentComponent,
+ dropToChild: true,
+ }
+ : {
+ component: columnItems,
+ parentComponent: columnComponent,
+ })}
+ depth={depth + 1}
+ index={0}
+ orientation="column"
+ onDrop={handleComponentDrop}
+ className={cx(
+ 'empty-droptarget',
+ columnItems.length > 0 && 'droptarget-edge',
+ )}
+ editMode
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && <div {...dropIndicatorProps} />
+ }
+ </Droppable>
+ )}
{columnItems.length === 0 ? (
<div css={emptyColumnContentStyles}>{t('Empty column')}</div>
) : (
columnItems.map((componentId, itemIndex) => (
- <DashboardComponent
- key={componentId}
- id={componentId}
- parentId={columnComponent.id}
- depth={depth + 1}
- index={itemIndex}
- availableColumnCount={columnComponent.meta.width}
- columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
- isComponentVisible={isComponentVisible}
- onChangeTab={onChangeTab}
- />
+ <React.Fragment key={componentId}>
+ <DashboardComponent
+ id={componentId}
+ parentId={columnComponent.id}
+ depth={depth + 1}
+ index={itemIndex}
+ availableColumnCount={columnComponent.meta.width}
+ columnWidth={columnWidth}
+ onResizeStart={onResizeStart}
+ onResize={onResize}
+ onResizeStop={onResizeStop}
+ isComponentVisible={isComponentVisible}
+ onChangeTab={onChangeTab}
+ />
+ {editMode && (
+ <Droppable
+ component={columnItems}
+ parentComponent={columnComponent}
+ depth={depth + 1}
+ index={itemIndex + 1}
+ orientation="column"
+ onDrop={handleComponentDrop}
+ className={cx(
+ 'empty-droptarget',
+ itemIndex === columnItems.length - 1 &&
+ 'droptarget-edge',
+ )}
+ editMode
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && (
+ <div {...dropIndicatorProps} />
+ )
+ }
+ </Droppable>
+ )}
+ </React.Fragment>
))
)}
-
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
</ColumnStyles>
</WithPopoverMenu>
</ResizableContainer>
)}
- </DragDroppable>
+ </Draggable>
);
}
}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx
index 2d759b812a..b1521211ad 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx
@@ -27,6 +27,14 @@ import { getMockStore } from 'spec/fixtures/mockStore';
import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
import { initialState } from 'src/SqlLab/fixtures';
+jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
+ Draggable: ({ children }) => (
+ <div data-test="mock-draggable">{children({})}</div>
+ ),
+ Droppable: ({ children }) => (
+ <div data-test="mock-droppable">{children({})}</div>
+ ),
+}));
jest.mock(
'src/dashboard/containers/DashboardComponent',
() =>
@@ -92,10 +100,12 @@ function setup(overrideProps) {
});
}
-test('should render a DragDroppable', () => {
- // don't count child DragDroppables
- const { getByTestId } = setup({ component: columnWithoutChildren });
- expect(getByTestId('dragdroppable-object')).toBeInTheDocument();
+test('should render a Draggable', () => {
+ const { getByTestId, queryByTestId } = setup({
+ component: columnWithoutChildren,
+ });
+ expect(getByTestId('mock-draggable')).toBeInTheDocument();
+ expect(queryByTestId('mock-droppable')).not.toBeInTheDocument();
});
test('should skip rendering HoverMenu and DeleteComponentButton when not in editMode', () => {
@@ -120,11 +130,14 @@ test('should render a ResizableContainer', () => {
test('should render a HoverMenu in editMode', () => {
// we cannot set props on the Row because of the WithDragDropContext wrapper
- const { container } = setup({
+ const { container, getAllByTestId } = setup({
component: columnWithoutChildren,
editMode: true,
});
expect(container.querySelector('.hover-menu')).toBeInTheDocument();
+
+ // Droppable area enabled in editMode
+ expect(getAllByTestId('mock-droppable').length).toBe(1);
});
test('should render a DeleteComponentButton in editMode', () => {
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx b/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx
index 078405be3e..638527aa3b 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx
@@ -20,7 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { css, styled } from '@superset-ui/core';
-import DragDroppable from '../dnd/DragDroppable';
+import { Draggable } from '../dnd/DragDroppable';
import HoverMenu from '../menu/HoverMenu';
import DeleteComponentButton from '../DeleteComponentButton';
import { componentShape } from '../../util/propShapes';
@@ -84,7 +84,7 @@ class Divider extends React.PureComponent {
} = this.props;
return (
- <DragDroppable
+ <Draggable
component={component}
parentComponent={parentComponent}
orientation="row"
@@ -93,20 +93,17 @@ class Divider extends React.PureComponent {
onDrop={handleComponentDrop}
editMode={editMode}
>
- {({ dropIndicatorProps, dragSourceRef }) => (
+ {({ dragSourceRef }) => (
<div ref={dragSourceRef}>
{editMode && (
<HoverMenu position="left">
<DeleteComponentButton onDelete={this.handleDeleteComponent} />
</HoverMenu>
)}
-
<DividerLine className="dashboard-component dashboard-component-divider" />
-
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
</div>
)}
- </DragDroppable>
+ </Draggable>
);
}
}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx
index 6331f68832..f98378ab72 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx
@@ -24,7 +24,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import Divider from 'src/dashboard/components/gridComponents/Divider';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import {
@@ -56,9 +56,9 @@ describe('Divider', () => {
return wrapper;
}
- it('should render a DragDroppable', () => {
+ it('should render a Draggable', () => {
const wrapper = setup();
- expect(wrapper.find(DragDroppable)).toExist();
+ expect(wrapper.find(Draggable)).toExist();
});
it('should render a div with class "dashboard-component-divider"', () => {
diff --git a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
index 707597d8c0..f1752588d2 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx
@@ -21,7 +21,7 @@ import { DashboardComponentMetadata, JsonObject, t } from '@superset-ui/core';
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import cx from 'classnames';
import { useSelector } from 'react-redux';
-import DragDroppable from '../dnd/DragDroppable';
+import { Draggable } from '../dnd/DragDroppable';
import { COLUMN_TYPE, ROW_TYPE } from '../../util/componentTypes';
import WithPopoverMenu from '../menu/WithPopoverMenu';
import ResizableContainer from '../resizable/ResizableContainer';
@@ -106,7 +106,7 @@ const DynamicComponent: FC<FilterSummaryType> = ({
);
return (
- <DragDroppable
+ <Draggable
// @ts-ignore
component={component}
// @ts-ignore
@@ -117,7 +117,7 @@ const DynamicComponent: FC<FilterSummaryType> = ({
onDrop={handleComponentDrop}
editMode={editMode}
>
- {({ dropIndicatorProps, dragSourceRef }) => (
+ {({ dragSourceRef }) => (
<WithPopoverMenu
menuItems={[
<BackgroundStyleDropdown
@@ -168,10 +168,9 @@ const DynamicComponent: FC<FilterSummaryType> = ({
</div>
</ResizableContainer>
</div>
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
</WithPopoverMenu>
)}
- </DragDroppable>
+ </Draggable>
);
};
export default DynamicComponent;
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Header.jsx b/superset-frontend/src/dashboard/components/gridComponents/Header.jsx
index 253f377fc2..85f4cd7cc7 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Header.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Header.jsx
@@ -23,7 +23,7 @@ import { css, styled } from '@superset-ui/core';
import PopoverDropdown from 'src/components/PopoverDropdown';
import EditableTitle from 'src/components/EditableTitle';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import DragHandle from 'src/dashboard/components/dnd/DragHandle';
import AnchorLink from 'src/dashboard/components/AnchorLink';
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
@@ -178,7 +178,7 @@ class Header extends React.PureComponent {
);
return (
- <DragDroppable
+ <Draggable
component={component}
parentComponent={parentComponent}
orientation="row"
@@ -188,7 +188,7 @@ class Header extends React.PureComponent {
disableDragDrop={isFocused}
editMode={editMode}
>
- {({ dropIndicatorProps, dragSourceRef }) => (
+ {({ dragSourceRef }) => (
<div ref={dragSourceRef}>
{editMode &&
depth <= 2 && ( // drag handle looks bad when nested
@@ -239,11 +239,9 @@ class Header extends React.PureComponent {
)}
</HeaderStyles>
</WithPopoverMenu>
-
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
</div>
)}
- </DragDroppable>
+ </Draggable>
);
}
}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx
index 6f903a05c1..64ff712129 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx
@@ -27,7 +27,7 @@ import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButto
import EditableTitle from 'src/components/EditableTitle';
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import Header from 'src/dashboard/components/gridComponents/Header';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import {
@@ -65,9 +65,9 @@ describe('Header', () => {
return wrapper;
}
- it('should render a DragDroppable', () => {
+ it('should render a Draggable', () => {
const wrapper = setup();
- expect(wrapper.find(DragDroppable)).toExist();
+ expect(wrapper.find(Draggable)).toExist();
});
it('should render a WithPopoverMenu', () => {
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx
index 9febfacf90..23a06a0f7d 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx
@@ -26,7 +26,7 @@ import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
import { MarkdownEditor } from 'src/components/AsyncAceEditor';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
import ResizableContainer from 'src/dashboard/components/resizable/ResizableContainer';
import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown';
@@ -332,7 +332,7 @@ class Markdown extends React.PureComponent {
const isEditing = editorMode === 'edit';
return (
- <DragDroppable
+ <Draggable
component={component}
parentComponent={parentComponent}
orientation={parentComponent.type === ROW_TYPE ? 'column' : 'row'}
@@ -342,7 +342,7 @@ class Markdown extends React.PureComponent {
disableDragDrop={isFocused}
editMode={editMode}
>
- {({ dropIndicatorProps, dragSourceRef }) => (
+ {({ dragSourceRef }) => (
<WithPopoverMenu
onChangeFocus={this.handleChangeFocus}
menuItems={[
@@ -396,10 +396,9 @@ class Markdown extends React.PureComponent {
</div>
</ResizableContainer>
</MarkdownStyles>
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
</WithPopoverMenu>
)}
- </DragDroppable>
+ </Draggable>
);
}
}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx
index ac97c38a4a..b15c4c504f 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx
@@ -30,7 +30,7 @@ import MarkdownConnected from 'src/dashboard/components/gridComponents/Markdown'
import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import ResizableContainer from 'src/dashboard/components/resizable/ResizableContainer';
@@ -62,7 +62,7 @@ describe('Markdown', () => {
function setup(overrideProps) {
// We have to wrap provide DragDropContext for the underlying DragDroppable
- // otherwise we cannot assert on DragDroppable children
+ // otherwise we cannot assert on Droppable children
const wrapper = mount(
<Provider store={mockStore}>
<DndProvider backend={HTML5Backend}>
@@ -73,9 +73,9 @@ describe('Markdown', () => {
return wrapper;
}
- it('should render a DragDroppable', () => {
+ it('should render a Draggable', () => {
const wrapper = setup();
- expect(wrapper.find(DragDroppable)).toExist();
+ expect(wrapper.find(Draggable)).toExist();
});
it('should render a WithPopoverMenu', () => {
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
index d645a1a2cf..95560e3b59 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx
@@ -19,15 +19,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
+import { debounce } from 'lodash';
import {
css,
+ FAST_DEBOUNCE,
FeatureFlag,
isFeatureEnabled,
styled,
t,
} from '@superset-ui/core';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import {
+ Draggable,
+ Droppable,
+} from 'src/dashboard/components/dnd/DragDroppable';
import DragHandle from 'src/dashboard/components/dnd/DragHandle';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
@@ -39,6 +44,7 @@ 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 { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
import { isCurrentUserBot } from 'src/utils/isBot';
const propTypes = {
@@ -57,6 +63,7 @@ const propTypes = {
onResizeStart: PropTypes.func.isRequired,
onResize: PropTypes.func.isRequired,
onResizeStop: PropTypes.func.isRequired,
+ maxChildrenHeight: PropTypes.number.isRequired,
// dnd
handleComponentDrop: PropTypes.func.isRequired,
@@ -65,7 +72,7 @@ const propTypes = {
};
const GridRow = styled.div`
- ${({ theme }) => css`
+ ${({ theme, editMode }) => css`
position: relative;
display: flex;
flex-direction: row;
@@ -75,7 +82,35 @@ const GridRow = styled.div`
height: fit-content;
& > :not(:last-child):not(.hover-menu) {
- margin-right: ${theme.gridUnit * 4}px;
+ ${!editMode && `margin-right: ${theme.gridUnit * 4}px;`}
+ }
+
+ & .empty-droptarget {
+ position: relative;
+ align-self: center;
+ &.empty-droptarget--vertical {
+ min-width: ${theme.gridUnit * 4}px;
+ &:not(:last-child) {
+ width: ${theme.gridUnit * 4}px;
+ }
+ &:first-child:not(.droptarget-side) {
+ z-index: ${EMPTY_CONTAINER_Z_INDEX};
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ }
+ }
+ &.droptarget-side {
+ z-index: ${EMPTY_CONTAINER_Z_INDEX};
+ position: absolute;
+ width: ${theme.gridUnit * 4}px;
+ &:first-child {
+ inset-inline-start: 0;
+ }
+ &:last-child {
+ inset-inline-end: 0;
+ }
+ }
}
&.grid-row--empty {
@@ -108,6 +143,10 @@ class Row extends React.PureComponent {
'background',
);
this.handleChangeFocus = this.handleChangeFocus.bind(this);
+ this.setVerticalEmptyContainerHeight = debounce(
+ this.setVerticalEmptyContainerHeight.bind(this),
+ FAST_DEBOUNCE,
+ );
this.containerRef = React.createRef();
this.observerEnabler = null;
@@ -145,10 +184,28 @@ class Row extends React.PureComponent {
if (element) {
this.observerEnabler.observe(element);
this.observerDisabler.observe(element);
+ this.setVerticalEmptyContainerHeight();
}
}
}
+ componentDidUpdate() {
+ this.setVerticalEmptyContainerHeight();
+ }
+
+ setVerticalEmptyContainerHeight() {
+ const { containerHeight } = this.state;
+ const { editMode } = this.props;
+ const updatedHeight = this.containerRef.current?.clientHeight;
+ if (
+ editMode &&
+ this.containerRef.current &&
+ updatedHeight !== containerHeight
+ ) {
+ this.setState({ containerHeight: updatedHeight });
+ }
+ }
+
componentWillUnmount() {
this.observerEnabler?.disconnect();
this.observerDisabler?.disconnect();
@@ -195,6 +252,7 @@ class Row extends React.PureComponent {
onChangeTab,
isComponentVisible,
} = this.props;
+ const { containerHeight } = this.state;
const rowItems = rowComponent.children || [];
@@ -202,9 +260,10 @@ class Row extends React.PureComponent {
opt =>
opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
);
+ const remainColumnCount = availableColumnCount - occupiedColumnCount;
return (
- <DragDroppable
+ <Draggable
component={rowComponent}
parentComponent={parentComponent}
orientation="row"
@@ -213,7 +272,7 @@ class Row extends React.PureComponent {
onDrop={handleComponentDrop}
editMode={editMode}
>
- {({ dropIndicatorProps, dragSourceRef }) => (
+ {({ dragSourceRef }) => (
<WithPopoverMenu
isFocused={this.state.isFocused}
onChangeFocus={this.handleChangeFocus}
@@ -245,36 +304,94 @@ class Row extends React.PureComponent {
)}
data-test={`grid-row-${backgroundStyle.className}`}
ref={this.containerRef}
+ editMode={editMode}
>
- {rowItems.length === 0 ? (
+ {editMode && (
+ <Droppable
+ {...(rowItems.length === 0
+ ? {
+ component: rowComponent,
+ parentComponent,
+ dropToChild: true,
+ }
+ : {
+ component: rowItems,
+ parentComponent: rowComponent,
+ })}
+ depth={depth + 1}
+ index={0}
+ orientation="row"
+ onDrop={handleComponentDrop}
+ className={cx(
+ 'empty-droptarget',
+ 'empty-droptarget--vertical',
+ rowItems.length > 0 && 'droptarget-side',
+ )}
+ editMode
+ style={{
+ height: rowItems.length > 0 ? containerHeight : '100%',
+ ...(rowItems.length > 0 && { width: 16 }),
+ }}
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && <div {...dropIndicatorProps} />
+ }
+ </Droppable>
+ )}
+ {rowItems.length === 0 && (
<div css={emptyRowContentStyles}>{t('Empty row')}</div>
- ) : (
- rowItems.map((componentId, itemIndex) => (
- <DashboardComponent
- key={componentId}
- id={componentId}
- parentId={rowComponent.id}
- depth={depth + 1}
- index={itemIndex}
- availableColumnCount={
- availableColumnCount - occupiedColumnCount
- }
- columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
- isComponentVisible={isComponentVisible}
- onChangeTab={onChangeTab}
- isInView={this.state.isInView}
- />
- ))
)}
-
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
+ {rowItems.length > 0 &&
+ rowItems.map((componentId, itemIndex) => (
+ <React.Fragment key={componentId}>
+ <DashboardComponent
+ key={componentId}
+ id={componentId}
+ parentId={rowComponent.id}
+ depth={depth + 1}
+ index={itemIndex}
+ availableColumnCount={remainColumnCount}
+ columnWidth={columnWidth}
+ onResizeStart={onResizeStart}
+ onResize={onResize}
+ onResizeStop={onResizeStop}
+ isComponentVisible={isComponentVisible}
+ onChangeTab={onChangeTab}
+ isInView={this.state.isInView}
+ />
+ {editMode && (
+ <Droppable
+ component={rowItems}
+ parentComponent={rowComponent}
+ depth={depth + 1}
+ index={itemIndex + 1}
+ orientation="row"
+ onDrop={handleComponentDrop}
+ className={cx(
+ 'empty-droptarget',
+ 'empty-droptarget--vertical',
+ remainColumnCount === 0 &&
+ itemIndex === rowItems.length - 1 &&
+ 'droptarget-side',
+ )}
+ editMode
+ style={{
+ height: containerHeight,
+ ...(remainColumnCount === 0 &&
+ itemIndex === rowItems.length - 1 && { width: 16 }),
+ }}
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && <div {...dropIndicatorProps} />
+ }
+ </Droppable>
+ )}
+ </React.Fragment>
+ ))}
</GridRow>
</WithPopoverMenu>
)}
- </DragDroppable>
+ </Draggable>
);
}
}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx
index b69a8f7e97..e330195d77 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx
@@ -28,6 +28,14 @@ import { getMockStore } from 'spec/fixtures/mockStore';
import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
import { initialState } from 'src/SqlLab/fixtures';
+jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
+ Draggable: ({ children }) => (
+ <div data-test="mock-draggable">{children({})}</div>
+ ),
+ Droppable: ({ children }) => (
+ <div data-test="mock-droppable">{children({})}</div>
+ ),
+}));
jest.mock(
'src/dashboard/containers/DashboardComponent',
() =>
@@ -92,10 +100,14 @@ function setup(overrideProps) {
});
}
-test('should render a DragDroppable', () => {
+test('should render a Draggable', () => {
// don't count child DragDroppables
- const { getByTestId } = setup({ component: rowWithoutChildren });
- expect(getByTestId('dragdroppable-object')).toBeInTheDocument();
+ const { getByTestId, queryByTestId } = setup({
+ component: rowWithoutChildren,
+ });
+
+ expect(getByTestId('mock-draggable')).toBeInTheDocument();
+ expect(queryByTestId('mock-droppable')).not.toBeInTheDocument();
});
test('should skip rendering HoverMenu and DeleteComponentButton when not in editMode', () => {
@@ -113,11 +125,14 @@ test('should render a WithPopoverMenu', () => {
});
test('should render a HoverMenu in editMode', () => {
- const { container } = setup({
+ const { container, getAllByTestId } = setup({
component: rowWithoutChildren,
editMode: true,
});
expect(container.querySelector('.hover-menu')).toBeInTheDocument();
+
+ // Droppable area enabled in editMode
+ expect(getAllByTestId('mock-droppable').length).toBe(1);
});
test('should render a DeleteComponentButton in editMode', () => {
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
index d1d08176ba..b546947e31 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx
@@ -28,7 +28,9 @@ import EditableTitle from 'src/components/EditableTitle';
import { setEditMode } from 'src/dashboard/actions/dashboardState';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import AnchorLink from 'src/dashboard/components/AnchorLink';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import DragDroppable, {
+ Droppable,
+} from 'src/dashboard/components/dnd/DragDroppable';
import { componentShape } from 'src/dashboard/util/propShapes';
export const RENDER_TAB = 'RENDER_TAB';
@@ -83,15 +85,8 @@ const TabTitleContainer = styled.div`
`}
`;
-const renderDraggableContentBottom = dropProps =>
- dropProps.dropIndicatorProps && (
- <div className="drop-indicator drop-indicator--bottom" />
- );
-
-const renderDraggableContentTop = dropProps =>
- dropProps.dropIndicatorProps && (
- <div className="drop-indicator drop-indicator--top" />
- );
+const renderDraggableContent = dropProps =>
+ dropProps.dropIndicatorProps && <div {...dropProps.dropIndicatorProps} />;
class Tab extends React.PureComponent {
constructor(props) {
@@ -147,7 +142,6 @@ class Tab extends React.PureComponent {
renderTabContent() {
const {
component: tabComponent,
- parentComponent: tabParentComponent,
depth,
availableColumnCount,
columnWidth,
@@ -166,21 +160,25 @@ class Tab extends React.PureComponent {
<div className="dashboard-component-tabs-content">
{/* Make top of tab droppable */}
{editMode && (
- <DragDroppable
+ <Droppable
component={tabComponent}
- parentComponent={tabParentComponent}
orientation="column"
index={0}
depth={depth}
- onDrop={this.handleTopDropTargetDrop}
+ onDrop={
+ tabComponent.children.length === 0
+ ? this.handleTopDropTargetDrop
+ : this.handleDrop
+ }
editMode
className={classNames({
'empty-droptarget': true,
'empty-droptarget--full': tabComponent.children.length === 0,
})}
+ dropToChild={tabComponent.children.length === 0}
>
- {renderDraggableContentTop}
- </DragDroppable>
+ {renderDraggableContent}
+ </Droppable>
)}
{shouldDisplayEmptyState && (
<EmptyStateMedium
@@ -220,39 +218,38 @@ class Tab extends React.PureComponent {
/>
)}
{tabComponent.children.map((componentId, componentIndex) => (
- <DashboardComponent
- key={componentId}
- id={componentId}
- parentId={tabComponent.id}
- depth={depth} // see isValidChild.js for why tabs don't increment child depth
- index={componentIndex}
- onDrop={this.handleDrop}
- onHover={this.handleOnHover}
- availableColumnCount={availableColumnCount}
- columnWidth={columnWidth}
- onResizeStart={onResizeStart}
- onResize={onResize}
- onResizeStop={onResizeStop}
- isComponentVisible={isComponentVisible}
- onChangeTab={this.handleChangeTab}
- />
+ <React.Fragment key={componentId}>
+ <DashboardComponent
+ id={componentId}
+ parentId={tabComponent.id}
+ depth={depth} // see isValidChild.js for why tabs don't increment child depth
+ index={componentIndex}
+ onDrop={this.handleDrop}
+ onHover={this.handleOnHover}
+ availableColumnCount={availableColumnCount}
+ columnWidth={columnWidth}
+ onResizeStart={onResizeStart}
+ onResize={onResize}
+ onResizeStop={onResizeStop}
+ isComponentVisible={isComponentVisible}
+ onChangeTab={this.handleChangeTab}
+ />
+ {/* Make bottom of tab droppable */}
+ {editMode && (
+ <Droppable
+ component={tabComponent}
+ orientation="column"
+ index={componentIndex + 1}
+ depth={depth}
+ onDrop={this.handleDrop}
+ editMode
+ className="empty-droptarget"
+ >
+ {renderDraggableContent}
+ </Droppable>
+ )}
+ </React.Fragment>
))}
- {/* Make bottom of tab droppable */}
- {editMode && tabComponent.children.length > 0 && (
- <DragDroppable
- component={tabComponent}
- parentComponent={tabParentComponent}
- orientation="column"
- index={tabComponent.children.length}
- depth={depth}
- onDrop={this.handleDrop}
- onHover={this.handleOnHover}
- editMode
- className="empty-droptarget"
- >
- {renderDraggableContentBottom}
- </DragDroppable>
- )}
</div>
);
}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.test.tsx
index d995595c49..e19a086d81 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tab.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.test.tsx
@@ -22,7 +22,6 @@ import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
import EditableTitle from 'src/components/EditableTitle';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
import { setEditMode } from 'src/dashboard/actions/dashboardState';
import Tab from './Tab';
@@ -37,8 +36,9 @@ jest.mock('src/components/EditableTitle', () =>
</button>
)),
);
-jest.mock('src/dashboard/components/dnd/DragDroppable', () =>
- jest.fn(props => {
+jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
+ ...jest.requireActual('src/dashboard/components/dnd/DragDroppable'),
+ Droppable: jest.fn(props => {
const childProps = props.editMode
? {
dragSourceRef: props.dragSourceRef,
@@ -47,14 +47,14 @@ jest.mock('src/dashboard/components/dnd/DragDroppable', () =>
: {};
return (
<div>
- <button type="button" data-test="DragDroppable" onClick={props.onDrop}>
+ <button type="button" data-test="MockDroppable" onClick={props.onDrop}>
DragDroppable
</button>
{props.children(childProps)}
</div>
);
}),
-);
+}));
jest.mock('src/dashboard/actions/dashboardState', () => ({
setEditMode: jest.fn(() => ({
type: 'SET_EDIT_MODE',
@@ -106,30 +106,39 @@ beforeEach(() => {
test('Render tab (no content)', () => {
const props = createProps();
props.renderType = 'RENDER_TAB';
- render(<Tab {...props} />, { useRedux: true, useDnd: true });
+ const { getByTestId } = render(<Tab {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
expect(screen.getByText('🚀 Aspiring Developers')).toBeInTheDocument();
expect(EditableTitle).toBeCalledTimes(1);
- expect(DragDroppable).toBeCalledTimes(1);
+ expect(getByTestId('dragdroppable-object')).toBeInTheDocument();
});
test('Render tab (no content) editMode:true', () => {
const props = createProps();
props.editMode = true;
props.renderType = 'RENDER_TAB';
- render(<Tab {...props} />, { useRedux: true, useDnd: true });
+ const { getByTestId } = render(<Tab {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
expect(screen.getByText('🚀 Aspiring Developers')).toBeInTheDocument();
expect(EditableTitle).toBeCalledTimes(1);
- expect(DragDroppable).toBeCalledTimes(1);
+ expect(getByTestId('dragdroppable-object')).toBeInTheDocument();
});
test('Edit table title', () => {
const props = createProps();
props.editMode = true;
props.renderType = 'RENDER_TAB';
- render(<Tab {...props} />, { useRedux: true, useDnd: true });
+ const { getByTestId } = render(<Tab {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
expect(EditableTitle).toBeCalledTimes(1);
- expect(DragDroppable).toBeCalledTimes(1);
+ expect(getByTestId('dragdroppable-object')).toBeInTheDocument();
expect(props.updateComponents).not.toBeCalled();
userEvent.click(screen.getByText('🚀 Aspiring Developers'));
@@ -139,7 +148,10 @@ test('Edit table title', () => {
test('Render tab (with content)', () => {
const props = createProps();
props.isFocused = true;
- render(<Tab {...props} />, { useRedux: true, useDnd: true });
+ const { queryByTestId } = render(<Tab {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
expect(DashboardComponent).toBeCalledTimes(2);
expect(DashboardComponent).toHaveBeenNthCalledWith(
1,
@@ -177,7 +189,7 @@ test('Render tab (with content)', () => {
}),
{},
);
- expect(DragDroppable).toBeCalledTimes(0);
+ expect(queryByTestId('dragdroppable-object')).not.toBeInTheDocument();
});
test('Render tab content with no children', () => {
@@ -215,7 +227,10 @@ test('Render tab (with content) editMode:true', () => {
const props = createProps();
props.isFocused = true;
props.editMode = true;
- render(<Tab {...props} />, { useRedux: true, useDnd: true });
+ const { getAllByTestId } = render(<Tab {...props} />, {
+ useRedux: true,
+ useDnd: true,
+ });
expect(DashboardComponent).toBeCalledTimes(2);
expect(DashboardComponent).toHaveBeenNthCalledWith(
1,
@@ -253,20 +268,28 @@ test('Render tab (with content) editMode:true', () => {
}),
{},
);
- expect(DragDroppable).toBeCalledTimes(2);
+ // 3 droppable area exists for two child components
+ expect(getAllByTestId('MockDroppable')).toHaveLength(3);
});
test('Should call "handleDrop" and "handleTopDropTargetDrop"', () => {
const props = createProps();
props.isFocused = true;
props.editMode = true;
- render(<Tab {...props} />, { useRedux: true, useDnd: true });
+ const { getAllByTestId, rerender } = render(
+ <Tab {...props} component={{ ...props.component, children: [] }} />,
+ {
+ useRedux: true,
+ useDnd: true,
+ },
+ );
expect(props.handleComponentDrop).not.toBeCalled();
- userEvent.click(screen.getAllByRole('button')[0]);
+ userEvent.click(getAllByTestId('MockDroppable')[0]);
expect(props.handleComponentDrop).toBeCalledTimes(1);
expect(props.onDropOnTab).not.toBeCalled();
- userEvent.click(screen.getAllByRole('button')[1]);
+ rerender(<Tab {...props} />);
+ userEvent.click(getAllByTestId('MockDroppable')[1]);
expect(props.onDropOnTab).toBeCalledTimes(1);
expect(props.handleComponentDrop).toBeCalledTimes(2);
});
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
index 67f4b3c598..c9f35d3ba5 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx
@@ -23,7 +23,7 @@ import { connect } from 'react-redux';
import { LineEditableTabs } from 'src/components/Tabs';
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
import { AntdModal } from 'src/components';
-import DragDroppable from '../dnd/DragDroppable';
+import { Draggable } from '../dnd/DragDroppable';
import DragHandle from '../dnd/DragHandle';
import DashboardComponent from '../../containers/DashboardComponent';
import DeleteComponentButton from '../DeleteComponentButton';
@@ -32,7 +32,7 @@ import findTabIndexByComponentId from '../../util/findTabIndexByComponentId';
import getDirectPathToTabIndex from '../../util/getDirectPathToTabIndex';
import getLeafComponentIdFromPath from '../../util/getLeafComponentIdFromPath';
import { componentShape } from '../../util/propShapes';
-import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
+import { NEW_TAB_ID } from '../../util/constants';
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
import { TABS_TYPE, TAB_TYPE } from '../../util/componentTypes';
@@ -339,7 +339,7 @@ export class Tabs extends React.PureComponent {
tabsToHighlight = nativeFilters.filters[highlightedFilterId]?.tabsInScope;
}
return (
- <DragDroppable
+ <Draggable
component={tabsComponent}
parentComponent={parentComponent}
orientation="row"
@@ -348,10 +348,7 @@ export class Tabs extends React.PureComponent {
onDrop={this.handleDrop}
editMode={editMode}
>
- {({
- dropIndicatorProps: tabsDropIndicatorProps,
- dragSourceRef: tabsDragSourceRef,
- }) => (
+ {({ dragSourceRef: tabsDragSourceRef }) => (
<StyledTabsContainer
className="dashboard-component dashboard-component-tabs"
data-test="dashboard-component-tabs"
@@ -415,15 +412,9 @@ export class Tabs extends React.PureComponent {
</LineEditableTabs.TabPane>
))}
</LineEditableTabs>
-
- {/* don't indicate that a drop on root is allowed when tabs already exist */}
- {tabsDropIndicatorProps &&
- parentComponent.id !== DASHBOARD_ROOT_ID && (
- <div {...tabsDropIndicatorProps} />
- )}
</StyledTabsContainer>
)}
- </DragDroppable>
+ </Draggable>
);
}
}
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
index 127b4d42db..7f39bdcc68 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx
@@ -29,6 +29,14 @@ import { getMockStore } from 'spec/fixtures/mockStore';
import { nativeFilters } from 'spec/fixtures/mockNativeFilters';
import { initialState } from 'src/SqlLab/fixtures';
+jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
+ Draggable: ({ children }) => (
+ <div data-test="mock-draggable">{children({})}</div>
+ ),
+ Droppable: ({ children }) => (
+ <div data-test="mock-droppable">{children({})}</div>
+ ),
+}));
jest.mock('src/dashboard/containers/DashboardComponent', () => ({ id }) => (
<div data-test="mock-dashboard-component">{id}</div>
));
@@ -88,12 +96,12 @@ function setup(overrideProps) {
});
}
-test('should render a DragDroppable', () => {
- // test just Tabs with no children DragDroppables
+test('should render a Draggable', () => {
+ // test just Tabs with no children Draggable
const { getByTestId } = setup({
component: { ...props.component, children: [] },
});
- expect(getByTestId('dragdroppable-object')).toBeInTheDocument();
+ expect(getByTestId('mock-draggable')).toBeInTheDocument();
});
test('should render non-editable tabs', () => {
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
index 9ee9fc6866..6aef193b44 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx
@@ -22,7 +22,7 @@ import React from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import { nativeFiltersInfo } from 'src/dashboard/fixtures/mockNativeFilters';
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
-import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
+import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout';
@@ -55,8 +55,8 @@ jest.mock('src/dashboard/components/DeleteComponentButton', () =>
);
jest.mock('src/dashboard/util/getLeafComponentIdFromPath', () => jest.fn());
-jest.mock('src/dashboard/components/dnd/DragDroppable', () =>
- jest.fn(props => {
+jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({
+ Draggable: jest.fn(props => {
const childProps = props.editMode
? {
dragSourceRef: props.dragSourceRef,
@@ -72,7 +72,7 @@ jest.mock('src/dashboard/components/dnd/DragDroppable', () =>
</div>
);
}),
-);
+}));
const createProps = () => ({
id: 'TABS-L-d9eyOE-b',
@@ -123,7 +123,7 @@ test('Should render editMode:true', () => {
const props = createProps();
render(<Tabs {...props} />, { useRedux: true, useDnd: true });
expect(screen.getAllByRole('tab')).toHaveLength(3);
- expect(DragDroppable).toBeCalledTimes(1);
+ expect(Draggable).toBeCalledTimes(1);
expect(DashboardComponent).toBeCalledTimes(4);
expect(DeleteComponentButton).toBeCalledTimes(1);
expect(screen.getAllByRole('button', { name: 'remove' })).toHaveLength(3);
@@ -135,7 +135,7 @@ test('Should render editMode:false', () => {
props.editMode = false;
render(<Tabs {...props} />, { useRedux: true, useDnd: true });
expect(screen.getAllByRole('tab')).toHaveLength(3);
- expect(DragDroppable).toBeCalledTimes(1);
+ expect(Draggable).toBeCalledTimes(1);
expect(DashboardComponent).toBeCalledTimes(4);
expect(DeleteComponentButton).not.toBeCalled();
expect(
diff --git a/superset-frontend/src/dashboard/constants.ts b/superset-frontend/src/dashboard/constants.ts
index d4e5fed4b6..d512c2a8a6 100644
--- a/superset-frontend/src/dashboard/constants.ts
+++ b/superset-frontend/src/dashboard/constants.ts
@@ -43,6 +43,7 @@ export const FILTER_BAR_HEADER_HEIGHT = 80;
export const FILTER_BAR_TABS_HEIGHT = 46;
export const BUILDER_SIDEPANEL_WIDTH = 374;
export const OVERWRITE_INSPECT_FIELDS = ['css', 'json_metadata.filter_scopes'];
+export const EMPTY_CONTAINER_Z_INDEX = 10;
export const DEFAULT_CROSS_FILTER_SCOPING: NativeFilterScope = {
rootPath: [DASHBOARD_ROOT_ID],
diff --git a/superset-frontend/src/dashboard/util/getDropPosition.js b/superset-frontend/src/dashboard/util/getDropPosition.js
index 81cbc48f46..c4a21ad113 100644
--- a/superset-frontend/src/dashboard/util/getDropPosition.js
+++ b/superset-frontend/src/dashboard/util/getDropPosition.js
@@ -23,6 +23,7 @@ export const DROP_TOP = 'DROP_TOP';
export const DROP_RIGHT = 'DROP_RIGHT';
export const DROP_BOTTOM = 'DROP_BOTTOM';
export const DROP_LEFT = 'DROP_LEFT';
+export const DROP_FORBIDDEN = 'DROP_FORBIDDEN';
// this defines how close the mouse must be to the edge of a component to display
// a sibling type drop indicator
@@ -72,7 +73,7 @@ export default function getDropPosition(monitor, Component) {
});
if (!validChild && !validSibling) {
- return null;
+ return DROP_FORBIDDEN;
}
const hasChildren = (component.children || []).length > 0;
diff --git a/superset-frontend/src/dashboard/util/getDropPosition.test.js b/superset-frontend/src/dashboard/util/getDropPosition.test.js
index 71f506b1bd..9fdd02afa6 100644
--- a/superset-frontend/src/dashboard/util/getDropPosition.test.js
+++ b/superset-frontend/src/dashboard/util/getDropPosition.test.js
@@ -21,6 +21,7 @@ import getDropPosition, {
DROP_RIGHT,
DROP_BOTTOM,
DROP_LEFT,
+ DROP_FORBIDDEN,
} from 'src/dashboard/util/getDropPosition';
import {
@@ -80,7 +81,7 @@ describe('getDropPosition', () => {
}
describe('invalid child + invalid sibling', () => {
- it('should return null', () => {
+ it('should return DROP_FORBIDDEN', () => {
const result = getDropPosition(
// TAB is an invalid child + sibling of GRID > ROW
...getMocks({
@@ -89,7 +90,7 @@ describe('getDropPosition', () => {
draggingType: TAB_TYPE,
}),
);
- expect(result).toBeNull();
+ expect(result).toBe(DROP_FORBIDDEN);
});
});