@eeacms/volto-eea-map 4.0.0 → 5.0.0

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 (73) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/package.json +6 -3
  3. package/src/Arcgis/Editor/Editor.jsx +130 -0
  4. package/src/Arcgis/Editor/EditorContext.jsx +2 -0
  5. package/src/Arcgis/Editor/Fold/Fold.jsx +56 -0
  6. package/src/Arcgis/Editor/Panels/Panel.jsx +8 -0
  7. package/src/Arcgis/Editor/Panels/SettingsGeneralPanel.jsx +217 -0
  8. package/src/Arcgis/Editor/Panels/SettingsLayersPanel.jsx +216 -0
  9. package/src/Arcgis/Editor/Panels/StructureBaseLayerPanel.jsx +60 -0
  10. package/src/Arcgis/Editor/Panels/StructureLayersPanel.jsx +394 -0
  11. package/src/Arcgis/Editor/Panels/StructureWidgetsPanel.jsx +181 -0
  12. package/src/Arcgis/Editor/Panels/index.js +6 -0
  13. package/src/Arcgis/Editor/SidebarGroup.jsx +62 -0
  14. package/src/Arcgis/Layer/Layer.jsx +247 -0
  15. package/src/Arcgis/Map/Map.jsx +287 -0
  16. package/src/Arcgis/Map/MapBuilder.jsx +111 -0
  17. package/src/Arcgis/Map/MapContext.jsx +3 -0
  18. package/src/Arcgis/Widget/Widget.jsx +170 -0
  19. package/src/Arcgis/helpers.js +140 -0
  20. package/src/Blocks/EmbedEEAMap/Edit.jsx +40 -0
  21. package/src/Blocks/EmbedEEAMap/View.jsx +122 -0
  22. package/src/Blocks/EmbedEEAMap/helpers.js +12 -0
  23. package/src/Blocks/EmbedEEAMap/schema.js +126 -0
  24. package/src/{components → Toolbar}/Share.jsx +1 -1
  25. package/src/{components/ExtraViews.jsx → Toolbar/Toolbar.jsx} +14 -16
  26. package/src/{components/visualization → Views}/VisualizationView.jsx +8 -9
  27. package/src/Widgets/ArcgisColorPickerWidget.jsx +95 -0
  28. package/src/Widgets/ArcgisRendererWidget/ArcgisRendererWidget.jsx +106 -0
  29. package/src/Widgets/ArcgisRendererWidget/RendererEditor/ClassBreaks.jsx +8 -0
  30. package/src/Widgets/ArcgisRendererWidget/RendererEditor/Dictionary.jsx +8 -0
  31. package/src/Widgets/ArcgisRendererWidget/RendererEditor/DotDensity.jsx +8 -0
  32. package/src/Widgets/ArcgisRendererWidget/RendererEditor/Heatmap.jsx +8 -0
  33. package/src/Widgets/ArcgisRendererWidget/RendererEditor/PieChart.jsx +8 -0
  34. package/src/Widgets/ArcgisRendererWidget/RendererEditor/Simple.jsx +109 -0
  35. package/src/Widgets/ArcgisRendererWidget/RendererEditor/UniqueValue.jsx +8 -0
  36. package/src/Widgets/ArcgisRendererWidget/RendererEditor/_Editor.jsx +29 -0
  37. package/src/Widgets/ArcgisRendererWidget/RendererEditor/_EditorModal.jsx +88 -0
  38. package/src/Widgets/ArcgisRendererWidget/RendererEditor/_defaults.js +30 -0
  39. package/src/Widgets/ArcgisSliderWidget.jsx +79 -0
  40. package/src/Widgets/ArcgisViewpointWidget.jsx +112 -0
  41. package/src/{components/visualization → Widgets}/VisualizationViewWidget.jsx +9 -10
  42. package/src/Widgets/VisualizationWidget.jsx +200 -0
  43. package/src/arcgis.js +48 -0
  44. package/src/constants.js +225 -7
  45. package/src/hocs/withArcgis.jsx +27 -0
  46. package/src/hooks/useChangedProps.jsx +24 -0
  47. package/src/hooks/useClass.jsx +17 -0
  48. package/src/hooks/useCopyToClipboard.jsx +25 -0
  49. package/src/index.js +16 -16
  50. package/src/jsoneditor.js +72 -0
  51. package/src/styles/editor.less +446 -0
  52. package/src/styles/map.less +3 -0
  53. package/src/components/Blocks/EmbedEEAMap/Edit.jsx +0 -161
  54. package/src/components/Blocks/EmbedEEAMap/Schema.js +0 -161
  55. package/src/components/Blocks/EmbedEEAMap/View.jsx +0 -79
  56. package/src/components/Blocks/EmbedEEAMap/helpers.js +0 -45
  57. package/src/components/LegendView.jsx +0 -150
  58. package/src/components/Webmap.jsx +0 -365
  59. package/src/components/index.js +0 -6
  60. package/src/components/visualization/VisualizationEditorWidget.jsx +0 -122
  61. package/src/components/visualization/panelsSchema.js +0 -229
  62. package/src/components/widgets/DataQueryWidget.jsx +0 -51
  63. package/src/components/widgets/LayerSelectWidget.jsx +0 -456
  64. package/src/components/widgets/LayersPanelWidget.jsx +0 -59
  65. package/src/components/widgets/SimpleColorPickerWidget.jsx +0 -121
  66. package/src/hocs/index.js +0 -3
  67. package/src/hocs/withDeviceSize.jsx +0 -45
  68. package/src/less/global.less +0 -253
  69. package/src/less/variables.less +0 -5
  70. package/src/utils.js +0 -151
  71. /package/src/{components → Toolbar}/FigureNote.jsx +0 -0
  72. /package/src/{components → Toolbar}/MoreInfoLink.jsx +0 -0
  73. /package/src/{components → Toolbar}/Sources.jsx +0 -0
