@eeacms/volto-eea-map 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.coverage.babel.config.js +9 -0
  2. package/.project.eslintrc.js +46 -0
  3. package/.release-it.json +17 -0
  4. package/CHANGELOG.md +40 -0
  5. package/DEVELOP.md +51 -0
  6. package/LICENSE.md +9 -0
  7. package/README.md +103 -0
  8. package/RELEASE.md +74 -0
  9. package/babel.config.js +17 -0
  10. package/bootstrap +41 -0
  11. package/cypress.json +17 -0
  12. package/jest-addon.config.js +36 -0
  13. package/locales/volto.pot +0 -0
  14. package/package.json +44 -0
  15. package/src/components/Blocks/EEAMap/Edit.jsx +37 -0
  16. package/src/components/Blocks/EEAMap/Schema.js +32 -0
  17. package/src/components/Blocks/EEAMap/View.jsx +20 -0
  18. package/src/components/Blocks/EEAMap/components/TextView.jsx +7 -0
  19. package/src/components/Blocks/EEAMap/components/Webmap.jsx +194 -0
  20. package/src/components/Blocks/EEAMap/components/widgets/ExtraViews.jsx +53 -0
  21. package/src/components/Blocks/EEAMap/components/widgets/LayerSelectWidget.jsx +113 -0
  22. package/src/components/Blocks/EEAMap/components/widgets/LayersPanel.jsx +56 -0
  23. package/src/components/Blocks/EEAMap/components/widgets/LegendWidget.jsx +63 -0
  24. package/src/components/Blocks/EEAMap/components/widgets/MapEditorWidget.jsx +81 -0
  25. package/src/components/Blocks/EEAMap/components/widgets/ObjectTypesWidget.jsx +53 -0
  26. package/src/components/Blocks/EEAMap/components/widgets/panelsSchema.js +190 -0
  27. package/src/components/Blocks/EEAMap/constants.js +29 -0
  28. package/src/components/Blocks/EEAMap/styles/map.css +22 -0
  29. package/src/components/Blocks/EEAMap/utils.js +17 -0
  30. package/src/components/index.js +4 -0
  31. package/src/index.js +41 -0
