[Maps] disable edit layer button when flyout is open for add layer or map settings (#64230)

* [Maps] disable edit layer button to avoid user data loss

* remove layer_toc_actions

* fix tslint errors

* update jest snapshots

* review feedback

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-04-28 18:34:10 -06:00 committed by GitHub
parent 30439f6df0
commit 4a18894fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 199 additions and 113 deletions

View file

@ -74,3 +74,11 @@ export function updateMapSetting(
settingKey: string,
settingValue: string | boolean | number
): AnyAction;
export function cloneLayer(layerId: string): AnyAction;
export function fitToLayerExtent(layerId: string): AnyAction;
export function removeLayer(layerId: string): AnyAction;
export function toggleLayerVisible(layerId: string): AnyAction;

View file

@ -9,12 +9,10 @@ exports[`TOCEntry is rendered 1`] = `
<div
className="mapTocEntry-visible"
>
<LayerTocActions
cloneLayer={[Function]}
<Connect(TOCEntryActionsPopover)
displayName="layer 1"
editLayer={[Function]}
escapedDisplayName="layer_1"
fitToBounds={[Function]}
layer={
Object {
"getDisplayName": [Function],
@ -26,9 +24,6 @@ exports[`TOCEntry is rendered 1`] = `
"showAtZoomLevel": [Function],
}
}
removeLayer={[Function]}
toggleVisible={[Function]}
zoom={0}
/>
<div
className="mapTocEntry__layerIcons"
@ -76,12 +71,10 @@ exports[`TOCEntry props Should shade background when not selected layer 1`] = `
<div
className="mapTocEntry-visible"
>
<LayerTocActions
cloneLayer={[Function]}
<Connect(TOCEntryActionsPopover)
displayName="layer 1"
editLayer={[Function]}
escapedDisplayName="layer_1"
fitToBounds={[Function]}
layer={
Object {
"getDisplayName": [Function],
@ -93,9 +86,6 @@ exports[`TOCEntry props Should shade background when not selected layer 1`] = `
"showAtZoomLevel": [Function],
}
}
removeLayer={[Function]}
toggleVisible={[Function]}
zoom={0}
/>
<div
className="mapTocEntry__layerIcons"
@ -143,12 +133,10 @@ exports[`TOCEntry props Should shade background when selected layer 1`] = `
<div
className="mapTocEntry-visible"
>
<LayerTocActions
cloneLayer={[Function]}
<Connect(TOCEntryActionsPopover)
displayName="layer 1"
editLayer={[Function]}
escapedDisplayName="layer_1"
fitToBounds={[Function]}
layer={
Object {
"getDisplayName": [Function],
@ -160,9 +148,6 @@ exports[`TOCEntry props Should shade background when selected layer 1`] = `
"showAtZoomLevel": [Function],
}
}
removeLayer={[Function]}
toggleVisible={[Function]}
zoom={0}
/>
<div
className="mapTocEntry__layerIcons"
@ -210,13 +195,10 @@ exports[`TOCEntry props isReadOnly 1`] = `
<div
className="mapTocEntry-visible"
>
<LayerTocActions
cloneLayer={[Function]}
<Connect(TOCEntryActionsPopover)
displayName="layer 1"
editLayer={[Function]}
escapedDisplayName="layer_1"
fitToBounds={[Function]}
isReadOnly={true}
layer={
Object {
"getDisplayName": [Function],
@ -228,9 +210,6 @@ exports[`TOCEntry props isReadOnly 1`] = `
"showAtZoomLevel": [Function],
}
}
removeLayer={[Function]}
toggleVisible={[Function]}
zoom={0}
/>
</div>
<span
@ -261,12 +240,10 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is
<div
className="mapTocEntry-visible"
>
<LayerTocActions
cloneLayer={[Function]}
<Connect(TOCEntryActionsPopover)
displayName="layer 1"
editLayer={[Function]}
escapedDisplayName="layer_1"
fitToBounds={[Function]}
layer={
Object {
"getDisplayName": [Function],
@ -278,9 +255,6 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is
"showAtZoomLevel": [Function],
}
}
removeLayer={[Function]}
toggleVisible={[Function]}
zoom={0}
/>
<div
className="mapTocEntry__layerIcons"

