Skip to content

Commit

Permalink
PSP-9570 Property Information leaflet should open up when a property …
Browse files Browse the repository at this point in the history
…is part of the PIMS Inventory regardless if it has a PID or not (#4539)

* PSP-9570 Display property information panel for PIMS properties with a PIN only (no PID)

* New route for PIN properties

* Adjust routing logic so that non-invertory PIN properties are supported

* Test updates

* PR feedback
  • Loading branch information
asanchezr authored Dec 24, 2024
1 parent 7bf12a9 commit 4a5b20b
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { dequal } from 'dequal';
import { LatLngBounds, LatLngLiteral } from 'leaflet';
import React, { useCallback, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { AnyEventObject } from 'xstate';

import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models';
import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types';
Expand All @@ -13,7 +14,7 @@ import {
IPropertyFilter,
} from '@/features/properties/filter/IPropertyFilter';
import { exists } from '@/utils';
import { pidParser } from '@/utils/propertyUtils';
import { pidParser, pinParser } from '@/utils/propertyUtils';

import { mapMachine } from './machineDefinition/mapMachine';
import { MachineContext, SideBarType } from './machineDefinition/types';
Expand Down Expand Up @@ -120,35 +121,48 @@ export const MapStateMachineProvider: React.FC<React.PropsWithChildren<unknown>>
actions: {
navigateToProperty: context => {
const selectedFeatureData = context.mapLocationFeatureDataset;
if (selectedFeatureData?.pimsFeature?.properties?.PROPERTY_ID) {
if (exists(selectedFeatureData?.pimsFeature?.properties?.PROPERTY_ID)) {
const pimsFeature = selectedFeatureData.pimsFeature;
history.push(`/mapview/sidebar/property/${pimsFeature.properties.PROPERTY_ID}`);
} else if (selectedFeatureData?.parcelFeature?.properties?.PID) {
const parcelFeature = selectedFeatureData.parcelFeature;
} else if (exists(selectedFeatureData?.parcelFeature?.properties?.PID)) {
const parcelFeature = selectedFeatureData?.parcelFeature;
const parsedPid = pidParser(parcelFeature.properties.PID);
history.push(`/mapview/sidebar/non-inventory-property/${parsedPid}`);
history.push(`/mapview/sidebar/non-inventory-property/pid/${parsedPid}`);
} else if (exists(selectedFeatureData?.parcelFeature?.properties?.PIN)) {
const parcelFeature = selectedFeatureData?.parcelFeature;
const parsedPin = pinParser(parcelFeature.properties.PIN);
history.push(`/mapview/sidebar/non-inventory-property/pin/${parsedPin}`);
}
},
},
services: {
loadLocationData: (context: MachineContext, event: any) => {
const result = locationLoader.loadLocationDetails(
event.type === 'MAP_CLICK' ? event.latlng : event.featureSelected.latlng,
);

if (event.type === 'MAP_MARKER_CLICK') {
loadLocationData: async (
context: MachineContext,
event:
| (AnyEventObject & { type: 'MAP_CLICK'; latlng: LatLngLiteral })
| (AnyEventObject & { type: 'MAP_MARKER_CLICK'; featureSelected: FeatureSelected }),
) => {
let result: LocationFeatureDataset | undefined = undefined;

if (event.type === 'MAP_CLICK') {
result = await locationLoader.loadLocationDetails({ latLng: event.latlng });
} else if (event.type === 'MAP_MARKER_CLICK') {
result = await locationLoader.loadLocationDetails({
latLng: event.featureSelected.latlng,
pimsPropertyId: event.featureSelected?.pimsLocationFeature?.PROPERTY_ID ?? null,
});
// In the case of the map marker being clicked, we must use the search result properties, as the minimal layer does not have the necessary feature data.
// However, use the coordinates of the clicked marker.
result.then(data => {
data.pimsFeature = {
properties: { ...data.pimsFeature.properties },
if (exists(result.pimsFeature)) {
result.pimsFeature = {
properties: { ...result.pimsFeature?.properties },
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [event.featureSelected.latlng.lng, event.featureSelected.latlng.lat],
},
};
});
}
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { WHSE_Municipalities_Feature_Properties } from '@/models/layers/municipa
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 { exists, isValidId } from '@/utils';

export interface LocationFeatureDataset {
selectingComponentId: string | null;
Expand Down Expand Up @@ -72,7 +73,13 @@ const useLocationFeatureLoader = () => {
const crownLandLayerServiceFindOneInventory = crownLandLayerService.findOneCrownLandInventory;

const loadLocationDetails = useCallback(
async (latLng: LatLngLiteral): Promise<LocationFeatureDataset> => {
async ({
latLng,
pimsPropertyId,
}: {
latLng: LatLngLiteral;
pimsPropertyId?: number | null;
}): Promise<LocationFeatureDataset> => {
const result: LocationFeatureDataset = {
selectingComponentId: null,
location: latLng,
Expand Down Expand Up @@ -100,6 +107,7 @@ const useLocationFeatureLoader = () => {
const crownLandTenuresTask = crownLandLayerServiceFindOneTenure(latLng);
const crownLandInventoryTask = crownLandLayerServiceFindOneInventory(latLng);
const crownLandInclusionsTask = crownLandLayerServiceFindOneInclusion(latLng);
const municipalityFeatureTask = adminLegalBoundaryLayerServiceFindOneMunicipality(latLng);

const [
parcelFeature,
Expand All @@ -111,6 +119,7 @@ const useLocationFeatureLoader = () => {
crownLandTenuresFeature,
crownLandInventoryFeature,
crownLandInclusionsFeature,
municipalityFeature,
] = await Promise.all([
fullyAttributedTask,
regionTask,
Expand All @@ -121,31 +130,33 @@ const useLocationFeatureLoader = () => {
crownLandTenuresTask,
crownLandInventoryTask,
crownLandInclusionsTask,
municipalityFeatureTask,
]);

let pimsLocationProperties:
| FeatureCollection<Geometry, PIMS_Property_Location_View>
| undefined = undefined;

// Load PimsProperties
if (latLng) {
const latLngFeature = await findOneByBoundary(latLng, 'GEOMETRY', 4326);
if (latLngFeature !== undefined) {
pimsLocationProperties = { features: [latLngFeature], type: 'FeatureCollection' };
// - first attempt to find it by our internal PIMS id
// - then try to find it on our boundary layer.
// - if not found by boundary attempt to match it by PID / PIN coming from parcel-map
const isInPims = isValidId(Number(pimsPropertyId));
if (isInPims) {
pimsLocationProperties = await loadProperties({ PROPERTY_ID: Number(pimsPropertyId) });
} else {
const boundaryFeature = await findOneByBoundary(latLng, 'GEOMETRY', 4326);
if (exists(boundaryFeature)) {
pimsLocationProperties = { features: [boundaryFeature], type: 'FeatureCollection' };
} else if (exists(parcelFeature)) {
pimsLocationProperties = await loadProperties({
PID: parcelFeature.properties?.PID || '',
PIN: parcelFeature.properties?.PIN?.toString() || '',
});
}
} else if (parcelFeature !== undefined) {
pimsLocationProperties = await loadProperties({
PID: parcelFeature.properties?.PID || '',
PIN: parcelFeature.properties?.PIN?.toString() || '',
});
}

const municipalityFeature = await adminLegalBoundaryLayerServiceFindOneMunicipality(latLng);

if (
pimsLocationProperties?.features !== undefined &&
pimsLocationProperties.features.length > 0
) {
if (exists(pimsLocationProperties?.features) && pimsLocationProperties.features.length > 0) {
result.pimsFeature = pimsLocationProperties.features[0] ?? null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { createMemoryHistory } from 'history';
import { useMap } from 'react-leaflet';

import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext';
import Claims from '@/constants/claims';
import { mockLookups } from '@/mocks/lookups.mock';
import { mapMachineBaseMock } from '@/mocks/mapFSM.mock';
import { emptyPmbcParcel } from '@/models/layers/parcelMapBC';
import { EmptyPropertyLocation } from '@/models/layers/pimsPropertyLocationView';
import { lookupCodesSlice } from '@/store/slices/lookupCodes';
import { pidParser } from '@/utils/propertyUtils';
import { pidParser, pinParser } from '@/utils/propertyUtils';
import { act, render, RenderOptions, userEvent } from '@/utils/test-utils';

import { ILayerPopupViewProps, LayerPopupView } from './LayerPopupView';
import { emptyPimsBoundaryFeatureCollection } from '@/components/common/mapFSM/models';
import { useKeycloak } from '@react-keycloak/web';

vi.mock('react-leaflet');

Expand Down Expand Up @@ -54,6 +49,7 @@ describe('LayerPopupView component', () => {
});
expect(asFragment()).toMatchSnapshot();
});

describe('fly out behaviour', () => {
it('fly out is hidden by default', async () => {
const { queryByText } = setup({
Expand Down Expand Up @@ -122,7 +118,7 @@ describe('LayerPopupView component', () => {
expect(history.location.pathname).toBe(`/mapview/sidebar/property/${propertyId}`);
});

it('handles view property action for non-inventory properties', async () => {
it('handles view property action for non-inventory properties - with PID', async () => {
const pid = '123456789';
const parsedPid = pidParser(pid);
const { getByTestId, getByText } = setup({
Expand Down Expand Up @@ -162,7 +158,51 @@ describe('LayerPopupView component', () => {
const link = getByText('View Property Info');
await act(async () => userEvent.click(link));
expect(history.location.pathname).toBe(
`/mapview/sidebar/non-inventory-property/${parsedPid}`,
`/mapview/sidebar/non-inventory-property/pid/${parsedPid}`,
);
});

it('handles view property action for non-inventory properties - with PIN', async () => {
const pin = '123456789';
const parsedPin = pinParser(pin);
const { getByTestId, getByText } = setup({
layerPopup: {
layers: [
{
data: { PIN: pin },
title: '',
config: {},
},
],
latlng: undefined,
},
featureDataset: {
parcelFeature: {
type: 'Feature',
properties: { ...emptyPmbcParcel, PIN: parsedPin },
geometry: { type: 'Point', coordinates: [] },
},
location: { lat: 0, lng: 0 },
fileLocation: null,
pimsFeature: null,
regionFeature: null,
districtFeature: null,
municipalityFeature: null,
highwayFeature: null,
selectingComponentId: null,
crownLandLeasesFeature: null,
crownLandLicensesFeature: null,
crownLandTenuresFeature: null,
crownLandInventoryFeature: null,
crownLandInclusionsFeature: null,
},
});
const ellipsis = getByTestId('fly-out-ellipsis');
await act(async () => userEvent.click(ellipsis));
const link = getByText('View Property Info');
await act(async () => userEvent.click(link));
expect(history.location.pathname).toBe(
`/mapview/sidebar/non-inventory-property/pin/${parsedPin}`,
);
});

Expand Down Expand Up @@ -440,5 +480,39 @@ describe('LayerPopupView component', () => {
await act(async () => userEvent.click(link));
expect(history.location.pathname).toBe('/mapview/sidebar/consolidation/new');
});

it('handles create lease and licence file action', async () => {
const { getByTestId, getByText } = setup({
layerPopup: {
latlng: undefined,
layers: [],
},
featureDataset: null,

claims: [Claims.LEASE_ADD],
});
const ellipsis = getByTestId('fly-out-ellipsis');
await act(async () => userEvent.click(ellipsis));
const link = getByText('Lease/Licence File');
await act(async () => userEvent.click(link));
expect(history.location.pathname).toBe('/mapview/sidebar/lease/new');
});

it('handles create disposition file action', async () => {
const { getByTestId, getByText } = setup({
layerPopup: {
latlng: undefined,
layers: [],
},
featureDataset: null,

claims: [Claims.DISPOSITION_ADD],
});
const ellipsis = getByTestId('fly-out-ellipsis');
await act(async () => userEvent.click(ellipsis));
const link = getByText('Disposition File');
await act(async () => userEvent.click(link));
expect(history.location.pathname).toBe('/mapview/sidebar/disposition/new');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFe
import SimplePagination from '@/components/common/SimplePagination';
import TooltipWrapper from '@/components/common/TooltipWrapper';
import { Scrollable } from '@/features/projects/styles';
import { exists, isValidId, pidParser } from '@/utils';
import { exists, isValidId, pidParser, pinParser } from '@/utils';

import { LayerPopupContent } from './components/LayerPopupContent';
import { LayerPopupFlyout } from './components/LayerPopupFlyout';
Expand Down Expand Up @@ -45,14 +45,19 @@ export const LayerPopupView: React.FC<React.PropsWithChildren<ILayerPopupViewPro
if (isInPims) {
closeFlyout();
history.push(`/mapview/sidebar/property/${pimsPropertyId}`);
} else if (featureDataset?.parcelFeature?.properties?.PID) {
} else if (exists(featureDataset?.parcelFeature?.properties?.PID)) {
closeFlyout();
const parcelFeature = featureDataset?.parcelFeature;
const parsedPid = pidParser(parcelFeature?.properties?.PID);
history.push(`/mapview/sidebar/non-inventory-property/${parsedPid}`);
history.push(`/mapview/sidebar/non-inventory-property/pid/${parsedPid}`);
} else if (exists(featureDataset?.parcelFeature?.properties?.PIN)) {
closeFlyout();
const parcelFeature = featureDataset?.parcelFeature;
const parsedPin = pinParser(parcelFeature?.properties?.PIN);
history.push(`/mapview/sidebar/non-inventory-property/pin/${parsedPin}`);
} else {
console.warn('Invalid marker when trying to see property information');
toast.warn('A map parcel must have a PID in order to view detailed information');
toast.warn('A map parcel must have a PID or PIN in order to view detailed information');
}
};

Expand Down
1 change: 1 addition & 0 deletions source/frontend/src/constants/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface IPaginateProperties extends IPaginateParams {
}

export interface IGeoSearchParams {
PROPERTY_ID?: number;
STREET_ADDRESS_1?: string;
PID?: string;
PID_PADDED?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const InventoryTabs: React.FunctionComponent<
const history = useHistory();
const match = useRouteMatch<{
propertyId: string;
pid: string;
pin: string;
menuIndex: string;
id: string;
researchId: string;
Expand Down Expand Up @@ -63,6 +65,8 @@ export const InventoryTabs: React.FunctionComponent<
} else {
const path = generatePath(match.path, {
propertyId: match.params.propertyId,
pid: match.params.pid,
pin: match.params.pin,
tab,
});
history.push(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { createMemoryHistory } from 'history';

import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext';
import { Claims } from '@/constants/claims';
import { getMockCrownTenuresLayerResponse } from '@/mocks/crownTenuresLayerResponse.mock';
import { mockLookups } from '@/mocks/lookups.mock';
import { getMockLtsaResponse } from '@/mocks/ltsa.mock';
import { mapMachineBaseMock } from '@/mocks/mapFSM.mock';
import { lookupCodesSlice } from '@/store/slices/lookupCodes';
import { act, cleanup, render, RenderOptions, userEvent, waitFor } from '@/utils/test-utils';

import MotiInventoryContainer, { IMotiInventoryContainerProps } from './MotiInventoryContainer';
import { getMockCrownTenuresLayerResponse } from '@/mocks/crownTenuresLayerResponse.mock';
import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext';
import { mapMachineBaseMock } from '@/mocks/mapFSM.mock';

const mockAxios = new MockAdapter(axios);
const history = createMemoryHistory();
Expand Down Expand Up @@ -199,7 +199,7 @@ describe('MotiInventoryContainer component', () => {
});

it('hides the property information tab for non-inventory properties', async () => {
history.push('/mapview/sidebar/non-inventory-property/9212434');
history.push('/mapview/sidebar/non-inventory-property/pid/9212434');
// non-inventory properties will not attempt to contact the backend.
const error = {
isAxiosError: true,
Expand Down
Loading

0 comments on commit 4a5b20b

Please sign in to comment.