exb-calendar 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/README.md +28 -0
- package/calendarExample.png +0 -0
- package/config.json +3 -0
- package/icon.svg +7 -0
- package/manifest.json +23 -0
- package/package.json +16 -0
- package/src/config.ts +32 -0
- package/src/runtime/style.css +51 -0
- package/src/runtime/translations/default.ts +4 -0
- package/src/runtime/widget.tsx +229 -0
- package/src/setting/setting.tsx +419 -0
- package/src/setting/translations/default.ts +4 -0
- package/tests/calendar-widget.test.tsx +100 -0
- package/tests/feature-service.ts +188 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Calendar Widget
|
|
2
|
+
|
|
3
|
+
This widget provides a simple interactive calendar to work alongside an ArcGIS Online feature layer. Using the widget is as simple as adding the widget to your application, selecting a data source, and then mapping the fields. This widget is also set up to use message actions, meaning selecting an event on the calendar also selects the feature on any maps, lists, tables, etc. that the data source is also used in. This can also be set to zoom in on a map, apply filtering, and much more. Data filtering also works on the calendar, so if the data source is filtered by another widget, the calendar events will also be filtered.
|
|
4
|
+
|
|
5
|
+
## Interactive Example
|
|
6
|
+
|
|
7
|
+
This widget can be used and interacted with on my example application, found [here](https://exb.luciuscreamer.com/calendar). Worth noting, the calendar widget follows Experience Builder styling, to fit the look of your application. If you don't vibe with the dark theme, the widget works just fine in a light themed application.
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Setting up the Calendar
|
|
11
|
+
|
|
12
|
+
The calendar has a few data formatting requirements if you want all features to work flawlessly OOTB. These will be explained below.
|
|
13
|
+
|
|
14
|
+
1. Label Field
|
|
15
|
+
- The label field is what shows up on the event in the calendar (This is a short tagline.)
|
|
16
|
+
- Type: Text
|
|
17
|
+
2. Start Date
|
|
18
|
+
- The start date needs to be a Date field, and is required for all events to be populated on the calendar.
|
|
19
|
+
- Type: Date (Date and time)
|
|
20
|
+
3. End Date
|
|
21
|
+
- The end date needs to be a date field, but is not required for "all day" events.
|
|
22
|
+
- Type: Date (Date and time)
|
|
23
|
+
4. All Day Field
|
|
24
|
+
- The all day field is a simple "y/n" field, to denote whether an event is an "all day" event. The widget only checks if the field is set to "y" to trigger it treating an event as an "all day" event.
|
|
25
|
+
- Type: Text
|
|
26
|
+
5. Description Field
|
|
27
|
+
- The description is shown in the tooltip shown when the user hovers over the event. This can be a longer text field, but should likely not be longer than a sentence or two.
|
|
28
|
+
- Type: text
|
|
Binary file
|
package/config.json
ADDED
package/icon.svg
ADDED
package/manifest.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Calendar",
|
|
3
|
+
"label": "Calendar",
|
|
4
|
+
"type": "widget",
|
|
5
|
+
"version": "1.19.0",
|
|
6
|
+
"exbVersion": "1.19.0",
|
|
7
|
+
"author": "Lucius Creamer",
|
|
8
|
+
"description": "This widget uses a calendar from the fullcalendar library, to display ArcGIS Data in interactable calendar format.",
|
|
9
|
+
"copyright": "",
|
|
10
|
+
"dependency": [
|
|
11
|
+
"jimu-arcgis"
|
|
12
|
+
],
|
|
13
|
+
"license": "http://www.apache.org/licenses/LICENSE-2.0",
|
|
14
|
+
"publishMessages": ["DATA_RECORDS_SELECTION_CHANGE"],
|
|
15
|
+
"properties": {},
|
|
16
|
+
"translatedLocales": [
|
|
17
|
+
"en"
|
|
18
|
+
],
|
|
19
|
+
"defaultSize": {
|
|
20
|
+
"width": 800,
|
|
21
|
+
"height": 500
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exb-calendar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Calendar widget for Experience Builder Developer Edition",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Lucius Creamer",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@fullcalendar/daygrid": "^6.1.20",
|
|
9
|
+
"@fullcalendar/interaction": "^6.1.20",
|
|
10
|
+
"@fullcalendar/react": "^6.1.20",
|
|
11
|
+
"@fullcalendar/timegrid": "^6.1.20"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies":{
|
|
14
|
+
"polished": "4.3.1"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ImmutableObject } from 'seamless-immutable'
|
|
2
|
+
import type { UseDataSource } from 'jimu-core'
|
|
3
|
+
|
|
4
|
+
export interface colorset {
|
|
5
|
+
id: string;
|
|
6
|
+
fieldValue: string;
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface data {
|
|
11
|
+
id: string // Unique ID for React keys
|
|
12
|
+
|
|
13
|
+
// Individual field keys selected in the settings. Store as a single field name (jimuName).
|
|
14
|
+
labelField?: string
|
|
15
|
+
startDateField?: string
|
|
16
|
+
endDateField?: string
|
|
17
|
+
allDayField?: string
|
|
18
|
+
descriptionField?: string
|
|
19
|
+
colorsetField?: string
|
|
20
|
+
|
|
21
|
+
// Color settings
|
|
22
|
+
defaultEventColor?: string
|
|
23
|
+
colorsets?: colorset[]
|
|
24
|
+
// optional datasource selection saved per dataset
|
|
25
|
+
useDataSources?: UseDataSource[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Config {
|
|
29
|
+
dataSets: data[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type IMConfig = ImmutableObject<Config>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
:root{
|
|
2
|
+
--fc-small-font-size: .85em;
|
|
3
|
+
--fc-page-bg-color: var(--sys-color-surface-background);
|
|
4
|
+
--fc-neutral-bg-color: hsla(0, 0%, 82%, .3);
|
|
5
|
+
--fc-neutral-text-color: var(--sys-color-surface-header-text);
|
|
6
|
+
--fc-border-color: var(--sys-color-divider-primary);
|
|
7
|
+
--fc-button-text-color: var(--sys-color-action-text);
|
|
8
|
+
--fc-button-bg-color: var(--sys-color-action);
|
|
9
|
+
--fc-button-border-color: var(--sys-color-divider-switch);
|
|
10
|
+
--fc-button-hover-bg-color: var(--sys-color-action-hover);
|
|
11
|
+
--fc-button-hover-border-color: var(--sys-color-divider-switch);
|
|
12
|
+
--fc-button-active-bg-color: var(--sys-color-action-selected);
|
|
13
|
+
--fc-button-active-border-color: var(--sys-color-divider-switch);
|
|
14
|
+
--fc-event-bg-color: var(--sys-color-secondary-light);
|
|
15
|
+
--fc-event-border-color: var(--sys-color-divider-switch);
|
|
16
|
+
--fc-event-text-color: var(--sys-color-action-selected-text);
|
|
17
|
+
--fc-event-selected-overlay-color: rgba(0, 0, 0, 0.3);
|
|
18
|
+
--fc-more-link-bg-color: #d0d0d0;
|
|
19
|
+
--fc-more-link-text-color: inherit;
|
|
20
|
+
--fc-event-resizer-thickness: 8px;
|
|
21
|
+
--fc-event-resizer-dot-total-width: 8px;
|
|
22
|
+
--fc-event-resizer-dot-border-width: 1px;
|
|
23
|
+
--fc-non-business-color: hsla(0, 0%, 84%, .3);
|
|
24
|
+
--fc-bg-event-color: #8fdf82;
|
|
25
|
+
--fc-bg-event-opacity: 0.3;
|
|
26
|
+
--fc-highlight-color: var(--sys-color-action-focus);
|
|
27
|
+
--fc-today-bg-color: rgba(255, 220, 40, .15);
|
|
28
|
+
--fc-now-indicator-color: red;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
.fc-button-primary:not(:disabled).fc-button-active{
|
|
33
|
+
color: var(--sys-color-action-selected-text)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.fc {
|
|
37
|
+
box-sizing: content-box;
|
|
38
|
+
height: 100%;
|
|
39
|
+
width:100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.widget-calendar-not-configured {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
height: 100%;
|
|
48
|
+
width: 100%;
|
|
49
|
+
background-color: var(--light);
|
|
50
|
+
color: var(--dark);
|
|
51
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
React,
|
|
3
|
+
type AllWidgetProps,
|
|
4
|
+
type DataSource,
|
|
5
|
+
DataSourceComponent,
|
|
6
|
+
type FeatureLayerQueryParams,
|
|
7
|
+
DataSourceStatus,
|
|
8
|
+
MessageManager,
|
|
9
|
+
DataRecordsSelectionChangeMessage
|
|
10
|
+
} from "jimu-core"
|
|
11
|
+
import type { data, IMConfig } from "../config"
|
|
12
|
+
import "./style.css"
|
|
13
|
+
|
|
14
|
+
import FullCalendar from "@fullcalendar/react"
|
|
15
|
+
import dayGridPlugin from "@fullcalendar/daygrid"
|
|
16
|
+
import interactionPlugin from "@fullcalendar/interaction"
|
|
17
|
+
import timeGridPlugin from "@fullcalendar/timegrid"
|
|
18
|
+
import { cssVar } from "polished"
|
|
19
|
+
|
|
20
|
+
export default function Widget(props: AllWidgetProps<IMConfig>) {
|
|
21
|
+
const { config } = props
|
|
22
|
+
const [datasources, setDatasources] = React.useState<DataSource[]>([])
|
|
23
|
+
// Store events keyed by datasource ID to support multiple datasources
|
|
24
|
+
const [eventsByDsId, setEventsByDsId] = React.useState<{
|
|
25
|
+
[dsId: string]: any[]
|
|
26
|
+
}>({})
|
|
27
|
+
|
|
28
|
+
// Compute flat events array from all datasources
|
|
29
|
+
const events = Object.values(eventsByDsId).flat()
|
|
30
|
+
|
|
31
|
+
const isConfigured =
|
|
32
|
+
config.dataSets &&
|
|
33
|
+
config.dataSets.length > 0 &&
|
|
34
|
+
config.dataSets[0].useDataSources &&
|
|
35
|
+
config.dataSets[0].useDataSources.length === 1 &&
|
|
36
|
+
config.dataSets[0].startDateField &&
|
|
37
|
+
config.dataSets[0].endDateField
|
|
38
|
+
|
|
39
|
+
const fillCalendarEvents = (ds: DataSource, dsConfig: data) => {
|
|
40
|
+
if (!ds) return
|
|
41
|
+
try {
|
|
42
|
+
const dsId = ds.id
|
|
43
|
+
const records = ds.getRecords() || []
|
|
44
|
+
const currentEventsForDs = eventsByDsId[dsId] || []
|
|
45
|
+
|
|
46
|
+
// Only update if record count changed to avoid unnecessary state updates
|
|
47
|
+
if (records.length === currentEventsForDs.length) return
|
|
48
|
+
|
|
49
|
+
const loadedEvents = records.map((record) => {
|
|
50
|
+
const title = record.getFieldValue(dsConfig.labelField) as string
|
|
51
|
+
const rawStart = record.getFieldValue(dsConfig.startDateField)
|
|
52
|
+
const rawEnd = record.getFieldValue(dsConfig.endDateField)
|
|
53
|
+
const rawAllDay = record.getFieldValue(dsConfig.allDayField) as string
|
|
54
|
+
const description = record.getFieldValue(
|
|
55
|
+
dsConfig.descriptionField
|
|
56
|
+
) as string
|
|
57
|
+
const colorFieldValue = record.getFieldValue(
|
|
58
|
+
dsConfig.colorsetField
|
|
59
|
+
) as string
|
|
60
|
+
|
|
61
|
+
let start: string, end: string, color: string | number
|
|
62
|
+
|
|
63
|
+
const toISO = (v: any) => {
|
|
64
|
+
if (v == null) return undefined
|
|
65
|
+
const d = v instanceof Date ? v : new Date(v)
|
|
66
|
+
return isNaN(d.getTime()) ? undefined : d.toISOString()
|
|
67
|
+
}
|
|
68
|
+
const toISODateOnly = (v: any) => {
|
|
69
|
+
if (v == null) return undefined
|
|
70
|
+
const d = v instanceof Date ? v : new Date(v)
|
|
71
|
+
d.setHours(d.getHours() - d.getTimezoneOffset() / 60) // Adjust to local date
|
|
72
|
+
return isNaN(d.getTime()) ? undefined : d.toISOString().split("T")[0]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (rawAllDay === "y") {
|
|
76
|
+
start = toISODateOnly(rawStart)
|
|
77
|
+
} else {
|
|
78
|
+
start = toISO(rawStart)
|
|
79
|
+
end = toISO(rawEnd)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (dsConfig.colorsets && colorFieldValue) {
|
|
83
|
+
const matchedColorSet = dsConfig.colorsets.find(
|
|
84
|
+
(cs) => cs.fieldValue === colorFieldValue
|
|
85
|
+
)
|
|
86
|
+
if (matchedColorSet) {
|
|
87
|
+
color = matchedColorSet.color
|
|
88
|
+
} else {
|
|
89
|
+
color =
|
|
90
|
+
dsConfig.defaultEventColor ||
|
|
91
|
+
cssVar("--ref-palette-secondary-500")
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
color =
|
|
95
|
+
dsConfig.defaultEventColor || cssVar("--ref-palette-secondary-500")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
id: `${dsId}_${record.getId()}`, // Prefix with dsId to ensure unique IDs across datasources
|
|
100
|
+
originalId: record.getId(),
|
|
101
|
+
dataSource: ds,
|
|
102
|
+
title: title ?? "",
|
|
103
|
+
start: start,
|
|
104
|
+
end: end ?? undefined,
|
|
105
|
+
color: color,
|
|
106
|
+
description: description ?? ""
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Update only this datasource's events, preserving others
|
|
111
|
+
setEventsByDsId((prev) => ({
|
|
112
|
+
...prev,
|
|
113
|
+
[dsId]: loadedEvents
|
|
114
|
+
}))
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error("Failed to load events from datasource", e)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const handleEventClick = (clickInfo) => {
|
|
121
|
+
const eventDs = clickInfo.event.extendedProps.dataSource as DataSource
|
|
122
|
+
const originalId = clickInfo.event.extendedProps.originalId as string
|
|
123
|
+
eventDs.selectRecordsByIds([originalId])
|
|
124
|
+
const record = eventDs.getRecordById(originalId)
|
|
125
|
+
const message = new DataRecordsSelectionChangeMessage(
|
|
126
|
+
props.widgetId,
|
|
127
|
+
[record],
|
|
128
|
+
[eventDs.id]
|
|
129
|
+
)
|
|
130
|
+
MessageManager.getInstance().publishMessage(message)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const handleClearSelection = () => {
|
|
134
|
+
/// Loop through all datasets, if there is a selection, clear it and post a message.
|
|
135
|
+
datasources.forEach((ds) => {
|
|
136
|
+
if (ds.getSelectedRecords().length > 0) {
|
|
137
|
+
ds.clearSelection()
|
|
138
|
+
const message = new DataRecordsSelectionChangeMessage(
|
|
139
|
+
props.widgetId,
|
|
140
|
+
[],
|
|
141
|
+
[props.useDataSources[0].dataSourceId]
|
|
142
|
+
)
|
|
143
|
+
MessageManager.getInstance().publishMessage(message)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!isConfigured) {
|
|
149
|
+
return (
|
|
150
|
+
<div className="widget-calendar-not-configured">
|
|
151
|
+
Please configure the Calendar widget in the settings panel.
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<>
|
|
158
|
+
<FullCalendar
|
|
159
|
+
plugins={[timeGridPlugin, dayGridPlugin, interactionPlugin]}
|
|
160
|
+
initialView="dayGridMonth"
|
|
161
|
+
events={events}
|
|
162
|
+
eventClick={handleEventClick}
|
|
163
|
+
eventDidMount={(info) => {
|
|
164
|
+
// Tooltip showing full date/time info
|
|
165
|
+
const description = info.event.extendedProps.description
|
|
166
|
+
const start = info.event.start
|
|
167
|
+
const end = info.event.end
|
|
168
|
+
let tooltipText = description + "\nStart: " + start.toLocaleString()
|
|
169
|
+
if (end) {
|
|
170
|
+
tooltipText += "\nEnd: " + end.toLocaleString()
|
|
171
|
+
}
|
|
172
|
+
info.el.setAttribute("title", tooltipText)
|
|
173
|
+
}}
|
|
174
|
+
customButtons={{
|
|
175
|
+
clearSelection: {
|
|
176
|
+
text: "Clear Selection",
|
|
177
|
+
click: () => {
|
|
178
|
+
handleClearSelection()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}}
|
|
182
|
+
buttonText={{
|
|
183
|
+
today: "Today",
|
|
184
|
+
month: "Month",
|
|
185
|
+
week: "Week",
|
|
186
|
+
day: "Day"
|
|
187
|
+
}}
|
|
188
|
+
headerToolbar={{
|
|
189
|
+
left: "prev,next today clearSelection",
|
|
190
|
+
center: "title",
|
|
191
|
+
right: "dayGridMonth,timeGridWeek,timeGridDay"
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
{config.dataSets && config.dataSets.length > 0 && (
|
|
195
|
+
<>
|
|
196
|
+
{config.dataSets.map((dsConfig, index) => (
|
|
197
|
+
<DataSourceComponent
|
|
198
|
+
key={index}
|
|
199
|
+
useDataSource={dsConfig.useDataSources[0]}
|
|
200
|
+
query={
|
|
201
|
+
{
|
|
202
|
+
where: "1=1",
|
|
203
|
+
outFields: ["*"],
|
|
204
|
+
returnGeometry: true
|
|
205
|
+
} as FeatureLayerQueryParams
|
|
206
|
+
}
|
|
207
|
+
widgetId={props.widgetId}
|
|
208
|
+
>
|
|
209
|
+
{(ds: DataSource) => {
|
|
210
|
+
if (ds && ds.getStatus() === DataSourceStatus.Loaded) {
|
|
211
|
+
setDatasources((prevDatasources) => {
|
|
212
|
+
// Avoids adding duplicates
|
|
213
|
+
if (!prevDatasources.includes(ds)) {
|
|
214
|
+
return [...prevDatasources, ds]
|
|
215
|
+
}
|
|
216
|
+
return prevDatasources
|
|
217
|
+
})
|
|
218
|
+
// Data source is loaded — populate calendar events
|
|
219
|
+
fillCalendarEvents(ds, dsConfig.asMutable({ deep: true }))
|
|
220
|
+
}
|
|
221
|
+
return null
|
|
222
|
+
}}
|
|
223
|
+
</DataSourceComponent>
|
|
224
|
+
))}
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
227
|
+
</>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import {
|
|
2
|
+
React,
|
|
3
|
+
Immutable,
|
|
4
|
+
type UseDataSource,
|
|
5
|
+
DataSourceTypes,
|
|
6
|
+
utils,
|
|
7
|
+
type ImmutableObject,
|
|
8
|
+
css
|
|
9
|
+
} from "jimu-core"
|
|
10
|
+
import type { AllWidgetSettingProps } from "jimu-for-builder"
|
|
11
|
+
import { SettingRow, SettingSection } from "jimu-ui/advanced/setting-components"
|
|
12
|
+
import { Button, TextInput, Tabs, Tab, CollapsablePanel } from "jimu-ui"
|
|
13
|
+
import type { IMConfig, colorset, data } from "../config"
|
|
14
|
+
import {
|
|
15
|
+
DataSourceSelector,
|
|
16
|
+
FieldSelector
|
|
17
|
+
} from "jimu-ui/advanced/data-source-selector"
|
|
18
|
+
|
|
19
|
+
export default function Setting(props: AllWidgetSettingProps<IMConfig>) {
|
|
20
|
+
const { id, config } = props
|
|
21
|
+
const [activeTab, setActiveTab] = React.useState<string | undefined>(
|
|
22
|
+
config?.dataSets?.[0]?.id
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// helper to get a mutable copy of datasets
|
|
26
|
+
const getDataSets = () =>
|
|
27
|
+
config?.dataSets ? config.dataSets.asMutable({ deep: true }) : []
|
|
28
|
+
|
|
29
|
+
const updateDataset = (datasetId: string, newData: Partial<any>) => {
|
|
30
|
+
const arr = getDataSets()
|
|
31
|
+
const idx = arr.findIndex((d: any) => d.id === datasetId)
|
|
32
|
+
if (idx === -1) return
|
|
33
|
+
arr[idx] = { ...arr[idx], ...newData }
|
|
34
|
+
const newConfig = (config || Immutable({})).set("dataSets", arr)
|
|
35
|
+
props.onSettingChange({ id, config: newConfig })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const addDataset = () => {
|
|
39
|
+
const newDataset = {
|
|
40
|
+
id: `ds_${utils.getUUID()}`,
|
|
41
|
+
labelField: undefined,
|
|
42
|
+
startDateField: undefined,
|
|
43
|
+
endDateField: undefined,
|
|
44
|
+
allDayField: undefined,
|
|
45
|
+
descriptionField: undefined,
|
|
46
|
+
colorsetField: undefined,
|
|
47
|
+
defaultEventColor: "#3788d8",
|
|
48
|
+
colorsets: [] as colorset[],
|
|
49
|
+
useDataSources: [] as UseDataSource[]
|
|
50
|
+
}
|
|
51
|
+
const arr = getDataSets()
|
|
52
|
+
arr.push(newDataset)
|
|
53
|
+
const newConfig = (config || Immutable({})).set("dataSets", arr)
|
|
54
|
+
props.onSettingChange({ id, config: newConfig })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const removeDataset = (datasetId: string) => {
|
|
58
|
+
const arr = getDataSets().filter((d: any) => d.id !== datasetId)
|
|
59
|
+
const newConfig = (config || Immutable({})).set("dataSets", arr)
|
|
60
|
+
props.onSettingChange({ id, config: newConfig })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const addValue = (datasetId: string) => {
|
|
64
|
+
const newValue: colorset = {
|
|
65
|
+
id: `view_${utils.getUUID()}`,
|
|
66
|
+
fieldValue: ``,
|
|
67
|
+
color: `#000000`
|
|
68
|
+
}
|
|
69
|
+
const arr = getDataSets()
|
|
70
|
+
const idx = arr.findIndex((d: any) => d.id === datasetId)
|
|
71
|
+
if (idx === -1) return
|
|
72
|
+
const ds = arr[idx]
|
|
73
|
+
ds.colorsets = ds.colorsets ? [...ds.colorsets, newValue] : [newValue]
|
|
74
|
+
updateDataset(datasetId, { colorsets: ds.colorsets })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const removeValue = (datasetId: string, viewId: string) => {
|
|
78
|
+
const arr = getDataSets()
|
|
79
|
+
const idx = arr.findIndex((d: any) => d.id === datasetId)
|
|
80
|
+
if (idx === -1) return
|
|
81
|
+
const ds = arr[idx]
|
|
82
|
+
const newcolorsets = (ds.colorsets || []).filter(
|
|
83
|
+
(v: any) => v.id !== viewId
|
|
84
|
+
)
|
|
85
|
+
updateDataset(datasetId, { colorsets: newcolorsets })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const updateValue = (
|
|
89
|
+
datasetId: string,
|
|
90
|
+
viewId: string,
|
|
91
|
+
newViewData: Partial<colorset>
|
|
92
|
+
) => {
|
|
93
|
+
const arr = getDataSets()
|
|
94
|
+
const idx = arr.findIndex((d: any) => d.id === datasetId)
|
|
95
|
+
if (idx === -1) return
|
|
96
|
+
const ds = arr[idx]
|
|
97
|
+
const newcolorsets = (ds.colorsets || []).map((v: any) =>
|
|
98
|
+
v.id === viewId ? { ...v, ...newViewData } : v
|
|
99
|
+
)
|
|
100
|
+
updateDataset(datasetId, { colorsets: newcolorsets })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="view-layers-toggle-setting">
|
|
105
|
+
<SettingSection>
|
|
106
|
+
<SettingRow
|
|
107
|
+
flow={"no-wrap"}
|
|
108
|
+
level={1}
|
|
109
|
+
tag={"div"}
|
|
110
|
+
label={props.intl.formatMessage({
|
|
111
|
+
id: "dataSources",
|
|
112
|
+
defaultMessage: "Add Data Sources"
|
|
113
|
+
})}
|
|
114
|
+
>
|
|
115
|
+
<Button type="secondary" onClick={addDataset}>
|
|
116
|
+
{props.intl.formatMessage({
|
|
117
|
+
id: "Add Dataset",
|
|
118
|
+
defaultMessage: "Add Dataset"
|
|
119
|
+
})}
|
|
120
|
+
</Button>
|
|
121
|
+
</SettingRow>
|
|
122
|
+
</SettingSection>
|
|
123
|
+
|
|
124
|
+
{config?.dataSets && config.dataSets.length > 0 && (
|
|
125
|
+
<SettingSection title={props.intl.formatMessage({ id: "Datasets" })}>
|
|
126
|
+
<Tabs
|
|
127
|
+
value={activeTab}
|
|
128
|
+
onChange={(id) => {
|
|
129
|
+
setActiveTab(id)
|
|
130
|
+
}}
|
|
131
|
+
type="tabs"
|
|
132
|
+
keepMount
|
|
133
|
+
scrollable={true}
|
|
134
|
+
onClose={(id) => {
|
|
135
|
+
removeDataset(id)
|
|
136
|
+
}}
|
|
137
|
+
children={
|
|
138
|
+
config.dataSets.map(
|
|
139
|
+
(ds: ImmutableObject<data>, dsIndex: number) => (
|
|
140
|
+
<Tab
|
|
141
|
+
id={ds.id}
|
|
142
|
+
key={ds.id}
|
|
143
|
+
title={`Dataset ${dsIndex + 1}`}
|
|
144
|
+
closeable
|
|
145
|
+
>
|
|
146
|
+
<SettingRow label={"Data Source"} level={1} flow={"wrap"}>
|
|
147
|
+
<DataSourceSelector
|
|
148
|
+
types={Immutable([DataSourceTypes.FeatureLayer])}
|
|
149
|
+
mustUseDataSource={true}
|
|
150
|
+
isMultiple={false}
|
|
151
|
+
useDataSources={ds.useDataSources}
|
|
152
|
+
useDataSourcesEnabled={props.useDataSourcesEnabled}
|
|
153
|
+
onChange={(uds: UseDataSource[]) => {
|
|
154
|
+
updateDataset(ds.id, { useDataSources: uds })
|
|
155
|
+
}}
|
|
156
|
+
widgetId={props.id}
|
|
157
|
+
/>
|
|
158
|
+
</SettingRow>
|
|
159
|
+
{ds.useDataSources?.length === 1 && (
|
|
160
|
+
<>
|
|
161
|
+
<SettingRow
|
|
162
|
+
label={"Select Label Field"}
|
|
163
|
+
level={2}
|
|
164
|
+
flow={"wrap"}
|
|
165
|
+
tag={"label"}
|
|
166
|
+
>
|
|
167
|
+
<FieldSelector
|
|
168
|
+
useDataSources={ds.useDataSources}
|
|
169
|
+
useDropdown={true}
|
|
170
|
+
isMultiple={false}
|
|
171
|
+
isDataSourceDropDownHidden={true}
|
|
172
|
+
onChange={(fields) => {
|
|
173
|
+
updateDataset(ds.id, {
|
|
174
|
+
labelField: fields?.[0]?.jimuName ?? null
|
|
175
|
+
})
|
|
176
|
+
}}
|
|
177
|
+
selectedFields={
|
|
178
|
+
ds?.labelField
|
|
179
|
+
? Immutable([ds.labelField])
|
|
180
|
+
: ds?.useDataSources?.[0]?.fields
|
|
181
|
+
}
|
|
182
|
+
/>
|
|
183
|
+
</SettingRow>
|
|
184
|
+
|
|
185
|
+
<SettingRow
|
|
186
|
+
label={"Start Date Field"}
|
|
187
|
+
level={1}
|
|
188
|
+
flow={"wrap"}
|
|
189
|
+
tag={"label"}
|
|
190
|
+
>
|
|
191
|
+
<FieldSelector
|
|
192
|
+
useDataSources={ds.useDataSources}
|
|
193
|
+
useDropdown={true}
|
|
194
|
+
isMultiple={false}
|
|
195
|
+
isDataSourceDropDownHidden={true}
|
|
196
|
+
onChange={(fields) => {
|
|
197
|
+
updateDataset(ds.id, {
|
|
198
|
+
startDateField: fields?.[0]?.jimuName ?? null
|
|
199
|
+
})
|
|
200
|
+
}}
|
|
201
|
+
selectedFields={
|
|
202
|
+
ds?.startDateField
|
|
203
|
+
? Immutable([ds.startDateField])
|
|
204
|
+
: ds?.useDataSources?.[0]?.fields
|
|
205
|
+
}
|
|
206
|
+
/>
|
|
207
|
+
</SettingRow>
|
|
208
|
+
|
|
209
|
+
<SettingRow
|
|
210
|
+
label={"End Date Field"}
|
|
211
|
+
level={2}
|
|
212
|
+
flow={"wrap"}
|
|
213
|
+
tag={"label"}
|
|
214
|
+
>
|
|
215
|
+
<FieldSelector
|
|
216
|
+
useDataSources={ds.useDataSources}
|
|
217
|
+
useDropdown={true}
|
|
218
|
+
isMultiple={false}
|
|
219
|
+
isDataSourceDropDownHidden={true}
|
|
220
|
+
onChange={(fields) => {
|
|
221
|
+
updateDataset(ds.id, {
|
|
222
|
+
endDateField: fields?.[0]?.jimuName ?? null
|
|
223
|
+
})
|
|
224
|
+
}}
|
|
225
|
+
selectedFields={
|
|
226
|
+
ds?.endDateField
|
|
227
|
+
? Immutable([ds.endDateField])
|
|
228
|
+
: ds?.useDataSources?.[0]?.fields
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
</SettingRow>
|
|
232
|
+
|
|
233
|
+
<SettingRow
|
|
234
|
+
label={"All Day Field"}
|
|
235
|
+
level={2}
|
|
236
|
+
flow={"wrap"}
|
|
237
|
+
tag={"label"}
|
|
238
|
+
>
|
|
239
|
+
<FieldSelector
|
|
240
|
+
useDataSources={ds.useDataSources}
|
|
241
|
+
useDropdown={true}
|
|
242
|
+
isMultiple={false}
|
|
243
|
+
isDataSourceDropDownHidden={true}
|
|
244
|
+
onChange={(fields) => {
|
|
245
|
+
updateDataset(ds.id, {
|
|
246
|
+
allDayField: fields?.[0]?.jimuName ?? null
|
|
247
|
+
})
|
|
248
|
+
}}
|
|
249
|
+
selectedFields={
|
|
250
|
+
ds?.allDayField
|
|
251
|
+
? Immutable([ds.allDayField])
|
|
252
|
+
: ds?.useDataSources?.[0]?.fields
|
|
253
|
+
}
|
|
254
|
+
/>
|
|
255
|
+
</SettingRow>
|
|
256
|
+
|
|
257
|
+
<SettingRow
|
|
258
|
+
label={"Description Field"}
|
|
259
|
+
level={2}
|
|
260
|
+
flow={"wrap"}
|
|
261
|
+
tag={"label"}
|
|
262
|
+
>
|
|
263
|
+
<FieldSelector
|
|
264
|
+
useDataSources={ds.useDataSources}
|
|
265
|
+
useDropdown={true}
|
|
266
|
+
isMultiple={false}
|
|
267
|
+
isDataSourceDropDownHidden={true}
|
|
268
|
+
onChange={(fields) => {
|
|
269
|
+
updateDataset(ds.id, {
|
|
270
|
+
descriptionField: fields?.[0]?.jimuName ?? null
|
|
271
|
+
})
|
|
272
|
+
}}
|
|
273
|
+
selectedFields={
|
|
274
|
+
ds?.descriptionField
|
|
275
|
+
? Immutable([ds.descriptionField])
|
|
276
|
+
: ds?.useDataSources?.[0]?.fields
|
|
277
|
+
}
|
|
278
|
+
/>
|
|
279
|
+
</SettingRow>
|
|
280
|
+
{ds.labelField &&
|
|
281
|
+
ds.startDateField &&
|
|
282
|
+
ds.endDateField && (
|
|
283
|
+
<>
|
|
284
|
+
<CollapsablePanel
|
|
285
|
+
label={"Color Settings"}
|
|
286
|
+
defaultIsOpen={true}
|
|
287
|
+
>
|
|
288
|
+
<SettingRow
|
|
289
|
+
label={"Default Event Color"}
|
|
290
|
+
level={1}
|
|
291
|
+
flow={"no-wrap"}
|
|
292
|
+
tag={"label"}
|
|
293
|
+
>
|
|
294
|
+
<input
|
|
295
|
+
type="color"
|
|
296
|
+
value={ds?.defaultEventColor || "#3788d8"}
|
|
297
|
+
onChange={(e) => {
|
|
298
|
+
updateDataset(ds.id, {
|
|
299
|
+
defaultEventColor: e.target.value
|
|
300
|
+
})
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
</SettingRow>
|
|
304
|
+
<SettingRow
|
|
305
|
+
label={"Color Field"}
|
|
306
|
+
level={2}
|
|
307
|
+
flow={"wrap"}
|
|
308
|
+
tag={"label"}
|
|
309
|
+
>
|
|
310
|
+
<FieldSelector
|
|
311
|
+
useDataSources={ds.useDataSources}
|
|
312
|
+
useDropdown={true}
|
|
313
|
+
isMultiple={false}
|
|
314
|
+
isDataSourceDropDownHidden={true}
|
|
315
|
+
onChange={(fields) => {
|
|
316
|
+
updateDataset(ds.id, {
|
|
317
|
+
colorsetField:
|
|
318
|
+
fields?.[0]?.jimuName ?? null
|
|
319
|
+
})
|
|
320
|
+
}}
|
|
321
|
+
selectedFields={
|
|
322
|
+
ds?.colorsetField
|
|
323
|
+
? Immutable([ds.colorsetField])
|
|
324
|
+
: ds?.useDataSources?.[0]?.fields
|
|
325
|
+
}
|
|
326
|
+
/>
|
|
327
|
+
</SettingRow>
|
|
328
|
+
{ds.colorsetField && (
|
|
329
|
+
<SettingRow
|
|
330
|
+
label={"Map field values to colors"}
|
|
331
|
+
level={2}
|
|
332
|
+
flow={"wrap"}
|
|
333
|
+
tag={"div"}
|
|
334
|
+
>
|
|
335
|
+
{(ds.colorsets || []).map(
|
|
336
|
+
(value: any, index: number) => (
|
|
337
|
+
<div
|
|
338
|
+
key={value.id}
|
|
339
|
+
className="value-config-container"
|
|
340
|
+
style={{ paddingBottom: "8px" }}
|
|
341
|
+
>
|
|
342
|
+
<SettingRow
|
|
343
|
+
label={`${props.intl.formatMessage({
|
|
344
|
+
id: "Color"
|
|
345
|
+
})} ${index + 1}`}
|
|
346
|
+
flow="no-wrap"
|
|
347
|
+
level={2}
|
|
348
|
+
>
|
|
349
|
+
<Button
|
|
350
|
+
size="sm"
|
|
351
|
+
type="tertiary"
|
|
352
|
+
onClick={() => {
|
|
353
|
+
removeValue(ds.id, value.id)
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
Remove
|
|
357
|
+
</Button>
|
|
358
|
+
</SettingRow>
|
|
359
|
+
<SettingRow
|
|
360
|
+
flow="wrap"
|
|
361
|
+
label={props.intl.formatMessage({
|
|
362
|
+
id: "Value to Color"
|
|
363
|
+
})}
|
|
364
|
+
level={3}
|
|
365
|
+
css={css`
|
|
366
|
+
margin-top: 4px !important;
|
|
367
|
+
`}
|
|
368
|
+
>
|
|
369
|
+
<TextInput
|
|
370
|
+
size="sm"
|
|
371
|
+
value={value.fieldValue}
|
|
372
|
+
onChange={(e) => {
|
|
373
|
+
updateValue(ds.id, value.id, {
|
|
374
|
+
fieldValue:
|
|
375
|
+
e.currentTarget.value
|
|
376
|
+
})
|
|
377
|
+
}}
|
|
378
|
+
/>
|
|
379
|
+
<input
|
|
380
|
+
type="color"
|
|
381
|
+
value={value.color || "#3788d8"}
|
|
382
|
+
onChange={(e) => {
|
|
383
|
+
updateValue(ds.id, value.id, {
|
|
384
|
+
color: e.currentTarget.value
|
|
385
|
+
})
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
</SettingRow>
|
|
389
|
+
</div>
|
|
390
|
+
)
|
|
391
|
+
)}
|
|
392
|
+
<Button
|
|
393
|
+
type="primary"
|
|
394
|
+
className="w-100 mt-2"
|
|
395
|
+
onClick={() => {
|
|
396
|
+
addValue(ds.id)
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
{props.intl.formatMessage({
|
|
400
|
+
id: "Add Color"
|
|
401
|
+
})}
|
|
402
|
+
</Button>
|
|
403
|
+
</SettingRow>
|
|
404
|
+
)}
|
|
405
|
+
</CollapsablePanel>
|
|
406
|
+
</>
|
|
407
|
+
)}
|
|
408
|
+
</>
|
|
409
|
+
)}
|
|
410
|
+
</Tab>
|
|
411
|
+
)
|
|
412
|
+
) as any
|
|
413
|
+
}
|
|
414
|
+
/>
|
|
415
|
+
</SettingSection>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { React, type ImmutableArray } from "jimu-core"
|
|
2
|
+
import _Widget from "../src/runtime/widget"
|
|
3
|
+
import { widgetRender, wrapWidget } from "jimu-for-test"
|
|
4
|
+
import { screen } from "@testing-library/react"
|
|
5
|
+
import "@testing-library/jest-dom"
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
// Mock the DataSource so the widget receives a data source during tests
|
|
9
|
+
jest.mock("jimu-core", () => {
|
|
10
|
+
return {
|
|
11
|
+
...jest.requireActual("jimu-core"),
|
|
12
|
+
DataSourceComponent: ({ onDataSourceCreated }: any) => {
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const fakeDs = () => {
|
|
15
|
+
mockFeatureLayer(featureLayer)
|
|
16
|
+
}
|
|
17
|
+
onDataSourceCreated && onDataSourceCreated(fakeDs)
|
|
18
|
+
}, [onDataSourceCreated])
|
|
19
|
+
return React.createElement("div", null, "mock-datasource")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const render = widgetRender()
|
|
26
|
+
describe("test Calendar Widget", () => {
|
|
27
|
+
it("handle unconfigured settings panel", () => {
|
|
28
|
+
const Widget = wrapWidget(_Widget, {
|
|
29
|
+
config: {}
|
|
30
|
+
})
|
|
31
|
+
const { queryByText, rerender } = render(<Widget widgetId="Widget_1" />)
|
|
32
|
+
expect(
|
|
33
|
+
queryByText("Please configure the Calendar widget in the settings panel.")
|
|
34
|
+
).toBeTruthy()
|
|
35
|
+
|
|
36
|
+
rerender(
|
|
37
|
+
<Widget
|
|
38
|
+
widgetId="Widget_1"
|
|
39
|
+
config={{}}
|
|
40
|
+
useDataSources={
|
|
41
|
+
[
|
|
42
|
+
{
|
|
43
|
+
dataSourceId: "mock-datasource",
|
|
44
|
+
mainDataSourceId: "mock-datasource"
|
|
45
|
+
}
|
|
46
|
+
] as unknown as ImmutableArray<any>
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
expect(
|
|
51
|
+
queryByText("Please configure the Calendar widget in the settings panel.")
|
|
52
|
+
).toBeTruthy()
|
|
53
|
+
expect(queryByText("Month", { selector: "button" })).toBeFalsy()
|
|
54
|
+
|
|
55
|
+
rerender(
|
|
56
|
+
<Widget
|
|
57
|
+
widgetId="Widget_1"
|
|
58
|
+
config={[
|
|
59
|
+
{
|
|
60
|
+
labelField: "name",
|
|
61
|
+
startDateField: "start_date",
|
|
62
|
+
endDateField: "end_date",
|
|
63
|
+
allDayField: "all_day"
|
|
64
|
+
}
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
expect(
|
|
69
|
+
queryByText("Please configure the Calendar widget in the settings panel.")
|
|
70
|
+
).toBeTruthy()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("basic render with configured settings (not verifying events load)", () => {
|
|
74
|
+
const Widget = wrapWidget(_Widget, {
|
|
75
|
+
config: [
|
|
76
|
+
{
|
|
77
|
+
labelField: "label",
|
|
78
|
+
startDateField: "start_date",
|
|
79
|
+
endDateField: "end_date",
|
|
80
|
+
allDayField: "all_day",
|
|
81
|
+
useDataSources: [
|
|
82
|
+
{
|
|
83
|
+
dataSourceId: "mock-datasource",
|
|
84
|
+
mainDataSourceId: "mock-datasource"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
render(<Widget widgetId="Widget_1" />)
|
|
91
|
+
//expect(screen.queryByText("mock-datasource")).toBeTruthy()
|
|
92
|
+
expect(screen.queryAllByText("Month", { selector: "button" })).toBeTruthy()
|
|
93
|
+
/*
|
|
94
|
+
const event = await screen.findByText("Event 1", {
|
|
95
|
+
selector: ".fc-event-title"
|
|
96
|
+
})
|
|
97
|
+
expect(event).toBeTruthy()
|
|
98
|
+
*/
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { MockFeatureLayerData } from "jimu-for-test"
|
|
2
|
+
|
|
3
|
+
const FEATURE_LAYER_URL = 'https://services3.arcgis.com/SMmkLJuWWI7vDDUq/arcgis/rest/services/Events/FeatureServer/0'
|
|
4
|
+
|
|
5
|
+
const NOW = new Date(Date.now()).toISOString()
|
|
6
|
+
const LATER = new Date(Date.now() + 60*60*1000).toISOString()
|
|
7
|
+
|
|
8
|
+
const SERVER_INFO = {
|
|
9
|
+
currentVersion: 10.71,
|
|
10
|
+
fullVersion: '10.7.1',
|
|
11
|
+
soapUrl: 'https://sunshinegis.maps.arcgis.com/arcgis/services',
|
|
12
|
+
secureSoapUrl: 'https://sunshinegis.maps.arcgis.com/arcgis/services',
|
|
13
|
+
authInfo: {
|
|
14
|
+
isTokenBasedSecurity: true,
|
|
15
|
+
tokenServicesUrl: 'https://sunshinegis.maps.arcgis.com/arcgis/tokens/',
|
|
16
|
+
shortLivedTokenValidity: 60
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mocked point feature layer.
|
|
22
|
+
*/
|
|
23
|
+
export const featureLayer: MockFeatureLayerData = {
|
|
24
|
+
url: FEATURE_LAYER_URL,
|
|
25
|
+
serverInfo: SERVER_INFO,
|
|
26
|
+
layerDefinition: {
|
|
27
|
+
currentVersion: 10.71,
|
|
28
|
+
id: 0,
|
|
29
|
+
name: 'Incidents',
|
|
30
|
+
type: 'Feature Layer',
|
|
31
|
+
geometryType: 'esriGeometryPoint',
|
|
32
|
+
objectIdField: 'objectid',
|
|
33
|
+
globalIdField: '',
|
|
34
|
+
displayField: 'req_type',
|
|
35
|
+
typeIdField: 'req_type',
|
|
36
|
+
subtypeField: '',
|
|
37
|
+
fields: [
|
|
38
|
+
{
|
|
39
|
+
name: 'objectid',
|
|
40
|
+
type: 'esriFieldTypeOID',
|
|
41
|
+
alias: 'Object ID',
|
|
42
|
+
domain: null,
|
|
43
|
+
editable: false,
|
|
44
|
+
nullable: false,
|
|
45
|
+
defaultValue: null,
|
|
46
|
+
modelName: 'OBJECTID'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'label',
|
|
50
|
+
type: 'esriFieldTypeString',
|
|
51
|
+
alias: 'Label',
|
|
52
|
+
domain: null,
|
|
53
|
+
editable: false,
|
|
54
|
+
nullable: false,
|
|
55
|
+
defaultValue: null,
|
|
56
|
+
modelName: 'LABEL'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'start_date',
|
|
60
|
+
type: 'esriFieldTypeDate',
|
|
61
|
+
alias: 'Start Date',
|
|
62
|
+
domain: null,
|
|
63
|
+
editable: false,
|
|
64
|
+
nullable: false,
|
|
65
|
+
defaultValue: null,
|
|
66
|
+
modelName: 'START_DATE'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'end_date',
|
|
70
|
+
type: 'esriFieldTypeDate',
|
|
71
|
+
alias: 'End Date',
|
|
72
|
+
domain: null,
|
|
73
|
+
editable: false,
|
|
74
|
+
nullable: false,
|
|
75
|
+
defaultValue: null,
|
|
76
|
+
modelName: 'END_DATE'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'all_day',
|
|
80
|
+
type: 'esriFieldTypeString',
|
|
81
|
+
alias: 'All Day',
|
|
82
|
+
domain: null,
|
|
83
|
+
editable: false,
|
|
84
|
+
nullable: true,
|
|
85
|
+
defaultValue: null,
|
|
86
|
+
modelName: 'ALL_DAY'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'description',
|
|
90
|
+
type: 'esriFieldTypeString',
|
|
91
|
+
alias: 'Description',
|
|
92
|
+
domain: null,
|
|
93
|
+
editable: false,
|
|
94
|
+
nullable: true,
|
|
95
|
+
defaultValue: null,
|
|
96
|
+
modelName: 'DESCRIPTION'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'color',
|
|
100
|
+
type: 'esriFieldTypeString',
|
|
101
|
+
alias: 'Color',
|
|
102
|
+
domain: null,
|
|
103
|
+
editable: false,
|
|
104
|
+
nullable: true,
|
|
105
|
+
defaultValue: null,
|
|
106
|
+
modelName: 'COLOR'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'geometryField',
|
|
110
|
+
type: 'esriFieldTypeGeometry'
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
advancedQueryCapabilities: {
|
|
114
|
+
useStandardizedQueries: true,
|
|
115
|
+
supportsStatistics: true,
|
|
116
|
+
supportsHavingClause: true,
|
|
117
|
+
supportsOrderBy: true,
|
|
118
|
+
supportsDistinct: true,
|
|
119
|
+
supportsCountDistinct: true,
|
|
120
|
+
supportsPagination: true,
|
|
121
|
+
supportsPaginationOnAggregatedQueries: true,
|
|
122
|
+
supportsTrueCurve: true,
|
|
123
|
+
supportsReturningQueryExtent: true,
|
|
124
|
+
supportsQueryWithDistance: true,
|
|
125
|
+
supportsSqlExpression: true
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
queries: [{
|
|
129
|
+
url: `${FEATURE_LAYER_URL}/query?f=json&where=1=1&outFields=*`,
|
|
130
|
+
result: {
|
|
131
|
+
fields: [{
|
|
132
|
+
name: 'objectid',
|
|
133
|
+
type: 'esriFieldTypeOID',
|
|
134
|
+
alias: 'Object ID'
|
|
135
|
+
},{
|
|
136
|
+
name: 'label',
|
|
137
|
+
type: 'esriFieldTypeString',
|
|
138
|
+
alias: 'Label'
|
|
139
|
+
},{
|
|
140
|
+
name: 'start_date',
|
|
141
|
+
type: 'esriFieldTypeDate',
|
|
142
|
+
alias: 'Start Date'
|
|
143
|
+
},{
|
|
144
|
+
name: 'end_date',
|
|
145
|
+
type: 'esriFieldTypeDate',
|
|
146
|
+
alias: 'End Date'
|
|
147
|
+
},{
|
|
148
|
+
name: 'all_day',
|
|
149
|
+
type: 'esriFieldTypeString',
|
|
150
|
+
alias: 'All Day'
|
|
151
|
+
},{
|
|
152
|
+
name: 'description',
|
|
153
|
+
type: 'esriFieldTypeString',
|
|
154
|
+
alias: 'Description'
|
|
155
|
+
},{
|
|
156
|
+
name: 'color',
|
|
157
|
+
type: 'esriFieldTypeString',
|
|
158
|
+
alias: 'Color'
|
|
159
|
+
}],
|
|
160
|
+
features: [{
|
|
161
|
+
attributes: {
|
|
162
|
+
objectid: 1,
|
|
163
|
+
label: 'Event 1',
|
|
164
|
+
start_date: NOW,
|
|
165
|
+
all_day: 'y',
|
|
166
|
+
description: 'Description for Event 1',
|
|
167
|
+
color: 'High'
|
|
168
|
+
}},{
|
|
169
|
+
attributes: {
|
|
170
|
+
objectid: 2,
|
|
171
|
+
label: 'Event 2',
|
|
172
|
+
start_date: NOW,
|
|
173
|
+
end_date: LATER,
|
|
174
|
+
all_day: 'n',
|
|
175
|
+
description: 'Description for Event 2',
|
|
176
|
+
color: 'Medium'
|
|
177
|
+
}},{attributes: {
|
|
178
|
+
objectid: 3,
|
|
179
|
+
label: 'Event 3',
|
|
180
|
+
start_date: 1622764800000,
|
|
181
|
+
all_day: 'y',
|
|
182
|
+
description: 'Description for Event 3',
|
|
183
|
+
color: 'Low'
|
|
184
|
+
}}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
}]
|
|
188
|
+
} as MockFeatureLayerData
|