From 07d66f5ed7ff8159549ef8536f11e36b708ff7a9 Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:09:25 -0800 Subject: [PATCH 01/10] Psp 9405 allow layers to be refreshed programatically. (#4439) * psp-9405 allow updates that cause changes to map layers to be refreshed * formatting corrections. * mock correction. * code review corrections. --- .../common/mapFSM/MapStateMachineContext.tsx | 11 +++++++++++ .../mapFSM/machineDefinition/mapMachine.ts | 7 +++++++ .../common/mapFSM/machineDefinition/types.ts | 1 + .../maps/leaflet/Control/LayersControl/data.ts | 4 +++- .../leaflet/Layers/LeafletLayerListener.tsx | 18 +++++++++++++++++- .../detail/AcquisitionSummaryView.test.tsx | 11 ++++++++++- source/frontend/src/mocks/mapFSM.mock.ts | 2 ++ 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index ac7d4ae11a..e8e3bcf40e 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -46,6 +46,7 @@ export interface IMapStateMachineContext { showDisposed: boolean; showRetired: boolean; activeLayers: ILayerItem[]; + mapLayersToRefresh: ILayerItem[]; requestFlyToLocation: (latlng: LatLngLiteral) => void; requestFlyToBounds: (bounds: LatLngBounds) => void; @@ -74,6 +75,7 @@ export interface IMapStateMachineContext { toggleMapLayerControl: () => void; setFilePropertyLocations: (locations: LatLngLiteral[]) => void; setMapLayers: (layers: ILayerItem[]) => void; + setMapLayersToRefresh: (layers: ILayerItem[]) => void; setDefaultMapLayers: (layers: ILayerItem[]) => void; setVisiblePimsProperties: (propertyIds: number[]) => void; @@ -299,6 +301,13 @@ export const MapStateMachineProvider: React.FC> [serviceSend], ); + const setMapLayersToRefresh = useCallback( + (refreshLayers: ILayerItem[]) => { + serviceSend({ type: 'SET_REFRESH_MAP_LAYERS', refreshLayers }); + }, + [serviceSend], + ); + const setDefaultMapLayers = useCallback( (activeLayers: ILayerItem[]) => { serviceSend({ type: 'DEFAULT_MAP_LAYERS', activeLayers }); @@ -397,6 +406,7 @@ export const MapStateMachineProvider: React.FC> activePimsPropertyIds: state.context.activePimsPropertyIds, showDisposed: state.context.showDisposed, showRetired: state.context.showRetired, + mapLayersToRefresh: state.context.mapLayersToRefresh, setMapSearchCriteria, refreshMapProperties, @@ -422,6 +432,7 @@ export const MapStateMachineProvider: React.FC> setShowDisposed, setShowRetired, setMapLayers, + setMapLayersToRefresh, setDefaultMapLayers, setFullWidthSideBar, resetMapFilter, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index d5712e3e6d..86deff2731 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -2,6 +2,7 @@ import { latLngBounds } from 'leaflet'; import { assign, createMachine, raise, send } from 'xstate'; import { defaultBounds } from '@/components/maps/constants'; +import { PIMS_PROPERTY_BOUNDARY_KEY } from '@/components/maps/leaflet/Control/LayersControl/data'; import { defaultPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; import { emptyFeatureData } from '../models'; @@ -84,6 +85,9 @@ const featureDataLoaderStates = { }), target: 'loading', }, + SET_REFRESH_MAP_LAYERS: { + actions: assign({ mapLayersToRefresh: (_, event: any) => event.refreshLayers }), + }, }, }, loading: { @@ -97,6 +101,7 @@ const featureDataLoaderStates = { isLoading: () => false, mapFeatureData: (_, event: any) => event.data, fitToResultsAfterLoading: () => false, + mapLayersToRefresh: () => [{ key: PIMS_PROPERTY_BOUNDARY_KEY }], }), raise('REQUEST_FIT_BOUNDS'), ], @@ -108,6 +113,7 @@ const featureDataLoaderStates = { isLoading: () => false, mapFeatureData: (_, event: any) => event.data, fitToResultsAfterLoading: () => false, + mapLayersToRefresh: () => [{ key: PIMS_PROPERTY_BOUNDARY_KEY }], }), ], target: 'idle', @@ -437,6 +443,7 @@ export const mapMachine = createMachine({ showDisposed: false, showRetired: false, activeLayers: [], + mapLayersToRefresh: [], }, // State definitions diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index af746a3f3b..5be71ae232 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -43,6 +43,7 @@ export type MachineContext = { filePropertyLocations: LatLngLiteral[]; activePimsPropertyIds: number[]; activeLayers: ILayerItem[]; + mapLayersToRefresh: ILayerItem[]; isFiltering: boolean; showDisposed: boolean; showRetired: boolean; diff --git a/source/frontend/src/components/maps/leaflet/Control/LayersControl/data.ts b/source/frontend/src/components/maps/leaflet/Control/LayersControl/data.ts index 76e5a88741..a03e849944 100644 --- a/source/frontend/src/components/maps/leaflet/Control/LayersControl/data.ts +++ b/source/frontend/src/components/maps/leaflet/Control/LayersControl/data.ts @@ -2,6 +2,8 @@ import { MAP_MAX_NATIVE_ZOOM, MAP_MAX_ZOOM } from '@/constants/strings'; import { ILayerItem } from './types'; +export const PIMS_PROPERTY_BOUNDARY_KEY = 'PIMS_PROPERTY_BOUNDARY_KEY'; + export const layersTree: ILayerItem[] = [ { key: 'Administrative Boundaries', @@ -357,7 +359,7 @@ export const layersTree: ILayerItem[] = [ on: false, nodes: [ { - key: 'pims_properties', + key: 'PIMS_PROPERTY_BOUNDARY_KEY', label: 'Property Boundaries', on: true, layers: 'psp:PIMS_PROPERTY_BOUNDARY_VW', diff --git a/source/frontend/src/components/maps/leaflet/Layers/LeafletLayerListener.tsx b/source/frontend/src/components/maps/leaflet/Layers/LeafletLayerListener.tsx index c77a4a5376..1e9757d1a0 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/LeafletLayerListener.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/LeafletLayerListener.tsx @@ -4,13 +4,14 @@ import { useEffect } from 'react'; import { useMap } from 'react-leaflet'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; import { exists } from '@/utils'; import { wmsHeaders } from '../Control/LayersControl/wmsHeaders'; const featureGroup = new L.FeatureGroup(); export const LeafletLayerListener = () => { - const { activeLayers } = useMapStateMachine(); + const { activeLayers, mapLayersToRefresh, setMapLayersToRefresh } = useMapStateMachine(); const mapInstance = useMap(); useEffect(() => { @@ -23,6 +24,21 @@ export const LeafletLayerListener = () => { }; }, [mapInstance]); + useDeepCompareEffect(() => { + if (mapLayersToRefresh?.length) { + const currentLayers = featureGroup.getLayers().filter(exists); + mapLayersToRefresh.forEach(configLayer => { + const currentLayer = currentLayers.find(l => (l as any).options.key === configLayer.key); + + if (currentLayer) { + featureGroup.removeLayer(currentLayer); + featureGroup.addLayer(currentLayer); + } + }); + setMapLayersToRefresh([]); + } + }, [mapInstance, mapLayersToRefresh, setMapLayersToRefresh]); + useEffect(() => { if (mapInstance) { const currentLayers = featureGroup.getLayers().filter(exists); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.test.tsx index 1eff856b28..64981d85c4 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.test.tsx @@ -9,7 +9,16 @@ import { getEmptyPerson } from '@/mocks/contacts.mock'; import { getEmptyOrganization } from '@/mocks/organization.mock'; import { ApiGen_Concepts_Person } from '@/models/api/generated/ApiGen_Concepts_Person'; import { toTypeCodeNullable } from '@/utils/formUtils'; -import { act, cleanup, render, RenderOptions, userEvent, waitForEffects } from '@/utils/test-utils'; +import { + act, + cleanup, + findAllByTestId, + findByTestId, + render, + RenderOptions, + userEvent, + waitForEffects, +} from '@/utils/test-utils'; import AcquisitionSummaryView, { IAcquisitionSummaryViewProps } from './AcquisitionSummaryView'; diff --git a/source/frontend/src/mocks/mapFSM.mock.ts b/source/frontend/src/mocks/mapFSM.mock.ts index ffddca46de..d56d099b1d 100644 --- a/source/frontend/src/mocks/mapFSM.mock.ts +++ b/source/frontend/src/mocks/mapFSM.mock.ts @@ -50,6 +50,7 @@ export const mapMachineBaseMock: IMapStateMachineContext = { isShowingMapLayers: false, showDisposed: false, showRetired: false, + mapLayersToRefresh: [], requestFlyToLocation: vi.fn(), @@ -79,4 +80,5 @@ export const mapMachineBaseMock: IMapStateMachineContext = { toggleSidebarDisplay: vi.fn(), setFullWidthSideBar: vi.fn(), resetMapFilter: vi.fn(), + setMapLayersToRefresh: vi.fn(), }; From 99ffd2c27c60164956ed641bc50134a98b97319b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:09:52 +0000 Subject: [PATCH 02/10] CI: Bump version to v5.6.0-92.31 --- source/backend/api/Pims.Api.csproj | 4 ++-- source/frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index fbcfa20f09..f7dec1e9e9 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,8 +2,8 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.6.0-92.30 - 5.6.0-92.30 + 5.6.0-92.31 + 5.6.0-92.31 5.6.0.92 true 16BC0468-78F6-4C91-87DA-7403C919E646 diff --git a/source/frontend/package.json b/source/frontend/package.json index b99a652bb9..192d464174 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.6.0-92.30", + "version": "5.6.0-92.31", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 99f7f5f2e50e1836955702f5cdbc44fa46add09a Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:51:15 -0800 Subject: [PATCH 03/10] =?UTF-8?q?psp-9436,=20require=20the=20user=20to=20c?= =?UTF-8?q?hange=20the=20advanced=20filter=20for=20it=20to=20be=E2=80=A6?= =?UTF-8?q?=20(#4434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * psp-9436, require the user to change the advanced filter for it to be applied. requires tests. * psp-9436 change advanced filter so that the default filter is "always on". - Update point clusterer to not cluster properties that are unable to display a marker. * re-sort dependencies --------- Co-authored-by: Smith --- .../common/mapFSM/MapStateMachineContext.tsx | 24 ++++- .../mapFSM/machineDefinition/mapMachine.ts | 12 ++- .../common/mapFSM/machineDefinition/types.ts | 2 + .../FilterContentContainer.test.tsx | 27 ++---- .../AdvancedFilter/FilterContentContainer.tsx | 41 +++----- .../maps/leaflet/Layers/PointClusterer.tsx | 42 +++----- .../components/maps/leaflet/Layers/util.tsx | 6 +- .../properties/map/MapContainer.test.tsx | 96 ++++++++++++++----- .../features/properties/map/MapContainer.tsx | 28 +++++- source/frontend/src/mocks/mapFSM.mock.ts | 4 + 10 files changed, 173 insertions(+), 109 deletions(-) diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index e8e3bcf40e..3617677836 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -1,8 +1,10 @@ import { useInterpret, useSelector } from '@xstate/react'; +import { dequal } from 'dequal'; import { LatLngBounds, LatLngLiteral } from 'leaflet'; import React, { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; +import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types'; import { IGeoSearchParams } from '@/constants/API'; import { IMapSideBarViewState } from '@/features/mapSideBar/MapSideBar'; @@ -47,6 +49,8 @@ export interface IMapStateMachineContext { showRetired: boolean; activeLayers: ILayerItem[]; mapLayersToRefresh: ILayerItem[]; + advancedSearchCriteria: PropertyFilterFormModel; + isMapVisible: boolean; requestFlyToLocation: (latlng: LatLngLiteral) => void; requestFlyToBounds: (bounds: LatLngBounds) => void; @@ -83,6 +87,7 @@ export interface IMapStateMachineContext { setShowRetired: (show: boolean) => void; setFullWidthSideBar: (fullWidth: boolean) => void; resetMapFilter: () => void; + setAdvancedSearchCriteria: (advancedSearchCriteria: PropertyFilterFormModel) => void; } const MapStateMachineContext = React.createContext( @@ -252,6 +257,13 @@ export const MapStateMachineProvider: React.FC> [serviceSend], ); + const setAdvancedSearchCriteria = useCallback( + (advancedSearchCriteria: PropertyFilterFormModel) => { + serviceSend({ type: 'SET_ADVANCED_SEARCH_CRITERIA', advancedSearchCriteria }); + }, + [serviceSend], + ); + const prepareForCreation = useCallback(() => { serviceSend({ type: 'PREPARE_FOR_CREATION' }); }, [serviceSend]); @@ -380,7 +392,12 @@ export const MapStateMachineProvider: React.FC> > showPopup: showPopup, isLoading: state.context.isLoading, mapSearchCriteria: state.context.searchCriteria, + advancedSearchCriteria: state.context.advancedSearchCriteria, mapFeatureData: state.context.mapFeatureData, filePropertyLocations: state.context.filePropertyLocations, pendingFitBounds: state.matches({ mapVisible: { mapRequest: 'pendingFitBounds' } }), @@ -399,7 +417,7 @@ export const MapStateMachineProvider: React.FC> isSelecting: state.matches({ mapVisible: { featureView: 'selecting' } }), isRepositioning: isRepositioning, selectingComponentId: state.context.selectingComponentId, - isFiltering: state.context.isFiltering, + isFiltering: !dequal(state.context.advancedSearchCriteria, new PropertyFilterFormModel()), isShowingMapFilter: isShowingMapFilter, isShowingMapLayers: isShowingMapLayers, activeLayers: state.context.activeLayers, @@ -407,6 +425,7 @@ export const MapStateMachineProvider: React.FC> showDisposed: state.context.showDisposed, showRetired: state.context.showRetired, mapLayersToRefresh: state.context.mapLayersToRefresh, + isMapVisible: state.matches({ mapVisible: {} }), setMapSearchCriteria, refreshMapProperties, @@ -436,6 +455,7 @@ export const MapStateMachineProvider: React.FC> setDefaultMapLayers, setFullWidthSideBar, resetMapFilter, + setAdvancedSearchCriteria, }} > {children} diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index 86deff2731..57033e12f7 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -2,6 +2,7 @@ import { latLngBounds } from 'leaflet'; import { assign, createMachine, raise, send } from 'xstate'; import { defaultBounds } from '@/components/maps/constants'; +import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { PIMS_PROPERTY_BOUNDARY_KEY } from '@/components/maps/leaflet/Control/LayersControl/data'; import { defaultPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; @@ -373,7 +374,6 @@ const advancedFilterSideBarStates = { }, }, mapFilterOpened: { - entry: [assign({ isFiltering: () => true })], on: { TOGGLE_FILTER: { target: 'closed', @@ -381,8 +381,10 @@ const advancedFilterSideBarStates = { TOGGLE_LAYERS: { target: 'layerControl', }, - SET_VISIBLE_PROPERTIES: { - actions: assign({ activePimsPropertyIds: (_, event: any) => event.propertyIds }), + SET_ADVANCED_SEARCH_CRITERIA: { + actions: assign({ + advancedSearchCriteria: (_, event: any) => event.advancedSearchCriteria, + }), }, SET_SHOW_DISPOSED: { actions: assign({ showDisposed: (_, event: any) => event.show }), @@ -436,6 +438,7 @@ export const mapMachine = createMachine({ isLoading: false, fitToResultsAfterLoading: false, searchCriteria: null, + advancedSearchCriteria: new PropertyFilterFormModel(), mapFeatureData: emptyFeatureData, filePropertyLocations: [], activePimsPropertyIds: [], @@ -496,6 +499,9 @@ export const mapMachine = createMachine({ DEFAULT_MAP_LAYERS: { actions: assign({ activeLayers: (_, event: any) => event.activeLayers }), }, + SET_VISIBLE_PROPERTIES: { + actions: assign({ activePimsPropertyIds: (_, event: any) => event.propertyIds }), + }, }, states: { featureView: featureViewStates, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index 5be71ae232..1fcdefc1dc 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -1,5 +1,6 @@ import { LatLngBounds, LatLngLiteral } from 'leaflet'; +import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types'; import { IMapSideBarViewState as IMapSideBarState } from '@/features/mapSideBar/MapSideBar'; import { IPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; @@ -35,6 +36,7 @@ export type MachineContext = { // TODO: this is partially in the URL. Either move it completly there or remove it searchCriteria: IPropertyFilter | null; + advancedSearchCriteria: PropertyFilterFormModel | null; isLoading: boolean; fitToResultsAfterLoading: boolean; diff --git a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.test.tsx b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.test.tsx index 7551f7ead4..20734c5f07 100644 --- a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.test.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.test.tsx @@ -16,20 +16,6 @@ const storeState = { [lookupCodesSlice.name]: { lookupCodes: mockLookups }, }; -const mockGetApi = { - error: undefined, - response: [1] as number[] | undefined, - execute: vi.fn().mockResolvedValue([1]), - loading: false, -}; -vi.mock('@/hooks/repositories/usePimsPropertyRepository', () => ({ - usePimsPropertyRepository: () => { - return { - getMatchingProperties: mockGetApi, - }; - }, -})); - describe('FilterContentContainer component', () => { let viewProps: IFilterContentFormProps; @@ -46,7 +32,7 @@ describe('FilterContentContainer component', () => { ...renderOptions, store: storeState, history, - mockMapMachine: { ...mapMachineBaseMock, isFiltering: true }, + mockMapMachine: { ...mapMachineBaseMock, isFiltering: false, isShowingMapFilter: true }, }); return { @@ -62,12 +48,13 @@ describe('FilterContentContainer component', () => { vi.clearAllMocks(); }); - it('fetches filter data from the api', async () => { - mockGetApi.execute.mockResolvedValue([1, 2]); + it('fetches filter data from the api if filter changed', async () => { setup({}); - await act(async () => viewProps.onChange(new PropertyFilterFormModel())); - expect(mockGetApi.execute).toHaveBeenCalledWith(new PropertyFilterFormModel().toApi()); - expect(mapMachineBaseMock.setVisiblePimsProperties).toHaveBeenCalledWith([1, 2]); + const filter = new PropertyFilterFormModel(); + filter.isRetired = true; + + await act(async () => viewProps.onChange(filter)); + expect(mapMachineBaseMock.setAdvancedSearchCriteria).toHaveBeenCalledWith(filter); }); it(`resets the map filter state when "onReset" is called`, async () => { diff --git a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx index 31c43801a5..e746dcef3f 100644 --- a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx @@ -1,8 +1,6 @@ import React, { useCallback } from 'react'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { usePimsPropertyRepository } from '@/hooks/repositories/usePimsPropertyRepository'; -import { Api_PropertyFilterCriteria } from '@/models/api/ProjectFilterCriteria'; import { IFilterContentFormProps } from './FilterContentForm'; import { PropertyFilterFormModel } from './models'; @@ -12,42 +10,27 @@ export interface IFilterContentContainerProps { } export const FilterContentContainer: React.FC = ({ View }) => { - const { isFiltering, setVisiblePimsProperties, setShowDisposed, setShowRetired, resetMapFilter } = - useMapStateMachine(); - - const { getMatchingProperties } = usePimsPropertyRepository(); - - const matchProperties = getMatchingProperties.execute; - - const filterProperties = useCallback( - async (filter: Api_PropertyFilterCriteria) => { - const retrievedProperties = await matchProperties(filter); - - if (retrievedProperties !== undefined) { - setVisiblePimsProperties(retrievedProperties); - } - }, - [matchProperties, setVisiblePimsProperties], - ); + const { + setShowDisposed, + setShowRetired, + resetMapFilter, + setAdvancedSearchCriteria, + isShowingMapFilter, + isLoading, + } = useMapStateMachine(); const onChange = useCallback( async (model: PropertyFilterFormModel) => { - await filterProperties(model.toApi()); + setAdvancedSearchCriteria(model); setShowDisposed(model.isDisposed); setShowRetired(model.isRetired); }, - [filterProperties, setShowDisposed, setShowRetired], + [setShowDisposed, setShowRetired, setAdvancedSearchCriteria], ); // Only render if the map state is filtering. - if (isFiltering) { - return ( - - ); + if (isShowingMapFilter) { + return ; } else { return <>; } diff --git a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx index 090f6c8028..a7a0d0a04b 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx @@ -18,11 +18,12 @@ import { PIMS_Property_Boundary_View, PIMS_Property_Location_View, } from '@/models/layers/pimsPropertyLocationView'; +import { exists } from '@/utils'; import { ONE_HUNDRED_METER_PRECISION } from '../../constants'; import SinglePropertyMarker from '../Markers/SingleMarker'; import { Spiderfier, SpiderSet } from './Spiderfier'; -import { getDraftIcon, pointToLayer, zoomToCluster } from './util'; +import { getDraftIcon, getMarkerIcon, pointToLayer, zoomToCluster } from './util'; export type PointClustererProps = { bounds?: BBox; @@ -100,38 +101,23 @@ export const PointClusterer: React.FC = useMemo(() => { - if (mapMachine.isFiltering && mapMachine.mapFeatureData.pimsLocationFeatures !== null) { - let filteredFeatures = mapMachine.mapFeatureData.pimsLocationFeatures.features.filter(x => - mapMachine.activePimsPropertyIds.includes(Number(x.properties.PROPERTY_ID)), - ); - - // allow clustering of retired properties when advanced filter is open - if (!mapMachine.showRetired) { - filteredFeatures = filteredFeatures.filter(x => !x.properties.IS_RETIRED); - } + let filteredFeatures = mapMachine.mapFeatureData.pimsLocationFeatures.features.filter(x => + mapMachine.activePimsPropertyIds.includes(Number(x.properties.PROPERTY_ID)), + ); - return { - type: mapMachine.mapFeatureData.pimsLocationFeatures.type, - features: filteredFeatures, - }; - } else { - if (mapMachine.mapFeatureData.pimsLocationFeatures !== null) { - // By default, all properties that are marked as retired, are not displayed on the map, regardless of other states on the property - const filteredFeatures = mapMachine.mapFeatureData.pimsLocationFeatures.features.filter( - x => !x.properties.IS_RETIRED, - ); + if (!mapMachine.showRetired) { + filteredFeatures = filteredFeatures.filter(x => !x.properties.IS_RETIRED); + } - return { - type: mapMachine.mapFeatureData.pimsLocationFeatures.type, - features: filteredFeatures, - }; - } + // Do not cluster any points that do not have markers on the map. + const displayableFeatures = filteredFeatures.filter(f => exists(getMarkerIcon(f, false))); - return mapMachine.mapFeatureData.pimsLocationFeatures; - } + return { + type: mapMachine.mapFeatureData.pimsLocationFeatures.type, + features: displayableFeatures, + }; }, [ mapMachine.activePimsPropertyIds, - mapMachine.isFiltering, mapMachine.mapFeatureData.pimsLocationFeatures, mapMachine.showRetired, ]); diff --git a/source/frontend/src/components/maps/leaflet/Layers/util.tsx b/source/frontend/src/components/maps/leaflet/Layers/util.tsx index 7ae8f38d91..d8beb25384 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/util.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/util.tsx @@ -1,4 +1,4 @@ -import { GeoJsonProperties } from 'geojson'; +import { Feature, GeoJsonProperties, Geometry } from 'geojson'; import L, { DivIcon, GeoJSON, LatLngExpression, Layer, Map, Marker } from 'leaflet'; import ReactDOMServer from 'react-dom/server'; import Supercluster from 'supercluster'; @@ -169,7 +169,9 @@ export function pointToLayer

, + feature: + | Supercluster.PointFeature + | Feature, selected: boolean, showDisposed = false, showRetired = false, diff --git a/source/frontend/src/features/properties/map/MapContainer.test.tsx b/source/frontend/src/features/properties/map/MapContainer.test.tsx index 601c90e4c0..10f1388f13 100644 --- a/source/frontend/src/features/properties/map/MapContainer.test.tsx +++ b/source/frontend/src/features/properties/map/MapContainer.test.tsx @@ -35,6 +35,7 @@ import { useApiProperties } from '@/hooks/pims-api/useApiProperties'; import { ApiGen_Base_Page } from '@/models/api/generated/ApiGen_Base_Page'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import MapContainer from './MapContainer'; +import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; const mockAxios = new MockAdapter(axios); @@ -87,17 +88,17 @@ interface ParcelSeed { } export const largeMockParcels: ParcelSeed[] = [ - { id: 1, latitude: 53.917061, longitude: -122.749672 }, - { id: 2, latitude: 53.917062, longitude: -122.749692 }, - { id: 3, latitude: 53.917063, longitude: -122.749682 }, - { id: 4, latitude: 53.917064, longitude: -122.749672 }, - { id: 5, latitude: 53.917065, longitude: -122.749662 }, - { id: 6, latitude: 53.917066, longitude: -122.749652 }, - { id: 7, latitude: 53.917067, longitude: -122.749642 }, - { id: 8, latitude: 53.917068, longitude: -122.749632 }, - { id: 9, latitude: 53.917069, longitude: -122.749622 }, - { id: 10, latitude: 53.917071, longitude: -122.749612 }, - { id: 11, latitude: 53.918172, longitude: -122.749772 }, + { id: 1, latitude: 53.917061, longitude: -122.749672, propertyId: 1 }, + { id: 2, latitude: 53.917062, longitude: -122.749692, propertyId: 2 }, + { id: 3, latitude: 53.917063, longitude: -122.749682, propertyId: 3 }, + { id: 4, latitude: 53.917064, longitude: -122.749672, propertyId: 4 }, + { id: 5, latitude: 53.917065, longitude: -122.749662, propertyId: 5 }, + { id: 6, latitude: 53.917066, longitude: -122.749652, propertyId: 6 }, + { id: 7, latitude: 53.917067, longitude: -122.749642, propertyId: 7 }, + { id: 8, latitude: 53.917068, longitude: -122.749632, propertyId: 8 }, + { id: 9, latitude: 53.917069, longitude: -122.749622, propertyId: 9 }, + { id: 10, latitude: 53.917071, longitude: -122.749612, propertyId: 10 }, + { id: 11, latitude: 53.918172, longitude: -122.749772, propertyId: 11 }, ]; export const distantMockParcels: ParcelSeed[] = [ @@ -122,23 +123,38 @@ export const createPimsFeatures = ( PROPERTY_ID: x.propertyId ?? null, PID: x.pid ?? null, IS_OWNED: true, + IS_OTHER_INTEREST: true, }, }; }), }; }; +const mockGetApi = { + error: undefined, + response: [1] as number[] | undefined, + execute: vi.fn().mockResolvedValue([1]), + loading: false, +}; +vi.mock('@/hooks/repositories/usePimsPropertyRepository', () => ({ + usePimsPropertyRepository: () => { + return { + getMatchingProperties: mockGetApi, + }; + }, +})); + // This mocks the parcels of land a user can see - render a cluster and a marker const smallMockParcels: ParcelSeed[] = [ - { id: 1, latitude: 54.917061, longitude: -122.749672 }, - { id: 3, latitude: 54.918162, longitude: -122.749772 }, + { id: 1, latitude: 54.917061, longitude: -122.749672, propertyId: 1 }, + { id: 3, latitude: 54.918162, longitude: -122.749772, propertyId: 2 }, ]; // This mocks the parcels of land a user can see - render a cluster and a marker const mockParcels: ParcelSeed[] = [ - { id: 1, latitude: 55.917161, longitude: -122.749612, pid: 7771 }, - { id: 2, latitude: 55.917262, longitude: -122.749622, pid: 7772 }, - { id: 3, latitude: 55.917363, longitude: -122.749732, pid: 7773 }, + { id: 1, latitude: 55.917161, longitude: -122.749612, pid: 7771, propertyId: 1 }, + { id: 2, latitude: 55.917262, longitude: -122.749622, pid: 7772, propertyId: 2 }, + { id: 3, latitude: 55.917363, longitude: -122.749732, pid: 7773, propertyId: 3 }, ]; // This will spoof the active parcel (the one that will populate the popup details) @@ -155,6 +171,25 @@ let history = createMemoryHistory(); describe('MapContainer', () => { const setup = async (renderOptions: RenderOptions = {}) => { + const activePimsPropertyIds = mockParcels.map(mp => mp.propertyId); + const defaultMapMachine = { + ...mapMachineBaseMock, + activePimsPropertyIds: activePimsPropertyIds, + mapFeatureData: { + pimsLocationFeatures: createPimsFeatures(mockParcels), + pimsBoundaryFeatures: emptyPimsBoundaryFeatureCollection, + fullyAttributedFeatures: emptyPmbcFeatureCollection, + }, + }; + if ( + renderOptions?.mockMapMachine?.mapFeatureData?.pimsLocationFeatures && + !renderOptions?.mockMapMachine?.activePimsPropertyIds?.length + ) { + renderOptions.mockMapMachine.activePimsPropertyIds = + renderOptions.mockMapMachine.mapFeatureData.pimsLocationFeatures?.features.map( + mp => mp.properties.PROPERTY_ID, + ); + } const utils = render( <> @@ -162,14 +197,7 @@ describe('MapContainer', () => { { store, history, - mockMapMachine: { - ...mapMachineBaseMock, - mapFeatureData: { - pimsLocationFeatures: createPimsFeatures(mockParcels), - pimsBoundaryFeatures: emptyPimsBoundaryFeatureCollection, - fullyAttributedFeatures: emptyPmbcFeatureCollection, - }, - }, + mockMapMachine: defaultMapMachine, ...renderOptions, useMockAuthentication: true, }, @@ -485,4 +513,24 @@ describe('MapContainer', () => { // verify the correct feature got clicked expect(testMapMock.mapMarkerClick).toHaveBeenCalledWith(expectedFeature); }); + + it('calls matchproperties with advanced search criteria', async () => { + mockKeycloak({ claims: [Claims.ADMIN_PROPERTIES] }); + const testMapMock: IMapStateMachineContext = { + ...mapMachineBaseMock, + isMapVisible: false, + }; + await setup({ mockMapMachine: testMapMock }); + + expect(mockGetApi.execute).not.toHaveBeenCalled(); + expect(mapMachineBaseMock.setVisiblePimsProperties).not.toHaveBeenCalled(); + }); + + it('Does not call matchproperties with advanced search criteria if map not visible', async () => { + mockKeycloak({ claims: [Claims.ADMIN_PROPERTIES] }); + await setup(); + + expect(mockGetApi.execute).toHaveBeenCalledWith(new PropertyFilterFormModel().toApi()); + expect(mapMachineBaseMock.setVisiblePimsProperties).toHaveBeenCalled(); + }); }); diff --git a/source/frontend/src/features/properties/map/MapContainer.tsx b/source/frontend/src/features/properties/map/MapContainer.tsx index 81a8f04052..9c5aec82e5 100644 --- a/source/frontend/src/features/properties/map/MapContainer.tsx +++ b/source/frontend/src/features/properties/map/MapContainer.tsx @@ -1,5 +1,5 @@ import clsx from 'classnames'; -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { MapContainerProps } from 'react-leaflet'; import styled from 'styled-components'; @@ -16,6 +16,8 @@ import MapSideBar from '@/features/mapSideBar/MapSideBar'; import CompensationRequisitionRouter from '@/features/mapSideBar/router/CompensationRequisitionRouter'; import PropertyActivityRouter from '@/features/mapSideBar/router/PropertyActivityRouter'; import RightSideLayout from '@/features/rightSideLayout/RightSideLayout'; +import { usePimsPropertyRepository } from '@/hooks/repositories/usePimsPropertyRepository'; +import { Api_PropertyFilterCriteria } from '@/models/api/ProjectFilterCriteria'; enum MapCursors { DRAFT = 'draft-cursor', @@ -32,8 +34,32 @@ const MapContainer: React.FC> = () => isRepositioning, toggleMapFilterDisplay, toggleMapLayerControl, + setVisiblePimsProperties, + advancedSearchCriteria, + isMapVisible, } = useMapStateMachine(); + const { getMatchingProperties } = usePimsPropertyRepository(); + + const matchProperties = getMatchingProperties.execute; + + const filterProperties = useCallback( + async (filter: Api_PropertyFilterCriteria) => { + if (isMapVisible) { + const retrievedProperties = await matchProperties(filter); + + if (retrievedProperties !== undefined) { + setVisiblePimsProperties(retrievedProperties); + } + } + }, + [matchProperties, setVisiblePimsProperties, isMapVisible], + ); + + useEffect(() => { + filterProperties(advancedSearchCriteria?.toApi()); + }, [filterProperties, advancedSearchCriteria]); + const cursorClass = isSelecting ? MapCursors.DRAFT : isRepositioning diff --git a/source/frontend/src/mocks/mapFSM.mock.ts b/source/frontend/src/mocks/mapFSM.mock.ts index d56d099b1d..b25c60f594 100644 --- a/source/frontend/src/mocks/mapFSM.mock.ts +++ b/source/frontend/src/mocks/mapFSM.mock.ts @@ -6,6 +6,7 @@ import { emptyPmbcFeatureCollection, } from '@/components/common/mapFSM/models'; import { defaultBounds } from '@/components/maps/constants'; +import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { layersTree } from '@/components/maps/leaflet/Control/LayersControl/data'; export const mapMachineBaseMock: IMapStateMachineContext = { @@ -51,6 +52,8 @@ export const mapMachineBaseMock: IMapStateMachineContext = { showDisposed: false, showRetired: false, mapLayersToRefresh: [], + advancedSearchCriteria: new PropertyFilterFormModel(), + isMapVisible: true, requestFlyToLocation: vi.fn(), @@ -81,4 +84,5 @@ export const mapMachineBaseMock: IMapStateMachineContext = { setFullWidthSideBar: vi.fn(), resetMapFilter: vi.fn(), setMapLayersToRefresh: vi.fn(), + setAdvancedSearchCriteria: vi.fn(), }; From add7189e0c7d805ba5f73d5c7f548a5a59ef0334 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:51:39 +0000 Subject: [PATCH 04/10] CI: Bump version to v5.6.0-92.32 --- source/backend/api/Pims.Api.csproj | 4 ++-- source/frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index f7dec1e9e9..8dc303a290 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,8 +2,8 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.6.0-92.31 - 5.6.0-92.31 + 5.6.0-92.32 + 5.6.0-92.32 5.6.0.92 true 16BC0468-78F6-4C91-87DA-7403C919E646 diff --git a/source/frontend/package.json b/source/frontend/package.json index 192d464174..0997363220 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.6.0-92.31", + "version": "5.6.0-92.32", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 9c3fcb9ed3ac15848d9c5e1427b7f2855ab30dc9 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 6 Nov 2024 16:03:08 -0800 Subject: [PATCH 05/10] PSP-9488 Titles are missing on Properties links and File Summary (#4456) * Titles are missing on Properties links and File Summary * Update snapshots --- .../__snapshots__/AcquisitionView.test.tsx.snap | 8 +++++++- .../acquisition/common/AcquisitionMenu.tsx | 14 ++++++++++---- .../common/GenerateForm/GenerateFormView.tsx | 1 + .../__snapshots__/AcquisitionMenu.test.tsx.snap | 8 +++++++- .../detail/AcquisitionSummaryView.test.tsx | 11 +---------- .../update/UpdateAcquisitionForm.test.tsx | 6 +++--- .../__snapshots__/DispositionView.test.tsx.snap | 7 ++++++- .../disposition/common/DispositionMenu.tsx | 14 ++++++++++---- .../__snapshots__/DispositionMenu.test.tsx.snap | 8 +++++++- .../ResearchContainer.test.tsx.snap | 7 ++++++- .../mapSideBar/research/common/ResearchMenu.tsx | 16 ++++++++++++---- .../__snapshots__/ResearchMenu.test.tsx.snap | 8 +++++++- 12 files changed, 77 insertions(+), 31 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap index e215cd1feb..d41a4834c9 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap @@ -911,7 +911,11 @@ exports[`AcquisitionView component > renders as expected 1`] = `

- File Summary + + File Summary +
renders as expected 1`] = ` >