View file

@ -4,36 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import { connect } from 'react-redux';
import { TOCEntry } from './view';
import { FLYOUT_STATE } from '../../../../../reducers/ui';
import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions';
import { getIsReadOnly, getOpenTOCDetails } from '../../../../../selectors/ui_selectors';
import {
fitToLayerExtent,
setSelectedLayer,
toggleLayerVisible,
removeTransientLayer,
cloneLayer,
removeLayer,
} from '../../../../../actions/map_actions';
import {
getMapZoom,
hasDirtyState,
getSelectedLayer,
isUsingSearch,
} from '../../../../../selectors/map_selectors';
import {
getIsReadOnly,
getOpenTOCDetails,
getFlyoutDisplay,
} from '../../../../../selectors/ui_selectors';
import { setSelectedLayer, removeTransientLayer } from '../../../../../actions/map_actions';
function mapStateToProps(state = {}, ownProps) {
const flyoutDisplay = getFlyoutDisplay(state);
return {
isReadOnly: getIsReadOnly(state),
zoom: _.get(state, 'map.mapState.zoom', 0),
zoom: getMapZoom(state),
selectedLayer: getSelectedLayer(state),
hasDirtyStateSelector: hasDirtyState(state),
isLegendDetailsOpen: getOpenTOCDetails(state).includes(ownProps.layer.getId()),
isUsingSearch: isUsingSearch(state),
isEditButtonDisabled:
flyoutDisplay !== FLYOUT_STATE.NONE && flyoutDisplay !== FLYOUT_STATE.LAYER_PANEL,
};
}
@ -44,18 +40,6 @@ function mapDispatchToProps(dispatch) {
await dispatch(setSelectedLayer(layerId));
dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL));
},
toggleVisible: layerId => {
dispatch(toggleLayerVisible(layerId));
},
fitToBounds: layerId => {
dispatch(fitToLayerExtent(layerId));
},
cloneLayer: layerId => {
dispatch(cloneLayer(layerId));
},
removeLayer: layerId => {
dispatch(removeLayer(layerId));
},
hideTOCDetails: layerId => {
dispatch(hideTOCDetails(layerId));
},

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LayerTocActions is rendered 1`] = `
exports[`TOCEntryActionsPopover is rendered 1`] = `
<EuiPopover
anchorClassName="mapLayTocActions__popoverAnchor"
anchorPosition="leftUp"
@ -86,15 +86,18 @@ exports[`LayerTocActions is rendered 1`] = `
/>,
"name": "Hide layer",
"onClick": [Function],
"toolTipContent": null,
},
Object {
"data-test-subj": "editLayerButton",
"disabled": false,
"icon": <EuiIcon
size="m"
type="pencil"
/>,
"name": "Edit layer",
"onClick": [Function],
"toolTipContent": null,
},
Object {
"data-test-subj": "cloneLayerButton",
@ -104,6 +107,7 @@ exports[`LayerTocActions is rendered 1`] = `
/>,
"name": "Clone layer",
"onClick": [Function],
"toolTipContent": null,
},
Object {
"data-test-subj": "removeLayerButton",
@ -113,6 +117,7 @@ exports[`LayerTocActions is rendered 1`] = `
/>,
"name": "Remove layer",
"onClick": [Function],
"toolTipContent": null,
},
],
"title": "Layer actions",
@ -123,7 +128,7 @@ exports[`LayerTocActions is rendered 1`] = `
</EuiPopover>
`;
exports[`LayerTocActions should disable fit to data when supportsFitToBounds is false 1`] = `
exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBounds is false 1`] = `
<EuiPopover
anchorClassName="mapLayTocActions__popoverAnchor"
anchorPosition="leftUp"
@ -209,15 +214,18 @@ exports[`LayerTocActions should disable fit to data when supportsFitToBounds is
/>,
"name": "Hide layer",
"onClick": [Function],
"toolTipContent": null,
},
Object {
"data-test-subj": "editLayerButton",
"disabled": false,
"icon": <EuiIcon
size="m"
type="pencil"
/>,
"name": "Edit layer",
"onClick": [Function],
"toolTipContent": null,
},
Object {
"data-test-subj": "cloneLayerButton",
@ -227,6 +235,7 @@ exports[`LayerTocActions should disable fit to data when supportsFitToBounds is
/>,
"name": "Clone layer",
"onClick": [Function],
"toolTipContent": null,
},
Object {
"data-test-subj": "removeLayerButton",
@ -236,6 +245,7 @@ exports[`LayerTocActions should disable fit to data when supportsFitToBounds is
/>,
"name": "Remove layer",
"onClick": [Function],
"toolTipContent": null,
},
],
"title": "Layer actions",
@ -246,7 +256,7 @@ exports[`LayerTocActions should disable fit to data when supportsFitToBounds is
</EuiPopover>
`;
exports[`LayerTocActions should not show edit actions in read only mode 1`] = `
exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1`] = `
<EuiPopover
anchorClassName="mapLayTocActions__popoverAnchor"
anchorPosition="leftUp"
@ -332,6 +342,7 @@ exports[`LayerTocActions should not show edit actions in read only mode 1`] = `
/>,
"name": "Hide layer",
"onClick": [Function],
"toolTipContent": null,
},
],
"title": "Layer actions",