@@ -0,0 +1,194 @@
1
+ /* eslint-disable */
2
+ import React from 'react';
3
+ import { loadModules } from 'esri-loader';
4
+
5
+ const MODULES = [
6
+ 'esri/Map',
7
+ 'esri/views/MapView',
8
+ 'esri/layers/FeatureLayer',
9
+ 'esri/layers/MapImageLayer',
10
+ 'esri/layers/GroupLayer',
11
+ 'esri/widgets/Legend',
12
+ 'esri/widgets/Expand',
13
+ 'esri/widgets/Print',
14
+ 'esri/widgets/Zoom',
15
+ ];
16
+
17
+ const Webmap = (props) => {
18
+ const { data = {}, editMode, height } = props;
19
+ const {
20
+ base = {},
21
+ layers = {},
22
+ id,
23
+ legend = {},
24
+ print = {},
25
+ zoom = {},
26
+ } = data;
27
+
28
+ const { base_layer = '' } = base;
29
+
30
+ const map_layers =
31
+ layers &&
32
+ layers.map_layers &&
33
+ layers.map_layers
34
+ .filter(({ map_layer }) => map_layer)
35
+ .map((l, i) => l.map_layer);
36
+ const options = {
37
+ css: true,
38
+ };
39
+ const mapRef = React.useRef();
40
+ const [modules, setModules] = React.useState({});
41
+ const modules_loaded = React.useRef(false);
42
+
43
+ React.useEffect(() => {
44
+ if (!modules_loaded.current) {
45
+ modules_loaded.current = true;
46
+ loadModules(MODULES, options).then((modules) => {
47
+ const [
48
+ Map,
49
+ MapView,
50
+ FeatureLayer,
51
+ MapImageLayer,
52
+ GroupLayer,
53
+ Legend,
54
+ Expand,
55
+ Print,
56
+ Zoom,
57
+ ] = modules;
58
+ setModules({
59
+ Map,
60
+ MapView,
61
+ FeatureLayer,
62
+ MapImageLayer,
63
+ GroupLayer,
64
+ Legend,
65
+ Expand,
66
+ Print,
67
+ Zoom,
68
+ });
69
+ });
70
+ }
71
+ }, [setModules, options]);
72
+
73
+ const esri = React.useMemo(() => {
74
+ if (Object.keys(modules).length === 0) return {};
75
+ const {
76
+ Map,
77
+ MapView,
78
+ FeatureLayer,
79
+ MapImageLayer,
80
+ GroupLayer,
81
+ Legend,
82
+ Expand,
83
+ Print,
84
+ Zoom,
85
+ } = modules;
86
+ let layers =
87
+ map_layers &&
88
+ map_layers.length > 0 &&
89
+ map_layers
90
+ .filter(({ map_service_url, layer }) => map_service_url && layer)
91
+ .map(({ map_service_url, layer }) => {
92
+ const url = `${map_service_url}/${layer}`;
93
+
94
+ let mapLayer;
95
+
96
+ //TODO: add more layers and error catch for unrecognized layer
97
+ switch (layer.type) {
98
+ case 'Raster Layer':
99
+ mapLayer = new MapImageLayer({
100
+ url: map_service_url,
101
+ });
102
+ break;
103
+ case 'Feature Layer':
104
+ mapLayer = new FeatureLayer({ url });
105
+ break;
106
+ case 'Group Layer':
107
+ mapLayer = new GroupLayer({ url });
108
+ break;
109
+ default:
110
+ break;
111
+ }
112
+ return mapLayer;
113
+ });
114
+
115
+ const map = new Map({
116
+ basemap: base_layer || 'hybrid',
117
+ layers,
118
+ });
119
+ const view = new MapView({
120
+ container: mapRef.current,
121
+ map,
122
+ center: zoom?.long && zoom?.lat ? [zoom.long, zoom.lat] : [0, 40],
123
+ zoom: zoom?.zoom_level ? zoom?.zoom_level : 2,
124
+ ui: {
125
+ components: ['attribution'],
126
+ },
127
+ });
128
+
129
+ if (zoom?.show_zoom) {
130
+ const zoomPosition = zoom && zoom.position ? zoom.position : 'top-right';
131
+ const zoomWidget = new Zoom({
132
+ view: view,
133
+ });
134
+ view.ui.add(zoomWidget, zoomPosition);
135
+ }
136
+
137
+ if (legend?.legend?.show_legend) {
138
+ const legendPosition =
139
+ legend && legend.legend && legend.legend.position
140
+ ? legend.legend.position
141
+ : 'top-right';
142
+
143
+ const legendWidget = new Expand({
144
+ content: new Legend({
145
+ view: view,
146
+ style: 'classic', // other styles include 'classic'
147
+ }),
148
+ view: view,
149
+ expanded: false,
150
+ expandIconClass: 'esri-icon-legend',
151
+ expandTooltip: 'Legend',
152
+ classNames: 'some-cool-expand',
153
+ });
154
+ view.ui.add(legendWidget, legendPosition);
155
+ }
156
+
157
+ if (print?.show_print) {
158
+ const printPosition =
159
+ print && print.position ? print.position : 'top-right';
160
+ const printWidget = new Expand({
161
+ content: new Print({
162
+ view: view,
163
+ }),
164
+ view: view,
165
+ expanded: false,
166
+ expandIconClass: 'esri-icon-printer',
167
+ expandTooltip: 'Print',
168
+ });
169
+ view.ui.add(printWidget, printPosition);
170
+ }
171
+
172
+ if (layers && layers.length > 0) {
173
+ view.whenLayerView(layers[0]).then((layerView) => {
174
+ layerView.watch('updating', (val) => {});
175
+ });
176
+ }
177
+ return { view, map };
178
+ }, [modules, base_layer, map_layers]);
179
+
180
+ const currentLayerView = esri.view?.layerViews?.items?.[0];
181
+ return (
182
+ <div>
183
+ <div
184
+ style={{
185
+ height: height && !editMode ? `${height}px` : '500px',
186
+ }}
187
+ ref={mapRef}
188
+ className="esri-map"
189
+ ></div>
190
+ </div>
191
+ );
192
+ };
193
+
194
+ export default Webmap;
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Button } from 'semantic-ui-react';
3
+ import { Icon } from '@plone/volto/components';
4
+
5
+ import TextView from '../TextView';
6
+ import LegendWidget from './LegendWidget';
7
+
8
+ import worldSVG from '@plone/volto/icons/world.svg';
9
+ import downloadSVG from '@plone/volto/icons/download.svg';
10
+
11
+ const ExtraViews = ({ data }) => {
12
+ const { map_data = {}, description, show_description } = data;
13
+ const { general = {} } = map_data;
14
+ return (
15
+ <>
16
+ {general.show_legend && <LegendWidget data={map_data} />}
17
+ {show_description && description && <TextView text={description} />}
18
+ {(general.show_download || general.show_viewer) && (
19
+ <div
20
+ style={{ display: 'flex', justifyContent: 'end', margin: '10px 0' }}
21
+ >
22
+ {general.show_download && (
23
+ <a
24
+ target="_blank"
25
+ rel="noreferrer"
26
+ href={`${map_data.layers.map_layers[0].map_layer.map_service_url}?f=lyr&v=9.3`}
27
+ >
28
+ <Button size="tiny">
29
+ <Icon name={downloadSVG} title="Download" size="25px" />
30
+ </Button>
31
+ </a>
32
+ )}
33
+ {general.show_viewer && (
34
+ <a
35
+ target="_blank"
36
+ rel="noreferrer"
37
+ href={
38
+ `https://www.arcgis.com/home/webmap/viewer.html?url=` +
39
+ `${map_data.layers.map_layers[0].map_layer.map_service_url}&source=sd`
40
+ }
41
+ >
42
+ <Button size="tiny">
43
+ <Icon name={worldSVG} title="Check Url" size="25px" />
44
+ </Button>
45
+ </a>
46
+ )}
47
+ </div>
48
+ )}
49
+ </>
50
+ );
51
+ };
52
+
53
+ export default ExtraViews;
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import { Icon } from '@plone/volto/components';
3
+ import { Input, Select, Button, Grid } from 'semantic-ui-react';
4
+
5
+ import checkSVG from '@plone/volto/icons/check.svg';
6
+ import closeSVG from '@plone/volto/icons/clear.svg';
7
+
8
+ import { fetchArcgisData } from '../../utils';
9
+
10
+ const LayerSelectWidget = (props) => {
11
+ const { onChange, value = {}, id } = props;
12
+
13
+ const { available_layers, map_data, map_service_url, layer } = value;
14
+
15
+ const [mapData, setMapData] = React.useState(map_data);
16
+ const [checkColor, setCheckColor] = React.useState('');
17
+ const [serviceUrlError, setServiceUrlError] = React.useState('');
18
+ const [serviceUrl, setServiceUrl] = React.useState(map_service_url);
19
+ const [selectedLayer, setSelectedLayer] = React.useState(layer);
20
+ const [availableLayers, setAvailableLayers] = React.useState(
21
+ available_layers,
22
+ );
23
+
24
+ const handleServiceUrlCheck = async () => {
25
+ // fetch url, save it, populate layers options
26
+ try {
27
+ let mapData = await fetchArcgisData(serviceUrl);
28
+ setCheckColor('green');
29
+ setMapData(mapData);
30
+ setServiceUrlError('');
31
+ if (mapData.layers && mapData.layers.length > 0) {
32
+ setAvailableLayers(
33
+ mapData.layers.map((layer, i) => {
34
+ return { key: layer.id, value: layer, text: layer.name };
35
+ }),
36
+ );
37
+ }
38
+ onChange(id, {
39
+ layer: selectedLayer,
40
+ map_service_url: serviceUrl,
41
+ available_layers: availableLayers,
42
+ map_data: mapData,
43
+ });
44
+ } catch (e) {
45
+ setCheckColor('youtube');
46
+ setServiceUrlError({ error: e.message, status: e.status });
47
+ }
48
+ };
49
+
50
+ const handleSelectLayer = (layer) => {
51
+ setSelectedLayer(layer);
52
+ onChange(id, {
53
+ layer,
54
+ map_service_url: serviceUrl,
55
+ available_layers: availableLayers,
56
+ map_data: mapData,
57
+ });
58
+ };
59
+
60
+ return (
61
+ <div
62
+ style={{
63
+ padding: '0 5px',
64
+ }}
65
+ >
66
+ <Grid>
67
+ <h4>Service URL</h4>
68
+ <Grid.Row>
69
+ <Input
70
+ type="text"
71
+ onChange={(e, { value }) => setServiceUrl(value)}
72
+ style={{ width: '100%' }}
73
+ value={serviceUrl}
74
+ />
75
+ <Grid.Row>
76
+ <Button
77
+ style={{
78
+ margin: '10px 0',
79
+ display: 'flex',
80
+ alignItems: 'center',
81
+ }}
82
+ color={checkColor}
83
+ onClick={handleServiceUrlCheck}
84
+ >
85
+ <p style={{ fontSize: '14px', margin: '0', marginRight: '5px' }}>
86
+ Check Url
87
+ </p>
88
+ <Icon
89
+ name={serviceUrlError ? closeSVG : checkSVG}
90
+ style={{ marginLeft: '5px' }}
91
+ title="Check Url"
92
+ size="17px"
93
+ />
94
+ </Button>
95
+ </Grid.Row>
96
+ </Grid.Row>
97
+ <h4>Layer</h4>
98
+ <Grid.Row>
99
+ <Select
100
+ onChange={(e, { value }) => handleSelectLayer(value)}
101
+ options={availableLayers}
102
+ style={{ width: '100%' }}
103
+ placeholder="Select layer"
104
+ value={selectedLayer}
105
+ />
106
+ </Grid.Row>
107
+ <Grid.Row stretched></Grid.Row>
108
+ </Grid>
109
+ </div>
110
+ );
111
+ };
112
+
113
+ export default LayerSelectWidget;
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { Button } from 'semantic-ui-react';
3
+ import LayerSelectWidget from './LayerSelectWidget';
4
+
5
+ const LayersPanel = ({ data, onChange, block }) => {
6
+ const { map_layers } = data || {};
7
+
8
+ React.useEffect(() => {
9
+ if (!data.map_layers) {
10
+ onChange('map_data', {
11
+ ...data,
12
+ map_layers: [
13
+ {
14
+ map_service_url: '',
15
+ layer: '',
16
+ available_layers: [],
17
+ map_data: {},
18
+ },
19
+ ],
20
+ });
21
+ }
22
+ }, [data, block, onChange]);
23
+
24
+ const handleAddLayer = () => {
25
+ onChange('map_data', {
26
+ ...data,
27
+ map_layers: [
28
+ ...data.map_layers,
29
+ { map_service_url: '', layer: '', available_layers: [], map_data: {} },
30
+ ],
31
+ });
32
+ };
33
+
34
+ return (
35
+ <div>
36
+ {map_layers &&
37
+ map_layers.length > 0 &&
38
+ map_layers.map((layer, i) => (
39
+ <LayerSelectWidget
40
+ key={i}
41
+ index={i}
42
+ layer={layer}
43
+ onChange={onChange}
44
+ block={block}
45
+ data={data}
46
+ />
47
+ ))}
48
+
49
+ <Button size="tiny" onClick={handleAddLayer}>
50
+ Add Layer
51
+ </Button>
52
+ </div>
53
+ );
54
+ };
55
+
56
+ export default LayersPanel;
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { Grid } from 'semantic-ui-react';
3
+ import { fetchArcgisData } from '../../utils';
4
+
5
+ const LayerLegend = ({ data }) => {
6
+ return (
7
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
8
+ {data.length > 0 &&
9
+ data.map((item, i) => {
10
+ return (
11
+ <span style={{ display: 'flex', alignItems: 'center' }}>
12
+ <img alt="alt" src={`data:image/png;base64,${item.imageData}`} />
13
+ <span style={{ fontSize: '13px' }}>{item.label}</span>
14
+ </span>
15
+ );
16
+ })}
17
+ </div>
18
+ );
19
+ };
20
+
21
+ const LegendWidget = (props) => {
22
+ const { data = {} } = props;
23
+ const { layers = {} } = data;
24
+ const { map_layers = [] } = layers;
25
+ const [legendLayers, setLegendLayers] = React.useState([]);
26
+
27
+ const activeLayer = map_layers.length > 0 ? map_layers[0]?.map_layer : '';
28
+ const fetchLegend = async (url) => {
29
+ let legendData = await fetchArcgisData(url);
30
+ const { layers = [] } = legendData;
31
+
32
+ setLegendLayers(layers);
33
+ };
34
+
35
+ React.useEffect(() => {
36
+ fetchLegend(activeLayer.map_service_url + '/legend');
37
+ }, [data, activeLayer.map_service_url]);
38
+
39
+ return (
40
+ <>
41
+ <div style={{ margin: '10px 0' }}>
42
+ <Grid>
43
+ <Grid.Row>
44
+ <Grid.Column>
45
+ <h3>Legend:</h3>
46
+ {legendLayers.length > 0 &&
47
+ legendLayers.map((layer, index) => {
48
+ return (
49
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
50
+ <h5>{layer?.layerName}</h5>
51
+ <LayerLegend data={layer.legend} />
52
+ </div>
53
+ );
54
+ })}
55
+ </Grid.Column>
56
+ </Grid.Row>
57
+ </Grid>
58
+ </div>
59
+ </>
60
+ );
61
+ };
62
+
63
+ export default LegendWidget;
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { Modal, Button, Grid } from 'semantic-ui-react';
3
+ import Webmap from '../Webmap';
4
+ import { FormFieldWrapper, InlineForm } from '@plone/volto/components';
5
+
6
+ import { panelsSchema } from './panelsSchema';
7
+
8
+ const MapEditorWidget = (props) => {
9
+ const [open, setOpen] = React.useState(false);
10
+ const { onChange = {}, block = {}, value = {}, id } = props;
11
+ const [intValue, setIntValue] = React.useState(value);
12
+
13
+ const dataForm = { map_data: intValue };
14
+
15
+ const handleApplyChanges = () => {
16
+ onChange(id, intValue);
17
+ setOpen(false);
18
+ };
19
+
20
+ const handleClose = () => {
21
+ setIntValue(value);
22
+ setOpen(false);
23
+ };
24
+
25
+ const handleChangeField = (id, fieldVal) => {
26
+ setIntValue(fieldVal);
27
+ };
28
+
29
+ return (
30
+ <FormFieldWrapper {...props}>
31
+ <Modal
32
+ id="map-editor-modal"
33
+ style={{ width: '95% !important' }}
34
+ onClose={handleClose}
35
+ onOpen={() => setOpen(true)}
36
+ open={open}
37
+ trigger={
38
+ <Button size="tiny" className="map-modal-trigger-button">
39
+ Open Map Editor
40
+ </Button>
41
+ }
42
+ >
43
+ <Modal.Content scrolling>
44
+ <Grid>
45
+ <Grid.Row>
46
+ <Grid.Column width={4}>
47
+ <InlineForm
48
+ block={block}
49
+ title={panelsSchema.title}
50
+ schema={panelsSchema}
51
+ onChangeField={(id, value) => {
52
+ handleChangeField(id, value);
53
+ }}
54
+ formData={dataForm}
55
+ />
56
+ </Grid.Column>
57
+ <Grid.Column width={8}>
58
+ <Webmap data={intValue} editMode={true} />
59
+ </Grid.Column>
60
+ </Grid.Row>
61
+ </Grid>
62
+ </Modal.Content>
63
+ <Modal.Actions>
64
+ <Grid>
65
+ <Grid.Row>
66
+ <Grid.Column width={8}></Grid.Column>
67
+ <Grid.Column width={4}>
68
+ <Button onClick={handleClose}>Close</Button>
69
+ <Button color="green" onClick={handleApplyChanges}>
70
+ Apply changes
71
+ </Button>
72
+ </Grid.Column>
73
+ </Grid.Row>
74
+ </Grid>
75
+ </Modal.Actions>
76
+ </Modal>
77
+ </FormFieldWrapper>
78
+ );
79
+ };
80
+
81
+ export default MapEditorWidget;
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Menu, Tab } from 'semantic-ui-react';
3
+ import { ObjectWidget } from '@plone/volto/components';
4
+
5
+ export const ObjectTypesWidget = (props) => {
6
+ const { schemas, value = {}, onChange, errors = {}, id } = props;
7
+ const objectId = id;
8
+
9
+ const defaultActiveTab = 0;
10
+
11
+ const [activeTab, setActiveTab] = React.useState(
12
+ defaultActiveTab > -1 ? defaultActiveTab : 0,
13
+ );
14
+ const createTab = ({ schema, id, icon }, index) => {
15
+ return {
16
+ menuItem: () => (
17
+ <Menu.Item
18
+ onClick={() => setActiveTab(index)}
19
+ active={activeTab === index}
20
+ key={id}
21
+ >
22
+ <p style={{ fontSize: '14px', fontWeight: 'bold' }}>{schema.title}</p>
23
+ </Menu.Item>
24
+ ),
25
+ render: () => {
26
+ return (
27
+ <Tab.Pane>
28
+ <ObjectWidget
29
+ schema={schema}
30
+ id={id}
31
+ errors={errors}
32
+ value={value[id] || {}}
33
+ onChange={(schemaId, v) => {
34
+ onChange(objectId, { ...value, [schemaId]: v });
35
+ }}
36
+ />
37
+ </Tab.Pane>
38
+ );
39
+ },
40
+ };
41
+ };
42
+
43
+ return (
44
+ <Tab
45
+ menu={{ fluid: true, vertical: true, tabular: true }}
46
+ panes={schemas.map(createTab)}
47
+ activeIndex={activeTab}
48
+ grid={{ paneWidth: 8, tabWidth: 4 }}
49
+ />
50
+ );
51
+ };
52
+
53
+ export default ObjectTypesWidget;