blob: 51df31503b18a173ff69cf18d7631c3cef0ca81a [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ActionCreators as UndoActionCreators } from 'redux-undo';
import { t } from '@superset-ui/core';
import { addWarningToast } from '../../messageToasts/actions';
import { updateLayoutComponents } from './dashboardFilters';
import { setUnsavedChanges } from './dashboardState';
import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes';
import {
DASHBOARD_ROOT_ID,
NEW_COMPONENTS_SOURCE_ID,
DASHBOARD_HEADER_ID,
} from '../util/constants';
import dropOverflowsParent from '../util/dropOverflowsParent';
import findParentId from '../util/findParentId';
import isInDifferentFilterScopes from '../util/isInDifferentFilterScopes';
// Component CRUD -------------------------------------------------------------
export const UPDATE_COMPONENTS = 'UPDATE_COMPONENTS';
// this is a helper that takes an action as input and dispatches
// an additional setUnsavedChanges(true) action after the dispatch in the case
// that dashboardState.hasUnsavedChanges is false.
function setUnsavedChangesAfterAction(action) {
return (...args) => (dispatch, getState) => {
const result = action(...args);
if (typeof result === 'function') {
dispatch(result(dispatch, getState));
} else {
dispatch(result);
}
const isComponentLevelEvent =
result.type === UPDATE_COMPONENTS &&
result.payload &&
result.payload.nextComponents;
// trigger dashboardFilters state update if dashboard layout is changed.
if (!isComponentLevelEvent) {
const components = getState().dashboardLayout.present;
dispatch(updateLayoutComponents(components));
}
if (!getState().dashboardState.hasUnsavedChanges) {
dispatch(setUnsavedChanges(true));
}
};
}
export const updateComponents = setUnsavedChangesAfterAction(
nextComponents => ({
type: UPDATE_COMPONENTS,
payload: {
nextComponents,
},
}),
);
export function updateDashboardTitle(text) {
return (dispatch, getState) => {
const { dashboardLayout } = getState();
dispatch(
updateComponents({
[DASHBOARD_HEADER_ID]: {
...dashboardLayout.present[DASHBOARD_HEADER_ID],
meta: {
text,
},
},
}),
);
};
}
export const DASHBOARD_TITLE_CHANGED = 'DASHBOARD_TITLE_CHANGED';
// call this one when it's not an undo-able action
export function dashboardTitleChanged(text) {
return {
type: DASHBOARD_TITLE_CHANGED,
text,
};
}
export const DELETE_COMPONENT = 'DELETE_COMPONENT';
export const deleteComponent = setUnsavedChangesAfterAction((id, parentId) => ({
type: DELETE_COMPONENT,
payload: {
id,
parentId,
},
}));
export const CREATE_COMPONENT = 'CREATE_COMPONENT';
export const createComponent = setUnsavedChangesAfterAction(dropResult => ({
type: CREATE_COMPONENT,
payload: {
dropResult,
},
}));
// Tabs -----------------------------------------------------------------------
export const CREATE_TOP_LEVEL_TABS = 'CREATE_TOP_LEVEL_TABS';
export const createTopLevelTabs = setUnsavedChangesAfterAction(dropResult => ({
type: CREATE_TOP_LEVEL_TABS,
payload: {
dropResult,
},
}));
export const DELETE_TOP_LEVEL_TABS = 'DELETE_TOP_LEVEL_TABS';
export const deleteTopLevelTabs = setUnsavedChangesAfterAction(() => ({
type: DELETE_TOP_LEVEL_TABS,
payload: {},
}));
// Resize ---------------------------------------------------------------------
export const RESIZE_COMPONENT = 'RESIZE_COMPONENT';
export function resizeComponent({ id, width, height }) {
return (dispatch, getState) => {
const { dashboardLayout: undoableLayout } = getState();
const { present: dashboard } = undoableLayout;
const component = dashboard[id];
const widthChanged = width && component.meta.width !== width;
const heightChanged = height && component.meta.height !== height;
if (component && (widthChanged || heightChanged)) {
// update the size of this component
const updatedComponents = {
[id]: {
...component,
meta: {
...component.meta,
width: width || component.meta.width,
height: height || component.meta.height,
},
},
};
dispatch(updateComponents(updatedComponents));
}
};
}
// Drag and drop --------------------------------------------------------------
export const MOVE_COMPONENT = 'MOVE_COMPONENT';
const moveComponent = setUnsavedChangesAfterAction(dropResult => ({
type: MOVE_COMPONENT,
payload: {
dropResult,
},
}));
export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
export function handleComponentDrop(dropResult) {
return (dispatch, getState) => {
const overflowsParent = dropOverflowsParent(
dropResult,
getState().dashboardLayout.present,
);
if (overflowsParent) {
return dispatch(
addWarningToast(
t(
`There is not enough space for this component. Try decreasing its width, or increasing the destination width.`,
),
),
);
}
const { source, destination } = dropResult;
const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_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) {
dispatch(createTopLevelTabs(dropResult));
} else if (destination && isNewComponent) {
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 (
destination &&
source &&
!(
// ensure it has moved
(destination.id === source.id && destination.index === source.index)
)
) {
dispatch(moveComponent(dropResult));
}
// call getState() again down here in case redux state is stale after
// previous dispatch(es)
const { dashboardFilters, dashboardLayout: undoableLayout } = getState();
// if we moved a child from a Tab or Row parent and it was the only child, delete the parent.
if (!isNewComponent) {
const { present: layout } = undoableLayout;
const sourceComponent = layout[source.id] || {};
const destinationComponent = layout[destination.id] || {};
if (
(sourceComponent.type === TABS_TYPE ||
sourceComponent.type === ROW_TYPE) &&
sourceComponent.children &&
sourceComponent.children.length === 0
) {
const parentId = findParentId({
childId: source.id,
layout,
});
dispatch(deleteComponent(source.id, parentId));
}
// show warning if item has been moved between different scope
if (
isInDifferentFilterScopes({
dashboardFilters,
source: (sourceComponent.parents || []).concat(source.id),
destination: (destinationComponent.parents || []).concat(
destination.id,
),
})
) {
dispatch(
addWarningToast(
t('This chart has been moved to a different filter scope.'),
),
);
}
}
return null;
};
}
// Undo redo ------------------------------------------------------------------
export function undoLayoutAction() {
return (dispatch, getState) => {
dispatch(UndoActionCreators.undo());
const { dashboardLayout, dashboardState } = getState();
if (
dashboardLayout.past.length === 0 &&
!dashboardState.maxUndoHistoryExceeded &&
!dashboardState.updatedColorScheme
) {
dispatch(setUnsavedChanges(false));
}
};
}
export const redoLayoutAction = setUnsavedChangesAfterAction(
UndoActionCreators.redo,
);
// Update component parents list ----------------------------------------------
export const UPDATE_COMPONENTS_PARENTS_LIST = 'UPDATE_COMPONENTS_PARENTS_LIST';