exb-camviewer 1.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.
- package/LICENSE.md +7 -0
- package/README.md +16 -0
- package/cameraExample.png +0 -0
- package/config.json +3 -0
- package/icon.svg +7 -0
- package/manifest.json +22 -0
- package/package.json +25 -0
- package/src/config.ts +8 -0
- package/src/runtime/style.css +43 -0
- package/src/runtime/translations/default.ts +4 -0
- package/src/runtime/widget.tsx +259 -0
- package/src/setting/setting.tsx +101 -0
- package/src/setting/translations/default.ts +4 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Lucius Creamer
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# CamViewer Widget
|
|
2
|
+
|
|
3
|
+
This is a simple widget to view .m3u8 streams using a url field in a feature layer. The widget appears as a button, which when clicked will turn on the "camera" layer, and when a camera is clicked a popup video stream will appear to start streaming the live camera feed.
|
|
4
|
+
|
|
5
|
+
## Interactive Example
|
|
6
|
+
|
|
7
|
+
This widget can be used and interacted with on my example application, found [here](https://exb.luciuscreamer.com/camera). In case you miss it, the camera widget is the little button at the bottom of the screen. Click it to turn on the camera layer.
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Setting up the widget
|
|
11
|
+
|
|
12
|
+
This widget can be set up by adding the widget to your application, then selecting the map and data source the camera layer is stored in. You will then select the field that the .m3u8 url is stored in, as well as an icon for the main widget button. It is recommended that you set a background color in the style settings, and also set the height and width to "auto".
|
|
13
|
+
|
|
14
|
+
## Using the widget
|
|
15
|
+
|
|
16
|
+
Click the widget button (Icon selected by builder), and cameras should appear on your map. Click a camera icon on your map, and a viewing window should appear. You can then click the X icon in the top right of the camera window to close the camera feed and unselect the camera feature/features.
|
|
Binary file
|
package/config.json
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
2
|
+
|
|
3
|
+
<svg fill="#ffffff" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px" viewBox="0 0 856.506 856.506" xml:space="preserve" stroke="#ffffff">
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
package/manifest.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "CamViewer",
|
|
3
|
+
"label": "Camera Viewer",
|
|
4
|
+
"type": "widget",
|
|
5
|
+
"version": "1.18.0",
|
|
6
|
+
"exbVersion": "1.18.0",
|
|
7
|
+
"author": "Lucius Creamer",
|
|
8
|
+
"description": "This widget allows users to view and interact with camera feeds in a map context.",
|
|
9
|
+
"copyright": "",
|
|
10
|
+
"dependency": [
|
|
11
|
+
"jimu-arcgis"
|
|
12
|
+
],
|
|
13
|
+
"license": "http://www.apache.org/licenses/LICENSE-2.0",
|
|
14
|
+
"properties": {},
|
|
15
|
+
"translatedLocales": [
|
|
16
|
+
"en"
|
|
17
|
+
],
|
|
18
|
+
"defaultSize": {
|
|
19
|
+
"width": 800,
|
|
20
|
+
"height": 500
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exb-camviewer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A camera viewer, to view .m3u8 streams via a url in a feature layer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Lucius Creamer",
|
|
7
|
+
"homepage": "https://github.com/SunshineLuke90/widgets#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/SunshineLuke90/widgets/issues"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"hls.js": "^1.6.15"
|
|
13
|
+
},
|
|
14
|
+
"main": "src/runtime/widget.tsx",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"exb-widget",
|
|
17
|
+
"experience-builder",
|
|
18
|
+
"exb"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/SunshineLuke90/widgets.git",
|
|
23
|
+
"directory": "CamViewer"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.widget-camviewer .view-buttons-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
.widget-camviewer .jimu-widget{
|
|
7
|
+
display: flex;
|
|
8
|
+
text-align: center;
|
|
9
|
+
color: var(--dark);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.widget-camviewer-not-configured {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
height: 100%;
|
|
18
|
+
width: 100%;
|
|
19
|
+
background-color: var(--light);
|
|
20
|
+
color: var(--dark);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.widget-camviewer .view-button {
|
|
24
|
+
text-align: center;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
/* Default state using theme variables */
|
|
27
|
+
background-color: transparent;
|
|
28
|
+
color: var(--dark);
|
|
29
|
+
align-items: center;
|
|
30
|
+
vertical-align: middle;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
gap: 4px;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
text-overflow: ellipsis;
|
|
36
|
+
size: auto;
|
|
37
|
+
height: auto;
|
|
38
|
+
width: auto;
|
|
39
|
+
button {
|
|
40
|
+
background-color: var(--light);
|
|
41
|
+
color: var(--dark);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { React, type AllWidgetProps, type DataSource, DataSourceComponent, type FeatureLayerQueryParams } from 'jimu-core'
|
|
2
|
+
import type { IMConfig } from '../config'
|
|
3
|
+
import defaultMessages from './translations/default'
|
|
4
|
+
import { JimuMapViewComponent, type JimuMapView, type JimuLayerView } from 'jimu-arcgis'
|
|
5
|
+
import { Button, Icon } from 'jimu-ui'
|
|
6
|
+
import './style.css'
|
|
7
|
+
import { useCallback, useState } from 'react'
|
|
8
|
+
import { createPortal } from 'react-dom'
|
|
9
|
+
|
|
10
|
+
// You need to install hls.js into your client directory
|
|
11
|
+
// Run npm install hls.js in your client directory to install.
|
|
12
|
+
import Hls from 'hls.js'
|
|
13
|
+
|
|
14
|
+
export default function Widget(props: AllWidgetProps<IMConfig>) {
|
|
15
|
+
const { useDataSources, useMapWidgetIds } = props
|
|
16
|
+
const [jimuMapView, setJimuMapView] = React.useState<JimuMapView>(null)
|
|
17
|
+
|
|
18
|
+
const isConfigured = useDataSources && useDataSources.length > 0 && useDataSources[0].dataSourceId && useDataSources[0].fields && useDataSources[0].fields.length > 0
|
|
19
|
+
|
|
20
|
+
// Dedicated state for currentURL and layerView
|
|
21
|
+
const [currentURL, setCurrentURL] = React.useState<string | undefined>(undefined)
|
|
22
|
+
const [layerView, setLayerView] = React.useState<JimuLayerView | undefined>(undefined)
|
|
23
|
+
const [camLayerOn, setCamLayerOn] = React.useState(false)
|
|
24
|
+
const [ds, setDs] = useState<DataSource>(null)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// State for video position and size
|
|
28
|
+
const [videoPos, setVideoPos] = useState({ x: 100, y: 100 })
|
|
29
|
+
const [dragging, setDragging] = useState(false)
|
|
30
|
+
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 })
|
|
31
|
+
const [resizing, setResizing] = useState(false)
|
|
32
|
+
const [resizeStart, setResizeStart] = useState({ x: 0, y: 0, width: 250, height: 140 })
|
|
33
|
+
const [videoSize, setVideoSize] = useState({ width: 250, height: 140 })
|
|
34
|
+
const [showVideo, setShowVideo] = useState(true)
|
|
35
|
+
const [aspectRatio, setAspectRatio] = useState(16 / 9)
|
|
36
|
+
|
|
37
|
+
const dataRender = (ds: DataSource) => {
|
|
38
|
+
const selectedRecords = ds.getSelectedRecords()
|
|
39
|
+
const fieldanme = props.useDataSources[0].fields[0]
|
|
40
|
+
if (selectedRecords.length > 0) {
|
|
41
|
+
const url = selectedRecords[0].getFieldValue(fieldanme)
|
|
42
|
+
setCurrentURL(url)
|
|
43
|
+
setShowVideo(true)
|
|
44
|
+
}
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Draggable, resizable, closable video portal
|
|
49
|
+
const handleMouseMove = (e) => {
|
|
50
|
+
if (dragging) {
|
|
51
|
+
setVideoPos({ x: e.clientX - dragOffset.x, y: e.clientY - dragOffset.y })
|
|
52
|
+
} else if (resizing) {
|
|
53
|
+
const dx = e.clientX - resizeStart.x
|
|
54
|
+
const newWidth = Math.max(120, resizeStart.width + dx)
|
|
55
|
+
const newHeight = Math.round(newWidth / aspectRatio)
|
|
56
|
+
setVideoSize({ width: newWidth, height: newHeight })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const handleMouseUp = () => {
|
|
60
|
+
setDragging(false)
|
|
61
|
+
setResizing(false)
|
|
62
|
+
}
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (dragging || resizing) {
|
|
65
|
+
window.addEventListener('mousemove', handleMouseMove)
|
|
66
|
+
window.addEventListener('mouseup', handleMouseUp)
|
|
67
|
+
return () => {
|
|
68
|
+
window.removeEventListener('mousemove', handleMouseMove)
|
|
69
|
+
window.removeEventListener('mouseup', handleMouseUp)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const videoPortal = (currentURL && showVideo) ? createPortal(
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
position: 'fixed',
|
|
78
|
+
left: videoPos.x,
|
|
79
|
+
top: videoPos.y,
|
|
80
|
+
zIndex: 9999,
|
|
81
|
+
cursor: dragging ? 'grabbing' : 'grab',
|
|
82
|
+
background: 'rgba(0,0,0,0.2)',
|
|
83
|
+
borderRadius: 10,
|
|
84
|
+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)',
|
|
85
|
+
padding: 4,
|
|
86
|
+
userSelect: 'none',
|
|
87
|
+
minWidth: 120,
|
|
88
|
+
minHeight: 68
|
|
89
|
+
}}
|
|
90
|
+
onMouseDown={e => {
|
|
91
|
+
// Only drag if not clicking close or resize
|
|
92
|
+
if ((e.target as HTMLElement).classList.contains('video-close-btn') || (e.target as HTMLElement).classList.contains('video-resize-handle')) return
|
|
93
|
+
setDragging(true)
|
|
94
|
+
setDragOffset({ x: e.clientX - videoPos.x, y: e.clientY - videoPos.y })
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{/* Close button */}
|
|
98
|
+
<div
|
|
99
|
+
className="video-close-btn"
|
|
100
|
+
style={{
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
top: 2,
|
|
103
|
+
right: 6,
|
|
104
|
+
fontSize: 18,
|
|
105
|
+
color: '#fff',
|
|
106
|
+
background: 'rgba(0,0,0,0.5)',
|
|
107
|
+
borderRadius: '50%',
|
|
108
|
+
width: 24,
|
|
109
|
+
height: 24,
|
|
110
|
+
display: 'flex',
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
cursor: 'pointer',
|
|
114
|
+
zIndex: 2
|
|
115
|
+
}}
|
|
116
|
+
onClick={() => {
|
|
117
|
+
ds.clearSelection()
|
|
118
|
+
setShowVideo(false)
|
|
119
|
+
}}
|
|
120
|
+
title="Close"
|
|
121
|
+
>×</div>
|
|
122
|
+
{/* Video */}
|
|
123
|
+
<video
|
|
124
|
+
src={currentURL}
|
|
125
|
+
autoPlay={true}
|
|
126
|
+
style={{ width: videoSize.width, height: videoSize.height, borderRadius: 10, display: 'block' }}
|
|
127
|
+
className={'cameraViewer-video'}
|
|
128
|
+
onLoadedMetadata={e => {
|
|
129
|
+
const video = e.currentTarget
|
|
130
|
+
if (video.videoWidth && video.videoHeight) {
|
|
131
|
+
const ratio = video.videoWidth / video.videoHeight
|
|
132
|
+
setAspectRatio(ratio)
|
|
133
|
+
// Optionally, update window size to match new aspect ratio
|
|
134
|
+
setVideoSize(prev => ({ width: prev.width, height: Math.round(prev.width / ratio) }))
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
{/* Resize handle */}
|
|
139
|
+
<div
|
|
140
|
+
className="video-resize-handle"
|
|
141
|
+
style={{
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
right: 2,
|
|
144
|
+
bottom: 2,
|
|
145
|
+
width: 18,
|
|
146
|
+
height: 18,
|
|
147
|
+
background: 'rgba(0,0,0,0.3)',
|
|
148
|
+
borderRadius: 4,
|
|
149
|
+
cursor: 'nwse-resize',
|
|
150
|
+
zIndex: 2,
|
|
151
|
+
display: 'flex',
|
|
152
|
+
alignItems: 'flex-end',
|
|
153
|
+
justifyContent: 'flex-end',
|
|
154
|
+
}}
|
|
155
|
+
onMouseDown={e => {
|
|
156
|
+
e.stopPropagation()
|
|
157
|
+
setResizing(true)
|
|
158
|
+
setResizeStart({ x: e.clientX, y: e.clientY, width: videoSize.width, height: videoSize.height })
|
|
159
|
+
}}
|
|
160
|
+
title="Resize"
|
|
161
|
+
>
|
|
162
|
+
<svg width="16" height="16" viewBox="0 0 16 16"><polyline points="4,16 16,4" stroke="#fff" strokeWidth="2" fill="none"/></svg>
|
|
163
|
+
</div>
|
|
164
|
+
</div>,
|
|
165
|
+
document.body
|
|
166
|
+
) : null
|
|
167
|
+
|
|
168
|
+
const activeViewChangeHandler = (jmv: JimuMapView): void => {
|
|
169
|
+
if (jmv) {
|
|
170
|
+
setJimuMapView(jmv)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const toggleLayerVisibility = useCallback(() => {
|
|
175
|
+
if (!jimuMapView || !props.useDataSources?.[0] || !layerView) {
|
|
176
|
+
console.warn('Map view, data source, or layerView is not ready.')
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
const newVal = !camLayerOn
|
|
180
|
+
setCamLayerOn(newVal)
|
|
181
|
+
layerView.view.set({ visible: newVal })
|
|
182
|
+
console.log('Layer visibility toggled:', newVal)
|
|
183
|
+
}, [jimuMapView, props.useDataSources, layerView, camLayerOn])
|
|
184
|
+
|
|
185
|
+
React.useEffect(() => {
|
|
186
|
+
if (jimuMapView && props.useDataSources?.length > 0 && !layerView) {
|
|
187
|
+
const jimuLayerView = jimuMapView.getJimuLayerViewByDataSourceId(props.useDataSources[0].dataSourceId)
|
|
188
|
+
if (!jimuLayerView) {
|
|
189
|
+
console.warn('Cannot find layer view.')
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
setLayerView(jimuLayerView)
|
|
193
|
+
setCamLayerOn(false)
|
|
194
|
+
jimuLayerView.layer.set({ visible: false })
|
|
195
|
+
console.log('Layer visibility turned off (Default):', false)
|
|
196
|
+
}
|
|
197
|
+
}, [jimuMapView, props.useDataSources, layerView])
|
|
198
|
+
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
console.log("useEffect called")
|
|
201
|
+
const hls = new Hls({ debug: true })
|
|
202
|
+
console.log("HLS instance created:", hls)
|
|
203
|
+
if (Hls.isSupported() && currentURL) {
|
|
204
|
+
hls.loadSource(currentURL)
|
|
205
|
+
hls.on(Hls.Events.ERROR, (err) => {
|
|
206
|
+
console.log(err)
|
|
207
|
+
})
|
|
208
|
+
} else {
|
|
209
|
+
console.log("load")
|
|
210
|
+
}
|
|
211
|
+
return () => {
|
|
212
|
+
hls.destroy()
|
|
213
|
+
console.log("HLS instance destroyed")
|
|
214
|
+
}
|
|
215
|
+
}, [currentURL])
|
|
216
|
+
|
|
217
|
+
if (!isConfigured) {
|
|
218
|
+
return (
|
|
219
|
+
<div className="widget-camviewer jimu-widget">
|
|
220
|
+
<div className="widget-camviewer-not-configured">
|
|
221
|
+
{defaultMessages.notConfigured}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<>
|
|
229
|
+
<div className="widget-camviewer jimu-widget" style={{ overflow: 'auto' }}>
|
|
230
|
+
{useMapWidgetIds?.length > 0 && (
|
|
231
|
+
<JimuMapViewComponent
|
|
232
|
+
useMapWidgetId={useMapWidgetIds?.[0]}
|
|
233
|
+
onActiveViewChange={activeViewChangeHandler}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
<div className="view-button">
|
|
237
|
+
<Button
|
|
238
|
+
aria-label="Button"
|
|
239
|
+
icon
|
|
240
|
+
onClick={toggleLayerVisibility}
|
|
241
|
+
size="default"
|
|
242
|
+
>
|
|
243
|
+
<Icon icon={props.config.icon.svg} size="l" />
|
|
244
|
+
</Button>
|
|
245
|
+
</div>
|
|
246
|
+
<DataSourceComponent useDataSource={props.useDataSources[0]} query={{ where: '1=1' } as FeatureLayerQueryParams} widgetId={props.id}>
|
|
247
|
+
{
|
|
248
|
+
(ds: DataSource) => {
|
|
249
|
+
dataRender(ds)
|
|
250
|
+
setDs(ds)
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
</DataSourceComponent>
|
|
255
|
+
</div>
|
|
256
|
+
{videoPortal}
|
|
257
|
+
</>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Immutable,
|
|
3
|
+
type UseDataSource,
|
|
4
|
+
DataSourceTypes,
|
|
5
|
+
type IMFieldSchema
|
|
6
|
+
} from "jimu-core"
|
|
7
|
+
import type { AllWidgetSettingProps } from "jimu-for-builder"
|
|
8
|
+
import {
|
|
9
|
+
MapWidgetSelector,
|
|
10
|
+
SettingRow,
|
|
11
|
+
SettingSection
|
|
12
|
+
} from "jimu-ui/advanced/setting-components"
|
|
13
|
+
import defaultI18nMessages from "./translations/default"
|
|
14
|
+
import type { IMConfig } from "../config"
|
|
15
|
+
import {
|
|
16
|
+
DataSourceSelector,
|
|
17
|
+
FieldSelector
|
|
18
|
+
} from "jimu-ui/advanced/data-source-selector"
|
|
19
|
+
import { IconPicker } from "jimu-ui/advanced/resource-selector"
|
|
20
|
+
|
|
21
|
+
export default function Setting(props: AllWidgetSettingProps<IMConfig>) {
|
|
22
|
+
const onMapSelected = (useMapWidgetIds: string[]) => {
|
|
23
|
+
props.onSettingChange({
|
|
24
|
+
id: props.id,
|
|
25
|
+
useMapWidgetIds: useMapWidgetIds
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const onFieldsChange = (fields: IMFieldSchema[]) => {
|
|
30
|
+
const useDataSource = props.useDataSources[0]
|
|
31
|
+
.set(
|
|
32
|
+
"fields",
|
|
33
|
+
fields?.map((f) => f.jimuName)
|
|
34
|
+
)
|
|
35
|
+
.asMutable({ deep: true })
|
|
36
|
+
// Save the selected fields to widget json.
|
|
37
|
+
props.onSettingChange({
|
|
38
|
+
id: props.id,
|
|
39
|
+
useDataSources: [useDataSource]
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const onDataSourceChange = (useDataSources: UseDataSource[]) => {
|
|
44
|
+
props.onSettingChange({
|
|
45
|
+
id: props.id,
|
|
46
|
+
useDataSources: useDataSources
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const onIconChange = (icon) => {
|
|
51
|
+
props.onSettingChange({
|
|
52
|
+
id: props.id,
|
|
53
|
+
config: {
|
|
54
|
+
icon: icon
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<div className="view-layers-toggle-setting">
|
|
60
|
+
<SettingSection
|
|
61
|
+
title={props.intl.formatMessage({
|
|
62
|
+
id: "selectedMapLabel",
|
|
63
|
+
defaultMessage: defaultI18nMessages.selectedMap
|
|
64
|
+
})}
|
|
65
|
+
>
|
|
66
|
+
<SettingRow>
|
|
67
|
+
<IconPicker
|
|
68
|
+
icon={props.config?.icon as any}
|
|
69
|
+
onChange={onIconChange}
|
|
70
|
+
/>
|
|
71
|
+
</SettingRow>
|
|
72
|
+
<SettingRow>
|
|
73
|
+
<MapWidgetSelector
|
|
74
|
+
onSelect={onMapSelected}
|
|
75
|
+
useMapWidgetIds={props.useMapWidgetIds}
|
|
76
|
+
/>
|
|
77
|
+
</SettingRow>
|
|
78
|
+
<SettingRow>
|
|
79
|
+
<DataSourceSelector
|
|
80
|
+
types={Immutable([DataSourceTypes.FeatureLayer])}
|
|
81
|
+
mustUseDataSource={true}
|
|
82
|
+
useDataSources={props.useDataSources}
|
|
83
|
+
useDataSourcesEnabled={props.useDataSourcesEnabled}
|
|
84
|
+
onChange={onDataSourceChange}
|
|
85
|
+
widgetId={props.id}
|
|
86
|
+
/>
|
|
87
|
+
</SettingRow>
|
|
88
|
+
<SettingRow>
|
|
89
|
+
<FieldSelector
|
|
90
|
+
useDataSources={props.useDataSources}
|
|
91
|
+
useDropdown={true}
|
|
92
|
+
isMultiple={false}
|
|
93
|
+
isDataSourceDropDownHidden={true}
|
|
94
|
+
onChange={onFieldsChange}
|
|
95
|
+
selectedFields={props.useDataSources?.[0].fields}
|
|
96
|
+
/>
|
|
97
|
+
</SettingRow>
|
|
98
|
+
</SettingSection>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|