From 11f77e2879b41246b5a85e07a41dee5c4c573c01 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Wed, 3 Dec 2025 09:20:44 -0800 Subject: [PATCH 1/5] Updated FeatureDataset and removed similar interfaces. Created reusable property selector and refactor code to use it --- .../common/mapFSM/MapStateMachineContext.tsx | 92 ++--- .../mapFSM/machineDefinition/mapMachine.ts | 73 ++-- .../common/mapFSM/machineDefinition/types.ts | 28 +- .../src/components/common/mapFSM/models.ts | 7 - .../mapFSM/useLocationFeatureLoader.tsx | 87 ++--- .../src/components/maps/MapLeafletView.tsx | 15 +- .../src/components/maps/ZoomToLocation.tsx | 31 +- .../Search/PropertyQuickInfoContainer.tsx | 71 +--- .../Control/Search/SearchContainer.tsx | 55 ++- .../leaflet/Control/Search/SearchView.tsx | 6 +- .../LayerPopup/MultiplePropertyPopupView.tsx | 19 +- .../leaflet/Layers/FilePropertiesLayer.tsx | 53 ++- .../leaflet/Layers/WorklistMarkersLayer.tsx | 15 +- .../leaflet/Layers/WorklistParcelsLayer.tsx | 13 +- .../propertySelector/MapClickMonitor.tsx | 95 +---- .../MapSelectorContainer.test.tsx | 8 +- .../propertySelector/MapSelectorContainer.tsx | 77 ++-- .../map/PropertyMapSelectorFormView.tsx | 29 +- .../map/PropertyMapSelectorSubForm.test.tsx | 2 +- .../map/PropertyMapSelectorSubForm.tsx | 9 +- .../search/PropertySearchSelectorFormView.tsx | 66 ++-- .../PropertySelectorSearchContainer.tsx | 35 +- .../search/mapPropertyColumns.tsx | 38 +- .../SelectedPropertyRow.test.tsx | 84 ++-- .../SelectedPropertyRow.tsx | 38 +- .../leases/add/AddLeaseContainer.test.tsx | 4 +- .../features/leases/add/AddLeaseContainer.tsx | 138 +++---- .../src/features/leases/add/AddLeaseForm.tsx | 9 +- source/frontend/src/features/leases/models.ts | 6 +- .../LeasePropertySelector.test.tsx | 61 +-- .../propertyPicker/LeasePropertySelector.tsx | 170 +-------- .../LeaseUpdatePropertySelector.tsx | 170 +++++---- .../SelectedPropertyRow.test.tsx | 6 +- .../SelectedPropertyRow.tsx | 12 +- .../acquisition/AcquisitionView.tsx | 5 +- .../add/AcquisitionPropertiesSubForm.test.tsx | 116 ------ .../add/AcquisitionPropertiesSubForm.tsx | 161 -------- .../add/AddAcquisitionContainer.test.tsx | 4 +- .../add/AddAcquisitionContainer.tsx | 12 +- .../acquisition/add/AddAcquisitionForm.tsx | 55 ++- .../hooks/useGenerateResearchNotice.ts | 18 +- .../AddConsolidationContainer.tsx | 10 +- .../AddConsolidationMarkerSynchronizer.tsx | 8 +- .../consolidation/AddConsolidationModel.ts | 20 +- .../AddConsolidationView.test.tsx | 2 +- .../consolidation/AddConsolidationView.tsx | 15 +- .../mapSideBar/context/sidebarContext.tsx | 6 +- .../disposition/DispositionView.tsx | 4 +- .../add/AddDispositionContainer.test.tsx | 6 +- .../add/AddDispositionContainer.tsx | 11 +- .../disposition/form/DispositionForm.tsx | 9 +- .../form/DispositionPropertiesSubForm.tsx | 163 -------- .../mapSideBar/layer/LayerTabContainer.tsx | 40 +- .../mapSideBar/lease/LeaseContainer.tsx | 14 +- .../mapSideBar/management/ManagementView.tsx | 6 +- .../add/AddManagementContainer.test.tsx | 6 +- .../management/add/AddManagementContainer.tsx | 9 +- .../management/form/ManagementForm.tsx | 9 +- .../form/ManagementPropertiesSubForm.test.tsx | 114 ------ .../form/ManagementPropertiesSubForm.tsx | 160 -------- .../mapSideBar/property/ComposedProperty.ts | 31 +- .../property/MotiInventoryContainer.test.tsx | 16 +- .../property/MotiInventoryHeader.tsx | 16 +- .../mapSideBar/property/PropertyContainer.tsx | 43 +-- .../mapSideBar/research/ResearchView.tsx | 4 +- .../add/AddResearchContainer.test.tsx | 4 +- .../research/add/AddResearchContainer.tsx | 9 +- .../research/add/AddResearchForm.tsx | 16 +- .../research/add/ResearchProperties.test.tsx | 140 ------- .../research/add/ResearchProperties.tsx | 130 ------- .../shared/detail/PropertyFileContainer.tsx | 40 +- .../src/features/mapSideBar/shared/models.ts | 89 +++-- .../operations/SelectedOperationProperty.tsx | 15 +- .../PropertiesListContainer.test.tsx} | 78 ++-- .../properties/PropertiesListContainer.tsx | 168 ++++++++ .../properties/UpdateProperties.test.tsx | 42 +- .../update/properties/UpdateProperties.tsx | 358 ------------------ .../properties/UpdatePropertiesContainer.tsx | 207 ++++++++++ .../subdivision/AddSubdivisionContainer.tsx | 10 +- .../AddSubdivisionMarkerSynchronizer.tsx | 5 +- .../subdivision/AddSubdivisionModel.ts | 18 +- .../subdivision/AddSubdivisionView.test.tsx | 16 +- .../subdivision/AddSubdivisionView.tsx | 23 +- .../properties/parcelList/ParcelItem.tsx | 61 ++- .../parcelList/ParcelListContainer.tsx | 5 +- .../properties/parcelList/ParcelListView.tsx | 7 +- .../features/properties/parcelList/models.ts | 95 ----- .../worklist/WorklistContainer.test.tsx | 4 +- .../properties/worklist/WorklistContainer.tsx | 21 +- .../worklist/WorklistMapClickMonitor.test.tsx | 50 +-- .../worklist/WorklistMapClickMonitor.tsx | 55 ++- .../properties/worklist/WorklistView.tsx | 6 +- .../worklist/context/WorklistContext.test.tsx | 19 +- .../worklist/context/WorklistContext.tsx | 92 +---- .../src/hooks/layer-api/useGeoServer.ts | 14 +- .../repositories/useComposedProperties.ts | 35 +- .../src/hooks/useDraftMarkerSynchronizer.ts | 19 +- .../src/hooks/useEditPropertiesNotifier.ts | 50 --- .../src/hooks/useEnrichWithPimsFeatures.ts | 24 +- .../hooks/useInitialMapSelectorProperties.ts | 6 +- ...seLocationFeatureDatasetsWithAddresses.ts} | 18 +- ....ts => usePropertyFormSyncronizer.test.ts} | 50 +-- .../src/hooks/usePropertyFormSyncronizer.ts | 85 +++++ source/frontend/src/mocks/featureset.mock.ts | 28 +- source/frontend/src/mocks/mapFSM.mock.ts | 11 +- .../frontend/src/mocks/worklistParcel.mock.ts | 16 +- .../src/models/generate/GenerateForm12.ts | 12 +- .../src/models/generate/GenerateNotice.ts | 12 +- .../frontend/src/utils/TestCommonWrapper.tsx | 4 +- .../src/utils/mapPropertyUtils.test.tsx | 104 ++--- source/frontend/src/utils/mapPropertyUtils.ts | 254 ++++++++----- source/frontend/src/utils/propertyUtils.ts | 48 +-- source/frontend/src/utils/test-utils.tsx | 4 +- source/frontend/src/utils/utils.ts | 4 + 114 files changed, 1913 insertions(+), 3383 deletions(-) delete mode 100644 source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx delete mode 100644 source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.tsx delete mode 100644 source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx delete mode 100644 source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx delete mode 100644 source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.tsx delete mode 100644 source/frontend/src/features/mapSideBar/research/add/ResearchProperties.test.tsx delete mode 100644 source/frontend/src/features/mapSideBar/research/add/ResearchProperties.tsx rename source/frontend/src/features/mapSideBar/{disposition/form/DispositionPropertiesSubForm.test.tsx => shared/update/properties/PropertiesListContainer.test.tsx} (54%) create mode 100644 source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx delete mode 100644 source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx create mode 100644 source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx delete mode 100644 source/frontend/src/features/properties/parcelList/models.ts delete mode 100644 source/frontend/src/hooks/useEditPropertiesNotifier.ts rename source/frontend/src/hooks/{useFeatureDatasetsWithAddresses.ts => useLocationFeatureDatasetsWithAddresses.ts} (71%) rename source/frontend/src/hooks/{useEditPropertiesNotifier.test.ts => usePropertyFormSyncronizer.test.ts} (69%) create mode 100644 source/frontend/src/hooks/usePropertyFormSyncronizer.ts diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index a83707d05f..8f3e9f2362 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -1,5 +1,6 @@ import { useInterpret, useSelector } from '@xstate/react'; import { dequal } from 'dequal'; +import { Feature, Geometry } from 'geojson'; import { LatLngBounds, LatLngLiteral } from 'leaflet'; import React, { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; @@ -12,23 +13,15 @@ import { defaultPropertyFilter, IPropertyFilter, } from '@/features/properties/filter/IPropertyFilter'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; +import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; import { exists, firstOrNull, isValidString } from '@/utils'; import { pidParser, pinParser } from '@/utils/propertyUtils'; import { mapMachine } from './machineDefinition/mapMachine'; import { MachineContext, SideBarType } from './machineDefinition/types'; -import { - LocationBoundaryDataset, - MapFeatureData, - MarkerSelected, - RequestedCenterTo, - RequestedFlyTo, -} from './models'; -import useLocationFeatureLoader, { - LocationFeatureDataset, - SelectedFeatureDataset, - WorklistLocationFeatureDataset, -} from './useLocationFeatureLoader'; +import { MapFeatureData, MarkerSelected, RequestedCenterTo, RequestedFlyTo } from './models'; +import useLocationFeatureLoader, { LocationFeatureDataset } from './useLocationFeatureLoader'; import { useMapSearch } from './useMapSearch'; export interface IMapStateMachineContext { @@ -42,22 +35,21 @@ export interface IMapStateMachineContext { mapMarkedLocation: LatLngLiteral | null; mapLocationSelected: LatLngLiteral | null; mapLocationFeatureDataset: LocationFeatureDataset | null; - selectedFeatures: SelectedFeatureDataset[]; - repositioningFeatureDataset: SelectedFeatureDataset | null; - repositioningPropertyIndex: number | null; + locationFeaturesForAddition: LocationFeatureDataset[] | null; + repositioningFeature: Feature | null; + pendingLocationFeaturesAddition: boolean; // worklist-related state worklistSelectedMapLocation: LatLngLiteral | null; - worklistLocationFeatureDataset: WorklistLocationFeatureDataset | null; + worklistLocationFeatureDataset: LocationFeatureDataset | null; showPopup: boolean; isLoading: boolean; mapSearchCriteria: IPropertyFilter | null; mapFeatureData: MapFeatureData; - filePropertyLocations: LocationBoundaryDataset[]; + filePropertyLocations: ApiGen_Concepts_FileProperty[]; pendingFitBounds: boolean; requestedFitBounds: LatLngBounds; isSelecting: boolean; isRepositioning: boolean; - selectingComponentId: string | null; isFiltering: boolean; isShowingMapFilter: boolean; isShowingMapLayers: boolean; @@ -94,19 +86,15 @@ export interface IMapStateMachineContext { // worklist worklistMapClick: (latlng: LatLngLiteral) => void; - worklistAdd: (dataset: WorklistLocationFeatureDataset) => void; + worklistAdd: (dataset: LocationFeatureDataset) => void; setMapSearchCriteria: (searchCriteria: IPropertyFilter) => void; refreshMapProperties: () => void; - prepareForCreation: (selectedFeatures: SelectedFeatureDataset[]) => void; - processCreation: () => void; - startSelection: (selectingComponentId?: string) => void; + requestLocationFeatureAddition: (selectedFeatures: LocationFeatureDataset[]) => void; + processLocationFeaturesAddition: () => void; + startSelection: () => void; finishSelection: () => void; - startReposition: ( - repositioningFeatureDataset: SelectedFeatureDataset, - index: number, - selectingComponentId?: string, - ) => void; + startReposition: (featureDataSet: Feature) => void; finishReposition: () => void; toggleMapFilterDisplay: () => void; toggleMapLayerControl: () => void; @@ -116,7 +104,7 @@ export interface IMapStateMachineContext { openQuickInfo: () => void; closeQuickInfo: () => void; minimizeQuickInfo: () => void; - setFilePropertyLocations: (locations: LocationBoundaryDataset[]) => void; + setFilePropertyLocations: (locations: ApiGen_Concepts_FileProperty[]) => void; setMapLayers: (layers: Set) => void; setMapLayersToRefresh: (layers: Set) => void; setDefaultMapLayers: (layers: Set) => void; @@ -217,7 +205,7 @@ export const MapStateMachineProvider: React.FC> loadWorklistLocationData: async ( context: MachineContext, event: AnyEventObject & { type: 'WORKLIST_MAP_CLICK'; latlng: LatLngLiteral }, - ): Promise => { + ): Promise => { const response = locationLoader.loadWorklistLocationDetails({ latLng: event.latlng }); return response; }, @@ -335,7 +323,7 @@ export const MapStateMachineProvider: React.FC> ); const worklistAdd = useCallback( - (dataset: WorklistLocationFeatureDataset) => { + (dataset: LocationFeatureDataset) => { serviceSend({ type: 'WORKLIST_ADD', dataset, @@ -414,41 +402,32 @@ export const MapStateMachineProvider: React.FC> [serviceSend], ); - const prepareForCreation = useCallback( - (selectedFeatures: SelectedFeatureDataset[]) => { - serviceSend({ type: 'PREPARE_FOR_CREATION', selectedFeatures }); + const requestLocationFeatureAddition = useCallback( + (selectedFeatures: LocationFeatureDataset[]) => { + serviceSend({ type: 'REQUEST_LOCATION_ADDITION', selectedFeatures }); }, [serviceSend], ); - const processCreation = useCallback(() => { + const processLocationFeaturesAddition = useCallback(() => { serviceSend({ - type: 'PROCESS_CREATION', + type: 'PROCESS_LOCATION_ADDITION', }); }, [serviceSend]); - const startSelection = useCallback( - (selectingComponentId?: string) => { - serviceSend({ type: 'START_SELECTION', selectingComponentId }); - }, - [serviceSend], - ); + const startSelection = useCallback(() => { + serviceSend({ type: 'START_SELECTION' }); + }, [serviceSend]); const finishSelection = useCallback(() => { serviceSend({ type: 'FINISH_SELECTION' }); }, [serviceSend]); const startReposition = useCallback( - ( - repositioningFeatureDataset: SelectedFeatureDataset, - index: number, - selectingComponentId?: string, - ) => { + (feature: Feature) => { serviceSend({ type: 'START_REPOSITION', - repositioningFeatureDataset, - repositioningPropertyIndex: index, - selectingComponentId, + feature, }); }, [serviceSend], @@ -459,7 +438,7 @@ export const MapStateMachineProvider: React.FC> }, [serviceSend]); const setFilePropertyLocations = useCallback( - (locations: LocationBoundaryDataset[]) => { + (locations: ApiGen_Concepts_FileProperty[]) => { serviceSend({ type: 'SET_FILE_PROPERTY_LOCATIONS', locations }); }, [serviceSend], @@ -625,10 +604,8 @@ export const MapStateMachineProvider: React.FC> mapMarkerSelected: state.context.mapFeatureSelected, mapLocationSelected: state.context.mapLocationSelected, mapMarkedLocation: state.context.mapMarkedLocation, - selectedFeatures: state.context.selectedFeatures, + locationFeaturesForAddition: state.context.locationFeaturesForAddition, mapLocationFeatureDataset: state.context.mapLocationFeatureDataset, - repositioningFeatureDataset: state.context.repositioningFeatureDataset, - repositioningPropertyIndex: state.context.repositioningPropertyIndex, worklistSelectedMapLocation: state.context.worklistSelectedMapLocation, worklistLocationFeatureDataset: state.context.worklistLocationFeatureDataset, showPopup: showPopup, @@ -641,7 +618,6 @@ export const MapStateMachineProvider: React.FC> requestedFitBounds: state.context.requestedFitBounds, isSelecting: state.matches({ mapVisible: { featureView: 'selecting' } }), isRepositioning: isRepositioning, - selectingComponentId: state.context.selectingComponentId, isFiltering: !dequal(state.context.advancedSearchCriteria, new PropertyFilterFormModel()), isShowingMapFilter: isShowingMapFilter, isShowingMapLayers: isShowingMapLayers, @@ -657,6 +633,10 @@ export const MapStateMachineProvider: React.FC> isMapVisible: state.matches({ mapVisible: {} }), currentMapBounds: state.context.currentMapBounds, isEditPropertiesMode: state.context.isEditPropertiesMode, + pendingLocationFeaturesAddition: state.matches({ + mapVisible: { locationFeatureAddition: 'pendingLocationFeatureAddition' }, + }), + repositioningFeature: state.context.repositioningFeature, setMapSearchCriteria, refreshMapProperties, @@ -676,8 +656,8 @@ export const MapStateMachineProvider: React.FC> worklistMapClick, worklistAdd, closePopup, - prepareForCreation, - processCreation, + requestLocationFeatureAddition, + processLocationFeaturesAddition, startSelection, finishSelection, startReposition, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index d5c5b0c218..ddeda63f1f 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -7,9 +7,9 @@ import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/Advan import { pimsBoundaryLayers } from '@/components/maps/leaflet/Control/LayersControl/LayerDefinitions'; import { initialEnabledLayers } from '@/components/maps/leaflet/Control/LayersControl/LayersMenuLayout'; import { defaultPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; -import { emptyFeatureData, LocationBoundaryDataset } from '../models'; -import { SelectedFeatureDataset } from '../useLocationFeatureLoader'; +import { emptyFeatureData } from '../models'; import { MachineContext, SideBarType } from './types'; const featureViewStates = { @@ -19,17 +19,12 @@ const featureViewStates = { on: { START_SELECTION: { target: 'selecting', - actions: [ - assign({ selectingComponentId: (_, event: any) => event.selectingComponentId }), - ], }, START_REPOSITION: { target: 'repositioning', actions: [ assign({ - selectingComponentId: (_, event: any) => event.selectingComponentId, - repositioningFeatureDataset: (_, event: any) => event.repositioningFeatureDataset, - repositioningPropertyIndex: (_, event: any) => event.repositioningPropertyIndex, + repositioningFeature: (_, event: any) => event.feature, }), ], }, @@ -39,14 +34,13 @@ const featureViewStates = { on: { FINISH_SELECTION: { target: 'browsing', - actions: [assign({ selectingComponentId: () => null })], }, SET_FILE_PROPERTY_LOCATIONS: { actions: [ assign({ filePropertyLocations: ( _, - event: AnyEventObject & { locations: LocationBoundaryDataset[] }, + event: AnyEventObject & { locations: ApiGen_Concepts_FileProperty[] }, ) => event.locations ?? [], }), ], @@ -59,9 +53,7 @@ const featureViewStates = { target: 'browsing', actions: [ assign({ - repositioningFeatureDataset: () => null, - repositioningPropertyIndex: () => null, - selectingComponentId: () => null, + repositioningFeature: () => null, }), ], }, @@ -70,7 +62,7 @@ const featureViewStates = { assign({ filePropertyLocations: ( _, - event: AnyEventObject & { locations: LocationBoundaryDataset[] }, + event: AnyEventObject & { locations: ApiGen_Concepts_FileProperty[] }, ) => event.locations ?? [], }), ], @@ -239,6 +231,32 @@ const mapRequestStates = { }, }; +const locationFeatureAdditionStates = { + initial: 'nothingPending', + states: { + nothingPending: { + on: { + REQUEST_LOCATION_ADDITION: { + actions: assign({ + locationFeaturesForAddition: (_, event: any) => event.selectedFeatures, + }), + target: 'pendingLocationFeatureAddition', + }, + }, + }, + pendingLocationFeatureAddition: { + on: { + PROCESS_LOCATION_ADDITION: { + actions: assign({ + locationFeaturesForAddition: () => null, + }), + target: 'nothingPending', + }, + }, + }, + }, +}; + const selectedFeatureLoaderStates = { initial: 'idle', states: { @@ -249,7 +267,7 @@ const selectedFeatureLoaderStates = { assign({ isLoading: () => false, showPopup: () => false, - mapLocationFeatureDataset: (context: any, event: any) => { + mapLocationFeatureDataset: (_, event: any) => { return { ...event.locationDataset, }; @@ -312,10 +330,9 @@ const selectedFeatureLoaderStates = { assign({ isLoading: () => false, showPopup: () => false, - mapLocationFeatureDataset: (context: any, event: any) => { + mapLocationFeatureDataset: (_, event: any) => { return { ...event.data, - selectingComponentId: context.selectingComponentId, }; }, }), @@ -396,7 +413,7 @@ const sideBarStates = { assign({ filePropertyLocations: ( _, - event: AnyEventObject & { locations: LocationBoundaryDataset[] }, + event: AnyEventObject & { locations: ApiGen_Concepts_FileProperty[] }, ) => event.locations ?? [], }), ], @@ -661,10 +678,7 @@ export const mapMachine = createMachine({ mapFeatureSelected: null, mapLocationFeatureDataset: null, mapMarkedLocation: null, - selectedFeatures: [], - repositioningFeatureDataset: null, - repositioningPropertyIndex: null, - selectingComponentId: null, + locationFeaturesForAddition: null, worklistSelectedMapLocation: null, worklistLocationFeatureDataset: null, isLoading: false, @@ -680,6 +694,7 @@ export const mapMachine = createMachine({ mapLayersToRefresh: new Set(), currentMapBounds: null, isEditPropertiesMode: false, + repositioningFeature: null, }, // State definitions @@ -722,19 +737,6 @@ export const mapMachine = createMachine({ EXIT_MAP: { target: 'notMap', }, - PREPARE_FOR_CREATION: { - actions: assign({ - selectedFeatures: ( - _, - event: AnyEventObject & { selectedFeatures: SelectedFeatureDataset[] }, - ) => event.selectedFeatures, - }), - }, - PROCESS_CREATION: { - actions: assign({ - selectedFeatures: () => [], - }), - }, SET_EDIT_PROPERTIES_MODE: { actions: assign({ isEditPropertiesMode: (_, event: AnyEventObject & { isEditPropertiesMode: boolean }) => @@ -755,6 +757,7 @@ export const mapMachine = createMachine({ featureView: featureViewStates, featureDataLoader: featureDataLoaderStates, mapRequest: mapRequestStates, + locationFeatureAddition: locationFeatureAdditionStates, selectedFeatureLoader: selectedFeatureLoaderStates, sideBar: sideBarStates, rightSideBar: rightSideBarStates, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index bcb45fd187..e4a1371e5f 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -1,21 +1,14 @@ +import { Feature, Geometry } from 'geojson'; import { LatLngBounds, LatLngLiteral } from 'leaflet'; import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { IMapSideBarViewState as IMapSideBarState } from '@/features/mapSideBar/MapSideBar'; import { IPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; +import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; -import { - LocationBoundaryDataset, - MapFeatureData, - MarkerSelected, - RequestedCenterTo, - RequestedFlyTo, -} from '../models'; -import { - LocationFeatureDataset, - SelectedFeatureDataset, - WorklistLocationFeatureDataset, -} from '../useLocationFeatureLoader'; +import { MapFeatureData, MarkerSelected, RequestedCenterTo, RequestedFlyTo } from '../models'; +import { LocationFeatureDataset } from '../useLocationFeatureLoader'; export enum SideBarType { NOT_DEFINED = 'NOT_DEFINED', @@ -37,16 +30,13 @@ export type MachineContext = { mapLocationSelected: LatLngLiteral | null; mapLocationFeatureDataset: LocationFeatureDataset | null; mapMarkedLocation: LatLngLiteral | null; - selectedFeatures: SelectedFeatureDataset[]; - repositioningFeatureDataset: SelectedFeatureDataset | null; - repositioningPropertyIndex: number | null; - selectingComponentId: string | null; - + locationFeaturesForAddition: LocationFeatureDataset[] | null; + repositioningFeature: Feature | null; mapFeatureData: MapFeatureData; // worklist-related state worklistSelectedMapLocation: LatLngLiteral | null; - worklistLocationFeatureDataset: WorklistLocationFeatureDataset | null; + worklistLocationFeatureDataset: LocationFeatureDataset | null; // TODO: this is partially in the URL. Either move it completly there or remove it searchCriteria: IPropertyFilter | null; @@ -56,7 +46,7 @@ export type MachineContext = { requestedFitBounds: LatLngBounds; requestedFlyTo: RequestedFlyTo; requestedCenterTo: RequestedCenterTo; - filePropertyLocations: LocationBoundaryDataset[]; + filePropertyLocations: ApiGen_Concepts_FileProperty[]; activePimsPropertyIds: number[]; activeLayers: Set; mapLayersToRefresh: Set; diff --git a/source/frontend/src/components/common/mapFSM/models.ts b/source/frontend/src/components/common/mapFSM/models.ts index 3bf97ca133..cec41452a4 100644 --- a/source/frontend/src/components/common/mapFSM/models.ts +++ b/source/frontend/src/components/common/mapFSM/models.ts @@ -45,13 +45,6 @@ export interface RequestedCenterTo { readonly location: LatLngLiteral | null; } -export interface LocationBoundaryDataset { - readonly location: LatLngLiteral; - readonly boundary: Geometry | null; - readonly fileBoundary: Geometry | null; - readonly isActive?: boolean; -} - export const emptyPimsLocationFeatureCollection: FeatureCollection< Geometry, PIMS_Property_Location_View diff --git a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx index 7eeb69046e..3d93bc57a0 100644 --- a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx +++ b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx @@ -1,4 +1,4 @@ -import { Feature, FeatureCollection, Geometry, MultiPolygon, Polygon } from 'geojson'; +import { Feature, FeatureCollection, Geometry } from 'geojson'; import { LatLngLiteral } from 'leaflet'; import { useCallback } from 'react'; @@ -7,6 +7,8 @@ import { useFullyAttributedParcelMapLayer } from '@/hooks/repositories/mapLayer/ import { useLegalAdminBoundariesMapLayer } from '@/hooks/repositories/mapLayer/useLegalAdminBoundariesMapLayer'; import { usePimsPropertyLayer } from '@/hooks/repositories/mapLayer/usePimsPropertyLayer'; import { useMapProperties } from '@/hooks/repositories/useMapProperties'; +import { ADM_IndianReserveBands_Feature_Properties } from '@/models/layers/admIndianReserveBands'; +import { WHSE_AgriculturalLandReservePoly_Feature_Properties } from '@/models/layers/alcAgriculturalReserve'; import { TANTALIS_CrownLandInclusions_Feature_Properties, TANTALIS_CrownLandInventory_Feature_Properties, @@ -14,23 +16,22 @@ import { TANTALIS_CrownLandLicenses_Feature_Properties, TANTALIS_CrownLandTenures_Feature_Properties, } from '@/models/layers/crownLand'; +import { EBC_ELECTORAL_DISTS_BS10_SVW_Feature_Properties } from '@/models/layers/electoralBoundaries'; import { MOT_DistrictBoundary_Feature_Properties } from '@/models/layers/motDistrictBoundary'; import { MOT_RegionalBoundary_Feature_Properties } from '@/models/layers/motRegionalBoundary'; import { WHSE_Municipalities_Feature_Properties } from '@/models/layers/municipalities'; import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; import { ISS_ProvincialPublicHighway } from '@/models/layers/pimsHighwayLayer'; -import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; +import { + PIMS_Property_Boundary_View, + PIMS_Property_Location_View, +} from '@/models/layers/pimsPropertyLocationView'; import { exists, isValidId } from '@/utils'; export interface FeatureDataset { - selectingComponentId: string | null; - location: LatLngLiteral; - fileLocation: LatLngLiteral | null; -} - -export interface LocationFeatureDataset extends FeatureDataset { parcelFeatures: Feature[] | null; pimsFeatures: Feature[] | null; + pimsBoundaryFeatures: Feature[] | null; regionFeature: Feature | null; districtFeature: Feature | null; municipalityFeatures: Feature[] | null; @@ -42,36 +43,41 @@ export interface LocationFeatureDataset extends FeatureDataset { crownLandTenuresFeatures: | Feature[] | null; + crownLandInventoryFeatures: | Feature[] | null; crownLandInclusionsFeatures: | Feature[] | null; + + electoralFeatures: Feature[] | null; + alrFeatures: Feature[] | null; + firstNationFeatures: Feature[] | null; } -export interface SelectedFeatureDataset extends FeatureDataset { - id?: string; - location: LatLngLiteral; - fileBoundary: Polygon | MultiPolygon | null; - parcelFeature: Feature | null; - pimsFeature: Feature | null; - regionFeature: Feature | null; - districtFeature: Feature | null; - municipalityFeature: Feature | null; - isActive?: boolean; - displayOrder?: number; +export function emptyFeatureDataset(): FeatureDataset { + return { + parcelFeatures: null, + pimsFeatures: null, + pimsBoundaryFeatures: null, + regionFeature: null, + districtFeature: null, + municipalityFeatures: null, + highwayFeatures: null, + crownLandLeasesFeatures: null, + crownLandLicensesFeatures: null, + crownLandTenuresFeatures: null, + crownLandInventoryFeatures: null, + crownLandInclusionsFeatures: null, + electoralFeatures: null, + alrFeatures: null, + firstNationFeatures: null, + }; } -export interface WorklistLocationFeatureDataset - extends Omit { - fullyAttributedFeatures: FeatureCollection< - Geometry, - PMBC_FullyAttributed_Feature_Properties - > | null; - pimsFeature: Feature | null; - regionFeature: Feature | null; - districtFeature: Feature | null; +export interface LocationFeatureDataset extends FeatureDataset { + location: LatLngLiteral; } const useLocationFeatureLoader = () => { @@ -103,20 +109,8 @@ const useLocationFeatureLoader = () => { pimsPropertyId?: number | null; }): Promise => { const result: LocationFeatureDataset = { - selectingComponentId: null, + ...emptyFeatureDataset(), location: latLng, - fileLocation: latLng, - pimsFeatures: null, - parcelFeatures: null, - regionFeature: null, - districtFeature: null, - municipalityFeatures: null, - highwayFeatures: null, - crownLandLeasesFeatures: null, - crownLandLicensesFeatures: null, - crownLandTenuresFeatures: null, - crownLandInventoryFeatures: null, - crownLandInclusionsFeatures: null, }; // call these APIs in parallel - notice there is no "await" @@ -195,13 +189,10 @@ const useLocationFeatureLoader = () => { ); const loadWorklistLocationDetails = useCallback( - async ({ latLng }: { latLng: LatLngLiteral }): Promise => { - const result: WorklistLocationFeatureDataset = { + async ({ latLng }: { latLng: LatLngLiteral }): Promise => { + const result: LocationFeatureDataset = { + ...emptyFeatureDataset(), location: latLng, - fullyAttributedFeatures: null, - pimsFeature: null, - regionFeature: null, - districtFeature: null, }; // call these APIs in parallel - notice there is no "await" @@ -215,7 +206,7 @@ const useLocationFeatureLoader = () => { districtTask, ]); - result.fullyAttributedFeatures = pmbcFeatures ?? null; + result.parcelFeatures = pmbcFeatures.features ?? null; result.regionFeature = regionFeature ?? null; result.districtFeature = districtFeature ?? null; diff --git a/source/frontend/src/components/maps/MapLeafletView.tsx b/source/frontend/src/components/maps/MapLeafletView.tsx index 8292b559af..205ca64bd0 100644 --- a/source/frontend/src/components/maps/MapLeafletView.tsx +++ b/source/frontend/src/components/maps/MapLeafletView.tsx @@ -153,12 +153,8 @@ const MapLeafletView: React.FC> = ( [mapMachine.mapFeatureData], ); - const { - mapLocationFeatureDataset, - repositioningFeatureDataset, - isRepositioning, - setDefaultMapLayers, - } = mapMachine; + const { mapLocationFeatureDataset, repositioningFeature, isRepositioning, setDefaultMapLayers } = + mapMachine; // Initialize layers useEffect(() => { @@ -171,10 +167,9 @@ const MapLeafletView: React.FC> = ( activeFeatureLayer?.clearLayers(); if (isRepositioning) { - const pimsFeature = repositioningFeatureDataset?.pimsFeature; - if (exists(pimsFeature)) { + if (exists(repositioningFeature)) { // File marker repositioning is active - highlight the property and the corresponding boundary when user triggers the relocate action. - activeFeatureLayer?.addData(pimsFeature); + activeFeatureLayer?.addData(repositioningFeature); } } else if (exists(mapLocationFeatureDataset)) { if (firstOrNull(mapLocationFeatureDataset.parcelFeatures) !== null) { @@ -182,7 +177,7 @@ const MapLeafletView: React.FC> = ( activeFeatureLayer?.addData(activeFeature); } } - }, [activeFeatureLayer, isRepositioning, mapLocationFeatureDataset, repositioningFeatureDataset]); + }, [activeFeatureLayer, isRepositioning, mapLocationFeatureDataset, repositioningFeature]); useEffect(() => { if (hasPendingFlyTo && isMapReady) { diff --git a/source/frontend/src/components/maps/ZoomToLocation.tsx b/source/frontend/src/components/maps/ZoomToLocation.tsx index 825f63e5d5..d0f93965bb 100644 --- a/source/frontend/src/components/maps/ZoomToLocation.tsx +++ b/source/frontend/src/components/maps/ZoomToLocation.tsx @@ -8,24 +8,25 @@ import { LinkButton } from '@/components/common/buttons/LinkButton'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import TooltipWrapper from '@/components/common/TooltipWrapper'; import { PropertyForm } from '@/features/mapSideBar/shared/models'; -import { ParcelDataset } from '@/features/properties/parcelList/models'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; import { boundaryFromFileProperty, exists, + firstValidOrNull, latLngLiteralToGeometry, pimsGeomeryToGeometry, } from '@/utils'; +import { LocationFeatureDataset } from '../common/mapFSM/useLocationFeatureLoader'; import TooltipIcon from '../common/TooltipIcon'; -export interface IUpdatePropertiesProps { +export interface IZoomToLocationProps { formProperties?: PropertyForm[] | null; pimsProperties?: ApiGen_Concepts_Property[] | null; pimsFileProperties?: ApiGen_Concepts_FileProperty[] | null; - parcelDataset?: ParcelDataset | null; + locationFeatureDataset?: LocationFeatureDataset | null; featureCollection?: Feature[] | null; pimsFeatures?: Feature[] | null; @@ -38,11 +39,11 @@ export enum ZoomIconType { area, } -export const ZoomToLocation: React.FunctionComponent = ({ +export const ZoomToLocation: React.FunctionComponent = ({ formProperties, pimsProperties, pimsFileProperties, - parcelDataset, + locationFeatureDataset, featureCollection, geometry, icon, @@ -78,20 +79,22 @@ export const ZoomToLocation: React.FunctionComponent = ( }); } - if (exists(parcelDataset?.pmbcFeature?.geometry)) { - const pmbcGeometry = parcelDataset.pmbcFeature.geometry; + const validParcelFeature = firstValidOrNull(locationFeatureDataset?.parcelFeatures, exists); + const validPimsFeature = firstValidOrNull(locationFeatureDataset?.pimsFeatures, exists); + if (exists(validParcelFeature?.geometry)) { + const pmbcGeometry = validParcelFeature.geometry; const pmbcBounds = geoJSON(pmbcGeometry)?.getBounds(); if (exists(pmbcBounds) && pmbcBounds.isValid()) { propertyLocations.push(pmbcGeometry); } - } else if (exists(parcelDataset?.pimsFeature?.geometry)) { - const pimsFeatureGeometry = parcelDataset.pimsFeature.geometry; + } else if (exists(validPimsFeature?.geometry)) { + const pimsFeatureGeometry = validPimsFeature.geometry; const pimsBounds = geoJSON(pimsFeatureGeometry)?.getBounds(); if (exists(pimsBounds) && pimsBounds.isValid()) { propertyLocations.push(pimsFeatureGeometry); } - } else if (exists(parcelDataset?.location)) { - propertyLocations.push(latLngLiteralToGeometry(parcelDataset.location)); + } else if (exists(locationFeatureDataset?.location)) { + propertyLocations.push(latLngLiteralToGeometry(locationFeatureDataset.location)); } if (exists(featureCollection)) { @@ -114,9 +117,9 @@ export const ZoomToLocation: React.FunctionComponent = ( featureCollection, formProperties, geometry, - parcelDataset?.location, - parcelDataset?.pimsFeature, - parcelDataset?.pmbcFeature, + locationFeatureDataset?.location, + locationFeatureDataset?.parcelFeatures, + locationFeatureDataset?.pimsFeatures, pimsFileProperties, pimsProperties, ]); diff --git a/source/frontend/src/components/maps/leaflet/Control/Search/PropertyQuickInfoContainer.tsx b/source/frontend/src/components/maps/leaflet/Control/Search/PropertyQuickInfoContainer.tsx index 5e3a426018..b22a95d3f8 100644 --- a/source/frontend/src/components/maps/leaflet/Control/Search/PropertyQuickInfoContainer.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/Search/PropertyQuickInfoContainer.tsx @@ -10,10 +10,7 @@ import ManagementIcon from '@/assets/images/management-icon.svg?react'; import ResearchIcon from '@/assets/images/research-icon.svg?react'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { - SelectedFeatureDataset, - WorklistLocationFeatureDataset, -} from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import MoreOptionsMenu, { MenuOption } from '@/components/common/MoreOptionsMenu'; import { SectionField } from '@/components/common/Section/SectionField'; import TooltipWrapper from '@/components/common/TooltipWrapper'; @@ -26,7 +23,7 @@ import { useLtsa } from '@/hooks/useLtsa'; import { exists, firstOrNull, - getPropertyNameFromSelectedFeatureSet, + getPropertyNameFromLocationFeatureSet, isValidString, pidFormatter, } from '@/utils'; @@ -38,7 +35,7 @@ export const PropertyQuickInfoContainer: React.FC = () const { mapLocationFeatureDataset, - prepareForCreation, + requestLocationFeatureAddition: requestAddition, worklistAdd, isEditPropertiesMode, mapMarkedLocation, @@ -158,74 +155,44 @@ export const PropertyQuickInfoContainer: React.FC = () mapMachine.closeQuickInfo(); }, [mapMachine]); - // Convert to an object that can be consumed by the file creation process - const selectedFeatureDataset = useMemo(() => { - return { - selectingComponentId: mapLocationFeatureDataset?.selectingComponentId ?? null, - location: mapLocationFeatureDataset?.location, - fileLocation: mapLocationFeatureDataset?.fileLocation ?? null, - fileBoundary: null, - parcelFeature: firstOrNull(mapLocationFeatureDataset?.parcelFeatures), - pimsFeature: firstOrNull(mapLocationFeatureDataset?.pimsFeatures), - regionFeature: mapLocationFeatureDataset?.regionFeature ?? null, - districtFeature: mapLocationFeatureDataset?.districtFeature ?? null, - municipalityFeature: firstOrNull(mapLocationFeatureDataset?.municipalityFeatures), - isActive: true, - displayOrder: 0, - }; - }, [ - mapLocationFeatureDataset?.selectingComponentId, - mapLocationFeatureDataset?.location, - mapLocationFeatureDataset?.fileLocation, - mapLocationFeatureDataset?.parcelFeatures, - mapLocationFeatureDataset?.pimsFeatures, - mapLocationFeatureDataset?.regionFeature, - mapLocationFeatureDataset?.districtFeature, - mapLocationFeatureDataset?.municipalityFeatures, - ]); - const onAddToWorklist = useCallback(() => { - const worklistDataSet: WorklistLocationFeatureDataset = { - ...selectedFeatureDataset, - fullyAttributedFeatures: { - type: 'FeatureCollection', - features: [selectedFeatureDataset.parcelFeature], - }, + const worklistDataSet: LocationFeatureDataset = { + ...mapLocationFeatureDataset, }; worklistAdd(worklistDataSet); - }, [selectedFeatureDataset, worklistAdd]); + }, [mapLocationFeatureDataset, worklistAdd]); const onCreateResearchFile = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); + requestAddition([mapLocationFeatureDataset]); pathGenerator.newFile('research'); - }, [pathGenerator, prepareForCreation, selectedFeatureDataset]); + }, [pathGenerator, requestAddition, mapLocationFeatureDataset]); const onCreateAcquisitionFile = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); + requestAddition([mapLocationFeatureDataset]); pathGenerator.newFile('acquisition'); - }, [pathGenerator, prepareForCreation, selectedFeatureDataset]); + }, [pathGenerator, requestAddition, mapLocationFeatureDataset]); const onCreateDispositionFile = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); + requestAddition([mapLocationFeatureDataset]); pathGenerator.newFile('disposition'); - }, [pathGenerator, prepareForCreation, selectedFeatureDataset]); + }, [pathGenerator, requestAddition, mapLocationFeatureDataset]); const onCreateLeaseFile = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); + requestAddition([mapLocationFeatureDataset]); pathGenerator.newFile('lease'); - }, [pathGenerator, prepareForCreation, selectedFeatureDataset]); + }, [pathGenerator, requestAddition, mapLocationFeatureDataset]); const onCreateManagementFile = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); + requestAddition([mapLocationFeatureDataset]); pathGenerator.newFile('management'); - }, [pathGenerator, prepareForCreation, selectedFeatureDataset]); + }, [pathGenerator, requestAddition, mapLocationFeatureDataset]); const onAddToOpenFile = useCallback(() => { // If in edit properties mode, prepare the parcel for addition to an open file if (isEditPropertiesMode) { - prepareForCreation([selectedFeatureDataset]); + requestAddition([mapLocationFeatureDataset]); } - }, [isEditPropertiesMode, prepareForCreation, selectedFeatureDataset]); + }, [isEditPropertiesMode, requestAddition, mapLocationFeatureDataset]); const menuOptions: MenuOption[] = useMemo(() => { const options: MenuOption[] = []; @@ -405,7 +372,7 @@ export const PropertyQuickInfoContainer: React.FC = () {isEmpty && exists(mapMarkedLocation) && ( - {getPropertyNameFromSelectedFeatureSet(selectedFeatureDataset).value} + {getPropertyNameFromLocationFeatureSet(mapLocationFeatureDataset).value} )} diff --git a/source/frontend/src/components/maps/leaflet/Control/Search/SearchContainer.tsx b/source/frontend/src/components/maps/leaflet/Control/Search/SearchContainer.tsx index e1b48b364c..70ea7e2b98 100644 --- a/source/frontend/src/components/maps/leaflet/Control/Search/SearchContainer.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/Search/SearchContainer.tsx @@ -4,15 +4,18 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { + emptyFeatureDataset, + LocationFeatureDataset, +} from '@/components/common/mapFSM/useLocationFeatureLoader'; import usePathGenerator from '@/features/mapSideBar/shared/sidebarPathGenerator'; import { IPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; import { useAdminBoundaryMapLayer } from '@/hooks/repositories/mapLayer/useAdminBoundaryMapLayer'; import { exists, - featureSetToLatLngKey, getFeatureBoundedCenter, getRegionAndDistrictsResults, + latLngToKey, } from '@/utils'; import { ISearchViewProps } from './SearchView'; @@ -30,7 +33,7 @@ export const SearchContainer: React.FC = ({ View }) => { mapFeatureData, mapMarkLocation, mapClearLocationMark, - prepareForCreation, + requestLocationFeatureAddition: requestAddition, isEditPropertiesMode, } = useMapStateMachine(); @@ -73,27 +76,21 @@ export const SearchContainer: React.FC = ({ View }) => { }; // Base dataset (no region/district yet) - const baseDatasets = useMemo(() => { + const baseDatasets = useMemo(() => { return ( - mapFeatureData?.fullyAttributedFeatures.features.map(pmbcParcel => { + mapFeatureData?.fullyAttributedFeatures.features.map(pmbcParcel => { const center = getFeatureBoundedCenter(pmbcParcel); return { - parcelFeature: pmbcParcel, - pimsFeature: null, + ...emptyFeatureDataset(), location: { lat: center[1], lng: center[0] }, - regionFeature: null, - fileLocation: null, - fileBoundary: null, - districtFeature: null, - municipalityFeature: null, - selectingComponentId: null, + parcelFeatures: [pmbcParcel], }; }) ?? [] ); }, [mapFeatureData?.fullyAttributedFeatures?.features]); // Enrich dataset with region/district info - const [selectedFeatureDatasets, setSelectedFeatureDatasets] = useState( + const [locationFeatureDatasets, setLocationFeatureDatasets] = useState( [], ); @@ -102,7 +99,7 @@ export const SearchContainer: React.FC = ({ View }) => { async function fetchRegions() { if (baseDatasets.length === 0) { - setSelectedFeatureDatasets([]); + setLocationFeatureDatasets([]); return; } @@ -111,7 +108,7 @@ export const SearchContainer: React.FC = ({ View }) => { if (cancelled) return; const enriched = baseDatasets.map(dataset => { - const key = featureSetToLatLngKey(dataset); + const key = latLngToKey(dataset.location); if (results.has(key)) { const { regionResult, districtResult } = results.get(key)!; return { @@ -124,7 +121,7 @@ export const SearchContainer: React.FC = ({ View }) => { } }); - setSelectedFeatureDatasets(enriched); + setLocationFeatureDatasets(enriched); } fetchRegions(); @@ -137,36 +134,36 @@ export const SearchContainer: React.FC = ({ View }) => { // Actions for creating new files const onCreateResearchFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); + requestAddition(locationFeatureDatasets); pathGenerator.newFile('research'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); + }, [pathGenerator, requestAddition, locationFeatureDatasets]); const onCreateAcquisitionFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); + requestAddition(locationFeatureDatasets); pathGenerator.newFile('acquisition'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); + }, [pathGenerator, requestAddition, locationFeatureDatasets]); const onCreateDispositionFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); + requestAddition(locationFeatureDatasets); pathGenerator.newFile('disposition'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); + }, [pathGenerator, requestAddition, locationFeatureDatasets]); const onCreateLeaseFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); + requestAddition(locationFeatureDatasets); pathGenerator.newFile('lease'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); + }, [pathGenerator, requestAddition, locationFeatureDatasets]); const onCreateManagementFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); + requestAddition(locationFeatureDatasets); pathGenerator.newFile('management'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); + }, [pathGenerator, requestAddition, locationFeatureDatasets]); const onAddToOpenFile = useCallback(() => { // If in edit properties mode, prepare the parcel for addition to an open file if (isEditPropertiesMode) { - prepareForCreation(selectedFeatureDatasets); + requestAddition(locationFeatureDatasets); } - }, [isEditPropertiesMode, prepareForCreation, selectedFeatureDatasets]); + }, [isEditPropertiesMode, requestAddition, locationFeatureDatasets]); return ( = props => { groupedFeatures .value() .flatMap(x => x) - .map(x => ParcelDataset.fromFullyAttributedFeature(x.feature)) ?? []; + .map(x => featureToLocationFeatureDataset(x.feature)) ?? []; const pimsGroupedFeatures = chain(props.searchResult?.pimsLocationFeatures.features) .groupBy(feature => feature?.properties?.SURVEY_PLAN_NUMBER) @@ -104,7 +104,7 @@ export const SearchView: React.FC = props => { pimsGroupedFeatures .value() .flatMap(x => x) - .map(x => ParcelDataset.fromPimsFeature(x.feature)) ?? []; + .map(x => featureToLocationFeatureDataset(x.feature)) ?? []; const menuOptions: MenuOption[] = useMemo(() => { const options: MenuOption[] = []; diff --git a/source/frontend/src/components/maps/leaflet/LayerPopup/MultiplePropertyPopupView.tsx b/source/frontend/src/components/maps/leaflet/LayerPopup/MultiplePropertyPopupView.tsx index ef9823a417..d8ca1ef91b 100644 --- a/source/frontend/src/components/maps/leaflet/LayerPopup/MultiplePropertyPopupView.tsx +++ b/source/frontend/src/components/maps/leaflet/LayerPopup/MultiplePropertyPopupView.tsx @@ -7,14 +7,11 @@ import styled from 'styled-components'; import { LinkButton, StyledIconButton } from '@/components/common/buttons'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { - LocationFeatureDataset, - WorklistLocationFeatureDataset, -} from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import TooltipWrapper from '@/components/common/TooltipWrapper'; import { StyledScrollable } from '@/features/documents/commonStyles'; import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; -import { exists, firstOrNull } from '@/utils'; +import { exists } from '@/utils'; import { isStrataCommonProperty, pidFormatter } from '@/utils/propertyUtils'; export interface IMultiplePropertyPopupView { @@ -48,17 +45,7 @@ export const MultiplePropertyPopupView: React.FC< }; const onAddAllToWorklist = () => { - const worklistDataSet: WorklistLocationFeatureDataset = { - fullyAttributedFeatures: { - features: featureDataset.parcelFeatures, - type: 'FeatureCollection', - }, - pimsFeature: firstOrNull(featureDataset.pimsFeatures), - regionFeature: featureDataset.regionFeature, - districtFeature: featureDataset.districtFeature, - location: featureDataset.location, - }; - mapMachine.worklistAdd(worklistDataSet); + mapMachine.worklistAdd(featureDataset); }; const groupedFeatures = chain(featureDataset?.parcelFeatures) diff --git a/source/frontend/src/components/maps/leaflet/Layers/FilePropertiesLayer.tsx b/source/frontend/src/components/maps/leaflet/Layers/FilePropertiesLayer.tsx index cd2887fb06..700e3204e6 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/FilePropertiesLayer.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/FilePropertiesLayer.tsx @@ -1,6 +1,6 @@ import { feature, featureCollection } from '@turf/turf'; -import { FeatureCollection } from 'geojson'; -import L, { LatLng } from 'leaflet'; +import { FeatureCollection, Geometry } from 'geojson'; +import L, { LatLng, LatLngLiteral } from 'leaflet'; import find from 'lodash/find'; import { useEffect, useMemo, useRef } from 'react'; import { FeatureGroup, GeoJSON, Marker, Pane } from 'react-leaflet'; @@ -8,13 +8,34 @@ import { useTheme } from 'styled-components'; import { v4 as uuidv4 } from 'uuid'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { LocationBoundaryDataset } from '@/components/common/mapFSM/models'; import { usePrevious } from '@/hooks/usePrevious'; import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; -import { exists } from '@/utils'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; +import { exists, getLatLng, locationFromFileProperty } from '@/utils'; import { useFilterContext } from '../../providers/FilterProvider'; import { getDisabledDraftIcon, getDraftIcon } from './util'; +interface FileLocationBoundaryDataset { + readonly location: LatLngLiteral; + readonly propertyBoundary: Geometry | null; + readonly fileBoundary: Geometry | null; + readonly isActive?: boolean; +} + +function filePropertyToFileLocationBoundaryDataset( + fileProperty: ApiGen_Concepts_FileProperty | undefined | null, +): FileLocationBoundaryDataset | null { + const geom = locationFromFileProperty(fileProperty); + const location = getLatLng(geom); + return exists(location) + ? { + location, + propertyBoundary: fileProperty?.property?.boundary ?? null, + fileBoundary: fileProperty?.boundary ?? null, + isActive: fileProperty.isActive, + } + : null; +} export const FilePropertiesLayer: React.FunctionComponent = () => { const draftFeatureGroupRef = useRef(null); @@ -23,19 +44,23 @@ export const FilePropertiesLayer: React.FunctionComponent = () => { const mapMachine = useMapStateMachine(); const mapMarkerClickFn = mapMachine.mapMarkerClick; - const filePropertyLocations = mapMachine.filePropertyLocations; - const draftPoints = useMemo(() => { + const filePropertyLocations = useMemo( + () => mapMachine.filePropertyLocations.map(x => filePropertyToFileLocationBoundaryDataset(x)), + [mapMachine.filePropertyLocations], + ); + + const draftPoints = useMemo(() => { return (filePropertyLocations ?? []).filter( dp => exists(dp?.location?.lat) && exists(dp?.location?.lng), ); }, [filePropertyLocations]); // These are the boundaries for the properties - const draftBoundaryFeatures = useMemo(() => { + const propertyBoundaryFeatures = useMemo(() => { // ignore properties without a valid boundary const validBoundaries = (filePropertyLocations ?? []) - .map(pl => pl.boundary) + .map(pl => pl.propertyBoundary) .filter(exists) .map(boundary => feature(boundary)); @@ -55,17 +80,17 @@ export const FilePropertiesLayer: React.FunctionComponent = () => { const boundaryLayerKeyRef = useRef(uuidv4()); const fileBoundaryLayerKeyRef = useRef(uuidv4()); - const previousBoundaries = usePrevious(draftBoundaryFeatures); + const previousPropertyBoundaries = usePrevious(propertyBoundaryFeatures); const previousFileBoundaries = usePrevious(fileBoundaryFeatures); // We need to regenerate an unique `key` on the `` element when the underlying data changes. // This is to force React to re-render the GeoJSON component with the updated property boundaries. // https://github.com/PaulLeCam/react-leaflet/issues/332 useEffect(() => { - if (previousBoundaries !== draftBoundaryFeatures) { + if (previousPropertyBoundaries !== propertyBoundaryFeatures) { boundaryLayerKeyRef.current = uuidv4(); } - }, [draftBoundaryFeatures, previousBoundaries]); + }, [propertyBoundaryFeatures, previousPropertyBoundaries]); useEffect(() => { if (previousFileBoundaries !== fileBoundaryFeatures) { @@ -138,11 +163,11 @@ export const FilePropertiesLayer: React.FunctionComponent = () => { - {draftBoundaryFeatures?.features?.length > 0 && ( + {propertyBoundaryFeatures?.features?.length > 0 && ( @@ -161,7 +186,7 @@ export const FilePropertiesLayer: React.FunctionComponent = () => { ), [ draftPoints, - draftBoundaryFeatures, + propertyBoundaryFeatures, fileBoundaryFeatures, theme.css.pimsRed80, mapMarkerClickFn, diff --git a/source/frontend/src/components/maps/leaflet/Layers/WorklistMarkersLayer.tsx b/source/frontend/src/components/maps/leaflet/Layers/WorklistMarkersLayer.tsx index 6b1494d6b9..17e25361af 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/WorklistMarkersLayer.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/WorklistMarkersLayer.tsx @@ -1,9 +1,9 @@ import React, { useMemo } from 'react'; import { Marker } from 'react-leaflet'; -import { ParcelDataset } from '@/features/properties/parcelList/models'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { useWorklistContext } from '@/features/properties/worklist/context/WorklistContext'; -import { exists } from '@/utils'; +import { exists, firstOrNull, latLngToKey } from '@/utils'; import { getNotOwnerMarkerIcon } from './util'; @@ -12,15 +12,20 @@ export const WorklistMarkersLayer: React.FunctionComponent = () => { // Now, lat/long properties in the worklist will display on the map as markers. // But must not have a Feature. - const validLocations = useMemo( - () => (parcels ?? []).filter(p => !exists(p?.pmbcFeature) && exists(p?.location)), + const validLocations = useMemo( + () => + (parcels ?? []).filter(p => !exists(firstOrNull(p?.parcelFeatures)) && exists(p?.location)), [parcels], ); return ( {validLocations.map(vp => ( - + ))} ); diff --git a/source/frontend/src/components/maps/leaflet/Layers/WorklistParcelsLayer.tsx b/source/frontend/src/components/maps/leaflet/Layers/WorklistParcelsLayer.tsx index 82944f4ea0..6f6e6ddea8 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/WorklistParcelsLayer.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/WorklistParcelsLayer.tsx @@ -2,17 +2,18 @@ import React, { useMemo } from 'react'; import { GeoJSON } from 'react-leaflet'; import { v4 as uuidv4 } from 'uuid'; -import { ParcelDataset } from '@/features/properties/parcelList/models'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { useWorklistContext } from '@/features/properties/worklist/context/WorklistContext'; -import { exists } from '@/utils'; +import { exists, firstValidOrNull, latLngToKey } from '@/utils'; export const WorklistParcelsLayer: React.FunctionComponent = () => { const { parcels } = useWorklistContext(); // For now, lat/long properties in the worklist will not display on the map // Ignore properties without a valid boundary - const validParcels = useMemo( - () => (parcels ?? []).filter(p => exists(p?.pmbcFeature?.geometry)), + const validParcels = useMemo( + () => + (parcels ?? []).filter(p => exists(firstValidOrNull(p?.parcelFeatures, exists)?.geometry)), [parcels], ); @@ -20,8 +21,8 @@ export const WorklistParcelsLayer: React.FunctionComponent = () => { {validParcels.map(vp => ( ))} diff --git a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx index 9497053014..779c7b42eb 100644 --- a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx +++ b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx @@ -1,63 +1,28 @@ -import { LatLngLiteral } from 'leaflet'; -import { toast } from 'react-toastify'; - import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; import { usePrevious } from '@/hooks/usePrevious'; import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; -import { exists, firstOrNull, isValidId } from '@/utils'; -import { featuresetToLocationBoundaryDataset } from '@/utils/mapPropertyUtils'; +import { exists, isValidId } from '@/utils'; -import { SelectedFeatureDataset } from '../common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '../common/mapFSM/useLocationFeatureLoader'; interface IMapClickMonitorProps { - addProperty?: (property: SelectedFeatureDataset) => void; - repositionProperty: ( - property: SelectedFeatureDataset, - latLng: LatLngLiteral, - propertyIndex: number | null, - ) => void; - modifiedProperties: SelectedFeatureDataset[]; // TODO: this should be just a list of lat longs - selectedComponentId: string | null; + onNewLocation: (locationDataset: LocationFeatureDataset, hasMultipleProperties: boolean) => void; } export const MapClickMonitor: React.FunctionComponent = ({ - addProperty, - repositionProperty, - modifiedProperties, - selectedComponentId, + onNewLocation, }) => { const mapMachine = useMapStateMachine(); const previous = usePrevious(mapMachine.mapLocationFeatureDataset); - const modifiedMapProperties = modifiedProperties.map(mp => - featuresetToLocationBoundaryDataset(mp), - ); - useDraftMarkerSynchronizer(selectedComponentId ? [] : modifiedMapProperties); // disable the draft marker synchronizer if the selecting component is set - the parent will need to control the draft markers. useDeepCompareEffect(() => { - if ( - mapMachine.isSelecting && - mapMachine.mapLocationFeatureDataset && - previous !== mapMachine.mapLocationFeatureDataset && - previous !== undefined && - (!selectedComponentId || - selectedComponentId === mapMachine.mapLocationFeatureDataset.selectingComponentId) - ) { - const selectedFeature: SelectedFeatureDataset = { - location: mapMachine.mapLocationFeatureDataset.location, - parcelFeature: firstOrNull(mapMachine.mapLocationFeatureDataset.parcelFeatures), - pimsFeature: firstOrNull(mapMachine.mapLocationFeatureDataset.pimsFeatures), - regionFeature: mapMachine.mapLocationFeatureDataset.regionFeature, - districtFeature: mapMachine.mapLocationFeatureDataset.districtFeature, - municipalityFeature: firstOrNull(mapMachine.mapLocationFeatureDataset.municipalityFeatures), - selectingComponentId: mapMachine.mapLocationFeatureDataset.selectingComponentId, - fileLocation: mapMachine.mapLocationFeatureDataset.fileLocation, - fileBoundary: null, - }; + const selectedFeature = mapMachine.mapLocationFeatureDataset; + + if (exists(selectedFeature) && previous !== selectedFeature) { const parcelFeaturesNotInPims = - mapMachine.mapLocationFeatureDataset.parcelFeatures?.filter(pf => { - const matchingProperty = mapMachine.mapLocationFeatureDataset.pimsFeatures?.find( + selectedFeature.parcelFeatures?.filter(pf => { + const matchingProperty = selectedFeature.pimsFeatures?.find( plp => (isValidId(pf.properties.PID_NUMBER) && plp.properties.PID === pf.properties.PID_NUMBER) || @@ -66,45 +31,11 @@ export const MapClickMonitor: React.FunctionComponent = ( return !exists(matchingProperty); }) ?? []; - // psp-10193, we cannot support adding a property via click when there are multiple parcel map of multiple pims references already at this location. - if ( - parcelFeaturesNotInPims.length + - (mapMachine.mapLocationFeatureDataset.pimsFeatures?.length ?? 0) > - 1 - ) { - toast.error( - 'There are multiple properties at the clicked location. Use the "Search" functionality instead of "Locate on Map" to add one of the properties at this location instead.', - { autoClose: false }, - ); - return; - } - - addProperty?.(selectedFeature); - } - - if ( - mapMachine.isRepositioning && - mapMachine.repositioningFeatureDataset && - mapMachine.mapLocationFeatureDataset && - previous !== mapMachine.mapLocationFeatureDataset && - previous !== undefined && - (!selectedComponentId || - selectedComponentId === mapMachine.mapLocationFeatureDataset.selectingComponentId) - ) { - repositionProperty( - mapMachine.repositioningFeatureDataset, - mapMachine.mapLocationFeatureDataset.location, - mapMachine.repositioningPropertyIndex, - ); + const hasMultipleProperties = + parcelFeaturesNotInPims.length + (selectedFeature.pimsFeatures?.length ?? 0) > 1; + onNewLocation(selectedFeature, hasMultipleProperties); } - }, [ - addProperty, - mapMachine.isSelecting, - mapMachine.isRepositioning, - mapMachine.mapLocationFeatureDataset, - mapMachine.repositioningFeatureDataset, - previous, - ]); + }, [mapMachine.mapLocationFeatureDataset, previous]); return <>; }; diff --git a/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx b/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx index ca682d7ec7..7455d1c5b9 100644 --- a/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx +++ b/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx @@ -6,10 +6,8 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { useMapProperties } from '@/hooks/repositories/useMapProperties'; -import { - getMockLocationFeatureDataset, - getMockSelectedFeatureDataset, -} from '@/mocks/featureset.mock'; +import { getMockLocationFeatureDataset } from '@/mocks/featureset.mock'; +import { getMockSelectedFeatureDataset } from '@/mocks/getMockSelectedFeatureDataset'; import { mockFAParcelLayerResponse, mockGeocoderOptions } from '@/mocks/index.mock'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { act, fillInput, render, RenderOptions, screen, userEvent } from '@/utils/test-utils'; @@ -248,7 +246,7 @@ describe('MapSelectorContainer component', () => { const testMapMock: IMapStateMachineContext = { ...mapMachineBaseMock, isRepositioning: true, - repositioningFeatureDataset: {} as any, + mapFeatureData: {} as any, mapLocationFeatureDataset: {} as any, }; const mapProperties = [ diff --git a/source/frontend/src/components/propertySelector/MapSelectorContainer.tsx b/source/frontend/src/components/propertySelector/MapSelectorContainer.tsx index 3c082ffd84..321b2f2789 100644 --- a/source/frontend/src/components/propertySelector/MapSelectorContainer.tsx +++ b/source/frontend/src/components/propertySelector/MapSelectorContainer.tsx @@ -1,52 +1,43 @@ -import { LatLngLiteral } from 'leaflet'; import { FunctionComponent, useCallback, useState } from 'react'; import { toast } from 'react-toastify'; import { Button } from '@/components/common/buttons'; import { useMapProperties } from '@/hooks/repositories/useMapProperties'; -import { isValidId } from '@/utils'; +import { firstOrNull, isValidId } from '@/utils'; import { - areSelectedFeaturesEqual, - getPropertyNameFromSelectedFeatureSet, + areLocationFeatureDatasetsEqual, + getPropertyNameFromLocationFeatureSet, pidFromFeatureSet, pinFromFeatureSet, } from '@/utils/mapPropertyUtils'; -import { SelectedFeatureDataset } from '../common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '../common/mapFSM/useLocationFeatureLoader'; import PropertyMapSelectorFormView from './map/PropertyMapSelectorFormView'; import { PropertySelectorTabsView, SelectorTabNames } from './PropertySelectorTabsView'; import PropertySelectorSearchContainer from './search/PropertySelectorSearchContainer'; export interface IMapSelectorContainerProps { - addSelectedProperties: (properties: SelectedFeatureDataset[]) => void; - repositionSelectedProperty: ( - property: SelectedFeatureDataset, - latLng: LatLngLiteral, - propertyIndex: number | null, - ) => void; - modifiedProperties: SelectedFeatureDataset[]; // TODO: Figure out if this component really needs the entire LocationFeatureDataset. It could be that only the lat long are needed. - selectedComponentId?: string; + addSelectedProperties: (properties: LocationFeatureDataset[]) => void; + modifiedProperties: LocationFeatureDataset[]; // TODO: Figure out if this component really needs the entire LocationFeatureDataset. It could be that only the lat long are needed. } export const MapSelectorContainer: FunctionComponent = ({ addSelectedProperties, - repositionSelectedProperty, modifiedProperties, - selectedComponentId, }) => { const [searchSelectedProperties, setSearchSelectedProperties] = useState< - SelectedFeatureDataset[] + LocationFeatureDataset[] >([]); const [activeSelectorTab, setActiveSelectorTab] = useState( SelectorTabNames.map, ); - const modifiedMapProperties = modifiedProperties.map(mp => mp); + const [lastSelectedProperty, setLastSelectedProperty] = useState< - SelectedFeatureDataset | undefined + LocationFeatureDataset | undefined >( modifiedProperties?.length === 1 && - (modifiedProperties[0]?.pimsFeature || modifiedProperties[0]?.parcelFeature) // why? Because create from map needs to show the info differently - ? modifiedMapProperties[0] + (modifiedProperties[0]?.pimsFeatures || modifiedProperties[0]?.parcelFeatures) // why? Because create from map needs to show the info differently + ? modifiedProperties[0] : undefined, ); const { @@ -54,9 +45,10 @@ export const MapSelectorContainer: FunctionComponent } = useMapProperties(); const addWithPimsFeature = useCallback( - async (properties: SelectedFeatureDataset[]) => { + async (properties: LocationFeatureDataset[]) => { const updatedPropertiesPromises = properties.map(async property => { - if (property.pimsFeature?.properties?.PROPERTY_ID) { + // TODO: Might need an update to work with multiple properties + if (firstOrNull(property.pimsFeatures)?.properties?.PROPERTY_ID) { return property; } const pid = pidFromFeatureSet(property); @@ -73,8 +65,7 @@ export const MapSelectorContainer: FunctionComponent } const pimsProperty = await loadProperties(queryObject); if (pimsProperty.features.length > 0) { - // TODO: Might need updates to work with multiple properties - property.pimsFeature = pimsProperty.features[0]; + property.pimsFeatures = [pimsProperty.features[0]]; } return property; }); @@ -90,26 +81,16 @@ export const MapSelectorContainer: FunctionComponent setActiveTab={setActiveSelectorTab} MapSelectorView={ { - setLastSelectedProperty(property); - await addProperties([property], modifiedMapProperties, addWithPimsFeature); - }} - onRepositionedProperty={( - property: SelectedFeatureDataset, - latLng: LatLngLiteral, - propertyIndex: number | null, - ) => { + onNewLocation={async (property: LocationFeatureDataset) => { setLastSelectedProperty(property); - repositionSelectedProperty(property, latLng, propertyIndex); + await addProperties([property], modifiedProperties, addWithPimsFeature); }} - selectedProperties={modifiedMapProperties} - selectedComponentId={selectedComponentId} lastSelectedProperty={ lastSelectedProperty - ? modifiedMapProperties.find( + ? modifiedProperties.find( p => - getPropertyNameFromSelectedFeatureSet(p).value === - getPropertyNameFromSelectedFeatureSet(lastSelectedProperty).value, + getPropertyNameFromLocationFeatureSet(p).value === + getPropertyNameFromLocationFeatureSet(lastSelectedProperty).value, ) : undefined // use the property from the modified properties list from the parent, for consistency. } @@ -126,11 +107,7 @@ export const MapSelectorContainer: FunctionComponent - - )} - - { - arrayHelpersRef.current = arrayHelpers; - return ( -
- Selected Properties - - lf?.property)} - /> - - - } - > - - {formikProps.values.properties.map((leaseProperty, index) => { - const property = leaseProperty?.property; - if (exists(property)) { - return ( - onRemoveClick(index)} - nameSpace={`properties.${index}`} - index={index} - property={property.toFeatureDataset()} - showSeparator={index < formikProps.values.properties.length - 1} - /> - ); - } - return <>; - })} - {formikProps.values.properties.length === 0 && No Properties selected} -
- ); - }} + + ); }; export default LeasePropertySelector; - -const StyledButtonWrapper = styled.div` - margin: 0 1.6rem; - padding-left: 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx index 16fc55b0c6..e4f19cb83b 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx @@ -1,5 +1,4 @@ import { AxiosError } from 'axios'; -import { dequal } from 'dequal'; import { FieldArray, FieldArrayRenderProps, Formik, FormikProps } from 'formik'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; @@ -10,9 +9,12 @@ import { Button } from '@/components/common/buttons'; import GenericModal, { ModalProps } from '@/components/common/GenericModal'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { Section } from '@/components/common/Section/Section'; import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; +import AreaContainer from '@/components/measurements/AreaContainer'; +import MapClickMonitor from '@/components/propertySelector/MapClickMonitor'; +import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; import { ModalContext } from '@/contexts/modalContext'; import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; @@ -22,18 +24,24 @@ import AddPropertiesGuide from '@/features/mapSideBar/shared/update/properties/A import { UpdatePropertiesYupSchema } from '@/features/mapSideBar/shared/update/properties/UpdatePropertiesYupSchema'; import { usePropertyLeaseRepository } from '@/hooks/repositories/usePropertyLeaseRepository'; import useApiUserOverride from '@/hooks/useApiUserOverride'; -import { useEnrichWithPimsFeatures } from '@/hooks/useEnrichWithPimsFeatures'; -import { useFeatureDatasetsWithAddresses } from '@/hooks/useFeatureDatasetsWithAddresses'; +import { useLocationFeatureDatasetsWithAddresses } from '@/hooks/useLocationFeatureDatasetsWithAddresses'; import { getCancelModalProps } from '@/hooks/useModalContext'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { arePropertyFormsEqual, exists, firstOrNull, isValidId } from '@/utils'; +import { + arePropertyFormsEqual, + exists, + isEmptyOrNull, + isLatLngInFeatureSetBoundary, + isNumber, + isValidId, +} from '@/utils'; +import { withNameSpace } from '@/utils/formUtils'; import { useLeaseDetail } from '../../hooks/useLeaseDetail'; import { FormLeaseProperty, LeaseFormModel } from '../../models'; import SelectedPropertyHeaderRow from './selectedPropertyList/SelectedPropertyHeaderRow'; -import SelectedPropertyRow from './selectedPropertyList/SelectedPropertyRow'; interface LeaseUpdatePropertySelectorProp { lease: ApiGen_Concepts_Lease; } @@ -43,6 +51,7 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< > = ({ lease }) => { const pathSolver = usePathGenerator(); const [showSaveConfirmModal, setShowSaveConfirmModal] = useState(false); + const [repositionPropertyIndex, setRepositionPropertyIndex] = useState(null); const [isValid, setIsValid] = useState(true); const hasWarnedRef = useRef(false); @@ -52,48 +61,19 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< const formikRef = useRef>(); const arrayHelpersRef = useRef(null); - const { - datasets, - loading: pimsFeatureLoading, - enrichWithPimsFeatures, - } = useEnrichWithPimsFeatures(); const { updateLeaseProperties } = usePropertyLeaseRepository(); const { getCompleteLease } = useLeaseDetail(lease?.id ?? undefined); const { - refreshMapProperties, - setEditPropertiesMode, - selectedFeatures, - processCreation, + locationFeaturesForAddition, + processLocationFeaturesAddition: processCreation, mapLocationFeatureDataset, - prepareForCreation, + requestLocationFeatureAddition: prepareForCreation, + isRepositioning, + finishReposition, + setEditPropertiesMode, + refreshMapProperties, } = useMapStateMachine(); - const prevSelectedRef = useRef(); - - const selectedFeatureDataset = useMemo(() => { - return { - selectingComponentId: mapLocationFeatureDataset?.selectingComponentId ?? null, - location: mapLocationFeatureDataset?.location, - fileLocation: mapLocationFeatureDataset?.fileLocation ?? null, - fileBoundary: null, - parcelFeature: firstOrNull(mapLocationFeatureDataset?.parcelFeatures), - pimsFeature: firstOrNull(mapLocationFeatureDataset?.pimsFeatures), - regionFeature: mapLocationFeatureDataset?.regionFeature ?? null, - districtFeature: mapLocationFeatureDataset?.districtFeature ?? null, - municipalityFeature: firstOrNull(mapLocationFeatureDataset?.municipalityFeatures), - isActive: true, - displayOrder: 0, - }; - }, [ - mapLocationFeatureDataset?.selectingComponentId, - mapLocationFeatureDataset?.location, - mapLocationFeatureDataset?.fileLocation, - mapLocationFeatureDataset?.parcelFeatures, - mapLocationFeatureDataset?.pimsFeatures, - mapLocationFeatureDataset?.regionFeature, - mapLocationFeatureDataset?.districtFeature, - mapLocationFeatureDataset?.municipalityFeatures, - ]); const withUserOverride = useApiUserOverride< (userOverrideCodes: UserOverrideCode[]) => Promise @@ -130,18 +110,9 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< [], ); - // Enrich selected features with PIMS features - // This will add pimsFeature to each SelectedFeatureDataset if it exists - useEffect(() => { - if (selectedFeatures?.length > 0 && !dequal(prevSelectedRef.current, selectedFeatures)) { - hasWarnedRef.current = false; // reset the warning for new selection - prevSelectedRef.current = selectedFeatures; - enrichWithPimsFeatures(selectedFeatures); - } - }, [selectedFeatures, enrichWithPimsFeatures]); - - // Get FormLeaseProperties with addresses for all selected features - const { featuresWithAddresses, bcaLoading } = useFeatureDatasetsWithAddresses(datasets); + // Get PropertyForms with addresses for all selected features + const { locationFeaturesWithAddresses: featuresWithAddresses, bcaLoading } = + useLocationFeatureDatasetsWithAddresses(locationFeaturesForAddition ?? []); // Convert SelectedFeatureDataset to FormLeaseProperty const propertyForms = useMemo( @@ -320,8 +291,8 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< const initialValues = LeaseFormModel.fromApi(lease); const handleAddToSelection = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); - }, [prepareForCreation, selectedFeatureDataset]); + prepareForCreation([mapLocationFeatureDataset]); + }, [prepareForCreation, mapLocationFeatureDataset]); useEffect(() => { // Set the map state machine to edit properties mode so that the map selector knows what mode it is in. @@ -333,7 +304,7 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< return ( <> - + - {exists(selectedFeatureDataset?.parcelFeature) && ( + {!isEmptyOrNull(mapLocationFeatureDataset?.parcelFeatures) && ( @@ -385,20 +356,83 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< } > + { + if ( + isRepositioning && + isNumber(repositionPropertyIndex) && + repositionPropertyIndex >= 0 && + !hasMultipleProperties + ) { + // As long as the marker is repositioned within the boundary of the originally selected property simply reposition the marker without further notification. + const formProperty = + formikRef?.current?.values?.properties[repositionPropertyIndex]; + + if ( + isLatLngInFeatureSetBoundary( + locationDataSet.location, + formProperty.property.toLocationFeatureDataset(), + ) + ) { + const updatedFormProperty = + FormLeaseProperty.fromFormLeaseProperty(formProperty); + updatedFormProperty.property.fileLocation = locationDataSet.location; + + // Find property within formik values and reposition it based on incoming file marker position + arrayHelpers.replace(repositionPropertyIndex, updatedFormProperty); + + // Reset the reposition state + finishReposition(); + setRepositionPropertyIndex(null); + } + } else { + toast.warn( + 'Please choose a location that is within the (highlighted) boundary of this property.', + ); + } + }} + /> {formikProps.values.properties.map((leaseProperty, index) => { const property = leaseProperty?.property; - if (property !== undefined) { + if (exists(property)) { + const nameSpace = `properties.${index}`; return ( - onRemoveClick(index)} - nameSpace={`properties.${index}`} - index={index} - property={property.toFeatureDataset()} - showSeparator={index < formikProps.values.properties.length - 1} - /> + <> + onRemoveClick(index)} + canReposition={false} + nameSpace={`${nameSpace}.property`} + index={index} + property={property} + showDisable={false} + canUploadShapefile={false} + /> + + + { + formikProps.setFieldValue( + withNameSpace(nameSpace, 'landArea'), + landArea, + ); + formikProps.setFieldValue( + withNameSpace(nameSpace, 'areaUnitTypeCode'), + areaUnitTypeCode, + ); + }} + /> + + + ); } return <>; diff --git a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.test.tsx b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.test.tsx index a9210b8343..c4bdcf5d9d 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.test.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.test.tsx @@ -3,7 +3,7 @@ import { createMemoryHistory } from 'history'; import noop from 'lodash/noop'; import { PropertyForm } from '@/features/mapSideBar/shared/models'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; +import { getMockSelectedFeatureDataset } from '@/mocks/getMockSelectedFeatureDataset'; import { mockLookups } from '@/mocks/lookups.mock'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; @@ -30,7 +30,7 @@ describe('SelectedPropertyRow component', () => { initialValues={{ properties: [ exists(renderOptions.props?.property) - ? PropertyForm.fromFeatureDataset(renderOptions.props?.property) + ? PropertyForm.fromLocationFeatureDataset(renderOptions.props?.property) : new PropertyForm(), ], }} @@ -38,7 +38,7 @@ describe('SelectedPropertyRow component', () => { {formikProps => ( void; - property: SelectedFeatureDataset; + property: PropertyForm; formikProps: FormikProps; showSeparator?: boolean; } @@ -32,7 +32,7 @@ export const SelectedPropertyRow: React.FunctionComponent { const mapMachine = useMapStateMachine(); - const propertyName = getPropertyNameFromSelectedFeatureSet(property); + const propertyName = getPropertyNameFromForm(property); let propertyIdentifier = ''; switch (propertyName.label) { case NameSourceType.PID: @@ -77,14 +77,14 @@ export const SelectedPropertyRow: React.FunctionComponent { - mapMachine.startReposition(property, index); + mapMachine.startReposition(property.toFeature()); }} > - + = ( {file && ( - = ( } canRemove={canRemove} canUploadShapefiles={true} + canReposition={true} formikRef={formikRef} /> )} diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx deleted file mode 100644 index 1c67378533..0000000000 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Formik } from 'formik'; -import noop from 'lodash/noop'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; -import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; -import { act, render, RenderOptions, userEvent } from '@/utils/test-utils'; - -import { PropertyForm } from '../../shared/models'; -import { AcquisitionPropertiesSubForm } from './AcquisitionPropertiesSubForm'; -import { AcquisitionForm } from './models'; - -const mockStore = configureMockStore([thunk]); - -const customSetFilePropertyLocations = vi.fn(); - -describe('AcquisitionProperties component', () => { - // render component under test - const setup = async ( - props: { - initialForm: AcquisitionForm; - confirmBeforeAdd?: (propertyForm: PropertyForm) => Promise; - }, - renderOptions: RenderOptions = {}, - ) => { - const utils = render( - <> - - {formikProps => ( - - )} - - , - { - ...renderOptions, - store: mockStore({}), - claims: [], - mockMapMachine: { - ...mapMachineBaseMock, - setFilePropertyLocations: customSetFilePropertyLocations, - }, - }, - ); - - // Wait for any async effects to complete - await act(async () => {}); - - return { ...utils }; - }; - - let testForm: AcquisitionForm; - - beforeEach(() => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - testForm = new AcquisitionForm(); - testForm.fileName = 'Test name'; - testForm.properties = [ - PropertyForm.fromFeatureDataset({ - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '123-456-789', - }, - }, - }), - PropertyForm.fromFeatureDataset({ - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PIN: 1111222, - }, - }, - }), - ]; - }); - - afterEach(() => { - vi.clearAllMocks(); - customSetFilePropertyLocations.mockReset(); - }); - - it('renders as expected', async () => { - const { asFragment } = await setup({ initialForm: testForm }); - await act(async () => {}); - expect(asFragment()).toMatchSnapshot(); - }); - - it('renders list of properties', async () => { - const { getByText } = await setup({ initialForm: testForm }); - - expect(getByText('PID: 123-456-789')).toBeVisible(); - expect(getByText('PIN: 1111222')).toBeVisible(); - }); - - it('should remove property from list when Remove button is clicked', async () => { - const { getAllByTitle, queryByText } = await setup({ initialForm: testForm }); - const pidRow = getAllByTitle('remove')[0]; - await act(async () => userEvent.click(pidRow)); - - expect(queryByText('PID: 123-456-789')).toBeNull(); - }); - - it('should display properties with svg prefixed with incrementing id', async () => { - const { getByTitle } = await setup({ initialForm: testForm }); - expect(getByTitle('1')).toBeInTheDocument(); - expect(getByTitle('2')).toBeInTheDocument(); - }); -}); diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.tsx deleted file mode 100644 index b990a551f3..0000000000 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { FieldArray, FormikProps } from 'formik'; -import noop from 'lodash/noop'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; -import styled from 'styled-components'; - -import { Button } from '@/components/common/buttons'; -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { Section } from '@/components/common/Section/Section'; -import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; -import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; -import { UploadResponseModel } from '@/features/properties/shapeUpload/models'; -import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; -import { useFeatureDatasetsWithAddresses } from '@/hooks/useFeatureDatasetsWithAddresses'; -import { exists, featuresetToLocationBoundaryDataset, firstOrNull } from '@/utils'; -import { addPropertiesToCurrentFile } from '@/utils/propertyUtils'; - -import { PropertyForm } from '../../shared/models'; -import AddPropertiesGuide from '../../shared/update/properties/AddPropertiesGuide'; -import { AcquisitionForm } from './models'; - -export interface IAcquisitionPropertiesProps { - formikProps: FormikProps; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; -} - -export const AcquisitionPropertiesSubForm: React.FunctionComponent = ({ - formikProps, -}) => { - const localRef = useRef>(); - - const { selectedFeatures, processCreation, mapLocationFeatureDataset, prepareForCreation } = - useMapStateMachine(); - - useDraftMarkerSynchronizer( - formikProps.values.properties.map(p => - featuresetToLocationBoundaryDataset(p.toFeatureDataset()), - ), - ); - - const selectedFeatureDataset = useMemo(() => { - return { - selectingComponentId: mapLocationFeatureDataset?.selectingComponentId ?? null, - location: mapLocationFeatureDataset?.location, - fileLocation: mapLocationFeatureDataset?.fileLocation ?? null, - fileBoundary: null, - parcelFeature: firstOrNull(mapLocationFeatureDataset?.parcelFeatures), - pimsFeature: firstOrNull(mapLocationFeatureDataset?.pimsFeatures), - regionFeature: mapLocationFeatureDataset?.regionFeature ?? null, - districtFeature: mapLocationFeatureDataset?.districtFeature ?? null, - municipalityFeature: firstOrNull(mapLocationFeatureDataset?.municipalityFeatures), - isActive: true, - displayOrder: 0, - }; - }, [ - mapLocationFeatureDataset?.selectingComponentId, - mapLocationFeatureDataset?.location, - mapLocationFeatureDataset?.fileLocation, - mapLocationFeatureDataset?.parcelFeatures, - mapLocationFeatureDataset?.pimsFeatures, - mapLocationFeatureDataset?.regionFeature, - mapLocationFeatureDataset?.districtFeature, - mapLocationFeatureDataset?.municipalityFeatures, - ]); - - // Get PropertyForms with addresses for all selected features - const { featuresWithAddresses } = useFeatureDatasetsWithAddresses(selectedFeatures ?? []); - - // Convert SelectedFeatureDataset to PropertyForm - const propertyForms = useMemo( - () => - featuresWithAddresses.map(obj => { - const property = PropertyForm.fromFeatureDataset(obj.feature); - if (exists(obj.address)) { - property.address = obj.address; - } - return property; - }), - [featuresWithAddresses], - ); - - const handleAddToSelection = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); - }, [prepareForCreation, selectedFeatureDataset]); - - useEffect(() => { - if (exists(localRef.current) && propertyForms.length > 0) { - addPropertiesToCurrentFile(localRef, 'properties', propertyForms, noop); - processCreation(); - } - }, [localRef, processCreation, propertyForms]); - - return ( - -
- Select one or more properties that you want to include in this acquisition. You can choose a - location from the map, or search by other criteria. -
- - - {({ remove, replace }) => ( -
- - {exists(selectedFeatureDataset?.parcelFeature) && ( - - - - )} - - - {formikProps.values.properties.map((property, index) => ( - remove(index)} - nameSpace={`properties.${index}`} - index={index} - property={property.toFeatureDataset()} - canUploadShapefile={true} - onUploadShapefile={(result: UploadResponseModel | null) => { - // Update the property boundary based on the uploaded shapefile - if (exists(result)) { - if (result.isSuccess && exists(result.boundary)) { - const updatedFormProperty = new PropertyForm(property); - updatedFormProperty.fileBoundary = result.boundary; - replace(index, updatedFormProperty); - } - } - }} - /> - ))} - {formikProps.values.properties.length === 0 && No Properties selected} -
- )} -
-
- ); -}; - -export default AcquisitionPropertiesSubForm; - -const StyledComponentWrapper = styled.div` - margin: 0 1.6rem 0 1.6rem; - padding: 0 1.6rem 0 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; - -const StyledButtonWrapper = styled.div` - margin: 0 1.6rem; - padding-left: 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.test.tsx index 28760e19e2..5eef6a73cf 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.test.tsx @@ -320,7 +320,7 @@ describe('AddAcquisitionContainer component', () => { let testObj: any = undefined; const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, - processCreation: vi.fn(), + processLocationFeaturesAddition: vi.fn(), refreshMapProperties: vi.fn(), }; @@ -350,7 +350,7 @@ describe('AddAcquisitionContainer component', () => { const expectedValues = formValues.toApi(); expect(addAcquisitionFileApi.execute).toHaveBeenCalledWith(expectedValues, []); expect(onSuccess).toHaveBeenCalledWith(1); - expect(testMockMachine.processCreation).toHaveBeenCalled(); + expect(testMockMachine.processLocationFeaturesAddition).toHaveBeenCalled(); expect(testMockMachine.refreshMapProperties).toHaveBeenCalled(); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx index bbea929c89..6aa0f106b3 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx @@ -13,8 +13,8 @@ import { useAcquisitionProvider } from '@/hooks/repositories/useAcquisitionProvi import { usePropertyAssociations } from '@/hooks/repositories/usePropertyAssociations'; import { useQuery } from '@/hooks/use-query'; import useApiUserOverride from '@/hooks/useApiUserOverride'; -import { useEditPropertiesNotifier } from '@/hooks/useEditPropertiesNotifier'; import { useModalContext } from '@/hooks/useModalContext'; +import { usePropertyFormSyncronizer } from '@/hooks/usePropertyFormSyncronizer'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_AcquisitionFile } from '@/models/api/generated/ApiGen_Concepts_AcquisitionFile'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; @@ -65,14 +65,14 @@ export const AddAcquisitionContainer: React.FC = const mapMachine = useMapStateMachine(); - const { featuresWithAddresses, bcaLoading } = useEditPropertiesNotifier(formikRef, 'properties'); + const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); useEffect(() => { if (featuresWithAddresses?.length > 0 && !isSubFile && !formikRef?.current?.values?.region) { const firstPropertyFeature = firstOrNull(featuresWithAddresses)?.feature; if (exists(firstPropertyFeature)) { - const firstProperty = PropertyForm.fromFeatureDataset(firstPropertyFeature); + const firstProperty = PropertyForm.fromLocationFeatureDataset(firstPropertyFeature); formikRef?.current?.setFieldValue( 'region', firstProperty.regionName !== 'Cannot determine' @@ -131,7 +131,7 @@ export const AddAcquisitionContainer: React.FC = // This is the flow for Map Marker -> right-click -> create Acquisition File useEffect(() => { const runAsync = async () => { - if (exists(initialForm) && exists(formikRef.current) && needsUserConfirmation) { + if (exists(initialForm) && exists(formikRef?.current) && needsUserConfirmation) { if (initialForm.properties.length > 0) { // Check all properties for confirmation const needsConfirmation = await Promise.all( @@ -214,12 +214,12 @@ export const AddAcquisitionContainer: React.FC = handleSuccess(response); } } finally { - mapMachine.processCreation(); + mapMachine.processLocationFeaturesAddition(); formikHelpers?.setSubmitting(false); } }; - const loading = addAcquisitionFileLoading || bcaLoading; + const loading = addAcquisitionFileLoading || isLoading; return ( - {formikProps => { - return ( - - ); - }} + {formikProps => ( + + )} ); }; @@ -134,9 +132,7 @@ const AddAcquisitionDetailSubForm: React.FC<{ ApiGen_Concepts_Product[] | undefined >(undefined); - const { values, setFieldValue } = formikProps; - - const ownerSolicitorContact = values?.ownerSolicitor.contact; + const ownerSolicitorContact = formikProps.values.ownerSolicitor.contact; const { retrieveProjectProducts } = useProjectProvider(); const { getOptionsByType, getByType } = useLookupCodeHelpers(); @@ -186,12 +182,12 @@ const AddAcquisitionDetailSubForm: React.FC<{ React.useEffect(() => { if (orgPersons?.length === 0) { - setFieldValue('ownerSolicitor.primaryContactId', null); + formikProps.setFieldValue('ownerSolicitor.primaryContactId', null); } if (orgPersons?.length === 1) { - setFieldValue('ownerSolicitor.primaryContactId', orgPersons[0].personId); + formikProps.setFieldValue('ownerSolicitor.primaryContactId', orgPersons[0].personId); } - }, [orgPersons, setFieldValue]); + }, [formikProps, formikProps.setFieldValue, orgPersons]); React.useEffect(() => { if (ownerSolicitorContact?.organizationId) { @@ -209,13 +205,13 @@ const AddAcquisitionDetailSubForm: React.FC<{ label="Ministry project" tooltip="Sub-file has the same project as the main file and it can only be updated from the main file" > - {values?.formattedProject ?? ''} + {formikProps.values?.formattedProject ?? ''} - {values?.formattedProduct ?? ''} + {formikProps.values?.formattedProduct ?? ''} ) : ( @@ -254,12 +250,12 @@ const AddAcquisitionDetailSubForm: React.FC<{ .call(e.target.selectedOptions) .map((option: HTMLOptionElement & number) => option.value)[0]; if (isValidString(selectedValue) && selectedValue !== 'OTHER') { - setFieldValue('fundingTypeOtherDescription', ''); + formikProps.setFieldValue('fundingTypeOtherDescription', ''); } }} /> - {values?.fundingTypeCode === 'OTHER' && ( + {formikProps.values?.fundingTypeCode === 'OTHER' && ( @@ -336,9 +332,11 @@ const AddAcquisitionDetailSubForm: React.FC<{ : 'Properties to include in this file:' } > - verifyCallback()} + needsConfirmationBeforeAdd={confirmBeforeAdd} + canUploadShapefiles={true} /> @@ -390,7 +388,7 @@ const AddAcquisitionDetailSubForm: React.FC<{ ) { formikProps.setFieldValue('otherSubfileInterestType', null); } else { - formikProps.setFieldValue('otherSubfileInterestType', ''); + formikProps?.setFieldValue('otherSubfileInterestType', ''); } }} required @@ -399,7 +397,8 @@ const AddAcquisitionDetailSubForm: React.FC<{ )} {isSubFile && - values?.subfileInterestTypeCode === ApiGen_CodeTypes_SubfileInterestTypes.OTHER && ( + formikProps.values?.subfileInterestTypeCode === + ApiGen_CodeTypes_SubfileInterestTypes.OTHER && ( { : pimsProperty?.planNumber, id: pimsProperty?.id ?? 0, pimsProperty: pimsProperty, - parcelMapFeatureCollection: { - features: [ + featureDataset: { + ...emptyFeatureDataset(), + parcelFeatures: [ { type: 'Feature', properties: fullyAttributed, geometry: undefined, }, ], - type: 'FeatureCollection', }, - crownTenureFeatures: [], - crownLeaseFeatures: [], - crownLicenseFeatures: [], - crownInclusionFeatures: [], - crownInventoryFeatures: [], - highwayFeatures: [], - municipalityFeatures: [], ltsaOrders: undefined, spcpOrder: undefined, propertyAssociations: undefined, - pimsGeoserverFeatureCollection: undefined, bcAssessmentSummary: undefined, - firstNationFeatures: undefined, - alrFeatures: undefined, - electoralFeatures: undefined, }; return composed; }); diff --git a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx index ea5fcbce55..0414790db5 100644 --- a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx +++ b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx @@ -36,7 +36,7 @@ const AddConsolidationContainer: React.FC = ({ ); const formikRef = useRef>(null); const mapMachine = useMapStateMachine(); - const selectedFeatureDataset = firstOrNull(mapMachine.selectedFeatures); + const selectedFeatureDataset = mapMachine.mapLocationFeatureDataset; const { setModalContent, setDisplayModal } = useModalContext(); const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); @@ -61,16 +61,16 @@ const AddConsolidationContainer: React.FC = ({ async function loadInitialProperty() { if (selectedFeatureDataset !== null) { - const propertyForm = PropertyForm.fromFeatureDataset(selectedFeatureDataset); + const propertyForm = PropertyForm.fromLocationFeatureDataset(selectedFeatureDataset); if (isValidString(propertyForm.pid)) { - const pimsFeature = selectedFeatureDataset.pimsFeature; + const pimsFeature = firstOrNull(selectedFeatureDataset.pimsFeatures); propertyForm.address = pimsFeature?.properties ? AddressForm.fromPimsView(pimsFeature?.properties) : undefined; // TODO: Remove this once the conversion is cleaner propertyForm.isOwned = pimsFeature?.properties.IS_OWNED; const consolidationFormModel = new ConsolidationFormModel(); - consolidationFormModel.sourceProperties = [propertyForm.toApi()]; + consolidationFormModel.sourceProperties = [propertyForm]; setInitialForm(consolidationFormModel); } } @@ -141,7 +141,7 @@ const AddConsolidationContainer: React.FC = ({ handleSuccess(response); } } finally { - mapMachine.processCreation(); + mapMachine.processLocationFeaturesAddition(); formikHelpers?.setSubmitting(false); } }; diff --git a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationMarkerSynchronizer.tsx b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationMarkerSynchronizer.tsx index 544f2f19ba..5b266f8cbd 100644 --- a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationMarkerSynchronizer.tsx +++ b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationMarkerSynchronizer.tsx @@ -1,5 +1,4 @@ import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; -import { propertyToLocationBoundaryDataset } from '@/utils/mapPropertyUtils'; import { ConsolidationFormModel } from './AddConsolidationModel'; @@ -10,12 +9,7 @@ interface IAddConsolidationMarkerSynchronizerProps { const AddConsolidationMarkerSynchronizer: React.FunctionComponent< IAddConsolidationMarkerSynchronizerProps > = ({ values }) => { - useDraftMarkerSynchronizer([ - ...values.sourceProperties.map(dp => propertyToLocationBoundaryDataset(dp)), - ...(values.destinationProperty - ? [propertyToLocationBoundaryDataset(values.destinationProperty)] - : []), - ]); + useDraftMarkerSynchronizer([...values.sourceProperties, ...[values.destinationProperty]]); return null; }; diff --git a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationModel.ts b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationModel.ts index ed86b36c03..d15c7fc9ff 100644 --- a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationModel.ts +++ b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationModel.ts @@ -1,13 +1,14 @@ import { ApiGen_Base_CodeType } from '@/models/api/generated/ApiGen_Base_CodeType'; import { ApiGen_CodeTypes_PropertyOperationTypes } from '@/models/api/generated/ApiGen_CodeTypes_PropertyOperationTypes'; -import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { ApiGen_Concepts_PropertyOperation } from '@/models/api/generated/ApiGen_Concepts_PropertyOperation'; import { getEmptyBaseAudit } from '@/models/defaultInitializers'; import { exists } from '@/utils'; +import { PropertyForm } from '../shared/models'; + export class ConsolidationFormModel { - sourceProperties: ApiGen_Concepts_Property[] = []; - destinationProperty: ApiGen_Concepts_Property | null = null; + sourceProperties: PropertyForm[] = []; + destinationProperty: PropertyForm | null = null; propertyOperationNo: number | null = null; propertyOperationTypeCode: ApiGen_Base_CodeType | null = { id: ApiGen_CodeTypes_PropertyOperationTypes.CONSOLIDATE, @@ -19,12 +20,12 @@ export class ConsolidationFormModel { pid = ''; toApi(): ApiGen_Concepts_PropertyOperation[] { - return this.sourceProperties?.map(sp => ({ + return this.sourceProperties?.map(sp => ({ ...getEmptyBaseAudit(0), id: 0, destinationPropertyId: this.destinationProperty?.id ?? 0, - destinationProperty: this.destinationProperty, - sourceProperty: sp, + destinationProperty: this.destinationProperty.toApi(), + sourceProperty: sp.toApi(), sourcePropertyId: sp?.id ?? 0, operationDt: null, isDisabled: false, @@ -42,8 +43,11 @@ export class ConsolidationFormModel { subdivisionForm.propertyOperationNo = null; subdivisionForm.propertyOperationTypeCode = null; } - subdivisionForm.destinationProperty = operations[0].destinationProperty; - subdivisionForm.sourceProperties = operations.map(op => op.sourceProperty).filter(exists) ?? []; + subdivisionForm.destinationProperty = PropertyForm.fromPropertyApi( + operations[0].destinationProperty, + ); + subdivisionForm.sourceProperties = + operations.map(op => PropertyForm.fromPropertyApi(op.sourceProperty)).filter(exists) ?? []; subdivisionForm.propertyOperationNo = operations[0].propertyOperationNo; subdivisionForm.propertyOperationTypeCode = operations[0].propertyOperationTypeCode; return subdivisionForm; diff --git a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.test.tsx b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.test.tsx index b1a25db539..ec06b02f27 100644 --- a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.test.tsx +++ b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.test.tsx @@ -5,7 +5,7 @@ import { createRef } from 'react'; import { IMapSelectorContainerProps } from '@/components/propertySelector/MapSelectorContainer'; import { PropertySelectorPidSearchContainerProps } from '@/components/propertySelector/search/PropertySelectorPidSearchContainer'; import Claims from '@/constants/claims'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; +import { getMockSelectedFeatureDataset } from '@/mocks/getMockSelectedFeatureDataset'; import { mockLookups } from '@/mocks/lookups.mock'; import { getMockApiProperty } from '@/mocks/properties.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes/lookupCodesSlice'; diff --git a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx index 0534491c7b..ceb34f6e90 100644 --- a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx +++ b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx @@ -1,5 +1,4 @@ import { FieldArray, Formik, FormikHelpers, FormikProps } from 'formik'; -import noop from 'lodash/noop'; import { useCallback } from 'react'; import { Tab } from 'react-bootstrap'; import { FaInfoCircle } from 'react-icons/fa'; @@ -20,7 +19,6 @@ import { StyledTabView } from '@/components/propertySelector/PropertySelectorTab import { PropertySelectorPidSearchContainerProps } from '@/components/propertySelector/search/PropertySelectorPidSearchContainer'; import PropertySearchSelectorPidFormView from '@/components/propertySelector/search/PropertySelectorPidSearchView'; import { ApiGen_CodeTypes_AreaUnitTypes } from '@/models/api/generated/ApiGen_CodeTypes_AreaUnitTypes'; -import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { convertArea, exists } from '@/utils'; import MapSideBarLayout from '../layout/MapSideBarLayout'; @@ -156,10 +154,10 @@ const AddConsolidationView: React.FunctionComponent<

Select the child property to which parent properties were consolidated:

{ - const allProperties: ApiGen_Concepts_Property[] = []; + const allProperties: PropertyForm[] = []; await properties.reduce(async (promise, property) => { return promise.then(async () => { - const formProperty = PropertyForm.fromFeatureDataset(property); + const formProperty = PropertyForm.fromLocationFeatureDataset(property); formProperty.landArea = formProperty.landArea && formProperty.areaUnit ? convertArea( @@ -171,7 +169,7 @@ const AddConsolidationView: React.FunctionComponent< formProperty.areaUnit = ApiGen_CodeTypes_AreaUnitTypes.M2; if (formProperty.pid) { formProperty.address = await getPrimaryAddressByPid(formProperty.pid); - allProperties.push(formProperty.toApi()); + allProperties.push(formProperty); } else { toast.error('Selected property must have a PID'); } @@ -179,9 +177,7 @@ const AddConsolidationView: React.FunctionComponent< }, Promise.resolve()); setFieldValue('destinationProperty', allProperties[0]); }} - selectedComponentId="destination-property-selector" modifiedProperties={[]} - repositionSelectedProperty={noop} />
@@ -208,10 +204,7 @@ const AddConsolidationView: React.FunctionComponent< ); }; -const getDraftMarkerIndex = ( - property: ApiGen_Concepts_Property, - form: ConsolidationFormModel, -): number => { +const getDraftMarkerIndex = (property: PropertyForm, form: ConsolidationFormModel): number => { const index = form.sourceProperties.findIndex( p => p.latitude === property.latitude && diff --git a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx index 1bba3b1eff..95c3ec27ca 100644 --- a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx +++ b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx @@ -6,7 +6,7 @@ import { Api_LastUpdatedBy } from '@/models/api/File'; import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTypes_FileTypes'; import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { ApiGen_Concepts_Project } from '@/models/api/generated/ApiGen_Concepts_Project'; -import { exists, filePropertyToLocationBoundaryDataset } from '@/utils'; +import { exists } from '@/utils'; export interface TypedFile extends ApiGen_Concepts_File { fileType: ApiGen_CodeTypes_FileTypes; @@ -120,9 +120,7 @@ export const SideBarContextProvider = (props: ISideBarContextProviderProps) => { const resetFilePropertyLocations = useCallback(() => { if (exists(fileProperties)) { - const propertyLocations = fileProperties - .map(x => filePropertyToLocationBoundaryDataset(x)) - .filter(exists); + const propertyLocations = fileProperties.filter(exists); exists(setFilePropertyLocations) && setFilePropertyLocations(propertyLocations); } else { diff --git a/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx b/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx index 5ea834553c..bc2414022f 100644 --- a/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx @@ -31,7 +31,7 @@ import FileMenuView from '../shared/FileMenuView'; import { PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; -import UpdateProperties from '../shared/update/properties/UpdateProperties'; +import UpdatePropertiesContainer from '../shared/update/properties/UpdatePropertiesContainer'; import { DispositionHeader } from './common/DispositionHeader'; import DispositionRouter from './router/DispositionRouter'; import DispositionStatusUpdateSolver from './tabs/fileDetails/detail/DispositionStatusUpdateSolver'; @@ -109,7 +109,7 @@ export const DispositionView: React.FunctionComponent = ( {dispositionFile && ( - { it('calls onSuccess when the Disposition is saved successfully', async () => { const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, - processCreation: vi.fn(), + processLocationFeaturesAddition: vi.fn(), refreshMapProperties: vi.fn(), }; const formikHelpers: Partial> = { @@ -115,7 +115,7 @@ describe('Add Disposition Container component', () => { }); expect(onSuccess).toHaveBeenCalled(); - expect(testMockMachine.processCreation).toHaveBeenCalled(); + expect(testMockMachine.processLocationFeaturesAddition).toHaveBeenCalled(); expect(testMockMachine.refreshMapProperties).toHaveBeenCalled(); }); @@ -178,7 +178,7 @@ describe('Add Disposition Container component', () => { await act(async () => { const model = new DispositionFormModel(); - model.fileProperties = selectedFeatures.map(sf => PropertyForm.fromFeatureDataset(sf)); + model.fileProperties = selectedFeatures.map(sf => PropertyForm.fromLocationFeatureDataset(sf)); viewProps?.onSubmit(model, formikHelpers as FormikHelpers); }); diff --git a/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx b/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx index 3f1e511265..e77098cca0 100644 --- a/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx @@ -6,8 +6,8 @@ import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineCo import { useDispositionProvider } from '@/hooks/repositories/useDispositionProvider'; import { usePropertyAssociations } from '@/hooks/repositories/usePropertyAssociations'; import useApiUserOverride from '@/hooks/useApiUserOverride'; -import { useEditPropertiesNotifier } from '@/hooks/useEditPropertiesNotifier'; import { useModalContext } from '@/hooks/useModalContext'; +import { usePropertyFormSyncronizer } from '@/hooks/usePropertyFormSyncronizer'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_DispositionFile } from '@/models/api/generated/ApiGen_Concepts_DispositionFile'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; @@ -56,7 +56,7 @@ const AddDispositionContainer: React.FC = ({ ); const mapMachine = useMapStateMachine(); - const { featuresWithAddresses, bcaLoading } = useEditPropertiesNotifier( + const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer( formikRef, 'fileProperties', ); @@ -66,7 +66,7 @@ const AddDispositionContainer: React.FC = ({ const firstPropertyFeature = firstOrNull(featuresWithAddresses)?.feature; if (exists(firstPropertyFeature)) { - const firstProperty = PropertyForm.fromFeatureDataset(firstPropertyFeature); + const firstProperty = PropertyForm.fromLocationFeatureDataset(firstPropertyFeature); formikRef?.current?.setFieldValue( 'regionCode', firstProperty.regionName !== 'Cannot determine' ? firstProperty.region : undefined, @@ -84,7 +84,7 @@ const AddDispositionContainer: React.FC = ({ useEffect(() => { const runAsync = async () => { const incomingProperties = - featuresWithAddresses?.map(f => PropertyForm.fromFeatureDataset(f.feature)) ?? []; + featuresWithAddresses?.map(f => PropertyForm.fromLocationFeatureDataset(f.feature)) ?? []; if (exists(incomingProperties) && exists(formikRef.current) && needsUserConfirmation) { if (incomingProperties.length > 0) { // Check all properties for confirmation @@ -177,7 +177,6 @@ const AddDispositionContainer: React.FC = ({ handleSuccess(response); } } finally { - mapMachine.processCreation(); formikHelpers?.setSubmitting(false); } }; @@ -186,7 +185,7 @@ const AddDispositionContainer: React.FC = ({ = props => {
- verifyCallback()} + needsConfirmationBeforeAdd={confirmBeforeAdd} />
diff --git a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx b/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx deleted file mode 100644 index 94e35c2633..0000000000 --- a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { FieldArray, FormikProps } from 'formik'; -import { noop } from 'lodash'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { Col, Row } from 'react-bootstrap'; -import styled from 'styled-components'; - -import { Button } from '@/components/common/buttons'; -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { Section } from '@/components/common/Section/Section'; -import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; -import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; -import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; -import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; -import { useFeatureDatasetsWithAddresses } from '@/hooks/useFeatureDatasetsWithAddresses'; -import { exists, featuresetToLocationBoundaryDataset, firstOrNull } from '@/utils'; -import { addPropertiesToCurrentFile } from '@/utils/propertyUtils'; - -import { PropertyForm } from '../../shared/models'; -import AddPropertiesGuide from '../../shared/update/properties/AddPropertiesGuide'; -import { DispositionFormModel } from '../models/DispositionFormModel'; - -export interface DispositionPropertiesSubFormProps { - formikProps: FormikProps; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; -} - -const DispositionPropertiesSubForm: React.FunctionComponent = ({ - formikProps, -}) => { - const localRef = useRef>(); - - const { selectedFeatures, processCreation, mapLocationFeatureDataset, prepareForCreation } = - useMapStateMachine(); - - useDraftMarkerSynchronizer( - formikProps.values.fileProperties.map(p => - featuresetToLocationBoundaryDataset(p.toFeatureDataset()), - ), - ); - - const selectedFeatureDataset = useMemo(() => { - return { - selectingComponentId: mapLocationFeatureDataset?.selectingComponentId ?? null, - location: mapLocationFeatureDataset?.location, - fileLocation: mapLocationFeatureDataset?.fileLocation ?? null, - fileBoundary: null, - parcelFeature: firstOrNull(mapLocationFeatureDataset?.parcelFeatures), - pimsFeature: firstOrNull(mapLocationFeatureDataset?.pimsFeatures), - regionFeature: mapLocationFeatureDataset?.regionFeature ?? null, - districtFeature: mapLocationFeatureDataset?.districtFeature ?? null, - municipalityFeature: firstOrNull(mapLocationFeatureDataset?.municipalityFeatures), - isActive: true, - displayOrder: 0, - }; - }, [ - mapLocationFeatureDataset?.selectingComponentId, - mapLocationFeatureDataset?.location, - mapLocationFeatureDataset?.fileLocation, - mapLocationFeatureDataset?.parcelFeatures, - mapLocationFeatureDataset?.pimsFeatures, - mapLocationFeatureDataset?.regionFeature, - mapLocationFeatureDataset?.districtFeature, - mapLocationFeatureDataset?.municipalityFeatures, - ]); - - // Get PropertyForms with addresses for all selected features - const { featuresWithAddresses } = useFeatureDatasetsWithAddresses(selectedFeatures ?? []); - - // Convert SelectedFeatureDataset to PropertyForm - const propertyForms = useMemo( - () => - featuresWithAddresses.map(obj => { - const property = PropertyForm.fromFeatureDataset(obj.feature); - if (exists(obj.address)) { - property.address = obj.address; - } - return property; - }), - [featuresWithAddresses], - ); - - const handleAddToSelection = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); - }, [prepareForCreation, selectedFeatureDataset]); - - useEffect(() => { - if (exists(localRef.current) && propertyForms.length > 0) { - addPropertiesToCurrentFile(localRef, 'fileProperties', propertyForms, noop); - processCreation(); - } - }, [localRef, processCreation, propertyForms]); - - return ( - -
- Select one or more properties that you want to include in this disposition. You can choose a - location from the map, or search by other criteria. -
- - - {({ remove }) => ( -
- Selected Properties - - - - - } - > - - {exists(selectedFeatureDataset?.parcelFeature) && ( - - - - )} - - - {formikProps.values.fileProperties.map((property, index) => ( - remove(index)} - nameSpace={`fileProperties.${index}`} - index={index} - property={property.toFeatureDataset()} - /> - ))} - {formikProps.values.fileProperties.length === 0 && No Properties selected} -
- )} -
-
- ); -}; - -export default DispositionPropertiesSubForm; - -const StyledComponentWrapper = styled.div` - margin: 0 1.6rem 0 1.6rem; - padding: 0 1.6rem 0 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; - -const StyledButtonWrapper = styled.div` - margin: 0 1.6rem; - padding-left: 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; diff --git a/source/frontend/src/features/mapSideBar/layer/LayerTabContainer.tsx b/source/frontend/src/features/mapSideBar/layer/LayerTabContainer.tsx index 59b5dfb328..858da81e9d 100644 --- a/source/frontend/src/features/mapSideBar/layer/LayerTabContainer.tsx +++ b/source/frontend/src/features/mapSideBar/layer/LayerTabContainer.tsx @@ -88,29 +88,33 @@ export const LayerTabContainer: React.FC { - if (!composedProperty) return []; + if (!exists(composedProperty?.featureDataset)) { + return []; + } + + const featureDataset = composedProperty?.featureDataset; const crownFeaturesTotal = - (composedProperty.crownTenureFeatures?.length || 0) + - (composedProperty.crownLeaseFeatures?.length || 0) + - (composedProperty.crownInclusionFeatures?.length || 0) + - (composedProperty.crownInventoryFeatures?.length || 0) + - (composedProperty.crownLicenseFeatures?.length || 0); + (featureDataset.crownLandTenuresFeatures?.length || 0) + + (featureDataset.crownLandLeasesFeatures?.length || 0) + + (featureDataset.crownLandInclusionsFeatures?.length || 0) + + (featureDataset.crownLandInventoryFeatures?.length || 0) + + (featureDataset.crownLandLicensesFeatures?.length || 0); return [ ...buildLayerData({ - features: composedProperty.pimsGeoserverFeatureCollection?.features, + features: featureDataset?.pimsFeatures, titlePrefix: 'PIMS data', tab: InventoryTabNames.pims, config: pimsLayerConfig, }), ...buildLayerData({ - features: composedProperty.parcelMapFeatureCollection?.features, + features: featureDataset?.parcelFeatures, titlePrefix: 'LTSA ParcelMap data', tab: InventoryTabNames.pmbc, config: parcelLayerConfig, }), ...buildLayerData({ - features: composedProperty.crownTenureFeatures, + features: featureDataset?.crownLandTenuresFeatures, titlePrefix: 'Crown Land Tenures', tab: InventoryTabNames.crown, config: {}, @@ -118,7 +122,7 @@ export const LayerTabContainer: React.FC = ({ leaseId, onClos const lease: ApiGen_Concepts_Lease | null = isLeaseFile(file) ? file : null; - const locations: LocationBoundaryDataset[] = useMemo(() => { + const fileProperties: ApiGen_Concepts_FileProperty[] = useMemo(() => { if (exists(lease?.fileProperties)) { - return lease?.fileProperties - .map(leaseProp => filePropertyToLocationBoundaryDataset(leaseProp)) - .filter(exists); + return lease?.fileProperties.filter(exists); } else { return []; } @@ -328,8 +326,8 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos }, [fetchLastUpdatedBy, lastUpdatedBy, leaseId, staleLastUpdatedBy]); useEffect(() => { - setFilePropertyLocations(locations); - }, [setFilePropertyLocations, locations]); + setFilePropertyLocations(fileProperties); + }, [setFilePropertyLocations, fileProperties]); const onSelectFileSummary = () => { if (!exists(lease)) { diff --git a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx index a0a8b959c4..8792edc029 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx @@ -31,7 +31,7 @@ import FileMenuView from '../shared/FileMenuView'; import { PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; -import UpdateProperties from '../shared/update/properties/UpdateProperties'; +import UpdatePropertiesContainer from '../shared/update/properties/UpdatePropertiesContainer'; import ManagementHeader from './common/ManagementHeader'; import ManagementRouter from './router/ManagementRouter'; import ManagementStatusUpdateSolver from './tabs/fileDetails/detail/ManagementStatusUpdateSolver'; @@ -109,7 +109,7 @@ export const ManagementView: React.FunctionComponent = ({ {managementFile && ( - = ({ confirmBeforeAdd={confirmBeforeAdd} canRemove={canRemove} formikRef={formikRef} - disableProperties + showDisabledProperties confirmBeforeAddMessage={ <>

This property has already been added to one or more management files.

diff --git a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx index 09cd6b9ee5..1a790d85e5 100644 --- a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx @@ -106,7 +106,7 @@ describe('Add Management Container component', () => { it('calls onSuccess when the Management is saved successfully', async () => { const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, - processCreation: vi.fn(), + processLocationFeaturesAddition: vi.fn(), refreshMapProperties: vi.fn(), }; await setup({ mockMapMachine: testMockMachine }); @@ -119,7 +119,7 @@ describe('Add Management Container component', () => { }); expect(onSuccess).toHaveBeenCalled(); - expect(testMockMachine.processCreation).toHaveBeenCalled(); + expect(testMockMachine.processLocationFeaturesAddition).toHaveBeenCalled(); expect(testMockMachine.refreshMapProperties).toHaveBeenCalled(); }); @@ -178,7 +178,7 @@ describe('Add Management Container component', () => { await act(async () => { const model = new ManagementFormModel(); - model.fileProperties = selectedFeatures?.map(sf => PropertyForm.fromFeatureDataset(sf)); + model.fileProperties = selectedFeatures?.map(sf => PropertyForm.fromLocationFeatureDataset(sf)); await viewProps?.onSubmit(model, { setSubmitting: vi.fn(), resetForm: vi.fn(), diff --git a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx index ab6823519b..2817dbbb95 100644 --- a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx @@ -6,8 +6,8 @@ import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineCo import { useManagementFileRepository } from '@/hooks/repositories/useManagementFileRepository'; import { usePropertyAssociations } from '@/hooks/repositories/usePropertyAssociations'; import useApiUserOverride from '@/hooks/useApiUserOverride'; -import { useEditPropertiesNotifier } from '@/hooks/useEditPropertiesNotifier'; import { useModalContext } from '@/hooks/useModalContext'; +import { usePropertyFormSyncronizer } from '@/hooks/usePropertyFormSyncronizer'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; @@ -56,7 +56,7 @@ const AddManagementContainer: React.FC = ({ const mapMachine = useMapStateMachine(); - const { featuresWithAddresses, bcaLoading } = useEditPropertiesNotifier( + const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer( formikRef, 'fileProperties', ); @@ -71,7 +71,7 @@ const AddManagementContainer: React.FC = ({ useEffect(() => { const runAsync = async () => { const incomingProperties = - featuresWithAddresses?.map(f => PropertyForm.fromFeatureDataset(f.feature)) ?? []; + featuresWithAddresses?.map(f => PropertyForm.fromLocationFeatureDataset(f.feature)) ?? []; if (exists(incomingProperties) && exists(formikRef.current) && needsUserConfirmation) { if (incomingProperties.length > 0) { // Check all properties for confirmation @@ -163,7 +163,6 @@ const AddManagementContainer: React.FC = ({ handleSuccess(response); } } finally { - mapMachine.processCreation(); formikHelpers?.setSubmitting(false); } }; @@ -172,7 +171,7 @@ const AddManagementContainer: React.FC = ({ = props => {
- verifyCallback()} + needsConfirmationBeforeAdd={confirmBeforeAdd} + properties={formikProps.values.fileProperties} />
diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx deleted file mode 100644 index 707ed5fa03..0000000000 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Formik, FormikProps } from 'formik'; -import { createRef } from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; -import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; -import { act, render, RenderOptions, userEvent } from '@/utils/test-utils'; - -import { PropertyForm } from '../../shared/models'; -import { ManagementFormModel } from '../models/ManagementFormModel'; -import ManagementPropertiesSubForm from './ManagementPropertiesSubForm'; - -const mockStore = configureMockStore([thunk]); - -const customSetFilePropertyLocations = vi.fn(); - -const confirmBeforeAdd = vi.fn(); - -describe('ManagementPropertiesSubForm component', () => { - const setup = async ( - props: { initialForm: ManagementFormModel }, - renderOptions: RenderOptions = {}, - ) => { - const ref = createRef>(); - const utils = render( - - {formikProps => ( - - )} - , - { - ...renderOptions, - store: mockStore({}), - claims: [], - mockMapMachine: { - ...mapMachineBaseMock, - setFilePropertyLocations: customSetFilePropertyLocations, - }, - }, - ); - - // Wait for any async effects to complete - await act(async () => {}); - - return { - ...utils, - getFormikRef: () => ref, - }; - }; - - let testForm: ManagementFormModel; - - beforeEach(() => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - testForm = new ManagementFormModel(); - testForm.fileProperties = [ - PropertyForm.fromFeatureDataset({ - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '123-456-789', - }, - }, - }), - PropertyForm.fromFeatureDataset({ - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PIN: 1111222, - }, - }, - }), - ]; - }); - - afterEach(() => { - vi.clearAllMocks(); - customSetFilePropertyLocations.mockReset(); - }); - - it('renders as expected', async () => { - const { asFragment } = await setup({ initialForm: testForm }); - await act(async () => {}); - expect(asFragment()).toMatchSnapshot(); - }); - - it('renders list of properties', async () => { - const { getByText } = await setup({ initialForm: testForm }); - expect(getByText('PID: 123-456-789')).toBeVisible(); - expect(getByText('PIN: 1111222')).toBeVisible(); - }); - - it('should remove property from list when Remove button is clicked', async () => { - const { getAllByTitle, queryByText } = await setup({ initialForm: testForm }); - const pidRow = getAllByTitle('remove')[0]; - await act(async () => userEvent.click(pidRow)); - - expect(queryByText('PID: 123-456-789')).toBeNull(); - }); - - it('should display properties with svg prefixed with incrementing id', async () => { - const { getByTitle } = await setup({ initialForm: testForm }); - expect(getByTitle('1')).toBeInTheDocument(); - expect(getByTitle('2')).toBeInTheDocument(); - }); -}); diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.tsx deleted file mode 100644 index 3381de7059..0000000000 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { FieldArray, FormikProps } from 'formik'; -import { noop } from 'lodash'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { Col, Row } from 'react-bootstrap'; -import styled from 'styled-components'; - -import { Button } from '@/components/common/buttons'; -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { Section } from '@/components/common/Section/Section'; -import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; -import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; -import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; -import { useFeatureDatasetsWithAddresses } from '@/hooks/useFeatureDatasetsWithAddresses'; -import { featuresetToLocationBoundaryDataset } from '@/utils'; -import { addPropertiesToCurrentFile } from '@/utils/propertyUtils'; -import { exists, firstOrNull } from '@/utils/utils'; - -import { PropertyForm } from '../../shared/models'; -import AddPropertiesGuide from '../../shared/update/properties/AddPropertiesGuide'; -import { ManagementFormModel } from '../models/ManagementFormModel'; - -export interface ManagementPropertiesSubFormProps { - formikProps: FormikProps; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; -} - -const ManagementPropertiesSubForm: React.FunctionComponent = ({ - formikProps, -}) => { - const localRef = useRef>(null); - - const { selectedFeatures, processCreation, mapLocationFeatureDataset, prepareForCreation } = - useMapStateMachine(); - - useDraftMarkerSynchronizer( - formikProps.values.fileProperties.map(p => - featuresetToLocationBoundaryDataset(p.toFeatureDataset()), - ), - ); - - // Get PropertyForms with addresses for all selected features - const { featuresWithAddresses } = useFeatureDatasetsWithAddresses(selectedFeatures ?? []); - - const selectedFeatureDataset = useMemo(() => { - return { - selectingComponentId: mapLocationFeatureDataset?.selectingComponentId ?? null, - location: mapLocationFeatureDataset?.location, - fileLocation: mapLocationFeatureDataset?.fileLocation ?? null, - fileBoundary: null, - parcelFeature: firstOrNull(mapLocationFeatureDataset?.parcelFeatures), - pimsFeature: firstOrNull(mapLocationFeatureDataset?.pimsFeatures), - regionFeature: mapLocationFeatureDataset?.regionFeature ?? null, - districtFeature: mapLocationFeatureDataset?.districtFeature ?? null, - municipalityFeature: firstOrNull(mapLocationFeatureDataset?.municipalityFeatures), - isActive: true, - displayOrder: 0, - }; - }, [ - mapLocationFeatureDataset?.selectingComponentId, - mapLocationFeatureDataset?.location, - mapLocationFeatureDataset?.fileLocation, - mapLocationFeatureDataset?.parcelFeatures, - mapLocationFeatureDataset?.pimsFeatures, - mapLocationFeatureDataset?.regionFeature, - mapLocationFeatureDataset?.districtFeature, - mapLocationFeatureDataset?.municipalityFeatures, - ]); - - // Convert SelectedFeatureDataset to PropertyForm - const propertyForms = useMemo( - () => - featuresWithAddresses.map(obj => { - const property = PropertyForm.fromFeatureDataset(obj.feature); - if (exists(obj.address)) { - property.address = obj.address; - } - return property; - }), - [featuresWithAddresses], - ); - - const handleAddToSelection = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); - }, [prepareForCreation, selectedFeatureDataset]); - - useEffect(() => { - if (exists(localRef.current) && propertyForms.length > 0) { - addPropertiesToCurrentFile(localRef, 'fileProperties', propertyForms, noop); - processCreation(); - } - }, [localRef, processCreation, propertyForms]); - - return ( - -
- Select one or more properties that you want to include in this management file. You can - choose a location from the map, or search by other criteria. -
- - - {({ remove }) => ( -
- - {exists(selectedFeatureDataset?.parcelFeature) && ( - - - - )} - - {formikProps.values.fileProperties.map((property, index) => ( - remove(index)} - nameSpace={`fileProperties.${index}`} - index={index} - property={property.toFeatureDataset()} - /> - ))} - {formikProps.values.fileProperties.length === 0 && ( - - - No Properties selected - - - )} -
- )} -
-
- ); -}; - -export default ManagementPropertiesSubForm; - -const StyledComponentWrapper = styled.div` - margin: 0 1.6rem 0 1.6rem; - padding: 0 1.6rem 0 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; - -const StyledButtonWrapper = styled.div` - margin: 0 1.6rem; - padding-left: 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; - -export const HeaderRow = styled(Row)` - margin: 0 1.6rem 0 1.6rem; -`; diff --git a/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts b/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts index e46470ec09..00e93f17af 100644 --- a/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts +++ b/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts @@ -1,22 +1,8 @@ -import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; - +import { FeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { LtsaOrders, SpcpOrder } from '@/interfaces/ltsaModels'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { ApiGen_Concepts_PropertyAssociations } from '@/models/api/generated/ApiGen_Concepts_PropertyAssociations'; -import { ADM_IndianReserveBands_Feature_Properties } from '@/models/layers/admIndianReserveBands'; -import { WHSE_AgriculturalLandReservePoly_Feature_Properties } from '@/models/layers/alcAgriculturalReserve'; import { IBcAssessmentSummary } from '@/models/layers/bcAssesment'; -import { - TANTALIS_CrownLandInclusions_Feature_Properties, - TANTALIS_CrownLandInventory_Feature_Properties, - TANTALIS_CrownLandLeases_Feature_Properties, - TANTALIS_CrownLandLicenses_Feature_Properties, - TANTALIS_CrownLandTenures_Feature_Properties, -} from '@/models/layers/crownLand'; -import { EBC_ELECTORAL_DISTS_BS10_SVW_Feature_Properties } from '@/models/layers/electoralBoundaries'; -import { WHSE_Municipalities_Feature_Properties } from '@/models/layers/municipalities'; -import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; -import { ISS_ProvincialPublicHighway } from '@/models/layers/pimsHighwayLayer'; export interface ComposedProperty { pid: string | undefined; @@ -27,19 +13,6 @@ export interface ComposedProperty { spcpOrder: SpcpOrder | undefined; pimsProperty: ApiGen_Concepts_Property | undefined; propertyAssociations: ApiGen_Concepts_PropertyAssociations | undefined; - parcelMapFeatureCollection: - | FeatureCollection - | undefined; - pimsGeoserverFeatureCollection: FeatureCollection | undefined; // TODO: These need to be strongly typed bcAssessmentSummary: IBcAssessmentSummary | undefined; - crownTenureFeatures: Feature[]; - crownLeaseFeatures: Feature[]; - crownLicenseFeatures: Feature[]; - crownInclusionFeatures: Feature[]; - crownInventoryFeatures: Feature[]; - highwayFeatures: Feature[]; - municipalityFeatures: Feature[]; - firstNationFeatures: Feature[]; - alrFeatures: Feature[]; - electoralFeatures: Feature[]; + featureDataset: FeatureDataset; } diff --git a/source/frontend/src/features/mapSideBar/property/MotiInventoryContainer.test.tsx b/source/frontend/src/features/mapSideBar/property/MotiInventoryContainer.test.tsx index ab806260b0..6c9a40fd1e 100644 --- a/source/frontend/src/features/mapSideBar/property/MotiInventoryContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/MotiInventoryContainer.test.tsx @@ -13,6 +13,7 @@ import { act, cleanup, render, RenderOptions, userEvent, waitFor } from '@/utils import MotiInventoryContainer, { IMotiInventoryContainerProps } from './MotiInventoryContainer'; import { mockFAParcelLayerResponse } from '@/mocks/faParcelLayerResponse.mock'; +import { emptyFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; const mockAxios = new MockAdapter(axios); const history = createMemoryHistory(); @@ -140,23 +141,10 @@ describe('MotiInventoryContainer component', () => { it('shows the crown tab when property has a TANTALIS record', async () => { const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, - isSelecting: true, - selectingComponentId: undefined, mapLocationFeatureDataset: { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - pimsFeatures: null, parcelFeatures: mockFAParcelLayerResponse.features as any, - regionFeature: null, - districtFeature: null, - municipalityFeatures: null, - highwayFeatures: null, - selectingComponentId: null, - crownLandLeasesFeatures: null, - crownLandLicensesFeatures: null, - crownLandTenuresFeatures: null, - crownLandInventoryFeatures: null, - crownLandInclusionsFeatures: null, }, }; diff --git a/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.tsx b/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.tsx index 55f6c2853f..05f01095c1 100644 --- a/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.tsx +++ b/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.tsx @@ -34,14 +34,14 @@ export interface IMotiInventoryHeaderProps { } export const MotiInventoryHeader: React.FunctionComponent = props => { - const parcelMapData = props.composedProperty.parcelMapFeatureCollection; - const geoserverMapData = props.composedProperty.pimsGeoserverFeatureCollection; + const parcelMapData = firstOrNull(props.composedProperty.featureDataset.parcelFeatures); + const geoserverMapData = firstOrNull(props.composedProperty.featureDataset.pimsFeatures); const apiProperty = props.composedProperty.pimsProperty; let pmbcParcel: Feature | null = null; - if (exists(parcelMapData?.features[0])) { - pmbcParcel = firstOrNull(parcelMapData?.features); + if (exists(parcelMapData)) { + pmbcParcel = parcelMapData; } const pid = pidFormatter(props.composedProperty.pid); @@ -61,15 +61,11 @@ export const MotiInventoryHeader: React.FunctionComponent { - if ( - geoserverMapData?.features?.length && - exists(geoserverMapData?.features[0]) && - geoserverMapData?.features[0]?.properties?.IS_DISPOSED - ) { + if (exists(geoserverMapData?.properties?.IS_DISPOSED)) { return true; } return false; - }, [geoserverMapData?.features]); + }, [geoserverMapData]); return ( diff --git a/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx b/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx index 3140a04f48..e5d1ef9fa0 100644 --- a/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx +++ b/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx @@ -182,16 +182,18 @@ export const PropertyContainer: React.FunctionComponent property={{ ...toFormValues(composedPropertyState?.apiWrapper?.response), electoralDistrict: firstOrNull( - composedPropertyState?.composedProperty?.electoralFeatures, + composedPropertyState?.composedProperty?.featureDataset?.electoralFeatures, ), - isALR: composedPropertyState?.composedProperty?.alrFeatures?.length > 0, + isALR: composedPropertyState?.composedProperty?.featureDataset?.alrFeatures?.length > 0, firstNations: { bandName: - firstOrNull(composedPropertyState?.composedProperty?.firstNationFeatures) - ?.properties.BAND_NAME || '', + firstOrNull( + composedPropertyState?.composedProperty?.featureDataset?.firstNationFeatures, + )?.properties.BAND_NAME || '', reserveName: - firstOrNull(composedPropertyState?.composedProperty?.firstNationFeatures) - ?.properties.ENGLISH_NAME || '', + firstOrNull( + composedPropertyState?.composedProperty?.featureDataset?.firstNationFeatures, + )?.properties.ENGLISH_NAME || '', }, }} loading={composedPropertyState.apiWrapper?.loading ?? false} @@ -241,7 +243,8 @@ export const PropertyContainer: React.FunctionComponent if (exists(composedPropertyState?.composedProperty)) { const composedProperty = composedPropertyState?.composedProperty; - if (composedProperty?.parcelMapFeatureCollection?.features?.length > 0) { + const featureDataset = composedPropertyState?.composedProperty?.featureDataset; + if (featureDataset?.parcelFeatures?.length > 0) { tabViews.push({ content: ( name: 'PMBC', }); } - if ( - composedProperty?.pimsGeoserverFeatureCollection?.features?.length > 0 && - !exists(composedProperty?.id) - ) { + if (featureDataset?.pimsFeatures?.length > 0 && !exists(composedProperty?.id)) { tabViews.push({ content: ( }); } if ( - composedProperty?.crownInclusionFeatures?.length + - composedProperty?.crownInventoryFeatures?.length + - composedProperty?.crownLeaseFeatures?.length + - composedProperty?.crownLeaseFeatures?.length + - composedProperty?.crownLicenseFeatures?.length + - composedProperty?.crownTenureFeatures?.length > + featureDataset?.crownLandInclusionsFeatures?.length + + featureDataset?.crownLandInventoryFeatures?.length + + featureDataset?.crownLandLeasesFeatures?.length + + featureDataset?.crownLandLicensesFeatures?.length + + featureDataset?.crownLandTenuresFeatures?.length > 0 ) { tabViews.push({ @@ -291,7 +290,7 @@ export const PropertyContainer: React.FunctionComponent name: 'Crown', }); } - if (composedProperty?.highwayFeatures?.length > 0) { + if (featureDataset?.highwayFeatures?.length > 0) { tabViews.push({ content: ( }); } if ( - composedProperty?.municipalityFeatures?.length > 0 || - composedProperty?.electoralFeatures?.length > 0 || - composedProperty?.alrFeatures?.length > 0 || - (composedProperty?.firstNationFeatures?.length > 0 && + featureDataset?.municipalityFeatures?.length > 0 || + featureDataset?.electoralFeatures?.length > 0 || + featureDataset?.alrFeatures?.length > 0 || + (featureDataset?.firstNationFeatures?.length > 0 && !composedPropertyState.alrLoading && !composedPropertyState.electoralLoading && !composedPropertyState.electoralLoading && diff --git a/source/frontend/src/features/mapSideBar/research/ResearchView.tsx b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx index eddd37e0ff..bb12119659 100644 --- a/source/frontend/src/features/mapSideBar/research/ResearchView.tsx +++ b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx @@ -22,7 +22,7 @@ import FileMenuView from '../shared/FileMenuView'; import { PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; -import UpdateProperties from '../shared/update/properties/UpdateProperties'; +import UpdatePropertiesContainer from '../shared/update/properties/UpdatePropertiesContainer'; import ResearchHeader from './common/ResearchHeader'; import ResearchGenerateContainer from './ResearchGenerateContainer'; import ResearchRouter from './ResearchRouter'; @@ -85,7 +85,7 @@ const ResearchView: React.FunctionComponent = ({ {exists(researchFile) && ( - { it('should save the form and navigate to details view when Save button is clicked', async () => { const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, - processCreation: vi.fn(), + processLocationFeaturesAddition: vi.fn(), refreshMapProperties: vi.fn(), }; @@ -169,7 +169,7 @@ describe('AddResearchContainer component', () => { await act(async () => userEvent.click(getSaveButton())); expect(onSuccess).toHaveBeenCalled(); - expect(testMockMachine.processCreation).toHaveBeenCalled(); + expect(testMockMachine.processLocationFeaturesAddition).toHaveBeenCalled(); expect(testMockMachine.refreshMapProperties).toHaveBeenCalled(); }); diff --git a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx index f1533369be..06d580be73 100644 --- a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx +++ b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx @@ -10,8 +10,8 @@ import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineCo import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; import { usePropertyAssociations } from '@/hooks/repositories/usePropertyAssociations'; import useApiUserOverride from '@/hooks/useApiUserOverride'; -import { useEditPropertiesNotifier } from '@/hooks/useEditPropertiesNotifier'; import { useModalContext } from '@/hooks/useModalContext'; +import { usePropertyFormSyncronizer } from '@/hooks/usePropertyFormSyncronizer'; import { ApiGen_Concepts_ResearchFile } from '@/models/api/generated/ApiGen_Concepts_ResearchFile'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; import { exists, isValidId } from '@/utils'; @@ -58,7 +58,7 @@ export const AddResearchContainer: React.FunctionComponent { @@ -140,7 +140,6 @@ export const AddResearchContainer: React.FunctionComponent} footer={ - + diff --git a/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx b/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx index 3c90cbd041..b18216c5dc 100644 --- a/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx +++ b/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx @@ -1,3 +1,4 @@ +import { useFormikContext } from 'formik'; import React from 'react'; import { Col, Row } from 'react-bootstrap'; import styled from 'styled-components'; @@ -6,15 +7,18 @@ import { Input } from '@/components/common/form/'; import { Section } from '@/components/common/Section/Section'; import { PropertyForm } from '../../shared/models'; +import PropertiesListContainer from '../../shared/update/properties/PropertiesListContainer'; import { ResearchFileNameGuide } from '../common/ResearchFileNameGuide'; import { UpdateProjectsSubForm } from '../common/updateProjects/UpdateProjectsSubForm'; -import ResearchProperties from './ResearchProperties'; +import { ResearchForm } from './models'; export interface IAddResearchFormProps { confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; } const AddResearchForm: React.FC = props => { + const { values } = useFormikContext(); + return (
@@ -35,7 +39,15 @@ const AddResearchForm: React.FC = props => {
- + +
+ removeCallback()} + needsConfirmationBeforeAdd={props.confirmBeforeAdd} + canUploadShapefiles={false} + /> +
); }; diff --git a/source/frontend/src/features/mapSideBar/research/add/ResearchProperties.test.tsx b/source/frontend/src/features/mapSideBar/research/add/ResearchProperties.test.tsx deleted file mode 100644 index 9895fb905b..0000000000 --- a/source/frontend/src/features/mapSideBar/research/add/ResearchProperties.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Formik } from 'formik'; -import noop from 'lodash/noop'; -import { act } from 'react-dom/test-utils'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; -import { render, RenderOptions, userEvent } from '@/utils/test-utils'; - -import { PropertyForm } from '../../shared/models'; -import { ResearchForm } from './models'; -import ResearchProperties from './ResearchProperties'; - -const mockStore = configureMockStore([thunk]); - -let testForm: ResearchForm; - -const store = mockStore({}); -const setDraftProperties = vi.fn(); - -describe('ResearchProperties component', () => { - const setup = ( - renderOptions: RenderOptions & { - initialForm: ResearchForm; - confirmBeforeAdd?: (propertyForm: PropertyForm) => Promise; - } & Partial, - ) => { - // render component under test - const component = render( - - - , - { - ...renderOptions, - store: store, - claims: [], - useMockAuthentication: true, - }, - ); - - return { - store, - component, - }; - }; - - beforeEach(() => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - testForm = new ResearchForm(); - testForm.name = 'Test name'; - testForm.properties = [ - PropertyForm.fromFeatureDataset({ - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '123-456-789', - }, - }, - }), - PropertyForm.fromFeatureDataset({ - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PIN: 1111222, - }, - }, - }), - ]; - }); - - afterEach(() => { - vi.clearAllMocks(); - setDraftProperties.mockReset(); - }); - - it('renders as expected when provided no properties', async () => { - const { component } = setup({ initialForm: testForm }); - await act(async () => {}); - expect(component.asFragment()).toMatchSnapshot(); - }); - - it('renders as expected when provided a list of properties', async () => { - const { - component: { getByText }, - } = await setup({ initialForm: testForm }); - - await act(async () => {}); - expect(getByText('PID: 123-456-789')).toBeVisible(); - expect(getByText('PIN: 1111222')).toBeVisible(); - }); - - it('properties can be removed', async () => { - const { - component: { getAllByTitle, queryByText }, - } = await setup({ initialForm: testForm }); - const pidRow = getAllByTitle('remove')[0]; - - await act(async () => { - userEvent.click(pidRow); - }); - - expect(queryByText('PID: 123-456-789')).toBeNull(); - }); - - it('properties are prefixed by svg with incrementing id', async () => { - const { - component: { getByTitle }, - } = await setup({ initialForm: testForm }); - - await act(async () => {}); - expect(getByTitle('1')).toBeInTheDocument(); - expect(getByTitle('2')).toBeInTheDocument(); - }); - - it.skip('properties with lat/lng are synchronized', async () => { - const formWithProperties = testForm; - formWithProperties.properties[0].latitude = 1; - formWithProperties.properties[0].longitude = 2; - await setup({ initialForm: formWithProperties }); - - //TODO: correct assertions. - }); - - it.skip('multiple properties with lat/lng are synchronized', async () => { - const formWithProperties = testForm; - formWithProperties.properties[0].latitude = 1; - formWithProperties.properties[0].longitude = 2; - formWithProperties.properties[1].latitude = 3; - formWithProperties.properties[1].longitude = 4; - - await setup({ initialForm: formWithProperties }); - - //TODO: correct assertions. - }); -}); diff --git a/source/frontend/src/features/mapSideBar/research/add/ResearchProperties.tsx b/source/frontend/src/features/mapSideBar/research/add/ResearchProperties.tsx deleted file mode 100644 index 42d4de0f7e..0000000000 --- a/source/frontend/src/features/mapSideBar/research/add/ResearchProperties.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { FieldArray, FormikProps, useFormikContext } from 'formik'; -import noop from 'lodash/noop'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; -import styled from 'styled-components'; - -import { Button } from '@/components/common/buttons'; -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { Section } from '@/components/common/Section/Section'; -import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; -import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; -import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; -import { useEditPropertiesMode } from '@/hooks/useEditPropertiesMode'; -import { useFeatureDatasetsWithAddresses } from '@/hooks/useFeatureDatasetsWithAddresses'; -import { featuresetToLocationBoundaryDataset } from '@/utils'; -import { addPropertiesToCurrentFile } from '@/utils/propertyUtils'; -import { exists, firstOrNull } from '@/utils/utils'; - -import { PropertyForm } from '../../shared/models'; -import { AddPropertiesGuide } from '../../shared/update/properties/AddPropertiesGuide'; -import { ResearchForm } from './models'; - -export interface IResearchPropertiesProps { - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; -} - -const ResearchProperties: React.FC = () => { - const localRef = useRef>(); - const { values } = useFormikContext(); - useEditPropertiesMode(); - - const { selectedFeatures, processCreation, mapLocationFeatureDataset, prepareForCreation } = - useMapStateMachine(); - - useDraftMarkerSynchronizer( - values.properties.map(p => featuresetToLocationBoundaryDataset(p.toFeatureDataset())), - ); - - const selectedFeatureDataset = useMemo(() => { - return { - selectingComponentId: mapLocationFeatureDataset?.selectingComponentId ?? null, - location: mapLocationFeatureDataset?.location, - fileLocation: mapLocationFeatureDataset?.fileLocation ?? null, - fileBoundary: null, - parcelFeature: firstOrNull(mapLocationFeatureDataset?.parcelFeatures), - pimsFeature: firstOrNull(mapLocationFeatureDataset?.pimsFeatures), - regionFeature: mapLocationFeatureDataset?.regionFeature ?? null, - districtFeature: mapLocationFeatureDataset?.districtFeature ?? null, - municipalityFeature: firstOrNull(mapLocationFeatureDataset?.municipalityFeatures), - isActive: true, - displayOrder: 0, - }; - }, [ - mapLocationFeatureDataset?.selectingComponentId, - mapLocationFeatureDataset?.location, - mapLocationFeatureDataset?.fileLocation, - mapLocationFeatureDataset?.parcelFeatures, - mapLocationFeatureDataset?.pimsFeatures, - mapLocationFeatureDataset?.regionFeature, - mapLocationFeatureDataset?.districtFeature, - mapLocationFeatureDataset?.municipalityFeatures, - ]); - // Get PropertyForms with addresses for all selected features - const { featuresWithAddresses } = useFeatureDatasetsWithAddresses(selectedFeatures ?? []); - - // Convert SelectedFeatureDataset to PropertyForm - const propertyForms = useMemo( - () => - featuresWithAddresses.map(obj => { - const property = PropertyForm.fromFeatureDataset(obj.feature); - if (exists(obj.address)) { - property.address = obj.address; - } - return property; - }), - [featuresWithAddresses], - ); - - const handleAddToSelection = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); - }, [prepareForCreation, selectedFeatureDataset]); - - useEffect(() => { - if (exists(localRef.current) && propertyForms.length > 0) { - addPropertiesToCurrentFile(localRef, 'properties', propertyForms, noop); - processCreation(); - } - }, [localRef, processCreation, propertyForms]); - - return ( -
- - {exists(selectedFeatureDataset?.parcelFeature) && ( - - - - )} - - {({ remove }) => ( -
- - {values.properties.map((property, index) => ( - remove(index)} - nameSpace={`properties.${index}`} - index={index} - property={property.toFeatureDataset()} - /> - ))} - {values.properties.length === 0 && No Properties selected} -
- )} -
-
- ); -}; - -export default ResearchProperties; - -const StyledButtonWrapper = styled.div` - margin: 0 1.6rem; - padding-left: 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; diff --git a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx index f56bae30cc..9c88a1edbe 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx @@ -178,15 +178,19 @@ export const PropertyFileContainer: React.FunctionComponent< 0, + electoralDistrict: firstOrNull( + composedProperties?.composedProperty?.featureDataset?.electoralFeatures, + ), + isALR: composedProperties?.composedProperty?.featureDataset?.alrFeatures?.length > 0, firstNations: { bandName: - firstOrNull(composedProperties?.composedProperty?.firstNationFeatures)?.properties - .BAND_NAME || '', + firstOrNull( + composedProperties?.composedProperty?.featureDataset?.firstNationFeatures, + )?.properties.BAND_NAME || '', reserveName: - firstOrNull(composedProperties?.composedProperty?.firstNationFeatures)?.properties - .ENGLISH_NAME || '', + firstOrNull( + composedProperties?.composedProperty?.featureDataset?.firstNationFeatures, + )?.properties.ENGLISH_NAME || '', }, }} loading={composedProperties?.composedLoading ?? false} @@ -219,7 +223,8 @@ export const PropertyFileContainer: React.FunctionComponent< if (exists(composedProperties?.composedProperty)) { const composedProperty = composedProperties?.composedProperty; - if (composedProperty?.parcelMapFeatureCollection?.features?.length > 0) { + const featureDataset = composedProperty?.featureDataset; + if (featureDataset?.parcelFeatures?.length > 0) { tabViews.push({ content: ( 0 && - !exists(composedProperty?.id) - ) { + if (featureDataset?.pimsFeatures?.length > 0 && !exists(composedProperty?.id)) { tabViews.push({ content: ( + featureDataset?.crownLandInclusionsFeatures?.length + + featureDataset?.crownLandInventoryFeatures?.length + + featureDataset?.crownLandLeasesFeatures?.length + + featureDataset?.crownLandLicensesFeatures?.length + + featureDataset?.crownLandLicensesFeatures?.length + + featureDataset?.crownLandTenuresFeatures?.length > 0 ) { tabViews.push({ @@ -269,7 +271,7 @@ export const PropertyFileContainer: React.FunctionComponent< name: 'Crown', }); } - if (composedProperty?.highwayFeatures?.length > 0) { + if (composedProperty?.featureDataset?.highwayFeatures?.length > 0) { tabViews.push({ content: ( 0) { + if (composedProperty?.featureDataset?.municipalityFeatures?.length > 0) { tabViews.push({ content: ( { return { - parcelFeature: null, - selectingComponentId: null, - pimsFeature: { - properties: { - ...emptyPropertyLocation, - PROPERTY_ID: this.apiId, - PID: this.pid ? +this.pid.replaceAll(/-/g, '') : null, - PID_PADDED: this?.pid?.padStart(9, '0'), - PIN: this.pin ? +this.pin : null, - SURVEY_PLAN_NUMBER: this.planNumber, - REGION_CODE: this.region, - DISTRICT_CODE: this.district, - LAND_AREA: this.landArea, - PROPERTY_AREA_UNIT_TYPE_CODE: this.areaUnit, - STREET_ADDRESS_1: this.address?.streetAddress1 ?? this.formattedAddress, - STREET_ADDRESS_2: this.address?.streetAddress2, - STREET_ADDRESS_3: this.address?.streetAddress3, - MUNICIPALITY_NAME: this.address?.municipality, - POSTAL_CODE: this.address?.postalCode, - IS_RETIRED: this.isRetired, - LAND_LEGAL_DESCRIPTION: this.legalDescription, - }, - type: 'Feature', - geometry: this.polygon ? this.polygon : null, + properties: { + ...emptyPropertyLocation, + PROPERTY_ID: this.apiId, + PID: this.pid ? +this.pid.replaceAll(/-/g, '') : null, + PID_PADDED: this?.pid?.padStart(9, '0'), + PIN: this.pin ? +this.pin : null, + SURVEY_PLAN_NUMBER: this.planNumber, + REGION_CODE: this.region, + DISTRICT_CODE: this.district, + LAND_AREA: this.landArea, + PROPERTY_AREA_UNIT_TYPE_CODE: this.areaUnit, + STREET_ADDRESS_1: this.address?.streetAddress1 ?? this.formattedAddress, + STREET_ADDRESS_2: this.address?.streetAddress2, + STREET_ADDRESS_3: this.address?.streetAddress3, + MUNICIPALITY_NAME: this.address?.municipality, + POSTAL_CODE: this.address?.postalCode, + IS_RETIRED: this.isRetired, + LAND_LEGAL_DESCRIPTION: this.legalDescription, }, + type: 'Feature', + geometry: this.polygon ? this.polygon : null, + }; + } + + public toLocationFeatureDataset(): LocationFeatureDataset { + return { + ...emptyFeatureDataset(), location: { lat: this.latitude, lng: this.longitude }, - fileLocation: this.fileLocation ?? { lat: this.latitude, lng: this.longitude }, - fileBoundary: this.fileBoundary ?? null, + pimsFeatures: [ + { + ...this.toFeature(), + }, + ], regionFeature: { properties: { REGION_NAME: this.regionName, @@ -207,8 +220,6 @@ export class PropertyForm { type: 'Feature', geometry: null, }, - municipalityFeature: null, - isActive: this.isActive !== 'false', }; } @@ -350,6 +361,7 @@ export class AddressForm { public streetAddress3?: string; public municipality?: string; public postalCode?: string; + public province?: string; public static fromBcaAddress(model: IBcAssessmentSummary['ADDRESSES'][0]): AddressForm { const newForm = new AddressForm(); @@ -382,6 +394,7 @@ export class AddressForm { newForm.municipality = model.municipality ?? undefined; newForm.postalCode = model.postal ?? undefined; newForm.apiId = model?.id ?? undefined; + newForm.province = model?.province?.description ?? undefined; newForm.rowVersion = model.rowVersion ?? undefined; return newForm; diff --git a/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx b/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx index a65cce5e25..c8613e9b91 100644 --- a/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx +++ b/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx @@ -6,12 +6,13 @@ import OverflowTip from '@/components/common/OverflowTip'; import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; import DraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; import { ApiGen_CodeTypes_AreaUnitTypes } from '@/models/api/generated/ApiGen_CodeTypes_AreaUnitTypes'; -import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; -import { convertArea, formatApiAddress, formatNumber, pidFormatter } from '@/utils'; +import { convertArea, formatFormAddress, formatNumber, pidFormatter } from '@/utils'; + +import { PropertyForm } from '../models'; interface ISelectedOperationPropertyProps { - property: ApiGen_Concepts_Property; - getMarkerIndex: (property: ApiGen_Concepts_Property) => number; + property: PropertyForm; + getMarkerIndex: (property: PropertyForm) => number; nameSpace: string; onRemove: () => void; isEditable?: boolean; @@ -41,12 +42,12 @@ export const SelectedOperationProperty: React.FunctionComponent< {isEditable ? ( ) : ( - getAreaValue(property.landArea ?? 0, property.areaUnit?.id ?? '') + getAreaValue(property.landArea ?? 0, property.areaUnit ?? '') )} - {formatApiAddress(property?.address) ?? ''} + {formatFormAddress(property?.address) ?? ''} - + diff --git a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.test.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.test.tsx similarity index 54% rename from source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.test.tsx rename to source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.test.tsx index 18702774cd..7b7c29109a 100644 --- a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.test.tsx @@ -1,37 +1,35 @@ -import { Formik, FormikProps } from 'formik'; +import { FormikProps } from 'formik'; import { createRef } from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { act, render, RenderOptions, userEvent } from '@/utils/test-utils'; -import { PropertyForm } from '../../shared/models'; -import { DispositionFormModel } from '../models/DispositionFormModel'; -import DispositionPropertiesSubForm from './DispositionPropertiesSubForm'; +import { FileForm, PropertyForm } from '../../models'; +import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; +import PropertiesListContainer from './PropertiesListContainer'; +import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; const mockStore = configureMockStore([thunk]); const customSetFilePropertyLocations = vi.fn(); +const verifyCanRemove = vi.fn(); const confirmBeforeAdd = vi.fn(); -describe('DispositionPropertiesSubForm component', () => { +describe('PropertiesListContainer component', () => { const setup = async ( - props: { initialForm: DispositionFormModel }, + props: { properties: PropertyForm[] }, renderOptions: RenderOptions = {}, ) => { - const ref = createRef>(); + const ref = createRef>(); const utils = render( - - {formikProps => ( - - )} - , + , { ...renderOptions, store: mockStore({}), @@ -52,33 +50,39 @@ describe('DispositionPropertiesSubForm component', () => { }; }; - let testForm: DispositionFormModel; + let testForm: FileForm; beforeEach(() => { const mockFeatureSet = getMockSelectedFeatureDataset(); - testForm = new DispositionFormModel(); - testForm.fileProperties = [ - PropertyForm.fromFeatureDataset({ + testForm = new FileForm(); + testForm.properties = [ + PropertyForm.fromLocationFeatureDataset({ ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '123-456-789', + pimsFeatures: [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0].properties, + PID_PADDED: '123-456-789', + }, }, - }, + ], }), - PropertyForm.fromFeatureDataset({ + PropertyForm.fromLocationFeatureDataset({ ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PIN: 1111222, + pimsFeatures: [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0]?.properties, + PIN: 1111222, + }, }, - }, + ], }), ]; + testForm.properties[0].pid = '123456789'; + testForm.properties[1].pin = '1111222'; }); afterEach(() => { @@ -87,20 +91,20 @@ describe('DispositionPropertiesSubForm component', () => { }); it('renders as expected', async () => { - const { asFragment } = await setup({ initialForm: testForm }); + const { asFragment } = await setup({ properties: testForm.properties }); await act(async () => {}); expect(asFragment()).toMatchSnapshot(); }); it('renders list of properties', async () => { - const { getByText } = await setup({ initialForm: testForm }); + const { getByText } = await setup({ properties: testForm.properties }); expect(getByText('PID: 123-456-789')).toBeVisible(); expect(getByText('PIN: 1111222')).toBeVisible(); }); it('should remove property from list when Remove button is clicked', async () => { - const { getAllByTitle, queryByText } = await setup({ initialForm: testForm }); + const { getAllByTitle, queryByText } = await setup({ properties: testForm.properties }); const pidRow = getAllByTitle('remove')[0]; await act(async () => userEvent.click(pidRow)); @@ -108,7 +112,7 @@ describe('DispositionPropertiesSubForm component', () => { }); it('should display properties with svg prefixed with incrementing id', async () => { - const { getByTitle } = await setup({ initialForm: testForm }); + const { getByTitle } = await setup({ properties: testForm.properties }); expect(getByTitle('1')).toBeInTheDocument(); expect(getByTitle('2')).toBeInTheDocument(); diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx new file mode 100644 index 0000000000..e2e6790e7c --- /dev/null +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx @@ -0,0 +1,168 @@ +import { FieldArray } from 'formik'; +import { useCallback, useState } from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { toast } from 'react-toastify'; +import styled from 'styled-components'; + +import { Button } from '@/components/common/buttons'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { Section } from '@/components/common/Section/Section'; +import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; +import MapClickMonitor from '@/components/propertySelector/MapClickMonitor'; +import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; +import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; +import { UploadResponseModel } from '@/features/properties/shapeUpload/models'; +import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; +import { exists, isEmptyOrNull, isLatLngInFeatureSetBoundary, isNumber } from '@/utils'; + +import { PropertyForm } from '../../models'; +import AddPropertiesGuide from './AddPropertiesGuide'; + +export interface IPropertiesListContainerProps { + properties: PropertyForm[]; + verifyCanRemove: (propertyId: number, removeCallback: () => void) => void; + needsConfirmationBeforeAdd: (propertyForm: PropertyForm) => Promise; + confirmBeforeAddMessage?: React.ReactNode; + showDisabledProperties?: boolean; + canUploadShapefiles?: boolean; + canReposition?: boolean; +} + +export const PropertiesListContainer: React.FunctionComponent< + IPropertiesListContainerProps +> = props => { + const [repositionPropertyIndex, setRepositionPropertyIndex] = useState(null); + + useDraftMarkerSynchronizer(props.properties); + + const { + mapLocationFeatureDataset, + requestLocationFeatureAddition, + isRepositioning, + startReposition, + finishReposition, + } = useMapStateMachine(); + + const handleAddToSelection = useCallback(() => { + requestLocationFeatureAddition([mapLocationFeatureDataset]); + }, [requestLocationFeatureAddition, mapLocationFeatureDataset]); + + const onRepositionClick = useCallback( + (propertyIndex: number) => { + setRepositionPropertyIndex(propertyIndex); + const formProperty = props.properties[propertyIndex]; + startReposition(formProperty.toFeature()); + }, + [props.properties, startReposition], + ); + + return ( + <> + + {!isEmptyOrNull(mapLocationFeatureDataset?.parcelFeatures) && ( + + + + )} + + + {({ remove, replace }) => ( +
+ Selected Properties + + + + + } + > + { + if ( + props.canReposition && + isRepositioning && + isNumber(repositionPropertyIndex) && + repositionPropertyIndex >= 0 && + !hasMultipleProperties + ) { + debugger; + // As long as the marker is repositioned within the boundary of the originally selected property simply reposition the marker without further notification. + const formProperty = props.properties[repositionPropertyIndex]; + + if ( + isLatLngInFeatureSetBoundary( + locationDataSet.location, + formProperty.toLocationFeatureDataset(), + ) + ) { + const updatedFormProperty = new PropertyForm(formProperty); + updatedFormProperty.fileLocation = locationDataSet.location; + + // Find property within formik values and reposition it based on incoming file marker position + replace(repositionPropertyIndex, updatedFormProperty); + + // Reset the reposition state + finishReposition(); + setRepositionPropertyIndex(null); + } else { + toast.warn( + 'Please choose a location that is within the (highlighted) boundary of this property.', + ); + } + } + }} + /> + + {props.properties?.map((property, index) => ( + { + const removeCallback = () => { + remove(index); + }; + await props.verifyCanRemove(property.apiId, removeCallback); + }} + canReposition={props.canReposition} + onReposition={onRepositionClick} + nameSpace={`properties.${index}`} + index={index} + property={property} + showDisable={props.showDisabledProperties} + canUploadShapefile={props.canUploadShapefiles} + onUploadShapefile={(result: UploadResponseModel | null) => { + // Update the property boundary based on the uploaded shapefile + if (exists(result)) { + if (result.isSuccess && exists(result.boundary)) { + const updatedFormProperty = new PropertyForm(property); + updatedFormProperty.fileBoundary = result.boundary; + replace(index, updatedFormProperty); + } + } + }} + /> + ))} + {props.properties?.length === 0 && No Properties selected} +
+ )} +
+ + ); +}; + +export default PropertiesListContainer; + +const StyledButtonWrapper = styled.div` + margin: 0 1.6rem; + padding-left: 1.6rem; + text-align: left; + text-underline-offset: 2px; + + button { + font-size: 14px; + } +`; diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.test.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.test.tsx index d3087ecaf9..e00db37239 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.test.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.test.tsx @@ -20,7 +20,11 @@ import { waitFor, } from '@/utils/test-utils'; -import UpdateProperties, { IUpdatePropertiesProps } from './UpdateProperties'; +import UpdatePropertiesContainer, { + IUpdatePropertiesContainerProps, +} from './UpdatePropertiesContainer'; +import { IPropertiesListContainerProps } from './PropertiesListContainer'; +import { emptyFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; const mockAxios = new MockAdapter(axios); @@ -43,12 +47,12 @@ const updateFileProperties = vi.fn(); describe('UpdateProperties component', () => { // render component under test const setup = async ( - props: Partial, + props: Partial, renderOptions: RenderOptions = {}, ) => { const utils = render( - { mockMapMachine: { ...mapMachineBaseMock, // properties to be added to the current file via the map state machine (ie working list, etc) - selectedFeatures: [ + locationFeaturesForAddition: [ { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('111-111-111'), - regionFeature: null, - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, + parcelFeatures: [getMockFullyAttributedParcel('111-111-111')], }, { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('222-222-222'), - regionFeature: null, - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, + parcelFeatures: [getMockFullyAttributedParcel('222-222-222')], }, { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('333-333-333'), - regionFeature: null, - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, + parcelFeatures: [getMockFullyAttributedParcel('333-333-333')], }, ], }, diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx deleted file mode 100644 index 18aed9e88d..0000000000 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import axios, { AxiosError } from 'axios'; -import { FieldArray, Formik, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; -import noop from 'lodash/noop'; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Col, Row } from 'react-bootstrap'; -import { toast } from 'react-toastify'; -import styled from 'styled-components'; - -import { Button } from '@/components/common/buttons'; -import GenericModal from '@/components/common/GenericModal'; -import LoadingBackdrop from '@/components/common/LoadingBackdrop'; -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { Section } from '@/components/common/Section/Section'; -import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; -import MapClickMonitor from '@/components/propertySelector/MapClickMonitor'; -import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; -import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; -import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; -import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; -import { UploadResponseModel } from '@/features/properties/shapeUpload/models'; -import { useEditPropertiesMode } from '@/hooks/useEditPropertiesMode'; -import { useFeatureDatasetsWithAddresses } from '@/hooks/useFeatureDatasetsWithAddresses'; -import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; -import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; -import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { exists, firstOrNull, isLatLngInFeatureSetBoundary, isNumber, isValidId } from '@/utils'; -import { addPropertiesToCurrentFile } from '@/utils/propertyUtils'; - -import { FileForm, PropertyForm } from '../../models'; -import SidebarFooter from '../../SidebarFooter'; -import AddPropertiesGuide from './AddPropertiesGuide'; -import { UpdatePropertiesYupSchema } from './UpdatePropertiesYupSchema'; - -export interface IUpdatePropertiesProps { - file: ApiGen_Concepts_File; - setIsShowingPropertySelector: (isShowing: boolean) => void; - onSuccess: (updateProperties?: boolean, updateFile?: boolean) => void; - updateFileProperties: ( - file: ApiGen_Concepts_File, - userOverrideCodes: UserOverrideCode[], - ) => Promise; - canRemove: (propertyId: number) => Promise; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; - confirmBeforeAddMessage?: React.ReactNode; - formikRef?: React.RefObject>; - disableProperties?: boolean; - canUploadShapefiles?: boolean; -} - -export const UpdateProperties: React.FunctionComponent = props => { - const localRef = useRef>(null); - const formikRef = props.formikRef ? props.formikRef : localRef; - const formFile = FileForm.fromApi(props.file); - - const [showSaveConfirmModal, setShowSaveConfirmModal] = useState(false); - const [showAssociatedEntityWarning, setShowAssociatedEntityWarning] = useState(false); - const [isValid, setIsValid] = useState(true); - const { setModalContent, setDisplayModal } = useModalContext(); - const { resetFilePropertyLocations } = useContext(SideBarContext); - const { selectedFeatures, processCreation, mapLocationFeatureDataset, prepareForCreation } = - useMapStateMachine(); - - useEditPropertiesMode(); - - // Get PropertyForms with addresses for all selected features - const { featuresWithAddresses, bcaLoading } = useFeatureDatasetsWithAddresses( - selectedFeatures ?? [], - ); - - // Convert SelectedFeatureDataset to PropertyForm - const propertyForms = useMemo( - () => - featuresWithAddresses.map(obj => { - const property = PropertyForm.fromFeatureDataset(obj.feature); - if (exists(obj.address)) { - property.address = obj.address; - } - return property; - }), - [featuresWithAddresses], - ); - - // This effect is used to update the file properties when "add to open file" is clicked in the worklist. - useEffect(() => { - if (exists(formikRef.current) && propertyForms.length > 0) { - addPropertiesToCurrentFile(formikRef, 'properties', propertyForms, noop); - processCreation(); - } - }, [formikRef, processCreation, propertyForms]); - - const handleSaveClick = async () => { - await formikRef?.current?.validateForm(); - if (!formikRef?.current?.isValid) { - setIsValid(false); - } else { - setIsValid(true); - } - setShowSaveConfirmModal(true); - }; - - const handleCancelClick = () => { - if (formikRef !== undefined) { - if (formikRef.current?.dirty) { - setModalContent({ - ...getCancelModalProps(), - handleOk: () => { - handleCancelConfirm(); - setDisplayModal(false); - }, - handleCancel: () => setDisplayModal(false), - }); - setDisplayModal(true); - } else { - handleCancelConfirm(); - } - } else { - handleCancelConfirm(); - } - }; - - const handleSaveConfirm = async () => { - if (formikRef !== undefined) { - formikRef.current?.setSubmitting(true); - formikRef.current?.submitForm(); - } - }; - - const handleCancelConfirm = () => { - if (formikRef !== undefined) { - formikRef.current?.resetForm(); - } - resetFilePropertyLocations(); - props.setIsShowingPropertySelector(false); - }; - - const saveFile = async (file: ApiGen_Concepts_File) => { - try { - const response = await props.updateFileProperties(file, []); - - formikRef.current?.setSubmitting(false); - if (isValidId(response?.id)) { - if (file.fileProperties?.find(fp => !fp.property?.address && !fp.property?.id)) { - toast.warn( - 'Address could not be retrieved for this property, it will have to be provided manually in property details tab', - { autoClose: 15000 }, - ); - } - formikRef.current?.resetForm(); - props.setIsShowingPropertySelector(false); - props.onSuccess(true); - } - } catch (e) { - if (axios.isAxiosError(e) && (e as AxiosError).code === '409') { - setShowAssociatedEntityWarning(true); - } - } - }; - - const selectedFeatureDataset = useMemo(() => { - return { - selectingComponentId: mapLocationFeatureDataset?.selectingComponentId ?? null, - location: mapLocationFeatureDataset?.location, - fileLocation: mapLocationFeatureDataset?.fileLocation ?? null, - fileBoundary: null, - parcelFeature: firstOrNull(mapLocationFeatureDataset?.parcelFeatures), - pimsFeature: firstOrNull(mapLocationFeatureDataset?.pimsFeatures), - regionFeature: mapLocationFeatureDataset?.regionFeature ?? null, - districtFeature: mapLocationFeatureDataset?.districtFeature ?? null, - municipalityFeature: firstOrNull(mapLocationFeatureDataset?.municipalityFeatures), - isActive: true, - displayOrder: 0, - }; - }, [ - mapLocationFeatureDataset?.selectingComponentId, - mapLocationFeatureDataset?.location, - mapLocationFeatureDataset?.fileLocation, - mapLocationFeatureDataset?.parcelFeatures, - mapLocationFeatureDataset?.pimsFeatures, - mapLocationFeatureDataset?.regionFeature, - mapLocationFeatureDataset?.districtFeature, - mapLocationFeatureDataset?.municipalityFeatures, - ]); - - const handleAddToSelection = useCallback(() => { - prepareForCreation([selectedFeatureDataset]); - }, [prepareForCreation, selectedFeatureDataset]); - - return ( - <> - - - } - > - <> - - {exists(selectedFeatureDataset?.parcelFeature) && ( - - - - )} - - innerRef={formikRef} - initialValues={formFile} - validationSchema={UpdatePropertiesYupSchema} - onSubmit={async (values: FileForm) => { - const file: ApiGen_Concepts_File = values.toApi(); - await saveFile(file); - }} - > - {formikProps => ( - - {({ remove, replace }) => ( -
- Selected Properties - - - - - } - > - - - { - // As long as the marker is repositioned within the boundary of the originally selected property simply reposition the marker without further notification. - if ( - isNumber(index) && - index >= 0 && - isLatLngInFeatureSetBoundary(latLng, featureset) - ) { - const formProperty = formikProps.values.properties[index]; - const updatedFormProperty = new PropertyForm(formProperty); - updatedFormProperty.fileLocation = latLng; - - // Find property within formik values and reposition it based on incoming file marker position - replace(index, updatedFormProperty); - } else if (!isLatLngInFeatureSetBoundary(latLng, featureset)) { - toast.warn( - 'Please choose a location that is within the (highlighted) boundary of this property.', - ); - } - }} - modifiedProperties={formikProps.values.properties.map(p => - p.toFeatureDataset(), - )} - /> - - - - {formikProps.values.properties.map((property, index) => ( - { - if (!property.apiId || (await props.canRemove(property.apiId))) { - remove(index); - } else { - setShowAssociatedEntityWarning(true); - } - }} - nameSpace={`properties.${index}`} - index={index} - property={property.toFeatureDataset()} - showDisable={props.disableProperties} - canUploadShapefile={props.canUploadShapefiles} - onUploadShapefile={(result: UploadResponseModel | null) => { - // Update the property boundary based on the uploaded shapefile - if (exists(result)) { - if (result.isSuccess && exists(result.boundary)) { - const updatedFormProperty = new PropertyForm(property); - updatedFormProperty.fileBoundary = result.boundary; - replace(index, updatedFormProperty); - } - } - }} - /> - ))} - {formikProps.values.properties.length === 0 && ( - No Properties selected - )} -
- )} -
- )} - - -
- -
You have made changes to the properties in this file.
-
- Do you want to save these changes? - - } - handleOk={handleSaveConfirm} - handleCancel={() => setShowSaveConfirmModal(false)} - okButtonText="Save" - cancelButtonText="Cancel" - show - /> - -
- This property can not be removed from the file. This property is related to one or - more entities in the file, only properties that are not linked to any entities in the - file can be removed. -
- - } - handleOk={() => setShowAssociatedEntityWarning(false)} - okButtonText="Close" - show - /> - - ); -}; - -export default UpdateProperties; - -const StyledButtonWrapper = styled.div` - margin: 0 1.6rem; - padding-left: 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx new file mode 100644 index 0000000000..4c05710088 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx @@ -0,0 +1,207 @@ +import axios, { AxiosError } from 'axios'; +import { Formik, FormikProps } from 'formik'; +import { useContext, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; + +import GenericModal from '@/components/common/GenericModal'; +import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; +import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; +import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; +import { usePropertyFormSyncronizer } from '@/hooks/usePropertyFormSyncronizer'; +import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; +import { UserOverrideCode } from '@/models/api/UserOverrideCode'; +import { isValidId } from '@/utils'; + +import { FileForm, PropertyForm } from '../../models'; +import SidebarFooter from '../../SidebarFooter'; +import PropertiesListContainer from './PropertiesListContainer'; +import { UpdatePropertiesYupSchema } from './UpdatePropertiesYupSchema'; + +export interface IUpdatePropertiesContainerProps { + file: ApiGen_Concepts_File; + setIsShowingPropertySelector: (isShowing: boolean) => void; + onSuccess: (updateProperties?: boolean, updateFile?: boolean) => void; + updateFileProperties: ( + file: ApiGen_Concepts_File, + userOverrideCodes: UserOverrideCode[], + ) => Promise; + canRemove: (propertyId: number) => Promise; + confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; + confirmBeforeAddMessage?: React.ReactNode; + formikRef?: React.RefObject>; + showDisabledProperties?: boolean; + canUploadShapefiles?: boolean; + canReposition?: boolean; +} + +export const UpdatePropertiesContainer: React.FunctionComponent< + IUpdatePropertiesContainerProps +> = props => { + const localRef = useRef>(null); + const formikRef = props.formikRef ? props.formikRef : localRef; + const formFile = FileForm.fromApi(props.file); + + const [showSaveConfirmModal, setShowSaveConfirmModal] = useState(false); + const [showAssociatedEntityWarning, setShowAssociatedEntityWarning] = useState(false); + const [isValid, setIsValid] = useState(true); + const { setModalContent, setDisplayModal } = useModalContext(); + const { resetFilePropertyLocations } = useContext(SideBarContext); + + // Sets the state of the Map FSM to allow to edit properties + const { isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); + + const handleSaveClick = async () => { + await formikRef?.current?.validateForm(); + if (!formikRef?.current?.isValid) { + setIsValid(false); + } else { + setIsValid(true); + } + setShowSaveConfirmModal(true); + }; + + const handleCancelClick = () => { + if (formikRef !== undefined) { + if (formikRef.current?.dirty) { + setModalContent({ + ...getCancelModalProps(), + handleOk: () => { + handleCancelConfirm(); + setDisplayModal(false); + }, + handleCancel: () => setDisplayModal(false), + }); + setDisplayModal(true); + } else { + handleCancelConfirm(); + } + } else { + handleCancelConfirm(); + } + }; + + const handleSaveConfirm = async () => { + if (formikRef !== undefined) { + formikRef.current?.setSubmitting(true); + formikRef.current?.submitForm(); + } + }; + + const handleCancelConfirm = () => { + if (formikRef !== undefined) { + formikRef.current?.resetForm(); + } + resetFilePropertyLocations(); + props.setIsShowingPropertySelector(false); + }; + + const saveFile = async (file: ApiGen_Concepts_File) => { + try { + const response = await props.updateFileProperties(file, []); + + formikRef?.current?.setSubmitting(false); + if (isValidId(response?.id)) { + if (file.fileProperties?.find(fp => !fp.property?.address && !fp.property?.id)) { + toast.warn( + 'Address could not be retrieved for this property, it will have to be provided manually in property details tab', + { autoClose: 15000 }, + ); + } + formikRef?.current?.resetForm(); + props.setIsShowingPropertySelector(false); + props.onSuccess(true); + } + } catch (e) { + if (axios.isAxiosError(e) && (e as AxiosError).code === '409') { + setShowAssociatedEntityWarning(true); + } + } + }; + + const onRemoveClick = async (propertyApiId: number, removeCallback: () => void) => { + if (await props.canRemove(propertyApiId)) { + removeCallback(); + } else { + setShowAssociatedEntityWarning(true); + } + }; + + return ( + <> + + + } + > + + innerRef={formikRef} + initialValues={formFile} + validationSchema={UpdatePropertiesYupSchema} + onSubmit={async (values: FileForm) => { + const file: ApiGen_Concepts_File = values.toApi(); + await saveFile(file); + }} + > + {formikProps => ( + <> + Promise.resolve(true)} + canUploadShapefiles={props.canUploadShapefiles} + canReposition={props.canReposition} + showDisabledProperties={props.showDisabledProperties} + /> + {formikProps.values.properties.length} + + )} + + + +
+ This property can not be removed from the file. This property is related to one or + more entities in the file, only properties that are not linked to any entities in the + file can be removed. +
+ + } + handleOk={() => setShowAssociatedEntityWarning(false)} + okButtonText="Close" + show + /> + +
You have made changes to the properties in this file.
+
+ Do you want to save these changes? + + } + handleOk={handleSaveConfirm} + handleCancel={() => setShowSaveConfirmModal(false)} + okButtonText="Save" + cancelButtonText="Cancel" + show + /> + + ); +}; + +export default UpdatePropertiesContainer; diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx index db790e02b6..3a10bc97bf 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx @@ -34,7 +34,7 @@ const AddSubdivisionContainer: React.FC = ({ const [initialForm, setInitialForm] = useState(new SubdivisionFormModel()); const formikRef = useRef>(null); const mapMachine = useMapStateMachine(); - const selectedFeatureDataset = firstOrNull(mapMachine.selectedFeatures); + const selectedFeatureDataset = firstOrNull(mapMachine.locationFeaturesForAddition); const { setModalContent, setDisplayModal } = useModalContext(); const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); @@ -59,15 +59,15 @@ const AddSubdivisionContainer: React.FC = ({ async function loadInitialProperty() { // support creating a new subdivision from the map popup if (selectedFeatureDataset !== null) { - const propertyForm = PropertyForm.fromFeatureDataset(selectedFeatureDataset); + const propertyForm = PropertyForm.fromLocationFeatureDataset(selectedFeatureDataset); if (isValidString(propertyForm.pid)) { // TODO: This should work with multiple properties - const pimsFeature = selectedFeatureDataset.pimsFeature; + const pimsFeature = firstOrNull(selectedFeatureDataset.pimsFeatures); propertyForm.address = pimsFeature?.properties ? AddressForm.fromPimsView(pimsFeature?.properties) : undefined; const subdivisionFormModel = new SubdivisionFormModel(); - subdivisionFormModel.sourceProperty = propertyForm.toApi(); + subdivisionFormModel.sourceProperty = propertyForm; subdivisionFormModel.sourceProperty.isOwned = pimsFeature.properties.IS_OWNED; setInitialForm(subdivisionFormModel); } @@ -139,7 +139,7 @@ const AddSubdivisionContainer: React.FC = ({ handleSuccess(propertyOperations); } } finally { - mapMachine.processCreation(); + mapMachine.processLocationFeaturesAddition(); formikHelpers?.setSubmitting(false); } }; diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionMarkerSynchronizer.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionMarkerSynchronizer.tsx index d1b4741304..5f9252fefb 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionMarkerSynchronizer.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionMarkerSynchronizer.tsx @@ -1,5 +1,4 @@ import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; -import { propertyToLocationBoundaryDataset } from '@/utils/mapPropertyUtils'; import { SubdivisionFormModel } from './AddSubdivisionModel'; @@ -11,8 +10,8 @@ const AddSubdivisionMarkerSynchronizer: React.FunctionComponent< IAddSubdivisionMarkerSynchronizerProps > = ({ values }) => { useDraftMarkerSynchronizer([ - ...(values.sourceProperty ? [propertyToLocationBoundaryDataset(values.sourceProperty)] : []), - ...values.destinationProperties.map(dp => propertyToLocationBoundaryDataset(dp)), + ...(values.sourceProperty ? [values.sourceProperty] : []), + ...values.destinationProperties, ]); return null; }; diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts index 76e77654c7..49f155469d 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts @@ -1,13 +1,14 @@ import { ApiGen_Base_CodeType } from '@/models/api/generated/ApiGen_Base_CodeType'; import { ApiGen_CodeTypes_PropertyOperationTypes } from '@/models/api/generated/ApiGen_CodeTypes_PropertyOperationTypes'; -import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { ApiGen_Concepts_PropertyOperation } from '@/models/api/generated/ApiGen_Concepts_PropertyOperation'; import { getEmptyBaseAudit } from '@/models/defaultInitializers'; import { exists } from '@/utils'; +import { PropertyForm } from '../shared/models'; + export class SubdivisionFormModel { - sourceProperty: ApiGen_Concepts_Property | null = null; - destinationProperties: ApiGen_Concepts_Property[] = []; + sourceProperty: PropertyForm | null = null; + destinationProperties: PropertyForm[] = []; propertyOperationNo: number | null = null; propertyOperationTypeCode: ApiGen_Base_CodeType | null = { id: ApiGen_CodeTypes_PropertyOperationTypes.SUBDIVIDE, @@ -19,12 +20,12 @@ export class SubdivisionFormModel { pid = ''; toApi(): ApiGen_Concepts_PropertyOperation[] { - return this.destinationProperties?.map(dp => ({ + return this.destinationProperties?.map(dp => ({ ...getEmptyBaseAudit(0), id: 0, sourcePropertyId: this.sourceProperty?.id ?? 0, - sourceProperty: this.sourceProperty, - destinationProperty: dp, + sourceProperty: this.sourceProperty.toApi(), + destinationProperty: dp.toApi(), destinationPropertyId: dp?.id ?? 0, operationDt: null, isDisabled: false, @@ -42,9 +43,10 @@ export class SubdivisionFormModel { subdivisionForm.propertyOperationNo = null; subdivisionForm.propertyOperationTypeCode = null; } - subdivisionForm.sourceProperty = operations[0].sourceProperty; + subdivisionForm.sourceProperty = PropertyForm.fromPropertyApi(operations[0].sourceProperty); subdivisionForm.destinationProperties = - operations.map(op => op.destinationProperty).filter(exists) ?? []; + operations.map(op => PropertyForm.fromPropertyApi(op.destinationProperty)).filter(exists) ?? + []; subdivisionForm.propertyOperationNo = operations[0].propertyOperationNo; subdivisionForm.propertyOperationTypeCode = operations[0].propertyOperationTypeCode; return subdivisionForm; diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx index c6a88b4a5c..57a6fa47cd 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx @@ -5,7 +5,7 @@ import { createRef } from 'react'; import { IMapSelectorContainerProps } from '@/components/propertySelector/MapSelectorContainer'; import { PropertySelectorPidSearchContainerProps } from '@/components/propertySelector/search/PropertySelectorPidSearchContainer'; import Claims from '@/constants/claims'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; +import { getMockSelectedFeatureDataset } from '@/mocks/getMockSelectedFeatureDataset'; import { mockLookups } from '@/mocks/lookups.mock'; import { getMockApiProperty } from '@/mocks/properties.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes/lookupCodesSlice'; @@ -99,13 +99,15 @@ describe('Add Subdivision View', () => { mapSelectorProps.addSelectedProperties([ { ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '123-456-789', + pimsFeatures: [ + { + ...mockFeatureSet.pimsFeatures, + properties: { + ...mockFeatureSet.pimsFeature?.properties, + PID_PADDED: '123-456-789', + }, }, - }, + ], }, ]); }); diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx index a8ad71869d..6b24dcfb58 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx @@ -1,5 +1,4 @@ import { FieldArray, Formik, FormikHelpers, FormikProps } from 'formik'; -import noop from 'lodash/noop'; import { useCallback } from 'react'; import { Col, Row, Tab } from 'react-bootstrap'; import { FaInfoCircle } from 'react-icons/fa'; @@ -21,7 +20,6 @@ import { StyledTabView } from '@/components/propertySelector/PropertySelectorTab import { PropertySelectorPidSearchContainerProps } from '@/components/propertySelector/search/PropertySelectorPidSearchContainer'; import PropertySearchSelectorPidFormView from '@/components/propertySelector/search/PropertySelectorPidSearchView'; import { ApiGen_CodeTypes_AreaUnitTypes } from '@/models/api/generated/ApiGen_CodeTypes_AreaUnitTypes'; -import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { convertArea } from '@/utils/convertUtils'; import MapSideBarLayout from '../layout/MapSideBarLayout'; @@ -159,15 +157,19 @@ const AddSubdivisionView: React.FunctionComponent< const allProperties = [...values.destinationProperties]; await properties.reduce(async (promise, property) => { return promise.then(async () => { - const formProperty = PropertyForm.fromFeatureDataset(property); + const formProperty = PropertyForm.fromLocationFeatureDataset(property); formProperty.landArea = formProperty.landArea && formProperty.areaUnit - ? getAreaValue(formProperty.landArea, formProperty.areaUnit) + ? convertArea( + formProperty.landArea, + formProperty.areaUnit.toLocaleLowerCase(), + ApiGen_CodeTypes_AreaUnitTypes.M2, + ) : 0; formProperty.areaUnit = ApiGen_CodeTypes_AreaUnitTypes.M2; if (formProperty.pid) { formProperty.address = await getPrimaryAddressByPid(formProperty.pid); - allProperties.push(formProperty.toApi()); + allProperties.push(formProperty); } else { toast.error('Selected property must have a PID'); } @@ -175,11 +177,9 @@ const AddSubdivisionView: React.FunctionComponent< }, Promise.resolve()); setFieldValue('destinationProperties', allProperties); }} - selectedComponentId="destination-property-selector" modifiedProperties={values.destinationProperties.map(dp => - PropertyForm.fromPropertyApi(dp).toFeatureDataset(), + dp.toLocationFeatureDataset(), )} - repositionSelectedProperty={noop} /> {({ remove }) => ( @@ -190,7 +190,7 @@ const AddSubdivisionView: React.FunctionComponent< @@ -225,10 +225,7 @@ const AddSubdivisionView: React.FunctionComponent< ); }; -const getDraftMarkerIndex = ( - property: ApiGen_Concepts_Property, - form: SubdivisionFormModel, -): number => { +const getDraftMarkerIndex = (property: PropertyForm, form: SubdivisionFormModel): number => { let index = form.destinationProperties.findIndex( p => p.latitude === property.latitude && diff --git a/source/frontend/src/features/properties/parcelList/ParcelItem.tsx b/source/frontend/src/features/properties/parcelList/ParcelItem.tsx index 06e2d67002..2a5dbedf27 100644 --- a/source/frontend/src/features/properties/parcelList/ParcelItem.tsx +++ b/source/frontend/src/features/properties/parcelList/ParcelItem.tsx @@ -10,26 +10,29 @@ import ManagementIcon from '@/assets/images/management-icon.svg?react'; import ResearchIcon from '@/assets/images/research-icon.svg?react'; import { RemoveIconButton } from '@/components/common/buttons'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { WorklistLocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import MoreOptionsMenu, { MenuOption } from '@/components/common/MoreOptionsMenu'; import OverflowTip from '@/components/common/OverflowTip'; import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; import { Claims } from '@/constants'; import usePathGenerator from '@/features/mapSideBar/shared/sidebarPathGenerator'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; -import { exists, getPropertyNameFromSelectedFeatureSet, NameSourceType } from '@/utils'; - -import { ParcelDataset } from './models'; +import { + exists, + getPropertyNameFromLocationFeatureSet, + latLngToKey, + NameSourceType, +} from '@/utils'; export interface IParcelItemProps { canAddToWorklist: boolean; - parcel: ParcelDataset; + parcel: LocationFeatureDataset; onRemove: (id: string) => void | null; parcelIndex: number; } export function ParcelItem({ parcel, onRemove, canAddToWorklist, parcelIndex }: IParcelItemProps) { - const propertyName = getPropertyNameFromSelectedFeatureSet(parcel.toSelectedFeatureDataset()); + const propertyName = getPropertyNameFromLocationFeatureSet(parcel); let propertyIdentifier = ''; switch (propertyName.label) { case NameSourceType.PID: @@ -45,70 +48,60 @@ export function ParcelItem({ parcel, onRemove, canAddToWorklist, parcelIndex }: break; } - const { prepareForCreation, isEditPropertiesMode, worklistAdd, setSelectedLocation } = - useMapStateMachine(); + const { + requestLocationFeatureAddition: prepareForCreation, + isEditPropertiesMode, + worklistAdd, + setSelectedLocation, + } = useMapStateMachine(); const canAddToOpenFile = isEditPropertiesMode; const pathGenerator = usePathGenerator(); const handleSelect = useCallback(() => { - setSelectedLocation(parcel.toLocationFeatureDataset()); + setSelectedLocation(parcel); }, [parcel, setSelectedLocation]); const onAddToWorklist = useCallback(() => { - const featuresSet = parcel.toSelectedFeatureDataset(); + const featuresSet = parcel; - const worklistDataSet: WorklistLocationFeatureDataset = { + const worklistDataSet: LocationFeatureDataset = { ...featuresSet, - fullyAttributedFeatures: null, }; - if (exists(featuresSet.parcelFeature)) { - worklistDataSet.fullyAttributedFeatures = { - type: 'FeatureCollection', - features: [featuresSet.parcelFeature], - }; - } - worklistAdd(worklistDataSet); }, [parcel, worklistAdd]); const handleCreateResearchFile = useCallback(() => { - const featuresSet = parcel.toSelectedFeatureDataset(); - prepareForCreation([featuresSet]); + prepareForCreation([parcel]); pathGenerator.newFile('research'); }, [parcel, pathGenerator, prepareForCreation]); const handleCreateAcquisitionFile = useCallback(() => { - const featuresSet = parcel.toSelectedFeatureDataset(); - prepareForCreation([featuresSet]); + prepareForCreation([parcel]); pathGenerator.newFile('acquisition'); }, [parcel, pathGenerator, prepareForCreation]); const handleCreateDispositionFile = useCallback(() => { - const featuresSet = parcel.toSelectedFeatureDataset(); - prepareForCreation([featuresSet]); + prepareForCreation([parcel]); pathGenerator.newFile('disposition'); }, [parcel, pathGenerator, prepareForCreation]); const handleCreateLeaseFile = useCallback(() => { - const featuresSet = parcel.toSelectedFeatureDataset(); - prepareForCreation([featuresSet]); + prepareForCreation([parcel]); pathGenerator.newFile('lease'); }, [parcel, pathGenerator, prepareForCreation]); const handleCreateManagementFile = useCallback(() => { - const featuresSet = parcel.toSelectedFeatureDataset(); - prepareForCreation([featuresSet]); + prepareForCreation([parcel]); pathGenerator.newFile('management'); }, [parcel, pathGenerator, prepareForCreation]); const handleAddToOpenFile = useCallback(() => { // If in edit properties mode, prepare the parcels for addition to an open file if (isEditPropertiesMode) { - const featuresSet = parcel.toSelectedFeatureDataset(); - prepareForCreation([featuresSet]); + prepareForCreation([parcel]); } }, [isEditPropertiesMode, parcel, prepareForCreation]); @@ -197,15 +190,15 @@ export function ParcelItem({ parcel, onRemove, canAddToWorklist, parcelIndex }: - + {exists(onRemove) && ( { e.preventDefault(); e.stopPropagation(); - onRemove(parcel.id); + onRemove(latLngToKey(parcel.location)); }} /> )} diff --git a/source/frontend/src/features/properties/parcelList/ParcelListContainer.tsx b/source/frontend/src/features/properties/parcelList/ParcelListContainer.tsx index a8972b42a3..0869e1704c 100644 --- a/source/frontend/src/features/properties/parcelList/ParcelListContainer.tsx +++ b/source/frontend/src/features/properties/parcelList/ParcelListContainer.tsx @@ -1,9 +1,10 @@ -import { ParcelDataset } from './models'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; + import { IParcelListViewProps } from './ParcelListView'; export interface IParcelListContainwerProps { View: React.FC; - parcels: ParcelDataset[]; + parcels: LocationFeatureDataset[]; } export const ParcelListContainer: React.FC = ({ parcels, View }) => { diff --git a/source/frontend/src/features/properties/parcelList/ParcelListView.tsx b/source/frontend/src/features/properties/parcelList/ParcelListView.tsx index 9a8bf4865d..77e5c0aa3e 100644 --- a/source/frontend/src/features/properties/parcelList/ParcelListView.tsx +++ b/source/frontend/src/features/properties/parcelList/ParcelListView.tsx @@ -1,12 +1,13 @@ import styled from 'styled-components'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { Section } from '@/components/common/Section/Section'; +import { latLngToKey } from '@/utils'; -import { ParcelDataset } from './models'; import ParcelItem from './ParcelItem'; export interface IParcelListViewProps { - parcels: ParcelDataset[]; + parcels: LocationFeatureDataset[]; } export const ParcelListView: React.FC = ({ parcels }) => { @@ -24,7 +25,7 @@ export const ParcelListView: React.FC = ({ parcels }) => { {parcels.map((p, index) => ( | null; - public pimsFeature: Feature | null; - public regionFeature: Feature | null; - public districtFeature: Feature | null; - - public constructor(id?: string) { - this.id = id ?? uuidv4(); - this.name = ''; - this.location = null; - this.pmbcFeature = null; - this.pimsFeature = null; - this.regionFeature = null; - this.districtFeature = null; - } - - public static fromFullyAttributedFeature( - feature: Feature | null, - ) { - const parcel = new ParcelDataset(); - parcel.pmbcFeature = feature; - return parcel; - } - - public static fromPimsFeature(feature: Feature | null) { - const parcel = new ParcelDataset(); - parcel.pimsFeature = feature; - return parcel; - } - - public static fromSelectedFeatureDataset(featureSet: SelectedFeatureDataset): ParcelDataset { - const parcel = new ParcelDataset(); - parcel.id = featureSet.id ?? uuidv4(); - parcel.location = featureSet.location; - parcel.pmbcFeature = featureSet.parcelFeature; - parcel.pimsFeature = featureSet.pimsFeature; - parcel.regionFeature = featureSet.regionFeature; - parcel.districtFeature = featureSet.districtFeature; - return parcel; - } - - public toSelectedFeatureDataset(): SelectedFeatureDataset { - return { - selectingComponentId: null, - location: this.location ?? { lat: 0, lng: 0 }, - fileLocation: this.location ?? null, - fileBoundary: null, - id: this.id, - parcelFeature: this.pmbcFeature ?? null, - pimsFeature: this.pimsFeature ?? null, - regionFeature: this.regionFeature ?? null, - districtFeature: this.districtFeature ?? null, - municipalityFeature: null, - isActive: true, - displayOrder: 0, - }; - } - - public toLocationFeatureDataset(): LocationFeatureDataset { - const locationDataset: LocationFeatureDataset = { - parcelFeatures: exists(this.pmbcFeature) ? [this.pmbcFeature] : null, - pimsFeatures: exists(this.pimsFeature) ? [this.pimsFeature] : null, - regionFeature: this.regionFeature ?? null, - districtFeature: this.districtFeature ?? null, - municipalityFeatures: null, - highwayFeatures: null, - crownLandLeasesFeatures: null, - crownLandLicensesFeatures: null, - crownLandTenuresFeatures: null, - crownLandInventoryFeatures: null, - crownLandInclusionsFeatures: null, - selectingComponentId: null, - location: this.location, - fileLocation: null, - }; - return locationDataset; - } -} diff --git a/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx b/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx index 6f70fedebd..3c0e7fb638 100644 --- a/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx @@ -6,7 +6,7 @@ import { act, render, RenderOptions, screen } from '@/utils/test-utils'; import { useWorklistContext } from './context/WorklistContext'; import { WorklistContainer } from './WorklistContainer'; import { IWorklistViewProps } from './WorklistView'; -import { ParcelDataset } from '../parcelList/models'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; vi.mock('./context/WorklistContext'); @@ -25,7 +25,7 @@ vi.mock('leaflet', async () => { }); // Parcel list mock -let mockParcels: ParcelDataset[] = []; +let mockParcels: LocationFeatureDataset[] = []; // Mocks const select = vi.fn(); diff --git a/source/frontend/src/features/properties/worklist/WorklistContainer.tsx b/source/frontend/src/features/properties/worklist/WorklistContainer.tsx index 15d5e1177e..bda41514b7 100644 --- a/source/frontend/src/features/properties/worklist/WorklistContainer.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistContainer.tsx @@ -12,7 +12,8 @@ export interface IWorklistContainerProps { export const WorklistContainer: React.FC = ({ View }) => { const { parcels, remove, clearAll } = useWorklistContext(); - const { prepareForCreation, isEditPropertiesMode } = useMapStateMachine(); + const { requestLocationFeatureAddition: prepareForCreation, isEditPropertiesMode } = + useMapStateMachine(); const pathGenerator = usePathGenerator(); // Handle creating a research file from the worklist @@ -20,8 +21,7 @@ export const WorklistContainer: React.FC = ({ View }) = if (parcels.length === 0) { return; } - const featuresSets = parcels.map(p => p.toSelectedFeatureDataset()); - prepareForCreation(featuresSets); + prepareForCreation(parcels); pathGenerator.newFile('research'); }, [parcels, pathGenerator, prepareForCreation]); @@ -30,8 +30,7 @@ export const WorklistContainer: React.FC = ({ View }) = if (parcels.length === 0) { return; } - const featuresSets = parcels.map(p => p.toSelectedFeatureDataset()); - prepareForCreation(featuresSets); + prepareForCreation(parcels); pathGenerator.newFile('acquisition'); }, [parcels, pathGenerator, prepareForCreation]); @@ -40,8 +39,7 @@ export const WorklistContainer: React.FC = ({ View }) = if (parcels.length === 0) { return; } - const featuresSets = parcels.map(p => p.toSelectedFeatureDataset()); - prepareForCreation(featuresSets); + prepareForCreation(parcels); pathGenerator.newFile('disposition'); }, [parcels, pathGenerator, prepareForCreation]); @@ -50,8 +48,7 @@ export const WorklistContainer: React.FC = ({ View }) = if (parcels.length === 0) { return; } - const featuresSets = parcels.map(p => p.toSelectedFeatureDataset()); - prepareForCreation(featuresSets); + prepareForCreation(parcels); pathGenerator.newFile('lease'); }, [parcels, pathGenerator, prepareForCreation]); @@ -60,16 +57,14 @@ export const WorklistContainer: React.FC = ({ View }) = if (parcels.length === 0) { return; } - const featuresSets = parcels.map(p => p.toSelectedFeatureDataset()); - prepareForCreation(featuresSets); + prepareForCreation(parcels); pathGenerator.newFile('management'); }, [parcels, pathGenerator, prepareForCreation]); const handleAddToOpenFile = useCallback(() => { // If in edit properties mode, prepare the parcels for addition to an open file if (parcels.length > 0 && isEditPropertiesMode) { - const featuresSets = parcels.map(p => p.toSelectedFeatureDataset()); - prepareForCreation(featuresSets); + prepareForCreation(parcels); } }, [isEditPropertiesMode, parcels, prepareForCreation]); diff --git a/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.test.tsx b/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.test.tsx index 8fb2fcafe4..89ce9a9576 100644 --- a/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.test.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.test.tsx @@ -1,13 +1,16 @@ import * as turf from '@turf/turf'; import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext'; -import { WorklistLocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { emptyPmbcParcel } from '@/models/layers/parcelMapBC'; import { render, RenderOptions } from '@/utils/test-utils'; import { WorklistMapClickMonitor } from './WorklistMapClickMonitor'; import { useWorklistContext } from './context/WorklistContext'; +import { + emptyFeatureDataset, + LocationFeatureDataset, +} from '@/components/common/mapFSM/useLocationFeatureLoader'; vi.mock('./context/WorklistContext'); @@ -28,7 +31,7 @@ vi.mocked(useWorklistContext, { partial: true }).mockReturnValue({ }); // Shared mock state -let mockPrevious: WorklistLocationFeatureDataset = undefined; +let mockPrevious: LocationFeatureDataset = undefined; describe('WorklistMapClickMonitor', () => { const setup = (renderOptions: RenderOptions = {}) => { @@ -47,11 +50,8 @@ describe('WorklistMapClickMonitor', () => { const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, worklistLocationFeatureDataset: { + ...emptyFeatureDataset(), location: { lat: 49, lng: -123 }, - districtFeature: null, - regionFeature: null, - fullyAttributedFeatures: null, - pimsFeature: null, }, }; @@ -62,24 +62,20 @@ describe('WorklistMapClickMonitor', () => { it('adds range when new dataset has valid features', () => { mockPrevious = { + ...emptyFeatureDataset(), location: { lat: 49, lng: -123 }, - districtFeature: null, - regionFeature: null, - fullyAttributedFeatures: null, - pimsFeature: null, }; const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, worklistLocationFeatureDataset: { + ...emptyFeatureDataset(), location: { lat: 49.2, lng: -123.1 }, - districtFeature: null, - regionFeature: null, - fullyAttributedFeatures: turf.featureCollection([ + + parcelFeatures: turf.featureCollection([ turf.point([-123.1, 49.2], { ...emptyPmbcParcel }), turf.point([-75.3, 39.9], { ...emptyPmbcParcel }), - ]), - pimsFeature: null, + ]).features, }, }; @@ -90,27 +86,22 @@ describe('WorklistMapClickMonitor', () => { it('adds single parcel by lat/lng when no features exist', () => { mockPrevious = { + ...emptyFeatureDataset(), location: { lat: 49, lng: -123 }, - districtFeature: null, - regionFeature: null, - fullyAttributedFeatures: null, - pimsFeature: null, }; const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, worklistLocationFeatureDataset: { + ...emptyFeatureDataset(), location: { lat: 50, lng: -123 }, - districtFeature: null, - regionFeature: null, - fullyAttributedFeatures: turf.featureCollection([]), - pimsFeature: null, + parcelFeatures: [], }, }; setup({ mockMapMachine: testMockMachine }); expect(add).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ location: { lat: 50, lng: -123 }, }), ); @@ -118,14 +109,11 @@ describe('WorklistMapClickMonitor', () => { }); it('does not re-add if same dataset is passed again', () => { - const dataset: WorklistLocationFeatureDataset = { + const dataset: LocationFeatureDataset = { + ...emptyFeatureDataset(), location: { lat: 49, lng: -123 }, - districtFeature: null, - regionFeature: null, - fullyAttributedFeatures: turf.featureCollection([ - turf.point([-123, 49], { ...emptyPmbcParcel }), - ]), - pimsFeature: null, + parcelFeatures: turf.featureCollection([turf.point([-123, 49], { ...emptyPmbcParcel })]) + .features, }; mockPrevious = dataset; diff --git a/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.tsx b/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.tsx index e8a0a0a8e9..93d3dcf661 100644 --- a/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistMapClickMonitor.tsx @@ -1,9 +1,12 @@ import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import { + emptyFeatureDataset, + LocationFeatureDataset, +} from '@/components/common/mapFSM/useLocationFeatureLoader'; import { usePrevious } from '@/hooks/usePrevious'; import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; import { exists } from '@/utils'; -import { ParcelDataset } from '../parcelList/models'; import { useWorklistContext } from './context/WorklistContext'; export const WorklistMapClickMonitor: React.FunctionComponent = () => { @@ -18,16 +21,20 @@ export const WorklistMapClickMonitor: React.FunctionComponent = () => { previous !== undefined ) { // Loop over the location featurecollection, adding it to the worklist if the parcelFeature is not there already - const worklistParcels: ParcelDataset[] = []; + const worklistParcels: LocationFeatureDataset[] = []; - if (exists(worklistLocationFeatureDataset.fullyAttributedFeatures)) { + if (exists(worklistLocationFeatureDataset.parcelFeatures)) { const pmbcParcels = - worklistLocationFeatureDataset.fullyAttributedFeatures?.features + worklistLocationFeatureDataset.parcelFeatures ?.map(pmbcFeature => { - const newParcel = ParcelDataset.fromFullyAttributedFeature(pmbcFeature); - newParcel.location = worklistLocationFeatureDataset.location; - newParcel.regionFeature = worklistLocationFeatureDataset.regionFeature; - newParcel.districtFeature = worklistLocationFeatureDataset.districtFeature; + const newParcel: LocationFeatureDataset = { + ...emptyFeatureDataset(), + parcelFeatures: [pmbcFeature], + location: worklistLocationFeatureDataset.location, + regionFeature: worklistLocationFeatureDataset.regionFeature, + districtFeature: worklistLocationFeatureDataset.districtFeature, + }; + return newParcel; }) .filter(exists) ?? []; @@ -35,24 +42,34 @@ export const WorklistMapClickMonitor: React.FunctionComponent = () => { worklistParcels.push(...pmbcParcels); } - if (exists(worklistLocationFeatureDataset.pimsFeature)) { - const pimsParcel = ParcelDataset.fromPimsFeature( - worklistLocationFeatureDataset.pimsFeature, - ); - pimsParcel.location = worklistLocationFeatureDataset.location; - pimsParcel.regionFeature = worklistLocationFeatureDataset.regionFeature; - pimsParcel.districtFeature = worklistLocationFeatureDataset.districtFeature; + if (exists(worklistLocationFeatureDataset.pimsFeatures)) { + const pimsParcels = + worklistLocationFeatureDataset.pimsFeatures + ?.map(pimsFeature => { + const newParcel: LocationFeatureDataset = { + ...emptyFeatureDataset(), + pimsFeatures: [pimsFeature], + location: worklistLocationFeatureDataset.location, + regionFeature: worklistLocationFeatureDataset.regionFeature, + districtFeature: worklistLocationFeatureDataset.districtFeature, + }; + + return newParcel; + }) + .filter(exists) ?? []; - worklistParcels.push(pimsParcel); + worklistParcels.push(...pimsParcels); } if (worklistParcels.length > 0) { addRange(worklistParcels); } else { // We didn't find any parcel-map properties - add a lat/long location to the worklist - const latLongParcel = new ParcelDataset(); - latLongParcel.location = worklistLocationFeatureDataset.location; - latLongParcel.pmbcFeature = null; + const latLongParcel: LocationFeatureDataset = { + ...emptyFeatureDataset(), + location: worklistLocationFeatureDataset.location, + }; + add(latLongParcel); } } diff --git a/source/frontend/src/features/properties/worklist/WorklistView.tsx b/source/frontend/src/features/properties/worklist/WorklistView.tsx index da4fd59e0b..3da7d9f8cc 100644 --- a/source/frontend/src/features/properties/worklist/WorklistView.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistView.tsx @@ -8,17 +8,17 @@ import DispositionIcon from '@/assets/images/disposition-icon.svg?react'; import LeaseIcon from '@/assets/images/lease-icon.svg?react'; import ManagementIcon from '@/assets/images/management-icon.svg?react'; import ResearchIcon from '@/assets/images/research-icon.svg?react'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import MoreOptionsMenu, { MenuOption } from '@/components/common/MoreOptionsMenu'; import { Scrollable } from '@/components/common/Scrollable/Scrollable'; import { Section } from '@/components/common/Section/Section'; import { Claims } from '@/constants'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; -import { ParcelDataset } from '../parcelList/models'; import ParcelItem from '../parcelList/ParcelItem'; export interface IWorklistViewProps { - parcels: ParcelDataset[]; + parcels: LocationFeatureDataset[]; canAddToOpenFile?: boolean; onRemove: (id: string) => void; onClearAll: () => void; @@ -129,7 +129,7 @@ export const WorklistView: React.FC = ({ {parcels.map((p, index) => ( { vi.clearAllMocks(); }); - const renderWorklistHook = (initial: ParcelDataset[] = []) => + const renderWorklistHook = (initial: LocationFeatureDataset[] = []) => renderHook(() => useWorklistContext(), { wrapper: ({ children }) => ( @@ -51,7 +52,7 @@ describe('WorklistContextProvider', () => { const a = getMockWorklistParcel('a'); const b = getMockWorklistParcel('b'); const { result } = renderWorklistHook([a, b]); - expect(result.current.parcels.map(p => p.id)).toEqual(['a', 'b']); + expect(result.current.parcels.map(p => latLngToKey(p.location))).toEqual(['a', 'b']); }); it('select() sets selectedId', () => { @@ -66,7 +67,7 @@ describe('WorklistContextProvider', () => { act(() => result.current.add(parcel)); expect(result.current.parcels).toHaveLength(1); - expect(result.current.parcels[0].id).toBe('1'); + //expect(result.current.parcels[0].id).toBe('1'); expect(mockNotifier.error).not.toHaveBeenCalled(); }); @@ -155,11 +156,9 @@ describe('WorklistContextProvider', () => { act(() => result.current.addRange([p1, dupe, p2])); - expect(result.current.parcels.map(p => p.pmbcFeature?.properties?.PID)).toEqual([ - '111', - '222', - '333', - ]); + expect(result.current.parcels.map(p => firstOrNull(p.parcelFeatures)?.properties?.PID)).toEqual( + ['111', '222', '333'], + ); expect(mockNotifier.success).toHaveBeenCalledWith('Added 2 new parcel(s).'); expect(mockNotifier.warn).toHaveBeenCalledWith('1 duplicate parcel(s) were skipped.'); }); @@ -199,7 +198,7 @@ describe('WorklistContextProvider', () => { const { result } = renderWorklistHook([a, b]); act(() => result.current.remove('a')); - expect(result.current.parcels.map(p => p.id)).toEqual(['b']); + expect(result.current.parcels.map(p => latLngToKey(p.location))).toEqual(['b']); }); it('clearAll() removes all parcels', () => { diff --git a/source/frontend/src/features/properties/worklist/context/WorklistContext.tsx b/source/frontend/src/features/properties/worklist/context/WorklistContext.tsx index 25a2609203..c40c144e40 100644 --- a/source/frontend/src/features/properties/worklist/context/WorklistContext.tsx +++ b/source/frontend/src/features/properties/worklist/context/WorklistContext.tsx @@ -1,9 +1,8 @@ import { createContext, ReactNode, useCallback, useContext, useState } from 'react'; import { toast } from 'react-toastify'; -import { exists, pidFromFeatureSet, pinFromFeatureSet, planFromFeatureSet } from '@/utils'; - -import { ParcelDataset } from '../../parcelList/models'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { areFeatureDatasetsEqual, areLocationFeatureDatasetsEqual, latLngToKey } from '@/utils'; export interface IWorklistNotifier { error: (msg: string) => void; @@ -12,12 +11,12 @@ export interface IWorklistNotifier { } export interface IWorklistContext { - parcels: ParcelDataset[]; + parcels: LocationFeatureDataset[]; selectedId: string | null; select: (id: string) => void; remove: (id: string) => void; - add: (parcel: ParcelDataset) => void; - addRange: (parcel: ParcelDataset[]) => void; + add: (parcel: LocationFeatureDataset) => void; + addRange: (parcel: LocationFeatureDataset[]) => void; clearAll: () => void; } @@ -33,7 +32,7 @@ export function useWorklistContext() { export interface IWorklistContextProviderProps { children: ReactNode; - parcels?: ParcelDataset[]; + parcels?: LocationFeatureDataset[]; /** Override the default react‑toastify notifier in tests or other environments */ notifier?: IWorklistNotifier; } @@ -43,17 +42,20 @@ export function WorklistContextProvider({ parcels: initialParcels, notifier = toast, // default is react‑toastify’s toast object }: IWorklistContextProviderProps) { - const [parcels, setParcels] = useState(initialParcels ?? []); + const [parcels, setParcels] = useState(initialParcels ?? []); const [selectedId, setSelectedId] = useState(null); const select = useCallback((id: string) => setSelectedId(id), []); - const remove = useCallback((id: string) => setParcels(prev => prev.filter(p => p.id !== id)), []); + const remove = useCallback( + (id: string) => setParcels(prev => prev.filter(p => latLngToKey(p.location) !== id)), + [], + ); // The worklist should not allow duplicate property (using pid/pin/globalUID, lat/lng) const add = useCallback( - (parcel: ParcelDataset) => { + (parcel: LocationFeatureDataset) => { setParcels(prev => { - const alreadyExists = prev.some(p => areParcelsEqual(p, parcel)); + const alreadyExists = prev.some(p => areFeatureDatasetsEqual(p, parcel)); if (alreadyExists) { notifier.error('Duplicate parcel detected. Add to worklist skipped.'); return prev; @@ -65,10 +67,12 @@ export function WorklistContextProvider({ ); const addRange = useCallback( - (newParcels: ParcelDataset[]) => { + (newParcels: LocationFeatureDataset[]) => { setParcels(prev => { const uniqueParcels = newParcels.filter(newParcel => { - return !prev.some(existingParcel => areParcelsEqual(existingParcel, newParcel)); + return !prev.some(existingParcel => + areLocationFeatureDatasetsEqual(existingParcel, newParcel), + ); }); const duplicatesSkipped = newParcels.length - uniqueParcels.length; @@ -97,65 +101,3 @@ export function WorklistContextProvider({ ); } - -function areParcelsEqual(p1: ParcelDataset, p2: ParcelDataset): boolean { - if (!exists(p1) || !exists(p2)) { - return false; - } - - if (p1.id === p2.id) { - return true; - } - const fs1 = p1.toSelectedFeatureDataset(); - const fs2 = p2.toSelectedFeatureDataset(); - - const pid1 = pidFromFeatureSet(fs1) ?? null; - const pid2 = pidFromFeatureSet(fs2) ?? null; - if (exists(pid1) && pid1 === pid2) { - return true; - } - - if (exists(pid1) && pid1 === pid2) { - return true; - } - - const pin1 = pinFromFeatureSet(fs1) ?? null; - const pin2 = pinFromFeatureSet(fs2) ?? null; - - if (exists(pin1) && pin1 === pin2) { - return true; - } - - // Some parcels are only identified by their plan-number (e.g. common strata, parks) - // Only consider plan-number as an identifier when there are no PID/PIN - const planNumber1 = planFromFeatureSet(fs1) ?? null; - const planNumber2 = planFromFeatureSet(fs2) ?? null; - if ( - exists(planNumber1) && - !exists(pid1) && - !exists(pin1) && - !exists(pid2) && - !exists(pin2) && - planNumber1 === planNumber2 - ) { - return true; - } - - // Only consider lat/long when there are no PID/PIN - const location1 = p1.location; - const location2 = p2.location; - if ( - exists(location1) && - exists(location2) && - !exists(pid1) && - !exists(pin1) && - !exists(pid2) && - !exists(pin2) && - location1.lat === location2.lat && - location1.lng === location2.lng - ) { - return true; - } - - return false; -} diff --git a/source/frontend/src/hooks/layer-api/useGeoServer.ts b/source/frontend/src/hooks/layer-api/useGeoServer.ts index 3cf766a3c4..621c1f6099 100644 --- a/source/frontend/src/hooks/layer-api/useGeoServer.ts +++ b/source/frontend/src/hooks/layer-api/useGeoServer.ts @@ -3,6 +3,10 @@ import { Feature, FeatureCollection, Geometry, Point } from 'geojson'; import { useCallback, useContext } from 'react'; import CustomAxios from '@/customAxios'; +import { + PIMS_Property_Boundary_View, + PIMS_Property_Location_View, +} from '@/models/layers/pimsPropertyLocationView'; import { TenantContext } from '@/tenants'; import { useAxiosErrorHandler } from '@/utils'; @@ -28,7 +32,9 @@ export const useGeoServer = () => { return null; } const wfsUrl = buildUrl(baseUrl, { PROPERTY_ID: id }); - const { data } = await CustomAxios().get(wfsUrl); + const { data } = await CustomAxios().get< + FeatureCollection + >(wfsUrl); return data.features?.length ? (data.features[0] as Feature) : null; }, [baseUrl], @@ -37,7 +43,9 @@ export const useGeoServer = () => { requestFunction: useCallback( async (id: number) => { const wfsUrl = buildUrl(baseUrl, { PROPERTY_ID: id }); - return await CustomAxios().get(wfsUrl); + return await CustomAxios().get>( + wfsUrl, + ); }, [baseUrl], ), @@ -47,7 +55,7 @@ export const useGeoServer = () => { const getPropertyByBoundaryWfsWrapper = useApiRequestWrapper({ requestFunction: useCallback( async (boundary: Geometry) => { - return await CustomAxios().get( + return await CustomAxios().get>( `${baseBoundaryUrl}&cql_filter=INTERSECTS(GEOMETRY,SRID=4326;${geojsonToWKT(boundary)})`, ); }, diff --git a/source/frontend/src/hooks/repositories/useComposedProperties.ts b/source/frontend/src/hooks/repositories/useComposedProperties.ts index 3832499714..2e0fa9e174 100644 --- a/source/frontend/src/hooks/repositories/useComposedProperties.ts +++ b/source/frontend/src/hooks/repositories/useComposedProperties.ts @@ -3,6 +3,7 @@ import { AxiosResponse } from 'axios'; import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { emptyFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { ComposedProperty } from '@/features/mapSideBar/property/ComposedProperty'; import { LtsaOrders, SpcpOrder } from '@/interfaces/ltsaModels'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; @@ -227,19 +228,8 @@ export const useComposedProperties = ({ spcpOrder: undefined, pimsProperty: undefined, propertyAssociations: undefined, - parcelMapFeatureCollection: undefined, - pimsGeoserverFeatureCollection: undefined, bcAssessmentSummary: undefined, - crownTenureFeatures: undefined, - crownLeaseFeatures: undefined, - crownLicenseFeatures: undefined, - crownInclusionFeatures: undefined, - crownInventoryFeatures: undefined, - highwayFeatures: undefined, - municipalityFeatures: undefined, - firstNationFeatures: undefined, - electoralFeatures: undefined, - alrFeatures: undefined, + featureDataset: emptyFeatureDataset(), }); const typeCheckWrapper = useDeepCompareCallback( @@ -397,16 +387,19 @@ export const useComposedProperties = ({ spcpOrder: getStrataPlanCommonProperty.response, pimsProperty: getPropertyWrapper.response, propertyAssociations: getPropertyAssociationsWrapper.response, - parcelMapFeatureCollection: parcelResponse, - pimsGeoserverFeatureCollection: - getPropertyWfsWrapper.response ?? getPropertyByBoundaryWfsWrapper.response, bcAssessmentSummary: getSummaryWrapper.response, - ...crownResponse, - highwayFeatures: highwayResponse, - municipalityFeatures: municipalityResponse, - electoralFeatures: electoralResponse, - alrFeatures: alrResponse, - firstNationFeatures: firstNationsResponse, + featureDataset: { + ...emptyFeatureDataset(), + ...crownResponse, + pimsFeatures: getPropertyWfsWrapper.response?.features, + pimsBoundaryFeatures: getPropertyByBoundaryWfsWrapper.response?.features, + parcelFeatures: parcelResponse?.features, + highwayFeatures: highwayResponse, + municipalityFeatures: municipalityResponse, + electoralFeatures: electoralResponse, + alrFeatures: alrResponse, + firstNationFeatures: firstNationsResponse, + }, }); }, [ setComposedProperty, diff --git a/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts b/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts index 04d47610d1..4fec59f896 100644 --- a/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts +++ b/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts @@ -1,16 +1,17 @@ import debounce from 'lodash/debounce'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { LocationBoundaryDataset } from '@/components/common/mapFSM/models'; -import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; +import { PropertyForm } from '@/features/mapSideBar/shared/models'; import useIsMounted from '@/hooks/util/useIsMounted'; +import useDeepCompareEffect from './util/useDeepCompareEffect'; + /** * A hook that automatically syncs any updates to the lat/lngs of the parcel form with the map. * @param modifiedProperties array that contains the properties to be drawn. */ -const useDraftMarkerSynchronizer = (modifiedProperties: LocationBoundaryDataset[]) => { +const useDraftMarkerSynchronizer = (modifiedProperties: PropertyForm[]) => { const isMounted = useIsMounted(); const { setFilePropertyLocations } = useMapStateMachine(); @@ -20,9 +21,9 @@ const useDraftMarkerSynchronizer = (modifiedProperties: LocationBoundaryDataset[ * @param modifiedProperties the current properties */ const synchronizeMarkers = useCallback( - (modifiedProperties: LocationBoundaryDataset[]) => { + (modifiedProperties: PropertyForm[]) => { if (isMounted()) { - const filePropertyLocations = modifiedProperties; + const filePropertyLocations = modifiedProperties.map(x => x.toFilePropertyApi()); if (filePropertyLocations.length > 0) { setFilePropertyLocations(filePropertyLocations); } else { @@ -34,7 +35,7 @@ const useDraftMarkerSynchronizer = (modifiedProperties: LocationBoundaryDataset[ ); const synchronize = useRef( - debounce((modifiedProperties: LocationBoundaryDataset[]) => { + debounce((modifiedProperties: PropertyForm[]) => { synchronizeMarkers(modifiedProperties); }, 400), ).current; @@ -42,10 +43,6 @@ const useDraftMarkerSynchronizer = (modifiedProperties: LocationBoundaryDataset[ useDeepCompareEffect(() => { synchronize(modifiedProperties); }, [modifiedProperties, synchronize]); - - useEffect(() => { - return () => setFilePropertyLocations([]); - }, [setFilePropertyLocations]); }; export default useDraftMarkerSynchronizer; diff --git a/source/frontend/src/hooks/useEditPropertiesNotifier.ts b/source/frontend/src/hooks/useEditPropertiesNotifier.ts deleted file mode 100644 index a2f5ce821a..0000000000 --- a/source/frontend/src/hooks/useEditPropertiesNotifier.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { FormikProps } from 'formik'; -import { useEffect, useMemo } from 'react'; - -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { PropertyForm } from '@/features/mapSideBar/shared/models'; -import { addPropertiesToCurrentFile } from '@/utils/propertyUtils'; -import { exists } from '@/utils/utils'; - -import { useEditPropertiesMode } from './useEditPropertiesMode'; -import { useFeatureDatasetsWithAddresses } from './useFeatureDatasetsWithAddresses'; - -/** - * Notifies the map state machine to enter/exit edit properties mode. - * Use this hook in any component that needs to toggle edit properties mode on mount/unmount. - */ -export function useEditPropertiesNotifier( - formikRef: React.RefObject>, - fieldName: keyof T, - overrideFeatures?: SelectedFeatureDataset[], -) { - const { selectedFeatures, processCreation } = useMapStateMachine(); - // Get PropertyForms with addresses for all selected features - const { featuresWithAddresses, bcaLoading } = useFeatureDatasetsWithAddresses( - overrideFeatures ?? selectedFeatures ?? [], - ); - - useEditPropertiesMode(); // if we are listening to property add notifications we must tell the state machine we are in edit mode. - - // Convert SelectedFeatureDataset to PropertyForm - const propertyForms = useMemo( - () => - featuresWithAddresses.map(obj => { - const property = PropertyForm.fromFeatureDataset(obj.feature); - if (exists(obj.address)) { - property.address = obj.address; - } - return property; - }), - [featuresWithAddresses], - ); - // This effect is used to update the file properties when "add to open file" is clicked in the worklist. - useEffect(() => { - if (exists(formikRef?.current) && propertyForms.length > 0) { - addPropertiesToCurrentFile(formikRef, fieldName, propertyForms, processCreation); - } - }, [fieldName, formikRef, processCreation, propertyForms]); - - return { featuresWithAddresses, bcaLoading }; -} diff --git a/source/frontend/src/hooks/useEnrichWithPimsFeatures.ts b/source/frontend/src/hooks/useEnrichWithPimsFeatures.ts index 8e3ef55da5..bd4b8c750c 100644 --- a/source/frontend/src/hooks/useEnrichWithPimsFeatures.ts +++ b/source/frontend/src/hooks/useEnrichWithPimsFeatures.ts @@ -1,32 +1,32 @@ import { Feature, Geometry } from 'geojson'; import { useCallback, useState } from 'react'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { PIMS_Property_Boundary_View } from '@/models/layers/pimsPropertyLocationView'; -import { exists, isValidString, pidFromFeatureSet, pinFromFeatureSet } from '@/utils'; +import { isEmptyOrNull, isValidString, pidFromFeatureSet, pinFromFeatureSet } from '@/utils'; import { usePimsPropertyLayer } from './repositories/mapLayer/usePimsPropertyLayer'; interface UseEnrichWithPimsFeaturesResult { - datasets: SelectedFeatureDataset[]; + datasets: LocationFeatureDataset[]; loading: boolean; error: string | null; - enrichWithPimsFeatures: (inputDatasets: SelectedFeatureDataset[]) => Promise; + enrichWithPimsFeatures: (inputDatasets: LocationFeatureDataset[]) => Promise; } /** * Hook that enriches SelectedFeatureDataset[] with PIMS features using findOneByPidOrPin. */ export function useEnrichWithPimsFeatures(): UseEnrichWithPimsFeaturesResult { - const [datasets, setDatasets] = useState([]); + const [datasets, setDatasets] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { findOneByPidOrPin } = usePimsPropertyLayer(); const enrichWithPimsFeatures = useCallback( - async (inputDatasets: SelectedFeatureDataset[]) => { - if (!exists(inputDatasets) || inputDatasets.length === 0) { + async (inputDatasets: LocationFeatureDataset[]) => { + if (isEmptyOrNull(inputDatasets)) { setDatasets([]); return; } @@ -36,8 +36,12 @@ export function useEnrichWithPimsFeatures(): UseEnrichWithPimsFeaturesResult { try { const updatedPropertyPromises = inputDatasets.map(async dataset => { - if (!exists(dataset.parcelFeature)) return dataset; - if (exists(dataset.pimsFeature)) return dataset; // already enriched + if (isEmptyOrNull(dataset.parcelFeatures)) { + return dataset; + } + if (!isEmptyOrNull(dataset.pimsFeatures)) { + return dataset; // already enriched + } const pid = pidFromFeatureSet(dataset); const pin = pinFromFeatureSet(dataset); @@ -47,7 +51,7 @@ export function useEnrichWithPimsFeatures(): UseEnrichWithPimsFeaturesResult { try { const pimsFeature: Feature | undefined = await findOneByPidOrPin(pid, pin); - dataset.pimsFeature = pimsFeature ?? null; + dataset.pimsFeatures = [pimsFeature]; return dataset; } catch (innerErr) { return dataset; // leave dataset unchanged if API fails diff --git a/source/frontend/src/hooks/useInitialMapSelectorProperties.ts b/source/frontend/src/hooks/useInitialMapSelectorProperties.ts index 3271cb85ef..fb323c98a1 100644 --- a/source/frontend/src/hooks/useInitialMapSelectorProperties.ts +++ b/source/frontend/src/hooks/useInitialMapSelectorProperties.ts @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { AddressForm, PropertyForm } from '@/features/mapSideBar/shared/models'; import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; import { pidFromFeatureSet } from '@/utils/mapPropertyUtils'; -export const useInitialMapSelectorProperties = (selectedFeature: SelectedFeatureDataset | null) => { +export const useInitialMapSelectorProperties = (selectedFeature: LocationFeatureDataset | null) => { const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); const [bcaAddress, setBcaAddress] = useState(); @@ -24,7 +24,7 @@ export const useInitialMapSelectorProperties = (selectedFeature: SelectedFeature initialProperty: selectedFeature !== null ? { - ...PropertyForm.fromFeatureDataset(selectedFeature), + ...PropertyForm.fromLocationFeatureDataset(selectedFeature), address: bcaAddress, } : null, diff --git a/source/frontend/src/hooks/useFeatureDatasetsWithAddresses.ts b/source/frontend/src/hooks/useLocationFeatureDatasetsWithAddresses.ts similarity index 71% rename from source/frontend/src/hooks/useFeatureDatasetsWithAddresses.ts rename to source/frontend/src/hooks/useLocationFeatureDatasetsWithAddresses.ts index b27f4e26d8..368327679e 100644 --- a/source/frontend/src/hooks/useFeatureDatasetsWithAddresses.ts +++ b/source/frontend/src/hooks/useLocationFeatureDatasetsWithAddresses.ts @@ -1,24 +1,26 @@ import { useEffect, useState } from 'react'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { AddressForm } from '@/features/mapSideBar/shared/models'; import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; -import { exists } from '@/utils'; +import { isEmptyOrNull } from '@/utils'; import { pidFromFeatureSet } from '@/utils/mapPropertyUtils'; -export interface FeatureDatasetWithAddress { - feature: SelectedFeatureDataset; +export interface LocationFeatureDatasetWithAddress { + feature: LocationFeatureDataset; address?: AddressForm; } -export const useFeatureDatasetsWithAddresses = (features: SelectedFeatureDataset[] | null) => { +export const useLocationFeatureDatasetsWithAddresses = ( + features: LocationFeatureDataset[] | null, +) => { const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); - const [results, setResults] = useState([]); + const [results, setResults] = useState([]); useEffect(() => { let isMounted = true; const fetchAddresses = async () => { - if (!exists(features) || features.length === 0) { + if (isEmptyOrNull(features)) { setResults([]); return; } @@ -46,5 +48,5 @@ export const useFeatureDatasetsWithAddresses = (features: SelectedFeatureDataset }; }, [features, getPrimaryAddressByPid]); - return { featuresWithAddresses: results, bcaLoading }; + return { locationFeaturesWithAddresses: results, bcaLoading }; }; diff --git a/source/frontend/src/hooks/useEditPropertiesNotifier.test.ts b/source/frontend/src/hooks/usePropertyFormSyncronizer.test.ts similarity index 69% rename from source/frontend/src/hooks/useEditPropertiesNotifier.test.ts rename to source/frontend/src/hooks/usePropertyFormSyncronizer.test.ts index a103c8265e..4e2db651a0 100644 --- a/source/frontend/src/hooks/useEditPropertiesNotifier.test.ts +++ b/source/frontend/src/hooks/usePropertyFormSyncronizer.test.ts @@ -3,14 +3,14 @@ import { FormikProps } from 'formik'; import React from 'react'; import * as MapStateMachineContext from '@/components/common/mapFSM/MapStateMachineContext'; -import * as FeatureDatasetsHook from './useFeatureDatasetsWithAddresses'; +import * as FeatureDatasetsHook from './useLocationFeatureDatasetsWithAddresses'; import * as EditPropertiesModeHook from './useEditPropertiesMode'; -import { useEditPropertiesNotifier } from './useEditPropertiesNotifier'; import { PropertyForm } from '@/features/mapSideBar/shared/models'; import { toast } from 'react-toastify'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; -import { FeatureDatasetWithAddress } from './useFeatureDatasetsWithAddresses'; +import { LocationFeatureDatasetWithAddress } from './useLocationFeatureDatasetsWithAddresses'; import { vi, Mock } from 'vitest'; +import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; +import { usePropertyFormSyncronizer } from './usePropertyFormSyncronizer'; vi.mock('./useEditPropertiesMode', () => ({ useEditPropertiesMode: vi.fn(), @@ -22,7 +22,7 @@ vi.mock('./useFeatureDatasetsWithAddresses', () => ({ const toastSuccessSpy = vi.spyOn(toast, 'success'); const toastWarnSpy = vi.spyOn(toast, 'warn'); -describe('useEditPropertiesNotifier', () => { +describe('usePropertyFormSyncronizer', () => { let mockProcessCreation: Mock; let mockFormikRef: React.RefObject>; @@ -43,36 +43,36 @@ describe('useEditPropertiesNotifier', () => { processCreation: mockProcessCreation, } as any); - (FeatureDatasetsHook.useFeatureDatasetsWithAddresses as any).mockReturnValue({ + (FeatureDatasetsHook.useLocationFeatureDatasetsWithAddresses as any).mockReturnValue({ featuresWithAddresses: [ - { feature: getMockSelectedFeatureDataset() } as FeatureDatasetWithAddress, + { feature: getMockSelectedFeatureDataset() } as LocationFeatureDatasetWithAddress, ], bcaLoading: false, }); }); it('calls useEditPropertiesMode and returns featuresWithAddresses and bcaLoading', () => { - const { result } = renderHook(() => useEditPropertiesNotifier(mockFormikRef, 'properties')); + const { result } = renderHook(() => usePropertyFormSyncronizer(mockFormikRef, 'properties')); expect(EditPropertiesModeHook.useEditPropertiesMode).toHaveBeenCalled(); expect(result.current.featuresWithAddresses).toEqual([ { feature: getMockSelectedFeatureDataset() }, ]); - expect(result.current.bcaLoading).toBe(false); + expect(result.current.isLoading).toBe(false); }); it('uses overrideFeatures if provided', () => { const overrideFeatures = [{ feature: { id: 2 }, address: '456 Side St' }]; renderHook(() => - useEditPropertiesNotifier(mockFormikRef, 'properties', overrideFeatures as any), + usePropertyFormSyncronizer(mockFormikRef, 'properties', overrideFeatures as any), ); - expect(FeatureDatasetsHook.useFeatureDatasetsWithAddresses).toHaveBeenCalledWith( + expect(FeatureDatasetsHook.useLocationFeatureDatasetsWithAddresses).toHaveBeenCalledWith( overrideFeatures, ); }); it('calls setFieldValue, setFieldTouched, and shows success toast when unique properties are added', () => { mockFormikRef.current.values = { properties: [] }; - (FeatureDatasetsHook.useFeatureDatasetsWithAddresses as any).mockReturnValue({ + (FeatureDatasetsHook.useLocationFeatureDatasetsWithAddresses as any).mockReturnValue({ featuresWithAddresses: [ { feature: { id: 1 }, address: 'A' }, { feature: { id: 2 }, address: 'B' }, @@ -80,7 +80,7 @@ describe('useEditPropertiesNotifier', () => { bcaLoading: false, }); - renderHook(() => useEditPropertiesNotifier(mockFormikRef, 'properties')); + renderHook(() => usePropertyFormSyncronizer(mockFormikRef, 'properties')); expect(mockFormikRef.current.setFieldValue).toHaveBeenCalledWith('properties', [ expect.objectContaining({ address: 'A' }), @@ -93,18 +93,18 @@ describe('useEditPropertiesNotifier', () => { }); it('shows warning toast when duplicates are skipped', () => { - const mockPropertyForms = [PropertyForm.fromFeatureDataset(getMockSelectedFeatureDataset())]; + const mockPropertyForms = [PropertyForm.fromLocationFeatureDataset(getMockSelectedFeatureDataset())]; mockFormikRef.current.values = { properties: mockPropertyForms }; - (FeatureDatasetsHook.useFeatureDatasetsWithAddresses as any).mockReturnValue({ + (FeatureDatasetsHook.useLocationFeatureDatasetsWithAddresses as any).mockReturnValue({ featuresWithAddresses: [ - { feature: getMockSelectedFeatureDataset() } as FeatureDatasetWithAddress, - { feature: { id: 999 }, address: 'Unique' } as unknown as FeatureDatasetWithAddress, + { feature: getMockSelectedFeatureDataset() } as LocationFeatureDatasetWithAddress, + { feature: { id: 999 }, address: 'Unique' } as unknown as LocationFeatureDatasetWithAddress, ], bcaLoading: false, }); - renderHook(() => useEditPropertiesNotifier(mockFormikRef, 'properties')); + renderHook(() => usePropertyFormSyncronizer(mockFormikRef, 'properties')); expect(mockFormikRef.current.setFieldValue).toHaveBeenCalled(); expect(mockFormikRef.current.setFieldTouched).toHaveBeenCalledWith('properties', true); @@ -115,16 +115,16 @@ describe('useEditPropertiesNotifier', () => { it('shows only warning toast if all properties are duplicates', () => { mockFormikRef.current.values = { - properties: [PropertyForm.fromFeatureDataset(getMockSelectedFeatureDataset())], + properties: [PropertyForm.fromLocationFeatureDataset(getMockSelectedFeatureDataset())], }; - (FeatureDatasetsHook.useFeatureDatasetsWithAddresses as any).mockReturnValue({ + (FeatureDatasetsHook.useLocationFeatureDatasetsWithAddresses as any).mockReturnValue({ featuresWithAddresses: [ - { feature: getMockSelectedFeatureDataset() } as FeatureDatasetWithAddress, + { feature: getMockSelectedFeatureDataset() } as LocationFeatureDatasetWithAddress, ], bcaLoading: false, }); - renderHook(() => useEditPropertiesNotifier(mockFormikRef, 'properties')); + renderHook(() => usePropertyFormSyncronizer(mockFormikRef, 'properties')); expect(mockFormikRef.current.setFieldValue).not.toHaveBeenCalled(); expect(mockFormikRef.current.setFieldTouched).not.toHaveBeenCalled(); @@ -136,12 +136,12 @@ describe('useEditPropertiesNotifier', () => { it('handles empty propertyForms gracefully', () => { mockFormikRef.current.values = { properties: [{ id: 1 }] }; - (FeatureDatasetsHook.useFeatureDatasetsWithAddresses as any).mockReturnValue({ + (FeatureDatasetsHook.useLocationFeatureDatasetsWithAddresses as any).mockReturnValue({ featuresWithAddresses: [], bcaLoading: false, }); - renderHook(() => useEditPropertiesNotifier(mockFormikRef, 'properties')); + renderHook(() => usePropertyFormSyncronizer(mockFormikRef, 'properties')); expect(mockFormikRef.current.setFieldValue).not.toHaveBeenCalled(); expect(mockFormikRef.current.setFieldTouched).not.toHaveBeenCalled(); @@ -152,7 +152,7 @@ describe('useEditPropertiesNotifier', () => { it('handles undefined formikRef.current gracefully', () => { const emptyRef = { current: undefined } as React.RefObject>; - renderHook(() => useEditPropertiesNotifier(emptyRef, 'properties')); + renderHook(() => usePropertyFormSyncronizer(emptyRef, 'properties')); expect(toastSuccessSpy).not.toHaveBeenCalled(); expect(toastWarnSpy).not.toHaveBeenCalled(); diff --git a/source/frontend/src/hooks/usePropertyFormSyncronizer.ts b/source/frontend/src/hooks/usePropertyFormSyncronizer.ts new file mode 100644 index 0000000000..b0fb9b4427 --- /dev/null +++ b/source/frontend/src/hooks/usePropertyFormSyncronizer.ts @@ -0,0 +1,85 @@ +import { FormikProps, getIn } from 'formik'; +import { useEffect } from 'react'; +import { toast } from 'react-toastify'; + +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { PropertyForm } from '@/features/mapSideBar/shared/models'; +import { exists } from '@/utils'; +import { arePropertyFormsEqual } from '@/utils/mapPropertyUtils'; + +import { useEditPropertiesMode } from './useEditPropertiesMode'; +import { useLocationFeatureDatasetsWithAddresses } from './useLocationFeatureDatasetsWithAddresses'; + +/** + * Notifies the map state machine to enter/exit edit properties mode. + * Use this hook in any component that needs to toggle edit properties mode on mount/unmount. + */ +export function usePropertyFormSyncronizer( + formikRef: React.RefObject>, + fieldName: keyof T, + overrideFeatures?: LocationFeatureDataset[], +) { + const { + locationFeaturesForAddition, + pendingLocationFeaturesAddition, + processLocationFeaturesAddition, + } = useMapStateMachine(); + + // Get PropertyForms with addresses for all selected features + const { locationFeaturesWithAddresses: featuresWithAddresses, bcaLoading } = + useLocationFeatureDatasetsWithAddresses(overrideFeatures ?? locationFeaturesForAddition); + + // if we are listening to property add notifications we must tell the state machine we are in edit mode. + useEditPropertiesMode(); + + // This effect is used to update the file properties when "add to open file" is clicked in the worklist. + useEffect(() => { + if ( + exists(formikRef?.current) && + pendingLocationFeaturesAddition && + featuresWithAddresses.length > 0 + ) { + // Convert LocationFeatureDataset to PropertyForm + const newPropertyForms = featuresWithAddresses.map(obj => { + const property = PropertyForm.fromLocationFeatureDataset(obj.feature); + if (exists(obj.address)) { + property.address = obj.address; + } + return property; + }); + + const existingProperties = getIn(formikRef?.current?.values, fieldName as string) ?? []; + const uniqueNewProperties = newPropertyForms.filter(newProperty => { + return !existingProperties.some((existingProperty: PropertyForm) => + arePropertyFormsEqual(existingProperty, newProperty), + ); + }); + + const duplicatesSkipped = newPropertyForms.length - uniqueNewProperties.length; + + // If there are unique properties, add them to the formik values + if (uniqueNewProperties.length > 0) { + formikRef.current?.setFieldValue(fieldName as string, [ + ...existingProperties, + ...uniqueNewProperties, + ]); + formikRef.current?.setFieldTouched(fieldName as string, true); + toast.success(`Added ${uniqueNewProperties.length} new property(s) to the file.`); + } + + if (duplicatesSkipped > 0) { + toast.warn(`Skipped ${duplicatesSkipped} duplicate property(s).`); + } + processLocationFeaturesAddition(); + } + }, [ + featuresWithAddresses, + fieldName, + formikRef, + pendingLocationFeaturesAddition, + processLocationFeaturesAddition, + ]); + + return { featuresWithAddresses, isLoading: bcaLoading }; +} diff --git a/source/frontend/src/mocks/featureset.mock.ts b/source/frontend/src/mocks/featureset.mock.ts index 952205784a..6b85bf1a3e 100644 --- a/source/frontend/src/mocks/featureset.mock.ts +++ b/source/frontend/src/mocks/featureset.mock.ts @@ -1,19 +1,16 @@ import { + emptyFeatureDataset, LocationFeatureDataset, - SelectedFeatureDataset, } from '@/components/common/mapFSM/useLocationFeatureLoader'; import getMockISSResult from './mockISSResult'; export const getMockLocationFeatureDataset = (): LocationFeatureDataset => ({ + ...emptyFeatureDataset(), location: { lat: 48.432802005, lng: -123.310041775, }, - fileLocation: { - lat: 48.432802005, - lng: -123.310041775, - }, pimsFeatures: [ { properties: null, @@ -140,25 +137,8 @@ export const getMockLocationFeatureDataset = (): LocationFeatureDataset => ({ }, ], highwayFeatures: getMockISSResult().features, - selectingComponentId: '', - crownLandLeasesFeatures: [], - crownLandLicensesFeatures: [], - crownLandTenuresFeatures: [], - crownLandInventoryFeatures: [], - crownLandInclusionsFeatures: [], }); -export const getMockSelectedFeatureDataset = (): SelectedFeatureDataset => { - const locationFeatureDataset = getMockLocationFeatureDataset(); - return { - selectingComponentId: locationFeatureDataset.selectingComponentId, - location: locationFeatureDataset.location, - fileLocation: locationFeatureDataset.fileLocation, - fileBoundary: null, - parcelFeature: locationFeatureDataset.parcelFeatures[0], - pimsFeature: locationFeatureDataset.pimsFeatures[0], - regionFeature: locationFeatureDataset.regionFeature, - districtFeature: locationFeatureDataset.districtFeature, - municipalityFeature: locationFeatureDataset.municipalityFeatures[0], - }; +export const getMockSelectedFeatureDataset = (): LocationFeatureDataset => { + return getMockLocationFeatureDataset(); }; diff --git a/source/frontend/src/mocks/mapFSM.mock.ts b/source/frontend/src/mocks/mapFSM.mock.ts index f26a549c3d..5041f088cf 100644 --- a/source/frontend/src/mocks/mapFSM.mock.ts +++ b/source/frontend/src/mocks/mapFSM.mock.ts @@ -43,10 +43,7 @@ export const mapMachineBaseMock: IMapStateMachineContext = { mapMarkerSelected: null, mapLocationSelected: null, mapLocationFeatureDataset: null, - repositioningFeatureDataset: null, - repositioningPropertyIndex: null, - selectingComponentId: null, - selectedFeatures: [], + locationFeaturesForAddition: [], showPopup: false, isLoading: false, mapSearchCriteria: null, @@ -67,6 +64,8 @@ export const mapMachineBaseMock: IMapStateMachineContext = { advancedSearchCriteria: new PropertyFilterFormModel(), isMapVisible: true, currentMapBounds: defaultBounds, + pendingLocationFeaturesAddition: false, + repositioningFeature: null, requestFlyToLocation: vi.fn(), requestCenterToLocation: vi.fn(), @@ -80,7 +79,7 @@ export const mapMachineBaseMock: IMapStateMachineContext = { mapMarkerClick: vi.fn(), setMapSearchCriteria: vi.fn(), refreshMapProperties: vi.fn(), - prepareForCreation: vi.fn(), + requestLocationFeatureAddition: vi.fn(), startSelection: vi.fn(), finishSelection: vi.fn(), startReposition: vi.fn(), @@ -114,7 +113,7 @@ export const mapMachineBaseMock: IMapStateMachineContext = { closeQuickInfo: vi.fn(), minimizeQuickInfo: vi.fn(), isEditPropertiesMode: false, - processCreation: vi.fn(), + processLocationFeaturesAddition: vi.fn(), setEditPropertiesMode: vi.fn(), worklistAdd: vi.fn(), setSelectedLocation: vi.fn(), diff --git a/source/frontend/src/mocks/worklistParcel.mock.ts b/source/frontend/src/mocks/worklistParcel.mock.ts index 4407c6c0b0..d0f5e2f7e8 100644 --- a/source/frontend/src/mocks/worklistParcel.mock.ts +++ b/source/frontend/src/mocks/worklistParcel.mock.ts @@ -1,27 +1,33 @@ import * as turf from '@turf/turf'; import { LatLngLiteral } from 'leaflet'; -import { ParcelDataset } from '@/features/properties/parcelList/models'; +import { + emptyFeatureDataset, + LocationFeatureDataset, +} from '@/components/common/mapFSM/useLocationFeatureLoader'; import { emptyPmbcParcel, PMBC_FullyAttributed_Feature_Properties, } from '@/models/layers/parcelMapBC'; import { exists } from '@/utils'; -// Factory for ParcelFeature using fromFullyAttributedFeature +// Factory for ParcelFeature using export const getMockWorklistParcel = ( id: string, props: Partial = {}, coords?: LatLngLiteral, -): ParcelDataset => { +): LocationFeatureDataset => { const geometry = exists(coords) ? [coords.lng, coords.lat] : [0, 0]; const geoFeature = turf.point(geometry, { ...emptyPmbcParcel, ...props, }); - const parcel = ParcelDataset.fromFullyAttributedFeature(geoFeature); - parcel.id = id; + const parcel: LocationFeatureDataset = { + ...emptyFeatureDataset(), + location: coords, + parcelFeatures: [geoFeature], + }; if (exists(coords)) { parcel.location = coords; diff --git a/source/frontend/src/models/generate/GenerateForm12.ts b/source/frontend/src/models/generate/GenerateForm12.ts index 9ef4523a4b..8407ed2e07 100644 --- a/source/frontend/src/models/generate/GenerateForm12.ts +++ b/source/frontend/src/models/generate/GenerateForm12.ts @@ -1,4 +1,4 @@ -import { FeatureCollection, Geometry } from 'geojson'; +import { Feature, Geometry } from 'geojson'; import moment from 'moment'; import { ComposedProperty } from '@/features/mapSideBar/property/ComposedProperty'; @@ -16,12 +16,10 @@ export class Api_GenerateForm12 { constructor(composedProperties: ComposedProperty[]) { this.properties = composedProperties.filter(exists).map(p => { const parcelMapFeatures = - ( - p.parcelMapFeatureCollection as FeatureCollection< - Geometry, - PMBC_FullyAttributed_Feature_Properties - > - )?.features ?? []; + (p.featureDataset.parcelFeatures as Feature< + Geometry, + PMBC_FullyAttributed_Feature_Properties + >[]) ?? []; return new Api_GenerateProperty(p.pimsProperty, firstOrNull(parcelMapFeatures)?.properties); }); diff --git a/source/frontend/src/models/generate/GenerateNotice.ts b/source/frontend/src/models/generate/GenerateNotice.ts index b901d70a7d..00e987a34d 100644 --- a/source/frontend/src/models/generate/GenerateNotice.ts +++ b/source/frontend/src/models/generate/GenerateNotice.ts @@ -1,4 +1,4 @@ -import { FeatureCollection, Geometry } from 'geojson'; +import { Feature, Geometry } from 'geojson'; import moment from 'moment'; import { ComposedProperty } from '@/features/mapSideBar/property/ComposedProperty'; @@ -61,12 +61,10 @@ export class Api_GenerateNotice { this.properties = properties.filter(exists).map(p => { const parcelMapFeatures = - ( - p.parcelMapFeatureCollection as FeatureCollection< - Geometry, - PMBC_FullyAttributed_Feature_Properties - > - )?.features ?? []; + (p.featureDataset.parcelFeatures as Feature< + Geometry, + PMBC_FullyAttributed_Feature_Properties + >[]) ?? []; return new Api_GenerateProperty(p.pimsProperty, firstOrNull(parcelMapFeatures)?.properties); }); diff --git a/source/frontend/src/utils/TestCommonWrapper.tsx b/source/frontend/src/utils/TestCommonWrapper.tsx index 3eb9541ae7..49d449aa84 100644 --- a/source/frontend/src/utils/TestCommonWrapper.tsx +++ b/source/frontend/src/utils/TestCommonWrapper.tsx @@ -8,9 +8,9 @@ import { ThemeProvider } from 'styled-components'; import { vi } from 'vitest'; import css from '@/assets/scss/_variables.module.scss'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import ModalContainer from '@/components/common/ModalContainer'; import { ModalContextProvider } from '@/contexts/modalContext'; -import { ParcelDataset } from '@/features/properties/parcelList/models'; import { WorklistContextProvider } from '@/features/properties/worklist/context/WorklistContext'; import { ApiGen_Concepts_Organization } from '@/models/api/generated/ApiGen_Concepts_Organization'; import { TenantConsumer, TenantProvider } from '@/tenants'; @@ -24,7 +24,7 @@ interface TestProviderWrapperParams { claims?: string[]; roles?: string[]; history?: MemoryHistory; - worklistParcels?: ParcelDataset[]; + worklistParcels?: LocationFeatureDataset[]; } /** diff --git a/source/frontend/src/utils/mapPropertyUtils.test.tsx b/source/frontend/src/utils/mapPropertyUtils.test.tsx index 978299e620..feea8556f4 100644 --- a/source/frontend/src/utils/mapPropertyUtils.test.tsx +++ b/source/frontend/src/utils/mapPropertyUtils.test.tsx @@ -2,11 +2,8 @@ import { polygon } from '@turf/turf'; import { Feature, Geometry } from 'geojson'; import { LatLngLiteral } from 'leaflet'; -import { LocationBoundaryDataset } from '@/components/common/mapFSM/models'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; import { getEmptyFileProperty } from '@/mocks/fileProperty.mock'; -import { getMockLatLng, getMockLocation, getMockPolygon } from '@/mocks/geometries.mock'; +import { getMockLocation } from '@/mocks/geometries.mock'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; import { ApiGen_Concepts_Geometry } from '@/models/api/generated/ApiGen_Concepts_Geometry'; import { getEmptyProperty } from '@/models/defaultInitializers'; @@ -14,11 +11,10 @@ import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelM import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; import { - filePropertyToLocationBoundaryDataset, getFilePropertyName, getLatLng, getPrettyLatLng, - getPropertyNameFromSelectedFeatureSet, + getPropertyNameFromLocationFeatureSet, isLatLngInFeatureSetBoundary, latLngToApiLocation, locationFromFileProperty, @@ -27,13 +23,15 @@ import { pinFromFeatureSet, PropertyName, } from './mapPropertyUtils'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { getMockLocationFeatureDataset } from '@/mocks/featureset.mock'; describe('mapPropertyUtils', () => { it.each([ [{}, { label: NameSourceType.NONE, value: '' }], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: {} as any, parcelFeature: { properties: { PID: undefined } } as any, @@ -42,7 +40,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: {} as any, parcelFeature: { properties: { PID: '' } } as any, @@ -51,7 +49,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { PID: '000-000-001' } } as any, }, @@ -59,7 +57,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { @@ -73,7 +71,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: {} as any, parcelFeature: { properties: { PIN: undefined } } as any, @@ -82,7 +80,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: {} as any, parcelFeature: { properties: { PIN: '' } } as any, @@ -91,7 +89,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { PIN: 111112 } } as any, }, @@ -99,7 +97,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { @@ -112,7 +110,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: {} as any, parcelFeature: { properties: { PLAN_NUMBER: undefined } } as any, @@ -121,7 +119,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: {} as any, parcelFeature: { properties: { PLAN_NUMBER: '' } } as any, @@ -130,7 +128,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { PLAN_NUMBER: '1' } } as any, }, @@ -138,7 +136,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { PLAN_NUMBER: 'PB1000' } } as any, }, @@ -146,7 +144,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: { lat: 1, lng: 2 }, fileLocation: null, pimsFeature: {} as any, @@ -156,7 +154,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: { properties: { STREET_ADDRESS_1: undefined } } as any, parcelFeature: {} as any, @@ -165,7 +163,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: { properties: { STREET_ADDRESS_1: '' } } as any, parcelFeature: {} as any, @@ -174,7 +172,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, pimsFeature: { properties: { STREET_ADDRESS_1: '1234 fake st' } } as any, parcelFeature: {} as any, @@ -183,8 +181,8 @@ describe('mapPropertyUtils', () => { ], ])( 'getPropertyNameFromSelectedFeatureSet test with source %o expecting %o', - (featureSet: SelectedFeatureDataset, expectedName: PropertyName) => { - const actualName = getPropertyNameFromSelectedFeatureSet(featureSet); + (featureSet: LocationFeatureDataset, expectedName: PropertyName) => { + const actualName = getPropertyNameFromLocationFeatureSet(featureSet); expect(actualName.label).toEqual(expectedName.label); expect(actualName.value).toEqual(expectedName.value); }, @@ -251,14 +249,14 @@ describe('mapPropertyUtils', () => { it.each([ [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: { properties: { PID_PADDED: '123-456-789' } } as any, }, '123-456-789', ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { PID: '9999' } } as any, }, @@ -266,7 +264,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: {} as any, }, @@ -274,7 +272,7 @@ describe('mapPropertyUtils', () => { ], ])( 'pidFromFeatureSet test with feature set %o - expected %s', - (featureSet: SelectedFeatureDataset, expectedValue: string | null) => { + (featureSet: LocationFeatureDataset, expectedValue: string | null) => { const pid = pidFromFeatureSet(featureSet); expect(pid).toEqual(expectedValue); }, @@ -282,12 +280,12 @@ describe('mapPropertyUtils', () => { it.each([ [ - { ...getMockSelectedFeatureDataset(), pimsFeature: { properties: { PIN: 1234 } } as any }, + { ...getMockLocationFeatureDataset(), pimsFeature: { properties: { PIN: 1234 } } as any }, '1234', ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: { properties: { PIN: 9999 } } as any, }, @@ -295,7 +293,7 @@ describe('mapPropertyUtils', () => { ], [ { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: {} as any, parcelFeature: {} as any, }, @@ -303,6 +301,7 @@ describe('mapPropertyUtils', () => { ], [ { + ...getMockLocationFeatureDataset(), pimsFeature: { properties: { pin: '4321' } } as any, parcelFeature: { properties: { pid: 1234 } } as any, }, @@ -310,7 +309,7 @@ describe('mapPropertyUtils', () => { ], ])( 'pinFromFeatureSet test with feature set %o - expected %s', - (featureSet: SelectedFeatureDataset, expectedValue: string | null) => { + (featureSet: LocationFeatureDataset, expectedValue: string | null) => { const pid = pinFromFeatureSet(featureSet); expect(pid).toEqual(expectedValue); }, @@ -338,35 +337,6 @@ describe('mapPropertyUtils', () => { }, ); - it.each([ - [ - { ...getEmptyFileProperty(), location: getMockLocation() }, - { location: getMockLatLng(), boundary: null, isActive: null, fileBoundary: null }, - ], - [ - { - ...getEmptyFileProperty(), - location: null, - property: { - ...getEmptyProperty(), - location: getMockLocation(), - boundary: getMockPolygon(), - }, - }, - { location: getMockLatLng(), boundary: getMockPolygon(), isActive: null, fileBoundary: null }, - ], - [{ ...getEmptyFileProperty(), location: null }, null], - ])( - 'filePropertyToLocationBoundaryDataset test with file property %o - expected %o', - ( - fileProperty: ApiGen_Concepts_FileProperty | undefined | null, - expectedValue: LocationBoundaryDataset | null, - ) => { - const dataset = filePropertyToLocationBoundaryDataset(fileProperty); - expect(dataset).toEqual(expectedValue); - }, - ); - it.each([ [4, 5, { ...getMockLocation(4, 5) }], [null, null, null], @@ -386,7 +356,7 @@ describe('mapPropertyUtils', () => { [ { lat: 44, lng: -77 }, { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: polygon([ [ [-81, 41], @@ -402,7 +372,7 @@ describe('mapPropertyUtils', () => { [ { lat: 44, lng: 80 }, { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: polygon([ [ [-81, 41], @@ -418,7 +388,7 @@ describe('mapPropertyUtils', () => { [ { lat: 44, lng: -77 }, { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: null, parcelFeature: polygon([ [ @@ -435,7 +405,7 @@ describe('mapPropertyUtils', () => { [ { lat: 44, lng: 80 }, { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), pimsFeature: null, parcelFeature: polygon([ [ @@ -452,7 +422,7 @@ describe('mapPropertyUtils', () => { [ { lat: 44, lng: -77 }, { - ...getMockSelectedFeatureDataset(), + ...getMockLocationFeatureDataset(), location: null, fileLocation: null, pimsFeature: null, @@ -462,7 +432,7 @@ describe('mapPropertyUtils', () => { ], ])( 'isLatLngInFeatureSetBoundary test with lat/long %o, feature set %o - expected %o', - (latLng: LatLngLiteral, featureset: SelectedFeatureDataset, expectedValue: boolean) => { + (latLng: LatLngLiteral, featureset: LocationFeatureDataset, expectedValue: boolean) => { const result = isLatLngInFeatureSetBoundary(latLng, featureset); expect(result).toEqual(expectedValue); }, diff --git a/source/frontend/src/utils/mapPropertyUtils.ts b/source/frontend/src/utils/mapPropertyUtils.ts index a9108c0db5..389d3ff751 100644 --- a/source/frontend/src/utils/mapPropertyUtils.ts +++ b/source/frontend/src/utils/mapPropertyUtils.ts @@ -13,8 +13,11 @@ import { chain, compact, isNumber } from 'lodash'; import polylabel from 'polylabel'; import { toast } from 'react-toastify'; -import { LocationBoundaryDataset, MapFeatureData } from '@/components/common/mapFSM/models'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { MapFeatureData } from '@/components/common/mapFSM/models'; +import { + FeatureDataset, + LocationFeatureDataset, +} from '@/components/common/mapFSM/useLocationFeatureLoader'; import { ONE_HUNDRED_METER_PRECISION } from '@/components/maps/constants'; import { AddressForm, PropertyForm } from '@/features/mapSideBar/shared/models'; import { ApiGen_CodeTypes_GeoJsonTypes } from '@/models/api/generated/ApiGen_CodeTypes_GeoJsonTypes'; @@ -24,7 +27,7 @@ import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts import { MOT_DistrictBoundary_Feature_Properties } from '@/models/layers/motDistrictBoundary'; import { MOT_RegionalBoundary_Feature_Properties } from '@/models/layers/motRegionalBoundary'; import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; -import { exists, formatApiAddress, pidFormatter } from '@/utils'; +import { exists, firstOrNull, formatApiAddress, formatSplitAddress, pidFormatter } from '@/utils'; export enum NameSourceType { PID = 'PID', @@ -41,8 +44,8 @@ export interface PropertyName { value: string; } -export const getPropertyNameFromSelectedFeatureSet = ( - selectedFeature: SelectedFeatureDataset | null, +export const getPropertyNameFromLocationFeatureSet = ( + selectedFeature: LocationFeatureDataset | null, ): PropertyName => { if (!exists(selectedFeature)) { return { label: NameSourceType.NONE, value: '' }; @@ -72,6 +75,46 @@ export const getPropertyNameFromSelectedFeatureSet = ( return { label: NameSourceType.NONE, value: '' }; }; +export const getPropertyNameFromForm = (propertyForm: PropertyForm | null): PropertyName => { + if (!exists(propertyForm)) { + return { label: NameSourceType.NONE, value: '' }; + } + + const pid = propertyForm.pid; + const pin = propertyForm.pin; + const planNumber = propertyForm.planNumber; + const address = propertyForm.address; + const location = propertyForm.fileLocation; + + if (exists(pid) && pid?.toString()?.length > 0 && pid !== '0') { + return { label: NameSourceType.PID, value: pidFormatter(pid.toString()) }; + } else if (exists(pin) && pin?.toString()?.length > 0 && pin !== '0') { + return { label: NameSourceType.PIN, value: pin.toString() }; + } else if (exists(planNumber) && planNumber?.toString()?.length > 0) { + return { label: NameSourceType.PLAN, value: planNumber.toString() }; + } else if (exists(location?.lat) && exists(location?.lng)) { + return { + label: NameSourceType.LOCATION, + value: compact([location.lng?.toFixed(6), location.lat?.toFixed(6)]).join(', '), + }; + } else if (exists(address)) { + return { + label: NameSourceType.ADDRESS, + value: + formatSplitAddress( + address.streetAddress1, + address.streetAddress1, + address.streetAddress3, + address.municipality, + address.province, + address.postalCode, + ) ?? '', + }; + } + + return { label: NameSourceType.NONE, value: '' }; +}; + export const getPrettyLatLng = (location: ApiGen_Concepts_Geometry | undefined | null) => compact([ location?.coordinate?.x?.toFixed(6) ?? 0, @@ -170,17 +213,6 @@ export const getFeatureBoundedCenter = (feature: Feature { - return { - location: featureSet?.fileLocation ?? featureSet?.location, - boundary: featureSet?.pimsFeature?.geometry ?? featureSet?.parcelFeature?.geometry ?? null, - fileBoundary: featureSet?.fileBoundary ?? null, - isActive: featureSet.isActive, - }; -}; - export function pidFromFullyAttributedFeature( parcelFeature: Feature | null, ): string | null { @@ -201,40 +233,45 @@ export function planFromFullyAttributedFeature( : null; } -export function pidFromFeatureSet(featureset: SelectedFeatureDataset): string | null { - if (exists(featureset?.pimsFeature?.properties)) { - return exists(featureset?.pimsFeature?.properties?.PID_PADDED) - ? featureset?.pimsFeature?.properties?.PID_PADDED?.toString() +export function pidFromFeatureSet(featureset: FeatureDataset): string | null { + const pimsFeature = firstOrNull(featureset?.pimsFeatures); + if (exists(pimsFeature?.properties)) { + return exists(pimsFeature?.properties?.PID_PADDED) + ? pimsFeature?.properties?.PID_PADDED?.toString() : null; } - return pidFromFullyAttributedFeature(featureset?.parcelFeature); + const parcelFeature = firstOrNull(featureset?.parcelFeatures); + return pidFromFullyAttributedFeature(parcelFeature); } -export function pinFromFeatureSet(featureset: SelectedFeatureDataset): string | null { - if (exists(featureset?.pimsFeature?.properties)) { - return exists(featureset?.pimsFeature?.properties?.PIN) - ? featureset?.pimsFeature?.properties?.PIN?.toString() - : null; +export function pinFromFeatureSet(featureset: FeatureDataset): string | null { + const pimsFeature = firstOrNull(featureset?.pimsFeatures); + if (exists(pimsFeature?.properties)) { + return exists(pimsFeature?.properties?.PIN) ? pimsFeature?.properties?.PIN?.toString() : null; } - return pinFromFullyAttributedFeature(featureset?.parcelFeature); + const parcelFeature = firstOrNull(featureset?.parcelFeatures); + return pinFromFullyAttributedFeature(parcelFeature); } -export function planFromFeatureSet(featureset: SelectedFeatureDataset): string | null { - if (exists(featureset?.pimsFeature?.properties)) { - return exists(featureset?.pimsFeature?.properties?.SURVEY_PLAN_NUMBER) - ? featureset?.pimsFeature?.properties?.SURVEY_PLAN_NUMBER?.toString() +export function planFromFeatureSet(featureset: FeatureDataset): string | null { + const pimsFeature = firstOrNull(featureset?.pimsFeatures); + if (exists(pimsFeature?.properties)) { + return exists(pimsFeature?.properties?.SURVEY_PLAN_NUMBER) + ? pimsFeature?.properties?.SURVEY_PLAN_NUMBER?.toString() : null; } - return planFromFullyAttributedFeature(featureset?.parcelFeature); + const parcelFeature = firstOrNull(featureset?.parcelFeatures); + return planFromFullyAttributedFeature(parcelFeature); } -export function addressFromFeatureSet(featureset: SelectedFeatureDataset): string | null { - if (exists(featureset?.pimsFeature?.properties)) { - return exists(featureset?.pimsFeature?.properties?.STREET_ADDRESS_1) - ? formatApiAddress(AddressForm.fromPimsView(featureset?.pimsFeature?.properties).toApi()) +export function addressFromFeatureSet(featureset: FeatureDataset): string | null { + const pimsFeature = firstOrNull(featureset?.pimsFeatures); + if (exists(pimsFeature?.properties)) { + return exists(pimsFeature?.properties?.STREET_ADDRESS_1) + ? formatApiAddress(AddressForm.fromPimsView(pimsFeature?.properties).toApi()) : null; } @@ -273,34 +310,6 @@ export function pimsGeomeryToGeometry( return null; } -export function filePropertyToLocationBoundaryDataset( - fileProperty: ApiGen_Concepts_FileProperty | undefined | null, -): LocationBoundaryDataset | null { - const geom = locationFromFileProperty(fileProperty); - const location = getLatLng(geom); - return exists(location) - ? { - location, - boundary: fileProperty?.property?.boundary ?? null, - fileBoundary: fileProperty?.boundary ?? null, - isActive: fileProperty.isActive, - } - : null; -} - -export function propertyToLocationBoundaryDataset( - property: ApiGen_Concepts_Property | undefined | null, -): LocationBoundaryDataset | null { - const location = getLatLng(property.location); - return exists(location) - ? { - location, - boundary: property?.boundary ?? null, - fileBoundary: null, - } - : null; -} - /** * Takes a (Lat, Long) value and a FeatureSet and determines if the point resides inside the polygon. * The polygon can be convex or concave. The function accounts for holes. @@ -311,12 +320,14 @@ export function propertyToLocationBoundaryDataset( */ export function isLatLngInFeatureSetBoundary( latLng: LatLngLiteral, - featureset: SelectedFeatureDataset, + featureset: FeatureDataset, ): boolean { + // TODO: Make this work with the all of the features instead of just the first one + const pimsFeature = firstOrNull(featureset?.pimsFeatures); + const parcelFeature = firstOrNull(featureset?.parcelFeatures); + const location = point([latLng.lng, latLng.lat]); - const boundary = (featureset?.pimsFeature?.geometry ?? featureset?.parcelFeature?.geometry) as - | Polygon - | MultiPolygon; + const boundary = (pimsFeature?.geometry ?? parcelFeature?.geometry) as Polygon | MultiPolygon; return exists(boundary) && booleanPointInPolygon(location, boundary); } @@ -348,24 +359,76 @@ export function sortFileProperties( return null; } -export const areSelectedFeaturesEqual = ( - lhs: SelectedFeatureDataset, - rhs: SelectedFeatureDataset, -) => { - const lhsName = getPropertyNameFromSelectedFeatureSet(lhs); - const rhsName = getPropertyNameFromSelectedFeatureSet(rhs); +export function areFeatureDatasetsEqual(p1: FeatureDataset, p2: FeatureDataset): boolean { + if (!exists(p1) || !exists(p2)) { + return false; + } + + const pid1 = pidFromFeatureSet(p1) ?? null; + const pid2 = pidFromFeatureSet(p2) ?? null; + if (exists(pid1) && pid1 === pid2) { + return true; + } + + const pin1 = pinFromFeatureSet(p1) ?? null; + const pin2 = pinFromFeatureSet(p2) ?? null; + + if (exists(pin1) && pin1 === pin2) { + return true; + } + + // Some parcels are only identified by their plan-number (e.g. common strata, parks) + // Only consider plan-number as an identifier when there are no PID/PIN + const planNumber1 = planFromFeatureSet(p1) ?? null; + const planNumber2 = planFromFeatureSet(p2) ?? null; if ( - (lhsName.label === rhsName.label && - lhsName.label !== NameSourceType.NONE && - lhsName.label !== NameSourceType.PLAN) || - (lhsName.label === NameSourceType.PLAN && - lhs.location.lat === rhs.location.lat && - lhs.location.lng === rhs.location.lng) + exists(planNumber1) && + !exists(pid1) && + !exists(pin1) && + !exists(pid2) && + !exists(pin2) && + planNumber1 === planNumber2 ) { - return lhsName.value === rhsName.value; + return true; } + return false; -}; +} + +export function areLocationFeatureDatasetsEqual( + p1: LocationFeatureDataset, + p2: LocationFeatureDataset, +): boolean { + if (!exists(p1) || !exists(p2)) { + return false; + } + + if (areFeatureDatasetsEqual(p1, p2)) { + return true; + } + + // Only consider lat/long when there are no PID/PIN + const location1 = p1.location; + const location2 = p2.location; + const pid1 = pidFromFeatureSet(p1) ?? null; + const pid2 = pidFromFeatureSet(p2) ?? null; + const pin1 = pinFromFeatureSet(p1) ?? null; + const pin2 = pinFromFeatureSet(p2) ?? null; + if ( + exists(location1) && + exists(location2) && + !exists(pid1) && + !exists(pin1) && + !exists(pid2) && + !exists(pin2) && + location1.lat === location2.lat && + location1.lng === location2.lng + ) { + return true; + } + + return false; +} export const isEmptyFeatureCollection = (collection: FeatureCollection) => { return !(exists(collection?.features) && collection.features.length > 0); @@ -386,7 +449,10 @@ export const arePropertyFormsEqual = (lhs: PropertyForm, rhs: PropertyForm): boo if (!exists(lhs) || !exists(rhs)) { return exists(lhs) === exists(rhs); } - return areSelectedFeaturesEqual(lhs.toFeatureDataset(), rhs.toFeatureDataset()); + return areLocationFeatureDatasetsEqual( + lhs.toLocationFeatureDataset(), + rhs.toLocationFeatureDataset(), + ); }; export interface RegionDistrictResult { @@ -394,14 +460,9 @@ export interface RegionDistrictResult { districtResult: Feature; } -export function featureSetToLatLngKey(featureSet: SelectedFeatureDataset | null | undefined) { - if (exists(featureSet.location)) { - const latLng: LatLngLiteral = { - lat: featureSet.location.lat, - lng: featureSet.location.lng, - }; - - const key = `${latLng.lat}-${latLng.lng}`; +export function latLngToKey(location: LatLngLiteral | null | undefined) { + if (exists(location)) { + const key = `${location.lat}-${location.lng}`; return key; } @@ -416,7 +477,7 @@ export function featureSetToLatLngKey(featureSet: SelectedFeatureDataset | null * @returns A Map where keys are lat-lng strings and values are RegionDistrictResult objects. */ export async function getRegionAndDistrictsResults( - properties: SelectedFeatureDataset[], + properties: LocationFeatureDataset[], regionSearch: ( latlng: LatLngLiteral, geometryName?: string | undefined, @@ -435,15 +496,10 @@ export async function getRegionAndDistrictsResults( continue; } - const latLng: LatLngLiteral = { - lat: property.location.lat, - lng: property.location.lng, - }; - - const key = featureSetToLatLngKey(property); + const key = latLngToKey(property.location); if (!latLngMap.has(key)) { - latLngMap.set(key, latLng); + latLngMap.set(key, property.location); } } diff --git a/source/frontend/src/utils/propertyUtils.ts b/source/frontend/src/utils/propertyUtils.ts index 7b0b1cdb1a..8b0a976fe7 100644 --- a/source/frontend/src/utils/propertyUtils.ts +++ b/source/frontend/src/utils/propertyUtils.ts @@ -1,13 +1,11 @@ -import { FormikProps, getIn } from 'formik'; import { Feature, Geometry } from 'geojson'; -import { toast } from 'react-toastify'; -import { PropertyForm } from '@/features/mapSideBar/shared/models'; +import { AddressForm } from '@/features/mapSideBar/shared/models'; import { ApiGen_Concepts_Address } from '@/models/api/generated/ApiGen_Concepts_Address'; import { ApiGen_Concepts_PropertyManagement } from '@/models/api/generated/ApiGen_Concepts_PropertyManagement'; import { IBcAssessmentSummary } from '@/models/layers/bcAssesment'; import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; -import { arePropertyFormsEqual, firstOrNull } from '@/utils'; +import { firstOrNull } from '@/utils'; import { exists, isNumber, isValidString } from './utils'; @@ -78,6 +76,17 @@ export const formatApiAddress = (address: ApiGen_Concepts_Address | null | undef ); }; +export const formatFormAddress = (address: AddressForm | null | undefined) => { + return formatSplitAddress( + address?.streetAddress1 ?? '', + address?.streetAddress2 ?? '', + address?.streetAddress3 ?? '', + address?.municipality ?? '', + address?.province ?? '', + address?.postalCode ?? '', + ); +}; + /** * Provides a formatted address as a string. * @returns Civic address string value. @@ -152,34 +161,3 @@ export function isStrataCommonProperty( feature.properties.OWNER_TYPE === 'Unclassified' ); } - -export const addPropertiesToCurrentFile = ( - formikRef: React.RefObject>, - fieldName: keyof T, - propertyForms: PropertyForm[], - notifyAddComplete: () => void, -) => { - const existingProperties = getIn(formikRef?.current?.values, fieldName as string) ?? []; - const uniqueProperties = propertyForms.filter(newProperty => { - return !existingProperties.some((existingProperty: PropertyForm) => - arePropertyFormsEqual(existingProperty, newProperty), - ); - }); - - const duplicatesSkipped = propertyForms.length - uniqueProperties.length; - - // If there are unique properties, add them to the formik values - if (uniqueProperties.length > 0) { - formikRef.current?.setFieldValue(fieldName as string, [ - ...existingProperties, - ...uniqueProperties, - ]); - formikRef.current?.setFieldTouched(fieldName as string, true); - toast.success(`Added ${uniqueProperties.length} new property(s) to the file.`); - } - - if (duplicatesSkipped > 0) { - toast.warn(`Skipped ${duplicatesSkipped} duplicate property(s).`); - } - notifyAddComplete(); -}; diff --git a/source/frontend/src/utils/test-utils.tsx b/source/frontend/src/utils/test-utils.tsx index c5b855f66e..c36c8f52ec 100644 --- a/source/frontend/src/utils/test-utils.tsx +++ b/source/frontend/src/utils/test-utils.tsx @@ -17,8 +17,8 @@ import { Router } from 'react-router-dom/cjs/react-router-dom'; import { vi } from 'vitest'; import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext'; +import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { FilterProvider } from '@/components/maps/providers/FilterProvider'; -import { ParcelDataset } from '@/features/properties/parcelList/models'; import { IApiError } from '@/interfaces/IApiError'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; @@ -282,7 +282,7 @@ export interface RenderOptions extends RtlRenderOptions { roles?: string[]; mockMapMachine?: IMapStateMachineContext; keycloakMock?: any; - worklistParcels?: ParcelDataset[]; + worklistParcels?: LocationFeatureDataset[]; } function render( diff --git a/source/frontend/src/utils/utils.ts b/source/frontend/src/utils/utils.ts index 485e8eeddb..1a1b7435c1 100644 --- a/source/frontend/src/utils/utils.ts +++ b/source/frontend/src/utils/utils.ts @@ -170,6 +170,10 @@ export function exists(value: T | null | undefined): value is T { return value === (value ?? !value); } +export function isEmptyOrNull(array: Array | null | undefined): array is null { + return !(exists(array) && !isEmpty(array)); +} + /** * Returns true id an identifier belongs to an existing entry on the backend * @param value the paraneter to be assessed From c1c7e3b4f18e192e5b069c474d06bfe0ff4d88e1 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Sat, 6 Dec 2025 08:15:44 -0800 Subject: [PATCH 2/5] Updated Property selector --- .../SelectedPropertyRow.tsx | 2 +- .../features/leases/add/AddLeaseContainer.tsx | 68 +-- .../src/features/leases/add/AddLeaseForm.tsx | 15 +- .../features/leases/add/AddLeaseYupSchema.ts | 20 +- source/frontend/src/features/leases/models.ts | 106 +--- .../LeasePropertySelector.test.tsx | 335 ------------ .../propertyPicker/LeasePropertySelector.tsx | 75 --- .../LeaseUpdatePropertySelector.tsx | 483 ------------------ .../acquisition/AcquisitionContainer.tsx | 7 +- .../acquisition/AcquisitionView.tsx | 13 +- .../add/AddAcquisitionContainer.tsx | 137 +++-- .../mapSideBar/lease/LeaseContainer.tsx | 86 +++- .../features/mapSideBar/lease/LeaseView.tsx | 37 +- .../add/AddManagementContainer.test.tsx | 2 +- .../management/add/AddManagementContainer.tsx | 5 +- .../management/form/ManagementForm.tsx | 2 +- .../management/models/ManagementFormModel.ts | 6 +- .../properties/PropertiesListContainer.tsx | 88 ++-- .../properties/UpdatePropertiesContainer.tsx | 83 ++- .../src/hooks/usePropertyFormSyncronizer.ts | 42 +- 20 files changed, 409 insertions(+), 1203 deletions(-) delete mode 100644 source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.test.tsx delete mode 100644 source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx delete mode 100644 source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx index be6b865877..76a6151dc3 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx @@ -78,7 +78,7 @@ export const SelectedPropertyRow: React.FunctionComponent
- {property.isActive === 'false' ? ( + {property?.isActive === 'false' ? ( ) : ( diff --git a/source/frontend/src/features/leases/add/AddLeaseContainer.tsx b/source/frontend/src/features/leases/add/AddLeaseContainer.tsx index 4b744c6d3e..c7fb88facf 100644 --- a/source/frontend/src/features/leases/add/AddLeaseContainer.tsx +++ b/source/frontend/src/features/leases/add/AddLeaseContainer.tsx @@ -1,7 +1,6 @@ import { AxiosError } from 'axios'; -import { dequal } from 'dequal'; import { FormikHelpers, FormikProps } from 'formik'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; @@ -13,14 +12,12 @@ import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; import { PropertyForm } from '@/features/mapSideBar/shared/models'; import SidebarFooter from '@/features/mapSideBar/shared/SidebarFooter'; import useApiUserOverride from '@/hooks/useApiUserOverride'; -import { useEditPropertiesMode } from '@/hooks/useEditPropertiesMode'; -import { useEnrichWithPimsFeatures } from '@/hooks/useEnrichWithPimsFeatures'; import { useModalContext } from '@/hooks/useModalContext'; import { usePropertyFormSyncronizer } from '@/hooks/usePropertyFormSyncronizer'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { exists, firstOrNull, isValidId } from '@/utils'; +import { exists, isValidId } from '@/utils'; import { useAddLease } from '../hooks/useAddLease'; import { getDefaultFormLease, LeaseFormModel } from '../models'; @@ -44,59 +41,20 @@ export const AddLeaseContainer: React.FunctionComponent< const { addLease: { execute: addLease, loading: addLeaseLoading }, } = useAddLease(); - const { - datasets, - loading: pimsFeatureLoading, - enrichWithPimsFeatures, - } = useEnrichWithPimsFeatures(); const [isValid, setIsValid] = useState(true); // Support creating a new lease file from the worklist/quick-info const mapMachine = useMapStateMachine(); - const selectedFeatureDatasets = mapMachine.locationFeaturesForAddition; - const prevSelectedRef = useRef(); - const processCreation = mapMachine.processLocationFeaturesAddition; - - useEditPropertiesMode(); // track whether we've already shown the confirmation modal for this session const hasWarnedRef = useRef(false); - // Enrich selected features with PIMS features - // This will add pimsFeature to each SelectedFeatureDataset if it exists - useEffect(() => { - if ( - selectedFeatureDatasets?.length > 0 && - !dequal(prevSelectedRef.current, selectedFeatureDatasets) - ) { - hasWarnedRef.current = false; // reset the warning for new selection - prevSelectedRef.current = selectedFeatureDatasets; - enrichWithPimsFeatures(selectedFeatureDatasets); - } - }, [selectedFeatureDatasets, enrichWithPimsFeatures]); - // Get PropertyForms with addresses for all selected features const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); - const initialForm = useMemo(() => { - const leaseForm = getDefaultFormLease(); + const initialForm = getDefaultFormLease(); - if (featuresWithAddresses?.length > 0) { - // auto-select file region based upon the location of the property - const firstProperty = firstOrNull( - featuresWithAddresses?.map(f => PropertyForm.fromLocationFeatureDataset(f.feature)), - ); - if (exists(firstProperty)) { - leaseForm.regionId = - firstProperty?.regionName !== 'Cannot determine' - ? firstProperty?.region?.toString() - : undefined; - } - } - - return leaseForm; - }, [featuresWithAddresses]); const confirmBeforeAdd = useCallback( async (propertyForm: PropertyForm) => !isValidId(propertyForm?.apiId), [], @@ -105,10 +63,13 @@ export const AddLeaseContainer: React.FunctionComponent< // Require user confirmation before adding non-inventory properties to a lease. useEffect(() => { const runAsync = async () => { - if (exists(initialForm.properties) && exists(formikRef.current) && !hasWarnedRef.current) { + const incomingProperties = + featuresWithAddresses?.map(f => PropertyForm.fromLocationFeatureDataset(f.feature)) ?? []; + + if (exists(incomingProperties) && exists(formikRef.current) && !hasWarnedRef.current) { // Check all properties for confirmation - const needsConfirmation = await Promise.all( - initialForm.properties.map(formProperty => confirmBeforeAdd(formProperty?.property)), + const needsConfirmation = incomingProperties.some( + async feature => await confirmBeforeAdd(feature), ); if (needsConfirmation) { hasWarnedRef.current = true; // mark as shown @@ -138,7 +99,13 @@ export const AddLeaseContainer: React.FunctionComponent< }; runAsync(); - }, [confirmBeforeAdd, initialForm.properties, setDisplayModal, setModalContent]); + }, [ + confirmBeforeAdd, + featuresWithAddresses, + initialForm.properties, + setDisplayModal, + setModalContent, + ]); const saveLeaseFile = async ( leaseFormModel: LeaseFormModel, @@ -154,7 +121,6 @@ export const AddLeaseContainer: React.FunctionComponent< handleSuccess(response); } } finally { - mapMachine.processLocationFeaturesAddition(); formikHelpers.setSubmitting(false); } }; @@ -190,7 +156,7 @@ export const AddLeaseContainer: React.FunctionComponent< return formikRef?.current?.dirty && !formikRef?.current?.isSubmitting; }, [formikRef]); - const loading = addLeaseLoading || isLoading || pimsFeatureLoading; + const loading = addLeaseLoading || isLoading; return ( ( <> - +
+
+ Select one or more properties that you want to include in this lease/licence file. + You can choose a location from the map, or search by other criteria. +
+ callback()} + needsConfirmationBeforeAdd={confirmBeforeAdd} + showArea + /> +
diff --git a/source/frontend/src/features/leases/add/AddLeaseYupSchema.ts b/source/frontend/src/features/leases/add/AddLeaseYupSchema.ts index 021d1f86aa..f58fc48ebb 100644 --- a/source/frontend/src/features/leases/add/AddLeaseYupSchema.ts +++ b/source/frontend/src/features/leases/add/AddLeaseYupSchema.ts @@ -66,20 +66,18 @@ export const AddLeaseYupSchema = Yup.object().shape({ properties: Yup.array().of( Yup.object().shape({ name: Yup.string().max(250, 'Property name must be at most ${max} characters'), - property: Yup.object().shape({ - isRetired: Yup.boolean().when('id', { - is: (id: number) => !isValidId(id), - then: Yup.boolean().notOneOf( - [true], - 'Selected property is retired and can not be added to the file', - ), - otherwise: Yup.boolean().nullable(), - }), - isDisposed: Yup.boolean().notOneOf( + isRetired: Yup.boolean().when('id', { + is: (id: number) => !isValidId(id), + then: Yup.boolean().notOneOf( [true], - 'Selected property is disposed and can not be added to the file', + 'Selected property is retired and can not be added to the file', ), + otherwise: Yup.boolean().nullable(), }), + isDisposed: Yup.boolean().notOneOf( + [true], + 'Selected property is disposed and can not be added to the file', + ), }), ), consultations: Yup.array().of( diff --git a/source/frontend/src/features/leases/models.ts b/source/frontend/src/features/leases/models.ts index b81fe2432b..73320da6c0 100644 --- a/source/frontend/src/features/leases/models.ts +++ b/source/frontend/src/features/leases/models.ts @@ -1,6 +1,5 @@ import isNumber from 'lodash/isNumber'; -import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { fromApiOrganization, fromApiPerson, @@ -18,7 +17,7 @@ import { ApiGen_Concepts_PropertyLease } from '@/models/api/generated/ApiGen_Con import { EpochIsoDateTime, UtcIsoDateTime } from '@/models/api/UtcIsoDateTime'; import { getEmptyBaseAudit } from '@/models/defaultInitializers'; import { NumberFieldValue } from '@/typings/NumberFieldValue'; -import { applyDisplayOrder, exists, isValidId, isValidIsoDateTime } from '@/utils'; +import { applyDisplayOrder, enumFromValue, exists, isValidId, isValidIsoDateTime } from '@/utils'; import { emptyStringToNull, fromTypeCode, @@ -27,7 +26,7 @@ import { toTypeCodeNullable, } from '@/utils/formUtils'; -import { PropertyForm } from '../mapSideBar/shared/models'; +import { FileForm, PropertyForm } from '../mapSideBar/shared/models'; import { ChecklistItemFormModel } from '../mapSideBar/shared/tabs/checklist/update/models'; import { FormLeaseDeposit } from './detail/LeasePages/deposits/models/FormLeaseDeposit'; import { FormLeaseDepositReturn } from './detail/LeasePages/deposits/models/FormLeaseDepositReturn'; @@ -72,8 +71,7 @@ export class FormLeaseRenewal { } } -export class LeaseFormModel implements WithLeaseTeam { - id?: number; +export class LeaseFormModel extends FileForm implements WithLeaseTeam { lFileNo = ''; psFileNo = ''; tfaFileNumber = ''; @@ -111,7 +109,7 @@ export class LeaseFormModel implements WithLeaseTeam { project?: IAutocompletePrediction; productId: number | null; tenantNotes: string[] = []; - properties: FormLeaseProperty[] = []; + properties: PropertyForm[] = []; securityDeposits: FormLeaseDeposit[] = []; securityDepositReturns: FormLeaseDepositReturn[] = []; periods: FormLeasePeriod[] = []; @@ -157,7 +155,8 @@ export class LeaseFormModel implements WithLeaseTeam { leaseDetail.motiName = apiModel?.motiName || ''; leaseDetail.hasDigitalLicense = apiModel?.hasDigitalLicense ?? undefined; leaseDetail.hasPhysicalLicense = apiModel?.hasPhysicalLicense ?? undefined; - leaseDetail.properties = apiModel?.fileProperties?.map(p => FormLeaseProperty.fromApi(p)) ?? []; + leaseDetail.properties = + apiModel?.fileProperties?.map(p => LeaseFormModel.fromLeasePropertyApi(p)) ?? []; leaseDetail.isResidential = apiModel?.isResidential || false; leaseDetail.isCommercialBuilding = apiModel?.isCommercialBuilding || false; leaseDetail.isOtherImprovement = apiModel?.isOtherImprovement || false; @@ -186,9 +185,9 @@ export class LeaseFormModel implements WithLeaseTeam { public static toApi(formLease: LeaseFormModel): ApiGen_Concepts_Lease { const fileProperties = - formLease.properties?.map(p => FormLeaseProperty.toApi(p)).filter(x => exists(x)) ?? []; + formLease.properties?.map(p => LeaseFormModel.toLeasePropertyApi(p)).filter(x => exists(x)) ?? + []; const sortedProperties = applyDisplayOrder(fileProperties); - return { id: formLease.id ?? 0, lFileNo: stringToNull(formLease.lFileNo), @@ -246,86 +245,35 @@ export class LeaseFormModel implements WithLeaseTeam { fileChecklistItems: formLease.fileChecklist?.map(ck => ck.toApi()) ?? [], isExpired: false, programName: null, - renewalCount: formLease.renewals.length, + renewalCount: formLease.renewals?.length, totalAllowableCompensation: null, ...getEmptyBaseAudit(formLease.rowVersion), }; } - public static getPropertiesAsForm(leaseForm: LeaseFormModel): PropertyForm[] { - return leaseForm.properties - .map(property => { - return property.property; - }) - .filter(exists); - } -} - -export class FormLeaseProperty { - id?: number; - property?: PropertyForm; - leaseId: number | null; - rowVersion?: number; - name?: string; - landArea: number; - areaUnitTypeCode: string; - displayOrder: number | null; - - constructor(leaseId?: number | null) { - this.leaseId = leaseId ?? null; - this.landArea = 0; - this.areaUnitTypeCode = ApiGen_CodeTypes_AreaUnitTypes.M2; - } - - public static fromFormLeaseProperty(baseModel?: Partial): FormLeaseProperty { - const model = Object.assign(new FormLeaseProperty(), baseModel); - return model; - } - - public static fromApi(apiPropertyLease: ApiGen_Concepts_PropertyLease): FormLeaseProperty { - const model = new FormLeaseProperty(apiPropertyLease.file?.id); - model.property = PropertyForm.fromApi(apiPropertyLease); - model.id = apiPropertyLease.id; - model.rowVersion = apiPropertyLease.rowVersion ?? undefined; - model.name = apiPropertyLease.propertyName ?? undefined; - model.landArea = apiPropertyLease.leaseArea ?? 0; - model.areaUnitTypeCode = apiPropertyLease.areaUnitType?.id || ApiGen_CodeTypes_AreaUnitTypes.M2; - model.displayOrder = apiPropertyLease.displayOrder; - return model; - } - - public static fromFeatureDataset(mapProperty: LocationFeatureDataset): FormLeaseProperty { - const model = new FormLeaseProperty(); - model.property = PropertyForm.fromLocationFeatureDataset(mapProperty); - return model; - } - - public static fromPropertyForm(propertyForm: PropertyForm): FormLeaseProperty { - const model = new FormLeaseProperty(); - model.property = new PropertyForm(propertyForm); - return model; - } - - public static toApi(formLeaseProperty: FormLeaseProperty): ApiGen_Concepts_PropertyLease | null { - if (!exists(formLeaseProperty?.property)) { - return null; - } - - const apiFileProperty = formLeaseProperty?.property?.toFilePropertyApi( - formLeaseProperty.leaseId, + private static fromLeasePropertyApi( + apiPropertyLease: ApiGen_Concepts_PropertyLease, + ): PropertyForm { + const propertyForm = PropertyForm.fromApi(apiPropertyLease); + propertyForm.landArea = apiPropertyLease.leaseArea ?? 0; + propertyForm.areaUnit = enumFromValue( + apiPropertyLease.areaUnitType?.id ?? ApiGen_CodeTypes_AreaUnitTypes.M2, + ApiGen_CodeTypes_AreaUnitTypes, ); + return propertyForm; + } - return { + private static toLeasePropertyApi(property: PropertyForm): ApiGen_Concepts_PropertyLease { + const apiFileProperty = property.toFilePropertyApi(); + const leaseFileProperty: ApiGen_Concepts_PropertyLease = { ...apiFileProperty, - id: formLeaseProperty.id ?? 0, - file: null, - propertyName: formLeaseProperty.name ?? null, - leaseArea: isNumber(formLeaseProperty.landArea) ? formLeaseProperty.landArea : 0, - areaUnitType: isNumber(formLeaseProperty.landArea) - ? toTypeCodeNullable(formLeaseProperty.areaUnitTypeCode) ?? null + leaseArea: isNumber(property.landArea) ? property.landArea : 0, + areaUnitType: isNumber(property.landArea) + ? toTypeCodeNullable(property.areaUnit) ?? null : null, - ...getEmptyBaseAudit(formLeaseProperty.rowVersion), + file: null, }; + return leaseFileProperty; } } diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.test.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.test.tsx deleted file mode 100644 index 676de04c03..0000000000 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.test.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import { Formik, FormikProps } from 'formik'; -import noop from 'lodash/noop'; -import React from 'react'; - -import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext'; -import { FormLeaseProperty, getDefaultFormLease, LeaseFormModel } from '@/features/leases/models'; -import { getMockPolygon } from '@/mocks/geometries.mock'; -import { mockLookups } from '@/mocks/lookups.mock'; -import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; -import { getMockApiProperty } from '@/mocks/properties.mock'; -import { emptyRegion } from '@/models/layers/motRegionalBoundary'; -import { emptyPmbcParcel } from '@/models/layers/parcelMapBC'; -import { emptyPropertyLocation } from '@/models/layers/pimsPropertyLocationView'; -import { lookupCodesSlice } from '@/store/slices/lookupCodes'; -import * as mapUtils from '@/utils/mapPropertyUtils'; -import { act, render, RenderOptions, screen, userEvent } from '@/utils/test-utils'; - -import LeasePropertySelector from './LeasePropertySelector'; -import { PropertyForm } from '@/features/mapSideBar/shared/models'; -import { - emptyFeatureDataset, - LocationFeatureDataset, -} from '@/components/common/mapFSM/useLocationFeatureLoader'; - -const storeState = { - [lookupCodesSlice.name]: { lookupCodes: mockLookups }, -}; - -const confirmBeforeAdd = vi.fn(); - -describe('LeasePropertySelector component', () => { - // render component under test - const setup = ( - props: { - initialForm: LeaseFormModel; - }, - renderOptions: RenderOptions = {}, - ) => { - const formikRef = React.createRef>(); - const utils = render( - - initialValues={props.initialForm} - onSubmit={noop} - innerRef={formikRef} - > - {props => } - , - { - ...renderOptions, - store: storeState, - claims: [], - mockMapMachine: renderOptions.mockMapMachine, - }, - ); - - return { - ...utils, - formikRef, - rerender: () => { - utils.rerender( - - initialValues={props.initialForm} - onSubmit={noop} - innerRef={formikRef} - > - {props => ( - - )} - , - ); - }, - }; - }; - - let testForm: LeaseFormModel; - - beforeEach(() => { - testForm = getDefaultFormLease(); - testForm.lFileNo = 'Test name'; - testForm.properties = [ - FormLeaseProperty.fromApi({ - id: 1, - location: { coordinate: { y: 44, x: -77 } }, - boundary: null, - fileId: 1, - file: null, - isActive: null, - leaseArea: 0, - areaUnitType: null, - propertyName: null, - displayOrder: null, - rowVersion: 1, - propertyId: 1, - property: { - ...getMockApiProperty(), - pid: 123456789, - latitude: 44, - longitude: -77, - }, - }), - FormLeaseProperty.fromApi({ - id: 2, - location: { coordinate: { y: 44, x: -77 } }, - boundary: null, - fileId: 1, - file: null, - isActive: null, - leaseArea: 0, - areaUnitType: null, - propertyName: null, - displayOrder: null, - rowVersion: 1, - propertyId: 2, - property: { - ...getMockApiProperty(), - id: 2, - pin: 1111222, - pid: null, - latitude: 44, - longitude: -77, - }, - }), - ]; - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('renders as expected', async () => { - const { asFragment } = setup({ initialForm: testForm }); - await act(async () => {}); - expect(asFragment()).toMatchSnapshot(); - }); - - it('renders default message when list of properties is empty', async () => { - setup({ initialForm: { ...testForm, properties: [] } }); - expect(await screen.findByText(/No Properties selected/i)).toBeVisible(); - }); - - it('renders list of properties', async () => { - setup({ initialForm: testForm }); - - expect(await screen.findByText('PID: 123-456-789')).toBeVisible(); - expect(await screen.findByText('PIN: 1111222')).toBeVisible(); - }); - - it('should remove property from list when Remove button is clicked and popup is confirmed', async () => { - setup({ initialForm: testForm }); - - // click remove button and confirm the popup - const pidRow = screen.getAllByTitle('remove')[0]; - await act(async () => userEvent.click(pidRow)); - const ok = screen.getByTitle('ok-modal'); - await act(async () => userEvent.click(ok)); - - expect(screen.queryByText('PID: 123-456-789')).toBeNull(); - }); - - it('should display properties with svg prefixed with incrementing id', async () => { - setup({ initialForm: testForm }); - - expect(screen.getByTitle('1')).toBeInTheDocument(); - expect(screen.getByTitle('2')).toBeInTheDocument(); - }); - - // TODO: fix tests affected by the removal of the property selector tool - it.skip('should pre-populate the region if a property is selected', async () => { - const testMockMachine: IMapStateMachineContext = { - ...mapMachineBaseMock, - isSelecting: true, - mapLocationFeatureDataset: null, - }; - - const leaseWithoutProperties = testForm; - leaseWithoutProperties.properties = []; - - const { rerender, formikRef } = setup( - { initialForm: leaseWithoutProperties }, - { mockMapMachine: testMockMachine }, - ); - - // no region should be selected by default - expect(formikRef.current.values.regionId).toBe(''); - - // simulate a map click via the map state machine - testMockMachine.isSelecting = true; - testMockMachine.mapLocationFeatureDataset = { - ...emptyFeatureDataset(), - location: { lng: -120.69195885, lat: 50.25163372 }, - pimsFeatures: [ - { - type: 'Feature', - properties: { ...emptyPropertyLocation, PROPERTY_ID: 1, PID: 1 }, - geometry: getMockPolygon(), - }, - ], - parcelFeatures: [ - { - type: 'Feature', - properties: { ...emptyPmbcParcel, PID_NUMBER: 1 }, - geometry: getMockPolygon(), - }, - ], - regionFeature: { - type: 'Feature', - properties: { ...emptyRegion, REGION_NUMBER: 1, REGION_NAME: 'South Coast Region' }, - geometry: getMockPolygon(), - }, - }; - - // verify that upon map click the lease region is auto-selected based on the property region - await act(async () => rerender()); - expect(formikRef.current.values.regionId).toBe('1'); - }); - - // TODO: fix tests affected by the removal of the property selector tool - it.skip('should display a warning when adding a property to the inventory', async () => { - const testMockMachine: IMapStateMachineContext = { - ...mapMachineBaseMock, - isSelecting: true, - mapLocationFeatureDataset: null, - }; - - const { rerender, formikRef } = setup( - { initialForm: testForm }, - { mockMapMachine: testMockMachine }, - ); - - expect(formikRef.current.values.properties.length).toBe(testForm.properties.length); - - // simulate a map click via the map state machine - testMockMachine.isSelecting = true; - testMockMachine.mapLocationFeatureDataset = { - ...emptyFeatureDataset(), - location: { lng: -120.69195885, lat: 50.25163372 }, - pimsFeatures: null, // no PIMS property was found - meaning this property will be added to the inventory. - parcelFeatures: [ - { - type: 'Feature', - properties: { ...emptyPmbcParcel }, - geometry: getMockPolygon(), - }, - ], - regionFeature: { - type: 'Feature', - properties: { ...emptyRegion, REGION_NUMBER: 1, REGION_NAME: 'South Coast Region' }, - geometry: getMockPolygon(), - }, - }; - - // verify that upon map click the user gets a confirmation popup to add the property to inventory - await act(async () => rerender()); - - expect( - await screen.findByText( - 'You have selected a property not previously in the inventory. Do you want to add this property to the lease?', - ), - ).toBeVisible(); - - const okButton = screen.getByTitle('ok-modal'); - await act(async () => userEvent.click(okButton)); - - // the property gets added to the lease form successfully - expect(formikRef.current.values.properties.length).toBe(testForm.properties.length + 1); - }); - - // TODO: fix tests affected by the removal of the property selector tool - it.skip('should update the property lat/lng when file marker is repositioned', async () => { - // mock these functions to simplify unit test execution - const spy = vi.spyOn(mapUtils, 'isLatLngInFeatureSetBoundary'); - spy.mockImplementationOnce(() => true); - - const testMockMachine: IMapStateMachineContext = { - ...mapMachineBaseMock, - mapLocationFeatureDataset: null, - // provide fake logic for map marker repositioning - startReposition: vi.fn(() => { - testMockMachine.isRepositioning = true; - }), - }; - - const { rerender, formikRef } = setup( - { initialForm: testForm }, - { mockMapMachine: testMockMachine }, - ); - - // this is the original lat/lng of the property - expect(formikRef.current.values.properties[0].property.fileLocation).toStrictEqual({ - lng: -77, - lat: 44, - }); - - // click the button to reposition the first property marker - const moveButton = screen.getAllByTitle('move-pin-location')[0]; - await act(async () => userEvent.click(moveButton)); - - // map state machine method should have been called to start repositioning file marker on the map - expect(testMockMachine.startReposition).toHaveBeenCalled(); - - // simulate file marker repositioning via the map state machine - testMockMachine.isRepositioning = true; - testMockMachine.mapLocationFeatureDataset = { - ...emptyFeatureDataset(), - location: { lng: -120, lat: 50 }, // new lat/lng for the marker - pimsFeatures: [ - { - type: 'Feature', - properties: { ...emptyPropertyLocation, PROPERTY_ID: 1 }, - geometry: getMockPolygon(), - }, - ], - parcelFeatures: [ - { - type: 'Feature', - properties: { ...emptyPmbcParcel }, - geometry: getMockPolygon(), - }, - ], - regionFeature: { - type: 'Feature', - properties: { ...emptyRegion, REGION_NUMBER: 1, REGION_NAME: 'South Coast Region' }, - geometry: getMockPolygon(), - }, - }; - - await act(async () => rerender()); - - // verify that upon map click the file marker is repositioned and the new lat/lng is saved - expect(formikRef.current.values.properties[0].property.fileLocation).toStrictEqual({ - lng: -120, - lat: 50, - }); - }); -}); diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx deleted file mode 100644 index f04e8be570..0000000000 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { FormikProps } from 'formik'; -import { useCallback, useContext, useMemo, useRef } from 'react'; - -import { ModalProps } from '@/components/common/GenericModal'; -import LoadingBackdrop from '@/components/common/LoadingBackdrop'; -import { Section } from '@/components/common/Section/Section'; -import { ModalContext } from '@/contexts/modalContext'; -import { PropertyForm } from '@/features/mapSideBar/shared/models'; -import PropertiesListContainer from '@/features/mapSideBar/shared/update/properties/PropertiesListContainer'; -import { usePropertyFormSyncronizer } from '@/hooks/usePropertyFormSyncronizer'; - -import { LeaseFormModel } from '../../models'; - -interface LeasePropertySelectorProp { - formikProps: FormikProps; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; -} - -export const LeasePropertySelector: React.FunctionComponent = ({ - formikProps, - confirmBeforeAdd, -}) => { - const { values } = formikProps; - const localRef = useRef>(null); - - const { setModalContent, setDisplayModal } = useContext(ModalContext); - - const { isLoading } = usePropertyFormSyncronizer(localRef, 'properties'); - const formProperties = useMemo(() => values.properties.map(x => x.property), [values.properties]); - - const cancelRemove = useCallback(() => { - setDisplayModal(false); - }, [setDisplayModal]); - - const getRemoveModalProps = useCallback<(removeCallback: () => void) => ModalProps>( - (removeCallback: () => void) => { - return { - variant: 'info', - title: 'Removing Property from Lease/Licence', - message: 'Are you sure you want to remove this property from this lease/licence?', - display: false, - okButtonText: 'Remove', - cancelButtonText: 'Cancel', - handleOk: () => removeCallback(), - handleCancel: cancelRemove, - }; - }, - [cancelRemove], - ); - - const onRemoveClick = useCallback( - (_: number, removeCallback: () => void) => { - setModalContent(getRemoveModalProps(removeCallback)); - setDisplayModal(true); - }, - [getRemoveModalProps, setDisplayModal, setModalContent], - ); - - return ( -
-
- Select one or more properties that you want to include in this lease/licence file. You can - choose a location from the map, or search by other criteria. -
- - -
- ); -}; - -export default LeasePropertySelector; diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx deleted file mode 100644 index e4f19cb83b..0000000000 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import { AxiosError } from 'axios'; -import { FieldArray, FieldArrayRenderProps, Formik, FormikProps } from 'formik'; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Col, Row } from 'react-bootstrap'; -import { toast } from 'react-toastify'; -import styled from 'styled-components'; - -import { Button } from '@/components/common/buttons'; -import GenericModal, { ModalProps } from '@/components/common/GenericModal'; -import LoadingBackdrop from '@/components/common/LoadingBackdrop'; -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { Section } from '@/components/common/Section/Section'; -import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; -import AreaContainer from '@/components/measurements/AreaContainer'; -import MapClickMonitor from '@/components/propertySelector/MapClickMonitor'; -import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; -import { ModalContext } from '@/contexts/modalContext'; -import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; -import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; -import SidebarFooter from '@/features/mapSideBar/shared/SidebarFooter'; -import usePathGenerator from '@/features/mapSideBar/shared/sidebarPathGenerator'; -import AddPropertiesGuide from '@/features/mapSideBar/shared/update/properties/AddPropertiesGuide'; -import { UpdatePropertiesYupSchema } from '@/features/mapSideBar/shared/update/properties/UpdatePropertiesYupSchema'; -import { usePropertyLeaseRepository } from '@/hooks/repositories/usePropertyLeaseRepository'; -import useApiUserOverride from '@/hooks/useApiUserOverride'; -import { useLocationFeatureDatasetsWithAddresses } from '@/hooks/useLocationFeatureDatasetsWithAddresses'; -import { getCancelModalProps } from '@/hooks/useModalContext'; -import { IApiError } from '@/interfaces/IApiError'; -import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; -import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { - arePropertyFormsEqual, - exists, - isEmptyOrNull, - isLatLngInFeatureSetBoundary, - isNumber, - isValidId, -} from '@/utils'; -import { withNameSpace } from '@/utils/formUtils'; - -import { useLeaseDetail } from '../../hooks/useLeaseDetail'; -import { FormLeaseProperty, LeaseFormModel } from '../../models'; -import SelectedPropertyHeaderRow from './selectedPropertyList/SelectedPropertyHeaderRow'; -interface LeaseUpdatePropertySelectorProp { - lease: ApiGen_Concepts_Lease; -} - -export const LeaseUpdatePropertySelector: React.FunctionComponent< - LeaseUpdatePropertySelectorProp -> = ({ lease }) => { - const pathSolver = usePathGenerator(); - const [showSaveConfirmModal, setShowSaveConfirmModal] = useState(false); - const [repositionPropertyIndex, setRepositionPropertyIndex] = useState(null); - const [isValid, setIsValid] = useState(true); - const hasWarnedRef = useRef(false); - - const { setModalContent, setDisplayModal } = useContext(ModalContext); - const { resetFilePropertyLocations } = useContext(SideBarContext); - - const formikRef = useRef>(); - const arrayHelpersRef = useRef(null); - - const { updateLeaseProperties } = usePropertyLeaseRepository(); - const { getCompleteLease } = useLeaseDetail(lease?.id ?? undefined); - - const { - locationFeaturesForAddition, - processLocationFeaturesAddition: processCreation, - mapLocationFeatureDataset, - requestLocationFeatureAddition: prepareForCreation, - isRepositioning, - finishReposition, - setEditPropertiesMode, - refreshMapProperties, - } = useMapStateMachine(); - - const withUserOverride = useApiUserOverride< - (userOverrideCodes: UserOverrideCode[]) => Promise - >('Failed to update Lease File Properties'); - - const addPropertiesToCurrentFile = useCallback( - ( - formikRef: React.RefObject>, - leasePropertyForms: FormLeaseProperty[], - ) => { - const existingProperties = formikRef.current?.values?.properties ?? []; - const uniqueProperties = leasePropertyForms.filter(newProperty => { - return !existingProperties.some(existingProperty => - arePropertyFormsEqual(existingProperty.property, newProperty.property), - ); - }); - - const duplicatesSkipped = leasePropertyForms.length - uniqueProperties.length; - - // If there are unique properties, add them to the formik values - if (uniqueProperties.length > 0) { - formikRef.current?.setFieldValue('properties', [ - ...existingProperties, - ...uniqueProperties, - ]); - formikRef.current?.setFieldTouched('properties', true); - toast.success(`Added ${uniqueProperties.length} new property(s) to the file.`); - } - - if (duplicatesSkipped > 0) { - toast.warn(`Skipped ${duplicatesSkipped} duplicate property(s).`); - } - }, - [], - ); - - // Get PropertyForms with addresses for all selected features - const { locationFeaturesWithAddresses: featuresWithAddresses, bcaLoading } = - useLocationFeatureDatasetsWithAddresses(locationFeaturesForAddition ?? []); - - // Convert SelectedFeatureDataset to FormLeaseProperty - const propertyForms = useMemo( - () => - featuresWithAddresses.map(obj => { - const formProperty = FormLeaseProperty.fromFeatureDataset(obj.feature); - if (exists(obj.address)) { - formProperty.property.address = obj.address; - } - return formProperty; - }), - [featuresWithAddresses], - ); - - // This effect is used to update the file properties when "add to open file" is clicked in the worklist. - useEffect(() => { - if (exists(formikRef.current) && propertyForms.length > 0 && !hasWarnedRef.current) { - const needsWarning = propertyForms.some( - formProperty => exists(formProperty.property) && !isValidId(formProperty.property.apiId), - ); - - if (needsWarning) { - hasWarnedRef.current = true; // mark as shown - setModalContent({ - variant: 'info', - title: 'Not inventory property', - message: - 'You have selected a property not previously in the inventory. Do you want to add this property to the lease?', - okButtonText: 'Add', - cancelButtonText: 'Cancel', - handleOk: () => { - setDisplayModal(false); - addPropertiesToCurrentFile(formikRef, propertyForms); - }, - handleCancel: () => { - setDisplayModal(false); - }, - }); - setDisplayModal(true); - } else { - // If no warning is needed, simply add the properties to the current file. - addPropertiesToCurrentFile(formikRef, propertyForms); - } - processCreation(); - } - }, [ - formikRef, - propertyForms, - setModalContent, - setDisplayModal, - addPropertiesToCurrentFile, - processCreation, - ]); - - const cancelRemove = useCallback(() => { - setDisplayModal(false); - }, [setDisplayModal]); - - const confirmRemove = useCallback( - (indexToRemove: number) => { - if (indexToRemove !== undefined) { - arrayHelpersRef.current?.remove(indexToRemove); - } - setDisplayModal(false); - }, - [setDisplayModal], - ); - - const getRemoveModalProps = useCallback<(index: number) => ModalProps>( - (index: number) => { - return { - variant: 'info', - title: 'Removing Property from Lease/Licence', - message: 'Are you sure you want to remove this property from this lease/licence?', - display: false, - okButtonText: 'Remove', - cancelButtonText: 'Cancel', - handleOk: () => confirmRemove(index), - handleCancel: cancelRemove, - }; - }, - [confirmRemove, cancelRemove], - ); - - const onRemoveClick = useCallback( - (index: number) => { - setModalContent(getRemoveModalProps(index)); - setDisplayModal(true); - }, - [getRemoveModalProps, setDisplayModal, setModalContent], - ); - - const handleCancelConfirm = () => { - if (formikRef !== undefined) { - formikRef.current?.resetForm(); - } - processCreation(); - resetFilePropertyLocations(); - pathSolver.showFile('lease', lease.id); - }; - - const handleSaveClick = async () => { - await formikRef?.current?.validateForm(); - if (!formikRef?.current?.isValid) { - setIsValid(false); - } else { - setIsValid(true); - } - setShowSaveConfirmModal(true); - }; - - const handleCancelClick = () => { - if (formikRef !== undefined) { - if (formikRef.current?.dirty) { - setModalContent({ - ...getCancelModalProps(), - handleOk: () => { - handleCancelConfirm(); - setDisplayModal(false); - }, - handleCancel: () => setDisplayModal(false), - }); - setDisplayModal(true); - } else { - handleCancelConfirm(); - } - } else { - handleCancelConfirm(); - } - }; - - const handleSaveConfirm = async () => { - if (formikRef !== undefined) { - formikRef.current?.setSubmitting(true); - formikRef.current?.submitForm(); - } - }; - - const saveFile = async (file: ApiGen_Concepts_Lease) => { - return withUserOverride( - (userOverrideCodes: UserOverrideCode[]) => { - return updateLeaseProperties.execute(file, userOverrideCodes).then(async response => { - formikRef.current?.setSubmitting(false); - if (isValidId(response?.id)) { - if (file.fileProperties?.find(fp => !fp.property?.address && !fp.property?.id)) { - toast.warn( - 'Address could not be retrieved for this property, it will have to be provided manually in property details tab', - { autoClose: 15000 }, - ); - } - formikRef.current?.resetForm(); - await getCompleteLease(); - refreshMapProperties(); - pathSolver.showFile('lease', lease.id); - } - }); - }, - [], - (axiosError: AxiosError) => { - setModalContent({ - variant: 'error', - title: 'Error', - message: axiosError?.response?.data.error, - okButtonText: 'Close', - handleOk: async () => { - formikRef.current?.resetForm(); - await getCompleteLease(); - setDisplayModal(false); - }, - }); - setDisplayModal(true); - }, - ); - }; - - const initialValues = LeaseFormModel.fromApi(lease); - - const handleAddToSelection = useCallback(() => { - prepareForCreation([mapLocationFeatureDataset]); - }, [prepareForCreation, mapLocationFeatureDataset]); - - useEffect(() => { - // Set the map state machine to edit properties mode so that the map selector knows what mode it is in. - setEditPropertiesMode(true); - return () => { - setEditPropertiesMode(false); - }; - }, [setEditPropertiesMode]); - - return ( - <> - - - } - > - - innerRef={formikRef} - initialValues={initialValues} - validationSchema={UpdatePropertiesYupSchema} - onSubmit={async (values: LeaseFormModel) => { - try { - const file: ApiGen_Concepts_Lease = LeaseFormModel.toApi(values); - await saveFile(file); - } finally { - processCreation(); - } - }} - > - {formikProps => ( - { - arrayHelpersRef.current = arrayHelpers; - return ( - <> - - {!isEmptyOrNull(mapLocationFeatureDataset?.parcelFeatures) && ( - - - - )} -
- Selected Properties - - lp.property)} - /> - - - } - > - { - if ( - isRepositioning && - isNumber(repositionPropertyIndex) && - repositionPropertyIndex >= 0 && - !hasMultipleProperties - ) { - // As long as the marker is repositioned within the boundary of the originally selected property simply reposition the marker without further notification. - const formProperty = - formikRef?.current?.values?.properties[repositionPropertyIndex]; - - if ( - isLatLngInFeatureSetBoundary( - locationDataSet.location, - formProperty.property.toLocationFeatureDataset(), - ) - ) { - const updatedFormProperty = - FormLeaseProperty.fromFormLeaseProperty(formProperty); - updatedFormProperty.property.fileLocation = locationDataSet.location; - - // Find property within formik values and reposition it based on incoming file marker position - arrayHelpers.replace(repositionPropertyIndex, updatedFormProperty); - - // Reset the reposition state - finishReposition(); - setRepositionPropertyIndex(null); - } - } else { - toast.warn( - 'Please choose a location that is within the (highlighted) boundary of this property.', - ); - } - }} - /> - - {formikProps.values.properties.map((leaseProperty, index) => { - const property = leaseProperty?.property; - if (exists(property)) { - const nameSpace = `properties.${index}`; - return ( - <> - onRemoveClick(index)} - canReposition={false} - nameSpace={`${nameSpace}.property`} - index={index} - property={property} - showDisable={false} - canUploadShapefile={false} - /> - - - { - formikProps.setFieldValue( - withNameSpace(nameSpace, 'landArea'), - landArea, - ); - formikProps.setFieldValue( - withNameSpace(nameSpace, 'areaUnitTypeCode'), - areaUnitTypeCode, - ); - }} - /> - - - - ); - } - return <>; - })} - {formikProps.values.properties.length === 0 && ( - No Properties selected - )} -
- - ); - }} - /> - )} - -
- -
You have made changes to the properties in this file.
-
- Do you want to save these changes? - - } - handleOk={handleSaveConfirm} - handleCancel={() => setShowSaveConfirmModal(false)} - okButtonText="Save" - cancelButtonText="Cancel" - show - /> - - ); -}; - -export default LeaseUpdatePropertySelector; - -const StyledButtonWrapper = styled.div` - margin: 0 1.6rem; - padding-left: 1.6rem; - text-align: left; - text-underline-offset: 2px; - - button { - font-size: 14px; - } -`; diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx index b9b431f85f..f4c8fb2933 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx @@ -29,7 +29,7 @@ import { exists, isValidId, sortFileProperties, stripTrailingSlash } from '@/uti import { SideBarContext } from '../context/sidebarContext'; import { FileTabType } from '../shared/detail/FileTabs'; -import { PropertyForm } from '../shared/models'; +import { FileForm, PropertyForm } from '../shared/models'; import usePathGenerator from '../shared/sidebarPathGenerator'; import { IAcquisitionViewProps } from './AcquisitionView'; @@ -315,9 +315,8 @@ export const AcquisitionContainer: React.FunctionComponent => { + const onUpdateProperties = (fileForm: FileForm): Promise => { + const file = fileForm.toApi(); // The backend does not update the product or project so its safe to send nulls even if there might be data for those fields. return withUserOverride( (userOverrideCodes: UserOverrideCode[]) => { diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx index af67c7ddce..cf30d9c483 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx @@ -29,7 +29,7 @@ import { InventoryTabNames } from '../property/InventoryTabs'; import FilePropertyRouter from '../router/FilePropertyRouter'; import { FileTabType } from '../shared/detail/FileTabs'; import FileMenuView from '../shared/FileMenuView'; -import { PropertyForm } from '../shared/models'; +import { FileForm, PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import UpdatePropertiesContainer from '../shared/update/properties/UpdatePropertiesContainer'; @@ -48,7 +48,7 @@ export interface IAcquisitionViewProps { onSelectProperty: (propertyId: number) => void; onEditProperties: () => void; onSuccess: () => void; - onUpdateProperties: (file: ApiGen_Concepts_File) => Promise; + onUpdateProperties: (file: FileForm) => Promise; confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; canRemove: (propertyId: number) => Promise; isEditing: boolean; @@ -87,6 +87,7 @@ export const AcquisitionView: React.FunctionComponent = ( const acquisitionFile: ApiGen_Concepts_AcquisitionFile = { ...file, } as unknown as ApiGen_Concepts_AcquisitionFile; + const formFile = FileForm.fromApi(acquisitionFile); // match for property menu routes - eg /property/1/ltsa const fileMatch = matchPath>(location.pathname, `${match.path}/:tab`); @@ -117,14 +118,14 @@ export const AcquisitionView: React.FunctionComponent = ( return ( - {file && ( + {formFile && (

This property has already been added to one or more acquisition files.

Do you want to acknowledge and proceed?

diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx index 6aa0f106b3..59e2a8dd4b 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx @@ -41,7 +41,7 @@ export const AddAcquisitionContainer: React.FC = const { setModalContent, setDisplayModal } = useModalContext(); const { execute: getPropertyAssociations } = usePropertyAssociations(); - const [needsUserConfirmation, setNeedsUserConfirmation] = useState(true); + const [showFirstTimeConfirmation, setShowFirstTimeConfirmation] = useState(true); const { getAcquisitionFile: { execute: getAcquisitionFile, response: parentAcquisitionFile }, @@ -65,7 +65,71 @@ export const AddAcquisitionContainer: React.FC = const mapMachine = useMapStateMachine(); - const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); + //Verifies that the property does not belong to another acquisition file already + const confirmProperty = useCallback( + async (propertyForm: PropertyForm) => { + if (isValidId(propertyForm.apiId)) { + const response = await getPropertyAssociations(propertyForm.apiId); + const acquisitionAssociations = response?.acquisitionAssociations ?? []; + const otherAcqFiles = acquisitionAssociations.filter(a => exists(a.id)); + return otherAcqFiles.length > 0; + } else { + // the property is not in PIMS db -> no need to confirm + return false; + } + }, + [getPropertyAssociations], + ); + + // Require user confirmation before adding a property to file + // This is the flow for Map Marker -> right-click -> create Acquisition File + const confirmBeforeAdd = useCallback( + async (newPropertyForms: PropertyForm[], isValidCallback: (isValid: boolean) => void) => { + const needsConfirmation = await Promise.all( + newPropertyForms.map(formProperty => confirmProperty(formProperty)), + ); + if (showFirstTimeConfirmation && needsConfirmation.some(x => x === true)) { + setModalContent({ + variant: 'warning', + title: 'User Override Required', + message: ( + <> +

+ One or more properties have already been added to one or more acquisition files. +

+

Do you want to acknowledge and proceed?

+ + ), + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: () => { + // allow the property to be added to the file being created + isValidCallback(true); + setDisplayModal(false); + // show the user confirmation modal only once when creating a file + setShowFirstTimeConfirmation(false); + }, + handleCancel: () => { + // clear out the properties array as the user did not agree to the popup + isValidCallback(false); + setDisplayModal(false); + // show the user confirmation modal only once when creating a file + setShowFirstTimeConfirmation(false); + }, + }); + setDisplayModal(true); + } else { + isValidCallback(true); + } + }, + [confirmProperty, showFirstTimeConfirmation, setDisplayModal, setModalContent], + ); + + const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer( + formikRef, + 'properties', + confirmBeforeAdd, + ); useEffect(() => { if (featuresWithAddresses?.length > 0 && !isSubFile && !formikRef?.current?.values?.region) { @@ -111,73 +175,6 @@ export const AddAcquisitionContainer: React.FC = } }, [isSubFile, onClose, parentId]); - // Warn user that property is part of an existing acquisition file - const confirmBeforeAdd = useCallback( - async (propertyForm: PropertyForm) => { - if (isValidId(propertyForm.apiId)) { - const response = await getPropertyAssociations(propertyForm.apiId); - const acquisitionAssociations = response?.acquisitionAssociations ?? []; - const otherAcqFiles = acquisitionAssociations.filter(a => exists(a.id)); - return otherAcqFiles.length > 0; - } else { - // the property is not in PIMS db -> no need to confirm - return false; - } - }, - [getPropertyAssociations], - ); - - // Require user confirmation before adding a property to file - // This is the flow for Map Marker -> right-click -> create Acquisition File - useEffect(() => { - const runAsync = async () => { - if (exists(initialForm) && exists(formikRef?.current) && needsUserConfirmation) { - if (initialForm.properties.length > 0) { - // Check all properties for confirmation - const needsConfirmation = await Promise.all( - initialForm.properties.map(formProperty => confirmBeforeAdd(formProperty)), - ); - if (needsConfirmation.some(confirm => confirm)) { - setModalContent({ - variant: 'warning', - title: 'User Override Required', - message: ( - <> -

- One or more properties have already been added to one or more acquisition files. -

-

Do you want to acknowledge and proceed?

- - ), - okButtonText: 'Yes', - cancelButtonText: 'No', - handleOk: () => { - // allow the property to be added to the file being created - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', initialForm.properties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - handleCancel: () => { - // clear out the properties array as the user did not agree to the popup - initialForm.properties.splice(0, initialForm.properties.length); - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', initialForm.properties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - }); - setDisplayModal(true); - } - } - } - }; - - runAsync(); - }, [confirmBeforeAdd, initialForm, needsUserConfirmation, setDisplayModal, setModalContent]); - const checkState = useCallback(() => { return (isSubFile || formikRef?.current?.dirty) && !formikRef?.current?.isSubmitting; }, [formikRef, isSubFile]); @@ -260,7 +257,7 @@ export const AddAcquisitionContainer: React.FC = ); }} validationSchema={AddAcquisitionFileYupSchema} - confirmBeforeAdd={confirmBeforeAdd} + confirmBeforeAdd={confirmProperty} /> diff --git a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx index 68bba2c28f..1d7ba5312c 100644 --- a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx @@ -1,3 +1,4 @@ +import { AxiosError } from 'axios'; import { FormikProps } from 'formik'; import React, { useCallback, @@ -8,7 +9,8 @@ import React, { useRef, useState, } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import { toast } from 'react-toastify'; import * as Yup from 'yup'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; @@ -29,13 +31,18 @@ import LeaseStakeholderContainer from '@/features/leases/detail/LeasePages/stake import Surplus from '@/features/leases/detail/LeasePages/surplus/Surplus'; import { isLeaseFile, LeaseFormModel } from '@/features/leases/models'; import { useLeaseRepository } from '@/hooks/repositories/useLeaseRepository'; +import { usePropertyLeaseRepository } from '@/hooks/repositories/usePropertyLeaseRepository'; import { useQuery } from '@/hooks/use-query'; +import useApiUserOverride from '@/hooks/useApiUserOverride'; import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; +import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; -import { exists } from '@/utils'; +import { UserOverrideCode } from '@/models/api/UserOverrideCode'; +import { exists, isValidId } from '@/utils'; import { SideBarContext } from '../context/sidebarContext'; +import { PropertyForm } from '../shared/models'; import usePathGenerator from '../shared/sidebarPathGenerator'; import { LeaseFileTabNames } from './detail/LeaseFileTabs'; import { ILeaseViewProps } from './LeaseView'; @@ -211,10 +218,15 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos } = useContext(SideBarContext); const query = useQuery(); + const match = useRouteMatch(); const history = useHistory(); const [isValid, setIsValid] = useState(true); const { setModalContent, setDisplayModal } = useModalContext(); + const withUserOverride = useApiUserOverride< + (userOverrideCodes: UserOverrideCode[]) => Promise + >('Failed to update Lease File'); + const pathSolver = usePathGenerator(); const activeTab = containerState.activeTab; @@ -224,6 +236,8 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos getLastUpdatedBy: { execute: getLastUpdatedBy, loading: getLastUpdatedByLoading }, } = useLeaseRepository(); + const { updateLeaseProperties } = usePropertyLeaseRepository(); + const lease: ApiGen_Concepts_Lease | null = isLeaseFile(file) ? file : null; const fileProperties: ApiGen_Concepts_FileProperty[] = useMemo(() => { @@ -373,7 +387,6 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos } else { query.delete('edit'); } - setContainerState({ isEditing: value, }); @@ -386,14 +399,69 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos setIsPropertyEditing(query.get('edit') === 'true'); }, [query, setIsPropertyEditing]); - const onPropertyUpdate = () => { + const onSuccess = () => { setIsPropertyEditing(false); refresh(); }; + const closePropertySelector = () => { + onSuccess(); + history.push(`${match.url}`); + }; + + /* + useEffect(() => { + setIsPropertyEditing(query.get('edit') === 'true'); + }, [query, setIsPropertyEditing]); + */ + + const confirmBeforeAdd = async (propertyForm: PropertyForm): Promise => { + return !isValidId(propertyForm.apiId); + }; + + const onUpdateProperties = async (updatedFormFile: LeaseFormModel) => { + return withUserOverride( + (userOverrideCodes: UserOverrideCode[]) => { + const updatedFile = LeaseFormModel.toApi(updatedFormFile); + return updateLeaseProperties + .execute(updatedFile, userOverrideCodes) + .then(async response => { + formikRef.current?.setSubmitting(false); + if (isValidId(response?.id)) { + if ( + updatedFile.fileProperties?.find(fp => !fp.property?.address && !fp.property?.id) + ) { + toast.warn( + 'Address could not be retrieved for this property, it will have to be provided manually in property details tab', + { autoClose: 15000 }, + ); + } + formikRef.current?.resetForm(); + closePropertySelector(); + } + }); + }, + [], + (axiosError: AxiosError) => { + setModalContent({ + variant: 'error', + title: 'Error', + message: axiosError?.response?.data.error, + okButtonText: 'Close', + handleOk: async () => { + formikRef.current?.resetForm(); + await getCompleteLease(); + setDisplayModal(false); + }, + }); + setDisplayModal(true); + }, + ); + }; + // UI components if (loading || getLastUpdatedByLoading) { - return ; + return ; } return ( @@ -405,7 +473,7 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos onSelectFileSummary={onSelectFileSummary} onSelectProperty={onSelectProperty} onEditProperties={onEditProperties} - onPropertyUpdateSuccess={onPropertyUpdate} + onPropertyUpdateSuccess={onSuccess} onChildSuccess={onChildSuccess} refreshLease={refresh} setLease={setLease} @@ -414,7 +482,11 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos setContainerState={setContainerState} isFormValid={isValid} lease={lease} - > + setIsShowingPropertySelector={closePropertySelector} + onUpdateProperties={onUpdateProperties} + confirmBeforeAddProperty={confirmBeforeAdd} + canRemoveProperty={() => Promise.resolve(true)} + /> ); }; diff --git a/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx b/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx index cf833f74a9..6afa7dc763 100644 --- a/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx +++ b/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx @@ -4,9 +4,10 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom'; import LeaseIcon from '@/assets/images/lease-icon.svg?react'; import { Claims, Roles } from '@/constants'; -import LeaseUpdatePropertySelector from '@/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector'; +import { LeaseFormModel } from '@/features/leases/models'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTypes_FileTypes'; +import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; import { exists, stripTrailingSlash } from '@/utils'; @@ -18,8 +19,10 @@ import MapSideBarLayout from '../layout/MapSideBarLayout'; import { InventoryTabNames } from '../property/InventoryTabs'; import FilePropertyRouter from '../router/FilePropertyRouter'; import FileMenuView from '../shared/FileMenuView'; +import { FileForm, PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; +import UpdatePropertiesContainer from '../shared/update/properties/UpdatePropertiesContainer'; import LeaseHeader from './common/LeaseHeader'; import { LeaseContainerState } from './LeaseContainer'; import LeaseGenerateContainer from './LeaseGenerateContainer'; @@ -42,6 +45,10 @@ export interface ILeaseViewProps { formikRef: React.RefObject>; isFormValid: boolean; lease?: ApiGen_Concepts_Lease; + setIsShowingPropertySelector: (isShowing: boolean) => void; + onUpdateProperties: (file: FileForm) => Promise; + confirmBeforeAddProperty: (propertyForm: PropertyForm) => Promise; + canRemoveProperty: (propertyId: number) => Promise; } export const LeaseView: React.FunctionComponent = ({ @@ -61,6 +68,10 @@ export const LeaseView: React.FunctionComponent = ({ formikRef, isFormValid, lease, + setIsShowingPropertySelector, + onUpdateProperties, + confirmBeforeAddProperty, + canRemoveProperty, }) => { // match for the current route const currentRoute = useRouteMatch(); @@ -74,7 +85,29 @@ export const LeaseView: React.FunctionComponent = ({ return ( - {exists(lease) && } + {exists(lease) && ( + +

+ You have selected a property not previously in the inventory. Do you want to add + this property to the lease? +

+

Do you want to acknowledge and proceed?

+ + } + canRemove={canRemoveProperty} + canUploadShapefiles={false} + canReposition={true} + formikRef={formikRef} + showArea={true} + /> + )}
{ await act(async () => { const model = new ManagementFormModel(); - model.fileProperties = selectedFeatures?.map(sf => PropertyForm.fromLocationFeatureDataset(sf)); + model.properties = selectedFeatures?.map(sf => PropertyForm.fromLocationFeatureDataset(sf)); await viewProps?.onSubmit(model, { setSubmitting: vi.fn(), resetForm: vi.fn(), diff --git a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx index 2817dbbb95..323b8dd2c2 100644 --- a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx @@ -56,10 +56,7 @@ const AddManagementContainer: React.FC = ({ const mapMachine = useMapStateMachine(); - const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer( - formikRef, - 'fileProperties', - ); + const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); const initialForm = useMemo(() => { const managementForm = new ManagementFormModel(); diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx index 6b96d32918..8dd75e5064 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx @@ -110,7 +110,7 @@ const ManagementForm: React.FC = props => { verifyCallback()} needsConfirmationBeforeAdd={confirmBeforeAdd} - properties={formikProps.values.fileProperties} + properties={formikProps.values.properties} />
diff --git a/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts b/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts index e966a0bf10..34f68ec2f8 100644 --- a/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts +++ b/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts @@ -19,7 +19,7 @@ export class ManagementFormModel implements WithManagementTeam { productId: string | null = null; fundingTypeCode: string | null = null; purposeTypeCode: string | null = null; - fileProperties: PropertyForm[] = []; + properties: PropertyForm[] = []; team: ManagementTeamSubFormModel[] = []; constructor( @@ -34,7 +34,7 @@ export class ManagementFormModel implements WithManagementTeam { } toApi(): ApiGen_Concepts_ManagementFile { - const fileProperties = this.fileProperties.map(x => this.toPropertyApi(x)); + const fileProperties = this.properties.map(x => this.toPropertyApi(x)); const sortedProperties = applyDisplayOrder(fileProperties); return { id: this.id ?? 0, @@ -87,7 +87,7 @@ export class ManagementFormModel implements WithManagementTeam { managementForm.fileName = model.fileName ?? ''; managementForm.team = model.managementTeam?.map(x => ManagementTeamSubFormModel.fromApi(x)) || []; - managementForm.fileProperties = model.fileProperties?.map(x => PropertyForm.fromApi(x)) || []; + managementForm.properties = model.fileProperties?.map(x => PropertyForm.fromApi(x)) || []; return managementForm; } diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx index e2e6790e7c..67e2cc2b7d 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx @@ -1,5 +1,5 @@ -import { FieldArray } from 'formik'; -import { useCallback, useState } from 'react'; +import { FieldArray, useFormikContext } from 'formik'; +import { Fragment, useCallback, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; import { toast } from 'react-toastify'; import styled from 'styled-components'; @@ -9,12 +9,14 @@ import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineCo import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { Section } from '@/components/common/Section/Section'; import { ZoomIconType, ZoomToLocation } from '@/components/maps/ZoomToLocation'; +import AreaContainer from '@/components/measurements/AreaContainer'; import MapClickMonitor from '@/components/propertySelector/MapClickMonitor'; import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; import { UploadResponseModel } from '@/features/properties/shapeUpload/models'; import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; import { exists, isEmptyOrNull, isLatLngInFeatureSetBoundary, isNumber } from '@/utils'; +import { withNameSpace } from '@/utils/formUtils'; import { PropertyForm } from '../../models'; import AddPropertiesGuide from './AddPropertiesGuide'; @@ -27,11 +29,13 @@ export interface IPropertiesListContainerProps { showDisabledProperties?: boolean; canUploadShapefiles?: boolean; canReposition?: boolean; + showArea?: boolean; } export const PropertiesListContainer: React.FunctionComponent< IPropertiesListContainerProps > = props => { + const { setFieldValue } = useFormikContext(); const [repositionPropertyIndex, setRepositionPropertyIndex] = useState(null); useDraftMarkerSynchronizer(props.properties); @@ -90,7 +94,6 @@ export const PropertiesListContainer: React.FunctionComponent< repositionPropertyIndex >= 0 && !hasMultipleProperties ) { - debugger; // As long as the marker is repositioned within the boundary of the originally selected property simply reposition the marker without further notification. const formProperty = props.properties[repositionPropertyIndex]; @@ -118,34 +121,57 @@ export const PropertiesListContainer: React.FunctionComponent< }} /> - {props.properties?.map((property, index) => ( - { - const removeCallback = () => { - remove(index); - }; - await props.verifyCanRemove(property.apiId, removeCallback); - }} - canReposition={props.canReposition} - onReposition={onRepositionClick} - nameSpace={`properties.${index}`} - index={index} - property={property} - showDisable={props.showDisabledProperties} - canUploadShapefile={props.canUploadShapefiles} - onUploadShapefile={(result: UploadResponseModel | null) => { - // Update the property boundary based on the uploaded shapefile - if (exists(result)) { - if (result.isSuccess && exists(result.boundary)) { - const updatedFormProperty = new PropertyForm(property); - updatedFormProperty.fileBoundary = result.boundary; - replace(index, updatedFormProperty); - } - } - }} - /> - ))} + {props.properties?.map((property, index) => { + const namespace = `properties.${index}`; + + return ( + + { + const removeCallback = () => { + remove(index); + }; + await props.verifyCanRemove(property?.apiId, removeCallback); + }} + canReposition={props.canReposition} + onReposition={onRepositionClick} + nameSpace={namespace} + index={index} + property={property} + showDisable={props.showDisabledProperties} + canUploadShapefile={props.canUploadShapefiles} + onUploadShapefile={(result: UploadResponseModel | null) => { + // Update the property boundary based on the uploaded shapefile + if (exists(result)) { + if (result.isSuccess && exists(result.boundary)) { + const updatedFormProperty = new PropertyForm(property); + updatedFormProperty.fileBoundary = result.boundary; + replace(index, updatedFormProperty); + } + } + }} + /> + {props.showArea && ( + + + { + setFieldValue(withNameSpace(namespace, 'landArea'), landArea); + setFieldValue(withNameSpace(namespace, 'areaUnit'), areaUnitTypeCode); + }} + /> + + + )} + + ); + })} {props.properties?.length === 0 && No Properties selected}
)} diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx index 4c05710088..3e20d42a52 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx @@ -1,6 +1,6 @@ import axios, { AxiosError } from 'axios'; import { Formik, FormikProps } from 'formik'; -import { useContext, useRef, useState } from 'react'; +import { useCallback, useContext, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import GenericModal from '@/components/common/GenericModal'; @@ -19,20 +19,21 @@ import PropertiesListContainer from './PropertiesListContainer'; import { UpdatePropertiesYupSchema } from './UpdatePropertiesYupSchema'; export interface IUpdatePropertiesContainerProps { - file: ApiGen_Concepts_File; + formFile: FileForm; setIsShowingPropertySelector: (isShowing: boolean) => void; onSuccess: (updateProperties?: boolean, updateFile?: boolean) => void; updateFileProperties: ( - file: ApiGen_Concepts_File, + file: FileForm, userOverrideCodes: UserOverrideCode[], ) => Promise; canRemove: (propertyId: number) => Promise; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; - confirmBeforeAddMessage?: React.ReactNode; + canAdd: (propertyForm: PropertyForm) => Promise; + confirmAddMessage: React.ReactNode; formikRef?: React.RefObject>; showDisabledProperties?: boolean; canUploadShapefiles?: boolean; canReposition?: boolean; + showArea?: boolean; } export const UpdatePropertiesContainer: React.FunctionComponent< @@ -40,16 +41,49 @@ export const UpdatePropertiesContainer: React.FunctionComponent< > = props => { const localRef = useRef>(null); const formikRef = props.formikRef ? props.formikRef : localRef; - const formFile = FileForm.fromApi(props.file); + const canAdd = props.canAdd; + const confirmAddMessage = props.confirmAddMessage; const [showSaveConfirmModal, setShowSaveConfirmModal] = useState(false); const [showAssociatedEntityWarning, setShowAssociatedEntityWarning] = useState(false); const [isValid, setIsValid] = useState(true); - const { setModalContent, setDisplayModal } = useModalContext(); + const { setModalContent, setDisplayModal, modalProps } = useModalContext(); const { resetFilePropertyLocations } = useContext(SideBarContext); - // Sets the state of the Map FSM to allow to edit properties - const { isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); + // Require user confirmation before adding a property to file + const confirmBeforeAdd = useCallback( + async (newPropertyForms: PropertyForm[], isValidCallback: (isValid: boolean) => void) => { + const needsConfirmation = await Promise.all( + newPropertyForms.map(formProperty => canAdd(formProperty)), + ); + debugger; + if (needsConfirmation.some(x => x === true) && !modalProps.display) { + setModalContent({ + variant: 'warning', + title: 'User Override Required', + message: confirmAddMessage, + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: () => { + // allow the property to be added to the file being created + isValidCallback(true); + setDisplayModal(false); + }, + handleCancel: () => { + // clear out the properties array as the user did not agree to the popup + isValidCallback(false); + setDisplayModal(false); + }, + }); + setDisplayModal(true); + } else { + isValidCallback(true); + } + }, + [modalProps.display, canAdd, setModalContent, confirmAddMessage, setDisplayModal], + ); + + const { isLoading } = usePropertyFormSyncronizer(formikRef, 'properties', confirmBeforeAdd); const handleSaveClick = async () => { await formikRef?.current?.validateForm(); @@ -96,13 +130,13 @@ export const UpdatePropertiesContainer: React.FunctionComponent< props.setIsShowingPropertySelector(false); }; - const saveFile = async (file: ApiGen_Concepts_File) => { + const saveFile = async (fileForm: FileForm) => { try { - const response = await props.updateFileProperties(file, []); + const response = await props.updateFileProperties(fileForm, []); formikRef?.current?.setSubmitting(false); if (isValidId(response?.id)) { - if (file.fileProperties?.find(fp => !fp.property?.address && !fp.property?.id)) { + if (response.fileProperties?.find(fp => !fp.property?.address && !fp.property?.id)) { toast.warn( 'Address could not be retrieved for this property, it will have to be provided manually in property details tab', { autoClose: 15000 }, @@ -144,25 +178,22 @@ export const UpdatePropertiesContainer: React.FunctionComponent< > innerRef={formikRef} - initialValues={formFile} + initialValues={props.formFile} validationSchema={UpdatePropertiesYupSchema} onSubmit={async (values: FileForm) => { - const file: ApiGen_Concepts_File = values.toApi(); - await saveFile(file); + await saveFile(values); }} > {formikProps => ( - <> - Promise.resolve(true)} - canUploadShapefiles={props.canUploadShapefiles} - canReposition={props.canReposition} - showDisabledProperties={props.showDisabledProperties} - /> - {formikProps.values.properties.length} - + Promise.resolve(true)} + canUploadShapefiles={props.canUploadShapefiles} + canReposition={props.canReposition} + showDisabledProperties={props.showDisabledProperties} + showArea={props.showArea} + /> )}
diff --git a/source/frontend/src/hooks/usePropertyFormSyncronizer.ts b/source/frontend/src/hooks/usePropertyFormSyncronizer.ts index b0fb9b4427..ae7e22624b 100644 --- a/source/frontend/src/hooks/usePropertyFormSyncronizer.ts +++ b/source/frontend/src/hooks/usePropertyFormSyncronizer.ts @@ -1,11 +1,11 @@ import { FormikProps, getIn } from 'formik'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { PropertyForm } from '@/features/mapSideBar/shared/models'; -import { exists } from '@/utils'; +import { exists, isEmptyOrNull } from '@/utils'; import { arePropertyFormsEqual } from '@/utils/mapPropertyUtils'; import { useEditPropertiesMode } from './useEditPropertiesMode'; @@ -18,6 +18,10 @@ import { useLocationFeatureDatasetsWithAddresses } from './useLocationFeatureDat export function usePropertyFormSyncronizer( formikRef: React.RefObject>, fieldName: keyof T, + validateNewProperties: ( + newProperties: PropertyForm[], + validateCallback: (isValid: boolean) => void, + ) => void, overrideFeatures?: LocationFeatureDataset[], ) { const { @@ -30,10 +34,29 @@ export function usePropertyFormSyncronizer( const { locationFeaturesWithAddresses: featuresWithAddresses, bcaLoading } = useLocationFeatureDatasetsWithAddresses(overrideFeatures ?? locationFeaturesForAddition); + const [pendingConfirmation, setPendingConfirmation] = useState(null); + // if we are listening to property add notifications we must tell the state machine we are in edit mode. useEditPropertiesMode(); - // This effect is used to update the file properties when "add to open file" is clicked in the worklist. + const validationCallback = useCallback( + (isValid: boolean) => { + debugger; + if (isValid && !isEmptyOrNull(pendingConfirmation)) { + const existingProperties = getIn(formikRef?.current?.values, fieldName as string) ?? []; + formikRef.current?.setFieldValue(fieldName as string, [ + ...existingProperties, + ...pendingConfirmation, + ]); + formikRef.current?.setFieldTouched(fieldName as string, true); + toast.success(`Added ${pendingConfirmation.length} new property(s) to the file.`); + } + setPendingConfirmation(null); + }, + [fieldName, formikRef, pendingConfirmation], + ); + + // This effect willbe called whenever there are new locations pending addition. useEffect(() => { if ( exists(formikRef?.current) && @@ -58,16 +81,11 @@ export function usePropertyFormSyncronizer( const duplicatesSkipped = newPropertyForms.length - uniqueNewProperties.length; - // If there are unique properties, add them to the formik values + // If there are unique properties request a confirmation if (uniqueNewProperties.length > 0) { - formikRef.current?.setFieldValue(fieldName as string, [ - ...existingProperties, - ...uniqueNewProperties, - ]); - formikRef.current?.setFieldTouched(fieldName as string, true); - toast.success(`Added ${uniqueNewProperties.length} new property(s) to the file.`); + setPendingConfirmation(uniqueNewProperties); + validateNewProperties(uniqueNewProperties, validationCallback); } - if (duplicatesSkipped > 0) { toast.warn(`Skipped ${duplicatesSkipped} duplicate property(s).`); } @@ -79,6 +97,8 @@ export function usePropertyFormSyncronizer( formikRef, pendingLocationFeaturesAddition, processLocationFeaturesAddition, + validateNewProperties, + validationCallback, ]); return { featuresWithAddresses, isLoading: bcaLoading }; From 49e72781b910ac7ebe77cddbbed3c5f3d3fb8c41 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Mon, 8 Dec 2025 00:27:47 -0800 Subject: [PATCH 3/5] Updated Lease Acq Disp Sub Conso Research to work with new models --- .../src/components/maps/ZoomToLocation.tsx | 4 +- .../map/PropertyMapSelectorFormView.tsx | 11 +- .../features/leases/add/AddLeaseContainer.tsx | 86 ++++++------- .../src/features/leases/add/AddLeaseForm.tsx | 4 - .../add/AddAcquisitionContainer.tsx | 32 ++--- .../acquisition/add/AddAcquisitionForm.tsx | 7 - .../AddConsolidationContainer.tsx | 6 +- .../consolidation/AddConsolidationView.tsx | 6 +- .../disposition/DispositionContainer.tsx | 8 +- .../disposition/DispositionView.tsx | 9 +- .../add/AddDispositionContainer.tsx | 121 ++++++++---------- .../add/AddDispositionContainerView.tsx | 4 - .../disposition/form/DispositionForm.tsx | 7 +- .../models/DispositionFormModel.ts | 10 +- .../mapSideBar/lease/LeaseContainer.tsx | 7 +- .../features/mapSideBar/lease/LeaseView.tsx | 14 +- .../management/ManagementContainer.tsx | 8 +- .../mapSideBar/management/ManagementView.tsx | 19 ++- .../management/add/AddManagementContainer.tsx | 112 +++++++--------- .../add/AddManagementContainerView.tsx | 4 - .../management/form/ManagementForm.tsx | 5 +- .../management/models/ManagementFormModel.ts | 4 +- .../mapSideBar/research/ResearchContainer.tsx | 10 +- .../mapSideBar/research/ResearchView.tsx | 9 +- .../research/add/AddResearchContainer.tsx | 109 +++++++--------- .../research/add/AddResearchForm.tsx | 9 +- .../mapSideBar/research/add/models.ts | 4 +- .../src/features/mapSideBar/shared/models.ts | 6 +- .../properties/PropertiesListContainer.tsx | 1 - .../properties/UpdatePropertiesContainer.tsx | 23 ++-- .../subdivision/AddSubdivisionContainer.tsx | 6 +- .../subdivision/AddSubdivisionView.tsx | 5 +- .../src/hooks/usePropertyFormSyncronizer.ts | 33 ++--- 33 files changed, 306 insertions(+), 397 deletions(-) diff --git a/source/frontend/src/components/maps/ZoomToLocation.tsx b/source/frontend/src/components/maps/ZoomToLocation.tsx index d0f93965bb..2eaefd8277 100644 --- a/source/frontend/src/components/maps/ZoomToLocation.tsx +++ b/source/frontend/src/components/maps/ZoomToLocation.tsx @@ -52,7 +52,9 @@ export const ZoomToLocation: React.FunctionComponent = ({ const bounds: LatLngBounds | null = useMemo(() => { const propertyLocations: Geometry[] = - formProperties?.map(p => p?.polygon ?? latLngLiteralToGeometry(p?.fileLocation)) || []; + formProperties?.map( + p => p?.polygon ?? latLngLiteralToGeometry({ lat: p?.latitude, lng: p?.longitude }), + ) || []; if (exists(geometry)) { propertyLocations.push(geometry); diff --git a/source/frontend/src/components/propertySelector/map/PropertyMapSelectorFormView.tsx b/source/frontend/src/components/propertySelector/map/PropertyMapSelectorFormView.tsx index 4e389c1484..22b364ef8e 100644 --- a/source/frontend/src/components/propertySelector/map/PropertyMapSelectorFormView.tsx +++ b/source/frontend/src/components/propertySelector/map/PropertyMapSelectorFormView.tsx @@ -29,7 +29,16 @@ const PropertyMapSelectorFormView: React.FunctionComponent - + { + if (mapMachine.isSelecting) { + onNewLocation(locationDataset, hasMultipleProperties); + } + }} + /> ); }; diff --git a/source/frontend/src/features/leases/add/AddLeaseContainer.tsx b/source/frontend/src/features/leases/add/AddLeaseContainer.tsx index c7fb88facf..286ee34ddb 100644 --- a/source/frontend/src/features/leases/add/AddLeaseContainer.tsx +++ b/source/frontend/src/features/leases/add/AddLeaseContainer.tsx @@ -1,6 +1,6 @@ import { AxiosError } from 'axios'; import { FormikHelpers, FormikProps } from 'formik'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; @@ -51,61 +51,48 @@ export const AddLeaseContainer: React.FunctionComponent< const hasWarnedRef = useRef(false); // Get PropertyForms with addresses for all selected features - const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); const initialForm = getDefaultFormLease(); - const confirmBeforeAdd = useCallback( - async (propertyForm: PropertyForm) => !isValidId(propertyForm?.apiId), - [], - ); + const confirmProperty = async (propertyForm: PropertyForm) => !isValidId(propertyForm?.apiId); // Require user confirmation before adding non-inventory properties to a lease. - useEffect(() => { - const runAsync = async () => { - const incomingProperties = - featuresWithAddresses?.map(f => PropertyForm.fromLocationFeatureDataset(f.feature)) ?? []; - - if (exists(incomingProperties) && exists(formikRef.current) && !hasWarnedRef.current) { - // Check all properties for confirmation - const needsConfirmation = incomingProperties.some( - async feature => await confirmBeforeAdd(feature), - ); - if (needsConfirmation) { - hasWarnedRef.current = true; // mark as shown - - setModalContent({ - variant: 'info', - title: 'Not inventory property', - message: - 'You have selected a property not previously in the inventory. Do you want to add this property to the lease?', - okButtonText: 'Add', - cancelButtonText: 'Cancel', - handleOk: () => { - // allow the PIMS properties to be added to the lease being created - setDisplayModal(false); - formikRef.current?.setFieldValue('properties', initialForm.properties); - }, - handleCancel: () => { - // clear out the properties array as the user did not agree to the popup - initialForm.properties.splice(0, initialForm.properties.length); - formikRef.current?.setFieldValue('properties', []); - setDisplayModal(false); - }, - }); - setDisplayModal(true); - } + const confirmBeforeAdd = useCallback( + async ( + newProperties: PropertyForm[], + isValidCallback: (isValid: boolean, newProperties: PropertyForm[]) => void, + ) => { + // Check all properties for confirmation + const needsConfirmation = newProperties.some(async feature => await confirmProperty(feature)); + if (needsConfirmation && exists(formikRef.current) && !hasWarnedRef.current) { + hasWarnedRef.current = true; // mark as shown + setModalContent({ + variant: 'info', + title: 'Not inventory property', + message: + 'You have selected a property not previously in the inventory. Do you want to add this property to the lease?', + okButtonText: 'Add', + cancelButtonText: 'Cancel', + handleOk: () => { + // allow the PIMS properties to be added to the lease being created + setDisplayModal(false); + isValidCallback(true, newProperties); + }, + handleCancel: () => { + // clear out the properties array as the user did not agree to the popup + setDisplayModal(false); + isValidCallback(false, []); + }, + }); + setDisplayModal(true); + } else { + isValidCallback(true, newProperties); } - }; + }, + [setDisplayModal, setModalContent], + ); - runAsync(); - }, [ - confirmBeforeAdd, - featuresWithAddresses, - initialForm.properties, - setDisplayModal, - setModalContent, - ]); + const { isLoading } = usePropertyFormSyncronizer(formikRef, confirmBeforeAdd); const saveLeaseFile = async ( leaseFormModel: LeaseFormModel, @@ -194,7 +181,6 @@ export const AddLeaseContainer: React.FunctionComponent< } formikRef={formikRef} initialValues={initialForm} - confirmBeforeAdd={confirmBeforeAdd} />
diff --git a/source/frontend/src/features/leases/add/AddLeaseForm.tsx b/source/frontend/src/features/leases/add/AddLeaseForm.tsx index 382b64b24e..6ea27e08bb 100644 --- a/source/frontend/src/features/leases/add/AddLeaseForm.tsx +++ b/source/frontend/src/features/leases/add/AddLeaseForm.tsx @@ -2,7 +2,6 @@ import { Formik, FormikHelpers, FormikProps } from 'formik'; import styled from 'styled-components'; import { Section } from '@/components/common/Section/Section'; -import { PropertyForm } from '@/features/mapSideBar/shared/models'; import PropertiesListContainer from '@/features/mapSideBar/shared/update/properties/PropertiesListContainer'; import { LeaseFormModel } from '../models'; @@ -22,14 +21,12 @@ export interface IAddLeaseFormProps { formikRef: React.Ref>; /** Initial values of the form */ initialValues: LeaseFormModel; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; } const AddLeaseForm: React.FunctionComponent> = ({ onSubmit, formikRef, initialValues, - confirmBeforeAdd, }) => { return ( @@ -51,7 +48,6 @@ const AddLeaseForm: React.FunctionComponent callback()} - needsConfirmationBeforeAdd={confirmBeforeAdd} showArea /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx index 59e2a8dd4b..6d93349a15 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionContainer.tsx @@ -41,13 +41,15 @@ export const AddAcquisitionContainer: React.FC = const { setModalContent, setDisplayModal } = useModalContext(); const { execute: getPropertyAssociations } = usePropertyAssociations(); - const [showFirstTimeConfirmation, setShowFirstTimeConfirmation] = useState(true); + const [needsFirstTimeConfirmation, setNeedsFirstTimeConfirmation] = useState(true); const { getAcquisitionFile: { execute: getAcquisitionFile, response: parentAcquisitionFile }, addAcquisitionFile: { execute: addAcquisitionFile, loading: addAcquisitionFileLoading }, } = useAcquisitionProvider(); + const mapMachine = useMapStateMachine(); + // Check for parent acquisition file id for sub-files const params = useQuery(); const parentId = params.get('parentId'); @@ -63,8 +65,6 @@ export const AddAcquisitionContainer: React.FC = fetchParentFile(); }, [getAcquisitionFile, parentAcquisitionFile, parentId]); - const mapMachine = useMapStateMachine(); - //Verifies that the property does not belong to another acquisition file already const confirmProperty = useCallback( async (propertyForm: PropertyForm) => { @@ -82,13 +82,17 @@ export const AddAcquisitionContainer: React.FC = ); // Require user confirmation before adding a property to file - // This is the flow for Map Marker -> right-click -> create Acquisition File const confirmBeforeAdd = useCallback( - async (newPropertyForms: PropertyForm[], isValidCallback: (isValid: boolean) => void) => { + async ( + newPropertyForms: PropertyForm[], + isValidCallback: (isValid: boolean, newProperties: PropertyForm[]) => void, + ) => { const needsConfirmation = await Promise.all( newPropertyForms.map(formProperty => confirmProperty(formProperty)), ); - if (showFirstTimeConfirmation && needsConfirmation.some(x => x === true)) { + if (needsFirstTimeConfirmation && needsConfirmation.some(x => x === true)) { + // show the user confirmation modal only once when creating a file + setNeedsFirstTimeConfirmation(false); setModalContent({ variant: 'warning', title: 'User Override Required', @@ -104,30 +108,24 @@ export const AddAcquisitionContainer: React.FC = cancelButtonText: 'No', handleOk: () => { // allow the property to be added to the file being created - isValidCallback(true); + isValidCallback(true, newPropertyForms); setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setShowFirstTimeConfirmation(false); }, handleCancel: () => { - // clear out the properties array as the user did not agree to the popup - isValidCallback(false); + isValidCallback(false, []); setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setShowFirstTimeConfirmation(false); }, }); setDisplayModal(true); } else { - isValidCallback(true); + isValidCallback(true, newPropertyForms); } }, - [confirmProperty, showFirstTimeConfirmation, setDisplayModal, setModalContent], + [confirmProperty, needsFirstTimeConfirmation, setDisplayModal, setModalContent], ); const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer( formikRef, - 'properties', confirmBeforeAdd, ); @@ -211,7 +209,6 @@ export const AddAcquisitionContainer: React.FC = handleSuccess(response); } } finally { - mapMachine.processLocationFeaturesAddition(); formikHelpers?.setSubmitting(false); } }; @@ -257,7 +254,6 @@ export const AddAcquisitionContainer: React.FC = ); }} validationSchema={AddAcquisitionFileYupSchema} - confirmBeforeAdd={confirmProperty} /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionForm.tsx index 296797c27c..00642b0e67 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionForm.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/add/AddAcquisitionForm.tsx @@ -30,7 +30,6 @@ import { UserOverrideCode } from '@/models/api/UserOverrideCode'; import { exists, isValidId, isValidString } from '@/utils'; import { formatApiPersonNames } from '@/utils/personUtils'; -import { PropertyForm } from '../../shared/models'; import PropertiesListContainer from '../../shared/update/properties/PropertiesListContainer'; import { TeamMemberFormModal } from '../common/modals/AcquisitionFormModal'; import UpdateAcquisitionOwnersSubForm from '../common/update/acquisitionOwners/UpdateAcquisitionOwnersSubForm'; @@ -53,7 +52,6 @@ export interface IAddAcquisitionFormProps { formikHelpers: FormikHelpers, userOverrides: UserOverrideCode[], ) => void | Promise; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; } export const AddAcquisitionForm: React.FunctionComponent = ({ @@ -61,7 +59,6 @@ export const AddAcquisitionForm: React.FunctionComponent { const [showDiffMinistryRegionModal, setShowDiffMinistryRegionModal] = @@ -102,7 +99,6 @@ export const AddAcquisitionForm: React.FunctionComponent )} @@ -119,14 +115,12 @@ const AddAcquisitionDetailSubForm: React.FC<{ ) => void | Promise; showDiffMinistryRegionModal: boolean; setShowDiffMinistryRegionModal: React.Dispatch>; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; }> = ({ parentId, formikProps, onSubmit, showDiffMinistryRegionModal, setShowDiffMinistryRegionModal, - confirmBeforeAdd, }) => { const [projectProducts, setProjectProducts] = React.useState< ApiGen_Concepts_Product[] | undefined @@ -335,7 +329,6 @@ const AddAcquisitionDetailSubForm: React.FC<{ verifyCallback()} - needsConfirmationBeforeAdd={confirmBeforeAdd} canUploadShapefiles={true} /> diff --git a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx index 0414790db5..03cb597a7c 100644 --- a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx +++ b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationContainer.tsx @@ -39,6 +39,7 @@ const AddConsolidationContainer: React.FC = ({ const selectedFeatureDataset = mapMachine.mapLocationFeatureDataset; const { setModalContent, setDisplayModal } = useModalContext(); const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); + const [isFirstLoad, setIsFirstLoad] = useState(true); const { addPropertyOperationApi: { execute: addPropertyOperation, loading }, @@ -60,7 +61,7 @@ const AddConsolidationContainer: React.FC = ({ loadInitialProperty(); async function loadInitialProperty() { - if (selectedFeatureDataset !== null) { + if (selectedFeatureDataset !== null && isFirstLoad) { const propertyForm = PropertyForm.fromLocationFeatureDataset(selectedFeatureDataset); if (isValidString(propertyForm.pid)) { const pimsFeature = firstOrNull(selectedFeatureDataset.pimsFeatures); @@ -74,8 +75,9 @@ const AddConsolidationContainer: React.FC = ({ setInitialForm(consolidationFormModel); } } + setIsFirstLoad(false); } - }, [selectedFeatureDataset, getAddress]); + }, [selectedFeatureDataset, getAddress, isFirstLoad]); useEffect(() => { if (exists(initialForm) && exists(formikRef.current)) { diff --git a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx index ceb34f6e90..1f9cd84247 100644 --- a/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx +++ b/source/frontend/src/features/mapSideBar/consolidation/AddConsolidationView.tsx @@ -124,7 +124,9 @@ const AddConsolidationView: React.FunctionComponent< push(selectedProperty)} + setSelectProperty={selectedProperty => + push(PropertyForm.fromPropertyApi(selectedProperty)) + } PropertySelectorPidSearchView={PropertySearchSelectorPidFormView} /> @@ -137,7 +139,7 @@ const AddConsolidationView: React.FunctionComponent< onRemove={() => remove(index)} nameSpace={`sourceProperties.${index}`} getMarkerIndex={property => getDraftMarkerIndex(property, values)} - key={`destination-property-${property.pid}-${property.latitude}-${property.longitude}`} + key={`source-property-${property.pid}-${property.latitude}-${property.longitude}`} /> ))} {errors.sourceProperties && ( diff --git a/source/frontend/src/features/mapSideBar/disposition/DispositionContainer.tsx b/source/frontend/src/features/mapSideBar/disposition/DispositionContainer.tsx index 3b0242df7c..4f72e9e1da 100644 --- a/source/frontend/src/features/mapSideBar/disposition/DispositionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/DispositionContainer.tsx @@ -12,7 +12,6 @@ import useApiUserOverride from '@/hooks/useApiUserOverride'; import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTypes_FileTypes'; -import { ApiGen_Concepts_DispositionFile } from '@/models/api/generated/ApiGen_Concepts_DispositionFile'; import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; import { exists, isValidId, sortFileProperties, stripTrailingSlash } from '@/utils'; @@ -21,6 +20,7 @@ import { SideBarContext } from '../context/sidebarContext'; import { PropertyForm } from '../shared/models'; import usePathGenerator from '../shared/sidebarPathGenerator'; import { IDispositionViewProps } from './DispositionView'; +import { DispositionFormModel } from './models/DispositionFormModel'; export interface IDispositionContainerProps { dispositionFileId: number; @@ -245,7 +245,7 @@ export const DispositionContainer: React.FunctionComponent => { // The backend does not update the product or project so its safe to send nulls even if there might be data for those fields. return withUserOverride( @@ -253,7 +253,7 @@ export const DispositionContainer: React.FunctionComponent
+ /> ); }; diff --git a/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx b/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx index bc2414022f..51f85b7dd4 100644 --- a/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/DispositionView.tsx @@ -33,6 +33,7 @@ import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import UpdatePropertiesContainer from '../shared/update/properties/UpdatePropertiesContainer'; import { DispositionHeader } from './common/DispositionHeader'; +import { DispositionFormModel } from './models/DispositionFormModel'; import DispositionRouter from './router/DispositionRouter'; import DispositionStatusUpdateSolver from './tabs/fileDetails/detail/DispositionStatusUpdateSolver'; @@ -44,7 +45,7 @@ export interface IDispositionViewProps { onSelectProperty: (propertyId: number) => void; onEditProperties: () => void; onSuccess: (updateProperties?: boolean, updateFile?: boolean) => void; - onUpdateProperties: (file: ApiGen_Concepts_File) => Promise; + onUpdateProperties: (file: DispositionFormModel) => Promise; confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; canRemove: (propertyId: number) => Promise; isEditing: boolean; @@ -110,14 +111,14 @@ export const DispositionView: React.FunctionComponent = ( {dispositionFile && (

This property has already been added to one or more disposition files.

Do you want to acknowledge and proceed?

diff --git a/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx b/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx index e77098cca0..3ede006feb 100644 --- a/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainer.tsx @@ -1,6 +1,6 @@ import { AxiosError } from 'axios'; import { FormikHelpers, FormikProps } from 'formik'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { useDispositionProvider } from '@/hooks/repositories/useDispositionProvider'; @@ -33,14 +33,18 @@ const AddDispositionContainer: React.FC = ({ const { setModalContent, setDisplayModal } = useModalContext(); const { execute: getPropertyAssociations } = usePropertyAssociations(); - const [needsUserConfirmation, setNeedsUserConfirmation] = useState(true); + const [needsFirstTimeConfirmation, setNeedsFirstTimeConfirmation] = useState(true); const { addDispositionFileApi: { execute: addDispositionFileApi, loading }, } = useDispositionProvider(); + const mapMachine = useMapStateMachine(); + + const initialForm = new DispositionFormModel(); + // Warn user that property is part of an existing disposition file - const confirmBeforeAdd = useCallback( + const confirmProperty = useCallback( async (propertyForm: PropertyForm): Promise => { if (isValidId(propertyForm.apiId)) { const response = await getPropertyAssociations(propertyForm.apiId); @@ -55,10 +59,52 @@ const AddDispositionContainer: React.FC = ({ [getPropertyAssociations], ); - const mapMachine = useMapStateMachine(); + // Require user confirmation before adding a property to file + const confirmBeforeAdd = useCallback( + async ( + newPropertyForms: PropertyForm[], + isValidCallback: (isValid: boolean, newProperties: PropertyForm[]) => void, + ) => { + const needsConfirmation = await Promise.all( + newPropertyForms.map(formProperty => confirmProperty(formProperty)), + ); + if (needsFirstTimeConfirmation && needsConfirmation.some(x => x === true)) { + // show the user confirmation modal only once when creating a file + setNeedsFirstTimeConfirmation(false); + setModalContent({ + variant: 'warning', + title: 'User Override Required', + message: ( + <> +

+ One or more properties have already been added to one or more disposition files. +

+

Do you want to acknowledge and proceed?

+ + ), + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: () => { + // allow the property to be added to the file being created + isValidCallback(true, newPropertyForms); + setDisplayModal(false); + }, + handleCancel: () => { + isValidCallback(false, []); + setDisplayModal(false); + }, + }); + setDisplayModal(true); + } else { + isValidCallback(true, newPropertyForms); + } + }, + [confirmProperty, needsFirstTimeConfirmation, setDisplayModal, setModalContent], + ); + const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer( formikRef, - 'fileProperties', + confirmBeforeAdd, ); useEffect(() => { @@ -75,70 +121,6 @@ const AddDispositionContainer: React.FC = ({ } }, [featuresWithAddresses]); - const initialForm = useMemo(() => { - return new DispositionFormModel(); - }, []); - - // Require user confirmation before adding a property to file - // This is the flow for Map Marker -> right-click -> create Disposition File - useEffect(() => { - const runAsync = async () => { - const incomingProperties = - featuresWithAddresses?.map(f => PropertyForm.fromLocationFeatureDataset(f.feature)) ?? []; - if (exists(incomingProperties) && exists(formikRef.current) && needsUserConfirmation) { - if (incomingProperties.length > 0) { - // Check all properties for confirmation - const needsConfirmation = await Promise.all( - incomingProperties.map(formProperty => confirmBeforeAdd(formProperty)), - ); - if (needsConfirmation.some(confirm => confirm)) { - setModalContent({ - variant: 'warning', - title: 'User Override Required', - message: ( - <> -

- One or more properties have already been added to one or more disposition files. -

-

Do you want to acknowledge and proceed?

- - ), - okButtonText: 'Yes', - cancelButtonText: 'No', - handleOk: () => { - // allow the property to be added to the file being created - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', incomingProperties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - handleCancel: () => { - // clear out the properties array as the user did not agree to the popup - incomingProperties.splice(0, incomingProperties.length); - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', incomingProperties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - }); - setDisplayModal(true); - } - } - } - }; - - runAsync(); - }, [ - confirmBeforeAdd, - featuresWithAddresses, - initialForm, - needsUserConfirmation, - setDisplayModal, - setModalContent, - ]); - const handleCancel = useCallback(() => onClose(), [onClose]); const handleSave = async () => { @@ -187,7 +169,6 @@ const AddDispositionContainer: React.FC = ({ dispositionInitialValues={initialForm} loading={loading || isLoading} displayFormInvalid={!isFormValid} - confirmBeforeAdd={confirmBeforeAdd} onSave={handleSave} onCancel={handleCancel} onSubmit={( diff --git a/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainerView.tsx b/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainerView.tsx index ca2dc7337f..8c49b6fbc8 100644 --- a/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainerView.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/add/AddDispositionContainerView.tsx @@ -7,7 +7,6 @@ import ConfirmNavigation from '@/components/common/ConfirmNavigation'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import MapSideBarLayout from '../../layout/MapSideBarLayout'; -import { PropertyForm } from '../../shared/models'; import SidebarFooter from '../../shared/SidebarFooter'; import { StyledFormWrapper } from '../../shared/styles'; import DispositionForm from '../form/DispositionForm'; @@ -24,7 +23,6 @@ export interface IAddDispositionContainerViewProps { ) => void | Promise; onCancel: () => void; onSave: () => void; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; } const AddDispositionContainerView: React.FunctionComponent = ({ @@ -35,7 +33,6 @@ const AddDispositionContainerView: React.FunctionComponent { const history = useHistory(); @@ -64,7 +61,6 @@ const AddDispositionContainerView: React.FunctionComponent diff --git a/source/frontend/src/features/mapSideBar/disposition/form/DispositionForm.tsx b/source/frontend/src/features/mapSideBar/disposition/form/DispositionForm.tsx index 085311ed4b..77179cd152 100644 --- a/source/frontend/src/features/mapSideBar/disposition/form/DispositionForm.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/form/DispositionForm.tsx @@ -14,7 +14,6 @@ import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { IAutocompletePrediction } from '@/interfaces/IAutocomplete'; import { ApiGen_Concepts_Product } from '@/models/api/generated/ApiGen_Concepts_Product'; -import { PropertyForm } from '../../shared/models'; import PropertiesListContainer from '../../shared/update/properties/PropertiesListContainer'; import { AddDispositionFormYupSchema } from '../models/AddDispositionFormYupSchema'; import { DispositionFormModel } from '../models/DispositionFormModel'; @@ -27,11 +26,10 @@ export interface IDispositionFormProps { values: DispositionFormModel, formikHelpers: FormikHelpers, ) => void | Promise; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; } const DispositionForm: React.FC = props => { - const { initialValues, onSubmit, confirmBeforeAdd, formikRef } = props; + const { initialValues, onSubmit, formikRef } = props; const [projectProducts, setProjectProducts] = useState( undefined, @@ -126,9 +124,8 @@ const DispositionForm: React.FC = props => {
verifyCallback()} - needsConfirmationBeforeAdd={confirmBeforeAdd} />
diff --git a/source/frontend/src/features/mapSideBar/disposition/models/DispositionFormModel.ts b/source/frontend/src/features/mapSideBar/disposition/models/DispositionFormModel.ts index 7a3fa7f066..f2298792c9 100644 --- a/source/frontend/src/features/mapSideBar/disposition/models/DispositionFormModel.ts +++ b/source/frontend/src/features/mapSideBar/disposition/models/DispositionFormModel.ts @@ -6,14 +6,14 @@ import { applyDisplayOrder } from '@/utils'; import { emptyStringtoNullable, fromTypeCode, toTypeCodeNullable } from '@/utils/formUtils'; import { exists, isValidIsoDateTime } from '@/utils/utils'; -import { PropertyForm } from '../../shared/models'; +import { PropertyForm, WithFormProperties } from '../../shared/models'; import { ChecklistItemFormModel } from '../../shared/tabs/checklist/update/models'; import { DispositionOfferFormModel } from '../tabs/offersAndSale/dispositionOffer/models/DispositionOfferFormModel'; import { DispositionAppraisalFormModel } from './DispositionAppraisalFormModel'; import { DispositionSaleFormModel } from './DispositionSaleFormModel'; import { DispositionTeamSubFormModel, WithDispositionTeam } from './DispositionTeamSubFormModel'; -export class DispositionFormModel implements WithDispositionTeam { +export class DispositionFormModel implements WithDispositionTeam, WithFormProperties { fileName: string | null = ''; fileStatusTypeCode: string | null = null; referenceNumber: string | null = ''; @@ -31,7 +31,7 @@ export class DispositionFormModel implements WithDispositionTeam { initiatingDocumentTypeOther: string | null = ''; initiatingDocumentDate: string | null = null; regionCode: string | null = null; - fileProperties: PropertyForm[] = []; + properties: PropertyForm[] = []; team: DispositionTeamSubFormModel[] = []; offers: DispositionOfferFormModel[] = []; fileChecklist: ChecklistItemFormModel[] = []; @@ -52,7 +52,7 @@ export class DispositionFormModel implements WithDispositionTeam { } toApi(): ApiGen_Concepts_DispositionFile { - const fileProperties = this.fileProperties.map(x => this.toPropertyApi(x)); + const fileProperties = this.properties.map(x => this.toPropertyApi(x)); const sortedProperties = applyDisplayOrder(fileProperties); return { id: this.id ?? 0, @@ -135,7 +135,7 @@ export class DispositionFormModel implements WithDispositionTeam { dispositionForm.team = model.dispositionTeam?.map(x => DispositionTeamSubFormModel.fromApi(x)) || []; - dispositionForm.fileProperties = model.fileProperties?.map(x => PropertyForm.fromApi(x)) || []; + dispositionForm.properties = model.fileProperties?.map(x => PropertyForm.fromApi(x)) || []; return dispositionForm; } diff --git a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx index 1d7ba5312c..1358f41ca2 100644 --- a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx @@ -409,12 +409,7 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos history.push(`${match.url}`); }; - /* - useEffect(() => { - setIsPropertyEditing(query.get('edit') === 'true'); - }, [query, setIsPropertyEditing]); - */ - + // Properties that are not in PIMS need confirmation const confirmBeforeAdd = async (propertyForm: PropertyForm): Promise => { return !isValidId(propertyForm.apiId); }; diff --git a/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx b/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx index 6afa7dc763..a429936876 100644 --- a/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx +++ b/source/frontend/src/features/mapSideBar/lease/LeaseView.tsx @@ -91,8 +91,13 @@ export const LeaseView: React.FunctionComponent = ({ setIsShowingPropertySelector={setIsShowingPropertySelector} onSuccess={onPropertyUpdateSuccess} updateFileProperties={onUpdateProperties} - confirmBeforeAdd={confirmBeforeAddProperty} - confirmBeforeAddMessage={ + canRemove={canRemoveProperty} + canUploadShapefiles={false} + canReposition={true} + formikRef={formikRef} + showArea={true} + canAdd={confirmBeforeAddProperty} + confirmAddMessage={ <>

You have selected a property not previously in the inventory. Do you want to add @@ -101,11 +106,6 @@ export const LeaseView: React.FunctionComponent = ({

Do you want to acknowledge and proceed?

} - canRemove={canRemoveProperty} - canUploadShapefiles={false} - canReposition={true} - formikRef={formikRef} - showArea={true} /> )}
diff --git a/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx b/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx index 355650d1ad..471f2fc2d4 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx @@ -13,7 +13,6 @@ import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTypes_FileTypes'; import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; -import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; import { exists, isValidId, sortFileProperties, stripTrailingSlash } from '@/utils'; @@ -22,6 +21,7 @@ import { FileTabType } from '../shared/detail/FileTabs'; import { PropertyForm } from '../shared/models'; import usePathGenerator from '../shared/sidebarPathGenerator'; import { IManagementViewProps } from './ManagementView'; +import { ManagementFormModel } from './models/ManagementFormModel'; export interface IManagementContainerProps { managementFileId: number; @@ -241,7 +241,7 @@ export const ManagementContainer: React.FunctionComponent => { // The backend does not update the product or project so its safe to send nulls even if there might be data for those fields. return withUserOverride( @@ -249,7 +249,7 @@ export const ManagementContainer: React.FunctionComponent void; onEditProperties: () => void; onSuccess: (updateProperties?: boolean, updateFile?: boolean) => void; - onUpdateProperties: (file: ApiGen_Concepts_File) => Promise; + updateFileProperties: ( + file: WithFormProperties, + userOverrideCodes: UserOverrideCode[], + ) => Promise; confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; canRemove: (propertyId: number) => Promise; isEditing: boolean; @@ -63,7 +68,7 @@ export const ManagementView: React.FunctionComponent = ({ onSelectProperty, onEditProperties, onSuccess, - onUpdateProperties, + updateFileProperties, confirmBeforeAdd, canRemove, isEditing, @@ -110,15 +115,15 @@ export const ManagementView: React.FunctionComponent = ({ {managementFile && (

This property has already been added to one or more management files.

Do you want to acknowledge and proceed?

diff --git a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx index 323b8dd2c2..2c1ec506aa 100644 --- a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.tsx @@ -1,6 +1,6 @@ import { AxiosError } from 'axios'; import { FormikHelpers, FormikProps } from 'formik'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { useManagementFileRepository } from '@/hooks/repositories/useManagementFileRepository'; @@ -32,14 +32,16 @@ const AddManagementContainer: React.FC = ({ const formikRef = useRef>(null); const { setModalContent, setDisplayModal } = useModalContext(); const { execute: getPropertyAssociations } = usePropertyAssociations(); - const [needsUserConfirmation, setNeedsUserConfirmation] = useState(true); + const [needsFirstTimeConfirmation, setNeedsFirstTimeConfirmation] = useState(true); + const mapMachine = useMapStateMachine(); + const initialForm = new ManagementFormModel(); const { addManagementFileApi: { execute: addManagementFileApi, loading }, } = useManagementFileRepository(); // Warn user that property is part of an existing management file - const confirmBeforeAdd = useCallback( + const confirmProperty = useCallback( async (propertyForm: PropertyForm): Promise => { if (isValidId(propertyForm.apiId)) { const response = await getPropertyAssociations(propertyForm.apiId); @@ -54,73 +56,48 @@ const AddManagementContainer: React.FC = ({ [getPropertyAssociations], ); - const mapMachine = useMapStateMachine(); - - const { featuresWithAddresses, isLoading } = usePropertyFormSyncronizer(formikRef, 'properties'); - - const initialForm = useMemo(() => { - const managementForm = new ManagementFormModel(); - return managementForm; - }, []); - // Require user confirmation before adding a property to file - // This is the flow for Map Marker -> right-click -> create Management File - useEffect(() => { - const runAsync = async () => { - const incomingProperties = - featuresWithAddresses?.map(f => PropertyForm.fromLocationFeatureDataset(f.feature)) ?? []; - if (exists(incomingProperties) && exists(formikRef.current) && needsUserConfirmation) { - if (incomingProperties.length > 0) { - // Check all properties for confirmation - const needsConfirmation = await Promise.all( - incomingProperties.map(formProperty => confirmBeforeAdd(formProperty)), - ); - if (needsConfirmation.some(confirm => confirm)) { - setModalContent({ - variant: 'warning', - title: 'User Override Required', - message: ( - <> -

- One or more properties have already been added to one or more management files. -

-

Do you want to acknowledge and proceed?

- - ), - okButtonText: 'Yes', - cancelButtonText: 'No', - handleOk: () => { - // allow the property to be added to the file being created - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', incomingProperties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - handleCancel: () => { - // clear out the properties array as the user did not agree to the popup - incomingProperties.splice(0, incomingProperties.length); - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', incomingProperties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - }); - setDisplayModal(true); - } - } + const confirmBeforeAdd = useCallback( + async ( + newPropertyForms: PropertyForm[], + isValidCallback: (isValid: boolean, newProperties: PropertyForm[]) => void, + ) => { + const needsConfirmation = await Promise.all( + newPropertyForms.map(formProperty => confirmProperty(formProperty)), + ); + if (needsFirstTimeConfirmation && needsConfirmation.some(x => x === true)) { + // show the user confirmation modal only once when creating a file + setNeedsFirstTimeConfirmation(false); + setModalContent({ + variant: 'warning', + title: 'User Override Required', + message: ( + <> +

One or more properties have already been added to one or more management files.

+

Do you want to acknowledge and proceed?

+ + ), + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: () => { + // allow the property to be added to the file being created + isValidCallback(true, newPropertyForms); + setDisplayModal(false); + }, + handleCancel: () => { + isValidCallback(false, []); + setDisplayModal(false); + }, + }); + setDisplayModal(true); + } else { + isValidCallback(true, newPropertyForms); } - }; + }, + [confirmProperty, needsFirstTimeConfirmation, setDisplayModal, setModalContent], + ); - runAsync(); - }, [ - confirmBeforeAdd, - featuresWithAddresses, - needsUserConfirmation, - setDisplayModal, - setModalContent, - ]); + const { isLoading } = usePropertyFormSyncronizer(formikRef, confirmBeforeAdd); const handleCancel = useCallback(() => onClose(), [onClose]); @@ -170,7 +147,6 @@ const AddManagementContainer: React.FC = ({ managementInitialValues={initialForm} loading={loading || isLoading} displayFormInvalid={!isFormValid} - confirmBeforeAdd={confirmBeforeAdd} onSave={handleSave} onCancel={handleCancel} onSubmit={( diff --git a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.tsx b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.tsx index a8a7d1ebe3..278724cdc0 100644 --- a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.tsx +++ b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.tsx @@ -7,7 +7,6 @@ import ConfirmNavigation from '@/components/common/ConfirmNavigation'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import MapSideBarLayout from '../../layout/MapSideBarLayout'; -import { PropertyForm } from '../../shared/models'; import SidebarFooter from '../../shared/SidebarFooter'; import { StyledFormWrapper } from '../../shared/styles'; import ManagementForm from '../form/ManagementForm'; @@ -24,7 +23,6 @@ export interface IAddManagementContainerViewProps { ) => void | Promise; onCancel: () => void; onSave: () => void; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; } const AddManagementContainerView: React.FunctionComponent = ({ @@ -35,7 +33,6 @@ const AddManagementContainerView: React.FunctionComponent { const history = useHistory(); @@ -64,7 +61,6 @@ const AddManagementContainerView: React.FunctionComponent diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx index 8dd75e5064..2de90924cf 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx @@ -12,7 +12,6 @@ import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { IAutocompletePrediction } from '@/interfaces/IAutocomplete'; import { ApiGen_Concepts_Product } from '@/models/api/generated/ApiGen_Concepts_Product'; -import { PropertyForm } from '../../shared/models'; import PropertiesListContainer from '../../shared/update/properties/PropertiesListContainer'; import { AddManagementFormYupSchema } from '../models/AddManagementFormYupSchema'; import { ManagementFormModel } from '../models/ManagementFormModel'; @@ -25,11 +24,10 @@ export interface IManagementFormProps { values: ManagementFormModel, formikHelpers: FormikHelpers, ) => void | Promise; - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; } const ManagementForm: React.FC = props => { - const { initialValues, onSubmit, confirmBeforeAdd, formikRef } = props; + const { initialValues, onSubmit, formikRef } = props; const [projectProducts, setProjectProducts] = useState( undefined, @@ -109,7 +107,6 @@ const ManagementForm: React.FC = props => {
verifyCallback()} - needsConfirmationBeforeAdd={confirmBeforeAdd} properties={formikProps.values.properties} />
diff --git a/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts b/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts index 34f68ec2f8..c6b9593a19 100644 --- a/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts +++ b/source/frontend/src/features/mapSideBar/management/models/ManagementFormModel.ts @@ -6,10 +6,10 @@ import { applyDisplayOrder } from '@/utils'; import { fromTypeCode, toTypeCodeNullable } from '@/utils/formUtils'; import { exists } from '@/utils/utils'; -import { PropertyForm } from '../../shared/models'; +import { PropertyForm, WithFormProperties } from '../../shared/models'; import { ManagementTeamSubFormModel, WithManagementTeam } from './ManagementTeamSubFormModel'; -export class ManagementFormModel implements WithManagementTeam { +export class ManagementFormModel implements WithManagementTeam, WithFormProperties { fileName: string | null = ''; additionalDetails: string | null = ''; filePurpose: string | null = ''; diff --git a/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx b/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx index 9c36f4c9d4..4de2b123fb 100644 --- a/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx +++ b/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx @@ -20,6 +20,7 @@ import { exists, isValidId, sortFileProperties, stripTrailingSlash } from '@/uti import { SideBarContext } from '../context/sidebarContext'; import { PropertyForm } from '../shared/models'; import usePathGenerator from '../shared/sidebarPathGenerator'; +import { ResearchForm } from './add/models'; import { useGetResearch } from './hooks/useGetResearch'; import { useUpdateResearchProperties } from './hooks/useUpdateResearchProperties'; import { IResearchViewProps } from './ResearchView'; @@ -250,15 +251,10 @@ export const ResearchContainer: React.FunctionComponent [getPropertyAssociations, researchFileId], ); - const onUpdateProperties = ( - file: ApiGen_Concepts_File, - ): Promise => { + const onUpdateProperties = (file: ResearchForm): Promise => { return withUserOverride( (userOverrideCodes: UserOverrideCode[]) => { - return updateResearchFileProperties( - file as ApiGen_Concepts_ResearchFile, - userOverrideCodes, - ).then(response => { + return updateResearchFileProperties(file.toApi(), userOverrideCodes).then(response => { onSuccess(); return response; }); diff --git a/source/frontend/src/features/mapSideBar/research/ResearchView.tsx b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx index bb12119659..359c7f9eed 100644 --- a/source/frontend/src/features/mapSideBar/research/ResearchView.tsx +++ b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx @@ -23,6 +23,7 @@ import { PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import UpdatePropertiesContainer from '../shared/update/properties/UpdatePropertiesContainer'; +import { ResearchForm } from './add/models'; import ResearchHeader from './common/ResearchHeader'; import ResearchGenerateContainer from './ResearchGenerateContainer'; import ResearchRouter from './ResearchRouter'; @@ -42,7 +43,7 @@ export interface IResearchViewProps { onSelectFileSummary: () => void; onSelectProperty: (propertyId: number) => void; onEditProperties: () => void; - onUpdateProperties: (file: ApiGen_Concepts_File) => Promise; + onUpdateProperties: (file: ResearchForm) => Promise; confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; canRemove: (propertyId: number) => Promise; onSuccess: () => void; @@ -86,12 +87,12 @@ const ResearchView: React.FunctionComponent = ({ {exists(researchFile) && (

This property has already been added to one or more research files.

Do you want to acknowledge and proceed?

diff --git a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx index 06d580be73..8647e500d1 100644 --- a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx +++ b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.tsx @@ -1,5 +1,5 @@ import { Formik, FormikProps } from 'formik'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; @@ -21,13 +21,12 @@ import SidebarFooter from '../../shared/SidebarFooter'; import { StyledFormWrapper } from '../../shared/styles'; import { useAddResearch } from '../hooks/useAddResearch'; import { AddResearchFileYupSchema } from './AddResearchFileYupSchema'; -import { IAddResearchFormProps } from './AddResearchForm'; import { ResearchForm } from './models'; export interface IAddResearchContainerProps { onClose: () => void; onSuccess: (newResearchId: number) => void; - View: React.FC; + View: React.FC; } export const AddResearchContainer: React.FunctionComponent = props => { @@ -39,10 +38,12 @@ export const AddResearchContainer: React.FunctionComponent(true); + const [needsFirstTimeConfirmation, setNeedsFirstTimeConfirmation] = useState(true); + + const initialForm = new ResearchForm(); // Warn user that property is part of an existing research file - const confirmBeforeAdd = useCallback( + const confirmProperty = useCallback( async (propertyForm: PropertyForm): Promise => { if (isValidId(propertyForm.apiId)) { const response = await getPropertyAssociations(propertyForm.apiId); @@ -57,68 +58,54 @@ export const AddResearchContainer: React.FunctionComponent { - return new ResearchForm(); - }, []); - const withUserOverride = useApiUserOverride< (userOverrideCodes: UserOverrideCode[]) => Promise >('Failed to add Research File'); // Require user confirmation before adding a property to file - // This is the flow for Map Marker -> right-click -> create Research File - useEffect(() => { - const runAsync = async () => { - if (exists(initialForm) && exists(formikRef.current) && needsUserConfirmation) { - if (initialForm.properties.length > 0) { - // Check all properties for confirmation - const needsConfirmation = await Promise.all( - initialForm.properties.map(formProperty => confirmBeforeAdd(formProperty)), - ); - if (needsConfirmation.some(confirm => confirm)) { - setModalContent({ - variant: 'warning', - title: 'User Override Required', - message: ( - <> -

- One or more properties have already been added to one or more research files. -

-

Do you want to acknowledge and proceed?

- - ), - okButtonText: 'Yes', - cancelButtonText: 'No', - handleOk: () => { - // allow the properties to be added to the file being created - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', initialForm.properties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - handleCancel: () => { - // clear out the properties array as the user did not agree to the popup - initialForm.properties.splice(0, initialForm.properties.length); - formikRef.current.resetForm(); - formikRef.current.setFieldValue('properties', initialForm.properties); - setDisplayModal(false); - // show the user confirmation modal only once when creating a file - setNeedsUserConfirmation(false); - }, - }); - setDisplayModal(true); - } - } + const confirmBeforeAdd = useCallback( + async ( + newPropertyForms: PropertyForm[], + isValidCallback: (isValid: boolean, newProperties: PropertyForm[]) => void, + ) => { + const needsConfirmation = await Promise.all( + newPropertyForms.map(formProperty => confirmProperty(formProperty)), + ); + if (needsFirstTimeConfirmation && needsConfirmation.some(x => x === true)) { + // show the user confirmation modal only once when creating a file + setNeedsFirstTimeConfirmation(false); + setModalContent({ + variant: 'warning', + title: 'User Override Required', + message: ( + <> +

One or more properties have already been added to one or more research files.

+

Do you want to acknowledge and proceed?

+ + ), + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: () => { + // allow the properties to be added to the file being created + isValidCallback(true, newPropertyForms); + setDisplayModal(false); + }, + handleCancel: () => { + // clear out the properties array as the user did not agree to the popup + isValidCallback(false, []); + setDisplayModal(false); + }, + }); + setDisplayModal(true); + } else { + isValidCallback(true, newPropertyForms); } - }; + }, + [confirmProperty, needsFirstTimeConfirmation, setDisplayModal, setModalContent], + ); - runAsync(); - }, [confirmBeforeAdd, initialForm, needsUserConfirmation, setDisplayModal, setModalContent]); + // Get PropertyForms with addresses for all selected features + const { isLoading } = usePropertyFormSyncronizer(formikRef, confirmBeforeAdd); const saveResearchFile = async ( researchFile: ApiGen_Concepts_ResearchFile, @@ -186,7 +173,7 @@ export const AddResearchContainer: React.FunctionComponent - + diff --git a/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx b/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx index b18216c5dc..cca1de0990 100644 --- a/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx +++ b/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.tsx @@ -6,17 +6,12 @@ import styled from 'styled-components'; import { Input } from '@/components/common/form/'; import { Section } from '@/components/common/Section/Section'; -import { PropertyForm } from '../../shared/models'; import PropertiesListContainer from '../../shared/update/properties/PropertiesListContainer'; import { ResearchFileNameGuide } from '../common/ResearchFileNameGuide'; import { UpdateProjectsSubForm } from '../common/updateProjects/UpdateProjectsSubForm'; import { ResearchForm } from './models'; -export interface IAddResearchFormProps { - confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; -} - -const AddResearchForm: React.FC = props => { +const AddResearchForm: React.FC = () => { const { values } = useFormikContext(); return ( @@ -44,8 +39,6 @@ const AddResearchForm: React.FC = props => { removeCallback()} - needsConfirmationBeforeAdd={props.confirmBeforeAdd} - canUploadShapefiles={false} /> diff --git a/source/frontend/src/features/mapSideBar/research/add/models.ts b/source/frontend/src/features/mapSideBar/research/add/models.ts index d51173ca82..2d355bd51f 100644 --- a/source/frontend/src/features/mapSideBar/research/add/models.ts +++ b/source/frontend/src/features/mapSideBar/research/add/models.ts @@ -3,10 +3,10 @@ import { ApiGen_Concepts_ResearchFileProperty } from '@/models/api/generated/Api import { getEmptyBaseAudit } from '@/models/defaultInitializers'; import { applyDisplayOrder } from '@/utils'; -import { PropertyForm } from '../../shared/models'; +import { PropertyForm, WithFormProperties } from '../../shared/models'; import { ResearchFileProjectFormModel } from '../common/models'; -export class ResearchForm { +export class ResearchForm implements WithFormProperties { public id?: number; public name: string; public properties: PropertyForm[]; diff --git a/source/frontend/src/features/mapSideBar/shared/models.ts b/source/frontend/src/features/mapSideBar/shared/models.ts index 90dc425647..3454b4b204 100644 --- a/source/frontend/src/features/mapSideBar/shared/models.ts +++ b/source/frontend/src/features/mapSideBar/shared/models.ts @@ -36,7 +36,11 @@ import { } from '@/utils'; import { toTypeCodeNullable } from '@/utils/formUtils'; -export class FileForm { +export interface WithFormProperties { + properties: PropertyForm[]; +} + +export class FileForm implements WithFormProperties { public id?: number; public name: string; public properties: PropertyForm[]; diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx index 67e2cc2b7d..289f625a58 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.tsx @@ -24,7 +24,6 @@ import AddPropertiesGuide from './AddPropertiesGuide'; export interface IPropertiesListContainerProps { properties: PropertyForm[]; verifyCanRemove: (propertyId: number, removeCallback: () => void) => void; - needsConfirmationBeforeAdd: (propertyForm: PropertyForm) => Promise; confirmBeforeAddMessage?: React.ReactNode; showDisabledProperties?: boolean; canUploadShapefiles?: boolean; diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx index 3e20d42a52..3315ac8ac9 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.tsx @@ -13,17 +13,17 @@ import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_Fil import { UserOverrideCode } from '@/models/api/UserOverrideCode'; import { isValidId } from '@/utils'; -import { FileForm, PropertyForm } from '../../models'; +import { FileForm, PropertyForm, WithFormProperties } from '../../models'; import SidebarFooter from '../../SidebarFooter'; import PropertiesListContainer from './PropertiesListContainer'; import { UpdatePropertiesYupSchema } from './UpdatePropertiesYupSchema'; export interface IUpdatePropertiesContainerProps { - formFile: FileForm; + formFile: WithFormProperties; setIsShowingPropertySelector: (isShowing: boolean) => void; onSuccess: (updateProperties?: boolean, updateFile?: boolean) => void; updateFileProperties: ( - file: FileForm, + file: WithFormProperties, userOverrideCodes: UserOverrideCode[], ) => Promise; canRemove: (propertyId: number) => Promise; @@ -52,11 +52,13 @@ export const UpdatePropertiesContainer: React.FunctionComponent< // Require user confirmation before adding a property to file const confirmBeforeAdd = useCallback( - async (newPropertyForms: PropertyForm[], isValidCallback: (isValid: boolean) => void) => { + async ( + newPropertyForms: PropertyForm[], + isValidCallback: (isValid: boolean, newProperties: PropertyForm[]) => void, + ) => { const needsConfirmation = await Promise.all( newPropertyForms.map(formProperty => canAdd(formProperty)), ); - debugger; if (needsConfirmation.some(x => x === true) && !modalProps.display) { setModalContent({ variant: 'warning', @@ -66,24 +68,24 @@ export const UpdatePropertiesContainer: React.FunctionComponent< cancelButtonText: 'No', handleOk: () => { // allow the property to be added to the file being created - isValidCallback(true); + isValidCallback(true, newPropertyForms); setDisplayModal(false); }, handleCancel: () => { // clear out the properties array as the user did not agree to the popup - isValidCallback(false); + isValidCallback(false, []); setDisplayModal(false); }, }); setDisplayModal(true); } else { - isValidCallback(true); + isValidCallback(true, newPropertyForms); } }, [modalProps.display, canAdd, setModalContent, confirmAddMessage, setDisplayModal], ); - const { isLoading } = usePropertyFormSyncronizer(formikRef, 'properties', confirmBeforeAdd); + const { isLoading } = usePropertyFormSyncronizer(formikRef, confirmBeforeAdd); const handleSaveClick = async () => { await formikRef?.current?.validateForm(); @@ -176,7 +178,7 @@ export const UpdatePropertiesContainer: React.FunctionComponent< /> } > - + innerRef={formikRef} initialValues={props.formFile} validationSchema={UpdatePropertiesYupSchema} @@ -188,7 +190,6 @@ export const UpdatePropertiesContainer: React.FunctionComponent< Promise.resolve(true)} canUploadShapefiles={props.canUploadShapefiles} canReposition={props.canReposition} showDisabledProperties={props.showDisabledProperties} diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx index 3a10bc97bf..4fb0d5ca8c 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx @@ -37,6 +37,7 @@ const AddSubdivisionContainer: React.FC = ({ const selectedFeatureDataset = firstOrNull(mapMachine.locationFeaturesForAddition); const { setModalContent, setDisplayModal } = useModalContext(); const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); + const [isFirstLoad, setIsFirstLoad] = useState(true); const { addPropertyOperationApi: { execute: addPropertyOperation, loading }, @@ -58,7 +59,7 @@ const AddSubdivisionContainer: React.FC = ({ async function loadInitialProperty() { // support creating a new subdivision from the map popup - if (selectedFeatureDataset !== null) { + if (selectedFeatureDataset !== null && isFirstLoad) { const propertyForm = PropertyForm.fromLocationFeatureDataset(selectedFeatureDataset); if (isValidString(propertyForm.pid)) { // TODO: This should work with multiple properties @@ -72,8 +73,9 @@ const AddSubdivisionContainer: React.FC = ({ setInitialForm(subdivisionFormModel); } } + setIsFirstLoad(false); } - }, [selectedFeatureDataset, getAddress]); + }, [selectedFeatureDataset, getAddress, isFirstLoad]); useEffect(() => { if (exists(initialForm) && exists(formikRef.current)) { diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx index 6b24dcfb58..580f6d2909 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx @@ -129,7 +129,10 @@ const AddSubdivisionView: React.FunctionComponent< - setFieldValue('sourceProperty', selectedProperty) + setFieldValue( + 'sourceProperty', + PropertyForm.fromPropertyApi(selectedProperty), + ) } PropertySelectorPidSearchView={PropertySearchSelectorPidFormView} /> diff --git a/source/frontend/src/hooks/usePropertyFormSyncronizer.ts b/source/frontend/src/hooks/usePropertyFormSyncronizer.ts index ae7e22624b..955634dc41 100644 --- a/source/frontend/src/hooks/usePropertyFormSyncronizer.ts +++ b/source/frontend/src/hooks/usePropertyFormSyncronizer.ts @@ -1,10 +1,10 @@ import { FormikProps, getIn } from 'formik'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect } from 'react'; import { toast } from 'react-toastify'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; -import { PropertyForm } from '@/features/mapSideBar/shared/models'; +import { PropertyForm, WithFormProperties } from '@/features/mapSideBar/shared/models'; import { exists, isEmptyOrNull } from '@/utils'; import { arePropertyFormsEqual } from '@/utils/mapPropertyUtils'; @@ -15,12 +15,11 @@ import { useLocationFeatureDatasetsWithAddresses } from './useLocationFeatureDat * Notifies the map state machine to enter/exit edit properties mode. * Use this hook in any component that needs to toggle edit properties mode on mount/unmount. */ -export function usePropertyFormSyncronizer( +export function usePropertyFormSyncronizer( formikRef: React.RefObject>, - fieldName: keyof T, validateNewProperties: ( newProperties: PropertyForm[], - validateCallback: (isValid: boolean) => void, + validateCallback: (isValid: boolean, newProperties: PropertyForm[]) => void, ) => void, overrideFeatures?: LocationFeatureDataset[], ) { @@ -34,26 +33,21 @@ export function usePropertyFormSyncronizer( const { locationFeaturesWithAddresses: featuresWithAddresses, bcaLoading } = useLocationFeatureDatasetsWithAddresses(overrideFeatures ?? locationFeaturesForAddition); - const [pendingConfirmation, setPendingConfirmation] = useState(null); - // if we are listening to property add notifications we must tell the state machine we are in edit mode. useEditPropertiesMode(); + const fieldName = 'properties'; + const validationCallback = useCallback( - (isValid: boolean) => { - debugger; - if (isValid && !isEmptyOrNull(pendingConfirmation)) { - const existingProperties = getIn(formikRef?.current?.values, fieldName as string) ?? []; - formikRef.current?.setFieldValue(fieldName as string, [ - ...existingProperties, - ...pendingConfirmation, - ]); - formikRef.current?.setFieldTouched(fieldName as string, true); - toast.success(`Added ${pendingConfirmation.length} new property(s) to the file.`); + (isValid: boolean, newProperties: PropertyForm[]) => { + if (isValid && !isEmptyOrNull(newProperties)) { + const existingProperties = getIn(formikRef?.current?.values, fieldName) ?? []; + formikRef.current?.setFieldValue(fieldName, [...existingProperties, ...newProperties]); + formikRef.current?.setFieldTouched(fieldName, true); + toast.success(`Added ${newProperties.length} new property(s) to the file.`); } - setPendingConfirmation(null); }, - [fieldName, formikRef, pendingConfirmation], + [fieldName, formikRef], ); // This effect willbe called whenever there are new locations pending addition. @@ -83,7 +77,6 @@ export function usePropertyFormSyncronizer( // If there are unique properties request a confirmation if (uniqueNewProperties.length > 0) { - setPendingConfirmation(uniqueNewProperties); validateNewProperties(uniqueNewProperties, validationCallback); } if (duplicatesSkipped > 0) { From 8055df086647d5f37cc915a5d37e6cab97fe3fee Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Tue, 9 Dec 2025 02:07:25 -0800 Subject: [PATCH 4/5] Updated tests and snaps --- .../src/components/maps/ZoomToLocation.tsx | 16 +- .../MapSelectorContainer.test.tsx | 163 +-- .../map/PropertyMapSelectorSubForm.test.tsx | 26 +- .../PropertySearchSelectorFormView.test.tsx | 96 +- .../SelectedPropertyRow.test.tsx | 101 +- .../SelectedPropertyRow.test.tsx.snap | 179 +-- .../leases/add/AddLeaseContainer.test.tsx | 13 +- .../features/leases/add/AddLeaseContainer.tsx | 26 +- .../leases/add/AdministrationSubForm.test.tsx | 18 +- .../leases/add/LeaseDetailSubForm.test.tsx | 35 +- .../AddLeaseContainer.test.tsx.snap | 46 +- .../deposits/DepositsContainer.test.tsx | 37 +- .../DepositNotes/DepositNotes.test.tsx | 35 +- .../details/DetailDescription.test.tsx | 7 +- .../details/UpdateLeaseContainer.test.tsx | 21 +- .../payment/PeriodPaymentsContainer.test.tsx | 22 +- .../AddLeaseStakeholderContainer.test.tsx | 50 +- .../AddLeaseStakeholderContainer.tsx | 2 +- .../stakeholders/AddLeaseStakeholderForm.tsx | 14 +- .../SelectedPropertyRow.test.tsx | 153 --- .../SelectedPropertyRow.tsx | 119 -- .../acquisition/AcquisitionContainer.test.tsx | 3 +- .../add/AddAcquisitionContainer.test.tsx | 53 +- .../add/AddAcquisitionForm.test.tsx | 27 +- .../AddAcquisitionContainer.test.tsx.snap | 698 +++++----- .../AddAcquisitionForm.test.tsx.snap | 836 ++++++------ .../GenerateForm/hooks/useGenerateForm12.ts | 18 +- .../GenerateForm/hooks/useGenerateNotice.ts | 18 +- .../AddConsolidationContainer.test.tsx | 8 +- .../consolidation/AddConsolidationModel.ts | 4 +- .../AddConsolidationView.test.tsx | 41 +- .../disposition/DispositionContainer.test.tsx | 13 +- .../add/AddDispositionContainer.test.tsx | 40 +- .../add/AddDispositionContainerView.test.tsx | 1 - .../AddDispositionContainerView.test.tsx.snap | 334 +++-- .../mapSideBar/lease/LeaseView.test.tsx | 11 + .../management/ManagementContainer.test.tsx | 11 +- .../management/ManagementView.test.tsx | 4 +- .../add/AddManagementContainer.test.tsx | 36 +- .../add/AddManagementContainerView.test.tsx | 1 - .../AddManagementContainerView.test.tsx.snap | 320 ++--- .../property/MotiInventoryContainer.tsx | 6 +- .../property/MotiInventoryHeader.test.tsx | 24 +- .../add/AddResearchContainer.test.tsx | 48 +- .../research/add/AddResearchForm.test.tsx | 4 +- .../AddResearchContainer.test.tsx.snap | 56 +- .../AddResearchForm.test.tsx.snap | 56 +- .../detail/PropertyFileContainer.test.tsx | 2 +- .../shared/detail/PropertyFileContainer.tsx | 1 - .../src/features/mapSideBar/shared/models.ts | 1 + .../PropertiesListContainer.test.tsx | 24 +- ...tsx => UpdatePropertiesContainer.test.tsx} | 125 +- .../PropertiesListContainer.test.tsx.snap | 1152 +++++++++++++++++ .../UpdateProperties.test.tsx.snap | 258 ++-- .../UpdatePropertiesContainer.test.tsx.snap | 1013 +++++++++++++++ .../AddSubdivisionContainer.test.tsx | 11 +- .../subdivision/AddSubdivisionContainer.tsx | 2 +- .../subdivision/AddSubdivisionModel.ts | 4 +- .../subdivision/AddSubdivisionView.test.tsx | 55 +- .../subdivision/AddSubdivisionView.tsx | 6 +- .../properties/parcelList/ParcelItem.test.tsx | 21 +- .../worklist/WorklistContainer.test.tsx | 4 +- .../properties/worklist/WorklistView.test.tsx | 8 +- .../properties/worklist/WorklistView.tsx | 2 +- .../__snapshots__/WorklistView.test.tsx.snap | 4 +- .../worklist/context/WorklistContext.test.tsx | 59 +- .../worklist/context/WorklistContext.tsx | 4 +- .../repositories/useComposedProperties.ts | 23 +- .../hooks/usePropertyFormSyncronizer.test.ts | 93 +- source/frontend/src/mocks/featureset.mock.ts | 4 - .../frontend/src/mocks/worklistParcel.mock.ts | 1 - .../src/utils/mapPropertyUtils.test.tsx | 232 ++-- source/frontend/src/utils/mapPropertyUtils.ts | 2 +- 73 files changed, 4347 insertions(+), 2614 deletions(-) delete mode 100644 source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.test.tsx delete mode 100644 source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx rename source/frontend/src/features/mapSideBar/shared/update/properties/{UpdateProperties.test.tsx => UpdatePropertiesContainer.test.tsx} (76%) create mode 100644 source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/PropertiesListContainer.test.tsx.snap create mode 100644 source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdatePropertiesContainer.test.tsx.snap diff --git a/source/frontend/src/components/maps/ZoomToLocation.tsx b/source/frontend/src/components/maps/ZoomToLocation.tsx index 2eaefd8277..090c4d2d09 100644 --- a/source/frontend/src/components/maps/ZoomToLocation.tsx +++ b/source/frontend/src/components/maps/ZoomToLocation.tsx @@ -15,6 +15,7 @@ import { boundaryFromFileProperty, exists, firstValidOrNull, + isEmptyOrNull, latLngLiteralToGeometry, pimsGeomeryToGeometry, } from '@/utils'; @@ -51,11 +52,20 @@ export const ZoomToLocation: React.FunctionComponent = ({ const { requestFlyToBounds } = useMapStateMachine(); const bounds: LatLngBounds | null = useMemo(() => { - const propertyLocations: Geometry[] = - formProperties?.map( - p => p?.polygon ?? latLngLiteralToGeometry({ lat: p?.latitude, lng: p?.longitude }), + const propertyLocations: Geometry[] = []; + const points = + formProperties?.map(p => + p?.polygon ?? (p.latitude && p.longitude) + ? latLngLiteralToGeometry({ lat: p?.latitude, lng: p?.longitude }) + : null, ) || []; + if (!isEmptyOrNull(points)) { + points.forEach(point => { + propertyLocations.push(point); + }); + } + if (exists(geometry)) { propertyLocations.push(geometry); } diff --git a/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx b/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx index 7455d1c5b9..ebc1b8c8da 100644 --- a/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx +++ b/source/frontend/src/components/propertySelector/MapSelectorContainer.test.tsx @@ -7,7 +7,6 @@ import thunk from 'redux-thunk'; import { useMapProperties } from '@/hooks/repositories/useMapProperties'; import { getMockLocationFeatureDataset } from '@/mocks/featureset.mock'; -import { getMockSelectedFeatureDataset } from '@/mocks/getMockSelectedFeatureDataset'; import { mockFAParcelLayerResponse, mockGeocoderOptions } from '@/mocks/index.mock'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { act, fillInput, render, RenderOptions, screen, userEvent } from '@/utils/test-utils'; @@ -30,12 +29,7 @@ vi.mocked(useMapProperties).mockReturnValue({ error: null, response: { features: [] } as any, execute: vi.fn().mockResolvedValue({ - features: [ - { - ...getMockSelectedFeatureDataset().pimsFeature, - properties: {}, - }, - ], + features: getMockLocationFeatureDataset().pimsFeatures, }), loading: false, status: 200, @@ -49,9 +43,8 @@ describe('MapSelectorContainer component', () => { , @@ -107,23 +100,25 @@ describe('MapSelectorContainer component', () => { }); it('displays all selected property attributes', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); + const mockFeatureSet = getMockLocationFeatureDataset(); const { getByText } = await setup({ modifiedProperties: [ { ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID: 123456789, - SURVEY_PLAN_NUMBER: 'SPS22411', - LAND_LEGAL_DESCRIPTION: 'Test Legal Description', - STREET_ADDRESS_1: 'Test address 123', - REGION_CODE: 1, - DISTRICT_CODE: 5, + pimsFeatures: [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0]?.properties, + PID: 123456789, + SURVEY_PLAN_NUMBER: 'SPS22411', + LAND_LEGAL_DESCRIPTION: 'Test Legal Description', + STREET_ADDRESS_1: 'Test address 123', + REGION_CODE: 1, + DISTRICT_CODE: 5, + }, }, - }, + ], regionFeature: { ...mockFeatureSet.regionFeature, properties: { @@ -153,23 +148,25 @@ describe('MapSelectorContainer component', () => { }); it('selected properties display a warning if added', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); + const mockFeatureSet = getMockLocationFeatureDataset(); const { getByText, getByTitle, findByTestId, container } = await setup({ modifiedProperties: [ { ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID: 123456789, - SURVEY_PLAN_NUMBER: 'SPS22411', - LAND_LEGAL_DESCRIPTION: 'Test Legal Description', - STREET_ADDRESS_1: 'Test address 123', - REGION_CODE: 1, - DISTRICT_CODE: 5, + pimsFeatures: [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0]?.properties, + PID: 123456789, + SURVEY_PLAN_NUMBER: 'SPS22411', + LAND_LEGAL_DESCRIPTION: 'Test Legal Description', + STREET_ADDRESS_1: 'Test address 123', + REGION_CODE: 1, + DISTRICT_CODE: 5, + }, }, - }, + ], }, ], }); @@ -191,104 +188,4 @@ describe('MapSelectorContainer component', () => { expect(onSelectedProperties).toHaveBeenCalled(); }); - - it('selected properties display a warning if added multiple times', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - const { getByText, getByTitle, findByTestId, container } = await setup({ - modifiedProperties: [ - { - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '009-727-493', - SURVEY_PLAN_NUMBER: 'SPS22411', - LAND_LEGAL_DESCRIPTION: 'Test Legal Description', - STREET_ADDRESS_1: 'Test address 123', - REGION_CODE: 1, - DISTRICT_CODE: 5, - }, - }, - }, - ], - }); - - const searchTab = getByText('Search'); - await act(async () => userEvent.click(searchTab)); - await act(async () => { - fillInput(container, 'searchBy', 'pid', 'select'); - }); - await fillInput(container, 'pid', '009-727-493'); - const searchButton = getByTitle('search'); - - await act(async () => userEvent.click(searchButton)); - - const checkbox = await findByTestId( - 'selectrow-PID-009-727-493-48.76613749999999--123.46163749999998', - ); - expect(checkbox).toBeVisible(); - - await act(async () => userEvent.click(checkbox)); - const addButton = getByText('Add to selection'); - await act(async () => userEvent.click(addButton)); - - const toast = await screen.findAllByText( - 'A property that the user is trying to select has already been added to the selected properties list', - ); - expect(toast[0]).toBeVisible(); - }); - - it(`calls "repositionSelectedProperty" callback when file marker has been repositioned`, async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - - // simulate file marker repositioning via the map state machine - const testMapMock: IMapStateMachineContext = { - ...mapMachineBaseMock, - isRepositioning: true, - mapFeatureData: {} as any, - mapLocationFeatureDataset: {} as any, - }; - const mapProperties = [ - { - ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '009-727-493', - SURVEY_PLAN_NUMBER: 'SPS22411', - LAND_LEGAL_DESCRIPTION: 'Test Legal Description', - STREET_ADDRESS_1: 'Test address 123', - REGION_CODE: 1, - DISTRICT_CODE: 5, - }, - }, - }, - ]; - - const { rerender } = await setup({ - modifiedProperties: mapProperties, - mockMapMachine: testMapMock, - }); - - // simulate file marker repositioning via the map state machine - await act(async () => { - testMapMock.isRepositioning = true; - testMapMock.repositioningFeatureDataset = getMockSelectedFeatureDataset(); - testMapMock.mapLocationFeatureDataset = getMockLocationFeatureDataset(); - }); - - rerender( - - - , - ); - - expect(onRepositionSelectedProperty).toHaveBeenCalled(); - }); }); diff --git a/source/frontend/src/components/propertySelector/map/PropertyMapSelectorSubForm.test.tsx b/source/frontend/src/components/propertySelector/map/PropertyMapSelectorSubForm.test.tsx index 7f82cd9b42..4b5cbf8c97 100644 --- a/source/frontend/src/components/propertySelector/map/PropertyMapSelectorSubForm.test.tsx +++ b/source/frontend/src/components/propertySelector/map/PropertyMapSelectorSubForm.test.tsx @@ -1,12 +1,12 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { getMockSelectedFeatureDataset } from '@/mocks/getMockSelectedFeatureDataset'; import { render, RenderOptions, screen } from '@/utils/test-utils'; import PropertyMapSelectorSubForm, { IPropertyMapSelectorSubFormProps, } from './PropertyMapSelectorSubForm'; +import { getMockLocationFeatureDataset } from '@/mocks/featureset.mock'; const onClickDraftMarker = vi.fn(); @@ -20,7 +20,7 @@ describe('PropertySelectorSubForm component', () => { const utils = render( , { ...renderOptions, @@ -43,21 +43,23 @@ describe('PropertySelectorSubForm component', () => { }); it('renders as expected when provided a list of properties', () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); + const mockFeatureSet = getMockLocationFeatureDataset(); setup({ onClickDraftMarker, selectedProperty: { ...mockFeatureSet, - parcelFeature: { - ...mockFeatureSet.parcelFeature, - properties: { - ...mockFeatureSet.parcelFeature?.properties, - PID: '123-456-789', - PIN: 1111222, - LEGAL_DESCRIPTION: 'A legal description', - PLAN_NUMBER: 'VIP3881', + parcelFeatures: [ + { + ...mockFeatureSet.parcelFeatures[0], + properties: { + ...mockFeatureSet.parcelFeatures[0]?.properties, + PID: '123-456-789', + PIN: 1111222, + LEGAL_DESCRIPTION: 'A legal description', + PLAN_NUMBER: 'VIP3881', + }, }, - }, + ], }, }); expect(screen.getByText('123-456-789')).toBeVisible(); diff --git a/source/frontend/src/components/propertySelector/search/PropertySearchSelectorFormView.test.tsx b/source/frontend/src/components/propertySelector/search/PropertySearchSelectorFormView.test.tsx index 3f3aa1ec0d..5681484ef2 100644 --- a/source/frontend/src/components/propertySelector/search/PropertySearchSelectorFormView.test.tsx +++ b/source/frontend/src/components/propertySelector/search/PropertySearchSelectorFormView.test.tsx @@ -2,7 +2,6 @@ import { Feature, Geometry } from 'geojson'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { mockPropertyLayerSearchResponse } from '@/mocks/filterData.mock'; import { emptyPmbcParcel, @@ -16,6 +15,10 @@ import { PropertySearchSelectorFormView, } from './PropertySearchSelectorFormView'; import { featureToLocationFeatureDataset } from './PropertySelectorSearchContainer'; +import { + emptyFeatureDataset, + LocationFeatureDataset, +} from '@/components/common/mapFSM/useLocationFeatureLoader'; const mockStore = configureMockStore([thunk]); @@ -222,61 +225,56 @@ describe('PropertySearchSelectorFormView component', () => { ); await act(async () => userEvent.click(checkbox)); expect(checkbox).toBeChecked(); - expect(onSelectedProperties).toHaveBeenCalledWith([ - expect.objectContaining({ + expect(onSelectedProperties).toHaveBeenCalledWith([ + expect.objectContaining({ + ...emptyFeatureDataset(), districtFeature: null, - id: 'PID-006-772-331-55.706230240625004--121.60834946062499', location: { lat: 55.706230240625004, lng: -121.60834946062499, }, - municipalityFeature: null, - fileLocation: null, - fileBoundary: null, - parcelFeature: expect.objectContaining | null>({ - geometry: { - coordinates: [ - [ - [-121.60861991, 55.70650025], - [-121.60861925, 55.70588252], - [-121.60728684, 55.7061924], - [-121.60718833, 55.70627546], - [+-121.60718846, 55.70643785], - [-121.60729988, 55.70650069], - [-121.60861991, 55.70650025], + parcelFeatures: expect.arrayContaining([ + expect.objectContaining>({ + geometry: { + coordinates: [ + [ + [-121.60861991, 55.70650025], + [-121.60861925, 55.70588252], + [-121.60728684, 55.7061924], + [-121.60718833, 55.70627546], + [+-121.60718846, 55.70643785], + [-121.60729988, 55.70650069], + [-121.60861991, 55.70650025], + ], ], - ], - type: 'Polygon', - }, - id: 'WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW.fid-674bf6f8_180d8c9b18e_7c12', - properties: { - ...emptyPmbcParcel, - FEATURE_AREA_SQM: 4478.6462, - FEATURE_LENGTH_M: 281.3187, - MUNICIPALITY: 'Chetwynd, District of', - OBJECTID: 601612446, - OWNER_TYPE: 'Private', - PARCEL_CLASS: 'Subdivision', - PARCEL_FABRIC_POLY_ID: 1994518, - PARCEL_NAME: '006772331', - PARCEL_START_DATE: null, - PARCEL_STATUS: 'Active', - PID: '006772331', - PID_NUMBER: 6772331, - PIN: 10514131, - PLAN_NUMBER: 'PGP27005', - REGIONAL_DISTRICT: 'Peace River Regional District', - SE_ANNO_CAD_DATA: null, - WHEN_UPDATED: '2019-01-09Z', - }, - type: 'Feature', - }), - pimsFeature: null, + type: 'Polygon', + }, + id: 'WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW.fid-674bf6f8_180d8c9b18e_7c12', + properties: { + ...emptyPmbcParcel, + FEATURE_AREA_SQM: 4478.6462, + FEATURE_LENGTH_M: 281.3187, + MUNICIPALITY: 'Chetwynd, District of', + OBJECTID: 601612446, + OWNER_TYPE: 'Private', + PARCEL_CLASS: 'Subdivision', + PARCEL_FABRIC_POLY_ID: 1994518, + PARCEL_NAME: '006772331', + PARCEL_START_DATE: null, + PARCEL_STATUS: 'Active', + PID: '006772331', + PID_NUMBER: 6772331, + PIN: 10514131, + PLAN_NUMBER: 'PGP27005', + REGIONAL_DISTRICT: 'Peace River Regional District', + SE_ANNO_CAD_DATA: null, + WHEN_UPDATED: '2019-01-09Z', + }, + type: 'Feature', + }), + ]), + pimsFeatures: null, regionFeature: null, - selectingComponentId: null, }), ]); }); diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx index cbcdd52207..925f45faaf 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx @@ -95,13 +95,6 @@ describe('SelectedPropertyRow component', () => { expect(onRemove).toHaveBeenCalled(); }); - it('calls map machine when reposition button is clicked', async () => { - await setup(); - const moveButton = screen.getByTitle('move-pin-location'); - await act(async () => userEvent.click(moveButton)); - expect(mapMachineBaseMock.startReposition).toHaveBeenCalled(); - }); - it('displays pid', async () => { const mockFeatureSet = getMockLocationFeatureDataset(); mockFeatureSet.parcelFeatures = [] as any; @@ -120,16 +113,19 @@ describe('SelectedPropertyRow component', () => { it('falls back to pin', async () => { const mockFeatureSet = getMockLocationFeatureDataset(); - mockFeatureSet.parcelFeature = {} as any; - mockFeatureSet.pimsFeature = { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: undefined, - PIN: 1234, + mockFeatureSet.parcelFeatures = {} as any; + mockFeatureSet.pimsFeatures = [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0]?.properties, + PID_PADDED: undefined, + PIN: 1234, + }, }, - }; - await setup({ props: { property: mockFeatureSet } }); + ]; + const propertyForm = PropertyForm.fromLocationFeatureDataset(mockFeatureSet); + await setup({ props: { property: propertyForm } }); expect(screen.getByText('PIN: 1234')).toBeVisible(); }); @@ -147,7 +143,8 @@ describe('SelectedPropertyRow component', () => { }, }, ]; - await setup({ props: { property: mockFeatureSet } }); + const propertyForm = PropertyForm.fromLocationFeatureDataset(mockFeatureSet); + await setup({ props: { property: propertyForm } }); expect(screen.getByText('Plan #: VIP123')).toBeVisible(); }); @@ -157,7 +154,8 @@ describe('SelectedPropertyRow component', () => { mockFeatureSet.parcelFeatures = null; mockFeatureSet.location = { lat: 4, lng: 5 }; - await setup({ props: { property: mockFeatureSet } }); + const propertyForm = PropertyForm.fromLocationFeatureDataset(mockFeatureSet); + await setup({ props: { property: propertyForm } }); expect(screen.getByText('5.000000, 4.000000')).toBeVisible(); }); @@ -177,60 +175,79 @@ describe('SelectedPropertyRow component', () => { }, }, ]; - await setup({ props: { property: mockFeatureSet } }); + const propertyForm = PropertyForm.fromLocationFeatureDataset(mockFeatureSet); + await setup({ props: { property: propertyForm } }); expect(screen.getByText('Address: a test address')).toBeVisible(); }); it('shows Inactive as selected when isActive is false', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - mockFeatureSet.pimsFeature = {} as any; - mockFeatureSet.parcelFeature = {} as any; - mockFeatureSet.isActive = false; + const mockFeatureSet = getMockLocationFeatureDataset(); + mockFeatureSet.pimsFeatures = {} as any; + mockFeatureSet.parcelFeatures = {} as any; - await setup({ props: { property: mockFeatureSet, showDisable: true } }); + const propertyForm = PropertyForm.fromLocationFeatureDataset(mockFeatureSet); + propertyForm.isActive = 'false'; + + await setup({ props: { property: propertyForm, showDisable: true } }); expect(screen.getByDisplayValue('Inactive')).toBeInTheDocument(); }); it('shows Active as selected when isActive is true', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - mockFeatureSet.pimsFeature = {} as any; - mockFeatureSet.parcelFeature = {} as any; - mockFeatureSet.isActive = true; + const mockFeatureSet = getMockLocationFeatureDataset(); + mockFeatureSet.pimsFeatures = {} as any; + mockFeatureSet.parcelFeatures = {} as any; - await setup({ props: { property: mockFeatureSet, showDisable: true } }); + const propertyForm = PropertyForm.fromLocationFeatureDataset(mockFeatureSet); + propertyForm.isActive = 'true'; + + await setup({ props: { property: propertyForm, showDisable: true } }); expect(screen.getByDisplayValue('Active')).toBeInTheDocument(); }); // New tests for shapefile upload functionality it('renders upload button when canUploadShapefile is true', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - await setup({ props: { property: mockFeatureSet, canUploadShapefile: true } }); + const mockFeatureSet = getMockLocationFeatureDataset(); + await setup({ + props: { + property: PropertyForm.fromLocationFeatureDataset(mockFeatureSet), + canUploadShapefile: true, + }, + }); expect(screen.getByTestId('upload-shapefile-0')).toBeInTheDocument(); }); it('does not render upload button when canUploadShapefile is false or undefined', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - await setup({ props: { property: mockFeatureSet, canUploadShapefile: false } }); + const mockFeatureSet = getMockLocationFeatureDataset(); + await setup({ + props: { + property: PropertyForm.fromLocationFeatureDataset(mockFeatureSet), + canUploadShapefile: false, + }, + }); expect(screen.queryByTestId('upload-shapefile-0')).toBeNull(); }); it('opens ShapeUploadModal when upload button is clicked and passes property identifier, then calls onUploadShapefile on close', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); - mockFeatureSet.parcelFeature = {} as any; - mockFeatureSet.pimsFeature = { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '222-222-222', + const mockFeatureSet = getMockLocationFeatureDataset(); + mockFeatureSet.parcelFeatures = {} as any; + mockFeatureSet.pimsFeatures = [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0]?.properties, + PID_PADDED: '222-222-222', + }, }, - }; + ]; + + const propertyForm = PropertyForm.fromLocationFeatureDataset(mockFeatureSet); const onUploadShapefile = vi.fn(); await setup({ props: { - property: mockFeatureSet, + property: propertyForm, canUploadShapefile: true, onUploadShapefile, }, diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap b/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap index 39a8771d76..8ef4203bd6 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap @@ -6,7 +6,7 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` class="Toastify" />
- .c6.btn { + .c7.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -35,17 +35,17 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` cursor: pointer; } -.c6.btn .Button__value { +.c7.btn .Button__value { width: auto; } -.c6.btn:hover { +.c7.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c6.btn:focus { +.c7.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -53,31 +53,31 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` box-shadow: none; } -.c6.btn.btn-primary { +.c7.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c6.btn.btn-primary:hover, -.c6.btn.btn-primary:active, -.c6.btn.btn-primary:focus { +.c7.btn.btn-primary:hover, +.c7.btn.btn-primary:active, +.c7.btn.btn-primary:focus { background-color: #1E5189; } -.c6.btn.btn-secondary { +.c7.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c6.btn.btn-secondary:hover, -.c6.btn.btn-secondary:active, -.c6.btn.btn-secondary:focus { +.c7.btn.btn-secondary:hover, +.c7.btn.btn-secondary:active, +.c7.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c6.btn.btn-info { +.c7.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -85,66 +85,66 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` padding-right: 0.6rem; } -.c6.btn.btn-info:hover, -.c6.btn.btn-info:active, -.c6.btn.btn-info:focus { +.c7.btn.btn-info:hover, +.c7.btn.btn-info:active, +.c7.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c6.btn.btn-light { +.c7.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c6.btn.btn-light:hover, -.c6.btn.btn-light:active, -.c6.btn.btn-light:focus { +.c7.btn.btn-light:hover, +.c7.btn.btn-light:active, +.c7.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c6.btn.btn-dark { +.c7.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c6.btn.btn-dark:hover, -.c6.btn.btn-dark:active, -.c6.btn.btn-dark:focus { +.c7.btn.btn-dark:hover, +.c7.btn.btn-dark:active, +.c7.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c6.btn.btn-danger { +.c7.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c6.btn.btn-danger:hover, -.c6.btn.btn-danger:active, -.c6.btn.btn-danger:focus { +.c7.btn.btn-danger:hover, +.c7.btn.btn-danger:active, +.c7.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c6.btn.btn-warning { +.c7.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c6.btn.btn-warning:hover, -.c6.btn.btn-warning:active, -.c6.btn.btn-warning:focus { +.c7.btn.btn-warning:hover, +.c7.btn.btn-warning:active, +.c7.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c6.btn.btn-link { +.c7.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -168,9 +168,9 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` text-decoration: underline; } -.c6.btn.btn-link:hover, -.c6.btn.btn-link:active, -.c6.btn.btn-link:focus { +.c7.btn.btn-link:hover, +.c7.btn.btn-link:active, +.c7.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -180,15 +180,15 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` outline: none; } -.c6.btn.btn-link:disabled, -.c6.btn.btn-link.disabled { +.c7.btn.btn-link:disabled, +.c7.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c6.btn:disabled, -.c6.btn:disabled:hover { +.c7.btn:disabled, +.c7.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -200,66 +200,19 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` cursor: not-allowed; } -.c6.Button .Button__icon { +.c7.Button .Button__icon { margin-right: 1.6rem; } -.c6.Button--icon-only:focus { +.c7.Button--icon-only:focus { outline: none; } -.c6.Button--icon-only .Button__icon { +.c7.Button--icon-only .Button__icon { margin-right: 0; } -.c7.c7.btn { - background-color: unset; - border: none; -} - -.c7.c7.btn:hover, -.c7.c7.btn:focus, -.c7.c7.btn:active { - background-color: unset; - outline: none; - box-shadow: none; -} - -.c7.c7.btn svg { - -webkit-transition: all 0.3s ease-out; - transition: all 0.3s ease-out; -} - -.c7.c7.btn svg:hover { - -webkit-transition: all 0.3s ease-in; - transition: all 0.3s ease-in; -} - -.c7.c7.btn.btn-primary svg { - color: #013366; -} - -.c7.c7.btn.btn-primary svg:hover { - color: #013366; -} - -.c7.c7.btn.btn-light svg { - color: var(--surface-color-primary-button-default); -} - -.c7.c7.btn.btn-light svg:hover { - color: #CE3E39; -} - -.c7.c7.btn.btn-info svg { - color: var(--surface-color-primary-button-default); -} - -.c7.c7.btn.btn-info svg:hover { - color: var(--surface-color-primary-button-hover); -} - -.c9.c9.btn { +.c8.c8.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -267,13 +220,13 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` line-height: unset; } -.c9.c9.btn .text { +.c8.c8.btn .text { display: none; } -.c9.c9.btn:hover, -.c9.c9.btn:active, -.c9.c9.btn:focus { +.c8.c8.btn:hover, +.c8.c8.btn:active, +.c8.c8.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -287,9 +240,9 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` flex-direction: row; } -.c9.c9.btn:hover .text, -.c9.c9.btn:active .text, -.c9.c9.btn:focus .text { +.c8.c8.btn:hover .text, +.c8.c8.btn:active .text, +.c8.c8.btn:focus .text { display: inline; line-height: 2rem; } @@ -360,7 +313,7 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` justify-content: flex-end; } -.c8 { +.c6 { padding-left: 1.2rem; } @@ -490,35 +443,11 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = `
-
+
+ +
  • +
    + Select a property +
    - Selected Properties + Click on the map and the selection will be highlighed
    -
  • - + +
  • +
    + Add it to this file +
    +
    + Click "Add selected" property button when it appears below +
    +
  • + +
    + +
    +

    -
    +
    + Sub-interest file > renders as expe d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" /> - New workflow -
    -
    - - - - -
    +
    -
    -
      -
    1. -
      - Find a Property -
      -
      - Navigate to an area of the map OR use - -
      -
    2. -
    3. -
      - Select a property -
      -
      - Click on the map and the selection will be highlighed -
      -
    4. -
    5. -
      - Add it to this file -
      -
      - Click "Add selected" property button when it appears below -
      -
    6. -
    -
    +
    +

    +
    +
    -
    - Identifier -
    -
    +
    + Provide a descriptive name for this land + - Provide a descriptive name for this land - - - - - -
    + + +
    - - No Properties selected -
    + + No Properties selected +
    @@ -1866,7 +1878,7 @@ exports[`AddAcquisitionContainer component > Sub-interest file > renders as expe class="c16 required text-left col" >
    Sub-interest file > renders as expe class="c16 text-left col" >
    Sub-interest file > renders as expe class="col" >
    Bob Billy Smith
    -
    - + +
  • +
    + Select a property +
    +
    + Click on the map and the selection will be highlighed +
    +
  • +
  • +
    + Add it to this file +
    +
    + Click "Add selected" property button when it appears below +
    +
  • + +
    + +
    +

    -
    +
    + renders as expected 1`] = ` d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" /> - New workflow -
    -
    - - - - -
    +
    -
    -
      -
    1. -
      - Find a Property -
      -
      - Navigate to an area of the map OR use - -
      -
    2. -
    3. -
      - Select a property -
      -
      - Click on the map and the selection will be highlighed -
      -
    4. -
    5. -
      - Add it to this file -
      -
      - Click "Add selected" property button when it appears below -
      -
    6. -
    -
    +
    +
    +

    +
    +
    +
    + Identifier
    -
    - Identifier -
    -
    - Provide a descriptive name for this land - - - - - -
    + + +
    - - No Properties selected -
    + + No Properties selected +
    @@ -4749,7 +4773,7 @@ exports[`AddAcquisitionContainer component > renders as expected 1`] = ` class="c16 required text-left col" >
    renders as expected 1`] = ` class="c16 text-left col" >
    renders as expected 1`] = ` class="collapse show" >

    Each property in this file should be owned by the owner(s) in this section

    @@ -5134,7 +5158,7 @@ exports[`AddAcquisitionContainer component > renders as expected 1`] = ` class="col" >
    Select from contacts
    @@ -5208,7 +5232,7 @@ exports[`AddAcquisitionContainer component > renders as expected 1`] = ` class="col" >
    Select from contacts
    @@ -5295,7 +5319,7 @@ exports[`AddAcquisitionContainer component > renders as expected 1`] = ` class="" >
    Sub-interest files > renders as expected class="Toastify" />
    - .c16.btn { + .c15.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -35,17 +35,17 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected cursor: pointer; } -.c16.btn .Button__value { +.c15.btn .Button__value { width: auto; } -.c16.btn:hover { +.c15.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c16.btn:focus { +.c15.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -53,31 +53,31 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected box-shadow: none; } -.c16.btn.btn-primary { +.c15.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c16.btn.btn-primary:hover, -.c16.btn.btn-primary:active, -.c16.btn.btn-primary:focus { +.c15.btn.btn-primary:hover, +.c15.btn.btn-primary:active, +.c15.btn.btn-primary:focus { background-color: #1E5189; } -.c16.btn.btn-secondary { +.c15.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c16.btn.btn-secondary:hover, -.c16.btn.btn-secondary:active, -.c16.btn.btn-secondary:focus { +.c15.btn.btn-secondary:hover, +.c15.btn.btn-secondary:active, +.c15.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c16.btn.btn-info { +.c15.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -85,66 +85,66 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected padding-right: 0.6rem; } -.c16.btn.btn-info:hover, -.c16.btn.btn-info:active, -.c16.btn.btn-info:focus { +.c15.btn.btn-info:hover, +.c15.btn.btn-info:active, +.c15.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c16.btn.btn-light { +.c15.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c16.btn.btn-light:hover, -.c16.btn.btn-light:active, -.c16.btn.btn-light:focus { +.c15.btn.btn-light:hover, +.c15.btn.btn-light:active, +.c15.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c16.btn.btn-dark { +.c15.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c16.btn.btn-dark:hover, -.c16.btn.btn-dark:active, -.c16.btn.btn-dark:focus { +.c15.btn.btn-dark:hover, +.c15.btn.btn-dark:active, +.c15.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c16.btn.btn-danger { +.c15.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c16.btn.btn-danger:hover, -.c16.btn.btn-danger:active, -.c16.btn.btn-danger:focus { +.c15.btn.btn-danger:hover, +.c15.btn.btn-danger:active, +.c15.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c16.btn.btn-warning { +.c15.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c16.btn.btn-warning:hover, -.c16.btn.btn-warning:active, -.c16.btn.btn-warning:focus { +.c15.btn.btn-warning:hover, +.c15.btn.btn-warning:active, +.c15.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c16.btn.btn-link { +.c15.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -168,9 +168,9 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected text-decoration: underline; } -.c16.btn.btn-link:hover, -.c16.btn.btn-link:active, -.c16.btn.btn-link:focus { +.c15.btn.btn-link:hover, +.c15.btn.btn-link:active, +.c15.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -180,15 +180,15 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected outline: none; } -.c16.btn.btn-link:disabled, -.c16.btn.btn-link.disabled { +.c15.btn.btn-link:disabled, +.c15.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c16.btn:disabled, -.c16.btn:disabled:hover { +.c15.btn:disabled, +.c15.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -200,15 +200,15 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected cursor: not-allowed; } -.c16.Button .Button__icon { +.c15.Button .Button__icon { margin-right: 1.6rem; } -.c16.Button--icon-only:focus { +.c15.Button--icon-only:focus { outline: none; } -.c16.Button--icon-only .Button__icon { +.c15.Button--icon-only .Button__icon { margin-right: 0; } @@ -234,14 +234,14 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected border-color: #d8292f !important; } -.c19 { +.c18 { color: #474543; font-size: 1.6rem; -webkit-text-decoration: none; text-decoration: none; } -.c20 { +.c19 { position: relative; border-radius: 0.4rem; padding-top: 0.8rem; @@ -255,7 +255,7 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected cursor: pointer; } -.c20.is-invalid { +.c19.is-invalid { border: #CE3E39 solid 0.1rem; } @@ -285,7 +285,7 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected font-weight: bold; } -.c17 { +.c16 { margin: 0; padding: 1.6rem; font-size: 1.6rem; @@ -296,18 +296,18 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected font-family: 'BcSans-Bold'; } -.c8 { +.c7 { margin: 0 1.6rem 0 1.6rem; padding: 1.6rem; text-align: left; text-underline-offset: 2px; } -.c8 button { +.c7 button { font-size: 14px; } -.c9 { +.c8 { font-size: 16px; color: #1a5a96; font-style: italic; @@ -318,7 +318,7 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected margin-bottom: 1rem; } -.c13 { +.c12 { padding: 1.5rem 1.8rem; border-radius: 4px; background-color: #f0f7fc; @@ -330,42 +330,31 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected line-height: 24px; } -.c12 { +.c11 { float: right; cursor: pointer; color: #1a5a96; font-size: 2.8rem; } -.c11 { +.c10 { margin-right: 1.5rem; font-size: 1.3rem; } -.c10 { +.c9 { cursor: pointer; } -.c15 { +.c14 { font-weight: normal; } -.c14 { +.c13 { font-weight: bold; } -.c7 { - margin: 0 1.6rem 0 1.6rem; - padding: 0 1.6rem 0 1.6rem; - text-align: left; - text-underline-offset: 2px; -} - -.c7 button { - font-size: 14px; -} - -.c18 input.form-control { +.c17 input.form-control { min-width: 50rem; max-width: 100%; } @@ -1328,43 +1317,137 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected class="c7" >
    - Select one or more properties that you want to include in this acquisition. You can choose a location from the map, or search by other criteria. +
    +
    + + + + New workflow +
    +
    + + + + +
    +
    -

    -
    +
  • +
    + Find a Property +
    - Selected Properties + Navigate to an area of the map OR use +
    -
  • -

    + +
  • +
    + Select a property +
    +
    + Click on the map and the selection will be highlighed +
    +
  • +
  • +
    + Add it to this file +
    +
    + Click "Add selected" property button when it appears below +
    +
  • + +
    +
    +
    +

    -
    +
    + Sub-interest files > renders as expected d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" /> - New workflow -
    -
    - - - - -
    +
    -
    -
      -
    1. -
      - Find a Property -
      -
      - Navigate to an area of the map OR use - -
      -
    2. -
    3. -
      - Select a property -
      -
      - Click on the map and the selection will be highlighed -
      -
    4. -
    5. -
      - Add it to this file -
      -
      - Click "Add selected" property button when it appears below -
      -
    6. -
    -
    +
    +
    +

    +
    +
    +
    + Identifier
    -
    - Identifier -
    -
    - Provide a descriptive name for this land - - - - - -
    + + +
    - - No Properties selected -
    + + No Properties selected +
    @@ -1533,7 +1545,7 @@ exports[`AddAcquisitionForm component > Sub-interest files > renders as expected class="c4 required text-left col" >
    Sub-interest files > renders as expected class="c4 text-left col" >
    Sub-interest files > renders as expected class="collapse show" >
    -
    - + +
  • +
    + Select a property +
    +
    + Click on the map and the selection will be highlighed +
    +
  • +
  • +
    + Add it to this file +
    +
    + Click "Add selected" property button when it appears below +
    +
  • + +
    +
    +
    +

    -
    +
    + renders as expected 1`] = ` d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" /> - New workflow -
    -
    - - - - -
    +
    -
    -
      -
    1. -
      - Find a Property -
      -
      - Navigate to an area of the map OR use - -
      -
    2. -
    3. -
      - Select a property -
      -
      - Click on the map and the selection will be highlighed -
      -
    4. -
    5. -
      - Add it to this file -
      -
      - Click "Add selected" property button when it appears below -
      -
    6. -
    -
    +
    +

    +
    +
    -
    - Identifier -
    -
    +
    + Provide a descriptive name for this land + - Provide a descriptive name for this land - - - - - -
    + + +
    - - No Properties selected -
    + + No Properties selected +
    @@ -3716,7 +3740,7 @@ exports[`AddAcquisitionForm component > renders as expected 1`] = ` class="c4 required text-left col" >
    renders as expected 1`] = ` class="c4 text-left col" >
    renders as expected 1`] = ` class="collapse show" >
    -
    - + +
  • +
    + Select a property +
    +
    + Click on the map and the selection will be highlighed +
    +
  • +
  • +
    + Add it to this file +
    +
    + Click "Add selected" property button when it appears below +
    +
  • + +
    + +
    +

    -
    +
    + matches snapshot 1`] = ` d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" /> - New workflow -
    -
    - - - - -
    +
    -
    -
      -
    1. -
      - Find a Property -
      -
      - Navigate to an area of the map OR use - -
      -
    2. -
    3. -
      - Select a property -
      -
      - Click on the map and the selection will be highlighed -
      -
    4. -
    5. -
      - Add it to this file -
      -
      - Click "Add selected" property button when it appears below -
      -
    6. -
    -
    +
    +

    +
    +
    -
    - Identifier -
    -
    +
    + Provide a descriptive name for this land + - Provide a descriptive name for this land - - - - - -
    + + +
    - - No Properties selected -
    + + No Properties selected +
    @@ -1706,7 +1686,7 @@ exports[`Add Disposition Container View > matches snapshot 1`] = ` class="" >
    , } as unknown as ReturnType); +const setIsShowingPropertySelectorMock = vi.fn(); +const onUpdatePropertiesMock = vi.fn(); +const confirmBeforeAddPropertyMock = vi.fn(); +const canRemovePropertyMock = vi.fn(); + const history = createMemoryHistory(); describe('LeaseView component', () => { @@ -142,6 +149,10 @@ describe('LeaseView component', () => { fileProperties: getMockLeaseProperties(1), } } + setIsShowingPropertySelector={setIsShowingPropertySelectorMock} + onUpdateProperties={onUpdatePropertiesMock} + confirmBeforeAddProperty={confirmBeforeAddPropertyMock} + canRemoveProperty={canRemovePropertyMock} /> , { diff --git a/source/frontend/src/features/mapSideBar/management/ManagementContainer.test.tsx b/source/frontend/src/features/mapSideBar/management/ManagementContainer.test.tsx index 11431e93ef..f23f3bcc39 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementContainer.test.tsx @@ -29,6 +29,8 @@ import { SideBarContextProvider } from '../context/sidebarContext'; import { PropertyForm } from '../shared/models'; import ManagementContainer, { IManagementContainerProps } from './ManagementContainer'; import { IManagementViewProps } from './ManagementView'; +import ManagementForm from './form/ManagementForm'; +import { ManagementFormModel } from './models/ManagementFormModel'; const history = createMemoryHistory(); const mockAxios = new MockAdapter(axios); @@ -154,7 +156,7 @@ describe('ManagementContainer component', () => { await waitForElementToBeRemoved(spinner); await act(async () => { - await viewProps.onUpdateProperties(mockManagementFileApi); + await viewProps.updateFileProperties(ManagementFormModel.fromApi(mockManagementFileApi), []); }); expect(spinner).not.toBeVisible(); expect( @@ -186,7 +188,7 @@ describe('ManagementContainer component', () => { const errorMessage = 'You cannot add a property that is outside of your user account region'; mockAxios.onPut(new RegExp('managementfiles/1/properties')).reply(400, { error: errorMessage }); await act(async () => { - await viewProps.onUpdateProperties(mockManagementFileApi); + await viewProps.updateFileProperties(ManagementFormModel.fromApi(mockManagementFileApi), []); }); expect(spinner).not.toBeVisible(); expect(await screen.findByText(errorMessage)).toBeVisible(); @@ -333,7 +335,10 @@ describe('ManagementContainer component', () => { }); await act(async () => { - await viewProps.onUpdateProperties(mockManagementFileResponse()); + await viewProps.updateFileProperties( + ManagementFormModel.fromApi(mockManagementFileResponse()), + [], + ); }); expect( diff --git a/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx b/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx index a56a3afb96..3d44c1ce7b 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx @@ -50,7 +50,7 @@ const onClose = vi.fn(); const onSave = vi.fn(); const onCancel = vi.fn(); const onSuccess = vi.fn(); -const onUpdateProperties = vi.fn(); +const updateFileProperties = vi.fn(); const confirmBeforeAdd = vi.fn(); const canRemove = vi.fn(); const setIsEditing = vi.fn(); @@ -125,7 +125,7 @@ const DEFAULT_PROPS: IManagementViewProps = { onSelectProperty, onEditProperties, onSuccess, - onUpdateProperties, + updateFileProperties, confirmBeforeAdd, canRemove, isEditing: false, diff --git a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx index 1979e86ff9..e0fd529025 100644 --- a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainer.test.tsx @@ -4,7 +4,6 @@ import { createMemoryHistory } from 'history'; import { createRef } from 'react'; import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext'; -import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { useManagementFileRepository } from '@/hooks/repositories/useManagementFileRepository'; import { getMockFullyAttributedParcel } from '@/mocks/faParcelLayerResponse.mock'; import { getMockPolygon } from '@/mocks/geometries.mock'; @@ -25,6 +24,10 @@ import { PropertyForm } from '../../shared/models'; import { ManagementFormModel } from '../models/ManagementFormModel'; import AddManagementContainer, { IAddManagementContainerProps } from './AddManagementContainer'; import { IAddManagementContainerViewProps } from './AddManagementContainerView'; +import { + emptyFeatureDataset, + LocationFeatureDataset, +} from '@/components/common/mapFSM/useLocationFeatureLoader'; const history = createMemoryHistory(); @@ -106,7 +109,6 @@ describe('Add Management Container component', () => { it('calls onSuccess when the Management is saved successfully', async () => { const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, - processLocationFeaturesAddition: vi.fn(), refreshMapProperties: vi.fn(), }; await setup({ mockMapMachine: testMockMachine }); @@ -119,7 +121,6 @@ describe('Add Management Container component', () => { }); expect(onSuccess).toHaveBeenCalled(); - expect(testMockMachine.processLocationFeaturesAddition).toHaveBeenCalled(); expect(testMockMachine.refreshMapProperties).toHaveBeenCalled(); }); @@ -127,51 +128,36 @@ describe('Add Management Container component', () => { const testMockMachine: IMapStateMachineContext = { ...mapMachineBaseMock, }; - const selectedFeatures: SelectedFeatureDataset[] = [ + const selectedFeatures: LocationFeatureDataset[] = [ { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('111-111-111'), + parcelFeatures: [getMockFullyAttributedParcel('111-111-111')], regionFeature: feature(getMockPolygon(), { ...emptyRegion, REGION_NUMBER: 1, REGION_NAME: 'South Coast Region', }), - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, }, { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('222-222-222'), + parcelFeatures: [getMockFullyAttributedParcel('222-222-222')], regionFeature: feature(getMockPolygon(), { ...emptyRegion, REGION_NUMBER: 1, REGION_NAME: 'South Coast Region', }), - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, }, { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('333-333-333'), + parcelFeatures: [getMockFullyAttributedParcel('333-333-333')], regionFeature: feature(getMockPolygon(), { ...emptyRegion, REGION_NUMBER: 1, REGION_NAME: 'South Coast Region', }), - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, }, ]; await setup({ mockMapMachine: testMockMachine }); diff --git a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.test.tsx b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.test.tsx index 9f9b329ea9..a07895823f 100644 --- a/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/add/AddManagementContainerView.test.tsx @@ -56,7 +56,6 @@ describe('Add Management Container View', () => { onCancel={onCancel} onSave={onSave} onSubmit={onSubmit} - confirmBeforeAdd={confirmBeforeAdd} />, { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/management/add/__snapshots__/AddManagementContainerView.test.tsx.snap b/source/frontend/src/features/mapSideBar/management/add/__snapshots__/AddManagementContainerView.test.tsx.snap index a99127aef7..e11a096634 100644 --- a/source/frontend/src/features/mapSideBar/management/add/__snapshots__/AddManagementContainerView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/management/add/__snapshots__/AddManagementContainerView.test.tsx.snap @@ -333,7 +333,7 @@ exports[`Add Management Container View > matches snapshot 1`] = ` align-items: end; } -.c29 { +.c27 { position: -webkit-sticky; position: sticky; padding-top: 2rem; @@ -386,7 +386,7 @@ exports[`Add Management Container View > matches snapshot 1`] = ` font-weight: bold; } -.c27 { +.c26 { margin: 0; padding: 1.6rem; font-size: 1.6rem; @@ -397,18 +397,18 @@ exports[`Add Management Container View > matches snapshot 1`] = ` font-family: 'BcSans-Bold'; } -.c19 { +.c18 { margin: 0 1.6rem 0 1.6rem; padding: 1.6rem; text-align: left; text-underline-offset: 2px; } -.c19 button { +.c18 button { font-size: 14px; } -.c20 { +.c19 { font-size: 16px; color: #1a5a96; font-style: italic; @@ -419,7 +419,7 @@ exports[`Add Management Container View > matches snapshot 1`] = ` margin-bottom: 1rem; } -.c24 { +.c23 { padding: 1.5rem 1.8rem; border-radius: 4px; background-color: #f0f7fc; @@ -431,45 +431,30 @@ exports[`Add Management Container View > matches snapshot 1`] = ` line-height: 24px; } -.c23 { +.c22 { float: right; cursor: pointer; color: #1a5a96; font-size: 2.8rem; } -.c22 { +.c21 { margin-right: 1.5rem; font-size: 1.3rem; } -.c21 { +.c20 { cursor: pointer; } -.c26 { +.c25 { font-weight: normal; } -.c25 { +.c24 { font-weight: bold; } -.c18 { - margin: 0 1.6rem 0 1.6rem; - padding: 0 1.6rem 0 1.6rem; - text-align: left; - text-underline-offset: 2px; -} - -.c18 button { - font-size: 14px; -} - -.c28 { - margin: 0 1.6rem 0 1.6rem; -} - .c12 .form-section { margin: 0; padding-left: 0; @@ -830,43 +815,137 @@ exports[`Add Management Container View > matches snapshot 1`] = ` class="c18" >
    - Select one or more properties that you want to include in this management file. You can choose a location from the map, or search by other criteria. +
    +
    + + + + New workflow +
    +
    + + + + +
    +
    -

    -
    +
  • +
    + Find a Property +
    - Selected Properties + Navigate to an area of the map OR use +
    -
  • -

    + +
  • +
    + Select a property +
    +
    + Click on the map and the selection will be highlighed +
    +
  • +
  • +
    + Add it to this file +
    +
    + Click "Add selected" property button when it appears below +
    +
  • + +
    +
    +
    +

    -
    +
    + matches snapshot 1`] = ` d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" /> - New workflow -
    -
    - - - - -
    +
    -
    -
      -
    1. -
      - Find a Property -
      -
      - Navigate to an area of the map OR use - -
      -
    2. -
    3. -
      - Select a property -
      -
      - Click on the map and the selection will be highlighed -
      -
    4. -
    5. -
      - Add it to this file -
      -
      - Click "Add selected" property button when it appears below -
      -
    6. -
    -
    +
    +

    +
    +
    -
    - Identifier -
    -
    - Provide a descriptive name for this land - - - - - -
    + Identifier
    -
    - - No Properties selected - -
    + + + +
    + + No Properties selected +
    @@ -1209,7 +1209,7 @@ exports[`Add Management Container View > matches snapshot 1`] = ` class="" >
    >(null); let boundary: Geometry = null; if (exists(props.id)) { - boundary = firstOrNull(selectedFeatureData?.pimsFeatures)?.geometry; + boundary = firstOrNull(locationFeatureDataset?.pimsFeatures)?.geometry; } else if (exists(props.pid || props.pin)) { - boundary = firstOrNull(selectedFeatureData?.parcelFeatures)?.geometry; + boundary = firstOrNull(locationFeatureDataset?.parcelFeatures)?.geometry; } else if (exists(props.location?.lng) && exists(props.location?.lat)) { boundary = point([props.location?.lng, props.location?.lat])?.geometry; } diff --git a/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.test.tsx b/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.test.tsx index 99c894232d..ebd673a920 100644 --- a/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/MotiInventoryHeader.test.tsx @@ -9,6 +9,7 @@ import { act, render, RenderOptions, RenderResult } from '@/utils/test-utils'; import { ComposedProperty } from './ComposedProperty'; import { IMotiInventoryHeaderProps, MotiInventoryHeader } from './MotiInventoryHeader'; +import { emptyFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; const defaultComposedProperty: ComposedProperty = { pid: undefined, @@ -17,21 +18,12 @@ const defaultComposedProperty: ComposedProperty = { ltsaOrders: undefined, pimsProperty: undefined, propertyAssociations: undefined, - parcelMapFeatureCollection: undefined, - pimsGeoserverFeatureCollection: undefined, - bcAssessmentSummary: undefined, - crownTenureFeatures: undefined, + featureDataset: { + ...emptyFeatureDataset(), + }, planNumber: undefined, spcpOrder: undefined, - crownInclusionFeatures: undefined, - crownInventoryFeatures: undefined, - crownLeaseFeatures: undefined, - crownLicenseFeatures: undefined, - highwayFeatures: undefined, - municipalityFeatures: undefined, - firstNationFeatures: undefined, - alrFeatures: undefined, - electoralFeatures: undefined, + bcAssessmentSummary: undefined, }; vi.mock('@/hooks/repositories/useHistoricalNumberRepository'); @@ -143,9 +135,9 @@ describe('MotiInventoryHeader component', () => { const result = await setup({ composedProperty: { ...defaultComposedProperty, - pimsGeoserverFeatureCollection: { - type: 'FeatureCollection', - features: [testProperty], + featureDataset: { + ...emptyFeatureDataset(), + pimsFeatures: [testProperty], }, }, isLoading: false, diff --git a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx index 29b95226c3..b7e638fe72 100644 --- a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx @@ -24,6 +24,7 @@ import { SideBarContextProvider } from '../../context/sidebarContext'; import { useAddResearch } from '../hooks/useAddResearch'; import AddResearchContainer, { IAddResearchContainerProps } from './AddResearchContainer'; import AddResearchForm from './AddResearchForm'; +import { emptyFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; const mockStore = configureMockStore([thunk]); @@ -110,17 +111,12 @@ describe('AddResearchContainer component', () => { const { findByText } = await setup({ mockMapMachine: { ...mapMachineBaseMock, - selectedFeatures: [ + pendingLocationFeaturesAddition: true, + locationFeaturesForAddition: [ { + ...emptyFeatureDataset(), location: { lat: 0, lng: 0 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: selectedFeature, - regionFeature: null, - districtFeature: null, - selectingComponentId: null, - municipalityFeature: undefined, + parcelFeatures: [selectedFeature], }, ], }, @@ -169,7 +165,6 @@ describe('AddResearchContainer component', () => { await act(async () => userEvent.click(getSaveButton())); expect(onSuccess).toHaveBeenCalled(); - expect(testMockMachine.processLocationFeaturesAddition).toHaveBeenCalled(); expect(testMockMachine.refreshMapProperties).toHaveBeenCalled(); }); @@ -177,39 +172,22 @@ describe('AddResearchContainer component', () => { const { getNameTextbox, getSaveButton } = await setup({ mockMapMachine: { ...mapMachineBaseMock, - selectedFeatures: [ + pendingLocationFeaturesAddition: true, + locationFeaturesForAddition: [ { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('111-111-111'), - regionFeature: null, - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, + parcelFeatures: [getMockFullyAttributedParcel('111-111-111')], }, { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('222-222-222'), - regionFeature: null, - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, + parcelFeatures: [getMockFullyAttributedParcel('222-222-222')], }, { + ...emptyFeatureDataset(), location: { lng: -120.69195885, lat: 50.25163372 }, - fileLocation: null, - fileBoundary: null, - pimsFeature: null, - parcelFeature: getMockFullyAttributedParcel('333-333-333'), - regionFeature: null, - districtFeature: null, - selectingComponentId: null, - municipalityFeature: null, + parcelFeatures: [getMockFullyAttributedParcel('333-333-333')], }, ], }, diff --git a/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.test.tsx b/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.test.tsx index eadb74aed5..ba5bc6fe4a 100644 --- a/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/research/add/AddResearchForm.test.tsx @@ -32,9 +32,7 @@ describe('AddResearchForm component', () => { initialValues={renderOptions.initialValues} validationSchema={AddResearchFileYupSchema} > - {formikProps => ( - - )} + {formikProps => } , { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchContainer.test.tsx.snap index 879d652146..f0f97e60be 100644 --- a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchContainer.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchContainer.test.tsx.snap @@ -371,6 +371,17 @@ exports[`AddResearchContainer component > renders as expected 1`] = ` border-radius: 0.5rem; } +.c25 { + margin: 0; + padding: 1.6rem; + font-size: 1.6rem; + color: #9F9D9C; + border-bottom: 0.2rem solid #606060; + margin-bottom: 0.9rem; + padding-bottom: 0.25rem; + font-family: 'BcSans-Bold'; +} + .c15 { margin: 0 1.6rem 0 1.6rem; padding: 1.6rem; @@ -428,17 +439,6 @@ exports[`AddResearchContainer component > renders as expected 1`] = ` cursor: pointer; } -.c25 { - margin: 0; - padding: 1.6rem; - font-size: 1.6rem; - color: #9F9D9C; - border-bottom: 0.2rem solid #606060; - margin-bottom: 0.9rem; - padding-bottom: 0.25rem; - font-family: 'BcSans-Bold'; -} - .c24 { font-weight: normal; } @@ -892,7 +892,39 @@ exports[`AddResearchContainer component > renders as expected 1`] = `
    - Selected Properties +
    +
    + Selected Properties +
    +
    + + + + + +
    +
    diff --git a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchForm.test.tsx.snap index 5b6bdc7591..ca749b9cb0 100644 --- a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/AddResearchForm.test.tsx.snap @@ -227,6 +227,17 @@ exports[`AddResearchForm component > renders as expected 1`] = ` border-radius: 0.5rem; } +.c14 { + margin: 0; + padding: 1.6rem; + font-size: 1.6rem; + color: #9F9D9C; + border-bottom: 0.2rem solid #606060; + margin-bottom: 0.9rem; + padding-bottom: 0.25rem; + font-family: 'BcSans-Bold'; +} + .c3 { margin: 0 1.6rem 0 1.6rem; padding: 1.6rem; @@ -284,17 +295,6 @@ exports[`AddResearchForm component > renders as expected 1`] = ` cursor: pointer; } -.c14 { - margin: 0; - padding: 1.6rem; - font-size: 1.6rem; - color: #9F9D9C; - border-bottom: 0.2rem solid #606060; - margin-bottom: 0.9rem; - padding-bottom: 0.25rem; - font-family: 'BcSans-Bold'; -} - .c13 { font-weight: normal; } @@ -638,7 +638,39 @@ exports[`AddResearchForm component > renders as expected 1`] = `
    - Selected Properties +
    +
    + Selected Properties +
    +
    + + + + + +
    +
    diff --git a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.test.tsx b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.test.tsx index e34ee7bc9c..70490ee13a 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.test.tsx @@ -161,13 +161,13 @@ describe('PropertyFileContainer component', () => { .reply(200, getMockCrownTenuresLayerResponse()); await setup(); - expect(viewProps?.tabViews).toHaveLength(6); expect(viewProps?.tabViews[0].key).toBe(InventoryTabNames.title); expect(viewProps?.tabViews[1].key).toBe(InventoryTabNames.value); expect(viewProps?.tabViews[2].key).toBe(InventoryTabNames.property); expect(viewProps?.tabViews[3].key).toBe(InventoryTabNames.pims); expect(viewProps?.tabViews[4].key).toBe(InventoryTabNames.crown); expect(viewProps?.tabViews[5].key).toBe(InventoryTabNames.highway); + expect(viewProps?.tabViews).toHaveLength(6); }); it('does not call lease endpoints when user does not have lease permissions', async () => { diff --git a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx index 9c88a1edbe..474698d053 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx @@ -255,7 +255,6 @@ export const PropertyFileContainer: React.FunctionComponent< featureDataset?.crownLandInventoryFeatures?.length + featureDataset?.crownLandLeasesFeatures?.length + featureDataset?.crownLandLicensesFeatures?.length + - featureDataset?.crownLandLicensesFeatures?.length + featureDataset?.crownLandTenuresFeatures?.length > 0 ) { diff --git a/source/frontend/src/features/mapSideBar/shared/models.ts b/source/frontend/src/features/mapSideBar/shared/models.ts index 3454b4b204..035047509d 100644 --- a/source/frontend/src/features/mapSideBar/shared/models.ts +++ b/source/frontend/src/features/mapSideBar/shared/models.ts @@ -160,6 +160,7 @@ export class PropertyForm { pimsFeature?.properties?.LAND_LEGAL_DESCRIPTION ?? parcelFeature?.properties?.LEGAL_DESCRIPTION ?? '', + address: pimsFeature ? AddressForm.fromPimsView(pimsFeature?.properties) : undefined, }); } diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.test.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.test.tsx index 7b7c29109a..2d473ba31d 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/PropertiesListContainer.test.tsx @@ -1,4 +1,4 @@ -import { FormikProps } from 'formik'; +import { Formik, FormikProps } from 'formik'; import { createRef } from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -6,30 +6,27 @@ import thunk from 'redux-thunk'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { act, render, RenderOptions, userEvent } from '@/utils/test-utils'; -import { FileForm, PropertyForm } from '../../models'; -import { getMockSelectedFeatureDataset } from '@/mocks/featureset.mock'; +import { FileForm, PropertyForm, WithFormProperties } from '../../models'; import PropertiesListContainer from './PropertiesListContainer'; import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; +import { getMockLocationFeatureDataset } from '@/mocks/featureset.mock'; const mockStore = configureMockStore([thunk]); const customSetFilePropertyLocations = vi.fn(); const verifyCanRemove = vi.fn(); -const confirmBeforeAdd = vi.fn(); describe('PropertiesListContainer component', () => { const setup = async ( props: { properties: PropertyForm[] }, renderOptions: RenderOptions = {}, ) => { - const ref = createRef>(); + const ref = createRef>(); const utils = render( - , + + + , { ...renderOptions, store: mockStore({}), @@ -53,7 +50,7 @@ describe('PropertiesListContainer component', () => { let testForm: FileForm; beforeEach(() => { - const mockFeatureSet = getMockSelectedFeatureDataset(); + const mockFeatureSet = getMockLocationFeatureDataset(); testForm = new FileForm(); testForm.properties = [ PropertyForm.fromLocationFeatureDataset({ @@ -104,11 +101,12 @@ describe('PropertiesListContainer component', () => { }); it('should remove property from list when Remove button is clicked', async () => { - const { getAllByTitle, queryByText } = await setup({ properties: testForm.properties }); + const { getAllByTitle } = await setup({ properties: testForm.properties }); const pidRow = getAllByTitle('remove')[0]; await act(async () => userEvent.click(pidRow)); - expect(queryByText('PID: 123-456-789')).toBeNull(); + const theCallbackFn = expect.any(Function); // Expect a function + expect(verifyCanRemove).toHaveBeenCalledWith(testForm.properties[0].apiId, theCallbackFn); }); it('should display properties with svg prefixed with incrementing id', async () => { diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.test.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.test.tsx similarity index 76% rename from source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.test.tsx rename to source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.test.tsx index e00db37239..d128e81576 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.test.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdatePropertiesContainer.test.tsx @@ -25,6 +25,11 @@ import UpdatePropertiesContainer, { } from './UpdatePropertiesContainer'; import { IPropertiesListContainerProps } from './PropertiesListContainer'; import { emptyFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; +import { ResearchForm } from '@/features/mapSideBar/research/add/models'; +import React from 'react'; +import { FormikProps, getIn } from 'formik'; +import { PropertyForm, WithFormProperties } from '../../models'; +import { map } from 'lodash'; const mockAxios = new MockAdapter(axios); @@ -44,22 +49,24 @@ const setIsShowingPropertySelector = vi.fn(); const onSuccess = vi.fn(); const updateFileProperties = vi.fn(); -describe('UpdateProperties component', () => { +describe('UpdatePropertiesContainer component', () => { // render component under test const setup = async ( props: Partial, renderOptions: RenderOptions = {}, ) => { + const formikRef = React.createRef>(); const utils = render( , { @@ -76,6 +83,7 @@ describe('UpdateProperties component', () => { return { ...utils, + formikRef, }; }; @@ -96,7 +104,7 @@ describe('UpdateProperties component', () => { it('renders a row with an address', async () => { const { getByText } = await setup({ - file: { + formFile: ResearchForm.fromApi({ ...getMockResearchFile(), fileProperties: [ { @@ -149,9 +157,14 @@ describe('UpdateProperties component', () => { location: null, boundary: null, file: null, + isLegalOpinionRequired: false, + isLegalOpinionObtained: false, + documentReference: '', + researchSummary: '', + propertyResearchPurposeTypes: [], }, ], - }, + }), }); expect(getByText(/45 - 904 Ho/, { exact: false })).toBeVisible(); }); @@ -182,43 +195,55 @@ describe('UpdateProperties component', () => { it('should preserve the order of properties when saving', async () => { updateFileProperties.mockResolvedValue(getMockResearchFile()); - const { getByText } = await setup( + const { getByText, formikRef } = await setup({ + formFile: ResearchForm.fromApi({ + ...getMockResearchFile(), + fileProperties: [ + { + ...getMockApiPropertyFile(), + // existing property + property: { ...getMockApiProperty(), pid: 123456789, id: 1 }, + isLegalOpinionRequired: false, + isLegalOpinionObtained: false, + documentReference: '', + researchSummary: '', + propertyResearchPurposeTypes: [], + file: null, + }, + ], + }), + }); + + // properties to be added to the current file via the map state machine (ie working list, etc) + const formPropertiesForAddition = [ { - file: { - ...getMockResearchFile(), - fileProperties: [ - { - ...getMockApiPropertyFile(), - // existing property - property: { ...getMockApiProperty(), pid: 123456789, id: 1 }, - }, - ], - }, + ...emptyFeatureDataset(), + location: { lng: -120.69195885, lat: 50.25163372 }, + parcelFeatures: [getMockFullyAttributedParcel('111-111-111')], }, { - mockMapMachine: { - ...mapMachineBaseMock, - // properties to be added to the current file via the map state machine (ie working list, etc) - locationFeaturesForAddition: [ - { - ...emptyFeatureDataset(), - location: { lng: -120.69195885, lat: 50.25163372 }, - parcelFeatures: [getMockFullyAttributedParcel('111-111-111')], - }, - { - ...emptyFeatureDataset(), - location: { lng: -120.69195885, lat: 50.25163372 }, - parcelFeatures: [getMockFullyAttributedParcel('222-222-222')], - }, - { - ...emptyFeatureDataset(), - location: { lng: -120.69195885, lat: 50.25163372 }, - parcelFeatures: [getMockFullyAttributedParcel('333-333-333')], - }, - ], - }, + ...emptyFeatureDataset(), + location: { lng: -120.69195885, lat: 50.25163372 }, + parcelFeatures: [getMockFullyAttributedParcel('222-222-222')], }, - ); + { + ...emptyFeatureDataset(), + location: { lng: -120.69195885, lat: 50.25163372 }, + parcelFeatures: [getMockFullyAttributedParcel('333-333-333')], + }, + ].map(x => PropertyForm.fromLocationFeatureDataset(x)); + + const fieldName = 'properties'; + const existingProperties = getIn(formikRef?.current?.values, fieldName) ?? []; + + await act(async () => { + formikRef.current?.setFieldValue(fieldName, [ + ...existingProperties, + ...formPropertiesForAddition, + ]); + await formikRef.current?.setFieldTouched(fieldName, true); + }); + const saveButton = getByText('Save'); await act(async () => userEvent.click(saveButton)); @@ -227,23 +252,11 @@ describe('UpdateProperties component', () => { expect(updateFileProperties).toHaveBeenCalledWith( expect.objectContaining({ - fileProperties: expect.arrayContaining([ - expect.objectContaining({ - property: expect.objectContaining({ id: 1 }), - displayOrder: 0, - }), - expect.objectContaining({ - property: expect.objectContaining({ pid: 111111111 }), - displayOrder: 1, - }), - expect.objectContaining({ - property: expect.objectContaining({ pid: 222222222 }), - displayOrder: 2, - }), - expect.objectContaining({ - property: expect.objectContaining({ pid: 333333333 }), - displayOrder: 3, - }), + properties: expect.arrayContaining([ + expect.objectContaining({ id: 1 }), + expect.objectContaining({ pid: '111-111-111' }), + expect.objectContaining({ pid: '222-222-222' }), + expect.objectContaining({ pid: '333-333-333' }), ]), }), expect.any(Array), diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/PropertiesListContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/PropertiesListContainer.test.tsx.snap new file mode 100644 index 0000000000..92dcf39401 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/PropertiesListContainer.test.tsx.snap @@ -0,0 +1,1152 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PropertiesListContainer component > renders as expected 1`] = ` + +
    +
    + .c8.btn { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding: 0.4rem 1.2rem; + border: 0.2rem solid transparent; + border-radius: 0.4rem; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + font-size: 1.8rem; + font-family: 'BCSans','Noto Sans',Verdana,Arial,sans-serif; + font-weight: 700; + -webkit-letter-spacing: 0.1rem; + -moz-letter-spacing: 0.1rem; + -ms-letter-spacing: 0.1rem; + letter-spacing: 0.1rem; + cursor: pointer; +} + +.c8.btn .Button__value { + width: auto; +} + +.c8.btn:hover { + -webkit-text-decoration: underline; + text-decoration: underline; + opacity: 0.8; +} + +.c8.btn:focus { + outline-width: 2px; + outline-style: solid; + outline-color: #2E5DD7; + outline-offset: 2px; + box-shadow: none; +} + +.c8.btn.btn-primary { + color: #FFFFFF; + background-color: #013366; +} + +.c8.btn.btn-primary:hover, +.c8.btn.btn-primary:active, +.c8.btn.btn-primary:focus { + background-color: #1E5189; +} + +.c8.btn.btn-secondary { + color: #013366; + background: none; + border-color: #013366; +} + +.c8.btn.btn-secondary:hover, +.c8.btn.btn-secondary:active, +.c8.btn.btn-secondary:focus { + color: #FFFFFF; + background-color: #013366; +} + +.c8.btn.btn-info { + color: #9F9D9C; + border: none; + background: none; + padding-left: 0.6rem; + padding-right: 0.6rem; +} + +.c8.btn.btn-info:hover, +.c8.btn.btn-info:active, +.c8.btn.btn-info:focus { + color: var(--surface-color-primary-button-hover); + background: none; +} + +.c8.btn.btn-light { + color: #FFFFFF; + background-color: #606060; + border: none; +} + +.c8.btn.btn-light:hover, +.c8.btn.btn-light:active, +.c8.btn.btn-light:focus { + color: #FFFFFF; + background-color: #606060; +} + +.c8.btn.btn-dark { + color: #FFFFFF; + background-color: #474543; + border: none; +} + +.c8.btn.btn-dark:hover, +.c8.btn.btn-dark:active, +.c8.btn.btn-dark:focus { + color: #FFFFFF; + background-color: #474543; +} + +.c8.btn.btn-danger { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c8.btn.btn-danger:hover, +.c8.btn.btn-danger:active, +.c8.btn.btn-danger:focus { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c8.btn.btn-warning { + color: #FFFFFF; + background-color: #FCBA19; + border-color: #FCBA19; +} + +.c8.btn.btn-warning:hover, +.c8.btn.btn-warning:active, +.c8.btn.btn-warning:focus { + color: #FFFFFF; + border-color: #FCBA19; + background-color: #FCBA19; +} + +.c8.btn.btn-link { + font-size: 1.6rem; + font-weight: 400; + color: var(--surface-color-primary-button-default); + background: none; + border: none; + -webkit-text-decoration: none; + text-decoration: none; + min-height: 2.5rem; + line-height: 3rem; + -webkit-box-pack: left; + -webkit-justify-content: left; + -ms-flex-pack: left; + justify-content: left; + -webkit-letter-spacing: unset; + -moz-letter-spacing: unset; + -ms-letter-spacing: unset; + letter-spacing: unset; + text-align: left; + padding: 0; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c8.btn.btn-link:hover, +.c8.btn.btn-link:active, +.c8.btn.btn-link:focus { + color: var(--surface-color-primary-button-hover); + -webkit-text-decoration: underline; + text-decoration: underline; + border: none; + background: none; + box-shadow: none; + outline: none; +} + +.c8.btn.btn-link:disabled, +.c8.btn.btn-link.disabled { + color: #9F9D9C; + background: none; + pointer-events: none; +} + +.c8.btn:disabled, +.c8.btn:disabled:hover { + color: #9F9D9C; + background-color: #EDEBE9; + box-shadow: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + cursor: not-allowed; +} + +.c8.Button .Button__icon { + margin-right: 1.6rem; +} + +.c8.Button--icon-only:focus { + outline: none; +} + +.c8.Button--icon-only .Button__icon { + margin-right: 0; +} + +.c0 { + margin: 0 1.6rem 0 1.6rem; + padding: 1.6rem; + text-align: left; + text-underline-offset: 2px; +} + +.c0 button { + font-size: 14px; +} + +.c1 { + font-size: 16px; + color: #1a5a96; + font-style: italic; + font-weight: bold; + -webkit-text-decoration: none solid rgb(26,90,150); + text-decoration: none solid rgb(26,90,150); + line-height: 24px; + margin-bottom: 1rem; +} + +.c5 { + padding: 1.5rem 1.8rem; + border-radius: 4px; + background-color: #f0f7fc; + margin-bottom: 2rem; + font-size: 14px; + color: #053662; + -webkit-text-decoration: none solid rgb(49,49,50); + text-decoration: none solid rgb(49,49,50); + line-height: 24px; +} + +.c4 { + float: right; + cursor: pointer; + color: #1a5a96; + font-size: 2.8rem; +} + +.c3 { + margin-right: 1.5rem; + font-size: 1.3rem; +} + +.c2 { + cursor: pointer; +} + +.c7 { + font-weight: normal; +} + +.c6 { + font-weight: bold; +} + +
    +
    +
    +
    + + + + New workflow +
    +
    + + + + +
    +
    +
    +
    +
      +
    1. +
      + Find a Property +
      +
      + Navigate to an area of the map OR use + +
      +
    2. +
    3. +
      + Select a property +
      +
      + Click on the map and the selection will be highlighed +
      +
    4. +
    5. +
      + Add it to this file +
      +
      + Click "Add selected" property button when it appears below +
      +
    6. +
    +
    +
    + .c2.btn { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding: 0.4rem 1.2rem; + border: 0.2rem solid transparent; + border-radius: 0.4rem; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + font-size: 1.8rem; + font-family: 'BCSans','Noto Sans',Verdana,Arial,sans-serif; + font-weight: 700; + -webkit-letter-spacing: 0.1rem; + -moz-letter-spacing: 0.1rem; + -ms-letter-spacing: 0.1rem; + letter-spacing: 0.1rem; + cursor: pointer; +} + +.c2.btn .Button__value { + width: auto; +} + +.c2.btn:hover { + -webkit-text-decoration: underline; + text-decoration: underline; + opacity: 0.8; +} + +.c2.btn:focus { + outline-width: 2px; + outline-style: solid; + outline-color: #2E5DD7; + outline-offset: 2px; + box-shadow: none; +} + +.c2.btn.btn-primary { + color: #FFFFFF; + background-color: #013366; +} + +.c2.btn.btn-primary:hover, +.c2.btn.btn-primary:active, +.c2.btn.btn-primary:focus { + background-color: #1E5189; +} + +.c2.btn.btn-secondary { + color: #013366; + background: none; + border-color: #013366; +} + +.c2.btn.btn-secondary:hover, +.c2.btn.btn-secondary:active, +.c2.btn.btn-secondary:focus { + color: #FFFFFF; + background-color: #013366; +} + +.c2.btn.btn-info { + color: #9F9D9C; + border: none; + background: none; + padding-left: 0.6rem; + padding-right: 0.6rem; +} + +.c2.btn.btn-info:hover, +.c2.btn.btn-info:active, +.c2.btn.btn-info:focus { + color: var(--surface-color-primary-button-hover); + background: none; +} + +.c2.btn.btn-light { + color: #FFFFFF; + background-color: #606060; + border: none; +} + +.c2.btn.btn-light:hover, +.c2.btn.btn-light:active, +.c2.btn.btn-light:focus { + color: #FFFFFF; + background-color: #606060; +} + +.c2.btn.btn-dark { + color: #FFFFFF; + background-color: #474543; + border: none; +} + +.c2.btn.btn-dark:hover, +.c2.btn.btn-dark:active, +.c2.btn.btn-dark:focus { + color: #FFFFFF; + background-color: #474543; +} + +.c2.btn.btn-danger { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c2.btn.btn-danger:hover, +.c2.btn.btn-danger:active, +.c2.btn.btn-danger:focus { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c2.btn.btn-warning { + color: #FFFFFF; + background-color: #FCBA19; + border-color: #FCBA19; +} + +.c2.btn.btn-warning:hover, +.c2.btn.btn-warning:active, +.c2.btn.btn-warning:focus { + color: #FFFFFF; + border-color: #FCBA19; + background-color: #FCBA19; +} + +.c2.btn.btn-link { + font-size: 1.6rem; + font-weight: 400; + color: var(--surface-color-primary-button-default); + background: none; + border: none; + -webkit-text-decoration: none; + text-decoration: none; + min-height: 2.5rem; + line-height: 3rem; + -webkit-box-pack: left; + -webkit-justify-content: left; + -ms-flex-pack: left; + justify-content: left; + -webkit-letter-spacing: unset; + -moz-letter-spacing: unset; + -ms-letter-spacing: unset; + letter-spacing: unset; + text-align: left; + padding: 0; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c2.btn.btn-link:hover, +.c2.btn.btn-link:active, +.c2.btn.btn-link:focus { + color: var(--surface-color-primary-button-hover); + -webkit-text-decoration: underline; + text-decoration: underline; + border: none; + background: none; + box-shadow: none; + outline: none; +} + +.c2.btn.btn-link:disabled, +.c2.btn.btn-link.disabled { + color: #9F9D9C; + background: none; + pointer-events: none; +} + +.c2.btn:disabled, +.c2.btn:disabled:hover { + color: #9F9D9C; + background-color: #EDEBE9; + box-shadow: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + cursor: not-allowed; +} + +.c2.Button .Button__icon { + margin-right: 1.6rem; +} + +.c2.Button--icon-only:focus { + outline: none; +} + +.c2.Button--icon-only .Button__icon { + margin-right: 0; +} + +.c11.c11.btn { + font-size: 1.4rem; + color: #aaaaaa; + -webkit-text-decoration: none; + text-decoration: none; + line-height: unset; +} + +.c11.c11.btn .text { + display: none; +} + +.c11.c11.btn:hover, +.c11.c11.btn:active, +.c11.c11.btn:focus { + color: #d8292f; + -webkit-text-decoration: none; + text-decoration: none; + opacity: unset; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c11.c11.btn:hover .text, +.c11.c11.btn:active .text, +.c11.c11.btn:focus .text { + display: inline; + line-height: 2rem; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: baseline; + -webkit-box-align: baseline; + -ms-flex-align: baseline; + align-items: baseline; + gap: 0.8rem; +} + +.c8 .form-label { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c1 { + font-weight: bold; + color: var(--theme-blue-100); + border-bottom: 0.2rem var(--theme-blue-90) solid; + margin-bottom: 2.4rem; +} + +.c0 { + margin: 1.6rem; + padding: 1.6rem; + background-color: white; + text-align: left; + border-radius: 0.5rem; +} + +.c3 { + margin: 0; + padding: 1.6rem; + font-size: 1.6rem; + color: #9F9D9C; + border-bottom: 0.2rem solid #606060; + margin-bottom: 0.9rem; + padding-bottom: 0.25rem; + font-family: 'BcSans-Bold'; +} + +.c7 { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + margin: 0.15rem; + witdh: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + word-break: break-all; + line-break: break-all; +} + +.c5 { + min-width: 3rem; + min-height: 4.5rem; +} + +.c6 { + font-size: 1.2rem; +} + +.c4 { + min-height: 4.5rem; +} + +.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c10 { + padding-left: 1.2rem; +} + +
    +

    +
    +
    +
    +
    + Selected Properties +
    +
    + +
    +
    +
    +
    +

    +
    +
    +
    + Identifier +
    +
    + Provide a descriptive name for this land + + + + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + 1 + + + + 1 + + +
    + PID: 123-456-789 +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + 2 + + + + 2 + + +
    + PIN: 1111222 +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +`; diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap b/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap index 0954714743..099425343a 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap @@ -207,54 +207,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` margin-right: 0; } -.c27.c27.btn { - background-color: unset; - border: none; -} - -.c27.c27.btn:hover, -.c27.c27.btn:focus, -.c27.c27.btn:active { - background-color: unset; - outline: none; - box-shadow: none; -} - -.c27.c27.btn svg { - -webkit-transition: all 0.3s ease-out; - transition: all 0.3s ease-out; -} - -.c27.c27.btn svg:hover { - -webkit-transition: all 0.3s ease-in; - transition: all 0.3s ease-in; -} - -.c27.c27.btn.btn-primary svg { - color: #013366; -} - -.c27.c27.btn.btn-primary svg:hover { - color: #013366; -} - -.c27.c27.btn.btn-light svg { - color: var(--surface-color-primary-button-default); -} - -.c27.c27.btn.btn-light svg:hover { - color: #CE3E39; -} - -.c27.c27.btn.btn-info svg { - color: var(--surface-color-primary-button-default); -} - -.c27.c27.btn.btn-info svg:hover { - color: var(--surface-color-primary-button-hover); -} - -.c29.c29.btn { +.c28.c28.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -262,13 +215,13 @@ exports[`UpdateProperties component > renders as expected 1`] = ` line-height: unset; } -.c29.c29.btn .text { +.c28.c28.btn .text { display: none; } -.c29.c29.btn:hover, -.c29.c29.btn:active, -.c29.c29.btn:focus { +.c28.c28.btn:hover, +.c28.c28.btn:active, +.c28.c28.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -282,9 +235,9 @@ exports[`UpdateProperties component > renders as expected 1`] = ` flex-direction: row; } -.c29.c29.btn:hover .text, -.c29.c29.btn:active .text, -.c29.c29.btn:focus .text { +.c28.c28.btn:hover .text, +.c28.c28.btn:active .text, +.c28.c28.btn:focus .text { display: inline; line-height: 2rem; } @@ -329,84 +282,6 @@ exports[`UpdateProperties component > renders as expected 1`] = ` border-bottom: solid 0.5rem #3470B1; } -.c19 { - font-weight: bold; - color: var(--theme-blue-100); - border-bottom: 0.2rem var(--theme-blue-90) solid; - margin-bottom: 2.4rem; -} - -.c18 { - margin: 1.6rem; - padding: 1.6rem; - background-color: white; - text-align: left; - border-radius: 0.5rem; -} - -.c20 { - margin: 0; - padding: 1.6rem; - font-size: 1.6rem; - color: #9F9D9C; - border-bottom: 0.2rem solid #606060; - margin-bottom: 0.9rem; - padding-bottom: 0.25rem; - font-family: 'BcSans-Bold'; -} - -.c24 { - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; - margin: 0.15rem; - witdh: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - word-break: break-all; - line-break: break-all; -} - -.c22 { - min-width: 3rem; - min-height: 4.5rem; -} - -.c23 { - font-size: 1.2rem; -} - -.c21 { - min-height: 4.5rem; -} - -.c26 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; -} - -.c28 { - padding-left: 1.2rem; -} - .c5.btn.btn-light.Button { padding: 0; border: 0.1rem solid #9F9D9C; @@ -489,7 +364,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` align-items: end; } -.c30 { +.c29 { position: -webkit-sticky; position: sticky; padding-top: 2rem; @@ -499,6 +374,84 @@ exports[`UpdateProperties component > renders as expected 1`] = ` z-index: 10; } +.c19 { + font-weight: bold; + color: var(--theme-blue-100); + border-bottom: 0.2rem var(--theme-blue-90) solid; + margin-bottom: 2.4rem; +} + +.c18 { + margin: 1.6rem; + padding: 1.6rem; + background-color: white; + text-align: left; + border-radius: 0.5rem; +} + +.c20 { + margin: 0; + padding: 1.6rem; + font-size: 1.6rem; + color: #9F9D9C; + border-bottom: 0.2rem solid #606060; + margin-bottom: 0.9rem; + padding-bottom: 0.25rem; + font-family: 'BcSans-Bold'; +} + +.c24 { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + margin: 0.15rem; + witdh: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + word-break: break-all; + line-break: break-all; +} + +.c22 { + min-width: 3rem; + min-height: 4.5rem; +} + +.c23 { + font-size: 1.2rem; +} + +.c21 { + min-height: 4.5rem; +} + +.c26 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c27 { + padding-left: 1.2rem; +} + .c10 { margin: 0 1.6rem 0 1.6rem; padding: 1.6rem; @@ -800,13 +753,6 @@ exports[`UpdateProperties component > renders as expected 1`] = `
    -
    -
    -
    @@ -972,35 +918,11 @@ exports[`UpdateProperties component > renders as expected 1`] = `
    -
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + New workflow +
    +
    + + + + +
    +
    +
    +
    +
      +
    1. +
      + Find a Property +
      +
      + Navigate to an area of the map OR use + + +
      +
    2. +
    3. +
      + Select a property +
      +
      + Click on the map and the selection will be highlighed +
      +
    4. +
    5. +
      + Add it to this file +
      +
      + Click "Add selected" property button when it appears below +
      +
    6. +
    +
    +
    +
    +

    +
    +
    +
    +
    + Selected Properties +
    +
    + +
    +
    +
    +
    +

    +
    +
    +
    + Identifier +
    +
    + Provide a descriptive name for this land + + + + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + 1 + + + + 1 + + +
    + PID: 123-456-789 +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +`; diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.test.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.test.tsx index 7dd355a804..e5b2107ae1 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.test.tsx @@ -20,6 +20,7 @@ import AddSubdivisionContainer from './AddSubdivisionContainer'; import { ApiGen_Concepts_PropertyOperation } from '@/models/api/generated/ApiGen_Concepts_PropertyOperation'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { Input } from '@/components/common/form'; +import { PropertyForm } from '../shared/models'; const history = createMemoryHistory(); @@ -137,7 +138,7 @@ describe('Add Subdivision Container component', () => { mockAddPropertyOperation.execute.mockResolvedValue([{}]); const model = new SubdivisionFormModel(); - model.destinationProperties = [{} as ApiGen_Concepts_Property]; + model.destinationProperties = [new PropertyForm()]; await act(async () => { viewProps?.onSubmit(model, {} as any); }); @@ -148,10 +149,12 @@ describe('Add Subdivision Container component', () => { it('Calls onSuccess when the submit operation completes successfully when the response contains a viable property to navigate', async () => { await setup({}); - mockAddPropertyOperation.execute.mockResolvedValue([{}]); + mockAddPropertyOperation.execute.mockResolvedValue([{ sourceProperty: { id: 1 } }]); const model = new SubdivisionFormModel(); - model.destinationProperties = [{} as ApiGen_Concepts_Property]; - model.sourceProperty = { id: 1 } as ApiGen_Concepts_Property; + model.destinationProperties = [new PropertyForm()]; + model.sourceProperty = new PropertyForm(); + model.sourceProperty.id = 1; + await act(async () => { viewProps?.onSubmit(model, {} as any); }); diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx index 4fb0d5ca8c..83c7233328 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionContainer.tsx @@ -138,7 +138,7 @@ const AddSubdivisionContainer: React.FC = ({ const response = await addPropertyOperation(propertyOperations, userOverrideCodes); if (response?.length) { - handleSuccess(propertyOperations); + handleSuccess(response); } } finally { mapMachine.processLocationFeaturesAddition(); diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts index 49f155469d..f96c5dd0a5 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionModel.ts @@ -24,8 +24,8 @@ export class SubdivisionFormModel { ...getEmptyBaseAudit(0), id: 0, sourcePropertyId: this.sourceProperty?.id ?? 0, - sourceProperty: this.sourceProperty.toApi(), - destinationProperty: dp.toApi(), + sourceProperty: this.sourceProperty?.toApi(), + destinationProperty: dp?.toApi(), destinationPropertyId: dp?.id ?? 0, operationDt: null, isDisabled: false, diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx index 57a6fa47cd..f57e93875c 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.test.tsx @@ -5,13 +5,15 @@ import { createRef } from 'react'; import { IMapSelectorContainerProps } from '@/components/propertySelector/MapSelectorContainer'; import { PropertySelectorPidSearchContainerProps } from '@/components/propertySelector/search/PropertySelectorPidSearchContainer'; import Claims from '@/constants/claims'; -import { getMockSelectedFeatureDataset } from '@/mocks/getMockSelectedFeatureDataset'; import { mockLookups } from '@/mocks/lookups.mock'; import { getMockApiProperty } from '@/mocks/properties.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes/lookupCodesSlice'; import { act, render, RenderOptions, screen, userEvent } from '@/utils/test-utils'; import { SubdivisionFormModel } from './AddSubdivisionModel'; import AddSubdivisionView, { IAddSubdivisionViewProps } from './AddSubdivisionView'; +import { PropertyForm } from '../shared/models'; +import { getMockLocationFeatureDataset } from '@/mocks/featureset.mock'; +import { pidFormatter } from '@/utils'; const history = createMemoryHistory(); @@ -93,7 +95,7 @@ describe('Add Subdivision View', () => { }); it('calls getPrimaryAddressByPid when destination property is activated', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); + const mockFeatureSet = getMockLocationFeatureDataset(); await setup(); await act(async () => { mapSelectorProps.addSelectedProperties([ @@ -101,9 +103,9 @@ describe('Add Subdivision View', () => { ...mockFeatureSet, pimsFeatures: [ { - ...mockFeatureSet.pimsFeatures, + ...mockFeatureSet.pimsFeatures[0], properties: { - ...mockFeatureSet.pimsFeature?.properties, + ...mockFeatureSet.pimsFeatures[0]?.properties, PID_PADDED: '123-456-789', }, }, @@ -115,19 +117,21 @@ describe('Add Subdivision View', () => { }); it('does not call for address if property has no pid', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); + const mockFeatureSet = getMockLocationFeatureDataset(); await setup(); await act(async () => { mapSelectorProps.addSelectedProperties([ { ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: undefined, + pimsFeatures: [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0]?.properties, + PID_PADDED: undefined, + }, }, - }, + ], }, ]); }); @@ -141,13 +145,16 @@ describe('Add Subdivision View', () => { await act(async () => { pidSelectorProps.setSelectProperty(property); }); - const text = await screen.findByText(property.pid?.toString() ?? ''); + const text = await screen.findByText(pidFormatter(property?.pid.toString())); expect(text).toBeVisible(); }); it('selected source property can be removed', async () => { const initialFormModel = new SubdivisionFormModel(); - initialFormModel.sourceProperty = { ...getMockApiProperty(), pid: 111111111 }; + initialFormModel.sourceProperty = PropertyForm.fromPropertyApi({ + ...getMockApiProperty(), + pid: 111111111, + }); initialFormModel.destinationProperties = []; const { getByTitle, queryByText } = await setup({ @@ -165,7 +172,9 @@ describe('Add Subdivision View', () => { it('selected destination properties can be removed', async () => { const initialFormModel = new SubdivisionFormModel(); - initialFormModel.destinationProperties = [{ ...getMockApiProperty(), pid: 111111111 }]; + initialFormModel.destinationProperties = [ + PropertyForm.fromPropertyApi({ ...getMockApiProperty(), pid: 111111111 }), + ]; const { getByTitle, queryByText } = await setup({ props: { @@ -181,7 +190,7 @@ describe('Add Subdivision View', () => { }); it('property area only has at most 4 digits', async () => { - const mockFeatureSet = getMockSelectedFeatureDataset(); + const mockFeatureSet = getMockLocationFeatureDataset(); const initialFormModel = new SubdivisionFormModel(); getPrimaryAddressByPid.mockImplementation(() => Promise.resolve(undefined)); @@ -195,14 +204,16 @@ describe('Add Subdivision View', () => { mapSelectorProps.addSelectedProperties([ { ...mockFeatureSet, - pimsFeature: { - ...mockFeatureSet.pimsFeature, - properties: { - ...mockFeatureSet.pimsFeature?.properties, - PID_PADDED: '123-456-789', - LAND_AREA: 1.12345, + pimsFeatures: [ + { + ...mockFeatureSet.pimsFeatures[0], + properties: { + ...mockFeatureSet.pimsFeatures[0]?.properties, + PID_PADDED: '123-456-789', + LAND_AREA: 1.12345, + }, }, - }, + ], }, ]); }); diff --git a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx index 580f6d2909..8acc264182 100644 --- a/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx +++ b/source/frontend/src/features/mapSideBar/subdivision/AddSubdivisionView.tsx @@ -163,11 +163,7 @@ const AddSubdivisionView: React.FunctionComponent< const formProperty = PropertyForm.fromLocationFeatureDataset(property); formProperty.landArea = formProperty.landArea && formProperty.areaUnit - ? convertArea( - formProperty.landArea, - formProperty.areaUnit.toLocaleLowerCase(), - ApiGen_CodeTypes_AreaUnitTypes.M2, - ) + ? getAreaValue(formProperty.landArea, formProperty.areaUnit) : 0; formProperty.areaUnit = ApiGen_CodeTypes_AreaUnitTypes.M2; if (formProperty.pid) { diff --git a/source/frontend/src/features/properties/parcelList/ParcelItem.test.tsx b/source/frontend/src/features/properties/parcelList/ParcelItem.test.tsx index 85aee9cf25..59ed3e3659 100644 --- a/source/frontend/src/features/properties/parcelList/ParcelItem.test.tsx +++ b/source/frontend/src/features/properties/parcelList/ParcelItem.test.tsx @@ -15,9 +15,7 @@ describe('ParcelItem component', () => { const setup = (renderOptions: RenderOptions & { props?: Partial } = {}) => { return render( { [ NameSourceType.PID, '123-456-789', - getMockWorklistParcel('parcel-1', { PID: '123456789' }), + getMockWorklistParcel({ PID: '123456789' }), 'PID: 123-456-789', ], - [ - NameSourceType.PIN, - '99999999', - getMockWorklistParcel('parcel-1', { PIN: 99999999 }), - 'PIN: 99999999', - ], + [NameSourceType.PIN, '99999999', getMockWorklistParcel({ PIN: 99999999 }), 'PIN: 99999999'], [ NameSourceType.PLAN, 'SP-54321', - getMockWorklistParcel('parcel-1', { PLAN_NUMBER: 'SP-54321' }), + getMockWorklistParcel({ PLAN_NUMBER: 'SP-54321' }), 'Plan #: SP-54321', ], [ NameSourceType.LOCATION, '-123.100000, 49.250000', - getMockWorklistParcel('parcel-1', {}, { lat: 49.25, lng: -123.1 }), + getMockWorklistParcel({}, { lat: 49.25, lng: -123.1 }), '-123.100000, 49.250000', ], // no prefix ])('renders %s as "%s"', (_, __, mockParcel, expected) => { @@ -63,8 +56,8 @@ describe('ParcelItem component', () => { it('calls onRemove when remove button is clicked', async () => { setup(); - const removeButton = screen.getByTestId('delete-list-parcel-parcel-1'); + const removeButton = screen.getByTestId('delete-list-parcel-0-0'); await act(async () => userEvent.click(removeButton)); - expect(onRemove).toHaveBeenCalledWith('parcel-1'); + expect(onRemove).toHaveBeenCalledWith('0-0'); }); }); diff --git a/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx b/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx index 3c0e7fb638..8ca49c64fb 100644 --- a/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistContainer.test.tsx @@ -55,8 +55,8 @@ describe('WorklistContainer', () => { // clear the array in place (without assigning a new empty array instance) mockParcels.length = 0; mockParcels.push( - getMockWorklistParcel('parcel-1', {}, { lat: 49.0, lng: -123.0 }), - getMockWorklistParcel('parcel-2', {}, { lat: 50.0, lng: -122.0 }), + getMockWorklistParcel({}, { lat: 49.0, lng: -123.0 }), + getMockWorklistParcel({}, { lat: 50.0, lng: -122.0 }), ); }); diff --git a/source/frontend/src/features/properties/worklist/WorklistView.test.tsx b/source/frontend/src/features/properties/worklist/WorklistView.test.tsx index 73f29f7c7e..fa55396186 100644 --- a/source/frontend/src/features/properties/worklist/WorklistView.test.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistView.test.tsx @@ -21,9 +21,7 @@ describe('WorklistView', () => { const setup = (renderOptions: RenderOptions & { props?: Partial } = {}) => { return render( { const { asFragment } = setup({ props: { parcels: [ - getMockWorklistParcel('parcel-1', { PID: '123456789' }), - getMockWorklistParcel('parcel-2', { PIN: 99999999 }), + getMockWorklistParcel({ PID: '123456789' }, { lat: 1, lng: 1 }), + getMockWorklistParcel({ PIN: 99999999 }, { lat: 2, lng: 2 }), ], }, }); diff --git a/source/frontend/src/features/properties/worklist/WorklistView.tsx b/source/frontend/src/features/properties/worklist/WorklistView.tsx index 3da7d9f8cc..9123aa656a 100644 --- a/source/frontend/src/features/properties/worklist/WorklistView.tsx +++ b/source/frontend/src/features/properties/worklist/WorklistView.tsx @@ -129,7 +129,7 @@ export const WorklistView: React.FC = ({ {parcels.map((p, index) => ( matches snapshot with multiple parcels 1`] = `