View file

@ -0,0 +1,49 @@
/*
* 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 { AnyAction, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { MapStoreState } from '../../../../../../reducers/store';
import {
fitToLayerExtent,
toggleLayerVisible,
cloneLayer,
removeLayer,
} from '../../../../../../actions/map_actions';
import { getMapZoom, isUsingSearch } from '../../../../../../selectors/map_selectors';
import { getIsReadOnly } from '../../../../../../selectors/ui_selectors';
import { TOCEntryActionsPopover } from './toc_entry_actions_popover';
function mapStateToProps(state: MapStoreState) {
return {
isReadOnly: getIsReadOnly(state),
isUsingSearch: isUsingSearch(state),
zoom: getMapZoom(state),
};
}
function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
return {
cloneLayer: (layerId: string) => {
dispatch(cloneLayer(layerId));
},
fitToBounds: (layerId: string) => {
dispatch(fitToLayerExtent(layerId));
},
removeLayer: (layerId: string) => {
dispatch(removeLayer(layerId));
},
toggleVisible: (layerId: string) => {
dispatch(toggleLayerVisible(layerId));
},
};
}
const connectedTOCEntryActionsPopover = connect(
mapStateToProps,
mapDispatchToProps
)(TOCEntryActionsPopover);
export { connectedTOCEntryActionsPopover as TOCEntryActionsPopover };

View file

@ -3,21 +3,45 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable max-classes-per-file */
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { AbstractLayer, ILayer } from '../../../../../../layers/layer';
import { AbstractSource, ISource } from '../../../../../../layers/sources/source';
import { AbstractStyle, IStyle } from '../../../../../../layers/styles/style';
import { LayerTocActions } from './layer_toc_actions';
import { TOCEntryActionsPopover } from './toc_entry_actions_popover';
let supportsFitToBounds;
const layerMock = {
supportsFitToBounds: () => {
let supportsFitToBounds: boolean;
class MockSource extends AbstractSource implements ISource {}
class MockStyle extends AbstractStyle implements IStyle {}
class LayerMock extends AbstractLayer implements ILayer {
constructor() {
const sourceDescriptor = {
type: 'mySourceType',
};
const source = new MockSource(sourceDescriptor);
const style = new MockStyle({ type: 'myStyleType' });
const layerDescriptor = {
id: 'testLayer',
sourceDescriptor,
};
super({ layerDescriptor, source, style });
}
async supportsFitToBounds(): Promise<boolean> {
return supportsFitToBounds;
},
isVisible: () => {
}
isVisible() {
return true;
},
getIconAndTooltipContent: (zoom, isUsingSearch) => {
}
getIconAndTooltipContent(zoom: number, isUsingSearch: boolean) {
return {
icon: <span>mockIcon</span>,
tooltipContent: `simulated tooltip content at zoom: ${zoom}`,
@ -28,24 +52,31 @@ const layerMock = {
},
],
};
},
};
}
}
const defaultProps = {
cloneLayer: () => {},
displayName: 'layer 1',
editLayer: () => {},
escapedDisplayName: 'layer1',
zoom: 0,
layer: layerMock,
fitToBounds: () => {},
isEditButtonDisabled: false,
isReadOnly: false,
isUsingSearch: true,
layer: new LayerMock(),
removeLayer: () => {},
toggleVisible: () => {},
zoom: 0,
};
describe('LayerTocActions', () => {
describe('TOCEntryActionsPopover', () => {
beforeEach(() => {
supportsFitToBounds = true;
});
test('is rendered', async () => {
const component = shallowWithIntl(<LayerTocActions {...defaultProps} />);
const component = shallowWithIntl(<TOCEntryActionsPopover {...defaultProps} />);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -56,7 +87,9 @@ describe('LayerTocActions', () => {
});
test('should not show edit actions in read only mode', async () => {
const component = shallowWithIntl(<LayerTocActions {...defaultProps} isReadOnly={true} />);
const component = shallowWithIntl(
<TOCEntryActionsPopover {...defaultProps} isReadOnly={true} />
);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -68,7 +101,7 @@ describe('LayerTocActions', () => {
test('should disable fit to data when supportsFitToBounds is false', async () => {
supportsFitToBounds = false;
const component = shallowWithIntl(<LayerTocActions {...defaultProps} />);
const component = shallowWithIntl(<TOCEntryActionsPopover {...defaultProps} />);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));

View file

@ -8,8 +8,31 @@ import React, { Component, Fragment } from 'react';
import { EuiButtonEmpty, EuiPopover, EuiContextMenu, EuiIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ILayer } from '../../../../../../layers/layer';
interface Props {
cloneLayer: (layerId: string) => void;
displayName: string;
editLayer: () => void;
escapedDisplayName: string;
fitToBounds: (layerId: string) => void;
isEditButtonDisabled: boolean;
isReadOnly: boolean;
isUsingSearch: boolean;
layer: ILayer;
removeLayer: (layerId: string) => void;
toggleVisible: (layerId: string) => void;
zoom: number;
}
interface State {
isPopoverOpen: boolean;
supportsFitToBounds: boolean;
}
export class TOCEntryActionsPopover extends Component<Props, State> {
private _isMounted: boolean = false;
export class LayerTocActions extends Component {
state = {
isPopoverOpen: false,
supportsFitToBounds: false,
@ -43,6 +66,22 @@ export class LayerTocActions extends Component {
}));
};
_cloneLayer() {
this.props.cloneLayer(this.props.layer.getId());
}
_fitToBounds() {
this.props.fitToBounds(this.props.layer.getId());
}
_removeLayer() {
this.props.fitToBounds(this.props.layer.getId());
}
_toggleVisible() {
this.props.toggleVisible(this.props.layer.getId());
}
_renderPopoverToggleButton() {
const { icon, tooltipContent, footnotes } = this.props.layer.getIconAndTooltipContent(
this.props.zoom,
@ -108,7 +147,7 @@ export class LayerTocActions extends Component {
disabled: !this.state.supportsFitToBounds,
onClick: () => {
this._closePopover();
this.props.fitToBounds();
this._fitToBounds();
},
},
{
@ -121,20 +160,23 @@ export class LayerTocActions extends Component {
}),
icon: <EuiIcon type={this.props.layer.isVisible() ? 'eye' : 'eyeClosed'} size="m" />,
'data-test-subj': 'layerVisibilityToggleButton',
toolTipContent: null,
onClick: () => {
this._closePopover();
this.props.toggleVisible();
this._toggleVisible();
},
},
];
if (!this.props.isReadOnly) {
actionItems.push({
disabled: this.props.isEditButtonDisabled,
name: i18n.translate('xpack.maps.layerTocActions.editLayerTitle', {
defaultMessage: 'Edit layer',
}),
icon: <EuiIcon type="pencil" size="m" />,
'data-test-subj': 'editLayerButton',
toolTipContent: null,
onClick: () => {
this._closePopover();
this.props.editLayer();
@ -145,10 +187,11 @@ export class LayerTocActions extends Component {
defaultMessage: 'Clone layer',
}),
icon: <EuiIcon type="copy" size="m" />,
toolTipContent: null,
'data-test-subj': 'cloneLayerButton',
onClick: () => {
this._closePopover();
this.props.cloneLayer();
this._cloneLayer();
},
});
actionItems.push({
@ -156,10 +199,11 @@ export class LayerTocActions extends Component {
defaultMessage: 'Remove layer',
}),
icon: <EuiIcon type="trash" size="m" />,
toolTipContent: null,
'data-test-subj': 'removeLayerButton',
onClick: () => {
this._closePopover();
this.props.removeLayer();
this._removeLayer();
},
});
}

View file

@ -8,7 +8,7 @@ import React from 'react';
import classNames from 'classnames';
import { EuiIcon, EuiOverlayMask, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui';
import { LayerTocActions } from '../../../../../components/layer_toc_actions';
import { TOCEntryActionsPopover } from './toc_entry_actions_popover';
import { i18n } from '@kbn/i18n';
function escapeLayerName(name) {
@ -124,6 +124,7 @@ export class TOCEntry extends React.Component {
return (
<div className="mapTocEntry__layerIcons">
<EuiButtonIcon
isDisabled={this.props.isEditButtonDisabled}
iconType="pencil"
aria-label={i18n.translate('xpack.maps.layerControl.tocEntry.editButtonAriaLabel', {
defaultMessage: 'Edit layer',
@ -191,17 +192,7 @@ export class TOCEntry extends React.Component {
}
_renderLayerHeader() {
const {
removeLayer,
cloneLayer,
isReadOnly,
layer,
zoom,
toggleVisible,
fitToBounds,
isUsingSearch,
} = this.props;
const { layer, zoom } = this.props;
return (
<div
className={
@ -210,26 +201,12 @@ export class TOCEntry extends React.Component {
: 'mapTocEntry-notVisible'
}
>
<LayerTocActions
<TOCEntryActionsPopover
layer={layer}
isUsingSearch={isUsingSearch}
fitToBounds={() => {
fitToBounds(layer.getId());
}}
zoom={zoom}
toggleVisible={() => {
toggleVisible(layer.getId());
}}
displayName={this.state.displayName}
escapedDisplayName={escapeLayerName(this.state.displayName)}
cloneLayer={() => {
cloneLayer(layer.getId());
}}
editLayer={this._openLayerPanelWithCheck}
isReadOnly={isReadOnly}
removeLayer={() => {
removeLayer(layer.getId());
}}
isEditButtonDisabled={this.props.isEditButtonDisabled}
/>
{this._renderLayerIcons()}

View file

@ -45,7 +45,7 @@ export interface ILayer {
supportsFitToBounds(): Promise<boolean>;
getAttributions(): Promise<Attribution[]>;
getLabel(): string;
getCustomIconAndTooltipContent(): IconAndTooltipContent;
getCustomIconAndTooltipContent(): CustomIconAndTooltipContent;
getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent;
renderLegendDetails(): ReactElement<any> | null;
showAtZoomLevel(zoom: number): boolean;
@ -87,7 +87,11 @@ export type Footnote = {
export type IconAndTooltipContent = {
icon?: ReactElement<any> | null;
tooltipContent?: string | null;
footnotes?: Footnote[] | null;
footnotes: Footnote[];
};
export type CustomIconAndTooltipContent = {
icon: ReactElement<any> | null;
tooltipContent?: string | null;
areResultsTrimmed?: boolean;
};
@ -212,7 +216,7 @@ export class AbstractLayer implements ILayer {
return this._descriptor.label ? this._descriptor.label : '';
}
getCustomIconAndTooltipContent(): IconAndTooltipContent {
getCustomIconAndTooltipContent(): CustomIconAndTooltipContent {
return {
icon: <EuiIcon size="m" type={this.getLayerTypeIconName()} />,
};

View file

@ -22,4 +22,6 @@ export function getMapSettings(state: MapStoreState): MapSettings;
export function hasMapSettingsChanges(state: MapStoreState): boolean;
export function isUsingSearch(state: MapStoreState): boolean;
export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer;