mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
[dashboard] allow user re-order top-level tabs (#7390)
This commit is contained in:
parent
ca2996c78f
commit
9e703f399b
@ -39,7 +39,10 @@ import {
|
|||||||
} from '../../../../src/dashboard/actions/dashboardLayout';
|
} from '../../../../src/dashboard/actions/dashboardLayout';
|
||||||
|
|
||||||
import { setUnsavedChanges } from '../../../../src/dashboard/actions/dashboardState';
|
import { setUnsavedChanges } from '../../../../src/dashboard/actions/dashboardState';
|
||||||
import { addInfoToast } from '../../../../src/messageToasts/actions';
|
import {
|
||||||
|
addWarningToast,
|
||||||
|
ADD_TOAST,
|
||||||
|
} from '../../../../src/messageToasts/actions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DASHBOARD_GRID_TYPE,
|
DASHBOARD_GRID_TYPE,
|
||||||
@ -334,7 +337,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
|
|
||||||
const thunk = handleComponentDrop(dropResult);
|
const thunk = handleComponentDrop(dropResult);
|
||||||
thunk(dispatch, getState);
|
thunk(dispatch, getState);
|
||||||
expect(dispatch.getCall(0).args[0].type).toEqual(addInfoToast('').type);
|
expect(dispatch.getCall(0).args[0].type).toEqual(
|
||||||
|
addWarningToast('').type,
|
||||||
|
);
|
||||||
|
|
||||||
expect(dispatch.callCount).toBe(1);
|
expect(dispatch.callCount).toBe(1);
|
||||||
});
|
});
|
||||||
@ -402,6 +407,64 @@ describe('dashboardLayout actions', () => {
|
|||||||
|
|
||||||
expect(dispatch.callCount).toBe(2);
|
expect(dispatch.callCount).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should dispatch a toast if drop top-level tab into nested tab', () => {
|
||||||
|
const { getState, dispatch } = setup({
|
||||||
|
dashboardLayout: {
|
||||||
|
present: {
|
||||||
|
[DASHBOARD_ROOT_ID]: {
|
||||||
|
children: ['TABS-ROOT_TABS'],
|
||||||
|
id: DASHBOARD_ROOT_ID,
|
||||||
|
type: 'ROOT',
|
||||||
|
},
|
||||||
|
'TABS-ROOT_TABS': {
|
||||||
|
children: ['TAB-iMppmTOQy', 'TAB-rt1y8cQ6K9', 'TAB-X_pnCIwPN'],
|
||||||
|
id: 'TABS-ROOT_TABS',
|
||||||
|
meta: {},
|
||||||
|
parents: ['ROOT_ID'],
|
||||||
|
type: TABS_TYPE,
|
||||||
|
},
|
||||||
|
'TABS-ROW_TABS': {
|
||||||
|
children: [
|
||||||
|
'TAB-dKIDBT03bQ',
|
||||||
|
'TAB-PtxY5bbTe',
|
||||||
|
'TAB-Wc2P-yGMz',
|
||||||
|
'TAB-U-xe_si7i',
|
||||||
|
],
|
||||||
|
id: 'TABS-ROW_TABS',
|
||||||
|
meta: {},
|
||||||
|
parents: ['ROOT_ID', 'TABS-ROOT_TABS', 'TAB-X_pnCIwPN'],
|
||||||
|
type: TABS_TYPE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dropResult = {
|
||||||
|
source: {
|
||||||
|
id: 'TABS-ROOT_TABS',
|
||||||
|
index: 1,
|
||||||
|
type: TABS_TYPE,
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
id: 'TABS-ROW_TABS',
|
||||||
|
index: 1,
|
||||||
|
type: TABS_TYPE,
|
||||||
|
},
|
||||||
|
dragging: {
|
||||||
|
id: 'TAB-rt1y8cQ6K9',
|
||||||
|
meta: { text: 'New Tab' },
|
||||||
|
type: 'TAB',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const thunk1 = handleComponentDrop(dropResult);
|
||||||
|
thunk1(dispatch, getState);
|
||||||
|
|
||||||
|
const thunk2 = dispatch.getCall(0).args[0];
|
||||||
|
thunk2(dispatch, getState);
|
||||||
|
|
||||||
|
expect(dispatch.getCall(1).args[0].type).toEqual(ADD_TOAST);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('undoLayoutAction', () => {
|
describe('undoLayoutAction', () => {
|
||||||
|
@ -17,8 +17,9 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ActionCreators as UndoActionCreators } from 'redux-undo';
|
import { ActionCreators as UndoActionCreators } from 'redux-undo';
|
||||||
|
import { t } from '@superset-ui/translation';
|
||||||
|
|
||||||
import { addInfoToast } from '../../messageToasts/actions';
|
import { addWarningToast } from '../../messageToasts/actions';
|
||||||
import { setUnsavedChanges } from './dashboardState';
|
import { setUnsavedChanges } from './dashboardState';
|
||||||
import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes';
|
import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes';
|
||||||
import {
|
import {
|
||||||
@ -153,20 +154,40 @@ export function handleComponentDrop(dropResult) {
|
|||||||
|
|
||||||
if (overflowsParent) {
|
if (overflowsParent) {
|
||||||
return dispatch(
|
return dispatch(
|
||||||
addInfoToast(
|
addWarningToast(
|
||||||
|
t(
|
||||||
`There is not enough space for this component. Try decreasing its width, or increasing the destination width.`,
|
`There is not enough space for this component. Try decreasing its width, or increasing the destination width.`,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { source, destination } = dropResult;
|
const { source, destination } = dropResult;
|
||||||
const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
|
const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID;
|
||||||
const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
|
const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
|
||||||
|
const dashboardRoot = getState().dashboardLayout.present[DASHBOARD_ROOT_ID];
|
||||||
|
const rootChildId =
|
||||||
|
dashboardRoot && dashboardRoot.children ? dashboardRoot.children[0] : '';
|
||||||
|
|
||||||
if (droppedOnRoot) {
|
if (droppedOnRoot) {
|
||||||
dispatch(createTopLevelTabs(dropResult));
|
dispatch(createTopLevelTabs(dropResult));
|
||||||
} else if (destination && isNewComponent) {
|
} else if (destination && isNewComponent) {
|
||||||
dispatch(createComponent(dropResult));
|
dispatch(createComponent(dropResult));
|
||||||
|
} else if (
|
||||||
|
// Add additional allow-to-drop logic for tag/tags source.
|
||||||
|
// We only allow
|
||||||
|
// - top-level tab => top-level tab: rearrange top-level tab order
|
||||||
|
// - nested tab => top-level tab: allow row tab become top-level tab
|
||||||
|
// Dashboard does not allow top-level tab become nested tab, to avoid
|
||||||
|
// nested tab inside nested tab.
|
||||||
|
source.type === TABS_TYPE &&
|
||||||
|
destination.type === TABS_TYPE &&
|
||||||
|
source.id === rootChildId &&
|
||||||
|
destination.id !== rootChildId
|
||||||
|
) {
|
||||||
|
return dispatch(
|
||||||
|
addWarningToast(t(`Can not move top level tab into nested tabs`)),
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
destination &&
|
destination &&
|
||||||
source &&
|
source &&
|
||||||
@ -176,6 +197,8 @@ export function handleComponentDrop(dropResult) {
|
|||||||
dispatch(moveComponent(dropResult));
|
dispatch(moveComponent(dropResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// call getState() again down here in case redux state is stale after
|
||||||
|
// previous dispatch(es)
|
||||||
const { dashboardLayout: undoableLayout } = getState();
|
const { dashboardLayout: undoableLayout } = getState();
|
||||||
|
|
||||||
// if we moved a child from a Tab or Row parent and it was the only child, delete the parent.
|
// if we moved a child from a Tab or Row parent and it was the only child, delete the parent.
|
||||||
|
@ -84,10 +84,11 @@ class DashboardBuilder extends React.Component {
|
|||||||
const { dashboardLayout, directPathToChild } = props;
|
const { dashboardLayout, directPathToChild } = props;
|
||||||
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
|
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
|
||||||
const rootChildId = dashboardRoot.children[0];
|
const rootChildId = dashboardRoot.children[0];
|
||||||
const topLevelTabs =
|
|
||||||
rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
|
|
||||||
const tabIndex = findTabIndexByComponentId({
|
const tabIndex = findTabIndexByComponentId({
|
||||||
currentComponent: topLevelTabs || dashboardLayout[DASHBOARD_ROOT_ID],
|
currentComponent:
|
||||||
|
rootChildId === DASHBOARD_GRID_ID
|
||||||
|
? dashboardLayout[DASHBOARD_ROOT_ID]
|
||||||
|
: dashboardLayout[rootChildId],
|
||||||
directPathToChild,
|
directPathToChild,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@ import AnchorLink from '../../../components/AnchorLink';
|
|||||||
import DeleteComponentModal from '../DeleteComponentModal';
|
import DeleteComponentModal from '../DeleteComponentModal';
|
||||||
import WithPopoverMenu from '../menu/WithPopoverMenu';
|
import WithPopoverMenu from '../menu/WithPopoverMenu';
|
||||||
import { componentShape } from '../../util/propShapes';
|
import { componentShape } from '../../util/propShapes';
|
||||||
import { DASHBOARD_ROOT_DEPTH } from '../../util/constants';
|
|
||||||
|
|
||||||
export const RENDER_TAB = 'RENDER_TAB';
|
export const RENDER_TAB = 'RENDER_TAB';
|
||||||
export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
|
export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
|
||||||
@ -219,10 +218,6 @@ export default class Tab extends React.PureComponent {
|
|||||||
index={index}
|
index={index}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
onDrop={this.handleDrop}
|
onDrop={this.handleDrop}
|
||||||
// disable drag drop of top-level Tab's to prevent invalid nesting of a child in
|
|
||||||
// itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would
|
|
||||||
// reusult in circular children
|
|
||||||
disableDragDrop={depth <= DASHBOARD_ROOT_DEPTH + 1}
|
|
||||||
editMode={editMode}
|
editMode={editMode}
|
||||||
>
|
>
|
||||||
{({ dropIndicatorProps, dragSourceRef }) => (
|
{({ dropIndicatorProps, dragSourceRef }) => (
|
||||||
|
Loading…
Reference in New Issue
Block a user