From fec2a4263c1424fa7653dc257bc97b80decec448 Mon Sep 17 00:00:00 2001 From: Sumit Zarkar Date: Mon, 5 Feb 2024 19:04:20 +0530 Subject: [PATCH 1/4] Updated Create-Feature Component ## Implemented - #513 - updated create feature workflow --- src/components.d.ts | 13 ++ .../create-feature/create-feature.css | 27 +++ .../create-feature/create-feature.tsx | 159 +++++++++++++----- .../crowdsource-reporter.tsx | 53 ++++-- 4 files changed, 192 insertions(+), 60 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index bb05854a4..406388009 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -92,6 +92,10 @@ export namespace Components { * @returns Promise that resolves when the operation is complete */ "close": () => Promise; + /** + * boolean: Set this to true when have a custom submit button in the app. This will hide the header and footer elements of the editor and user needs to execute the submit method manually. + */ + "customizeSubmit"?: boolean; /** * esri/views/MapView: https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html */ @@ -1443,6 +1447,7 @@ declare global { "success": void; "fail": Error; "drawComplete": void; + "editingAttachment": boolean; } interface HTMLCreateFeatureElement extends Components.CreateFeature, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLCreateFeatureElement, ev: CreateFeatureCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2140,6 +2145,10 @@ declare namespace LocalJSX { "zoomAndScrollToSelected"?: boolean; } interface CreateFeature { + /** + * boolean: Set this to true when have a custom submit button in the app. This will hide the header and footer elements of the editor and user needs to execute the submit method manually. + */ + "customizeSubmit"?: boolean; /** * esri/views/MapView: https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html */ @@ -2148,6 +2157,10 @@ declare namespace LocalJSX { * Emitted on demand when drawing is completed */ "onDrawComplete"?: (event: CreateFeatureCustomEvent) => void; + /** + * Emitted on demand when editing attachments + */ + "onEditingAttachment"?: (event: CreateFeatureCustomEvent) => void; /** * Emitted on demand when the feature creation is failed */ diff --git a/src/components/create-feature/create-feature.css b/src/components/create-feature/create-feature.css index f0d54c514..72cde2d6a 100644 --- a/src/components/create-feature/create-feature.css +++ b/src/components/create-feature/create-feature.css @@ -18,6 +18,33 @@ display: block; } +/* Override to hide Editor settings panel collapsible */ .esri-editor__panel-toolbar { display: none !important; } + +/* Override to hide the Select section from Editor template */ +.esri-editor__update-actions { + display: none !important; +} + +/* Override to reduce the padding of panel content*/ +.esri-editor__panel-content { + padding-block: 0px !important; +} + +/* Override to hide header */ +.esri-editor .esri-item-list__group__header { + display: none !important; +} + +/* Override to hide the create feature heading title */ +.esri-editor__panel-content__section .esri-widget__heading { + display: none !important; +} + +/* Override to reduce padding for the template filter*/ +.esri-editor .esri-item-list__filter-container--sticky { + padding-block: 0px !important; + padding-inline: 10px !important; +} \ No newline at end of file diff --git a/src/components/create-feature/create-feature.tsx b/src/components/create-feature/create-feature.tsx index 2920c2b86..bf6c2e60f 100644 --- a/src/components/create-feature/create-feature.tsx +++ b/src/components/create-feature/create-feature.tsx @@ -16,7 +16,7 @@ import { Component, Element, EventEmitter, Prop, Host, h, Event, Watch, Method } from "@stencil/core"; import { loadModules } from "../../utils/loadModules"; -import { getLayerOrTable } from "../../utils/mapViewUtils"; +import { getAllLayers } from "../../utils/mapViewUtils"; @Component({ tag: "create-feature", @@ -48,6 +48,12 @@ export class CreateFeature { */ @Prop() selectedLayerId: string; + /** + * boolean: Set this to true when have a custom submit button in the app. + * This will hide the header and footer elements of the editor and user needs to execute the submit method manually. + */ + @Prop() customizeSubmit?: boolean = false; + //-------------------------------------------------------------------------- // // State (internal) @@ -73,9 +79,14 @@ export class CreateFeature { protected _editor: __esri.Editor; /** - * HTMLDivElement: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement - */ - protected _editContainer: HTMLDivElement; + * esri/core/reactiveUtils: https://developers.arcgis.com/javascript/latest/api-reference/esri-core-reactiveUtils.html + */ + protected reactiveUtils: typeof import("esri/core/reactiveUtils"); + + /** + * boolean: Flag to maintain the add attachment + */ + protected _addingAttachment: boolean //-------------------------------------------------------------------------- // @@ -141,6 +152,11 @@ export class CreateFeature { */ @Event() drawComplete: EventEmitter; + /** + * Emitted on demand when editing attachments + */ + @Event() editingAttachment: EventEmitter; + //-------------------------------------------------------------------------- // // Functions (lifecycle) @@ -183,17 +199,8 @@ export class CreateFeature { * Init Editor widget and starts the create workflow */ protected async init(): Promise { - if (this.mapView) { - if (this.mapView && this.selectedLayerId) { - await getLayerOrTable(this.mapView, this.selectedLayerId); - await this.createEditorWidget(); - await this.startCreate(); - this._editor.viewModel.featureFormViewModel.on('submit', this.submitted.bind(this)); - setTimeout(() => { - this.el.querySelector('.esri-editor').querySelectorAll('calcite-flow-item')[1].shadowRoot.querySelector('calcite-panel').shadowRoot.querySelector('article').querySelector('header').setAttribute('style', 'display: none'); - this.el.querySelector('.esri-editor').querySelectorAll('calcite-flow-item')[1].shadowRoot.querySelector('calcite-panel').shadowRoot.querySelector('article')?.querySelector('footer')?.setAttribute('style', 'display: none'); - }, 700) - } + if (this.mapView && this.selectedLayerId) { + await this.createEditorWidget(); } } @@ -203,10 +210,12 @@ export class CreateFeature { * @protected */ protected async initModules(): Promise { - const [Editor] = await loadModules([ - "esri/widgets/Editor" + const [Editor, reactiveUtils] = await loadModules([ + "esri/widgets/Editor", + "esri/core/reactiveUtils" ]); this.Editor = Editor; + this.reactiveUtils = reactiveUtils; } /** @@ -217,26 +226,56 @@ export class CreateFeature { if (this._editor) { this._editor.destroy(); } + const layerInfos = [] const container = document.createElement("div"); - const layer = await getLayerOrTable(this.mapView, this.selectedLayerId); - const selectedLayer = { - layer: layer - }; + const allMapLayers = await getAllLayers(this.mapView); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + allMapLayers.forEach(async (eachLayer: __esri.FeatureLayer) => { + layerInfos.push({ + layer: eachLayer, + enabled: eachLayer?.type === "feature" && eachLayer?.id === this.selectedLayerId, + addEnabled: true, // default is true, set to false to disable the ability to add a new feature + updateEnabled: false, // default is true, set to false to disable the ability to edit an existing feature + deleteEnabled: false // default is true, set to false to disable the ability to delete features + }) + }); + this._editor = new this.Editor({ allowedWorkflows: "create-features", view: this.mapView, - layerInfos: [selectedLayer], + layerInfos: layerInfos, visibleElements: { - snappingControls: false, - snappingControlsElements: { - featureEnabledToggle: false, // removes "Feature to feature" toggle - layerList: false, // removes Snapping layers list - enabledToggle: false - } + snappingControls: false }, - container, + container }); this.el.appendChild(container); + + //Add handle to watch if attachments are added/edited + const attachmentHandle = this.reactiveUtils.watch( + () => this._editor.viewModel.state, + (state) => { + if (state === 'adding-attachment' || state === 'editing-attachment') { + this._addingAttachment = true; + this.editingAttachment.emit(true); + } else { + if (this._addingAttachment) { + this.editingAttachment.emit(false); + this._addingAttachment = false; + } + } + }); + this._editor.viewModel.addHandles(attachmentHandle); + + //Add handle to watch featureTemplatesViewModel ready state and then start the creation + const handle = this.reactiveUtils.watch( + () => this._editor.viewModel.featureTemplatesViewModel.state, + (state) => { + if(state === 'ready'){ + void this.startCreate(); + } + }); + this._editor.viewModel.addHandles(handle); } /** @@ -244,23 +283,52 @@ export class CreateFeature { * @protected */ protected async startCreate(): Promise { - const layer = await getLayerOrTable(this.mapView, this.selectedLayerId); - if (layer) { - let template = layer.templates && layer.templates.length ? layer.templates[0] : {} as __esri.FeatureTemplate; - if (layer.sourceJSON?.types.length && layer.sourceJSON.types[0].templates?.length) { - template = layer.sourceJSON.types[0].templates[0]; + if (this._editor.viewModel.featureTemplatesViewModel.items?.length) { + const items: __esri.TemplateItem[] = this._editor.viewModel.featureTemplatesViewModel.items[0].get("items"); + //once the feature template is selected handle the event for formSubmit and sketch complete + //also, hide the headers and footer in the editor as we will be showing our own submit and cancel button + this._editor.viewModel.featureTemplatesViewModel.on('select', () => { + setTimeout(() => { + //on form submit + this._editor.viewModel.featureFormViewModel.on('submit', this.submitted.bind(this)); + //on sketch complete emit the event + this._editor.viewModel.sketchViewModel.on("create", (evt) => { + if (evt.state === "complete") { + this.drawComplete.emit(); + } + }) + this.hideEditorsElements(); + }, 700) + this.hideEditorsElements(); + }); + //if only one feature template then directly start geometry creation for that + //else allow feature template selection to user + if (items.length === 1) { + this._editor.viewModel.featureTemplatesViewModel.select(items[0]); } - const creationInfo: __esri.CreationInfo = { - layer: layer, - template: template - }; - await this._editor.startCreateFeaturesWorkflowAtFeatureCreation(creationInfo); - this._editor.viewModel.sketchViewModel.on("create", (evt) => { - if (evt.state === "complete") { - this.drawComplete.emit(); - } - }) + //hides the header and footer elements in editor widget + this.hideEditorsElements() + } + } + + /** + * Hides the elements of editor widget + * @protected + */ + protected hideEditorsElements(): void { + if(!this.customizeSubmit){ + return } + setTimeout(() => { + //hides the header and footer on the featureForm + this.el.querySelector('.esri-editor').querySelectorAll('calcite-flow-item')?.forEach((flowItem) => { + const article = flowItem.shadowRoot?.querySelector('calcite-panel')?.shadowRoot?.querySelector('article'); + //hide the header + article?.querySelector('header')?.setAttribute('style', 'display: none'); + //hide the footer + article?.querySelector('footer')?.setAttribute('style', 'display: none'); + }) + }, 700) } /** @@ -269,9 +337,12 @@ export class CreateFeature { * @protected */ protected async submitted(evt: any): Promise { + //return if any attribute is invalid , focus will be shifted to the invalid attribute in feature form if (evt.invalid.length) { return; } + //Submit only when valid attributes + //emit success or fail based on the result if (evt.valid.length) { try { await this._editor.activeWorkflow.commit(); diff --git a/src/components/crowdsource-reporter/crowdsource-reporter.tsx b/src/components/crowdsource-reporter/crowdsource-reporter.tsx index 43557be82..8e6ae27a0 100644 --- a/src/components/crowdsource-reporter/crowdsource-reporter.tsx +++ b/src/components/crowdsource-reporter/crowdsource-reporter.tsx @@ -579,10 +579,12 @@ export class CrowdsourceReporter {
{this._translations.featureEditFormInfoMsg}
this._createFeature = el as HTMLCreateFeatureElement} selectedLayerId={this._selectedLayerId} /> @@ -594,10 +596,18 @@ export class CrowdsourceReporter { * When drawing of incident location completed on map show the submit and cancel button * @protected */ - protected showSubmitCancelButton(): void { + protected onDrawComplete(): void { this._showSubmitCancelButton = true; } + /** + * When Add attachment panel is enabled hide the submit and cancel button + * @protected + */ + protected showSubmitCancelButton(evt: CustomEvent): void { + this._showSubmitCancelButton = !evt.detail; + } + /** * On back from create feature, call submit editor to destroy the Editor widget instance * @protected @@ -633,11 +643,23 @@ export class CrowdsourceReporter { * On submit report navigate to the layer list home page and refresh the layer list * @protected */ - protected navigateHomePage(): void { + protected onReportSubmitted(): void { this._reportSubmitted = true; + this.navigateToHomePage() + } + + /** + * Navigates to layer-list + * @protected + */ + protected navigateToHomePage(): void { + if (this._createFeature) { + this._createFeature.close(); + } if (this._layerList) { this._layerList.refresh(); } + this.setSelectedFeatures([]); this._flowItems = ["layer-list"]; } @@ -706,12 +728,11 @@ export class CrowdsourceReporter { protected backFromSelectedPanel(): void { const updatedFlowItems = [...this._flowItems]; updatedFlowItems.pop(); - //clear the selected layer and feature when back to layer list + //Back to layer list, and return as the flowItems will be reset in navigateToHomePage if (updatedFlowItems.length === 1) { - this.setSelectedLayer('', ''); - this.setSelectedFeatures([]); + this.navigateToHomePage(); + return; } - this._flowItems = [...updatedFlowItems]; } @@ -824,7 +845,7 @@ export class CrowdsourceReporter { */ protected setSelectedFeatures(features: __esri.Graphic[]): void { this._selectedFeature = features; - this.setCurrentFeature(this._selectedFeature.length ? this._selectedFeature[0] : null) + this.setCurrentFeature(this._selectedFeature.length ? this._selectedFeature[0] : null); } /** @@ -835,12 +856,12 @@ export class CrowdsourceReporter { if (selectedFeature && selectedFeature.layer) { const layer = selectedFeature.layer as __esri.FeatureLayer; this.setSelectedLayer(layer.id, layer.title); - this._currentFeatureId = selectedFeature.attributes[layer.objectIdField] + this._currentFeatureId = selectedFeature.attributes[layer.objectIdField]; } else { this.setSelectedLayer('', ''); - this._currentFeatureId = '' + this._currentFeatureId = ''; } - this._updateShareURL() + this._updateShareURL(); } /** @@ -922,7 +943,7 @@ export class CrowdsourceReporter { } }); //update the selectedFeature - this.setSelectedFeatures(clickedGraphics) + this.setSelectedFeatures(clickedGraphics); //if featureDetails not open then add it to the list else just reInit flowItems which will update details with newly selected features // eslint-disable-next-line unicorn/prefer-ternary if (this._flowItems.length && this._flowItems[this._flowItems.length - 1] !== "feature-details") { @@ -945,10 +966,9 @@ export class CrowdsourceReporter { /** * Updates the share url for current selected feature - * @returns * @protected */ - protected _updateShareURL(): string { + protected _updateShareURL(): void { const url = this._shareNode?.shareUrl; if (!url) { return; @@ -972,10 +992,11 @@ export class CrowdsourceReporter { /** * Navigates to selected features detail based on the URL params + * @protected */ protected async loadFeatureFromURLParams(): Promise { if (this.layerId && this.objectId) { - const layer = await getLayerOrTable(this.mapView, this.layerId) + const layer = await getLayerOrTable(this.mapView, this.layerId); if (layer) { // only query if we have some ids...query with no ids will result in all features being returned const featureSet = await queryFeaturesByID([Number(this.objectId)], layer, [], false, this.mapView.spatialReference); From ed96e0ff8dff487980ddb6595dcf84321af1fc98 Mon Sep 17 00:00:00 2001 From: Sumit Zarkar Date: Mon, 5 Feb 2024 19:05:22 +0530 Subject: [PATCH 2/4] Fix issue related to loading urlParams - Fixed - When opening any shared link and clicking on back from details user is again navigated to details automatically --- .../crowdsource-reporter/crowdsource-reporter.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/crowdsource-reporter/crowdsource-reporter.tsx b/src/components/crowdsource-reporter/crowdsource-reporter.tsx index 8e6ae27a0..002d9fcbc 100644 --- a/src/components/crowdsource-reporter/crowdsource-reporter.tsx +++ b/src/components/crowdsource-reporter/crowdsource-reporter.tsx @@ -275,6 +275,11 @@ export class CrowdsourceReporter { */ protected _currentFeatureId: string; + /** + * boolean: Maintains a flag to know if urls params are loaded or not + */ + protected _urlParamsLoaded: boolean; + //-------------------------------------------------------------------------- // // Watch handlers @@ -328,6 +333,7 @@ export class CrowdsourceReporter { * @returns Promise when complete */ async componentWillLoad(): Promise { + this._urlParamsLoaded = false; await this._initModules(); await this._getTranslations(); } @@ -709,7 +715,10 @@ export class CrowdsourceReporter { //update the has valid layer state this._hasValidLayers = layersListed.length > 0; //navigate to the feature details if URL params found - await this.loadFeatureFromURLParams(); + if (!this._urlParamsLoaded) { + this._urlParamsLoaded = true; + await this.loadFeatureFromURLParams(); + } } /**On click of layer list item show feature list From 92d86f70af9df3cc2dcfbf134349eaf37bdfedf7 Mon Sep 17 00:00:00 2001 From: Sumit Zarkar Date: Mon, 5 Feb 2024 19:06:26 +0530 Subject: [PATCH 3/4] Implemented #514 - #514 - Filter out other editable layers from webmap on click of a particular reporting layer --- src/components/crowdsource-reporter/crowdsource-reporter.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/crowdsource-reporter/crowdsource-reporter.tsx b/src/components/crowdsource-reporter/crowdsource-reporter.tsx index 002d9fcbc..179bfce37 100644 --- a/src/components/crowdsource-reporter/crowdsource-reporter.tsx +++ b/src/components/crowdsource-reporter/crowdsource-reporter.tsx @@ -406,6 +406,11 @@ export class CrowdsourceReporter { protected setSelectedLayer(layerId: string, layerName: string): void { this._selectedLayerId = layerId; this._selectedLayerName = layerName; + //show only current layer on map and hide other valid editable layers + //if layerId is empty then show all the layers on map + this._validLayers.forEach(layer => { + layer.set('visible', !layerId || (layer.id === layerId)) + }) } /** From 9ef9f16ad8f6b98c6ee23c2a3f2d937c231e4a52 Mon Sep 17 00:00:00 2001 From: Sumit Zarkar Date: Mon, 5 Feb 2024 19:07:45 +0530 Subject: [PATCH 4/4] Added sample for create-feature component ## Added sample for create-feature component --- src/demos/create-feature.html | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/demos/create-feature.html diff --git a/src/demos/create-feature.html b/src/demos/create-feature.html new file mode 100644 index 000000000..f8b7050a5 --- /dev/null +++ b/src/demos/create-feature.html @@ -0,0 +1,90 @@ + + + + + Create Feature + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file