From 4bbb81443b5e306b75b8257139baf2399085c92c Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Mon, 9 Feb 2026 09:34:09 -0800 Subject: [PATCH] psp-10976 HOTFIX correct pmbc result duplication when clicking on PIMS result options. Simplifies/unifies logic. Ensures region/district applied evenly. --- .../Control/Search/SearchContainer.tsx | 268 +++++++++++------- .../leaflet/Control/Search/SearchView.tsx | 216 +++++++------- 2 files changed, 266 insertions(+), 218 deletions(-) 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 42b227f7c8..8c32fcc340 100644 --- a/source/frontend/src/components/maps/leaflet/Control/Search/SearchContainer.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/Search/SearchContainer.tsx @@ -24,6 +24,56 @@ export interface ISearchContainerProps { View: React.FunctionComponent; } +const useEnrichedDatasets = ( + baseDatasets: SelectedFeatureDataset[], + findRegion: ReturnType['findRegion'], + findDistrict: ReturnType['findDistrict'], +): SelectedFeatureDataset[] => { + const [selectedFeatureDatasets, setSelectedFeatureDatasets] = useState( + [], + ); + + useEffect(() => { + let cancelled = false; + + async function fetchRegions() { + if (baseDatasets.length === 0) { + setSelectedFeatureDatasets([]); + return; + } + + const results = await getRegionAndDistrictsResults(baseDatasets, findRegion, findDistrict); + + if (cancelled) return; + + const enriched = baseDatasets.map(dataset => { + const key = featureSetToLatLngKey(dataset); + if (results.has(key)) { + const { regionResult, districtResult } = results.get(key)!; + return { + ...dataset, + regionFeature: regionResult, + districtFeature: districtResult, + }; + } else { + return dataset; + } + }); + + setSelectedFeatureDatasets(enriched); + } + + fetchRegions(); + + // Cleanup function to avoid state updates if component unmounts + return () => { + cancelled = true; + }; + }, [baseDatasets, findDistrict, findRegion]); + + return selectedFeatureDatasets; +}; + export const SearchContainer: React.FC = ({ View }) => { const { mapSearchCriteria, @@ -40,7 +90,6 @@ export const SearchContainer: React.FC = ({ View }) => { const pathGenerator = usePathGenerator(); const { findDistrict, findRegion } = useAdminBoundaryMapLayer(); - const [isPimsActive, setIsPimsActive] = useState(false); const handleMapFilterChange = (filter: IPropertyFilter) => { if (['coordinates', 'name', 'address'].includes(filter.searchBy)) { @@ -69,128 +118,128 @@ export const SearchContainer: React.FC = ({ View }) => { } }; - // Base dataset (no region/district yet) - const baseDatasets = useMemo(() => { - if (isPimsActive) { - return ( - mapFeatureData?.pimsFeatures.features.map(pimsParcel => { - const center = getFeatureBoundedCenter(pimsParcel); - return { - parcelFeature: null, - pimsFeature: pimsParcel, - location: { lat: center[1], lng: center[0] }, - regionFeature: null, - fileLocation: null, - fileBoundary: null, - districtFeature: null, - municipalityFeature: null, - selectingComponentId: null, - }; - }) ?? [] - ); - } else { - return ( - mapFeatureData?.fullyAttributedFeatures.features.map(pmbcParcel => { - const center = getFeatureBoundedCenter(pmbcParcel); - return { - parcelFeature: pmbcParcel, - pimsFeature: null, - location: - exists(center) && center.length >= 2 ? { lat: center[1], lng: center[0] } : null, - regionFeature: null, - fileLocation: null, - fileBoundary: null, - districtFeature: null, - municipalityFeature: null, - selectingComponentId: null, - }; - }) ?? [] - ); - } - }, [mapFeatureData, isPimsActive]); - - // Enrich dataset with region/district info - const [selectedFeatureDatasets, setSelectedFeatureDatasets] = useState( - [], + // Base datasets (no region/district yet) + const pimsBaseDatasets = useMemo(() => { + return ( + mapFeatureData?.pimsFeatures.features.map(pimsParcel => { + const center = getFeatureBoundedCenter(pimsParcel); + return { + parcelFeature: null, + pimsFeature: pimsParcel, + location: + exists(center) && center.length >= 2 ? { lat: center[1], lng: center[0] } : null, + regionFeature: null, + fileLocation: null, + fileBoundary: null, + districtFeature: null, + municipalityFeature: null, + selectingComponentId: null, + }; + }) ?? [] + ); + }, [mapFeatureData]); + + const pmbcBaseDatasets = useMemo(() => { + return ( + mapFeatureData?.fullyAttributedFeatures.features.map(pmbcParcel => { + const center = getFeatureBoundedCenter(pmbcParcel); + return { + parcelFeature: pmbcParcel, + pimsFeature: null, + location: + exists(center) && center.length >= 2 ? { lat: center[1], lng: center[0] } : null, + regionFeature: null, + fileLocation: null, + fileBoundary: null, + districtFeature: null, + municipalityFeature: null, + selectingComponentId: null, + }; + }) ?? [] + ); + }, [mapFeatureData]); + + const pimsSelectedFeatureDatasets = useEnrichedDatasets( + pimsBaseDatasets, + findRegion, + findDistrict, ); - useEffect(() => { - let cancelled = false; - - async function fetchRegions() { - if (baseDatasets.length === 0) { - setSelectedFeatureDatasets([]); - return; - } - - const results = await getRegionAndDistrictsResults(baseDatasets, findRegion, findDistrict); + const pmbcSelectedFeatureDatasets = useEnrichedDatasets( + pmbcBaseDatasets, + findRegion, + findDistrict, + ); - if (cancelled) return; + // Actions for creating new files + const onCreateResearchFile = useCallback( + (isPims: boolean) => { + const datasets = isPims ? pimsSelectedFeatureDatasets : pmbcSelectedFeatureDatasets; + prepareForCreation(datasets); + pathGenerator.newFile('research'); + }, + [pathGenerator, prepareForCreation, pimsSelectedFeatureDatasets, pmbcSelectedFeatureDatasets], + ); - const enriched = baseDatasets.map(dataset => { - const key = featureSetToLatLngKey(dataset); - if (results.has(key)) { - const { regionResult, districtResult } = results.get(key)!; - return { - ...dataset, - regionFeature: regionResult, - districtFeature: districtResult, - }; - } else { - return dataset; - } - }); + const onCreateAcquisitionFile = useCallback( + (isPims: boolean) => { + const datasets = isPims ? pimsSelectedFeatureDatasets : pmbcSelectedFeatureDatasets; + prepareForCreation(datasets); + pathGenerator.newFile('acquisition'); + }, + [pathGenerator, prepareForCreation, pimsSelectedFeatureDatasets, pmbcSelectedFeatureDatasets], + ); - setSelectedFeatureDatasets(enriched); - } + const onCreateDispositionFile = useCallback( + (isPims: boolean) => { + const datasets = isPims ? pimsSelectedFeatureDatasets : pmbcSelectedFeatureDatasets; + prepareForCreation(datasets); + pathGenerator.newFile('disposition'); + }, + [pathGenerator, prepareForCreation, pimsSelectedFeatureDatasets, pmbcSelectedFeatureDatasets], + ); - fetchRegions(); + const onCreateLeaseFile = useCallback( + (isPims: boolean) => { + const datasets = isPims ? pimsSelectedFeatureDatasets : pmbcSelectedFeatureDatasets; + prepareForCreation(datasets); + pathGenerator.newFile('lease'); + }, + [pathGenerator, prepareForCreation, pimsSelectedFeatureDatasets, pmbcSelectedFeatureDatasets], + ); - // Cleanup function to avoid state updates if component unmounts - return () => { - cancelled = true; - }; - }, [baseDatasets, findDistrict, findRegion]); + const onCreateManagementFile = useCallback( + (isPims: boolean) => { + const datasets = isPims ? pimsSelectedFeatureDatasets : pmbcSelectedFeatureDatasets; + prepareForCreation(datasets); + pathGenerator.newFile('management'); + }, + [pathGenerator, prepareForCreation, pimsSelectedFeatureDatasets, pmbcSelectedFeatureDatasets], + ); - // Actions for creating new files - const onCreateResearchFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); - pathGenerator.newFile('research'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); - - const onCreateAcquisitionFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); - pathGenerator.newFile('acquisition'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); - - const onCreateDispositionFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); - pathGenerator.newFile('disposition'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); - - const onCreateLeaseFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); - pathGenerator.newFile('lease'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); - - const onCreateManagementFile = useCallback(() => { - prepareForCreation(selectedFeatureDatasets); - pathGenerator.newFile('management'); - }, [pathGenerator, prepareForCreation, selectedFeatureDatasets]); - - const onAddToOpenFile = useCallback(() => { - // If in edit properties mode, prepare the parcel for addition to an open file - if (isEditPropertiesMode) { - prepareForCreation(selectedFeatureDatasets); - } - }, [isEditPropertiesMode, prepareForCreation, selectedFeatureDatasets]); + const onAddToOpenFile = useCallback( + (isPims: boolean) => { + // If in edit properties mode, prepare the parcel for addition to an open file + if (isEditPropertiesMode) { + const datasets = isPims ? pimsSelectedFeatureDatasets : pmbcSelectedFeatureDatasets; + prepareForCreation(datasets); + } + }, + [ + isEditPropertiesMode, + prepareForCreation, + pimsSelectedFeatureDatasets, + pmbcSelectedFeatureDatasets, + ], + ); return ( = ({ View }) => { onCreateLeaseFile={onCreateLeaseFile} onCreateManagementFile={onCreateManagementFile} onAddToOpenFile={onAddToOpenFile} - setIsPimsActive={setIsPimsActive} /> ); }; diff --git a/source/frontend/src/components/maps/leaflet/Control/Search/SearchView.tsx b/source/frontend/src/components/maps/leaflet/Control/Search/SearchView.tsx index 4daccf30a2..21422671c2 100644 --- a/source/frontend/src/components/maps/leaflet/Control/Search/SearchView.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/Search/SearchView.tsx @@ -1,6 +1,5 @@ -import { Feature, Geometry } from 'geojson'; import { chain } from 'lodash'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { FaPlus } from 'react-icons/fa'; import styled from 'styled-components'; @@ -24,7 +23,6 @@ import { ParcelDataset } from '@/features/properties/parcelList/models'; import { ParcelListContainer } from '@/features/properties/parcelList/ParcelListContainer'; import { ParcelListView } from '@/features/properties/parcelList/ParcelListView'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; -import { PIMS_Property_View } from '@/models/layers/pimsPropertyView'; import { exists } from '@/utils'; import { isStrataCommonProperty } from '@/utils/propertyUtils'; @@ -34,33 +32,32 @@ export interface ISearchViewProps { onFilterChange: (filter: IPropertyFilter) => void; propertyFilter: IPropertyFilter; searchResult: MapFeatureData; - selectedFeatureDatasets?: SelectedFeatureDataset[]; + pmbcSelectedFeatureDatasets?: SelectedFeatureDataset[]; + pimsSelectedFeatureDatasets?: SelectedFeatureDataset[]; canAddToOpenFile?: boolean; - onCreateResearchFile: () => void; - onCreateAcquisitionFile: () => void; - onCreateDispositionFile: () => void; - onCreateLeaseFile: () => void; - onCreateManagementFile: () => void; - onAddToOpenFile: () => void; - setIsPimsActive: (value: boolean) => void; -} - -interface PropertyProjection { - isStrataLot: boolean; - pid: string | null; - pin: string | null; - plan: string | null; - feature: Feature | null; + onCreateResearchFile: (isPims: boolean) => void; + onCreateAcquisitionFile: (isPims: boolean) => void; + onCreateDispositionFile: (isPims: boolean) => void; + onCreateLeaseFile: (isPims: boolean) => void; + onCreateManagementFile: (isPims: boolean) => void; + onAddToOpenFile: (isPims: boolean) => void; } export const SearchView: React.FC = props => { + const canAddToOpenFile = props.canAddToOpenFile; + const onAddToOpenFile = props.onAddToOpenFile; + const onCreateAcquisitionFile = props.onCreateAcquisitionFile; + const onCreateDispositionFile = props.onCreateDispositionFile; + const onCreateLeaseFile = props.onCreateLeaseFile; + const onCreateManagementFile = props.onCreateManagementFile; + const onCreateResearchFile = props.onCreateResearchFile; const keycloak = useKeycloakWrapper(); const propertyProjections = useMemo(() => { const fallbackFeatures = props.searchResult?.fullyAttributedFeatures?.features ?? []; - const baseParcels = props.selectedFeatureDatasets?.length - ? props.selectedFeatureDatasets.map(dataset => + const baseParcels = props.pmbcSelectedFeatureDatasets?.length + ? props.pmbcSelectedFeatureDatasets.map(dataset => ParcelDataset.fromSelectedFeatureDataset(dataset), ) : fallbackFeatures.map(feature => ParcelDataset.fromFullyAttributedFeature(feature)); @@ -78,93 +75,98 @@ export const SearchView: React.FC = props => { ) .value() .flat(); - }, [props.searchResult, props.selectedFeatureDatasets]); - - const pimsGroupedFeatures = chain(props.searchResult?.pimsFeatures.features) - .groupBy(feature => feature?.properties?.SURVEY_PLAN_NUMBER) - .map( - planGroup => - planGroup - ?.map>(x => ({ - pid: x.properties.PID_PADDED, - pin: exists(x.properties.PIN) ? String(x.properties.PIN) : null, - isStrataLot: false, - feature: x, - plan: x.properties.SURVEY_PLAN_NUMBER, - })) - .sort((a, b) => { - if (a.isStrataLot === b.isStrataLot) return 0; - if (a.isStrataLot) return -1; - if (b.isStrataLot) return 1; - return 0; - }) ?? [], - ); - - const pimsPropertyProjections = - pimsGroupedFeatures - .value() - .flatMap(x => x) - .map(x => ParcelDataset.fromPimsFeature(x.feature)) ?? []; + }, [props.searchResult, props.pmbcSelectedFeatureDatasets]); - const menuOptions: MenuOption[] = useMemo(() => { - const options: MenuOption[] = []; + const pimsPropertyProjections = useMemo(() => { + const fallbackFeatures = props.searchResult?.pimsFeatures?.features ?? []; + + const baseParcels = props.pimsSelectedFeatureDatasets?.length + ? props.pimsSelectedFeatureDatasets.map(dataset => + ParcelDataset.fromSelectedFeatureDataset(dataset), + ) + : fallbackFeatures.map(feature => ParcelDataset.fromPimsFeature(feature)); + + return chain(baseParcels) + .groupBy(pimsParcel => pimsParcel?.pimsFeature?.properties?.SURVEY_PLAN_NUMBER) + .map(planGroup => + planGroup.toSorted((a, b) => { + const aIsStrata = a.pmbcFeature ? isStrataCommonProperty(a.pmbcFeature) : false; + const bIsStrata = b.pmbcFeature ? isStrataCommonProperty(b.pmbcFeature) : false; + + if (aIsStrata === bIsStrata) return 0; + return aIsStrata ? -1 : 1; + }), + ) + .value() + .flat(); + }, [props.searchResult, props.pimsSelectedFeatureDatasets]); + + const createMenuOptions = useCallback( + (isPims: boolean): MenuOption[] => { + const options: MenuOption[] = []; + + if (keycloak.hasClaim(Claims.RESEARCH_ADD)) { + options.push({ + label: 'Create Research File', + onClick: () => onCreateResearchFile(isPims), + icon: , + }); + } + if (keycloak.hasClaim(Claims.ACQUISITION_ADD)) { + options.push({ + label: 'Create Acquisition File', + onClick: () => onCreateAcquisitionFile(isPims), + icon: , + }); + } + if (keycloak.hasClaim(Claims.MANAGEMENT_ADD)) { + options.push({ + label: 'Create Management File', + onClick: () => onCreateManagementFile(isPims), + icon: , + }); + } + if (keycloak.hasClaim(Claims.LEASE_ADD)) { + options.push({ + label: 'Create Lease File', + onClick: () => onCreateLeaseFile(isPims), + icon: , + }); + } + if (keycloak.hasClaim(Claims.DISPOSITION_ADD)) { + options.push({ + label: 'Create Disposition File', + onClick: () => onCreateDispositionFile(isPims), + icon: , + }); + } - if (keycloak.hasClaim(Claims.RESEARCH_ADD)) { - options.push({ - label: 'Create Research File', - onClick: props.onCreateResearchFile, - icon: , - }); - } - if (keycloak.hasClaim(Claims.ACQUISITION_ADD)) { - options.push({ - label: 'Create Acquisition File', - onClick: props.onCreateAcquisitionFile, - icon: , - }); - } - if (keycloak.hasClaim(Claims.MANAGEMENT_ADD)) { - options.push({ - label: 'Create Management File', - onClick: props.onCreateManagementFile, - icon: , - }); - } - if (keycloak.hasClaim(Claims.LEASE_ADD)) { - options.push({ - label: 'Create Lease File', - onClick: props.onCreateLeaseFile, - icon: , - }); - } - if (keycloak.hasClaim(Claims.DISPOSITION_ADD)) { options.push({ - label: 'Create Disposition File', - onClick: props.onCreateDispositionFile, - icon: , + label: 'Add to Open File', + onClick: () => onAddToOpenFile(isPims), + icon: canAddToOpenFile ? : undefined, + disabled: !canAddToOpenFile, + tooltip: 'A file must be open and in "edit property" mode', + separator: true, // Add a separator before the "Add to Open File" option }); - } - - options.push({ - label: 'Add to Open File', - onClick: props.onAddToOpenFile, - icon: props.canAddToOpenFile ? : undefined, - disabled: !props.canAddToOpenFile, - tooltip: 'A file must be open and in "edit property" mode', - separator: true, // Add a separator before the "Add to Open File" option - }); - - return options; - }, [ - keycloak, - props.canAddToOpenFile, - props.onAddToOpenFile, - props.onCreateAcquisitionFile, - props.onCreateDispositionFile, - props.onCreateLeaseFile, - props.onCreateManagementFile, - props.onCreateResearchFile, - ]); + + return options; + }, + [ + keycloak, + canAddToOpenFile, + onAddToOpenFile, + onCreateAcquisitionFile, + onCreateDispositionFile, + onCreateLeaseFile, + onCreateManagementFile, + onCreateResearchFile, + ], + ); + + const pmbcMenuOptions = useMemo(() => createMenuOptions(false), [createMenuOptions]); + + const pimsMenuOptions = useMemo(() => createMenuOptions(true), [createMenuOptions]); return ( = props => { header={ props.setIsPimsActive(false)} /> } @@ -202,9 +203,8 @@ export const SearchView: React.FC = props => { header={ props.setIsPimsActive(true)} /> }