Select to view content in your preferred language

Custom widget that moves selected features from one source to another

115
1
Jump to solution
Tuesday
MatthewGalvanFire
New Contributor

Hello All, 

Problem: Widget works fine in builder, but "fails to load" in preview.

This is my first time creating a custom widget. My goal was to create a widget where a user can select points on a map and send them to another layer that is connected to the map. The widget will also have a field where they can give those selected points a name. All attributes will be copied from the selected points and inserted into the other layer. Settings.tsx

/** @jsx jsx */
import { React, jsx, DataSourceTypes, Immutable } from 'jimu-core'
import type { AllWidgetSettingProps } from 'jimu-for-builder'
import type { UseDataSource } from 'jimu-core'
import { DataSourceSelector } from 'jimu-ui/advanced/data-source-selector'
import { MapWidgetSelector } from 'jimu-ui/advanced/setting-components'
import { useState } from 'react'

export default function Setting(props: AllWidgetSettingProps<any>) {
  const [statusMessage, setStatusMessage] = useState('')

  // Current target layer (DataSource)
  const targetUseDs: UseDataSource[] = props.config.targetDataSource
    ? [props.config.targetDataSource]
    : []

  // Toggle target layer enabled
  const onToggleTargetEnabled = (enabled: boolean) => {
    props.onSettingChange({
      id: props.id,
      config: props.config.set('useTargetDsEnabled', enabled)
    })
  }

  // Update target DataSource
  const onTargetDsChange = (useDataSources: UseDataSource[]) => {
    if (!useDataSources || useDataSources.length === 0) {
      setStatusMessage('Please select a target layer')
      props.onSettingChange({
        id: props.id,
        config: props.config.set('targetDataSource', null)
      })
      return
    }

    setStatusMessage('')
    props.onSettingChange({
      id: props.id,
      config: props.config.set('targetDataSource', useDataSources[0])
    })
  }

  return (
    <div className="widget-setting">
      <h5>Source Map Widget</h5>
      <MapWidgetSelector
        useMapWidgetIds={Immutable(props.useMapWidgetIds ?? [])}
        onSelect={(ids) => {
          props.onSettingChange({ id: props.id, useMapWidgetIds: ids })
        }}
      />

      <h5 className="mt-3">Target Layer</h5>
      <DataSourceSelector
        widgetId={props.id}
        types={Immutable([DataSourceTypes.FeatureLayer])}
        useDataSources={Immutable(targetUseDs)}
        useDataSourcesEnabled={props.config.useTargetDsEnabled ?? true}
        onChange={onTargetDsChange}
        onToggleUseDataEnabled={onToggleTargetEnabled}
      />

      {statusMessage && (
        <div className="status-message" style={{ marginTop: 8, color: 'red' }}>
          {statusMessage}
        </div>
      )}
    </div>
  )
}

 

 widget.tsx

/** @jsx jsx */
import { React, jsx, DataSourceManager,  } from 'jimu-core'
import type { FeatureLayerDataSource, DataRecord } from 'jimu-core'
import { useState } from 'react'
import { Button, TextInput, Alert } from 'jimu-ui'
import Graphic from '@arcgis/core/Graphic'

export default function MoveToStrikeTeamWidget(props) {
  const [strikeTeamName, setStrikeTeamName] = useState('')
  const [loading, setLoading] = useState(false)
  const [status, setStatus] = useState<{ type: 'success' | 'error' | 'warning'; message: string } | null>(null)

  const showStatus = (type: 'success' | 'error' | 'warning', message: string) => {
    setStatus({ type, message })
    setTimeout(() => { setStatus(null) }, 5000)
  }

  // Render fallback if configuration is missing
  if (!props.useMapWidgetIds?.length || !props.config?.targetDataSource) {
    return <div style={{ color: '#666' }}>Configure the source map widget and target layer in settings.</div>
  }

  const moveSelectedFeatures = async () => {
    if (!strikeTeamName.trim()) {
      showStatus('warning', 'Please enter a Strike Team Name.')
      return
    }

    setLoading(true)

    try {

      // Get all data sources as array
      const allDsArray = Object.values(DataSourceManager.getInstance().getDataSources())

      // Filter only FeatureLayerDataSources
      const sourceDsList = allDsArray.filter(
        ds => ds.type === 'FeatureLayer' && typeof ds.getSelectedRecords === 'function'
      ) as FeatureLayerDataSource[]

      if (!sourceDsList.length) {
        showStatus('error', 'No source FeatureLayer found in the selected map widget.')
        setLoading(false)
        return
      }

      // Collect selected records
      const selectedRecords: DataRecord[] = sourceDsList.flatMap(ds => ds.getSelectedRecords())
      if (!selectedRecords.length) {
        showStatus('warning', 'No features selected in the source map.')
        setLoading(false)
        return
      }

      // Get target DataSource
      const targetUseDs = props.config.targetDataSource
      const targetDs = DataSourceManager.getInstance().getDataSource(targetUseDs.dataSourceId) as FeatureLayerDataSource

      if (!targetDs || !targetDs.layer) {
        showStatus('error', 'Target data source not ready.')
        setLoading(false)
        return
      }

      const targetLayer = targetDs.layer
      await targetLayer.when() // ensure layer is fully loaded

      // Prepare graphics for target
      const addFeatures: Graphic[] = selectedRecords.map(r => {
        const json = r.toJson()
        return new Graphic({
          geometry: json.geometry,
          attributes: {
            ...json.attributes,
            StrikeTeamName: strikeTeamName
          }
        })
      })

      // Apply edits to target layer
      const addResults = await targetLayer.applyEdits({ addFeatures })
      if (!addResults.addFeatureResults || addResults.addFeatureResults.length === 0) {
        showStatus('error', 'Failed to add features to target layer.')
        setLoading(false)
        return
      }

      // Delete features from source layers
      for (const ds of sourceDsList) {
        const recordsToDelete = ds.getSelectedRecords()
        const sourceLayer = ds.layer
        if (recordsToDelete.length && sourceLayer) {
          await sourceLayer.when()
          const deleteFeatures = recordsToDelete.map(r => ({ objectId: Number(r.getId()) }))
          await sourceLayer.applyEdits({ deleteFeatures })
        }
      }

      showStatus('success', `Successfully moved ${addResults.addFeatureResults.length} feature(s) to StrikeTeamGroup.`)
      setStrikeTeamName('')

    } catch (err) {
      console.error('Runtime error in MoveToStrikeTeamWidget:', err)
      showStatus('error', 'Widget failed to load or move features. Check console.')
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="p-2">
      <h4>Move to Strike Team</h4>
      <TextInput
        placeholder="Enter Strike Team Name"
        value={strikeTeamName}
        onChange={e => { setStrikeTeamName(e.target.value) }}
        className="mb-2"
      />
      <Button
        type="primary"
        onClick={moveSelectedFeatures}
        disabled={loading}
        block
      >
        {loading ? 'Moving...' : 'Move Selected Features'}
      </Button>

      {status && (
        <Alert type={status.type} text={status.message} className="mt-2" />
      )}
    </div>
  )
}

 

 Thanks in advance.

-Matthew

0 Kudos
1 Solution

Accepted Solutions
MatthewGalvanFire
New Contributor

I found the solution in another post HERE.

Basically added "dependency""jimu-arcgis" to manifest.json

View solution in original post

0 Kudos
1 Reply
MatthewGalvanFire
New Contributor

I found the solution in another post HERE.

Basically added "dependency""jimu-arcgis" to manifest.json

0 Kudos