[Canvas] Adds edit menu (#64738)

This commit is contained in:
Catherine Liu 2020-04-30 14:51:24 -07:00 committed by GitHub
parent 127b324a5f
commit f9c1033d41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1100 additions and 826 deletions

View file

@ -804,17 +804,6 @@ export const ComponentStrings = {
}),
},
SidebarHeader: {
getAlignmentMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.alignmentMenuItemLabel', {
defaultMessage: 'Alignment',
description:
'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' +
'alignment options of the selected elements',
}),
getBottomAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel', {
defaultMessage: 'Bottom',
}),
getBringForwardAriaLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', {
defaultMessage: 'Move element up one layer',
@ -823,56 +812,6 @@ export const ComponentStrings = {
i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', {
defaultMessage: 'Move element to top layer',
}),
getCenterAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.centerAlignMenuItemLabel', {
defaultMessage: 'Center',
description: 'This refers to alignment centered horizontally.',
}),
getContextMenuTitle: () =>
i18n.translate('xpack.canvas.sidebarHeader.contextMenuAriaLabel', {
defaultMessage: 'Element options',
}),
getCreateElementModalTitle: () =>
i18n.translate('xpack.canvas.sidebarHeader.createElementModalTitle', {
defaultMessage: 'Create new element',
}),
getDistributionMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.distributionMenutItemLabel', {
defaultMessage: 'Distribution',
description:
'This refers to the options to evenly spacing the selected elements horizontall or vertically.',
}),
getGroupMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.groupMenuItemLabel', {
defaultMessage: 'Group',
description: 'This refers to grouping multiple selected elements.',
}),
getHorizontalDistributionMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel', {
defaultMessage: 'Horizontal',
}),
getLeftAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.leftAlignMenuItemLabel', {
defaultMessage: 'Left',
}),
getMiddleAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.middleAlignMenuItemLabel', {
defaultMessage: 'Middle',
description: 'This refers to alignment centered vertically.',
}),
getOrderMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.orderMenuItemLabel', {
defaultMessage: 'Order',
description: 'Refers to the order of the elements displayed on the page from front to back',
}),
getRightAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.rightAlignMenuItemLabel', {
defaultMessage: 'Right',
}),
getSaveElementMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.savedElementMenuItemLabel', {
defaultMessage: 'Save as new element',
}),
getSendBackwardAriaLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', {
defaultMessage: 'Move element down one layer',
@ -881,19 +820,6 @@ export const ComponentStrings = {
i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', {
defaultMessage: 'Move element to bottom layer',
}),
getTopAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.topAlignMenuItemLabel', {
defaultMessage: 'Top',
}),
getUngroupMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.ungroupMenuItemLabel', {
defaultMessage: 'Ungroup',
description: 'This refers to ungrouping a grouped element',
}),
getVerticalDistributionMenuItemLabel: () =>
i18n.translate('xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel', {
defaultMessage: 'Vertical',
}),
},
TextStylePicker: {
getAlignCenterOption: () =>
@ -1099,6 +1025,94 @@ export const ComponentStrings = {
defaultMessage: 'Set a custom interval',
}),
},
WorkpadHeaderEditMenu: {
getAlignmentMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel', {
defaultMessage: 'Alignment',
description:
'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' +
'alignment options of the selected elements',
}),
getBottomAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel', {
defaultMessage: 'Bottom',
}),
getCenterAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel', {
defaultMessage: 'Center',
description: 'This refers to alignment centered horizontally.',
}),
getCreateElementModalTitle: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.createElementModalTitle', {
defaultMessage: 'Create new element',
}),
getDistributionMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel', {
defaultMessage: 'Distribution',
description:
'This refers to the options to evenly spacing the selected elements horizontall or vertically.',
}),
getEditMenuButtonLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuButtonLabel', {
defaultMessage: 'Edit',
}),
getEditMenuLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuLabel', {
defaultMessage: 'Edit options',
}),
getGroupMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel', {
defaultMessage: 'Group',
description: 'This refers to grouping multiple selected elements.',
}),
getHorizontalDistributionMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel', {
defaultMessage: 'Horizontal',
}),
getLeftAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel', {
defaultMessage: 'Left',
}),
getMiddleAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel', {
defaultMessage: 'Middle',
description: 'This refers to alignment centered vertically.',
}),
getOrderMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel', {
defaultMessage: 'Order',
description: 'Refers to the order of the elements displayed on the page from front to back',
}),
getRedoMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.redoMenuItemLabel', {
defaultMessage: 'Redo',
}),
getRightAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel', {
defaultMessage: 'Right',
}),
getSaveElementMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel', {
defaultMessage: 'Save as new element',
}),
getTopAlignMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel', {
defaultMessage: 'Top',
}),
getUndoMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.undoMenuItemLabel', {
defaultMessage: 'Undo',
}),
getUngroupMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel', {
defaultMessage: 'Ungroup',
description: 'This refers to ungrouping a grouped element',
}),
getVerticalDistributionMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel', {
defaultMessage: 'Vertical',
}),
},
WorkpadHeaderElementMenu: {
getAssetsMenuItemLabel: () =>
i18n.translate('xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel', {

View file

@ -42,10 +42,10 @@ export const ShortcutStrings = {
defaultMessage: 'Delete',
}),
BRING_FORWARD: i18n.translate('xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText', {
defaultMessage: 'Bring to front',
defaultMessage: 'Bring forward',
}),
BRING_TO_FRONT: i18n.translate('xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText', {
defaultMessage: 'Bring forward',
defaultMessage: 'Bring to front',
}),
SEND_BACKWARD: i18n.translate('xpack.canvas.keyboardShortcuts.sendBackwardShortcutHelpText', {
defaultMessage: 'Send backward',

View file

@ -43,7 +43,7 @@ export class WorkpadApp extends React.PureComponent {
<div className="canvasLayout__cols">
<div className="canvasLayout__stage">
<div className="canvasLayout__stageHeader">
<WorkpadHeader />
<WorkpadHeader commit={this.interactivePageLayout || (() => {})} />
</div>
<div
@ -66,7 +66,7 @@ export class WorkpadApp extends React.PureComponent {
{isWriteable && (
<div className="canvasLayout__sidebar hide-for-sharing">
<Sidebar commit={this.interactivePageLayout || (() => {})} />
<Sidebar />
</div>
)}
</div>

View file

@ -212,7 +212,7 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = `
<dt
className="euiDescriptionList__title"
>
Bring forward
Bring to front
</dt>
<dd
className="euiDescriptionList__description"
@ -234,7 +234,7 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = `
<dt
className="euiDescriptionList__title"
>
Bring to front
Bring forward
</dt>
<dd
className="euiDescriptionList__description"

View file

@ -10,7 +10,6 @@ import { compose, branch, renderComponent } from 'recompose';
import { EuiSpacer } from '@elastic/eui';
import { getSelectedToplevelNodes, getSelectedElementId } from '../../state/selectors/workpad';
import { SidebarHeader } from '../sidebar_header';
import { globalStateUpdater } from '../workpad_page/integration_utils';
import { ComponentStrings } from '../../../i18n';
import { MultiElementSettings } from './multi_element_settings';
import { GroupSettings } from './group_settings';
@ -22,45 +21,19 @@ const { SidebarContent: strings } = ComponentStrings;
const mapStateToProps = state => ({
selectedToplevelNodes: getSelectedToplevelNodes(state),
selectedElementId: getSelectedElementId(state),
state,
});
const mergeProps = (
{ state, ...restStateProps },
{ dispatch, ...restDispatchProps },
ownProps
) => ({
...ownProps,
...restDispatchProps,
...restStateProps,
updateGlobalState: globalStateUpdater(dispatch, state),
});
const withGlobalState = (commit, updateGlobalState) => (type, payload) => {
const newLayoutState = commit(type, payload);
if (newLayoutState.currentScene.gestureEnd) {
updateGlobalState(newLayoutState);
}
};
const MultiElementSidebar = ({ commit, updateGlobalState }) => (
const MultiElementSidebar = () => (
<Fragment>
<SidebarHeader
title={strings.getMultiElementSidebarTitle()}
commit={withGlobalState(commit, updateGlobalState)}
/>
<SidebarHeader title={strings.getMultiElementSidebarTitle()} />
<EuiSpacer />
<MultiElementSettings />
</Fragment>
);
const GroupedElementSidebar = ({ commit, updateGlobalState }) => (
const GroupedElementSidebar = () => (
<Fragment>
<SidebarHeader
title={strings.getGroupedElementSidebarTitle()}
commit={withGlobalState(commit, updateGlobalState)}
groupIsSelected
/>
<SidebarHeader title={strings.getGroupedElementSidebarTitle()} groupIsSelected />
<EuiSpacer />
<GroupSettings />
</Fragment>
@ -92,7 +65,4 @@ const branches = [
),
];
export const SidebarContent = compose(
connect(mapStateToProps, null, mergeProps),
...branches
)(GlobalConfig);
export const SidebarContent = compose(connect(mapStateToProps), ...branches)(GlobalConfig);

View file

@ -20,80 +20,6 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = `
Selected layer
</h3>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Save as new element"
className="euiButtonIcon euiButtonIcon--text"
data-test-subj="canvasSidebarHeader__saveElementButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="indexOpen"
size="m"
/>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiPopover euiPopover--anchorDownCenter canvasContextMenu"
container={null}
id="sidebar-context-menu-popover"
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Element options"
className="euiButtonIcon euiButtonIcon--text"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="boxesVertical"
size="m"
/>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@ -224,72 +150,6 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = `
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Save as new element"
className="euiButtonIcon euiButtonIcon--text"
data-test-subj="canvasSidebarHeader__saveElementButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="indexOpen"
size="m"
/>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiPopover euiPopover--anchorDownCenter canvasContextMenu"
container={null}
id="sidebar-context-menu-popover"
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Element options"
className="euiButtonIcon euiButtonIcon--text"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="boxesVertical"
size="m"
/>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -10,26 +10,10 @@ import { action } from '@storybook/addon-actions';
import { SidebarHeader } from '../sidebar_header';
const handlers = {
cloneNodes: action('cloneNodes'),
copyNodes: action('copyNodes'),
cutNodes: action('cutNodes'),
pasteNodes: action('pasteNodes'),
deleteNodes: action('deleteNodes'),
bringToFront: action('bringToFront'),
bringForward: action('bringForward'),
sendBackward: action('sendBackward'),
sendToBack: action('sendToBack'),
createCustomElement: action('createCustomElement'),
groupNodes: action('groupNodes'),
ungroupNodes: action('ungroupNodes'),
alignLeft: action('alignLeft'),
alignMiddle: action('alignMiddle'),
alignRight: action('alignRight'),
alignTop: action('alignTop'),
alignCenter: action('alignCenter'),
alignBottom: action('alignBottom'),
distributeHorizontally: action('distributeHorizontally'),
distributeVertically: action('distributeVertically'),
};
storiesOf('components/Sidebar/SidebarHeader', module)

View file

@ -4,25 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component, Fragment } from 'react';
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiButtonIcon,
EuiContextMenu,
EuiToolTip,
EuiContextMenuPanelItemDescriptor,
EuiContextMenuPanelDescriptor,
EuiOverlayMask,
} from '@elastic/eui';
import { Popover } from '../popover';
import { CustomElementModal } from '../custom_element_modal';
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { ToolTipShortcut } from '../tool_tip_shortcut/';
import { ComponentStrings } from '../../../i18n/components';
import { ShortcutStrings } from '../../../i18n/shortcuts';
import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../common/lib/constants';
const { SidebarHeader: strings } = ComponentStrings;
const shortcutHelp = ShortcutStrings.getShortcutHelp();
@ -36,26 +23,6 @@ interface Props {
* indicated whether or not layer controls should be displayed
*/
showLayerControls?: boolean;
/**
* cuts selected elements
*/
cutNodes: () => void;
/**
* copies selected elements to clipboard
*/
copyNodes: () => void;
/**
* pastes elements stored in clipboard to page
*/
pasteNodes: () => void;
/**
* clones selected elements
*/
cloneNodes: () => void;
/**
* deletes selected elements
*/
deleteNodes: () => void;
/**
* moves selected element to top layer
*/
@ -72,493 +39,117 @@ interface Props {
* moves selected element to bottom layer
*/
sendToBack: () => void;
/**
* saves the selected elements as an custom-element saved object
*/
createCustomElement: (name: string, description: string, image: string) => void;
/**
* indicated whether the selected element is a group or not
*/
groupIsSelected: boolean;
/**
* only more than one selected element can be grouped
*/
selectedNodes: string[];
/**
* groups selected elements
*/
groupNodes: () => void;
/**
* ungroups selected group
*/
ungroupNodes: () => void;
/**
* left align selected elements
*/
alignLeft: () => void;
/**
* center align selected elements
*/
alignCenter: () => void;
/**
* right align selected elements
*/
alignRight: () => void;
/**
* top align selected elements
*/
alignTop: () => void;
/**
* middle align selected elements
*/
alignMiddle: () => void;
/**
* bottom align selected elements
*/
alignBottom: () => void;
/**
* horizontally distribute selected elements
*/
distributeHorizontally: () => void;
/**
* vertically distribute selected elements
*/
distributeVertically: () => void;
}
interface State {
/**
* indicates whether or not the custom element modal is open
*/
isModalVisible: boolean;
}
interface MenuTuple {
menuItem: EuiContextMenuPanelItemDescriptor;
panel: EuiContextMenuPanelDescriptor;
}
const contextMenuButton = (handleClick: React.MouseEventHandler<HTMLButtonElement>) => (
<EuiButtonIcon
color="text"
iconType="boxesVertical"
onClick={handleClick}
aria-label={strings.getContextMenuTitle()}
/>
);
export class SidebarHeader extends Component<Props, State> {
public static propTypes = {
title: PropTypes.string.isRequired,
showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements
cutNodes: PropTypes.func.isRequired,
copyNodes: PropTypes.func.isRequired,
pasteNodes: PropTypes.func.isRequired,
cloneNodes: PropTypes.func.isRequired,
deleteNodes: PropTypes.func.isRequired,
bringToFront: PropTypes.func.isRequired,
bringForward: PropTypes.func.isRequired,
sendBackward: PropTypes.func.isRequired,
sendToBack: PropTypes.func.isRequired,
createCustomElement: PropTypes.func.isRequired,
groupIsSelected: PropTypes.bool,
selectedNodes: PropTypes.array,
groupNodes: PropTypes.func.isRequired,
ungroupNodes: PropTypes.func.isRequired,
alignLeft: PropTypes.func.isRequired,
alignCenter: PropTypes.func.isRequired,
alignRight: PropTypes.func.isRequired,
alignTop: PropTypes.func.isRequired,
alignMiddle: PropTypes.func.isRequired,
alignBottom: PropTypes.func.isRequired,
distributeHorizontally: PropTypes.func.isRequired,
distributeVertically: PropTypes.func.isRequired,
};
public static defaultProps = {
groupIsSelected: false,
showLayerControls: false,
selectedNodes: [],
};
public state = {
isModalVisible: false,
};
private _isMounted = false;
private _showModal = () => this._isMounted && this.setState({ isModalVisible: true });
private _hideModal = () => this._isMounted && this.setState({ isModalVisible: false });
public componentDidMount() {
this._isMounted = true;
}
public componentWillUnmount() {
this._isMounted = false;
}
private _renderLayoutControls = () => {
const { bringToFront, bringForward, sendBackward, sendToBack } = this.props;
return (
<Fragment>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.BRING_TO_FRONT}
<ToolTipShortcut namespace="ELEMENT" action="BRING_TO_FRONT" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="sortUp"
onClick={bringToFront}
aria-label={strings.getBringToFrontAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.BRING_FORWARD}
<ToolTipShortcut namespace="ELEMENT" action="BRING_FORWARD" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="arrowUp"
onClick={bringForward}
aria-label={strings.getBringForwardAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.SEND_BACKWARD}
<ToolTipShortcut namespace="ELEMENT" action="SEND_BACKWARD" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="arrowDown"
onClick={sendBackward}
aria-label={strings.getSendBackwardAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.SEND_TO_BACK}
<ToolTipShortcut namespace="ELEMENT" action="SEND_TO_BACK" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="sortDown"
onClick={sendToBack}
aria-label={strings.getSendToBackAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
</Fragment>
);
};
private _getLayerMenuItems = (): MenuTuple => {
const { bringToFront, bringForward, sendBackward, sendToBack } = this.props;
return {
menuItem: {
name: strings.getOrderMenuItemLabel(),
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
panel: 1,
},
panel: {
id: 1,
title: strings.getOrderMenuItemLabel(),
items: [
{
name: shortcutHelp.BRING_TO_FRONT, // TODO: check against current element position and disable if already top layer
icon: 'sortUp',
onClick: bringToFront,
},
{
name: shortcutHelp.BRING_TO_FRONT, // TODO: same as above
icon: 'arrowUp',
onClick: bringForward,
},
{
name: shortcutHelp.SEND_BACKWARD, // TODO: check against current element position and disable if already bottom layer
icon: 'arrowDown',
onClick: sendBackward,
},
{
name: shortcutHelp.SEND_TO_BACK, // TODO: same as above
icon: 'sortDown',
onClick: sendToBack,
},
],
},
};
};
private _getAlignmentMenuItems = (close: (fn: () => void) => () => void): MenuTuple => {
const { alignLeft, alignCenter, alignRight, alignTop, alignMiddle, alignBottom } = this.props;
return {
menuItem: {
name: strings.getAlignmentMenuItemLabel(),
className: 'canvasContextMenu',
panel: 2,
},
panel: {
id: 2,
title: strings.getAlignmentMenuItemLabel(),
items: [
{
name: strings.getLeftAlignMenuItemLabel(),
icon: 'editorItemAlignLeft',
onClick: close(alignLeft),
},
{
name: strings.getCenterAlignMenuItemLabel(),
icon: 'editorItemAlignCenter',
onClick: close(alignCenter),
},
{
name: strings.getRightAlignMenuItemLabel(),
icon: 'editorItemAlignRight',
onClick: close(alignRight),
},
{
name: strings.getTopAlignMenuItemLabel(),
icon: 'editorItemAlignTop',
onClick: close(alignTop),
},
{
name: strings.getMiddleAlignMenuItemLabel(),
icon: 'editorItemAlignMiddle',
onClick: close(alignMiddle),
},
{
name: strings.getBottomAlignMenuItemLabel(),
icon: 'editorItemAlignBottom',
onClick: close(alignBottom),
},
],
},
};
};
private _getDistributionMenuItems = (close: (fn: () => void) => () => void): MenuTuple => {
const { distributeHorizontally, distributeVertically } = this.props;
return {
menuItem: {
name: strings.getDistributionMenuItemLabel(),
className: 'canvasContextMenu',
panel: 3,
},
panel: {
id: 3,
title: strings.getDistributionMenuItemLabel(),
items: [
{
name: strings.getHorizontalDistributionMenuItemLabel(),
icon: 'editorDistributeHorizontal',
onClick: close(distributeHorizontally),
},
{
name: strings.getVerticalDistributionMenuItemLabel(),
icon: 'editorDistributeVertical',
onClick: close(distributeVertically),
},
],
},
};
};
private _getGroupMenuItems = (
close: (fn: () => void) => () => void
): EuiContextMenuPanelItemDescriptor[] => {
const { groupIsSelected, ungroupNodes, groupNodes, selectedNodes } = this.props;
return groupIsSelected
? [
{
name: strings.getUngroupMenuItemLabel(),
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
onClick: close(ungroupNodes),
},
]
: selectedNodes.length > 1
? [
{
name: strings.getGroupMenuItemLabel(),
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
onClick: close(groupNodes),
},
]
: [];
};
private _getPanels = (closePopover: () => void): EuiContextMenuPanelDescriptor[] => {
const {
showLayerControls,
cutNodes,
copyNodes,
pasteNodes,
deleteNodes,
cloneNodes,
} = this.props;
// closes popover after invoking fn
const close = (fn: () => void) => () => {
fn();
closePopover();
};
const items: EuiContextMenuPanelItemDescriptor[] = [
{
name: shortcutHelp.CUT,
icon: 'cut',
onClick: close(cutNodes),
},
{
name: shortcutHelp.COPY,
icon: 'copy',
onClick: copyNodes,
},
{
name: shortcutHelp.PASTE, // TODO: can this be disabled if clipboard is empty?
icon: 'copyClipboard',
onClick: close(pasteNodes),
},
{
name: shortcutHelp.DELETE,
icon: 'trash',
onClick: close(deleteNodes),
},
{
name: shortcutHelp.CLONE,
onClick: close(cloneNodes),
},
...this._getGroupMenuItems(close),
];
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: strings.getContextMenuTitle(),
items,
},
];
const fillMenu = ({ menuItem, panel }: MenuTuple) => {
items.push(menuItem); // add Order menu item to first panel
panels.push(panel); // add nested panel for layers controls
};
if (showLayerControls) {
fillMenu(this._getLayerMenuItems());
}
if (this.props.selectedNodes.length > 1) {
fillMenu(this._getAlignmentMenuItems(close));
}
if (this.props.selectedNodes.length > 2) {
fillMenu(this._getDistributionMenuItems(close));
}
items.push({
name: strings.getSaveElementMenuItemLabel(),
icon: 'indexOpen',
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
onClick: this._showModal,
});
return panels;
};
private _renderContextMenu = () => (
<Popover
id="sidebar-context-menu-popover"
className="canvasContextMenu"
button={contextMenuButton}
panelPaddingSize="none"
tooltip={strings.getContextMenuTitle()}
tooltipPosition="bottom"
>
{({ closePopover }: { closePopover: () => void }) => (
<EuiContextMenu initialPanelId={0} panels={this._getPanels(closePopover)} />
)}
</Popover>
);
private _handleSave = (name: string, description: string, image: string) => {
const { createCustomElement } = this.props;
createCustomElement(name, description, image);
this._hideModal();
};
render() {
const { title, showLayerControls } = this.props;
const { isModalVisible } = this.state;
return (
<Fragment>
<EuiFlexGroup
className="canvasLayout__sidebarHeader"
gutterSize="none"
alignItems="center"
justifyContent="spaceBetween"
>
export const SidebarHeader: FunctionComponent<Props> = ({
title,
showLayerControls,
bringToFront,
bringForward,
sendBackward,
sendToBack,
}) => (
<EuiFlexGroup
className="canvasLayout__sidebarHeader"
gutterSize="none"
alignItems="center"
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
{showLayerControls ? (
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>{title}</h3>
</EuiTitle>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.BRING_TO_FRONT}
<ToolTipShortcut namespace="ELEMENT" action="BRING_TO_FRONT" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="sortUp"
onClick={bringToFront}
aria-label={strings.getBringToFrontAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none">
{showLayerControls ? this._renderLayoutControls() : null}
<EuiFlexItem grow={false}>
<EuiToolTip position="bottom" content={strings.getSaveElementMenuItemLabel()}>
<EuiButtonIcon
color="text"
iconType="indexOpen"
onClick={this._showModal}
data-test-subj="canvasSidebarHeader__saveElementButton"
aria-label={strings.getSaveElementMenuItemLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>{this._renderContextMenu()}</EuiFlexItem>
</EuiFlexGroup>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.BRING_FORWARD}
<ToolTipShortcut namespace="ELEMENT" action="BRING_FORWARD" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="arrowUp"
onClick={bringForward}
aria-label={strings.getBringForwardAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.SEND_BACKWARD}
<ToolTipShortcut namespace="ELEMENT" action="SEND_BACKWARD" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="arrowDown"
onClick={sendBackward}
aria-label={strings.getSendBackwardAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="bottom"
content={
<span>
{shortcutHelp.SEND_TO_BACK}
<ToolTipShortcut namespace="ELEMENT" action="SEND_TO_BACK" />
</span>
}
>
<EuiButtonIcon
color="text"
iconType="sortDown"
onClick={sendToBack}
aria-label={strings.getSendToBackAriaLabel()}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
{isModalVisible ? (
<EuiOverlayMask>
<CustomElementModal
title={strings.getCreateElementModalTitle()}
onSave={this._handleSave}
onCancel={this._hideModal}
/>
</EuiOverlayMask>
) : null}
</Fragment>
);
}
}
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
SidebarHeader.propTypes = {
title: PropTypes.string.isRequired,
showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements
bringToFront: PropTypes.func.isRequired,
bringForward: PropTypes.func.isRequired,
sendBackward: PropTypes.func.isRequired,
sendToBack: PropTypes.func.isRequired,
};
SidebarHeader.defaultProps = {
showLayerControls: false,
};

View file

@ -0,0 +1,205 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/WorkpadHeader/EditMenu 2 elements selected 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Edit options"
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
data-test-subj="canvasWorkpadEditMenuButton"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Edit
</span>
</span>
</button>
</div>
</div>
`;
exports[`Storyshots components/WorkpadHeader/EditMenu 3+ elements selected 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Edit options"
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
data-test-subj="canvasWorkpadEditMenuButton"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Edit
</span>
</span>
</button>
</div>
</div>
`;
exports[`Storyshots components/WorkpadHeader/EditMenu clipboard data exists 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Edit options"
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
data-test-subj="canvasWorkpadEditMenuButton"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Edit
</span>
</span>
</button>
</div>
</div>
`;
exports[`Storyshots components/WorkpadHeader/EditMenu default 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Edit options"
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
data-test-subj="canvasWorkpadEditMenuButton"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Edit
</span>
</span>
</button>
</div>
</div>
`;
exports[`Storyshots components/WorkpadHeader/EditMenu single element selected 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Edit options"
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
data-test-subj="canvasWorkpadEditMenuButton"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Edit
</span>
</span>
</button>
</div>
</div>
`;
exports[`Storyshots components/WorkpadHeader/EditMenu single grouped element selected 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Edit options"
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
data-test-subj="canvasWorkpadEditMenuButton"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Edit
</span>
</span>
</button>
</div>
</div>
`;

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import React from 'react';
import { EditMenu } from '../edit_menu';
const handlers = {
cutNodes: action('cutNodes'),
copyNodes: action('copyNodes'),
pasteNodes: action('pasteNodes'),
deleteNodes: action('deleteNodes'),
cloneNodes: action('cloneNodes'),
bringToFront: action('bringToFront'),
bringForward: action('bringForward'),
sendBackward: action('sendBackward'),
sendToBack: action('sendToBack'),
alignLeft: action('alignLeft'),
alignCenter: action('alignCenter'),
alignRight: action('alignRight'),
alignTop: action('alignTop'),
alignMiddle: action('alignMiddle'),
alignBottom: action('alignBottom'),
distributeHorizontally: action('distributeHorizontally'),
distributeVertically: action('distributeVertically'),
createCustomElement: action('createCustomElement'),
groupNodes: action('groupNodes'),
ungroupNodes: action('ungroupNodes'),
undoHistory: action('undoHistory'),
redoHistory: action('redoHistory'),
};
storiesOf('components/WorkpadHeader/EditMenu', module)
.add('default', () => (
<EditMenu selectedNodes={[]} groupIsSelected={false} hasPasteData={false} {...handlers} />
))
.add('clipboard data exists', () => (
<EditMenu selectedNodes={[]} groupIsSelected={false} hasPasteData={true} {...handlers} />
))
.add('single element selected', () => (
<EditMenu selectedNodes={['foo']} groupIsSelected={false} hasPasteData={false} {...handlers} />
))
.add('single grouped element selected', () => (
<EditMenu
selectedNodes={['foo', 'bar']}
groupIsSelected={true}
hasPasteData={false}
{...handlers}
/>
))
.add('2 elements selected', () => (
<EditMenu
selectedNodes={['foo', 'bar']}
groupIsSelected={false}
hasPasteData={false}
{...handlers}
/>
))
.add('3+ elements selected', () => (
<EditMenu
selectedNodes={['foo', 'bar', 'fizz']}
groupIsSelected={false}
hasPasteData={false}
{...handlers}
/>
));

View file

@ -0,0 +1,448 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FunctionComponent, useState } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiOverlayMask } from '@elastic/eui';
import { ComponentStrings } from '../../../../i18n/components';
import { ShortcutStrings } from '../../../../i18n/shortcuts';
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
import { Popover, ClosePopoverFn } from '../../popover';
import { CustomElementModal } from '../../custom_element_modal';
import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib/constants';
const { WorkpadHeaderEditMenu: strings } = ComponentStrings;
const shortcutHelp = ShortcutStrings.getShortcutHelp();
export interface Props {
/**
* cuts selected elements
*/
cutNodes: () => void;
/**
* copies selected elements to clipboard
*/
copyNodes: () => void;
/**
* pastes elements stored in clipboard to page
*/
pasteNodes: () => void;
/**
* clones selected elements
*/
cloneNodes: () => void;
/**
* deletes selected elements
*/
deleteNodes: () => void;
/**
* moves selected element to top layer
*/
bringToFront: () => void;
/**
* moves selected element up one layer
*/
bringForward: () => void;
/**
* moves selected element down one layer
*/
sendBackward: () => void;
/**
* moves selected element to bottom layer
*/
sendToBack: () => void;
/**
* saves the selected elements as an custom-element saved object
*/
createCustomElement: (name: string, description: string, image: string) => void;
/**
* indicated whether the selected element is a group or not
*/
groupIsSelected: boolean;
/**
* only more than one selected element can be grouped
*/
selectedNodes: string[];
/**
* groups selected elements
*/
groupNodes: () => void;
/**
* ungroups selected group
*/
ungroupNodes: () => void;
/**
* left align selected elements
*/
alignLeft: () => void;
/**
* center align selected elements
*/
alignCenter: () => void;
/**
* right align selected elements
*/
alignRight: () => void;
/**
* top align selected elements
*/
alignTop: () => void;
/**
* middle align selected elements
*/
alignMiddle: () => void;
/**
* bottom align selected elements
*/
alignBottom: () => void;
/**
* horizontally distribute selected elements
*/
distributeHorizontally: () => void;
/**
* vertically distribute selected elements
*/
distributeVertically: () => void;
/**
* Reverts last change to the workpad
*/
undoHistory: () => void;
/**
* Reapplies last reverted change to the workpad
*/
redoHistory: () => void;
/**
* Is there element clipboard data to paste?
*/
hasPasteData: boolean;
}
export const EditMenu: FunctionComponent<Props> = ({
cutNodes,
copyNodes,
pasteNodes,
deleteNodes,
cloneNodes,
bringToFront,
bringForward,
sendBackward,
sendToBack,
alignLeft,
alignCenter,
alignRight,
alignTop,
alignMiddle,
alignBottom,
distributeHorizontally,
distributeVertically,
createCustomElement,
selectedNodes,
groupIsSelected,
groupNodes,
ungroupNodes,
undoHistory,
redoHistory,
hasPasteData,
}) => {
const [isModalVisible, setModalVisible] = useState(false);
const showModal = () => setModalVisible(true);
const hideModal = () => setModalVisible(false);
const handleSave = (name: string, description: string, image: string) => {
createCustomElement(name, description, image);
hideModal();
};
const editControl = (togglePopover: React.MouseEventHandler<any>) => (
<EuiButtonEmpty
size="xs"
aria-label={strings.getEditMenuLabel()}
onClick={togglePopover}
data-test-subj="canvasWorkpadEditMenuButton"
>
{strings.getEditMenuButtonLabel()}
</EuiButtonEmpty>
);
const getPanelTree = (closePopover: ClosePopoverFn) => {
const groupMenuItem = groupIsSelected
? {
name: strings.getUngroupMenuItemLabel(),
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
icon: <EuiIcon type="empty" size="m" />,
onClick: () => {
ungroupNodes();
closePopover();
},
}
: {
name: strings.getGroupMenuItemLabel(),
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
icon: <EuiIcon type="empty" size="m" />,
disabled: selectedNodes.length < 2,
onClick: () => {
groupNodes();
closePopover();
},
};
const orderMenuItem = {
name: strings.getOrderMenuItemLabel(),
disabled: selectedNodes.length !== 1, // TODO: change to === 0 when we support relayering multiple elements
icon: <EuiIcon type="empty" size="m" />,
panel: {
id: 1,
title: strings.getOrderMenuItemLabel(),
items: [
{
name: shortcutHelp.BRING_TO_FRONT, // TODO: check against current element position and disable if already top layer
icon: 'sortUp',
onClick: bringToFront,
},
{
name: shortcutHelp.BRING_FORWARD, // TODO: same as above
icon: 'arrowUp',
onClick: bringForward,
},
{
name: shortcutHelp.SEND_BACKWARD, // TODO: check against current element position and disable if already bottom layer
icon: 'arrowDown',
onClick: sendBackward,
},
{
name: shortcutHelp.SEND_TO_BACK, // TODO: same as above
icon: 'sortDown',
onClick: sendToBack,
},
],
},
};
const alignmentMenuItem = {
name: strings.getAlignmentMenuItemLabel(),
className: 'canvasContextMenu',
disabled: groupIsSelected || selectedNodes.length < 2,
icon: <EuiIcon type="empty" size="m" />,
panel: {
id: 2,
title: strings.getAlignmentMenuItemLabel(),
items: [
{
name: strings.getLeftAlignMenuItemLabel(),
icon: 'editorItemAlignLeft',
onClick: () => {
alignLeft();
closePopover();
},
},
{
name: strings.getCenterAlignMenuItemLabel(),
icon: 'editorItemAlignCenter',
onClick: () => {
alignCenter();
closePopover();
},
},
{
name: strings.getRightAlignMenuItemLabel(),
icon: 'editorItemAlignRight',
onClick: () => {
alignRight();
closePopover();
},
},
{
name: strings.getTopAlignMenuItemLabel(),
icon: 'editorItemAlignTop',
onClick: () => {
alignTop();
closePopover();
},
},
{
name: strings.getMiddleAlignMenuItemLabel(),
icon: 'editorItemAlignMiddle',
onClick: () => {
alignMiddle();
closePopover();
},
},
{
name: strings.getBottomAlignMenuItemLabel(),
icon: 'editorItemAlignBottom',
onClick: () => {
alignBottom();
closePopover();
},
},
],
},
};
const distributionMenuItem = {
name: strings.getDistributionMenuItemLabel(),
className: 'canvasContextMenu',
disabled: groupIsSelected || selectedNodes.length < 3,
icon: <EuiIcon type="empty" size="m" />,
panel: {
id: 3,
title: strings.getAlignmentMenuItemLabel(),
items: [
{
name: strings.getHorizontalDistributionMenuItemLabel(),
icon: 'editorDistributeHorizontal',
onClick: () => {
distributeHorizontally();
closePopover();
},
},
{
name: strings.getVerticalDistributionMenuItemLabel(),
icon: 'editorDistributeVertical',
onClick: () => {
distributeVertically();
closePopover();
},
},
],
},
};
const savedElementMenuItem = {
name: strings.getSaveElementMenuItemLabel(),
icon: <EuiIcon type="indexOpen" size="m" />,
disabled: selectedNodes.length < 1,
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
'data-test-subj': 'canvasWorkpadEditMenu__saveElementButton',
onClick: () => {
showModal();
closePopover();
},
};
const items = [
{
// TODO: check history and disable when there are no more changes to revert
name: strings.getUndoMenuItemLabel(),
icon: <EuiIcon type="editorUndo" size="m" />,
onClick: () => {
undoHistory();
},
},
{
// TODO: check history and disable when there are no more changes to reapply
name: strings.getRedoMenuItemLabel(),
icon: <EuiIcon type="editorRedo" size="m" />,
onClick: () => {
redoHistory();
},
},
{
name: shortcutHelp.CUT,
icon: <EuiIcon type="cut" size="m" />,
className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
disabled: selectedNodes.length < 1,
onClick: () => {
cutNodes();
closePopover();
},
},
{
name: shortcutHelp.COPY,
disabled: selectedNodes.length < 1,
icon: <EuiIcon type="copy" size="m" />,
onClick: () => {
copyNodes();
},
},
{
name: shortcutHelp.PASTE, // TODO: can this be disabled if clipboard is empty?
icon: <EuiIcon type="copyClipboard" size="m" />,
disabled: !hasPasteData,
onClick: () => {
pasteNodes();
closePopover();
},
},
{
name: shortcutHelp.DELETE,
icon: <EuiIcon type="trash" size="m" />,
disabled: selectedNodes.length < 1,
onClick: () => {
deleteNodes();
closePopover();
},
},
{
name: shortcutHelp.CLONE,
icon: <EuiIcon type="empty" size="m" />,
disabled: selectedNodes.length < 1,
onClick: () => {
cloneNodes();
closePopover();
},
},
groupMenuItem,
orderMenuItem,
alignmentMenuItem,
distributionMenuItem,
savedElementMenuItem,
];
return {
id: 0,
// title: strings.getEditMenuLabel(),
items,
};
};
return (
<Fragment>
<Popover button={editControl} panelPaddingSize="none" anchorPosition="downLeft">
{({ closePopover }: { closePopover: ClosePopoverFn }) => (
<EuiContextMenu
initialPanelId={0}
panels={flattenPanelTree(getPanelTree(closePopover))}
/>
)}
</Popover>
{isModalVisible ? (
<EuiOverlayMask>
<CustomElementModal
title={strings.getCreateElementModalTitle()}
onSave={handleSave}
onCancel={hideModal}
/>
</EuiOverlayMask>
) : null}
</Fragment>
);
};
EditMenu.propTypes = {
cutNodes: PropTypes.func.isRequired,
copyNodes: PropTypes.func.isRequired,
pasteNodes: PropTypes.func.isRequired,
deleteNodes: PropTypes.func.isRequired,
cloneNodes: PropTypes.func.isRequired,
bringToFront: PropTypes.func.isRequired,
bringForward: PropTypes.func.isRequired,
sendBackward: PropTypes.func.isRequired,
sendToBack: PropTypes.func.isRequired,
alignLeft: PropTypes.func.isRequired,
alignCenter: PropTypes.func.isRequired,
alignRight: PropTypes.func.isRequired,
alignTop: PropTypes.func.isRequired,
alignMiddle: PropTypes.func.isRequired,
alignBottom: PropTypes.func.isRequired,
distributeHorizontally: PropTypes.func.isRequired,
distributeVertically: PropTypes.func.isRequired,
createCustomElement: PropTypes.func.isRequired,
selectedNodes: PropTypes.arrayOf(PropTypes.string).isRequired,
groupIsSelected: PropTypes.bool.isRequired,
groupNodes: PropTypes.func.isRequired,
ungroupNodes: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { compose, withHandlers, withProps } from 'recompose';
import { Dispatch } from 'redux';
import { State, PositionedElement } from '../../../../types';
import { getClipboardData } from '../../../lib/clipboard';
// @ts-ignore Untyped local
import { flatten } from '../../../lib/aeroelastic/functional';
// @ts-ignore Untyped local
import { globalStateUpdater } from '../../workpad_page/integration_utils';
// @ts-ignore Untyped local
import { crawlTree } from '../../workpad_page/integration_utils';
// @ts-ignore Untyped local
import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements';
// @ts-ignore Untyped local
import { undoHistory, redoHistory } from '../../../state/actions/history';
// @ts-ignore Untyped local
import { selectToplevelNodes } from '../../../state/actions/transient';
import {
getSelectedPage,
getNodes,
getSelectedToplevelNodes,
} from '../../../state/selectors/workpad';
import {
layerHandlerCreators,
clipboardHandlerCreators,
basicHandlerCreators,
groupHandlerCreators,
alignmentDistributionHandlerCreators,
} from '../../../lib/element_handler_creators';
import { EditMenu as Component, Props as ComponentProps } from './edit_menu';
type LayoutState = any;
type CommitFn = (type: string, payload: any) => LayoutState;
interface OwnProps {
commit: CommitFn;
}
const withGlobalState = (
commit: CommitFn,
updateGlobalState: (layoutState: LayoutState) => void
) => (type: string, payload: any) => {
const newLayoutState = commit(type, payload);
if (newLayoutState.currentScene.gestureEnd) {
updateGlobalState(newLayoutState);
}
};
/*
* TODO: this is all copied from interactive_workpad_page and workpad_shortcuts
*/
const mapStateToProps = (state: State) => {
const pageId = getSelectedPage(state);
const nodes = getNodes(state, pageId) as PositionedElement[];
const selectedToplevelNodes = getSelectedToplevelNodes(state);
const selectedPrimaryShapeObjects = selectedToplevelNodes
.map((id: string) => nodes.find((s: PositionedElement) => s.id === id))
.filter((shape?: PositionedElement) => shape) as PositionedElement[];
const selectedPersistentPrimaryNodes = flatten(
selectedPrimaryShapeObjects.map((shape: PositionedElement) =>
nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group?
? [shape.id]
: nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map(s => s.id)
)
);
const selectedNodeIds = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes)));
return {
pageId,
selectedToplevelNodes,
selectedNodes: selectedNodeIds.map((id: string) => nodes.find(s => s.id === id)),
state,
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
insertNodes: (selectedNodes: PositionedElement[], pageId: string) =>
dispatch(insertNodes(selectedNodes, pageId)),
removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)),
selectToplevelNodes: (nodes: PositionedElement[]) =>
dispatch(
selectToplevelNodes(nodes.filter((e: PositionedElement) => !e.position.parent).map(e => e.id))
),
elementLayer: (pageId: string, elementId: string, movement: number) => {
dispatch(elementLayer({ pageId, elementId, movement }));
},
undoHistory: () => dispatch(undoHistory()),
redoHistory: () => dispatch(redoHistory()),
dispatch,
});
const mergeProps = (
{ state, selectedToplevelNodes, ...restStateProps }: ReturnType<typeof mapStateToProps>,
{ dispatch, ...restDispatchProps }: ReturnType<typeof mapDispatchToProps>,
{ commit }: OwnProps
) => {
const updateGlobalState = globalStateUpdater(dispatch, state);
return {
...restDispatchProps,
...restStateProps,
commit: withGlobalState(commit, updateGlobalState),
groupIsSelected:
selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'),
};
};
export const EditMenu = compose<ComponentProps, OwnProps>(
connect(mapStateToProps, mapDispatchToProps, mergeProps),
withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })),
withHandlers(basicHandlerCreators),
withHandlers(clipboardHandlerCreators),
withHandlers(layerHandlerCreators),
withHandlers(groupHandlerCreators),
withHandlers(alignmentDistributionHandlerCreators)
)(Component);

View file

@ -139,7 +139,6 @@ export const ElementMenu: FunctionComponent<Props> = ({
return {
id: 0,
title: strings.getElementMenuLabel(),
items: [
elementListToMenuItems(textElements),
elementListToMenuItems(shapeElements),

View file

@ -62,7 +62,6 @@ export const ShareMenu: FunctionComponent<Props> = ({ onCopy, onExport, getExpor
const getPanelTree = (closePopover: ClosePopoverFn) => ({
id: 0,
title: strings.getShareWorkpadMessage(),
items: [
{
name: strings.getShareDownloadJSONTitle(),

View file

@ -15,6 +15,7 @@ import { ToolTipShortcut } from '../tool_tip_shortcut/';
import { RefreshControl } from './refresh_control';
// @ts-ignore untyped local
import { FullscreenControl } from './fullscreen_control';
import { EditMenu } from './edit_menu';
import { ElementMenu } from './element_menu';
import { ShareMenu } from './share_menu';
import { ViewMenu } from './view_menu';
@ -25,12 +26,14 @@ export interface Props {
isWriteable: boolean;
toggleWriteable: () => void;
canUserWrite: boolean;
commit: (type: string, payload: any) => any;
}
export const WorkpadHeader: FunctionComponent<Props> = ({
isWriteable,
canUserWrite,
toggleWriteable,
commit,
}) => {
const keyHandler = (action: string) => {
if (action === 'EDITING') {
@ -99,6 +102,9 @@ export const WorkpadHeader: FunctionComponent<Props> = ({
<EuiFlexItem grow={false}>
<ViewMenu />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EditMenu commit={commit} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ShareMenu />
</EuiFlexItem>

View file

@ -363,7 +363,11 @@ export function getNodesForPage(page: CanvasPage, withAst: boolean): CanvasEleme
}
// todo unify or DRY up with `getElements`
export function getNodes(state: State, pageId: string, withAst = true): CanvasElement[] {
export function getNodes(
state: State,
pageId: string,
withAst = true
): CanvasElement[] | PositionedElement[] {
const id = pageId || getSelectedPage(state);
if (!id) {
return [];

View file

@ -75,4 +75,8 @@ export interface ElementPosition {
parent: string | null;
}
export type PositionedElement = CanvasElement & { ast: ExpressionAstExpression };
export type PositionedElement = CanvasElement & {
ast: ExpressionAstExpression;
} & {
position: ElementPosition;
};

View file

@ -5521,26 +5521,10 @@
"xpack.canvas.sidebarContent.groupedElementSidebarTitle": "グループ化されたエレメント",
"xpack.canvas.sidebarContent.multiElementSidebarTitle": "複数エレメント",
"xpack.canvas.sidebarContent.singleElementSidebarTitle": "選択されたエレメント",
"xpack.canvas.sidebarHeader.alignmentMenuItemLabel": "アラインメント",
"xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel": "一番下",
"xpack.canvas.sidebarHeader.bringForwardArialLabel": "エレメントを 1 つ上のレイヤーに移動",
"xpack.canvas.sidebarHeader.bringToFrontArialLabel": "エレメントを一番上のレイヤーに移動",
"xpack.canvas.sidebarHeader.centerAlignMenuItemLabel": "中央",
"xpack.canvas.sidebarHeader.contextMenuAriaLabel": "エレメントオプション",
"xpack.canvas.sidebarHeader.createElementModalTitle": "新規エレメントの作成",
"xpack.canvas.sidebarHeader.distributionMenutItemLabel": "分布",
"xpack.canvas.sidebarHeader.groupMenuItemLabel": "グループ",
"xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel": "横",
"xpack.canvas.sidebarHeader.leftAlignMenuItemLabel": "左",
"xpack.canvas.sidebarHeader.middleAlignMenuItemLabel": "真ん中",
"xpack.canvas.sidebarHeader.orderMenuItemLabel": "順序",
"xpack.canvas.sidebarHeader.rightAlignMenuItemLabel": "右",
"xpack.canvas.sidebarHeader.savedElementMenuItemLabel": "新規エレメントとして保存",
"xpack.canvas.sidebarHeader.sendBackwardArialLabel": "エレメントを 1 つ下のレイヤーに移動",
"xpack.canvas.sidebarHeader.sendToBackArialLabel": "エレメントを一番下のレイヤーに移動",
"xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "一番上",
"xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "グループ解除",
"xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "縦",
"xpack.canvas.tags.presentationTag": "プレゼンテーション",
"xpack.canvas.tags.reportTag": "レポート",
"xpack.canvas.templates.darkHelp": "ダークカラーテーマのプレゼンテーションデッキです",
@ -5854,6 +5838,18 @@
"xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "設定",
"xpack.canvas.workpadHeaderCustomInterval.formDescription": "{secondsExample}、{minutesExample}、{hoursExample} のような短い表記を使用します",
"xpack.canvas.workpadHeaderCustomInterval.formLabel": "カスタム間隔を設定",
"xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel": "アラインメント",
"xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel": "一番下",
"xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel": "中央",
"xpack.canvas.workpadHeaderEditMenu.createElementModalTitle": "新規エレメントの作成",
"xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel": "分布",
"xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel": "グループ",
"xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel": "横",
"xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel": "左",
"xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel": "真ん中",
"xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel": "順序",
"xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel": "右",
"xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel": "新規エレメントとして保存",
"xpack.canvas.workpadHeaderKioskControl.controlTitle": "全画面ページのサイクル",
"xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "サイクル間隔を変更",
"xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "スライドを自動的にサイクル",

View file

@ -5522,26 +5522,10 @@
"xpack.canvas.sidebarContent.groupedElementSidebarTitle": "已分组元素",
"xpack.canvas.sidebarContent.multiElementSidebarTitle": "多个元素",
"xpack.canvas.sidebarContent.singleElementSidebarTitle": "选定元素",
"xpack.canvas.sidebarHeader.alignmentMenuItemLabel": "对齐方式",
"xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel": "下",
"xpack.canvas.sidebarHeader.bringForwardArialLabel": "将元素上移一层",
"xpack.canvas.sidebarHeader.bringToFrontArialLabel": "将元素移到顶层",
"xpack.canvas.sidebarHeader.centerAlignMenuItemLabel": "中",
"xpack.canvas.sidebarHeader.contextMenuAriaLabel": "元素选项",
"xpack.canvas.sidebarHeader.createElementModalTitle": "创建新元素",
"xpack.canvas.sidebarHeader.distributionMenutItemLabel": "分布",
"xpack.canvas.sidebarHeader.groupMenuItemLabel": "分组",
"xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel": "水平",
"xpack.canvas.sidebarHeader.leftAlignMenuItemLabel": "左",
"xpack.canvas.sidebarHeader.middleAlignMenuItemLabel": "中",
"xpack.canvas.sidebarHeader.orderMenuItemLabel": "顺序",
"xpack.canvas.sidebarHeader.rightAlignMenuItemLabel": "右",
"xpack.canvas.sidebarHeader.savedElementMenuItemLabel": "另存为新元素",
"xpack.canvas.sidebarHeader.sendBackwardArialLabel": "将元素下移一层",
"xpack.canvas.sidebarHeader.sendToBackArialLabel": "将元素移到底层",
"xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "上",
"xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "取消分组",
"xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "垂直",
"xpack.canvas.tags.presentationTag": "演示",
"xpack.canvas.tags.reportTag": "报告",
"xpack.canvas.templates.darkHelp": "深色主题的演示幻灯片",
@ -5856,6 +5840,21 @@
"xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "设置",
"xpack.canvas.workpadHeaderCustomInterval.formDescription": "使用速记表示法,如 {secondsExample}、{minutesExample} 或 {hoursExample}",
"xpack.canvas.workpadHeaderCustomInterval.formLabel": "设置定制时间间隔",
"xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel": "对齐方式",
"xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel": "下",
"xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel": "中",
"xpack.canvas.workpadHeaderEditMenu.createElementModalTitle": "创建新元素",
"xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel": "分布",
"xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel": "分组",
"xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel": "水平",
"xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel": "左",
"xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel": "中",
"xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel": "顺序",
"xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel": "右",
"xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel": "另存为新元素",
"xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel": "上",
"xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel": "取消分组",
"xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel": "垂直",
"xpack.canvas.workpadHeaderKioskControl.controlTitle": "循环播放全屏页面",
"xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "更改循环播放时间间隔",
"xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "自动循环播放幻灯片",

View file

@ -40,8 +40,11 @@ export default function canvasCustomElementTest({
// find the first workpad element (a markdown element) and click it to select it
await testSubjects.click('canvasWorkpadPage > canvasWorkpadPageElementContent', 20000);
// click "Edit" menu
await testSubjects.click('canvasWorkpadEditMenuButton', 20000);
// click the "Save as new element" button
await testSubjects.click('canvasSidebarHeader__saveElementButton', 20000);
await testSubjects.click('canvasWorkpadEditMenu__saveElementButton', 20000);
// fill out the custom element form and submit it
await PageObjects.canvas.fillOutCustomElementForm(