Skip to content

Commit

Permalink
[Feature]: add copy geometry to feature action panel (#1495)
Browse files Browse the repository at this point in the history
* add copy geometry to feature action panel

Signed-off-by: Shan He <heshan0131@gmail.com>
  • Loading branch information
heshan0131 committed Jun 7, 2021
1 parent d786d0f commit b4fcf7b
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 87 deletions.
15 changes: 12 additions & 3 deletions src/components/common/action-panel.js
Expand Up @@ -34,8 +34,9 @@ const StyledItem = styled.div`
min-height: ${props => props.theme.actionPanelHeight}px;
text-transform: capitalize;
background-color: ${props => props.theme.dropdownListBgd};
width: ${props => props.theme.actionPanelWidth}px;
max-width: 200px;
position: relative;
${props => (props.color ? `border-left: 3px solid rgb(${props.color});` : '')} :hover {
cursor: pointer;
color: ${props => props.theme.textColorHl};
Expand All @@ -46,20 +47,28 @@ const StyledItem = styled.div`
.label {
margin-left: 8px;
white-space: nowrap;
text-overflow: ellipsis;
}
.label-icon {
margin-left: auto;
}
.nested-group {
width: 110px;
max-width: 200px;
overflow: hidden;
display: none;
color: ${props => props.theme.textColor};
position: absolute;
left: 110px;
left: 100%;
top: 0px;
padding-left: 4px;
label {
white-space: nowrap;
text-overflow: ellipsis;
}
}
`;

Expand Down
1 change: 1 addition & 0 deletions src/components/editor/editor.js
Expand Up @@ -197,6 +197,7 @@ export default function EditorFactory(FeatureActionPanel) {
/>
{showActions && Boolean(selectedFeatureId) ? (
<FeatureActionPanel
selectedFeature={get(editor, ['selectedFeature'])}
datasets={datasets}
layers={availableLayers}
currentFilter={currentFilter}
Expand Down
163 changes: 95 additions & 68 deletions src/components/editor/feature-action-panel.js
Expand Up @@ -18,13 +18,16 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React, {PureComponent} from 'react';
import React, {useCallback, useState} from 'react';
import {useIntl} from 'react-intl';

import ActionPanel, {ActionPanelItem} from 'components/common/action-panel';
import styled from 'styled-components';
import onClickOutside from 'react-onclickoutside';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {Trash, Layers} from 'components/common/icons';
import {Trash, Layers, Copy, Checkmark} from 'components/common/icons';
import copy from 'copy-to-clipboard';

const LAYOVER_OFFSET = 4;

Expand All @@ -35,82 +38,106 @@ const StyledActionsLayer = styled.div`
PureFeatureActionPanelFactory.deps = [];

export function PureFeatureActionPanelFactory() {
class FeatureActionPanel extends PureComponent {
static propTypes = {
className: PropTypes.string,
datasets: PropTypes.object.isRequired,
position: PropTypes.object.isRequired,
layers: PropTypes.arrayOf(PropTypes.object).isRequired,
currentFilter: PropTypes.object,
onClose: PropTypes.func.isRequired,
onDeleteFeature: PropTypes.func.isRequired
};
const FeatureActionPanel = ({
className,
datasets,
selectedFeature,
position,
layers,
currentFilter,
onToggleLayer,
onDeleteFeature
}) => {
const [copied, setCopied] = useState(false);
const {layerId = []} = currentFilter || {};
const intl = useIntl();

static defaultProps = {
position: {}
};
const copyGeometry = useCallback(() => {
if (selectedFeature?.geometry) copy(JSON.stringify(selectedFeature.geometry));
setCopied(true);
}, [selectedFeature?.geometry]);

// Used by onClickOutside
handleClickOutside = e => {
e.preventDefault();
e.stopPropagation();
this.props.onClose();
};
return (
<StyledActionsLayer
className={classnames('feature-action-panel', className)}
style={{
top: `${position.y + LAYOVER_OFFSET}px`,
left: `${position.x + LAYOVER_OFFSET}px`
}}
>
<ActionPanel>
<ActionPanelItem
className="editor-layers-list"
label={intl.formatMessage({id: 'editor.filterLayer', defaultMessage: 'Filter layers'})}
Icon={Layers}
>
{layers.map((layer, index) => (
<ActionPanelItem
key={index}
label={layer.config.label}
color={datasets[layer.config.dataId].color}
isSelection={true}
isActive={layerId.includes(layer.id)}
onClick={() => onToggleLayer(layer)}
className="layer-panel-item"
/>
))}
</ActionPanelItem>
<ActionPanelItem
label={intl.formatMessage({id: 'editor.copyGeometry', defaultMessage: 'Copy Geometry'})}
className="delete-panel-item"
Icon={copied ? Checkmark : Copy}
onClick={copyGeometry}
/>

render() {
const {
className,
datasets,
position,
layers,
currentFilter,
onToggleLayer,
onDeleteFeature
} = this.props;

const {layerId = []} = currentFilter || {};

return (
<StyledActionsLayer
className={classnames('feature-action-panel', className)}
style={{
top: `${position.y + LAYOVER_OFFSET}px`,
left: `${position.x + LAYOVER_OFFSET}px`
}}
>
<ActionPanel>
<ActionPanelItem className="editor-layers-list" label="layers" Icon={Layers}>
{layers.map((layer, index) => (
<ActionPanelItem
key={index}
label={layer.config.label}
color={datasets[layer.config.dataId].color}
isSelection={true}
isActive={layerId.includes(layer.id)}
onClick={() => onToggleLayer(layer)}
className="layer-panel-item"
/>
))}
</ActionPanelItem>
<ActionPanelItem
label="delete"
className="delete-panel-item"
Icon={Trash}
onClick={onDeleteFeature}
/>
</ActionPanel>
</StyledActionsLayer>
);
}
}
<ActionPanelItem
label={intl.formatMessage({id: 'tooltip.delete', defaultMessage: 'Delete'})}
className="delete-panel-item"
Icon={Trash}
onClick={onDeleteFeature}
/>
</ActionPanel>
</StyledActionsLayer>
);
};

FeatureActionPanel.displayName = 'FeatureActionPanel';
FeatureActionPanel.propTypes = {
className: PropTypes.string,
datasets: PropTypes.object.isRequired,
position: PropTypes.object.isRequired,
layers: PropTypes.arrayOf(PropTypes.object).isRequired,
currentFilter: PropTypes.object,
onClose: PropTypes.func.isRequired,
onDeleteFeature: PropTypes.func.isRequired
};

FeatureActionPanel.defaultProps = {
position: {}
};

return FeatureActionPanel;
}

FeatureActionPanelFactory.deps = PureFeatureActionPanelFactory.deps;

export default function FeatureActionPanelFactory() {
return onClickOutside(PureFeatureActionPanelFactory());
const PureFeatureActionPanel = PureFeatureActionPanelFactory();

const ClickOutsideFeatureActionPanel = props => {
// @ts-ignore
ClickOutsideFeatureActionPanel.handleClickOutside = e => {
e.preventDefault();
e.stopPropagation();
props.onClose?.();
};
return <PureFeatureActionPanel {...props} />;
};

const clickOutsideConfig = {
// @ts-ignore
handleClickOutside: () => ClickOutsideFeatureActionPanel.handleClickOutside
};

return onClickOutside(ClickOutsideFeatureActionPanel, clickOutsideConfig);
}
15 changes: 10 additions & 5 deletions src/localization/en.js
Expand Up @@ -221,13 +221,18 @@ export default {
exportMap: 'Export Map',
shareMapURL: 'Share Map URL',
saveMap: 'Save Map',
select: 'select',
polygon: 'polygon',
rectangle: 'rectangle',
hide: 'hide',
show: 'show',
select: 'Select',
polygon: 'Polygon',
rectangle: 'Rectangle',
hide: 'Hide',
show: 'Show',
...LOCALES
},
editor: {
filterLayer: 'Filter Layers',
copyGeometry: 'Copy Geometry'
},

modal: {
title: {
deleteDataset: 'Delete Dataset',
Expand Down
39 changes: 28 additions & 11 deletions test/browser/components/editor/feature-action-panel-test.js
Expand Up @@ -20,9 +20,9 @@

import React from 'react';
import test from 'tape';
import {shallow} from 'enzyme';
import sinon from 'sinon';
import {PureFeatureActionPanelFactory} from 'components/editor/feature-action-panel';
import {IntlWrapper, mountWithTheme} from 'test/helpers/component-utils';

const FeatureActionPanel = PureFeatureActionPanelFactory();

Expand Down Expand Up @@ -51,17 +51,34 @@ test('FeatureActionPanel -> display layers', t => {
const onToggleLayer = sinon.spy();
const onDeleteFeature = sinon.spy();

const $ = shallow(
<FeatureActionPanel
className="action-item-test"
layers={layers}
datasets={datasets}
onToggleLayer={onToggleLayer}
onDeleteFeature={onDeleteFeature}
/>
);
let wrapper;

t.equal($.find('.layer-panel-item').length, 2, 'We should display only 2 action panel items');
t.doesNotThrow(() => {
wrapper = mountWithTheme(
<IntlWrapper>
<FeatureActionPanel
className="action-item-test"
layers={layers}
datasets={datasets}
onToggleLayer={onToggleLayer}
onDeleteFeature={onDeleteFeature}
/>
</IntlWrapper>
);
}, 'FeatureActionPanel should not fail mount');

t.equal(wrapper.find('Checkbox').length, 2, 'We should display only 2 layer checkbox');
for (let i = 0; i < wrapper.find('Checkbox').length; i++) {
t.equal(
wrapper
.find('Checkbox')
.at(i)
.find('label')
.text(),
`layer ${i + 1}`,
'should render correct layer label'
);
}

t.end();
});

0 comments on commit b4fcf7b

Please sign in to comment.