@@ -0,0 +1,216 @@
1
+ import { useState, useEffect, useContext, useMemo } from 'react';
2
+ import { Segment, Dimmer, Loader } from 'semantic-ui-react';
3
+ import { isNil, toNumber } from 'lodash';
4
+ import { v4 as uuid } from 'uuid';
5
+ import { InlineForm } from '@plone/volto/components';
6
+ import {
7
+ debounce,
8
+ getLayers,
9
+ getLayerDefaults,
10
+ } from '@eeacms/volto-eea-map/Arcgis/helpers';
11
+ import { blendModes } from '@eeacms/volto-eea-map/constants';
12
+ import EditorContext from '@eeacms/volto-eea-map/Arcgis/Editor/EditorContext';
13
+ import Panel from './Panel';
14
+ import Fold from '../Fold/Fold';
15
+
16
+ function Layer({ $map, layer, layers, index, value, onChangeValue }) {
17
+ const uid = useState(uuid());
18
+ const [isReady, setIsReady] = useState(false);
19
+ const { layersApi } = useContext(EditorContext);
20
+
21
+ const layerPath = useMemo(
22
+ () =>
23
+ !isNil(layer.url) && !isNil(layer.id) ? `${layer.url}/${layer.id}` : null,
24
+ [layer.url, layer.id],
25
+ );
26
+
27
+ const $layer = useMemo(
28
+ () => ({
29
+ data: layersApi.data[layerPath],
30
+ error: layersApi.error[layerPath],
31
+ loading: layersApi.loading[layerPath],
32
+ loaded: layersApi.loaded[layerPath],
33
+ load: layersApi.load,
34
+ }),
35
+ [layersApi, layerPath],
36
+ );
37
+
38
+ const schema = useMemo(
39
+ () => ({
40
+ title: 'General',
41
+ fieldsets: [
42
+ {
43
+ id: 'default',
44
+ title: 'Default',
45
+ fields: ['blendMode', 'minScale', 'maxScale', 'opacity', 'renderer'],
46
+ },
47
+ ],
48
+ properties: {
49
+ blendMode: {
50
+ title: 'Blend mode',
51
+ choices: blendModes,
52
+ },
53
+ minScale: {
54
+ title: 'Min scale',
55
+ type: 'number',
56
+ minimum: 0,
57
+ },
58
+ maxScale: {
59
+ title: 'Max scale',
60
+ type: 'number',
61
+ minimum: 0,
62
+ },
63
+ opacity: {
64
+ title: 'Opacity',
65
+ widget: 'arcgis_slider',
66
+ },
67
+ renderer: {
68
+ title: 'Renderer',
69
+ widget: 'arcgis_renderer',
70
+ $map,
71
+ },
72
+ },
73
+ required: [],
74
+ }),
75
+ [$map],
76
+ );
77
+
78
+ useEffect(() => {
79
+ if (!$layer.loaded && !$layer.loading && !$layer.error && layerPath) {
80
+ debounce(
81
+ () => {
82
+ $layer.load(layerPath);
83
+ },
84
+ 300,
85
+ `fetch:${uid}:${layerPath}`,
86
+ );
87
+ }
88
+ }, [uid, $layer, layerPath]);
89
+
90
+ useEffect(() => {
91
+ if (!$layer.loaded || $layer.data.id !== layer.id) return;
92
+ const defaults = getLayerDefaults($layer.data);
93
+ if (JSON.stringify(defaults) !== JSON.stringify(layer.defaults)) {
94
+ onChangeValue({
95
+ ...value,
96
+ layers: layers.map((layer, i) => {
97
+ if (i !== index) return layer;
98
+ return {
99
+ ...layer,
100
+ defaults,
101
+ };
102
+ }),
103
+ });
104
+ }
105
+ setIsReady(true);
106
+ }, [index, layer, $layer, layers, value, onChangeValue]);
107
+
108
+ if (!isReady) return null;
109
+
110
+ return (
111
+ <>
112
+ <Dimmer active={$layer.loading}>
113
+ <Loader />
114
+ </Dimmer>
115
+
116
+ <InlineForm
117
+ schema={schema}
118
+ formData={{
119
+ ...(layer.defaults || {}),
120
+ ...layer,
121
+ }}
122
+ onChangeField={(id, fieldValue) => {
123
+ let $fieldValue = fieldValue;
124
+
125
+ if (
126
+ ['minScale', 'maxScale', 'minZoom', 'maxZoom', 'opacity'].includes(
127
+ id,
128
+ )
129
+ ) {
130
+ $fieldValue = Math.max(toNumber(fieldValue) || 0, 0);
131
+ }
132
+
133
+ onChangeValue({
134
+ ...value,
135
+ layers: layers.map((layer, i) => {
136
+ if (i !== index) return layer;
137
+ return {
138
+ ...layer,
139
+ [id]: $fieldValue,
140
+ };
141
+ }),
142
+ });
143
+ }}
144
+ />
145
+ {layer.defaults && (
146
+ <Segment attached style={{ textAlign: 'right' }}>
147
+ <button
148
+ className="btn-primary"
149
+ onClick={() => {
150
+ const defaults = { ...layer };
151
+ Object.keys(schema.properties).forEach((key) => {
152
+ if (key in layer.defaults) {
153
+ defaults[key] = layer.defaults[key];
154
+ }
155
+ });
156
+ onChangeValue({
157
+ ...value,
158
+ layers: layers.map((layer, i) => {
159
+ if (i !== index) return layer;
160
+ return defaults;
161
+ }),
162
+ });
163
+ }}
164
+ >
165
+ Defaults
166
+ </button>
167
+ </Segment>
168
+ )}
169
+ </>
170
+ );
171
+ }
172
+
173
+ export default function SettingsLayersPanel({
174
+ $map,
175
+ value,
176
+ properties,
177
+ onChangeValue,
178
+ }) {
179
+ const data_query = properties?.data_query;
180
+
181
+ const layers = useMemo(
182
+ () =>
183
+ getLayers(
184
+ { layers: value.layers, styles: value.styles, data_query },
185
+ false,
186
+ ),
187
+ [value.layers, value.styles, data_query],
188
+ );
189
+
190
+ return (
191
+ <Panel
192
+ content={
193
+ <>
194
+ {layers.map((layer, index) => (
195
+ <Fold
196
+ key={index}
197
+ title={`Layer ${index + 1}${
198
+ layer.name ? ` - ${layer.name}` : ''
199
+ }`}
200
+ foldable
201
+ >
202
+ <Layer
203
+ $map={$map}
204
+ layer={layer}
205
+ layers={layers}
206
+ index={index}
207
+ value={value}
208
+ onChangeValue={onChangeValue}
209
+ />
210
+ </Fold>
211
+ ))}
212
+ </>
213
+ }
214
+ />
215
+ );
216
+ }
@@ -0,0 +1,60 @@
1
+ import { useMemo } from 'react';
2
+ import { InlineForm } from '@plone/volto/components';
3
+ import { basemaps } from '@eeacms/volto-eea-map/constants';
4
+ import { getBasemap } from '@eeacms/volto-eea-map/Arcgis/helpers';
5
+ import Fold from '../Fold/Fold';
6
+
7
+ import Panel from './Panel';
8
+
9
+ const schema = {
10
+ title: 'Base layer',
11
+ fieldsets: [
12
+ {
13
+ id: 'default',
14
+ title: 'Default',
15
+ fields: ['name', 'url_template'],
16
+ },
17
+ ],
18
+ properties: {
19
+ name: {
20
+ title: 'Name',
21
+ choices: basemaps,
22
+ },
23
+ url_template: {
24
+ title: 'Custom basemap',
25
+ widget: 'textarea',
26
+ },
27
+ },
28
+ required: [],
29
+ };
30
+
31
+ export default function StructureBaseLayerPanel({ value, onChangeValue }) {
32
+ const basemap = useMemo(
33
+ () => getBasemap({ basemap: value.basemap, base: value.base }) || {},
34
+ [value.basemap, value.base],
35
+ );
36
+
37
+ return (
38
+ <Panel
39
+ content={
40
+ <Fold title="Base layer" foldable>
41
+ <InlineForm
42
+ schema={schema}
43
+ formData={basemap}
44
+ onChangeField={(id, fieldValue) => {
45
+ const $value = { ...value };
46
+ delete $value.base; // not needed (backward compatibility)
47
+ onChangeValue({
48
+ ...$value,
49
+ basemap: {
50
+ ...basemap,
51
+ [id]: fieldValue,
52
+ },
53
+ });
54
+ }}
55
+ />
56
+ </Fold>
57
+ }
58
+ />
59
+ );
60
+ }
@@ -0,0 +1,394 @@
1
+ import { memo, useState, useEffect, useContext, useMemo } from 'react';
2
+ import { compose } from 'redux';
3
+ import { isNil, toNumber } from 'lodash';
4
+ import { v4 as uuid } from 'uuid';
5
+ import { QueryBuilder, Rule as QBRule, useRule } from 'react-querybuilder';
6
+ import { Dimmer, Loader } from 'semantic-ui-react';
7
+ import { Icon, InlineForm } from '@plone/volto/components';
8
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
9
+ import addSVG from '@plone/volto/icons/add.svg';
10
+ import {
11
+ debounce,
12
+ getLayers,
13
+ getLayerDefaults,
14
+ } from '@eeacms/volto-eea-map/Arcgis/helpers';
15
+ import EditorContext from '@eeacms/volto-eea-map/Arcgis/Editor/EditorContext';
16
+ import Panel from './Panel';
17
+ import Fold from '../Fold/Fold';
18
+
19
+ function getLayersChoices(layers = []) {
20
+ return layers.map((layer) => [
21
+ layer.id.toString(),
22
+ `${
23
+ (layer.parentLayerId > -1 ? `${layer.parentLayerId}.` : '') + layer.id
24
+ } - ${layer.name} (${layer.type})`,
25
+ ]);
26
+ }
27
+
28
+ function getSublayers(subLayerIds, data) {
29
+ return subLayerIds?.reduce((acc, subLayerId) => {
30
+ const subLayer = data?.layers.find((layer) => layer.id === subLayerId);
31
+ if (!subLayer) return acc;
32
+ acc.push({
33
+ ...(subLayer || {}),
34
+ id: subLayerId,
35
+ subLayers: getSublayers(subLayer?.subLayerIds, data),
36
+ });
37
+ return acc;
38
+ }, []);
39
+ }
40
+
41
+ const RuleComponent = memo((props) => {
42
+ const [inputValue, setInputValue] = useState('');
43
+ const r = useRule(props);
44
+
45
+ const dataQuery = props.rule.dataQuery;
46
+
47
+ function handleKeyDown(event) {
48
+ if (!inputValue) return;
49
+ switch (event.key) {
50
+ case 'Enter':
51
+ case 'Tab':
52
+ if (dataQuery?.includes(inputValue)) {
53
+ setInputValue('');
54
+ event.preventDefault();
55
+ break;
56
+ }
57
+ r.generateOnChangeHandler('dataQuery')([
58
+ ...(dataQuery || []),
59
+ event.target.value,
60
+ ]);
61
+ setInputValue('');
62
+ event.preventDefault();
63
+ break;
64
+ default:
65
+ break;
66
+ }
67
+ }
68
+
69
+ const Select = props.reactSelectCreateable.default;
70
+
71
+ return (
72
+ <div className="custom-rule">
73
+ <div>
74
+ <label htmlFor="dataQuery" style={{ fontWeight: '500' }}>
75
+ Query parameters
76
+ </label>
77
+ <p
78
+ style={{
79
+ fontSize: '90%',
80
+ marginBottom: '0.25rem',
81
+ fontStyle: 'italic',
82
+ }}
83
+ >
84
+ When using page level parameters to filter the map, please specify the
85
+ corresponding name
86
+ </p>
87
+ <Select
88
+ id="dataQuery"
89
+ title="Data Query"
90
+ className="react-select"
91
+ classNamePrefix="react-select"
92
+ placeholder=""
93
+ isClearable
94
+ isMulti
95
+ menuIsOpen={false}
96
+ options={[]}
97
+ inputValue={inputValue}
98
+ value={dataQuery?.map((value) => ({ label: value, value })) || null}
99
+ onChange={(newValue) => {
100
+ r.generateOnChangeHandler('dataQuery')(
101
+ newValue?.map((value) => value.value) || [],
102
+ );
103
+ }}
104
+ onInputChange={(newValue) => setInputValue(newValue)}
105
+ onKeyDown={handleKeyDown}
106
+ />
107
+ </div>
108
+ <QBRule {...props} />
109
+ </div>
110
+ );
111
+ });
112
+
113
+ const Rule = compose(injectLazyLibs(['reactSelectCreateable']))(RuleComponent);
114
+
115
+ function Layer({ layer, layers, index, value, onChangeValue }) {
116
+ const uid = useState(uuid());
117
+ const { servicesApi, layersApi } = useContext(EditorContext);
118
+
119
+ const layerPath = useMemo(
120
+ () =>
121
+ !isNil(layer.url) && !isNil(layer.id) ? `${layer.url}/${layer.id}` : null,
122
+ [layer.url, layer.id],
123
+ );
124
+
125
+ const $service = useMemo(
126
+ () => ({
127
+ data: servicesApi.data[layer.url],
128
+ error: servicesApi.error[layer.url],
129
+ loading: servicesApi.loading[layer.url],
130
+ loaded: servicesApi.loaded[layer.url],
131
+ load: servicesApi.load,
132
+ }),
133
+ [servicesApi, layer.url],
134
+ );
135
+
136
+ const $layer = useMemo(
137
+ () => ({
138
+ data: layersApi.data[layerPath],
139
+ error: layersApi.error[layerPath],
140
+ loading: layersApi.loading[layerPath],
141
+ loaded: layersApi.loaded[layerPath],
142
+ load: layersApi.load,
143
+ }),
144
+ [layersApi, layerPath],
145
+ );
146
+
147
+ useEffect(() => {
148
+ if (!$service.loaded && !$service.loading && layer.url) {
149
+ debounce(
150
+ () => {
151
+ $service.load(layer.url);
152
+ },
153
+ 300,
154
+ `fetch:${uid}:${layer.url}`,
155
+ );
156
+ }
157
+ }, [uid, $service, layer]);
158
+
159
+ useEffect(() => {
160
+ if (!$layer.loaded && !$layer.loading && !$layer.error && layerPath) {
161
+ debounce(
162
+ () => {
163
+ $layer.load(layerPath);
164
+ },
165
+ 300,
166
+ `fetch:${uid}:${layerPath}`,
167
+ );
168
+ }
169
+ }, [uid, $layer, layerPath]);
170
+
171
+ useEffect(() => {
172
+ if (!$layer.loaded || $layer.data.id !== layer.id) return;
173
+ const defaults = getLayerDefaults($layer.data);
174
+ if (JSON.stringify(defaults) !== JSON.stringify(layer.defaults)) {
175
+ onChangeValue({
176
+ ...value,
177
+ layers: layers.map((layer, i) => {
178
+ if (i !== index) return layer;
179
+ return {
180
+ ...layer,
181
+ defaults,
182
+ };
183
+ }),
184
+ });
185
+ }
186
+ }, [index, layer, $layer, layers, value, onChangeValue]);
187
+
188
+ return (
189
+ <>
190
+ <Dimmer active={$service.loading || $layer.loading}>
191
+ <Loader />
192
+ </Dimmer>
193
+
194
+ <InlineForm
195
+ schema={{
196
+ title: `Layer ${index + 1}`,
197
+ fieldsets: [
198
+ {
199
+ id: 'default',
200
+ title: 'Default',
201
+ fields: [
202
+ 'url',
203
+ ...($service.loaded ? ['layerId'] : []),
204
+ ...($service.loaded && $layer.loaded ? ['zoomToExtent'] : []),
205
+ ],
206
+ },
207
+ ],
208
+ properties: {
209
+ url: {
210
+ title: 'Service URL',
211
+ widget: 'textarea',
212
+ description: $service.error ? (
213
+ <p
214
+ className="error"
215
+ style={{
216
+ fontSize: '10px',
217
+ overflowWrap: 'break-word',
218
+ }}
219
+ >
220
+ {$service.error.message}
221
+ </p>
222
+ ) : null,
223
+ },
224
+ layerId: {
225
+ title: 'Layer',
226
+ choices: getLayersChoices($service.data?.layers),
227
+ widget: 'select',
228
+ description: $layer.error ? (
229
+ <p
230
+ className="error"
231
+ style={{
232
+ fontSize: '10px',
233
+ overflowWrap: 'break-word',
234
+ }}
235
+ >
236
+ {$layer.error.message}
237
+ </p>
238
+ ) : null,
239
+ },
240
+ zoomToExtent: {
241
+ title: 'Zoom to extent',
242
+ type: 'boolean',
243
+ },
244
+ },
245
+ required: [],
246
+ }}
247
+ formData={{
248
+ ...layer,
249
+ layerId: !isNil(layer.id)
250
+ ? {
251
+ label: `${layer.id}${layer.name ? ` - ${layer.name}` : ''}`,
252
+ value: layer.id,
253
+ }
254
+ : null,
255
+ }}
256
+ onChangeField={(id, fieldValue) => {
257
+ let $fieldValue = fieldValue;
258
+ let newLayer = {};
259
+
260
+ if (id === 'layerId') {
261
+ $fieldValue = toNumber(fieldValue) || 0;
262
+ newLayer =
263
+ $service.data?.layers.find((layer) => layer.id === $fieldValue) ||
264
+ {};
265
+ }
266
+
267
+ onChangeValue({
268
+ ...value,
269
+ layers: layers.map((layer, i) => {
270
+ if (i !== index)
271
+ return {
272
+ ...layer,
273
+ ...(id === 'zoomToExtent' ? { zoomToExtent: false } : {}),
274
+ };
275
+ if (id === 'url') {
276
+ return { url: $fieldValue };
277
+ }
278
+ return {
279
+ ...layer,
280
+ ...newLayer,
281
+ [id === 'layerId' ? 'id' : id]: $fieldValue,
282
+ subLayers: getSublayers(newLayer?.subLayerIds, $service.data),
283
+ };
284
+ }),
285
+ });
286
+ }}
287
+ />
288
+
289
+ {$service.loaded && $layer.loaded && $layer.data?.fields?.length && (
290
+ <>
291
+ <QueryBuilder
292
+ fields={
293
+ $layer.data.fields.map((field) => {
294
+ return { name: field.name, label: field.name };
295
+ }) || []
296
+ }
297
+ query={layer.definitionExpression}
298
+ onQueryChange={(query) => {
299
+ const newLayers = layers.map((layer, i) => {
300
+ if (i !== index) return layer;
301
+ return {
302
+ ...layer,
303
+ definitionExpression: query,
304
+ };
305
+ });
306
+ if (JSON.stringify(newLayers) !== JSON.stringify(layers)) {
307
+ onChangeValue({
308
+ ...value,
309
+ layers: newLayers,
310
+ });
311
+ }
312
+ }}
313
+ valueSource={'x'}
314
+ controlElements={{
315
+ rule: Rule,
316
+ }}
317
+ />
318
+ </>
319
+ )}
320
+ </>
321
+ );
322
+ }
323
+
324
+ export default function StructureLayersPanel({
325
+ value,
326
+ properties,
327
+ onChangeValue,
328
+ }) {
329
+ const data_query = properties?.data_query;
330
+
331
+ const layers = useMemo(
332
+ () =>
333
+ getLayers(
334
+ { layers: value.layers, styles: value.styles, data_query },
335
+ false,
336
+ ),
337
+ [value.layers, value.styles, data_query],
338
+ );
339
+
340
+ return (
341
+ <Panel
342
+ header={
343
+ <div style={{ width: '100%', textAlign: 'right' }}>
344
+ <button
345
+ className="btn-primary"
346
+ onClick={() => {
347
+ onChangeValue({
348
+ ...value,
349
+ layers: [
350
+ ...layers,
351
+ {
352
+ id: null,
353
+ url: null,
354
+ type: null,
355
+ },
356
+ ],
357
+ });
358
+ }}
359
+ >
360
+ <Icon name={addSVG} size="16px" /> Layer
361
+ </button>
362
+ </div>
363
+ }
364
+ content={
365
+ <>
366
+ {layers.map((layer, index) => (
367
+ <Fold
368
+ key={index}
369
+ title={`Layer ${index + 1}${
370
+ layer.name ? ` - ${layer.name}` : ''
371
+ }`}
372
+ onDelete={() => {
373
+ onChangeValue({
374
+ ...value,
375
+ layers: layers.filter((_, i) => i !== index),
376
+ });
377
+ }}
378
+ foldable
379
+ deletable
380
+ >
381
+ <Layer
382
+ layer={layer}
383
+ layers={layers}
384
+ index={index}
385
+ value={value}
386
+ onChangeValue={onChangeValue}
387
+ />
388
+ </Fold>
389
+ ))}
390
+ </>
391
+ }
392
+ />
393
+ );
394
+ }