From 3b70ce87e84821ecc3aa282390e43da10f108980 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Tue, 17 Jun 2025 17:09:26 -0700 Subject: [PATCH 01/61] psp-10371 allow file properties to be disabled. create a system generated note when a property is disabled. display a different marker for disabled properties. --- .../api/Services/ManagementFileService.cs | 29 +++ .../Models/Concepts/File/FilePropertyModel.cs | 5 + .../ManagementFilePropertyMap.cs | 2 + .../Helpers/Extensions/PropertyExtensions.cs | 31 +++ source/backend/entities/Partials/Property.cs | 2 +- .../tests/core/Entities/PropertyHelper.cs | 5 +- .../api/Services/ManagementFileServiceTest.cs | 42 ++++ .../Extensions/PropertyExtensionsTest.cs | 96 +++++++++ .../src/assets/images/inactive-property.svg | 15 ++ .../common/mapFSM/MapStateMachineContext.tsx | 11 +- .../mapFSM/machineDefinition/mapMachine.ts | 5 +- .../common/mapFSM/machineDefinition/types.ts | 3 +- .../mapFSM/useLocationFeatureLoader.tsx | 1 + .../frontend/src/components/common/styles.ts | 18 ++ .../maps/leaflet/Layers/PointClusterer.tsx | 28 ++- .../components/maps/leaflet/Layers/util.tsx | 14 +- .../SvgMarkers/DisabledDraftMarker.tsx | 51 +++++ .../propertySelector/MapClickMonitor.tsx | 1 + .../src/components/propertySelector/models.ts | 1 + .../DisabledDraftCircleNumber.tsx | 28 +++ .../SelectedPropertyRow.test.tsx | 36 ++++ .../SelectedPropertyRow.tsx | 34 +++- .../list/DispositionListView.test.tsx | 1 + .../DispositionSearchResults.test.tsx | 3 + .../detail/LeaseHeaderAddresses.test.tsx | 5 + .../LeasePages/surplus/Surplus.test.tsx | 3 + .../list/ManagementListView.test.tsx | 1 + .../ManagementSearchResults.test.tsx | 3 + .../acquisition/common/AcquisitionMenu.tsx | 18 +- .../hooks/useGenerateH0443.test.tsx | 2 + .../list/CompensationListContainer.test.tsx | 2 + .../mapSideBar/context/sidebarContext.tsx | 7 +- .../disposition/common/DispositionMenu.tsx | 18 +- .../mapSideBar/lease/LeaseContainer.tsx | 11 +- .../mapSideBar/management/ManagementView.tsx | 21 +- .../management/common/ManagementMenu.test.tsx | 182 ------------------ .../management/common/ManagementMenu.tsx | 165 ---------------- .../detail/FileActivityDetailView.tsx | 1 + .../edit/ManagementActivityEditForm.tsx | 12 +- .../AssociationHeader.tsx | 19 +- .../detail/PropertyResearchTabView.test.tsx | 1 + .../tabs/propertyResearch/update/models.ts | 1 + .../research/common/ResearchMenu.tsx | 21 +- .../mapSideBar/shared/FileMenuRow.tsx | 78 ++++++++ .../mapSideBar/shared/FileMenuView.tsx | 115 +++++------ .../src/features/mapSideBar/shared/models.ts | 7 + .../properties/UpdateProperties.test.tsx | 1 + .../update/properties/UpdateProperties.tsx | 2 + .../research/list/ResearchListView.test.tsx | 2 + .../src/hooks/useDraftMarkerSynchronizer.ts | 18 +- .../src/mocks/acquisitionFiles.mock.ts | 2 + .../frontend/src/mocks/compensations.mock.ts | 4 + .../frontend/src/mocks/fileProperty.mock.ts | 2 + source/frontend/src/mocks/lease.mock.ts | 1 + .../src/mocks/managementFiles.mock.ts | 1 + source/frontend/src/mocks/properties.mock.ts | 3 + .../frontend/src/mocks/researchFile.mock.ts | 4 +- .../generated/ApiGen_Concepts_FileProperty.ts | 1 + .../ApiGen_Concepts_PropertyActivity.ts | 1 - source/frontend/src/utils/mapPropertyUtils.ts | 24 +++ 60 files changed, 672 insertions(+), 549 deletions(-) create mode 100644 source/backend/tests/unit/dal/Helpers/Extensions/PropertyExtensionsTest.cs create mode 100644 source/frontend/src/assets/images/inactive-property.svg create mode 100644 source/frontend/src/components/maps/leaflet/SvgMarkers/DisabledDraftMarker.tsx create mode 100644 source/frontend/src/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber.tsx delete mode 100644 source/frontend/src/features/mapSideBar/management/common/ManagementMenu.test.tsx delete mode 100644 source/frontend/src/features/mapSideBar/management/common/ManagementMenu.tsx create mode 100644 source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx diff --git a/source/backend/api/Services/ManagementFileService.cs b/source/backend/api/Services/ManagementFileService.cs index d771b7cfd7..21b7f35f11 100644 --- a/source/backend/api/Services/ManagementFileService.cs +++ b/source/backend/api/Services/ManagementFileService.cs @@ -12,6 +12,7 @@ using Pims.Dal.Entities; using Pims.Dal.Entities.Models; using Pims.Dal.Exceptions; +using Pims.Dal.Helpers.Extensions; using Pims.Dal.Repositories; namespace Pims.Api.Services @@ -71,6 +72,7 @@ public PimsManagementFile Add(PimsManagementFile managementFile, IEnumerable overrideCodes) { foreach (var managementProperty in managementFile.PimsManagementFileProperties) diff --git a/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs b/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs index 74f43ad609..2945c81b34 100644 --- a/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs +++ b/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs @@ -37,6 +37,11 @@ public class FilePropertyModel : BaseConcurrentModel /// public PropertyModel Property { get; set; } + /// + /// get/set - Optional flag indicating if the relationship is active. + /// + public bool? IsActive { get; set; } + /// /// get/set - The relationship's property id. /// diff --git a/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs b/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs index 87b5b06015..e03b7fc340 100644 --- a/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs +++ b/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs @@ -16,6 +16,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.PropertyName, src => src.PropertyName) .Map(dest => dest.PropertyId, src => src.PropertyId) .Map(dest => dest.Property, src => src.Property) + .Map(dest => dest.IsActive, src => src.IsActive) .Inherits(); // Map from Model to Entity @@ -26,6 +27,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.PropertyName, src => src.PropertyName) .Map(dest => dest.PropertyId, src => src.PropertyId) .Map(dest => dest.Property, src => src.Property) + .Map(dest => dest.IsActive, src => src.IsActive) .Inherits(); } } diff --git a/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs b/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs index b0cd1394fc..9565ca551e 100644 --- a/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs +++ b/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs @@ -27,6 +27,37 @@ public static string GetHistoricalNumbersAsString(this PimsProperty property) // Print item return string.Join("; ", groupedHistorical.Select(g => g.GetAsString())); } + + public static string GetPropertyName(this IFilePropertyEntity fileProperty) + { + var property = fileProperty?.Property; + if(property == null) + { + return string.Empty; + } + + if (property.Pid.HasValue && property?.Pid.Value.ToString().Length > 0 && property?.Pid != '0') + { + return $"{property.Pid:000-000-000}"; + } + else if (property.Pin.HasValue && property?.Pin.Value.ToString()?.Length > 0 && property?.Pin != '0') + { + return property.Pin.ToString(); + } + else if (property?.SurveyPlanNumber != null && property?.SurveyPlanNumber.Length > 0) + { + return property.SurveyPlanNumber; + } + else if (property?.Location != null) + { + return $"{property.Location.Coordinate.X}, {property.Location.Coordinate.Y}"; + } + else if (property?.Address != null) + { + return property.Address.FormatAddress(); + } + return string.Empty; + } } // Helper class to aggregate historical numbers by type. diff --git a/source/backend/entities/Partials/Property.cs b/source/backend/entities/Partials/Property.cs index cd8aab170b..82d862f509 100644 --- a/source/backend/entities/Partials/Property.cs +++ b/source/backend/entities/Partials/Property.cs @@ -48,7 +48,7 @@ public PimsProperty() /// public PimsProperty(int pid, PimsPropertyType type, PimsAddress address, PimsPropPropTenureTyp tenure, PimsAreaUnitType areaUnit, PimsDataSourceType dataSource, DateTime dataSourceEffectiveDate, PimsPropertyStatusType status) { - this.Pid = pid; + this.Pid = pid > 0 ? pid : null; this.PropertyTypeCodeNavigation = type ?? throw new ArgumentNullException(nameof(type)); this.PropertyTypeCode = type.Id; this.Address = address ?? throw new ArgumentNullException(nameof(address)); diff --git a/source/backend/tests/core/Entities/PropertyHelper.cs b/source/backend/tests/core/Entities/PropertyHelper.cs index 37403877af..a76d2975ee 100644 --- a/source/backend/tests/core/Entities/PropertyHelper.cs +++ b/source/backend/tests/core/Entities/PropertyHelper.cs @@ -22,7 +22,7 @@ public static partial class EntityHelper /// /// /// - public static PimsProperty CreateProperty(int pid, int? pin = null, PimsPropertyType type = null, PimsAddress address = null, PimsPropertyTenureType tenure = null, PimsAreaUnitType areaUnit = null, PimsDataSourceType dataSource = null, PimsPropertyStatusType status = null, PimsLease lease = null, short? regionCode = null, bool? isCoreInventory = null, bool? isRetired = null) + public static PimsProperty CreateProperty(int pid, int? pin = null, PimsPropertyType type = null, PimsAddress address = null, PimsPropertyTenureType tenure = null, PimsAreaUnitType areaUnit = null, PimsDataSourceType dataSource = null, PimsPropertyStatusType status = null, PimsLease lease = null, short? regionCode = null, bool? isCoreInventory = null, bool? isRetired = null, string? surveyPlanNumber = null, bool? noLocation = null) { type ??= CreatePropertyType($"Land-{pid}"); address ??= CreateAddress(pid); @@ -37,9 +37,10 @@ public static PimsProperty CreateProperty(int pid, int? pin = null, PimsProperty PropertyId = pid, Pin = pin, ConcurrencyControlNumber = 1, - Location = new NetTopologySuite.Geometries.Point(0, 0) { SRID = SpatialReference.BCALBERS }, + Location = noLocation != true ? new NetTopologySuite.Geometries.Point(0, 0) { SRID = SpatialReference.BCALBERS } : null, SurplusDeclarationTypeCode = "SURPLUS", IsRetired = false, + SurveyPlanNumber = surveyPlanNumber, }; if (lease != null) diff --git a/source/backend/tests/unit/api/Services/ManagementFileServiceTest.cs b/source/backend/tests/unit/api/Services/ManagementFileServiceTest.cs index a564161781..6e8467d37d 100644 --- a/source/backend/tests/unit/api/Services/ManagementFileServiceTest.cs +++ b/source/backend/tests/unit/api/Services/ManagementFileServiceTest.cs @@ -1064,6 +1064,48 @@ public void UpdateProperties_ValidatePropertyRegions_Success() filePropertyRepository.Verify(x => x.GetPropertiesByManagementFileId(It.IsAny()), Times.Once); } + [Fact] + public void UpdateProperties_DisableProperties_AddsNote() + { + // Arrange + var service = this.CreateManagementServiceWithPermissions(Permissions.ManagementEdit, Permissions.PropertyAdd, Permissions.PropertyView); + + var managementFile = EntityHelper.CreateManagementFile(); + managementFile.ConcurrencyControlNumber = 1; + + var property = EntityHelper.CreateProperty(12345, regionCode: 1); + managementFile.PimsManagementFileProperties = new List() { new PimsManagementFileProperty() { Property = property, Internal_Id = 1, IsActive = true } }; + + var repository = this._helper.GetService>(); + repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); + repository.Setup(x => x.GetById(It.IsAny())).Returns(managementFile); + + var propertyRepository = this._helper.GetService>(); + propertyRepository.Setup(x => x.GetByPid(It.IsAny(), true)).Returns(property); + propertyRepository.Setup(x => x.GetPropertyRegion(It.IsAny())).Returns(1); + + var filePropertyRepository = this._helper.GetService>(); + filePropertyRepository.Setup(x => x.GetPropertiesByManagementFileId(It.IsAny())).Returns(new List() { new PimsManagementFileProperty() { Property = property, Internal_Id = 1, IsActive = false } }); + + var propertyActivityRepository = this._helper.GetService>(); + propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); + + var statusMock = this._helper.GetService>(); + statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); + statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); + + List note = new List(); + var entityNoteRepository = this._helper.GetService>>(); + entityNoteRepository.Setup(x => x.AddNoteRelationship(Capture.In(note))).Returns(new PimsManagementFileNote()); + + // Act + service.UpdateProperties(managementFile, new List()); + + // Assert + entityNoteRepository.Verify(x => x.AddNoteRelationship(It.IsAny()), Times.Once); + note.FirstOrDefault().Note.NoteTxt.Should().Be("Management File property 000-012-345 Enabled"); + } + [Fact] public void UpdateProperties_FinalFile_Error() { diff --git a/source/backend/tests/unit/dal/Helpers/Extensions/PropertyExtensionsTest.cs b/source/backend/tests/unit/dal/Helpers/Extensions/PropertyExtensionsTest.cs new file mode 100644 index 0000000000..bc2fa427b4 --- /dev/null +++ b/source/backend/tests/unit/dal/Helpers/Extensions/PropertyExtensionsTest.cs @@ -0,0 +1,96 @@ +using Moq; +using NetTopologySuite.Geometries; +using Pims.Core.Test; +using Pims.Dal.Entities; +using Pims.Dal.Helpers.Extensions; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Pims.Dal.Helpers.Extensions.Tests +{ + public class PropertyExtensionsTest + { + public static IEnumerable GetPropertyNameTestData() + { + // PID present and valid + yield return new object[] { + new TestFilePropertyEntity + { + Property = EntityHelper.CreateProperty(pid: 123456789) + }, + "123-456-789" + }; + + // PID is 0 (should skip to next) + yield return new object[] { + new TestFilePropertyEntity + { + Property = EntityHelper.CreateProperty(0, pin: 987654321) + }, + "987654321" + }; + + // PIN present and valid + yield return new object[] { + new TestFilePropertyEntity + { + Property = EntityHelper.CreateProperty(0, pin: 1234567) + }, + "1234567" + }; + + // PIN is 0 (should skip to next) + yield return new object[] { + new TestFilePropertyEntity + { + Property = EntityHelper.CreateProperty(0, surveyPlanNumber: "SPN-001") + }, + "SPN-001" + }; + + // SurveyPlanNumber present + yield return new object[] { + new TestFilePropertyEntity + { + Property = EntityHelper.CreateProperty(0, surveyPlanNumber: "SPN-002") + }, + "SPN-002" + }; + + // Location present + yield return new object[] { + new TestFilePropertyEntity + { + Property = EntityHelper.CreateProperty(0) + }, + "0, 0" + }; + + // Address present + yield return new object[] { + new TestFilePropertyEntity + { + Property = EntityHelper.CreateProperty(0, address: EntityHelper.CreateAddress(1, "123 Main St"), noLocation: true) + }, + "123 Main St" + }; + } + + [Theory] + [MemberData(nameof(GetPropertyNameTestData))] + public void GetPropertyName(IFilePropertyEntity fileProperty, string expected) + { + var result = PropertyExtensions.GetPropertyName(fileProperty); + Assert.Equal(expected, result); + } + + // Helper classes for mocking + private class TestFilePropertyEntity : IFilePropertyEntity + { + public PimsProperty Property { get; set; } + public long PropertyId { get; set; } + public Geometry Location { get; set; } + } + } +} diff --git a/source/frontend/src/assets/images/inactive-property.svg b/source/frontend/src/assets/images/inactive-property.svg new file mode 100644 index 0000000000..ab5b72818c --- /dev/null +++ b/source/frontend/src/assets/images/inactive-property.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index 1ec019a459..e1bd3ca7cc 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -7,6 +7,7 @@ import { AnyEventObject } from 'xstate'; import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types'; +import { IMapProperty } from '@/components/propertySelector/models'; import { IGeoSearchParams } from '@/constants/API'; import { IMapSideBarViewState } from '@/features/mapSideBar/MapSideBar'; import { @@ -42,7 +43,7 @@ export interface IMapStateMachineContext { isLoading: boolean; mapSearchCriteria: IPropertyFilter | null; mapFeatureData: MapFeatureData; - filePropertyLocations: LatLngLiteral[]; + filePropertyLocations: IMapProperty[]; pendingFitBounds: boolean; requestedFitBounds: LatLngBounds; isSelecting: boolean; @@ -87,7 +88,7 @@ export interface IMapStateMachineContext { finishReposition: () => void; toggleMapFilterDisplay: () => void; toggleMapLayerControl: () => void; - setFilePropertyLocations: (locations: LatLngLiteral[]) => void; + setFilePropertyLocations: (locations: IMapProperty[]) => void; setMapLayers: (layers: ILayerItem[]) => void; setMapLayersToRefresh: (layers: ILayerItem[]) => void; setDefaultMapLayers: (layers: ILayerItem[]) => void; @@ -351,8 +352,8 @@ export const MapStateMachineProvider: React.FC> serviceSend({ type: 'FINISH_REPOSITION' }); }, [serviceSend]); - const setFilePropertyLocations = useCallback( - (locations: LatLngLiteral[]) => { + const setFileProperties = useCallback( + (locations: IMapProperty[]) => { serviceSend({ type: 'SET_FILE_PROPERTY_LOCATIONS', locations }); }, [serviceSend], @@ -510,7 +511,7 @@ export const MapStateMachineProvider: React.FC> toggleMapFilterDisplay, toggleMapLayerControl, toggleSidebarDisplay, - setFilePropertyLocations, + setFilePropertyLocations: setFileProperties, setVisiblePimsProperties, setShowDisposed, setShowRetired, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index aa8afb09e8..76523acb58 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -6,6 +6,7 @@ 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/DefaultLayers'; import { defaultPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; +import { latLngFromMapProperty } from '@/utils/mapPropertyUtils'; import { emptyFeatureData } from '../models'; import { MachineContext, SideBarType } from './types'; @@ -210,7 +211,9 @@ const mapRequestStates = { // business logic, if there are file properties, use those, otherwise, zoom to a single feature if there is only one, or all features if there are more than one. if (context.filePropertyLocations.length > 0) { - return latLngBounds(context.filePropertyLocations); + return latLngBounds( + context.filePropertyLocations.map(fp => latLngFromMapProperty(fp)), + ); } }, }), diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index 9aa90dacb8..98e94283cb 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -2,6 +2,7 @@ import { LatLngBounds, LatLngLiteral } from 'leaflet'; import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types'; +import { IMapProperty } from '@/components/propertySelector/models'; import { IMapSideBarViewState as IMapSideBarState } from '@/features/mapSideBar/MapSideBar'; import { IPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; @@ -43,7 +44,7 @@ export type MachineContext = { requestedFitBounds: LatLngBounds; requestedFlyTo: RequestedFlyTo; requestedCenterTo: RequestedCenterTo; - filePropertyLocations: LatLngLiteral[]; + filePropertyLocations: IMapProperty[]; activePimsPropertyIds: number[]; activeLayers: ILayerItem[]; mapLayersToRefresh: ILayerItem[]; diff --git a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx index 17f795d8c5..d0a1fa10bd 100644 --- a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx +++ b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx @@ -60,6 +60,7 @@ export interface SelectedFeatureDataset extends FeatureDataset { regionFeature: Feature | null; districtFeature: Feature | null; municipalityFeature: Feature | null; + isActive?: boolean; } const useLocationFeatureLoader = () => { diff --git a/source/frontend/src/components/common/styles.ts b/source/frontend/src/components/common/styles.ts index 0879bb6ff2..523c1a6495 100644 --- a/source/frontend/src/components/common/styles.ts +++ b/source/frontend/src/components/common/styles.ts @@ -253,3 +253,21 @@ export const ListPage = styled.div` gap: 2.5rem; padding: 0; `; + +export const StyledIconWrapper = styled.div` + &.selected { + background-color: ${props => props.theme.bcTokens.themeGold100}; + } + + background-color: ${props => props.theme.css.numberBackgroundColor}; + font-size: 1.5rem; + border-radius: 50%; + opacity: 0.8; + width: 3.25rem; + height: 3.25rem; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + font-family: 'BCSans-Bold'; +`; diff --git a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx index 73e7441450..face4b2ee6 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx @@ -18,12 +18,18 @@ import { PIMS_Property_Boundary_View, PIMS_Property_Location_View, } from '@/models/layers/pimsPropertyLocationView'; -import { exists } from '@/utils'; +import { exists, latLngFromMapProperty } from '@/utils'; import { ONE_HUNDRED_METER_PRECISION } from '../../constants'; import SinglePropertyMarker from '../Markers/SingleMarker'; import { Spiderfier, SpiderSet } from './Spiderfier'; -import { getDraftIcon, getMarkerIcon, pointToLayer, zoomToCluster } from './util'; +import { + getDisabledDraftIcon, + getDraftIcon, + getMarkerIcon, + pointToLayer, + zoomToCluster, +} from './util'; export type PointClustererProps = { bounds?: BBox; @@ -92,10 +98,7 @@ export const PointClusterer: React.FC(() => { return mapMachine.filePropertyLocations.map(x => { // The values on the feature are rounded to the 4th decimal. Do the same to the draft points. - return { - lat: x.lat, - lng: x.lng, - }; + return latLngFromMapProperty(x); }); }, [mapMachine.filePropertyLocations]); @@ -350,12 +353,17 @@ export const PointClusterer: React.FC - {draftPoints.map((draftPoint, index) => { + {mapMachine.filePropertyLocations.map((draftPoint, index) => { + const latLng = latLngFromMapProperty(draftPoint); return ( { @@ -365,7 +373,7 @@ export const PointClusterer: React.FC return notOwnedPropertyIcon; } -// parcel icon (green) highlighted +// parcel icon (blue with number) highlighted export const getDraftIcon = (text: string) => { return L.divIcon({ iconSize: [29, 45], @@ -234,6 +235,17 @@ export const getDraftIcon = (text: string) => { }); }; +// disabled parcel icon (grey with number) highlighted +export const getDisabledDraftIcon = (text: string) => { + return L.divIcon({ + iconSize: [29, 40], + iconAnchor: [15, 40], + popupAnchor: [1, -34], + shadowSize: [41, 41], + html: ReactDOMServer.renderToStaticMarkup(), + }); +}; + /** * Creates a map pin for a single point; e.g. a parcel * @param feature the geojson object diff --git a/source/frontend/src/components/maps/leaflet/SvgMarkers/DisabledDraftMarker.tsx b/source/frontend/src/components/maps/leaflet/SvgMarkers/DisabledDraftMarker.tsx new file mode 100644 index 0000000000..4a1b3e3751 --- /dev/null +++ b/source/frontend/src/components/maps/leaflet/SvgMarkers/DisabledDraftMarker.tsx @@ -0,0 +1,51 @@ +import styled from 'styled-components'; + +interface IDraftMarkerProps { + text?: string; +} + +export const DisabledDraftMarker: React.FunctionComponent< + React.PropsWithChildren +> = ({ text, children }) => { + return ( + + + + + + + + + + {text} + {children} + + ); +}; + +const StyledMarker = styled.svg` + min-width: 2.9rem; + max-width: 2.9rem;1a +`; + +export default DisabledDraftMarker; diff --git a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx index c9b6a81217..f04e73f2f6 100644 --- a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx +++ b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx @@ -51,6 +51,7 @@ export const MapClickMonitor: React.FunctionComponent = ( municipalityFeature: firstOrNull(mapMachine.mapLocationFeatureDataset.municipalityFeatures), selectingComponentId: mapMachine.mapLocationFeatureDataset.selectingComponentId, fileLocation: mapMachine.mapLocationFeatureDataset.fileLocation, + isActive: true, }; const parcelFeaturesNotInPims = mapMachine.mapLocationFeatureDataset.parcelFeatures?.filter(pf => { diff --git a/source/frontend/src/components/propertySelector/models.ts b/source/frontend/src/components/propertySelector/models.ts index bf36f3b46d..355294653b 100644 --- a/source/frontend/src/components/propertySelector/models.ts +++ b/source/frontend/src/components/propertySelector/models.ts @@ -21,6 +21,7 @@ export interface IMapProperty { districtName?: string; landArea?: number; areaUnit?: AreaUnitTypes; + isActive?: boolean; } export interface ILayerSearchCriteria { diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber.tsx new file mode 100644 index 0000000000..f21a6c6f98 --- /dev/null +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber.tsx @@ -0,0 +1,28 @@ +import styled from 'styled-components'; + +import DisabledDraftMarker from '@/components/maps/leaflet/SvgMarkers/DisabledDraftMarker'; + +interface IDisabledDraftCircleNumberProps { + text?: string; +} + +export const DisabledDraftCircleNumber: React.FunctionComponent< + React.PropsWithChildren +> = ({ text }) => { + return ( + + + {text?.length ?? 0 <= 2 ? text : '..'} + + + ); +}; + +const StyledText = styled.text` + font-size: 6rem; + fill: black; + text-anchor: middle; + alignment-baseline: central; +`; + +export default DisabledDraftCircleNumber; diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx index ccfd482f8c..b6d6910735 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx @@ -40,6 +40,8 @@ describe('SelectedPropertyRow component', () => { } index={renderOptions.index ?? 0} onRemove={onRemove} + showDisable={renderOptions?.showDisable} + nameSpace="properties.0" /> )} , @@ -144,4 +146,38 @@ describe('SelectedPropertyRow component', () => { await act(async () => {}); expect(getByText('Address: a test address')).toBeVisible(); }); + + it('shows Inactive as selected when isActive is false', async () => { + const mapProperties: IMapProperty[] = [ + { + pid: '111111111', + latitude: 4, + longitude: 5, + isActive: false, + }, + ]; + const { getByDisplayValue } = setup({ + values: { properties: mapProperties }, + showDisable: true, + }); + await act(async () => {}); + expect(getByDisplayValue('Inactive')).toBeInTheDocument(); + }); + + it('shows Active as selected when isActive is true', async () => { + const mapProperties: IMapProperty[] = [ + { + pid: '111111111', + latitude: 4, + longitude: 5, + isActive: true, + }, + ]; + const { getByDisplayValue } = setup({ + values: { properties: mapProperties }, + showDisable: true, + }); + await act(async () => {}); + expect(getByDisplayValue('Active')).toBeInTheDocument(); + }); }); diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx index fc606cad60..9ab3a0dbe3 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx @@ -2,8 +2,10 @@ import { getIn, useFormikContext } from 'formik'; import { useEffect } from 'react'; import { Col, Row } from 'react-bootstrap'; import { RiDragMove2Line } from 'react-icons/ri'; +import styled from 'styled-components'; import { RemoveButton, StyledIconButton } from '@/components/common/buttons'; +import { Select } from '@/components/common/form'; import { InlineInput } from '@/components/common/form/styles'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; @@ -12,10 +14,13 @@ import DraftCircleNumber from '@/components/propertySelector/selectedPropertyLis import { withNameSpace } from '@/utils/formUtils'; import { featuresetToMapProperty, getPropertyName, NameSourceType } from '@/utils/mapPropertyUtils'; +import DisabledDraftCircleNumber from './DisabledDraftCircleNumber'; + export interface ISelectedPropertyRowProps { index: number; nameSpace?: string; onRemove: () => void; + showDisable?: boolean; property: SelectedFeatureDataset; } @@ -23,6 +28,7 @@ export const SelectedPropertyRow: React.FunctionComponent { const mapMachine = useMapStateMachine(); @@ -50,10 +56,14 @@ export const SelectedPropertyRow: React.FunctionComponent +
- + {property.isActive === false ? ( + + ) : ( + + )}
@@ -67,6 +77,18 @@ export const SelectedPropertyRow: React.FunctionComponent + {showDisable && ( + + + + )} - + - +
); }; +const StyledRow = styled(Row)` + min-height: 4.5rem; +`; + export default SelectedPropertyRow; diff --git a/source/frontend/src/features/disposition/list/DispositionListView.test.tsx b/source/frontend/src/features/disposition/list/DispositionListView.test.tsx index d0cf1a0568..09ff81a6d9 100644 --- a/source/frontend/src/features/disposition/list/DispositionListView.test.tsx +++ b/source/frontend/src/features/disposition/list/DispositionListView.test.tsx @@ -127,6 +127,7 @@ describe('Disposition List View', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/disposition/list/DispositionSearchResults/DispositionSearchResults.test.tsx b/source/frontend/src/features/disposition/list/DispositionSearchResults/DispositionSearchResults.test.tsx index 7c2e422ef7..eaa005c458 100644 --- a/source/frontend/src/features/disposition/list/DispositionSearchResults/DispositionSearchResults.test.tsx +++ b/source/frontend/src/features/disposition/list/DispositionSearchResults/DispositionSearchResults.test.tsx @@ -90,6 +90,7 @@ describe('Disposition search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -101,6 +102,7 @@ describe('Disposition search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -112,6 +114,7 @@ describe('Disposition search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx b/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx index 10dd874bf7..fda0c00856 100644 --- a/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx +++ b/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx @@ -65,6 +65,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -78,6 +79,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -91,6 +93,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -104,6 +107,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -117,6 +121,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx b/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx index aa253e6dbe..c8a176b95b 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx +++ b/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx @@ -72,6 +72,7 @@ describe('Lease Surplus Declaration', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], @@ -107,6 +108,7 @@ describe('Lease Surplus Declaration', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], @@ -150,6 +152,7 @@ describe('Lease Surplus Declaration', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/management/list/ManagementListView.test.tsx b/source/frontend/src/features/management/list/ManagementListView.test.tsx index fb1a1adc3f..39688bca47 100644 --- a/source/frontend/src/features/management/list/ManagementListView.test.tsx +++ b/source/frontend/src/features/management/list/ManagementListView.test.tsx @@ -126,6 +126,7 @@ describe('Management List View', () => { displayOrder: null, file: null, propertyName: null, + isActive: null, location: null, rowVersion: null, }, diff --git a/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx b/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx index b817a301a0..6b3400e1f6 100644 --- a/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx +++ b/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx @@ -86,6 +86,7 @@ describe('Management search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -97,6 +98,7 @@ describe('Management search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -108,6 +110,7 @@ describe('Management search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx index 4043ff538b..575c19e6e6 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionMenu.tsx @@ -6,6 +6,7 @@ import styled from 'styled-components'; import EditButton from '@/components/common/buttons/EditButton'; import { EditPropertiesIcon } from '@/components/common/buttons/EditPropertiesButton'; import { LinkButton } from '@/components/common/buttons/LinkButton'; +import { StyledIconWrapper } from '@/components/common/styles'; import TooltipIcon from '@/components/common/TooltipIcon'; import { Claims } from '@/constants/index'; import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; @@ -143,23 +144,6 @@ const StyledRow = styled(Row)` } `; -const StyledIconWrapper = styled.div` - &.selected { - background-color: ${props => props.theme.bcTokens.themeGold100}; - } - - background-color: ${props => props.theme.css.numberBackgroundColor}; - font-size: 1.5rem; - border-radius: 50%; - opacity: 0.8; - width: 2.5rem; - height: 2.5rem; - padding: 1rem; - display: flex; - justify-content: center; - align-items: center; -`; - const StyledMenuHeaderWrapper = styled.div` display: flex; justify-content: space-between; diff --git a/source/frontend/src/features/mapSideBar/acquisition/common/GenerateForm/hooks/useGenerateH0443.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/common/GenerateForm/hooks/useGenerateH0443.test.tsx index fd89ee6abf..c6a113e6b3 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/common/GenerateForm/hooks/useGenerateH0443.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/common/GenerateForm/hooks/useGenerateH0443.test.tsx @@ -284,6 +284,7 @@ describe('useGenerateH0443 functions', () => { property: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -295,6 +296,7 @@ describe('useGenerateH0443 functions', () => { property: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/mapSideBar/compensation/list/CompensationListContainer.test.tsx b/source/frontend/src/features/mapSideBar/compensation/list/CompensationListContainer.test.tsx index 729486b37f..70cf5c6615 100644 --- a/source/frontend/src/features/mapSideBar/compensation/list/CompensationListContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/compensation/list/CompensationListContainer.test.tsx @@ -298,6 +298,7 @@ describe('compensation list view container', () => { displayOrder: 0, property: undefined, propertyId: 0, + isActive: null, rowVersion: 0, }, { @@ -311,6 +312,7 @@ describe('compensation list view container', () => { displayOrder: 0, property: undefined, propertyId: 0, + isActive: null, rowVersion: 0, }, ], diff --git a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx index 43a653f3a4..d9ca3c8741 100644 --- a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx +++ b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx @@ -7,7 +7,7 @@ import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTy import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { ApiGen_Concepts_Project } from '@/models/api/generated/ApiGen_Concepts_Project'; import { exists } from '@/utils'; -import { getLatLng, locationFromFileProperty } from '@/utils/mapPropertyUtils'; +import { apiToMapProperty } from '@/utils/mapPropertyUtils'; export interface TypedFile extends ApiGen_Concepts_File { fileType: ApiGen_CodeTypes_FileTypes; @@ -119,10 +119,7 @@ export const SideBarContextProvider = (props: { const resetFilePropertyLocations = useCallback(() => { if (exists(fileProperties)) { - const propertyLocations = fileProperties - .map(x => locationFromFileProperty(x)) - .map(y => getLatLng(y)) - .filter(exists); + const propertyLocations = fileProperties.map(x => apiToMapProperty(x)).filter(exists); setFilePropertyLocations && setFilePropertyLocations(propertyLocations); } else { diff --git a/source/frontend/src/features/mapSideBar/disposition/common/DispositionMenu.tsx b/source/frontend/src/features/mapSideBar/disposition/common/DispositionMenu.tsx index 0e10a63e49..2251e95338 100644 --- a/source/frontend/src/features/mapSideBar/disposition/common/DispositionMenu.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/common/DispositionMenu.tsx @@ -6,6 +6,7 @@ import styled from 'styled-components'; import EditButton from '@/components/common/buttons/EditButton'; import { EditPropertiesIcon } from '@/components/common/buttons/EditPropertiesButton'; import { LinkButton } from '@/components/common/buttons/LinkButton'; +import { StyledIconWrapper } from '@/components/common/styles'; import TooltipIcon from '@/components/common/TooltipIcon'; import { Claims } from '@/constants/index'; import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; @@ -134,23 +135,6 @@ const StyledRow = styled(Row)` } `; -const StyledIconWrapper = styled.div` - &.selected { - background-color: ${props => props.theme.bcTokens.themeGold100}; - } - - background-color: ${props => props.theme.css.numberBackgroundColor}; - font-size: 1.5rem; - border-radius: 50%; - opacity: 0.8; - width: 2.5rem; - height: 2.5rem; - padding: 1rem; - display: flex; - justify-content: center; - align-items: center; -`; - const StyledMenuHeaderWrapper = styled.div` display: flex; justify-content: space-between; diff --git a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx index c997ca6863..a15ef95624 100644 --- a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx @@ -1,5 +1,4 @@ import { FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; import React, { useCallback, useContext, @@ -17,6 +16,7 @@ import LeaseIcon from '@/assets/images/lease-icon.svg?react'; import GenericModal from '@/components/common/GenericModal'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import { IMapProperty } from '@/components/propertySelector/models'; import { Claims, Roles } from '@/constants'; import { useLeaseDetail } from '@/features/leases'; import { AddLeaseYupSchema } from '@/features/leases/add/AddLeaseYupSchema'; @@ -37,7 +37,7 @@ import { useLeaseRepository } from '@/hooks/repositories/useLeaseRepository'; import { useQuery } from '@/hooks/use-query'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTypes_FileTypes'; -import { exists, getLatLng, locationFromFileProperty, stripTrailingSlash } from '@/utils'; +import { apiToMapProperty, exists, stripTrailingSlash } from '@/utils'; import GenerateFormView from '../acquisition/common/GenerateForm/GenerateFormView'; import { SideBarContext } from '../context/sidebarContext'; @@ -245,12 +245,9 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos const { setFilePropertyLocations } = useMapStateMachine(); - const locations: LatLngLiteral[] = useMemo(() => { + const locations: IMapProperty[] = useMemo(() => { if (exists(lease?.fileProperties)) { - return lease?.fileProperties - .map(leaseProp => locationFromFileProperty(leaseProp)) - .map(geom => getLatLng(geom)) - .filter(exists); + return lease?.fileProperties.map(leaseProp => apiToMapProperty(leaseProp)).filter(exists); } else { return []; } diff --git a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx index 7812d82ecb..b5d6cc4707 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx @@ -12,8 +12,11 @@ import { } from 'react-router-dom'; import ManagementIcon from '@/assets/images/management-grey-icon.svg?react'; +import Claims from '@/constants/claims'; +import Roles from '@/constants/roles'; import FileLayout from '@/features/mapSideBar/layout/FileLayout'; import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; +import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; 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'; @@ -25,12 +28,12 @@ import { SideBarContext } from '../context/sidebarContext'; 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 SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import UpdateProperties from '../shared/update/properties/UpdateProperties'; import ManagementHeader from './common/ManagementHeader'; -import ManagementMenu from './common/ManagementMenu'; import ManagementRouter from './router/ManagementRouter'; export interface IManagementViewProps { @@ -73,6 +76,7 @@ export const ManagementView: React.FunctionComponent = ({ const history = useHistory(); const match = useRouteMatch(); const { lastUpdatedBy } = useContext(SideBarContext); + const { hasClaim, hasRole } = useKeycloakWrapper(); // match for property menu routes - eg /property/1/ltsa const fileMatch = matchPath>(location.pathname, `${match.path}/:tab`); @@ -111,6 +115,7 @@ export const ManagementView: React.FunctionComponent = ({ confirmBeforeAdd={confirmBeforeAdd} canRemove={canRemove} formikRef={formikRef} + disableProperties confirmBeforeAddMessage={ <>

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

@@ -142,13 +147,13 @@ export const ManagementView: React.FunctionComponent = ({ > + onMenuChange(0)} + onSelectProperty={onMenuChange} + onEditProperties={onShowPropertySelector} + > } bodyComponent={ diff --git a/source/frontend/src/features/mapSideBar/management/common/ManagementMenu.test.tsx b/source/frontend/src/features/mapSideBar/management/common/ManagementMenu.test.tsx deleted file mode 100644 index 6d24f2ea94..0000000000 --- a/source/frontend/src/features/mapSideBar/management/common/ManagementMenu.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { Claims, Roles } from '@/constants/index'; -import { mockManagementFileResponse } from '@/mocks/managementFiles.mock'; -import { toTypeCode } from '@/utils/formUtils'; -import { act, render, RenderOptions, userEvent } from '@/utils/test-utils'; - -import ManagementMenu, { IManagementMenuProps } from './ManagementMenu'; -import { ApiGen_CodeTypes_ManagementFileStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_ManagementFileStatusTypes'; - -// mock auth library - -const onChange = vi.fn(); -const onShowPropertySelector = vi.fn(); - -const testData = ['one', 'two', 'three']; - -describe('ManagementMenu component', () => { - // render component under test - const setup = ( - props: Omit, - renderOptions: RenderOptions = {}, - ) => { - const utils = render( - , - { - useMockAuthentication: true, - claims: [Claims.ACQUISITION_EDIT], - ...renderOptions, - }, - ); - - return { ...utils }; - }; - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('matches snapshot', () => { - const { asFragment } = setup({ - managementFile: mockManagementFileResponse(), - items: testData, - selectedIndex: 0, - }); - expect(asFragment()).toMatchSnapshot(); - }); - - it('renders the items ', () => { - const { getByText } = setup({ - managementFile: mockManagementFileResponse(), - items: testData, - selectedIndex: 0, - }); - - expect(getByText('one')).toBeVisible(); - expect(getByText('two')).toBeVisible(); - expect(getByText('three')).toBeVisible(); - }); - - it('renders selected item with different style', () => { - const { getByTestId } = setup({ - managementFile: mockManagementFileResponse(), - items: testData, - selectedIndex: 1, - }); - - expect(getByTestId('menu-item-row-0')).not.toHaveClass('selected'); - expect(getByTestId('menu-item-row-1')).toHaveClass('selected'); - expect(getByTestId('menu-item-row-2')).not.toHaveClass('selected'); - }); - - it('allows the selected item to be changed', async () => { - const { getByText } = setup({ - managementFile: mockManagementFileResponse(), - items: testData, - selectedIndex: 1, - }); - - const lastItem = getByText('three'); - await act(async () => userEvent.click(lastItem)); - - expect(onChange).toHaveBeenCalledWith(2); - }); - - it(`renders the edit button for users with property edit permissions`, async () => { - const { getByTitle, queryByTestId } = setup( - { - managementFile: mockManagementFileResponse(), - items: testData, - selectedIndex: 1, - }, - { claims: [Claims.MANAGEMENT_EDIT] }, - ); - - const button = getByTitle('Change properties'); - expect(button).toBeVisible(); - - await act(async () => userEvent.click(button)); - - const icon = queryByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); - expect(onShowPropertySelector).toHaveBeenCalled(); - expect(icon).toBeNull(); - }); - - it(`doesn't render the edit button for users without edit permissions`, () => { - const { queryByTitle, queryByTestId } = setup( - { - managementFile: mockManagementFileResponse(), - items: testData, - selectedIndex: 1, - }, - { claims: [Claims.MANAGEMENT_VIEW] }, // no edit permissions, just view. - ); - - const button = queryByTitle('Change properties'); - const icon = queryByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); - expect(button).toBeNull(); - expect(icon).toBeNull(); - }); - - it(`renders the warning icon instead of the edit button for users`, () => { - const { queryByTitle, queryByTestId } = setup( - { - managementFile: { - ...mockManagementFileResponse(), - fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE), - }, - items: testData, - selectedIndex: 1, - }, - { claims: [Claims.MANAGEMENT_EDIT] }, - ); - - const button = queryByTitle('Change properties'); - const icon = queryByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); - expect(button).toBeNull(); - expect(icon).toBeVisible(); - }); - - it(`it renders the warning icon instead of the edit button for system admins`, () => { - const { queryByTitle, queryByTestId } = setup( - { - managementFile: { - ...mockManagementFileResponse(), - fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE), - }, - items: testData, - selectedIndex: 1, - }, - { claims: [Claims.MANAGEMENT_EDIT], roles: [Roles.SYSTEM_ADMINISTRATOR] }, - ); - - const button = queryByTitle('Change properties'); - const icon = queryByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); - expect(button).toBeNull(); - expect(icon).toBeVisible(); - }); - - it(`it renders the warning icon instead of the edit button when file in final state`, () => { - const { queryByTitle, queryByTestId } = setup( - { - managementFile: { - ...mockManagementFileResponse(), - fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE), - }, - items: testData, - selectedIndex: 1, - }, - { claims: [Claims.MANAGEMENT_EDIT] }, - ); - - const button = queryByTitle('Change properties'); - const icon = queryByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); - expect(button).toBeNull(); - expect(icon).toBeVisible(); - }); -}); diff --git a/source/frontend/src/features/mapSideBar/management/common/ManagementMenu.tsx b/source/frontend/src/features/mapSideBar/management/common/ManagementMenu.tsx deleted file mode 100644 index b2a24aa51d..0000000000 --- a/source/frontend/src/features/mapSideBar/management/common/ManagementMenu.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import cx from 'classnames'; -import { Col, Row } from 'react-bootstrap'; -import { FaCaretRight } from 'react-icons/fa'; -import styled from 'styled-components'; - -import EditButton from '@/components/common/buttons/EditButton'; -import { EditPropertiesIcon } from '@/components/common/buttons/EditPropertiesButton'; -import { LinkButton } from '@/components/common/buttons/LinkButton'; -import TooltipIcon from '@/components/common/TooltipIcon'; -import { Claims } from '@/constants/index'; -import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; -import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; - -import { cannotEditMessage } from '../../acquisition/common/constants'; -import { StyledMenuWrapper } from '../../shared/FileMenuView'; -import ManagementStatusUpdateSolver from '../tabs/fileDetails/detail/ManagementStatusUpdateSolver'; - -export interface IManagementMenuProps { - managementFile: ApiGen_Concepts_ManagementFile; - items: string[]; - selectedIndex: number; - onChange: (index: number) => void; - onShowPropertySelector: () => void; -} - -const ManagementMenu: React.FunctionComponent< - React.PropsWithChildren -> = props => { - const { hasClaim } = useKeycloakWrapper(); - const handleClick = (index: number) => { - props.onChange(index); - }; - const statusSolver = new ManagementStatusUpdateSolver(props.managementFile); - const canEditDetails = () => { - if (statusSolver?.canEditProperties()) { - return true; - } - return false; - }; - - return ( - - {props.items.map((label: string, index: number) => { - const activeIndex = props.selectedIndex === index; - if (index === 0) { - return ( - - {activeIndex ? ( - - {label} - - ) : ( - - handleClick(index)}> - {label} - - - )} - - Properties - {hasClaim(Claims.MANAGEMENT_EDIT) && canEditDetails() && ( - } - onClick={props.onShowPropertySelector} - /> - )} - {hasClaim(Claims.MANAGEMENT_EDIT) && !canEditDetails() && ( - - )} - - - ); - } else { - return ( - (props.selectedIndex !== index ? handleClick(index) : '')} - > - {props.selectedIndex === index && } - - - {index} - - - {activeIndex ? ( - - {label} - - ) : ( - - {label} - - )} - - ); - } - })} - - ); -}; - -export default ManagementMenu; - -const StyledMenuCol = styled(Col)` - min-height: 2.5rem; - line-height: 3rem; -`; - -const StyledRow = styled(Row)` - &.selected { - font-weight: bold; - cursor: default; - } - - font-size: 1.4rem; - font-weight: normal; - cursor: pointer; - padding-bottom: 0.5rem; - - div.Button__value { - font-size: 1.4rem; - } -`; - -const StyledIconWrapper = styled.div` - &.selected { - background-color: ${props => props.theme.bcTokens.themeGold100}; - } - - background-color: ${props => props.theme.css.numberBackgroundColor}; - font-size: 1.5rem; - border-radius: 50%; - opacity: 0.8; - width: 2.5rem; - height: 2.5rem; - padding: 1rem; - display: flex; - justify-content: center; - align-items: center; -`; - -const StyledMenuHeaderWrapper = styled.div` - display: flex; - justify-content: space-between; - align-items: flex-end; - width: 100%; - border-bottom: 1px solid ${props => props.theme.css.borderOutlineColor}; -`; - -const StyledMenuHeader = styled.span` - font-weight: bold; - font-size: 1.6rem; - color: ${props => props.theme.bcTokens.iconsColorSecondary}; - line-height: 2.2rem; -`; diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx index e4650af73a..aea8951a78 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx @@ -53,6 +53,7 @@ export const FileActivityDetailView: React.FunctionComponent< propertyId: ap.propertyId, propertyName: null, rowVersion: null, + isActive: null, })); if (props.activity !== null) { diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx index 7df49f9c2c..474bce82de 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx @@ -81,11 +81,13 @@ export const ManagementActivityEditForm: React.FunctionComponent< activityForm.activityStatusCode = 'NOTSTARTED'; activityForm.requestedDate = getCurrentIsoDate(); // By default, all properties are selected but user can unselect all or some - activityForm.activityProperties = (managementFile.fileProperties ?? []).map(fp => { - const newActivityProperty = new ActivityPropertyFormModel(); - newActivityProperty.propertyId = fp.propertyId; - return newActivityProperty; - }); + activityForm.activityProperties = (managementFile.fileProperties ?? []) + .filter(fp => fp.isActive !== false) + .map(fp => { + const newActivityProperty = new ActivityPropertyFormModel(); + newActivityProperty.propertyId = fp.propertyId; + return newActivityProperty; + }); } setActivityType(activityForm.activityTypeCode); diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx index b965feecd6..2c7c73e2bf 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx @@ -1,6 +1,8 @@ import { Col, Row } from 'react-bootstrap'; import styled from 'styled-components'; +import { StyledIconWrapper } from '@/components/common/styles'; + export interface IAssociationHeaderProps { icon: React.ReactNode; title: string; @@ -30,20 +32,3 @@ export default AssociationHeader; const StyledAssociationHeaderWrapper = styled.div` color: ${props => props.theme.bcTokens.iconsColorSecondary}; `; - -const StyledIconWrapper = styled.div` - color: white; - background-color: ${props => props.theme.css.activeActionColor}; - - font-size: 1.5rem; - - border-radius: 50%; - - width: 2.5rem; - height: 2.5rem; - padding: 1rem; - - display: flex; - justify-content: center; - align-items: center; -`; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx index 424fc82bb0..6cf02638e1 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx @@ -59,6 +59,7 @@ describe('PropertyResearchTabView component', () => { const fakePropertyResearch: ApiGen_Concepts_ResearchFileProperty = { id: 0, + isActive: null, propertyId: 1, property: getEmptyProperty(), file: getEmptyResearchFile(), diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts index b70b7d3472..0cf35a2690 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts @@ -113,6 +113,7 @@ export class UpdatePropertyFormModel { researchSummary: this.researchSummary ?? null, property: null, location: null, + isActive: null, fileId: this.researchFileId ?? 0, file: { ...getEmptyResearchFile(), rowVersion: this.researchFileRowVersion }, propertyResearchPurposeTypes: this.propertyResearchPurposeTypes?.map(x => x.toApi()) ?? null, diff --git a/source/frontend/src/features/mapSideBar/research/common/ResearchMenu.tsx b/source/frontend/src/features/mapSideBar/research/common/ResearchMenu.tsx index 015b6d5d2b..674f0b2dba 100644 --- a/source/frontend/src/features/mapSideBar/research/common/ResearchMenu.tsx +++ b/source/frontend/src/features/mapSideBar/research/common/ResearchMenu.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { LinkButton } from '@/components/common/buttons'; import { EditButton } from '@/components/common/buttons/EditButton'; import { EditPropertiesIcon } from '@/components/common/buttons/EditPropertiesButton'; +import { StyledIconWrapper } from '@/components/common/styles'; import TooltipIcon from '@/components/common/TooltipIcon'; import { Claims } from '@/constants/index'; import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; @@ -121,26 +122,6 @@ const StyledRow = styled(Row)` } `; -const StyledIconWrapper = styled.div` - &.selected { - background-color: ${props => props.theme.bcTokens.themeGold100}; - } - background-color: ${props => props.theme.css.numberBackgroundColor}; - - font-size: 1.5rem; - - border-radius: 50%; - opacity: 0.8; - - width: 2.5rem; - height: 2.5rem; - padding: 1rem; - - display: flex; - justify-content: center; - align-items: center; -`; - const StyledMenuHeaderWrapper = styled.div` display: flex; justify-content: space-between; diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx new file mode 100644 index 0000000000..23dec27e63 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx @@ -0,0 +1,78 @@ +import cx from 'classnames'; +import * as React from 'react'; +import { Col } from 'react-bootstrap'; +import { FaCaretRight } from 'react-icons/fa'; +import styled from 'styled-components'; + +import { LinkButton } from '@/components/common/buttons'; +import { StyledIconWrapper } from '@/components/common/styles'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; +import { getFilePropertyName } from '@/utils/mapPropertyUtils'; + +import { StyledRow } from './FileMenuView'; + +interface IFileMenuRowProps { + index: number; + currentPropertyIndex: number; + property: ApiGen_Concepts_FileProperty; + onSelectProperty: (propertyId: number) => void; +} + +export const FileMenuRow: React.FunctionComponent = ({ + index, + currentPropertyIndex, + property, + onSelectProperty, +}) => { + const propertyName = getFilePropertyName(property); + return ( + { + if (currentPropertyIndex !== index) { + onSelectProperty(property.id); + } + }} + > + {currentPropertyIndex === index && } + + {property?.isActive !== false ? ( + + {index + 1} + + ) : ( + {index + 1} + )} + + + {currentPropertyIndex === index ? ( + {propertyName.value} + ) : ( + {propertyName.value} + )} + + + ); +}; + +const StyledDisabledIconWrapper = styled.div` + &.selected { + border-color: ${props => props.theme.bcTokens.themeGray110}; + } + border: solid 0.3rem; + border-color: ${props => props.theme.bcTokens.themeGray100}; + font-size: 1.5rem; + border-radius: 20%; + width: 3.25rem; + height: 3.25rem; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + color: black; + font-family: 'BCSans-Bold'; +`; + +export default FileMenuRow; diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx index a13bcdea2c..f43fa91d4e 100644 --- a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx +++ b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx @@ -1,7 +1,6 @@ import cx from 'classnames'; import { useMemo } from 'react'; import { Col, Row } from 'react-bootstrap'; -import { FaCaretRight } from 'react-icons/fa'; import { matchPath, useLocation } from 'react-router-dom'; import styled from 'styled-components'; @@ -10,7 +9,9 @@ import { EditPropertiesIcon } from '@/components/common/buttons/EditPropertiesBu import { LinkButton } from '@/components/common/buttons/LinkButton'; import TooltipIcon from '@/components/common/TooltipIcon'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; -import { exists, getFilePropertyName } from '@/utils'; +import { exists } from '@/utils'; + +import FileMenuRow from './FileMenuRow'; export interface IFileMenuProps { properties: ApiGen_Concepts_FileProperty[]; @@ -46,6 +47,16 @@ const FileMenu: React.FunctionComponent> const isSummary = useMemo(() => !exists(currentPropertyIndex), [currentPropertyIndex]); + const activeProperties = []; + const inactiveProperties = []; + properties.forEach(p => { + if (p.isActive !== false) { + activeProperties.push(p); + } else { + inactiveProperties.push(p); + } + }); + return ( @@ -77,33 +88,28 @@ const FileMenu: React.FunctionComponent>
- {properties.map((property: ApiGen_Concepts_FileProperty, index: number) => { - const propertyName = getFilePropertyName(property); +

Active

+ {activeProperties.map((property: ApiGen_Concepts_FileProperty, index: number) => { + return ( + + ); + })} +

Inactive

+ {inactiveProperties.map((property: ApiGen_Concepts_FileProperty, index: number) => { return ( - { - if (currentPropertyIndex !== index) { - onSelectProperty(property.id); - } - }} - > - {currentPropertyIndex === index && } - - - {index + 1} - - - - {currentPropertyIndex === index ? ( - {propertyName.value} - ) : ( - {propertyName.value} - )} - - + ); })}
@@ -126,41 +132,6 @@ export const StyledMenuWrapper = styled.div` flex-direction: column; `; -const StyledRow = styled(Row)` - width: 100%; - - &.selected { - font-weight: bold; - cursor: default; - } - - font-size: 1.4rem; - font-weight: normal; - cursor: pointer; - padding-bottom: 0.5rem; - - div.Button__value { - font-size: 1.4rem; - } -`; - -const StyledIconWrapper = styled.div` - &.selected { - background-color: ${props => props.theme.bcTokens.themeGold100}; - } - - background-color: ${props => props.theme.css.numberBackgroundColor}; - font-size: 1.5rem; - border-radius: 50%; - opacity: 0.8; - width: 2.5rem; - height: 2.5rem; - padding: 1rem; - display: flex; - justify-content: center; - align-items: center; -`; - const StyledMenuHeaderWrapper = styled.div` display: flex; justify-content: space-between; @@ -179,3 +150,21 @@ const StyledMenuHeader = styled.span` const StyledMenuBodyWrapper = styled.div` flex-grow: 1; `; + +export const StyledRow = styled(Row)` + width: 100%; + + &.selected { + font-weight: bold; + cursor: default; + } + + font-size: 1.4rem; + font-weight: normal; + cursor: pointer; + padding-bottom: 0.5rem; + + div.Button__value { + font-size: 1.4rem; + } +`; diff --git a/source/frontend/src/features/mapSideBar/shared/models.ts b/source/frontend/src/features/mapSideBar/shared/models.ts index 69628efa1f..1ffc74ce88 100644 --- a/source/frontend/src/features/mapSideBar/shared/models.ts +++ b/source/frontend/src/features/mapSideBar/shared/models.ts @@ -93,6 +93,7 @@ export class PropertyForm { public areaUnit?: AreaUnitTypes; public isRetired?: boolean; public isDisposed?: boolean; + public isActive?: string; public constructor(baseModel?: Partial) { Object.assign(this, baseModel); @@ -116,6 +117,7 @@ export class PropertyForm { formattedAddress: model.address, landArea: model.landArea, areaUnit: model.areaUnit, + isActive: model.isActive !== false ? 'true' : 'false', }); } @@ -160,6 +162,7 @@ export class PropertyForm { pimsFeature?.properties?.LAND_LEGAL_DESCRIPTION ?? parcelFeature?.properties?.LEGAL_DESCRIPTION ?? '', + isActive: model.isActive !== false ? 'true' : 'false', }); } @@ -178,6 +181,7 @@ export class PropertyForm { districtName: this.districtName, legalDescription: this.legalDescription, address: this.address ? formatApiAddress(this.address.toApi()) : this.formattedAddress, + isActive: this.isActive !== 'false', }; } @@ -237,6 +241,7 @@ export class PropertyForm { geometry: null, }, municipalityFeature: null, + isActive: this.isActive !== 'false', }; } @@ -269,6 +274,7 @@ export class PropertyForm { : undefined; newForm.legalDescription = model.property?.landLegalDescription ?? undefined; newForm.isRetired = model.property?.isRetired ?? undefined; + newForm.isActive = model.isActive !== false ? 'true' : 'false'; return newForm; } @@ -308,6 +314,7 @@ export class PropertyForm { propertyName: this.name ?? null, location: latLngToApiLocation(this.fileLocation?.lat, this.fileLocation?.lng), displayOrder: this.displayOrder ?? null, + isActive: this.isActive !== 'false', rowVersion: this.rowVersion ?? null, }; } 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 17bc2bb566..6cead8a028 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 @@ -82,6 +82,7 @@ describe('UpdateProperties component', () => { fileProperties: [ { id: 3, + isActive: null, propertyId: 443, property: { ...getMockApiProperty(), diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx index 045cc282a4..8e65dc52ae 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx @@ -37,6 +37,7 @@ export interface IUpdatePropertiesProps { confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; confirmBeforeAddMessage?: React.ReactNode; formikRef?: React.RefObject>; + disableProperties?: boolean; } export const UpdateProperties: React.FunctionComponent = props => { @@ -234,6 +235,7 @@ export const UpdateProperties: React.FunctionComponent = nameSpace={`properties.${index}`} index={index} property={property.toFeatureDataset()} + showDisable={props.disableProperties} /> ))} {formikProps.values.properties.length === 0 && ( diff --git a/source/frontend/src/features/research/list/ResearchListView.test.tsx b/source/frontend/src/features/research/list/ResearchListView.test.tsx index 73bbb77e1b..ea888c354a 100644 --- a/source/frontend/src/features/research/list/ResearchListView.test.tsx +++ b/source/frontend/src/features/research/list/ResearchListView.test.tsx @@ -643,6 +643,7 @@ const mockResearchListViewResponse: ApiGen_Concepts_ResearchFile[] = [ fileProperties: [ { id: 1, + isActive: null, propertyId: 1, property: { ...getMockApiProperty(), @@ -675,6 +676,7 @@ const mockResearchListViewResponse: ApiGen_Concepts_ResearchFile[] = [ }, { id: 2, + isActive: null, propertyId: 2, property: { ...getMockApiProperty(), diff --git a/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts b/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts index 1dd618f11c..41f962a6c5 100644 --- a/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts +++ b/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts @@ -1,4 +1,3 @@ -import { LatLngLiteral } from 'leaflet'; import debounce from 'lodash/debounce'; import { useCallback, useRef } from 'react'; @@ -6,16 +5,6 @@ import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineCo import { IMapProperty } from '@/components/propertySelector/models'; import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; import useIsMounted from '@/hooks/util/useIsMounted'; -import { latLngFromMapProperty } from '@/utils'; - -/** - * Get a list of file property markers from the current form values. - * As long as a parcel/building has both a lat and a lng it will be returned by this method. - * @param modifiedProperties the current form values to extract lat/lngs from. - */ -const getFilePropertyLocations = (modifiedProperties: IMapProperty[]): LatLngLiteral[] => { - return modifiedProperties.map((property: IMapProperty) => latLngFromMapProperty(property)); -}; /** * A hook that automatically syncs any updates to the lat/lngs of the parcel form with the map. @@ -33,9 +22,8 @@ const useDraftMarkerSynchronizer = (modifiedProperties: IMapProperty[]) => { const synchronizeMarkers = useCallback( (modifiedProperties: IMapProperty[]) => { if (isMounted()) { - const filePropertyLocations = getFilePropertyLocations(modifiedProperties); - if (filePropertyLocations.length > 0) { - setFilePropertyLocations(filePropertyLocations); + if (modifiedProperties.length > 0) { + setFilePropertyLocations(modifiedProperties); } else { setFilePropertyLocations([]); } @@ -53,8 +41,6 @@ const useDraftMarkerSynchronizer = (modifiedProperties: IMapProperty[]) => { useDeepCompareEffect(() => { synchronize(modifiedProperties); }, [modifiedProperties, synchronize]); - - return; }; export default useDraftMarkerSynchronizer; diff --git a/source/frontend/src/mocks/acquisitionFiles.mock.ts b/source/frontend/src/mocks/acquisitionFiles.mock.ts index 417b90b02e..9c177f46df 100644 --- a/source/frontend/src/mocks/acquisitionFiles.mock.ts +++ b/source/frontend/src/mocks/acquisitionFiles.mock.ts @@ -127,6 +127,7 @@ export const mockAcquisitionFileResponse = ( fileProperties: [ { id: 1, + isActive: null, propertyId: 292, property: { ...getMockApiProperty(), @@ -170,6 +171,7 @@ export const mockAcquisitionFileResponse = ( }, { id: 2, + isActive: null, propertyId: 443, property: { ...getMockApiProperty(), diff --git a/source/frontend/src/mocks/compensations.mock.ts b/source/frontend/src/mocks/compensations.mock.ts index 79c833ae3a..faa1b3e859 100644 --- a/source/frontend/src/mocks/compensations.mock.ts +++ b/source/frontend/src/mocks/compensations.mock.ts @@ -154,6 +154,7 @@ export const getMockApiCompensationWithProperty = (): ApiGen_Concepts_Compensati propertyAcquisitionFileId: 2, acquisitionFileProperty: { id: 1, + isActive: null, file: null, propertyName: '', displayOrder: 0, @@ -177,6 +178,7 @@ export const getMockApiCompensationWithProperty = (): ApiGen_Concepts_Compensati propertyAcquisitionFileId: 2, acquisitionFileProperty: { id: 2, + isActive: null, file: null, propertyName: '', displayOrder: 0, @@ -434,6 +436,7 @@ export const getMockCompensationPropertiesReq = (): ApiGen_Concepts_AcquisitionF { file: null, id: 1, + isActive: null, propertyName: 'Property Test Name 1', location: { coordinate: { @@ -542,6 +545,7 @@ export const getMockCompensationPropertiesReq = (): ApiGen_Concepts_AcquisitionF { file: null, id: 26, + isActive: null, propertyName: 'Property Test Name 2', location: { coordinate: { diff --git a/source/frontend/src/mocks/fileProperty.mock.ts b/source/frontend/src/mocks/fileProperty.mock.ts index 04036df724..b1a04b1e33 100644 --- a/source/frontend/src/mocks/fileProperty.mock.ts +++ b/source/frontend/src/mocks/fileProperty.mock.ts @@ -5,6 +5,7 @@ import { getEmptyBaseAudit } from '@/models/defaultInitializers'; export const getEmptyFileProperty = (): ApiGen_Concepts_FileProperty => { return { id: 0, + isActive: null, propertyName: null, displayOrder: null, property: null, @@ -19,6 +20,7 @@ export const getEmptyFileProperty = (): ApiGen_Concepts_FileProperty => { export const getEmptyLeaseFileProperty = (): ApiGen_Concepts_PropertyLease => { const a: ApiGen_Concepts_PropertyLease = { file: undefined, + isActive: null, leaseArea: 0, areaUnitType: undefined, id: 0, diff --git a/source/frontend/src/mocks/lease.mock.ts b/source/frontend/src/mocks/lease.mock.ts index af79512a70..d9149fb92a 100644 --- a/source/frontend/src/mocks/lease.mock.ts +++ b/source/frontend/src/mocks/lease.mock.ts @@ -1675,6 +1675,7 @@ export const getMockLeaseStakeholders = (leaseId = 1): ApiGen_Concepts_LeaseStak export const getMockLeaseProperties = (leaseId = 1): ApiGen_Concepts_PropertyLease[] => [ { file: null, + isActive: null, leaseArea: 0, areaUnitType: null, id: 387, diff --git a/source/frontend/src/mocks/managementFiles.mock.ts b/source/frontend/src/mocks/managementFiles.mock.ts index b09cb33254..ee4a877f5c 100644 --- a/source/frontend/src/mocks/managementFiles.mock.ts +++ b/source/frontend/src/mocks/managementFiles.mock.ts @@ -149,6 +149,7 @@ export const mockManagementFilePropertiesResponse = (): ApiGen_Concepts_ManagementFileProperty[] => [ { id: 1, + isActive: null, propertyName: null, displayOrder: null, location: null, diff --git a/source/frontend/src/mocks/properties.mock.ts b/source/frontend/src/mocks/properties.mock.ts index bc2c72a272..42e42833aa 100644 --- a/source/frontend/src/mocks/properties.mock.ts +++ b/source/frontend/src/mocks/properties.mock.ts @@ -179,6 +179,7 @@ export const getMockApiPropertyFiles = (): ApiGen_Concepts_FileProperty[] => [ { id: 1, fileId: 1, + isActive: null, file: mockAcquisitionFileResponse(), propertyName: 'test property name', propertyId: 1, @@ -222,6 +223,7 @@ export const getMockApiPropertyFiles = (): ApiGen_Concepts_FileProperty[] => [ }, { id: 2, + isActive: null, propertyId: 2, fileId: 2, file: mockAcquisitionFileResponse(), @@ -278,6 +280,7 @@ export const getEmptyPropertyLease = (): ApiGen_Concepts_PropertyLease => { property: null, propertyId: 0, location: null, + isActive: null, rowVersion: null, }; }; diff --git a/source/frontend/src/mocks/researchFile.mock.ts b/source/frontend/src/mocks/researchFile.mock.ts index 375b53ac80..739b40c587 100644 --- a/source/frontend/src/mocks/researchFile.mock.ts +++ b/source/frontend/src/mocks/researchFile.mock.ts @@ -20,6 +20,7 @@ export const getMockResearchFile = (): ApiGen_Concepts_ResearchFile => ({ fileProperties: [ { id: 55, + isActive: null, propertyId: 495, property: { ...getMockApiProperty(), @@ -150,6 +151,7 @@ export const getEmptyResearchFileProperty = (): ApiGen_Concepts_ResearchFileProp propertyResearchPurposeTypes: null, researchSummary: null, location: null, + isActive: null, ...getEmptyBaseAudit(), }); @@ -161,7 +163,7 @@ export const getMockResearchFileProperty = ( fileId, propertyName: 'Corner of Nakya PL', propertyId: 495, - + isActive: null, propertyResearchPurposeTypes: [ { id: 22, diff --git a/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts b/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts index 05e4a9862a..dbb0fbcc5e 100644 --- a/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts +++ b/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts @@ -15,6 +15,7 @@ export interface ApiGen_Concepts_FileProperty extends ApiGen_Base_BaseConcurrent location: ApiGen_Concepts_Geometry | null; displayOrder: number | null; property: ApiGen_Concepts_Property | null; + isActive: boolean | null; propertyId: number; file: ApiGen_Concepts_File | null; } diff --git a/source/frontend/src/models/api/generated/ApiGen_Concepts_PropertyActivity.ts b/source/frontend/src/models/api/generated/ApiGen_Concepts_PropertyActivity.ts index 8d38e19103..e93256ee1c 100644 --- a/source/frontend/src/models/api/generated/ApiGen_Concepts_PropertyActivity.ts +++ b/source/frontend/src/models/api/generated/ApiGen_Concepts_PropertyActivity.ts @@ -20,7 +20,6 @@ export interface ApiGen_Concepts_PropertyActivity extends ApiGen_Base_BaseAudit managementFileId: number | null; managementFile: ApiGen_Concepts_ManagementFile | null; activityTypeCode: ApiGen_Base_CodeType | null; - activitySubtypeCode: ApiGen_Base_CodeType | null; activityStatusTypeCode: ApiGen_Base_CodeType | null; requestAddedDateOnly: UtcIsoDate; completionDateOnly: UtcIsoDate | null; diff --git a/source/frontend/src/utils/mapPropertyUtils.ts b/source/frontend/src/utils/mapPropertyUtils.ts index cc29b9c0da..f8754bff7e 100644 --- a/source/frontend/src/utils/mapPropertyUtils.ts +++ b/source/frontend/src/utils/mapPropertyUtils.ts @@ -171,6 +171,30 @@ export const getFeatureBoundedCenter = (feature: Feature, address?: string, From bd59f78390d47b03342ebd88b8596567150a103e Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Tue, 17 Jun 2025 17:35:32 -0700 Subject: [PATCH 02/61] ensure that clicking on a property selects the correct entry in the file. --- .../src/components/maps/leaflet/Layers/PointClusterer.tsx | 6 +++--- .../features/mapSideBar/management/ManagementContainer.tsx | 7 ++++--- .../src/features/mapSideBar/management/ManagementView.tsx | 6 +++--- .../src/features/mapSideBar/router/FilePropertyRouter.tsx | 1 + .../src/features/mapSideBar/shared/FileMenuView.tsx | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx index face4b2ee6..336007b7eb 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx @@ -388,11 +388,11 @@ export const PointClusterer: React.FC onClose && onClose(), [onClose]); - const navigateToMenuRoute = (selectedIndex: number) => { - const route = selectedIndex === 0 ? '' : `/property/${selectedIndex}`; - history.push(`${stripTrailingSlash(match.url)}${route}`); + const navigateToMenuRoute = (propertyId: number) => { + const menuIndex = managementFile.fileProperties.findIndex(x => x.id === propertyId); + + history.push(`${stripTrailingSlash(match.url)}/property/${menuIndex + 1}`); }; const onMenuChange = (selectedIndex: number) => { diff --git a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx index b5d6cc4707..970ab1a8c0 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx @@ -89,8 +89,6 @@ export const ManagementView: React.FunctionComponent = ({ `${stripTrailingSlash(match.path)}/property/:menuIndex/:tab`, ); - const selectedMenuIndex = propertiesMatch !== null ? Number(propertiesMatch.params.menuIndex) : 0; - const formTitle = isEditing ? getEditTitle(fileMatch, propertySelectorMatch, propertiesMatch) : 'Management File'; @@ -177,7 +175,9 @@ export const ManagementView: React.FunctionComponent = ({ render={({ match }) => ( = props => { return null; } + console.log(props.selectedMenuIndex); const fileProperty = getFileProperty(props.file, props.selectedMenuIndex); if (fileProperty == null) { toast.warn('Could not find property in the file, showing file details instead', { diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx index f43fa91d4e..8a30a15024 100644 --- a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx +++ b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx @@ -104,8 +104,8 @@ const FileMenu: React.FunctionComponent> {inactiveProperties.map((property: ApiGen_Concepts_FileProperty, index: number) => { return ( Date: Tue, 24 Jun 2025 10:45:31 -0600 Subject: [PATCH 03/61] =?UTF-8?q?PSP-10589=20:=20FT-REG:=20Management=20Fi?= =?UTF-8?q?les=20Activity=20-=20Properties=20at=20the=20Act=E2=80=A6=20(#4?= =?UTF-8?q?859)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ManagementActivityEditContainer.test.tsx | 10 +++++ .../edit/ManagementActivityEditContainer.tsx | 17 +++++++- .../edit/ManagementActivityEditForm.test.tsx | 41 ++++++++++++++++--- .../edit/ManagementActivityEditForm.tsx | 39 +----------------- .../management/tabs/activities/edit/models.ts | 37 +++++++++++++++-- 5 files changed, 96 insertions(+), 48 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.test.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.test.tsx index 15ae14f93e..e7bfc8c326 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.test.tsx @@ -17,6 +17,7 @@ import { } from './ManagementActivityEditContainer'; import { IManagementActivityEditFormProps } from './ManagementActivityEditForm'; import { ManagementActivityFormModel } from './models'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; const history = createMemoryHistory(); @@ -130,6 +131,15 @@ describe('ManagementActivityEditContainer component', () => { rowVersion: 1, }, ], + selectedProperties: [ + { + fileId: null, + id: 0, + property: null, + propertyId: 1, + rowVersion: 0, + } as ApiGen_Concepts_FileProperty, + ], rowVersion: 1, } as ManagementActivityFormModel), ); diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.tsx index e940311425..a0d18c2746 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditContainer.tsx @@ -4,6 +4,7 @@ import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; import useActivityContactRetriever from '@/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/hooks'; import usePathGenerator from '@/features/mapSideBar/shared/sidebarPathGenerator'; import { useManagementActivityRepository } from '@/hooks/repositories/useManagementActivityRepository'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; import { ApiGen_Concepts_PropertyActivity } from '@/models/api/generated/ApiGen_Concepts_PropertyActivity'; import { SystemConstants, useSystemConstants } from '@/store/slices/systemConstants'; @@ -61,17 +62,31 @@ export const ManagementActivityEditContainer: React.FunctionComponent< } await fetchProviderContact(retrieved); - setInitialValues(ManagementActivityFormModel.fromApi(retrieved)); + setInitialValues(ManagementActivityFormModel.fromApi(retrieved, castedFile.fileProperties)); } } else { // Create activity flow const defaultModel = new ManagementActivityFormModel(null, managementFileId); defaultModel.activityStatusCode = 'NOTSTARTED'; defaultModel.requestedDate = getCurrentIsoDate(); + defaultModel.selectedProperties = (castedFile?.fileProperties ?? []).map(x => { + return { + id: x.id, + fileId: castedFile.id, + propertyName: x.propertyName, + location: x.location, + displayOrder: x.displayOrder, + property: x.property, + propertyId: x.propertyId, + } as ApiGen_Concepts_FileProperty; + }); + setInitialValues(defaultModel); } }, [ activityId, + castedFile.fileProperties, + castedFile.id, fetchMinistryContacts, fetchPartiesContact, fetchProviderContact, diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.test.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.test.tsx index 1bbe1a0dbe..dc6355017f 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.test.tsx @@ -20,6 +20,7 @@ import { ManagementActivityEditForm, } from './ManagementActivityEditForm'; import { ManagementActivityFormModel } from './models'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; // Need to mock this library for unit tests vi.mock('react-visibility-sensor', () => { @@ -38,10 +39,13 @@ const storeState = { }; const mockManagementActivityFormValues: ManagementActivityFormModel = - ManagementActivityFormModel.fromApi({ - ...getMockPropertyManagementActivity(1), - activityProperties: [], - }); + ManagementActivityFormModel.fromApi( + { + ...getMockPropertyManagementActivity(1), + activityProperties: [], + }, + mockManagementFileResponse().fileProperties, + ); const onCancel = vi.fn(); const onSave = vi.fn(); @@ -95,7 +99,10 @@ describe('ManagementActivityEditForm component', () => { }); beforeEach(() => { - initialValues = ManagementActivityFormModel.fromApi(getMockPropertyManagementActivity(1)); + initialValues = ManagementActivityFormModel.fromApi( + getMockPropertyManagementActivity(1), + mockManagementFileResponse().fileProperties, + ); }); afterEach(() => { @@ -261,6 +268,17 @@ describe('ManagementActivityEditForm component', () => { it('shows all properties selected by default when creating management activity', async () => { const mockDefaultValues = new ManagementActivityFormModel(null, 1); + mockDefaultValues.selectedProperties = mockManagementFileResponse().fileProperties.map(x => { + return { + id: x.id, + fileId: mockManagementFileResponse().id, + propertyName: x.propertyName, + location: x.location, + displayOrder: x.displayOrder, + property: x.property, + propertyId: x.propertyId, + } as ApiGen_Concepts_FileProperty; + }); await setup({ props: { @@ -275,8 +293,19 @@ describe('ManagementActivityEditForm component', () => { expect(screen.getByTestId('selectrow-10')).toBeChecked(); }); - it('can select management file properties', async () => { + it('can de-select management file properties', async () => { const mockDefaultValues = new ManagementActivityFormModel(null, 1); + mockDefaultValues.selectedProperties = mockManagementFileResponse().fileProperties.map(x => { + return { + id: x.id, + fileId: mockManagementFileResponse().id, + propertyName: x.propertyName, + location: x.location, + displayOrder: x.displayOrder, + property: x.property, + propertyId: x.propertyId, + } as ApiGen_Concepts_FileProperty; + }); await setup({ props: { diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx index 479aa59137..8d60b87605 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/ManagementActivityEditForm.tsx @@ -23,7 +23,6 @@ import { PROP_MGMT_ACTIVITY_TYPES, } from '@/constants/API'; import SaveCancelButtons from '@/features/leases/SaveCancelButtons'; -import { ActivityPropertyFormModel } from '@/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/edit/models'; import { ManagementActivitySubTypeModel } from '@/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/models/ManagementActivitySubType'; import { StyledFormWrapper } from '@/features/mapSideBar/shared/styles'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; @@ -115,18 +114,6 @@ export const ManagementActivityEditForm: React.FunctionComponent< [activitySubTypeCodes], ); - const getSelectedProperties = (): ApiGen_Concepts_FileProperty[] => { - if (isValidId(initialValues.id)) { - return ( - initialValues.activityProperties - .map(ap => managementFile.fileProperties?.find(fp => fp.propertyId === ap.propertyId)) - .filter(exists) ?? [] - ); - } - - return managementFile.fileProperties ?? []; - }; - useEffect(() => { if (activitySubTypeOptions === null && isMounted) { setManagementActivitySubTypeOptions(initialValues.activityTypeCode); @@ -185,33 +172,11 @@ export const ManagementActivityEditForm: React.FunctionComponent< { - const activityProperties: ActivityPropertyFormModel[] = - fileProperties.map(fileProperty => { - const matchingProperty = - formikProps.values.activityProperties.find( - ap => ap.propertyId === fileProperty.propertyId, - ); - - if (exists(matchingProperty)) { - return matchingProperty; - } else { - const newActivityProperty = new ActivityPropertyFormModel(); - newActivityProperty.propertyId = fileProperty.propertyId; - newActivityProperty.propertyActivityId = isValidId( - formikProps.values?.id, - ) - ? formikProps.values?.id - : 0; - - return newActivityProperty; - } - }); - - formikProps.setFieldValue('activityProperties', activityProperties); + formikProps.setFieldValue('selectedProperties', fileProperties); }} > diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/models.ts b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/models.ts index e94830691d..bfab3aef0d 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/models.ts +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/edit/models.ts @@ -1,5 +1,7 @@ import { ManagementActivitySubTypeModel } from '@/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/models/ManagementActivitySubType'; import { fromApiPersonOrApiOrganization, IContactSearchResult } from '@/interfaces'; +import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; +import { ApiGen_Concepts_ManagementFileProperty } from '@/models/api/generated/ApiGen_Concepts_ManagementFileProperty'; import { ApiGen_Concepts_PropertyActivity } from '@/models/api/generated/ApiGen_Concepts_PropertyActivity'; import { ApiGen_Concepts_PropertyActivityInvoice } from '@/models/api/generated/ApiGen_Concepts_PropertyActivityInvoice'; import { ApiGen_Concepts_PropertyActivityInvolvedParty } from '@/models/api/generated/ApiGen_Concepts_PropertyActivityInvolvedParty'; @@ -26,6 +28,7 @@ export class ManagementActivityFormModel { pstAmount = 0; totalAmount = 0; activityProperties: ActivityPropertyFormModel[] = []; + selectedProperties: ApiGen_Concepts_FileProperty[] = []; constructor( readonly id: number | null = null, @@ -82,9 +85,17 @@ export class ManagementActivityFormModel { ...getEmptyBaseAudit(0), }; }), - activityProperties: this.activityProperties - .filter(exists) - .map(x => x.toApi()), + activityProperties: this.selectedProperties.map(x => { + const matched = this.activityProperties.find(y => y.propertyId === x.propertyId) ?? null; + + return { + id: matched ? matched.id : 0, + propertyActivityId: this.id ?? 0, + propertyId: x.propertyId, + property: null, + rowVersion: matched.rowVersion ?? 0, + } as ApiGen_Concepts_PropertyActivityProperty; + }), invoices: this.invoices.map(i => i.toApi(this.id ?? 0)), ...getEmptyBaseAudit(this.rowVersion), }; @@ -92,7 +103,10 @@ export class ManagementActivityFormModel { return apiActivity; } - static fromApi(model: ApiGen_Concepts_PropertyActivity | undefined): ManagementActivityFormModel { + static fromApi( + model: ApiGen_Concepts_PropertyActivity | undefined, + fileProperties: ApiGen_Concepts_ManagementFileProperty[], + ): ManagementActivityFormModel { const formModel = new ManagementActivityFormModel( model.id, model.managementFileId, @@ -128,6 +142,21 @@ export class ManagementActivityFormModel { formModel.activityProperties = model.activityProperties?.map(p => ActivityPropertyFormModel.fromApi(p)) ?? []; + formModel.selectedProperties = model.activityProperties.map(x => { + const matchProperty = + fileProperties.find( + y => y.fileId === formModel.managementFileId && y.propertyId === x.propertyId, + ) ?? null; + + return { + id: matchProperty ? matchProperty.id : 0, + fileId: formModel.managementFileId, + propertyId: x.propertyId, + property: x.property, + rowVersion: matchProperty?.rowVersion ?? 0, + } as ApiGen_Concepts_ManagementFileProperty; + }); + return formModel; } } From e75f3f19f3aa68924d26caae3bac88eacac9630d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:46:00 +0000 Subject: [PATCH 04/61] CI: Bump version to v5.11.0-106.3 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index d55a563577..7952d1e001 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.2 + 5.11.0-106.3 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index 5e6c4e8a82..677bb755f1 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.2", + "version": "5.11.0-106.3", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 4a0de1037da474f10f4355175d0eec1893775fd5 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 24 Jun 2025 10:29:39 -0700 Subject: [PATCH 05/61] PSP-10592 Add new layers: Parcel by Class, Parcel by Owner Type (#4863) --- .../Control/LayersControl/LayerDefinitions.ts | 24 +++++++++++++++++++ .../LayersControl/LayersMenuLayout.tsx | 10 ++++++++ 2 files changed, 34 insertions(+) diff --git a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts index 7faafbb8ea..c5d7d0bf6c 100644 --- a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts +++ b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts @@ -345,6 +345,30 @@ export const layerDefinitions: LayerDefinition[] = [ maxNativeZoom: MAP_MAX_NATIVE_ZOOM, maxZoom: MAP_MAX_ZOOM, }, + { + layerIdentifier: 'pmbc_parcel_by_class', + url: 'https://openmaps.gov.bc.ca/geo/pub/WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW/ows?', + layers: + 'pub:WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW,pub:WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW', + transparent: true, + format: 'image/png', + zIndex: 21, + styles: '7834,7943', + maxNativeZoom: MAP_MAX_NATIVE_ZOOM, + maxZoom: MAP_MAX_ZOOM, + }, + { + layerIdentifier: 'pmbc_parcel_by_owner', + url: 'https://openmaps.gov.bc.ca/geo/pub/WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW/ows?', + layers: + 'pub:WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW,pub:WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW', + transparent: true, + format: 'image/png', + zIndex: 20, + styles: '7834,6616', + maxNativeZoom: MAP_MAX_NATIVE_ZOOM, + maxZoom: MAP_MAX_ZOOM, + }, { layerIdentifier: 'bctfa_property', layers: 'psp:PMBC_BCTFA_PARCEL_POLYGON_FABRIC', diff --git a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx index 40f0526742..37627a5967 100644 --- a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx @@ -268,6 +268,16 @@ export const layersMenuTree: LayerMenuEntry = { label: 'BCTFA Ownership', color: '#42814A', }, + { + layerDefinitionId: 'pmbc_parcel_by_class', + key: 'pmbc_parcel_by_class', + label: 'Parcels By Class (multiple colors)', + }, + { + layerDefinitionId: 'pmbc_parcel_by_owner', + key: 'pmbc_parcel_by_owner', + label: 'Parcels By Owner Type (multiple colors)', + }, ], }, { From 89f8c1b3e515b0c9441eaf322461119fa0103c52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:30:05 +0000 Subject: [PATCH 06/61] CI: Bump version to v5.11.0-106.4 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 7952d1e001..68d4670f8d 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.3 + 5.11.0-106.4 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index 677bb755f1..99058c3e14 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.3", + "version": "5.11.0-106.4", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From b6e8904ee96030c6a5e3d5cd04a63806ccb8dc78 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Tue, 24 Jun 2025 10:43:28 -0700 Subject: [PATCH 07/61] migrate to new type instead of IMapProperty. --- .../common/mapFSM/MapStateMachineContext.tsx | 5 ++-- .../common/mapFSM/machineDefinition/types.ts | 4 +-- .../maps/leaflet/Layers/PointClusterer.tsx | 6 ++--- source/frontend/src/components/maps/types.ts | 8 ++++++ .../mapSideBar/context/sidebarContext.tsx | 4 +-- .../mapSideBar/lease/LeaseContainer.tsx | 8 +++--- source/frontend/src/utils/mapPropertyUtils.ts | 25 +++++++++++++++++++ 7 files changed, 47 insertions(+), 13 deletions(-) diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index e1bd3ca7cc..3c71af9a66 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -7,6 +7,7 @@ import { AnyEventObject } from 'xstate'; import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types'; +import { IFilePropertyLocation } from '@/components/maps/types'; import { IMapProperty } from '@/components/propertySelector/models'; import { IGeoSearchParams } from '@/constants/API'; import { IMapSideBarViewState } from '@/features/mapSideBar/MapSideBar'; @@ -43,7 +44,7 @@ export interface IMapStateMachineContext { isLoading: boolean; mapSearchCriteria: IPropertyFilter | null; mapFeatureData: MapFeatureData; - filePropertyLocations: IMapProperty[]; + filePropertyLocations: IFilePropertyLocation[]; pendingFitBounds: boolean; requestedFitBounds: LatLngBounds; isSelecting: boolean; @@ -88,7 +89,7 @@ export interface IMapStateMachineContext { finishReposition: () => void; toggleMapFilterDisplay: () => void; toggleMapLayerControl: () => void; - setFilePropertyLocations: (locations: IMapProperty[]) => void; + setFilePropertyLocations: (locations: IFilePropertyLocation[]) => void; setMapLayers: (layers: ILayerItem[]) => void; setMapLayersToRefresh: (layers: ILayerItem[]) => void; setDefaultMapLayers: (layers: ILayerItem[]) => void; diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index 98e94283cb..6e41c43349 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -2,7 +2,7 @@ import { LatLngBounds, LatLngLiteral } from 'leaflet'; import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types'; -import { IMapProperty } from '@/components/propertySelector/models'; +import { IFilePropertyLocation } from '@/components/maps/types'; import { IMapSideBarViewState as IMapSideBarState } from '@/features/mapSideBar/MapSideBar'; import { IPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; @@ -44,7 +44,7 @@ export type MachineContext = { requestedFitBounds: LatLngBounds; requestedFlyTo: RequestedFlyTo; requestedCenterTo: RequestedCenterTo; - filePropertyLocations: IMapProperty[]; + filePropertyLocations: IFilePropertyLocation[]; activePimsPropertyIds: number[]; activeLayers: ILayerItem[]; mapLayersToRefresh: ILayerItem[]; diff --git a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx index 336007b7eb..84ac2ea0dd 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/PointClusterer.tsx @@ -18,7 +18,7 @@ import { PIMS_Property_Boundary_View, PIMS_Property_Location_View, } from '@/models/layers/pimsPropertyLocationView'; -import { exists, latLngFromMapProperty } from '@/utils'; +import { exists, latLngFromFileProperty } from '@/utils'; import { ONE_HUNDRED_METER_PRECISION } from '../../constants'; import SinglePropertyMarker from '../Markers/SingleMarker'; @@ -98,7 +98,7 @@ export const PointClusterer: React.FC(() => { return mapMachine.filePropertyLocations.map(x => { // The values on the feature are rounded to the 4th decimal. Do the same to the draft points. - return latLngFromMapProperty(x); + return latLngFromFileProperty(x); }); }, [mapMachine.filePropertyLocations]); @@ -354,7 +354,7 @@ export const PointClusterer: React.FC {mapMachine.filePropertyLocations.map((draftPoint, index) => { - const latLng = latLngFromMapProperty(draftPoint); + const latLng = latLngFromFileProperty(draftPoint); return ( ; + +export interface IFilePropertyLocation { + latitude?: number; + longitude?: number; + fileLocation?: LatLngLiteral; + isActive?: boolean; +} diff --git a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx index d9ca3c8741..d431d6a272 100644 --- a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx +++ b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx @@ -7,7 +7,7 @@ import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTy import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { ApiGen_Concepts_Project } from '@/models/api/generated/ApiGen_Concepts_Project'; import { exists } from '@/utils'; -import { apiToMapProperty } from '@/utils/mapPropertyUtils'; +import { apiToFileProperty } from '@/utils/mapPropertyUtils'; export interface TypedFile extends ApiGen_Concepts_File { fileType: ApiGen_CodeTypes_FileTypes; @@ -119,7 +119,7 @@ export const SideBarContextProvider = (props: { const resetFilePropertyLocations = useCallback(() => { if (exists(fileProperties)) { - const propertyLocations = fileProperties.map(x => apiToMapProperty(x)).filter(exists); + const propertyLocations = fileProperties.map(x => apiToFileProperty(x)).filter(exists); setFilePropertyLocations && setFilePropertyLocations(propertyLocations); } else { diff --git a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx index a15ef95624..b193661377 100644 --- a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx @@ -16,7 +16,7 @@ import LeaseIcon from '@/assets/images/lease-icon.svg?react'; import GenericModal from '@/components/common/GenericModal'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { IMapProperty } from '@/components/propertySelector/models'; +import { IFilePropertyLocation } from '@/components/maps/types'; import { Claims, Roles } from '@/constants'; import { useLeaseDetail } from '@/features/leases'; import { AddLeaseYupSchema } from '@/features/leases/add/AddLeaseYupSchema'; @@ -37,7 +37,7 @@ import { useLeaseRepository } from '@/hooks/repositories/useLeaseRepository'; import { useQuery } from '@/hooks/use-query'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTypes_FileTypes'; -import { apiToMapProperty, exists, stripTrailingSlash } from '@/utils'; +import { apiToFileProperty, exists, stripTrailingSlash } from '@/utils'; import GenerateFormView from '../acquisition/common/GenerateForm/GenerateFormView'; import { SideBarContext } from '../context/sidebarContext'; @@ -245,9 +245,9 @@ export const LeaseContainer: React.FC = ({ leaseId, onClos const { setFilePropertyLocations } = useMapStateMachine(); - const locations: IMapProperty[] = useMemo(() => { + const locations: IFilePropertyLocation[] = useMemo(() => { if (exists(lease?.fileProperties)) { - return lease?.fileProperties.map(leaseProp => apiToMapProperty(leaseProp)).filter(exists); + return lease?.fileProperties.map(leaseProp => apiToFileProperty(leaseProp)).filter(exists); } else { return []; } diff --git a/source/frontend/src/utils/mapPropertyUtils.ts b/source/frontend/src/utils/mapPropertyUtils.ts index f8754bff7e..ba2c07f1ef 100644 --- a/source/frontend/src/utils/mapPropertyUtils.ts +++ b/source/frontend/src/utils/mapPropertyUtils.ts @@ -25,6 +25,8 @@ import { ApiGen_Concepts_Geometry } from '@/models/api/generated/ApiGen_Concepts import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { enumFromValue, exists, formatApiAddress, pidFormatter } from '@/utils'; +import { IFilePropertyLocation } from './../components/maps/types'; + export enum NameSourceType { PID = 'PID', PIN = 'PIN', @@ -195,6 +197,20 @@ export function apiToMapProperty(fileProperty: ApiGen_Concepts_FileProperty): IM }; } +export function apiToFileProperty( + fileProperty: ApiGen_Concepts_FileProperty, +): IFilePropertyLocation { + return { + latitude: fileProperty.property?.location?.coordinate.y, + longitude: fileProperty.property?.location?.coordinate.x, + fileLocation: { + lat: fileProperty.property?.location?.coordinate.y, + lng: fileProperty.property?.location?.coordinate.x, + }, + isActive: fileProperty?.isActive, + }; +} + function toMapProperty( feature: Feature, address?: string, @@ -334,6 +350,15 @@ export function latLngFromMapProperty( }; } +export function latLngFromFileProperty( + fileProperty: IFilePropertyLocation | undefined | null, +): LatLngLiteral | null { + return { + lat: Number(fileProperty?.fileLocation?.lat ?? fileProperty?.latitude ?? 0), + lng: Number(fileProperty?.fileLocation?.lng ?? fileProperty?.longitude ?? 0), + }; +} + /** * 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. From c578bdeeea364fda401829b10e4a19ea50665da4 Mon Sep 17 00:00:00 2001 From: FuriousLlama Date: Tue, 24 Jun 2025 14:52:58 -0700 Subject: [PATCH 08/61] Added buttons to zoom into properties from files --- .../mapFSM/machineDefinition/mapMachine.ts | 36 +-- .../common/mapFSM/machineDefinition/types.ts | 1 - .../src/components/maps/MapLeafletView.tsx | 6 +- .../SelectedPropertyRow.tsx | 21 +- .../SelectedPropertyRow.test.tsx.snap | 27 ++ .../AddLeaseContainer.test.tsx.snap | 38 ++- .../propertyPicker/LeasePropertySelector.tsx | 49 ++- .../LeaseUpdatePropertySelector.tsx | 45 ++- .../LeasePropertySelector.test.tsx.snap | 92 +++++- .../SelectedPropertyRow.tsx | 23 +- .../SelectedPropertyRow.test.tsx.snap | 27 ++ .../AcquisitionView.test.tsx.snap | 305 ++++++++++++------ ...AcquisitionPropertiesSubForm.test.tsx.snap | 54 ++++ .../DispositionView.test.tsx.snap | 226 ++++++++----- .../AddDispositionContainerView.test.tsx.snap | 38 ++- .../form/DispositionPropertiesSubForm.tsx | 41 ++- ...DispositionPropertiesSubForm.test.tsx.snap | 216 +++++++++---- .../__snapshots__/LeaseView.test.tsx.snap | 130 ++++++-- .../ManagementView.test.tsx.snap | 194 +++++++---- .../ManagementPropertiesSubForm.test.tsx.snap | 54 ++++ .../ResearchContainer.test.tsx.snap | 202 ++++++++---- .../ResearchProperties.test.tsx.snap | 54 ++++ .../mapSideBar/shared/FileMenuView.tsx | 123 +++++-- .../__snapshots__/FileMenuView.test.tsx.snap | 123 ++++++- .../operations/SelectedOperationProperty.tsx | 36 ++- .../update/properties/UpdateProperties.tsx | 42 ++- .../UpdateProperties.test.tsx.snap | 174 +++++----- .../subdivision/AddSubdivisionView.tsx | 42 ++- .../AddSubdivisionView.test.tsx.snap | 40 ++- source/frontend/src/utils/mapPropertyUtils.ts | 23 ++ 30 files changed, 1885 insertions(+), 597 deletions(-) diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index eacd60fd97..4acba39657 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -1,5 +1,5 @@ import { FeatureCollection, Geometry } from 'geojson'; -import { geoJSON, latLngBounds } from 'leaflet'; +import { geoJSON } from 'leaflet'; import { AnyEventObject, assign, createMachine, raise, send } from 'xstate'; import { defaultBounds } from '@/components/maps/constants'; @@ -7,7 +7,6 @@ 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 { exists } from '@/utils'; import { emptyFeatureData, LocationBoundaryDataset } from '../models'; import { MachineContext, SideBarType } from './types'; @@ -49,7 +48,6 @@ const featureViewStates = { event: AnyEventObject & { locations: LocationBoundaryDataset[] }, ) => event.locations ?? [], }), - raise('REQUEST_FIT_FILE_BOUNDS'), ], }, }, @@ -74,7 +72,6 @@ const featureViewStates = { event: AnyEventObject & { locations: LocationBoundaryDataset[] }, ) => event.locations ?? [], }), - raise('REQUEST_FIT_FILE_BOUNDS'), ], }, }, @@ -95,7 +92,6 @@ const featureDataLoaderStates = { actions: assign({ isLoading: () => true, searchCriteria: (_, event: any) => event.searchCriteria, - fitToResultsAfterLoading: () => true, }), target: 'loading', }, @@ -109,24 +105,10 @@ const featureDataLoaderStates = { src: 'loadFeatures', onDone: [ { - cond: (context: MachineContext) => context.fitToResultsAfterLoading === true, actions: [ - raise('REQUEST_FIT_BOUNDS'), assign({ isLoading: () => false, mapFeatureData: (_, event: any) => event.data, - fitToResultsAfterLoading: () => false, - mapLayersToRefresh: () => pimsBoundaryLayers, - }), - ], - target: 'idle', - }, - { - actions: [ - assign({ - isLoading: () => false, - mapFeatureData: (_, event: any) => event.data, - fitToResultsAfterLoading: () => false, mapLayersToRefresh: () => pimsBoundaryLayers, }), ], @@ -216,20 +198,6 @@ const mapRequestStates = { }), target: 'pendingFitBounds', }, - REQUEST_FIT_FILE_BOUNDS: { - actions: assign({ - requestedFitBounds: (context: MachineContext) => { - // zoom to the bounds that include all file properties - if (context.filePropertyLocations.length > 0) { - const locations = (context.filePropertyLocations ?? []) - .map(pl => pl.location) - .filter(exists); - return latLngBounds(locations); - } - }, - }), - target: 'pendingFitBounds', - }, }, }, pendingFlyTo: { @@ -404,7 +372,6 @@ const sideBarStates = { event: AnyEventObject & { locations: LocationBoundaryDataset[] }, ) => event.locations ?? [], }), - raise('REQUEST_FIT_FILE_BOUNDS'), ], }, @@ -525,7 +492,6 @@ export const mapMachine = createMachine({ repositioningPropertyIndex: null, selectingComponentId: null, isLoading: false, - fitToResultsAfterLoading: false, searchCriteria: null, advancedSearchCriteria: new PropertyFilterFormModel(), mapFeatureData: emptyFeatureData, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index 32de3a1bfe..61eb656dc5 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -44,7 +44,6 @@ export type MachineContext = { advancedSearchCriteria: PropertyFilterFormModel | null; isLoading: boolean; - fitToResultsAfterLoading: boolean; requestedFitBounds: LatLngBounds; requestedFlyTo: RequestedFlyTo; requestedCenterTo: RequestedCenterTo; diff --git a/source/frontend/src/components/maps/MapLeafletView.tsx b/source/frontend/src/components/maps/MapLeafletView.tsx index 75df81d8ce..894355319d 100644 --- a/source/frontend/src/components/maps/MapLeafletView.tsx +++ b/source/frontend/src/components/maps/MapLeafletView.tsx @@ -169,13 +169,13 @@ const MapLeafletView: React.FC> = ( useEffect(() => { if (hasPendingFlyTo && isMapReady) { if (requestedFlyTo.bounds !== null) { - mapRef?.current?.flyToBounds(requestedFlyTo.bounds, { animate: false }); + mapRef?.current?.flyToBounds(requestedFlyTo.bounds, { animate: true }); } if (requestedFlyTo.location !== null) { mapRef?.current?.flyTo(requestedFlyTo.location, MAP_MAX_ZOOM, { - animate: false, + animate: true, }); - mapRef?.current?.panTo(requestedFlyTo.location); + //mapRef?.current?.panTo(requestedFlyTo.location); } mapMachineProcessFlyTo(); diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx index fc606cad60..d358696d6d 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx @@ -1,14 +1,17 @@ import { getIn, useFormikContext } from 'formik'; -import { useEffect } from 'react'; +import { geoJSON } from 'leaflet'; +import { useCallback, useEffect } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { FaSearchPlus } from 'react-icons/fa'; import { RiDragMove2Line } from 'react-icons/ri'; -import { RemoveButton, StyledIconButton } from '@/components/common/buttons'; +import { LinkButton, RemoveButton, StyledIconButton } from '@/components/common/buttons'; import { InlineInput } from '@/components/common/form/styles'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import OverflowTip from '@/components/common/OverflowTip'; import DraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; +import { exists } from '@/utils'; import { withNameSpace } from '@/utils/formUtils'; import { featuresetToMapProperty, getPropertyName, NameSourceType } from '@/utils/mapPropertyUtils'; @@ -33,6 +36,15 @@ export const SelectedPropertyRow: React.FunctionComponent { + const geom = property.pimsFeature.geometry; + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + }, [mapMachine, property.pimsFeature.geometry]); + const propertyName = getPropertyName(featuresetToMapProperty(property)); let propertyIdentifier = ''; switch (propertyName.label) { @@ -67,6 +79,11 @@ export const SelectedPropertyRow: React.FunctionComponent + + + + + renders as expected 1`] = ` />
+
+ +
diff --git a/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap b/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap index 669a497b62..cf28e8fe34 100644 --- a/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap +++ b/source/frontend/src/features/leases/add/__snapshots__/AddLeaseContainer.test.tsx.snap @@ -1638,7 +1638,43 @@ exports[`AddLeaseContainer component > renders as expected 1`] = `
- Selected properties +
+
+ Selected Properties +
+
+ +
+
diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx index 1da1365fd3..1c920124f5 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx @@ -1,13 +1,17 @@ import { FieldArray, FieldArrayRenderProps, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { useCallback, useContext, useRef } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; +import { LinkButton } from '@/components/common/buttons'; import { 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 { Section } from '@/components/common/Section/Section'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; import { IMapProperty } from '@/components/propertySelector/models'; import { ModalContext } from '@/contexts/modalContext'; @@ -16,7 +20,13 @@ import { IPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; import { useProperties } from '@/hooks/repositories/useProperties'; import { ApiGen_Concepts_PropertyView } from '@/models/api/generated/ApiGen_Concepts_PropertyView'; -import { exists, isLatLngInFeatureSetBoundary, isValidId, isValidString } from '@/utils'; +import { + exists, + isLatLngInFeatureSetBoundary, + isValidId, + isValidString, + latLngLiteralToGeometry, +} from '@/utils'; import { FormLeaseProperty, LeaseFormModel } from '../../models'; import SelectedPropertyHeaderRow from './selectedPropertyList/SelectedPropertyHeaderRow'; @@ -34,6 +44,8 @@ export const LeasePropertySelector: React.FunctionComponent(null); const addProperties = useCallback( @@ -79,6 +91,21 @@ export const LeasePropertySelector: React.FunctionComponent { + const fileProperties = values.properties; + + if (exists(fileProperties)) { + const locations = fileProperties + .map(p => p?.property?.polygon ?? latLngLiteralToGeometry(p?.property?.fileLocation)) + .filter(exists); + const bounds = geoJSON(locations).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + } + }; + const confirmAdd = useCallback( (propertiesToConfirm: FormLeaseProperty[]) => { setDisplayModal(false); @@ -234,7 +261,23 @@ export const LeasePropertySelector: React.FunctionComponent -
+
+ Selected Properties + + + + + + + + + } + > {formikProps.values.properties.map((leaseProperty, index) => { const property = leaseProperty?.property; diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx index ecbe9a62f6..0f0bae2dda 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx @@ -1,16 +1,19 @@ import { AxiosError } from 'axios'; import { FieldArray, FieldArrayRenderProps, Formik, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { useCallback, useContext, useRef, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; import { toast } from 'react-toastify'; +import { LinkButton } 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 { Section } from '@/components/common/Section/Section'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; import { IMapProperty } from '@/components/propertySelector/models'; import { ModalContext } from '@/contexts/modalContext'; @@ -30,7 +33,13 @@ import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; import { ApiGen_Concepts_PropertyView } from '@/models/api/generated/ApiGen_Concepts_PropertyView'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { exists, isLatLngInFeatureSetBoundary, isValidId, isValidString } from '@/utils'; +import { + exists, + isLatLngInFeatureSetBoundary, + isValidId, + isValidString, + latLngLiteralToGeometry, +} from '@/utils'; import { useLeaseDetail } from '../../hooks/useLeaseDetail'; import { FormLeaseProperty, LeaseFormModel } from '../../models'; @@ -109,6 +118,20 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< const result = await getProperties.execute(params); return result?.items ?? undefined; }; + const fitBoundaries = () => { + const fileProperties = formikRef?.current?.values?.properties; + + if (exists(fileProperties)) { + const locations = fileProperties.map( + p => p?.property?.polygon ?? latLngLiteralToGeometry(p?.property?.fileLocation), + ); + const bounds = geoJSON(locations).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + } + }; const confirmAdd = useCallback( (propertiesToConfirm: FormLeaseProperty[]) => { @@ -365,7 +388,23 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< /> -
+
+ Selected Properties + + + + + + + + + } + > {formikProps.values.properties.map((leaseProperty, index) => { const property = leaseProperty?.property; diff --git a/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap b/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap index 060ff5bc3b..5367724fec 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap +++ b/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap @@ -1068,7 +1068,43 @@ exports[`LeasePropertySelector component > renders as expected 1`] = `
- Selected properties +
+
+ Selected Properties +
+
+ +
+
@@ -1233,6 +1269,33 @@ exports[`LeasePropertySelector component > renders as expected 1`] = ` +
+ +
@@ -1513,6 +1576,33 @@ exports[`LeasePropertySelector component > renders as expected 1`] = `
+
+ +
diff --git a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx index 094209c390..57510a726d 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx @@ -1,8 +1,11 @@ import { FormikProps, getIn } from 'formik'; +import { geoJSON } from 'leaflet'; +import { useCallback } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { FaSearchPlus } from 'react-icons/fa'; import { RiDragMove2Line } from 'react-icons/ri'; -import { RemoveButton, StyledIconButton } from '@/components/common/buttons'; +import { LinkButton, RemoveButton, StyledIconButton } from '@/components/common/buttons'; import { InlineInput } from '@/components/common/form/styles'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; @@ -10,6 +13,7 @@ import OverflowTip from '@/components/common/OverflowTip'; import AreaContainer from '@/components/measurements/AreaContainer'; import DraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; import { FormLeaseProperty, LeaseFormModel } from '@/features/leases/models'; +import { exists } from '@/utils'; import { withNameSpace } from '@/utils/formUtils'; import { featuresetToMapProperty, getPropertyName, NameSourceType } from '@/utils/mapPropertyUtils'; @@ -53,6 +57,18 @@ export const SelectedPropertyRow: React.FunctionComponent { + const geom = property?.parcelFeature?.geometry ?? property?.pimsFeature?.geometry; + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + }, + [mapMachine], + ); + return ( <> @@ -82,6 +98,11 @@ export const SelectedPropertyRow: React.FunctionComponent + + onZoomToProperty(property)}> + + + diff --git a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap index 0a23c96427..1b2f29fcdc 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap +++ b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap @@ -451,6 +451,33 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = `
+
+ +
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 3a4f4565c5..c9cff21674 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 @@ -230,32 +230,32 @@ exports[`AcquisitionView component > renders as expected 1`] = ` margin-right: 0; } -.c31 { +.c32 { height: 100%; } -.c31 .tab-content { +.c32 .tab-content { border-radius: 0 0.4rem 0.4rem 0.4rem; overflow-y: auto; background-color: #f2f2f2; } -.c31 .tab-content .tab-pane { +.c32 .tab-content .tab-pane { position: relative; } -.c32 { +.c33 { background-color: white; color: var(--surface-color-primary-button-default); font-size: 1.4rem; border-color: transparent; } -.c32 .nav-tabs { +.c33 .nav-tabs { height: auto; } -.c32 .nav-item { +.c33 .nav-item { color: var(--surface-color-primary-button-default); min-width: 5rem; padding: 0.1rem 1.3rem; @@ -263,7 +263,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` margin-left: 0.1rem; } -.c32 .nav-item:hover { +.c33 .nav-item:hover { color: #f2f2f2; background-color: #1e5189; border-color: transparent; @@ -273,7 +273,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c32 .nav-item.active { +.c33 .nav-item.active { border-radius: 0.4rem; color: #f2f2f2; background-color: #053662; @@ -291,7 +291,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` width: 22rem; } -.c29 { +.c30 { overflow: auto; height: 100%; width: 100%; @@ -317,7 +317,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` margin-bottom: 2.4rem; } -.c40 { +.c41 { color: #474543; font-size: 1.6rem; -webkit-text-decoration: none; @@ -336,7 +336,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` border-bottom: solid 0.5rem #3470B1; } -.c41 { +.c42 { width: 100%; height: 100%; position: absolute; @@ -453,7 +453,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` align-items: end; } -.c30 { +.c31 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -470,24 +470,24 @@ exports[`AcquisitionView component > renders as expected 1`] = ` overflow-y: auto; } -.c33 { +.c34 { background-color: #f2f2f2; padding-top: 1rem; } -.c34 { +.c35 { text-align: right; overflow: hidden; } -.c36 { +.c37 { font-weight: bold; color: var(--theme-blue-100); border-bottom: 0.2rem var(--theme-blue-90) solid; margin-bottom: 2.4rem; } -.c35 { +.c36 { margin: 1.6rem; padding: 1.6rem; background-color: white; @@ -495,14 +495,14 @@ exports[`AcquisitionView component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c38.required::before { +.c39.required::before { content: '*'; position: absolute; top: 0.75rem; left: 0rem; } -.c37 { +.c38 { font-weight: bold; } @@ -532,15 +532,16 @@ exports[`AcquisitionView component > renders as expected 1`] = ` padding-bottom: 0.5rem; } -.c24.selected { - font-weight: bold; - cursor: default; -} - .c24 div.Button__value { font-size: 1.4rem; } +.c29.selected { + font-weight: bold; + cursor: default; + line-height: 3rem; +} + .c28 { background-color: #fcba19; font-size: 1.5rem; @@ -564,6 +565,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` } .c28.selected { + font-weight: bold; background-color: #FCBA19; } @@ -660,7 +662,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` max-width: 60rem; } -.c39 { +.c40 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -991,11 +993,50 @@ exports[`AcquisitionView component > renders as expected 1`] = `
- - Properties - +
+ + Properties + +
+
+
+ +
+
renders as expected 1`] = ` >
renders as expected 1`] = `
+
+ +
renders as expected 1`] = `
+
+ +

renders as expected 1`] = ` class="pr-0 text-left col-4" >
@@ -1287,13 +1382,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Received @@ -1306,13 +1401,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
@@ -1323,13 +1418,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
@@ -1377,23 +1472,23 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >

renders as expected 1`] = ` class="pr-0 text-left col-4" >
Dec 18, 2024 @@ -1434,7 +1529,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Jul 29, 2022
@@ -1474,7 +1569,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >

Jul 10, 2024
@@ -1514,13 +1609,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Jul 10, 2025
@@ -1528,10 +1623,10 @@ exports[`AcquisitionView component > renders as expected 1`] = `

renders as expected 1`] = ` class="pr-0 text-left col-4" >
Test ACQ File
@@ -1571,7 +1666,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >

legacy file number
@@ -1611,13 +1706,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
Consensual Agreement
@@ -1683,13 +1778,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
South Coast Region
@@ -1697,10 +1792,10 @@ exports[`AcquisitionView component > renders as expected 1`] = `
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap b/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap index 4709420895..325b7599b9 100644 --- a/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap @@ -240,7 +240,7 @@ exports[`DispositionView component > renders as expected 1`] = ` width: 22rem; } -.c29 { +.c30 { overflow: auto; height: 100%; width: 100%; @@ -369,32 +369,32 @@ exports[`DispositionView component > renders as expected 1`] = ` align-items: end; } -.c31 { +.c32 { height: 100%; } -.c31 .tab-content { +.c32 .tab-content { border-radius: 0 0.4rem 0.4rem 0.4rem; overflow-y: auto; background-color: #f2f2f2; } -.c31 .tab-content .tab-pane { +.c32 .tab-content .tab-pane { position: relative; } -.c32 { +.c33 { background-color: white; color: var(--surface-color-primary-button-default); font-size: 1.4rem; border-color: transparent; } -.c32 .nav-tabs { +.c33 .nav-tabs { height: auto; } -.c32 .nav-item { +.c33 .nav-item { color: var(--surface-color-primary-button-default); min-width: 5rem; padding: 0.1rem 1.3rem; @@ -402,7 +402,7 @@ exports[`DispositionView component > renders as expected 1`] = ` margin-left: 0.1rem; } -.c32 .nav-item:hover { +.c33 .nav-item:hover { color: #f2f2f2; background-color: #1e5189; border-color: transparent; @@ -412,7 +412,7 @@ exports[`DispositionView component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c32 .nav-item.active { +.c33 .nav-item.active { border-radius: 0.4rem; color: #f2f2f2; background-color: #053662; @@ -420,7 +420,7 @@ exports[`DispositionView component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c30 { +.c31 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -437,24 +437,24 @@ exports[`DispositionView component > renders as expected 1`] = ` overflow-y: auto; } -.c33 { +.c34 { background-color: #f2f2f2; padding-top: 1rem; } -.c34 { +.c35 { text-align: right; overflow: hidden; } -.c38 { +.c39 { font-weight: bold; color: var(--theme-blue-100); border-bottom: 0.2rem var(--theme-blue-90) solid; margin-bottom: 2.4rem; } -.c35 { +.c36 { margin: 1.6rem; padding: 1.6rem; background-color: white; @@ -462,18 +462,18 @@ exports[`DispositionView component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c37.required::before { +.c38.required::before { content: '*'; position: absolute; top: 0.75rem; left: 0rem; } -.c36 { +.c37 { font-weight: bold; } -.c39 { +.c40 { padding: 0 0.4rem; display: block; } @@ -504,15 +504,16 @@ exports[`DispositionView component > renders as expected 1`] = ` padding-bottom: 0.5rem; } -.c24.selected { - font-weight: bold; - cursor: default; -} - .c24 div.Button__value { font-size: 1.4rem; } +.c29.selected { + font-weight: bold; + cursor: default; + line-height: 3rem; +} + .c28 { background-color: #fcba19; font-size: 1.5rem; @@ -536,6 +537,7 @@ exports[`DispositionView component > renders as expected 1`] = ` } .c28.selected { + font-weight: bold; background-color: #FCBA19; } @@ -927,11 +929,50 @@ exports[`DispositionView component > renders as expected 1`] = `
- - Properties - +
+ + Properties + +
+
+
+ +
+
renders as expected 1`] = ` >
renders as expected 1`] = `
+
+ +
renders as expected 1`] = ` class="pr-0 text-left col-5" >
Road Closure
@@ -1305,7 +1373,7 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
Surplus Declaration
@@ -1345,7 +1413,7 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
Jun 29, 1917
@@ -1385,13 +1453,13 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
Pending Litigation
@@ -1403,13 +1471,13 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
PLMB
@@ -1421,13 +1489,13 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
Cannot determine
@@ -1435,10 +1503,10 @@ exports[`DispositionView component > renders as expected 1`] = `

renders as expected 1`] = ` class="pr-0 text-left col-5" >

diff --git a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx b/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx index 41bfcade8e..b73a41fb8b 100644 --- a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx @@ -1,17 +1,21 @@ import { FieldArray, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; +import { LinkButton } from '@/components/common/buttons/LinkButton'; 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 TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; import { useModalContext } from '@/hooks/useModalContext'; -import { isLatLngInFeatureSetBoundary } from '@/utils'; +import { exists, isLatLngInFeatureSetBoundary, latLngLiteralToGeometry } from '@/utils'; import { AddressForm, PropertyForm } from '../../shared/models'; import { DispositionFormModel } from '../models/DispositionFormModel'; @@ -29,6 +33,21 @@ const DispositionPropertiesSubForm: React.FunctionComponent { + const fileProperties = formikProps.values.fileProperties; + + if (exists(fileProperties)) { + const locations = fileProperties.map( + p => p?.polygon ?? latLngLiteralToGeometry(p?.fileLocation), + ); + const bounds = geoJSON(locations).getBounds(); + + mapMachine.requestFlyToBounds(bounds); + } + }; + return ( <>
@@ -115,7 +134,23 @@ const DispositionPropertiesSubForm: React.FunctionComponent -
+
+ Selected properties + + + + + + + + + } + > {formikProps.values.fileProperties.map((property, index) => ( renders as expected 1`] = `
- .c7.btn { + .c2.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -923,19 +923,19 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` cursor: pointer; } -.c7.btn .Button__value { +.c2.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c7.btn:hover { +.c2.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c7.btn:focus { +.c2.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -943,31 +943,31 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` box-shadow: none; } -.c7.btn.btn-primary { +.c2.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-primary:hover, -.c7.btn.btn-primary:active, -.c7.btn.btn-primary:focus { +.c2.btn.btn-primary:hover, +.c2.btn.btn-primary:active, +.c2.btn.btn-primary:focus { background-color: #1E5189; } -.c7.btn.btn-secondary { +.c2.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c7.btn.btn-secondary:hover, -.c7.btn.btn-secondary:active, -.c7.btn.btn-secondary:focus { +.c2.btn.btn-secondary:hover, +.c2.btn.btn-secondary:active, +.c2.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-info { +.c2.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -975,66 +975,66 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` padding-right: 0.6rem; } -.c7.btn.btn-info:hover, -.c7.btn.btn-info:active, -.c7.btn.btn-info:focus { +.c2.btn.btn-info:hover, +.c2.btn.btn-info:active, +.c2.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c7.btn.btn-light { +.c2.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c7.btn.btn-light:hover, -.c7.btn.btn-light:active, -.c7.btn.btn-light:focus { +.c2.btn.btn-light:hover, +.c2.btn.btn-light:active, +.c2.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c7.btn.btn-dark { +.c2.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c7.btn.btn-dark:hover, -.c7.btn.btn-dark:active, -.c7.btn.btn-dark:focus { +.c2.btn.btn-dark:hover, +.c2.btn.btn-dark:active, +.c2.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c7.btn.btn-danger { +.c2.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-danger:hover, -.c7.btn.btn-danger:active, -.c7.btn.btn-danger:focus { +.c2.btn.btn-danger:hover, +.c2.btn.btn-danger:active, +.c2.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-warning { +.c2.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c7.btn.btn-warning:hover, -.c7.btn.btn-warning:active, -.c7.btn.btn-warning:focus { +.c2.btn.btn-warning:hover, +.c2.btn.btn-warning:active, +.c2.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c7.btn.btn-link { +.c2.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -1058,9 +1058,9 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` text-decoration: underline; } -.c7.btn.btn-link:hover, -.c7.btn.btn-link:active, -.c7.btn.btn-link:focus { +.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; @@ -1070,15 +1070,15 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` outline: none; } -.c7.btn.btn-link:disabled, -.c7.btn.btn-link.disabled { +.c2.btn.btn-link:disabled, +.c2.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c7.btn:disabled, -.c7.btn:disabled:hover { +.c2.btn:disabled, +.c2.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -1090,15 +1090,15 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` cursor: not-allowed; } -.c7.Button .Button__icon { +.c2.Button .Button__icon { margin-right: 1.6rem; } -.c7.Button--icon-only:focus { +.c2.Button--icon-only:focus { outline: none; } -.c7.Button--icon-only .Button__icon { +.c2.Button--icon-only .Button__icon { margin-right: 0; } @@ -1184,7 +1184,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` line-height: 2rem; } -.c6 { +.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1196,7 +1196,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` gap: 0.8rem; } -.c6 .form-label { +.c7 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -1217,7 +1217,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c2 { +.c3 { font-size: 1.6rem; color: #9F9D9C; border-bottom: 0.2rem solid #606060; @@ -1226,18 +1226,18 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` font-family: 'BcSans-Bold'; } -.c5 { +.c6 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c3 { +.c4 { min-width: 3rem; min-height: 4.5rem; } -.c4 { +.c5 { font-size: 1.2rem; } @@ -1253,7 +1253,43 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = `
- Selected properties +
+
+ Selected properties +
+
+ +
+
@@ -1261,7 +1297,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` class="collapse show" >
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PID: 123-456-789
@@ -1384,7 +1420,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
+
+
+
renders as expected 1`] = ` >
renders as expected 1`] = `
+
+ +
+
+ +
@@ -1564,6 +1591,33 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` />
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap index a8d2b089e5..23b785041d 100644 --- a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap @@ -246,7 +246,7 @@ exports[`ResearchContainer component > renders as expected 1`] = ` cursor: pointer; } -.c30 { +.c31 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -273,7 +273,7 @@ exports[`ResearchContainer component > renders as expected 1`] = ` width: 22rem; } -.c29 { +.c30 { overflow: auto; height: 100%; width: 100%; @@ -365,32 +365,32 @@ exports[`ResearchContainer component > renders as expected 1`] = ` align-items: end; } -.c31 { +.c32 { height: 100%; } -.c31 .tab-content { +.c32 .tab-content { border-radius: 0 0.4rem 0.4rem 0.4rem; overflow-y: auto; background-color: #f2f2f2; } -.c31 .tab-content .tab-pane { +.c32 .tab-content .tab-pane { position: relative; } -.c32 { +.c33 { background-color: white; color: var(--surface-color-primary-button-default); font-size: 1.4rem; border-color: transparent; } -.c32 .nav-tabs { +.c33 .nav-tabs { height: auto; } -.c32 .nav-item { +.c33 .nav-item { color: var(--surface-color-primary-button-default); min-width: 5rem; padding: 0.1rem 1.3rem; @@ -398,7 +398,7 @@ exports[`ResearchContainer component > renders as expected 1`] = ` margin-left: 0.1rem; } -.c32 .nav-item:hover { +.c33 .nav-item:hover { color: #f2f2f2; background-color: #1e5189; border-color: transparent; @@ -408,7 +408,7 @@ exports[`ResearchContainer component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c32 .nav-item.active { +.c33 .nav-item.active { border-radius: 0.4rem; color: #f2f2f2; background-color: #053662; @@ -416,24 +416,24 @@ exports[`ResearchContainer component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c33 { +.c34 { background-color: #f2f2f2; padding-top: 1rem; } -.c34 { +.c35 { text-align: right; overflow: hidden; } -.c36 { +.c37 { font-weight: bold; color: var(--theme-blue-100); border-bottom: 0.2rem var(--theme-blue-90) solid; margin-bottom: 2.4rem; } -.c35 { +.c36 { margin: 1.6rem; padding: 1.6rem; background-color: white; @@ -441,14 +441,14 @@ exports[`ResearchContainer component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c38.required::before { +.c39.required::before { content: '*'; position: absolute; top: 0.75rem; left: 0rem; } -.c37 { +.c38 { font-weight: bold; } @@ -478,15 +478,16 @@ exports[`ResearchContainer component > renders as expected 1`] = ` padding-bottom: 0.5rem; } -.c24.selected { - font-weight: bold; - cursor: default; -} - .c24 div.Button__value { font-size: 1.4rem; } +.c29.selected { + font-weight: bold; + cursor: default; + line-height: 3rem; +} + .c28 { background-color: #fcba19; font-size: 1.5rem; @@ -510,6 +511,7 @@ exports[`ResearchContainer component > renders as expected 1`] = ` } .c28.selected { + font-weight: bold; background-color: #FCBA19; } @@ -950,11 +952,50 @@ exports[`ResearchContainer component > renders as expected 1`] = `
- - Properties - +
+ + Properties + +
+
+
+ +
+
renders as expected 1`] = ` >
renders as expected 1`] = `
+
+ +

renders as expected 1`] = ` class="pr-0 text-left col-4" >
No
@@ -1352,14 +1420,14 @@ exports[`ResearchContainer component > renders as expected 1`] = ` class="pr-0 text-left col-4" >

An expropriation note diff --git a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap index 937c60053c..9f02546792 100644 --- a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap @@ -1191,6 +1191,33 @@ exports[`ResearchProperties component > renders as expected when provided no pro /> +
+ +
@@ -1360,6 +1387,33 @@ exports[`ResearchProperties component > renders as expected when provided no pro />
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx index 8a1ef62809..18c3f31c4a 100644 --- a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx +++ b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx @@ -1,15 +1,25 @@ import cx from 'classnames'; -import { useMemo } from 'react'; +import { geoJSON, latLngBounds } from 'leaflet'; +import { useCallback, useMemo } from 'react'; import { Col, Row } from 'react-bootstrap'; -import { FaCaretRight } from 'react-icons/fa'; +import { FaCaretRight, FaSearchPlus } from 'react-icons/fa'; +import { PiCornersOut } from 'react-icons/pi'; import styled from 'styled-components'; import { RestrictedEditControl } from '@/components/common/buttons'; import { EditPropertiesIcon } from '@/components/common/buttons/EditPropertiesButton'; import { LinkButton } from '@/components/common/buttons/LinkButton'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; -import { exists, getFilePropertyName, sortFileProperties } from '@/utils'; +import { + boundaryFromFileProperty, + exists, + getFilePropertyName, + getLatLng, + locationFromFileProperty, + sortFileProperties, +} from '@/utils'; import { cannotEditMessage } from '../acquisition/common/constants'; @@ -38,6 +48,39 @@ const FileMenuView: React.FunctionComponent !exists(currentPropertyIndex), [currentPropertyIndex]); + const mapMachine = useMapStateMachine(); + + const fitBoundaries = () => { + const fileProperties = file.fileProperties; + + if (exists(fileProperties)) { + const locations = fileProperties + .map(fileProp => locationFromFileProperty(fileProp)) + .map(geom => getLatLng(geom)) + .filter(exists); + const a = latLngBounds(locations); + + mapMachine.requestFlyToBounds(a); + } + }; + + const onPropertyClick = (index: number, propertyId: number) => { + if (currentPropertyIndex !== index) { + onSelectProperty(propertyId); + } + }; + + const onZoomToProperty = useCallback( + (property: ApiGen_Concepts_FileProperty) => { + const geom = boundaryFromFileProperty(property); + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + }, + [mapMachine], + ); return ( @@ -53,44 +96,56 @@ const FileMenuView: React.FunctionComponent - Properties - } - title="Change properties" - toolTipId={`${file?.id ?? 0}-summary-cannot-edit-tooltip`} - editRestrictionMessage={editRestrictionMessage} - onEdit={onEditProperties} - /> + + + Properties + + + } + title="Change properties" + toolTipId={`${file?.id ?? 0}-summary-cannot-edit-tooltip`} + editRestrictionMessage={editRestrictionMessage} + onEdit={onEditProperties} + /> + + + + + + +
{sortedProperties.map((property: ApiGen_Concepts_FileProperty, index: number) => { const propertyName = getFilePropertyName(property); + const isCurrentProperty = currentPropertyIndex === index; return ( - { - if (currentPropertyIndex !== index) { - onSelectProperty(property.id); - } - }} - > - {currentPropertyIndex === index && } + + {isCurrentProperty && } - + {index + 1} - - {currentPropertyIndex === index ? ( + onPropertyClick(index, property.id)} + data-testid={`menu-item-row-${index}`} + className={cx({ selected: isCurrentProperty })} + > + {isCurrentProperty ? ( {propertyName.value} ) : ( {propertyName.value} )} + + + onZoomToProperty(property)}> + + ); @@ -118,11 +173,6 @@ export const StyledMenuWrapper = styled.div` const StyledRow = styled(Row)` width: 100%; - &.selected { - font-weight: bold; - cursor: default; - } - font-size: 1.4rem; font-weight: normal; cursor: pointer; @@ -133,8 +183,17 @@ const StyledRow = styled(Row)` } `; +const StyledCol = styled(Col)` + &.selected { + font-weight: bold; + cursor: default; + line-height: 3rem; + } +`; + const StyledIconWrapper = styled.div` &.selected { + font-weight: bold; background-color: ${props => props.theme.bcTokens.themeGold100}; } diff --git a/source/frontend/src/features/mapSideBar/shared/__snapshots__/FileMenuView.test.tsx.snap b/source/frontend/src/features/mapSideBar/shared/__snapshots__/FileMenuView.test.tsx.snap index 4b0ead5f15..5740d390fe 100644 --- a/source/frontend/src/features/mapSideBar/shared/__snapshots__/FileMenuView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/shared/__snapshots__/FileMenuView.test.tsx.snap @@ -240,15 +240,16 @@ exports[`FileMenuView component > matches snapshot 1`] = ` padding-bottom: 0.5rem; } -.c1.selected { - font-weight: bold; - cursor: default; -} - .c1 div.Button__value { font-size: 1.4rem; } +.c7.selected { + font-weight: bold; + cursor: default; + line-height: 3rem; +} + .c6 { background-color: #fcba19; font-size: 1.5rem; @@ -272,6 +273,7 @@ exports[`FileMenuView component > matches snapshot 1`] = ` } .c6.selected { + font-weight: bold; background-color: #FCBA19; } @@ -332,11 +334,50 @@ exports[`FileMenuView component > matches snapshot 1`] = `
- - Properties - +
+ + Properties + +
+
+
+ +
+
matches snapshot 1`] = ` class="c5" >
matches snapshot 1`] = `
matches snapshot 1`] = ` 023-214-937
+
+ +
matches snapshot 1`] = `
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx b/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx index 645738bdd6..c14ad702f6 100644 --- a/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx +++ b/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx @@ -1,12 +1,23 @@ +import { geoJSON } from 'leaflet'; +import { useCallback } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { FaSearchPlus } from 'react-icons/fa'; -import { RemoveButton } from '@/components/common/buttons'; +import { LinkButton, RemoveButton } from '@/components/common/buttons'; import { InlineInput } from '@/components/common/form/styles'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import OverflowTip from '@/components/common/OverflowTip'; import DraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; import { AreaUnitTypes } from '@/constants'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; -import { convertArea, formatApiAddress, formatNumber, pidFormatter } from '@/utils'; +import { + convertArea, + exists, + formatApiAddress, + formatNumber, + pidFormatter, + pimsGeomeryToGeometry, +} from '@/utils'; interface ISelectedOperationPropertyProps { property: ApiGen_Concepts_Property; @@ -24,6 +35,20 @@ export const SelectedOperationProperty: React.FunctionComponent< return formatNumber(sqm, 0, 4); }; + const mapMachine = useMapStateMachine(); + + const onZoomToProperty = useCallback( + (property: ApiGen_Concepts_Property) => { + const geom = property?.boundary ?? pimsGeomeryToGeometry(property?.location); + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds)) { + mapMachine.requestFlyToBounds(bounds); + } + }, + [mapMachine], + ); + return ( @@ -44,7 +69,12 @@ export const SelectedOperationProperty: React.FunctionComponent< )} {formatApiAddress(property?.address) ?? ''} - + + onZoomToProperty(property)}> + + + + diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx index 045cc282a4..bba118d5b3 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx @@ -1,17 +1,20 @@ import axios, { AxiosError } from 'axios'; import { FieldArray, Formik, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { useContext, useRef, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; import { toast } from 'react-toastify'; +import { LinkButton } from '@/components/common/buttons/LinkButton'; 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 TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; -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'; @@ -19,7 +22,7 @@ import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { isLatLngInFeatureSetBoundary, isValidId } from '@/utils'; +import { exists, isLatLngInFeatureSetBoundary, isValidId, latLngLiteralToGeometry } from '@/utils'; import { AddressForm, FileForm, PropertyForm } from '../../models'; import SidebarFooter from '../../SidebarFooter'; @@ -50,6 +53,20 @@ export const UpdateProperties: React.FunctionComponent = const { setModalContent, setDisplayModal } = useModalContext(); const { resetFilePropertyLocations } = useContext(SideBarContext); const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); + const mapMachine = useMapStateMachine(); + + const fitBoundaries = () => { + const fileProperties = formFile.properties; + + if (exists(fileProperties)) { + const locations = fileProperties.map( + p => p?.polygon ?? latLngLiteralToGeometry(p?.fileLocation), + ); + const bounds = geoJSON(locations).getBounds(); + + mapMachine.requestFlyToBounds(bounds); + } + }; const handleSaveClick = async () => { await formikRef?.current?.validateForm(); @@ -219,8 +236,23 @@ export const UpdateProperties: React.FunctionComponent = /> -
- +
+ Selected Properties + + + + + + + + + } + > {formikProps.values.properties.map((property, index) => ( renders as expected 1`] = ` margin-right: 0; } -.c30.c30.btn { +.c29.c29.btn { background-color: unset; border: none; } -.c30.c30.btn:hover, -.c30.c30.btn:focus, -.c30.c30.btn:active { +.c29.c29.btn:hover, +.c29.c29.btn:focus, +.c29.c29.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c30.c30.btn svg { +.c29.c29.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c30.c30.btn svg:hover { +.c29.c29.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c30.c30.btn.btn-primary svg { +.c29.c29.btn.btn-primary svg { color: #013366; } -.c30.c30.btn.btn-primary svg:hover { +.c29.c29.btn.btn-primary svg:hover { color: #013366; } -.c30.c30.btn.btn-light svg { +.c29.c29.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c30.c30.btn.btn-light svg:hover { +.c29.c29.btn.btn-light svg:hover { color: #CE3E39; } -.c30.c30.btn.btn-info svg { +.c29.c29.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c30.c30.btn.btn-info svg:hover { +.c29.c29.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c31.c31.btn { +.c30.c30.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -264,13 +264,13 @@ exports[`UpdateProperties component > renders as expected 1`] = ` line-height: unset; } -.c31.c31.btn .text { +.c30.c30.btn .text { display: none; } -.c31.c31.btn:hover, -.c31.c31.btn:active, -.c31.c31.btn:focus { +.c30.c30.btn:hover, +.c30.c30.btn:active, +.c30.c30.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -284,9 +284,9 @@ exports[`UpdateProperties component > renders as expected 1`] = ` flex-direction: row; } -.c31.c31.btn:hover .text, -.c31.c31.btn:active .text, -.c31.c31.btn:focus .text { +.c30.c30.btn:hover .text, +.c30.c30.btn:active .text, +.c30.c30.btn:focus .text { display: inline; line-height: 2rem; } @@ -310,7 +310,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` border-bottom-left-radius: 0; } -.c29 { +.c28 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -322,7 +322,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` gap: 0.8rem; } -.c29 .form-label { +.c28 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -482,27 +482,18 @@ exports[`UpdateProperties component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c25 { - font-size: 1.6rem; - color: #9F9D9C; - border-bottom: 0.2rem solid #606060; - margin-bottom: 0.9rem; - padding-bottom: 0.25rem; - font-family: 'BcSans-Bold'; -} - -.c28 { +.c27 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c26 { +.c25 { min-width: 3rem; min-height: 4.5rem; } -.c27 { +.c26 { font-size: 1.2rem; } @@ -588,7 +579,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` align-items: end; } -.c32 { +.c31 { position: -webkit-sticky; position: sticky; padding-top: 2rem; @@ -1232,47 +1223,49 @@ exports[`UpdateProperties component > renders as expected 1`] = `
- Selected properties +
+
+ Selected Properties +
+
+ +
+
-
-
- Identifier -
-
- Provide a descriptive name for this land - - - - - -
-
@@ -1283,7 +1276,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PID: 123-456-789
@@ -1363,7 +1356,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
+
+
diff --git a/source/frontend/src/utils/mapPropertyUtils.ts b/source/frontend/src/utils/mapPropertyUtils.ts index 45b203c9c8..158f4cb29e 100644 --- a/source/frontend/src/utils/mapPropertyUtils.ts +++ b/source/frontend/src/utils/mapPropertyUtils.ts @@ -5,6 +5,7 @@ import { GeoJsonProperties, Geometry, MultiPolygon, + Point, Polygon, } from 'geojson'; import { geoJSON, LatLngLiteral } from 'leaflet'; @@ -302,6 +303,12 @@ export function locationFromFileProperty( return fileProperty?.location ?? fileProperty?.property?.location ?? null; } +export function boundaryFromFileProperty( + fileProperty: ApiGen_Concepts_FileProperty | undefined | null, +): Geometry | null { + return fileProperty?.property?.boundary ?? null; +} + export function latLngFromMapProperty( mapProperty: IMapProperty | undefined | null, ): LatLngLiteral | null { @@ -311,6 +318,22 @@ export function latLngFromMapProperty( }; } +export function latLngLiteralToGeometry(latLng: LatLngLiteral | null | undefined): Point | null { + if (exists(latLng)) { + return { type: 'Point', coordinates: [latLng.lng, latLng.lat] }; + } + return null; +} + +export function pimsGeomeryToGeometry( + pimsGeomery: ApiGen_Concepts_Geometry | null | undefined, +): Point | null { + if (exists(pimsGeomery?.coordinate)) { + return { type: 'Point', coordinates: [pimsGeomery.coordinate.x, pimsGeomery.coordinate.y] }; + } + return null; +} + export function filePropertyToLocationBoundaryDataset( fileProperty: ApiGen_Concepts_FileProperty | undefined | null, ): LocationBoundaryDataset | null { From 3ace24be9bd1f6fa84d1911efb1fc47358c17bc8 Mon Sep 17 00:00:00 2001 From: Sue Tairaku Date: Tue, 24 Jun 2025 17:16:54 -0700 Subject: [PATCH 09/61] PSP-10458 fix --- .../leases/list/LeaseSearchResults/LeaseProperties.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx b/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx index e24bc475b0..e6def47b07 100644 --- a/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx +++ b/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx @@ -30,6 +30,12 @@ const LeaseProperties: React.FunctionComponent< displayProperties = properties; } + const formatPIDNumber = (pid: number): string => { + if (!pid) return ''; + const paddedPID = pid.toString().padStart(9, '0'); + return `${paddedPID.slice(0, 3)}-${paddedPID.slice(3, 6)}-${paddedPID.slice(6, 9)}`; + }; + const rowItems = displayProperties.map((property, index) => { return ( @@ -40,7 +46,7 @@ const LeaseProperties: React.FunctionComponent< {property.pid && (
- PID: {property.pid} + PID: {formatPIDNumber(property.pid)}
)} From 6817fa8614206e3b3b00603a03b9589e447cfb0d Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Tue, 24 Jun 2025 17:41:20 -0700 Subject: [PATCH 10/61] update file property sidebar to use property id instead of index to promote consistency. --- .../propertySelector/MapClickMonitor.tsx | 1 - .../SelectedPropertyRow.test.tsx.snap | 160 +++---- .../AcquisitionView.test.tsx.snap | 5 +- .../add/AcquisitionPropertiesSubForm.test.tsx | 18 +- ...AcquisitionPropertiesSubForm.test.tsx.snap | 172 ++++---- .../AddConsolidationMarkerSynchronizer.tsx | 6 +- .../DispositionView.test.tsx.snap | 3 +- .../DispositionPropertiesSubForm.test.tsx | 18 +- ...DispositionPropertiesSubForm.test.tsx.snap | 172 ++++---- .../mapSideBar/lease/LeaseView.test.tsx | 6 +- .../__snapshots__/LeaseView.test.tsx.snap | 3 +- .../ManagementView.test.tsx.snap | 3 +- .../form/ManagementPropertiesSubForm.test.tsx | 18 +- .../ManagementPropertiesSubForm.test.tsx.snap | 172 ++++---- .../edit/ManagementActivityEditForm.test.tsx | 36 +- .../AssociationHeader.test.tsx.snap | 23 +- .../PropertyAssociationTabView.test.tsx.snap | 21 +- .../ResearchContainer.test.tsx.snap | 3 +- .../ResearchProperties.test.tsx.snap | 86 ++-- .../mapSideBar/shared/FileMenuRow.tsx | 3 +- .../mapSideBar/shared/FileMenuView.test.tsx | 10 +- .../mapSideBar/shared/FileMenuView.tsx | 5 +- .../__snapshots__/FileMenuView.test.tsx.snap | 37 +- .../UpdateProperties.test.tsx.snap | 74 ++-- .../AddSubdivisionMarkerSynchronizer.tsx | 8 +- .../src/mocks/acquisitionFiles.mock.ts | 4 +- .../src/mocks/managementFiles.mock.ts | 1 + .../src/utils/mapPropertyUtils.test.tsx | 4 +- source/frontend/src/utils/mapPropertyUtils.ts | 17 +- .../src/utils/mapPropertyUtils_BACKUP_3389.ts | 416 ------------------ .../src/utils/mapPropertyUtils_BASE_3389.ts | 331 -------------- .../src/utils/mapPropertyUtils_LOCAL_3389.ts | 380 ---------------- .../src/utils/mapPropertyUtils_REMOTE_3389.ts | 366 --------------- 33 files changed, 565 insertions(+), 2017 deletions(-) delete mode 100644 source/frontend/src/utils/mapPropertyUtils_BACKUP_3389.ts delete mode 100644 source/frontend/src/utils/mapPropertyUtils_BASE_3389.ts delete mode 100644 source/frontend/src/utils/mapPropertyUtils_LOCAL_3389.ts delete mode 100644 source/frontend/src/utils/mapPropertyUtils_REMOTE_3389.ts diff --git a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx index 64bfba52e3..486f7eb3d0 100644 --- a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx +++ b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx @@ -53,7 +53,6 @@ export const MapClickMonitor: React.FunctionComponent = ( municipalityFeature: firstOrNull(mapMachine.mapLocationFeatureDataset.municipalityFeatures), selectingComponentId: mapMachine.mapLocationFeatureDataset.selectingComponentId, fileLocation: mapMachine.mapLocationFeatureDataset.fileLocation, - isActive: true, }; const parcelFeaturesNotInPims = mapMachine.mapLocationFeatureDataset.parcelFeatures?.filter(pf => { 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 53a3b0400c..580b47964c 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" />
- .c4.btn { + .c5.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -35,19 +35,19 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` cursor: pointer; } -.c4.btn .Button__value { +.c5.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c4.btn:hover { +.c5.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c4.btn:focus { +.c5.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -55,31 +55,31 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` box-shadow: none; } -.c4.btn.btn-primary { +.c5.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c4.btn.btn-primary:hover, -.c4.btn.btn-primary:active, -.c4.btn.btn-primary:focus { +.c5.btn.btn-primary:hover, +.c5.btn.btn-primary:active, +.c5.btn.btn-primary:focus { background-color: #1E5189; } -.c4.btn.btn-secondary { +.c5.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c4.btn.btn-secondary:hover, -.c4.btn.btn-secondary:active, -.c4.btn.btn-secondary:focus { +.c5.btn.btn-secondary:hover, +.c5.btn.btn-secondary:active, +.c5.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c4.btn.btn-info { +.c5.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -87,66 +87,66 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` padding-right: 0.6rem; } -.c4.btn.btn-info:hover, -.c4.btn.btn-info:active, -.c4.btn.btn-info:focus { +.c5.btn.btn-info:hover, +.c5.btn.btn-info:active, +.c5.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c4.btn.btn-light { +.c5.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c4.btn.btn-light:hover, -.c4.btn.btn-light:active, -.c4.btn.btn-light:focus { +.c5.btn.btn-light:hover, +.c5.btn.btn-light:active, +.c5.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c4.btn.btn-dark { +.c5.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c4.btn.btn-dark:hover, -.c4.btn.btn-dark:active, -.c4.btn.btn-dark:focus { +.c5.btn.btn-dark:hover, +.c5.btn.btn-dark:active, +.c5.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c4.btn.btn-danger { +.c5.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c4.btn.btn-danger:hover, -.c4.btn.btn-danger:active, -.c4.btn.btn-danger:focus { +.c5.btn.btn-danger:hover, +.c5.btn.btn-danger:active, +.c5.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c4.btn.btn-warning { +.c5.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c4.btn.btn-warning:hover, -.c4.btn.btn-warning:active, -.c4.btn.btn-warning:focus { +.c5.btn.btn-warning:hover, +.c5.btn.btn-warning:active, +.c5.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c4.btn.btn-link { +.c5.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -170,9 +170,9 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` text-decoration: underline; } -.c4.btn.btn-link:hover, -.c4.btn.btn-link:active, -.c4.btn.btn-link:focus { +.c5.btn.btn-link:hover, +.c5.btn.btn-link:active, +.c5.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -182,15 +182,15 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` outline: none; } -.c4.btn.btn-link:disabled, -.c4.btn.btn-link.disabled { +.c5.btn.btn-link:disabled, +.c5.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c4.btn:disabled, -.c4.btn:disabled:hover { +.c5.btn:disabled, +.c5.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -202,66 +202,66 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` cursor: not-allowed; } -.c4.Button .Button__icon { +.c5.Button .Button__icon { margin-right: 1.6rem; } -.c4.Button--icon-only:focus { +.c5.Button--icon-only:focus { outline: none; } -.c4.Button--icon-only .Button__icon { +.c5.Button--icon-only .Button__icon { margin-right: 0; } -.c5.c5.btn { +.c6.c6.btn { background-color: unset; border: none; } -.c5.c5.btn:hover, -.c5.c5.btn:focus, -.c5.c5.btn:active { +.c6.c6.btn:hover, +.c6.c6.btn:focus, +.c6.c6.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c5.c5.btn svg { +.c6.c6.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c5.c5.btn svg:hover { +.c6.c6.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c5.c5.btn.btn-primary svg { +.c6.c6.btn.btn-primary svg { color: #013366; } -.c5.c5.btn.btn-primary svg:hover { +.c6.c6.btn.btn-primary svg:hover { color: #013366; } -.c5.c5.btn.btn-light svg { +.c6.c6.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c5.c5.btn.btn-light svg:hover { +.c6.c6.btn.btn-light svg:hover { color: #CE3E39; } -.c5.c5.btn.btn-info svg { +.c6.c6.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c5.c5.btn.btn-info svg:hover { +.c6.c6.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c6.c6.btn { +.c7.c7.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -269,13 +269,13 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` line-height: unset; } -.c6.c6.btn .text { +.c7.c7.btn .text { display: none; } -.c6.c6.btn:hover, -.c6.c6.btn:active, -.c6.c6.btn:focus { +.c7.c7.btn:hover, +.c7.c7.btn:active, +.c7.c7.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -289,14 +289,14 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` flex-direction: row; } -.c6.c6.btn:hover .text, -.c6.c6.btn:active .text, -.c6.c6.btn:focus .text { +.c7.c7.btn:hover .text, +.c7.c7.btn:active .text, +.c7.c7.btn:focus .text { display: inline; line-height: 2rem; } -.c3 { +.c4 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -308,29 +308,33 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` gap: 0.8rem; } -.c3 .form-label { +.c4 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; } -.c2 { +.c3 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c0 { +.c1 { min-width: 3rem; min-height: 4.5rem; } -.c1 { +.c2 { font-size: 1.2rem; } +.c0 { + min-height: 4.5rem; +} +
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
@@ -417,12 +421,12 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` class="col-md-5" >
@@ -432,7 +436,7 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` class="pl-3 col-md-1" >
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PIN: 1111222
@@ -1553,7 +1557,7 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` class="pl-3 col-md-1" >
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PIN: 1111222
@@ -1553,7 +1557,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` class="pl-3 col-md-1" >
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PIN: 1111222
@@ -1553,7 +1557,7 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` class="pl-3 col-md-1" >
renders as expected when provided no pro class="mb-0 d-flex align-items-center" > renders as expected when provided no pro renders as expected when provided no pro
PIN: 1111222
@@ -1349,7 +1353,7 @@ exports[`ResearchProperties component > renders as expected when provided no pro class="col-md-5" >
renders as expected when provided no pro class="pl-3 col-md-1" >
renders as expected 1`] = ` margin-right: 0; } -.c30.c30.btn { +.c31.c31.btn { background-color: unset; border: none; } -.c30.c30.btn:hover, -.c30.c30.btn:focus, -.c30.c30.btn:active { +.c31.c31.btn:hover, +.c31.c31.btn:focus, +.c31.c31.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c30.c30.btn svg { +.c31.c31.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c30.c30.btn svg:hover { +.c31.c31.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c30.c30.btn.btn-primary svg { +.c31.c31.btn.btn-primary svg { color: #013366; } -.c30.c30.btn.btn-primary svg:hover { +.c31.c31.btn.btn-primary svg:hover { color: #013366; } -.c30.c30.btn.btn-light svg { +.c31.c31.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c30.c30.btn.btn-light svg:hover { +.c31.c31.btn.btn-light svg:hover { color: #CE3E39; } -.c30.c30.btn.btn-info svg { +.c31.c31.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c30.c30.btn.btn-info svg:hover { +.c31.c31.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c31.c31.btn { +.c32.c32.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -264,13 +264,13 @@ exports[`UpdateProperties component > renders as expected 1`] = ` line-height: unset; } -.c31.c31.btn .text { +.c32.c32.btn .text { display: none; } -.c31.c31.btn:hover, -.c31.c31.btn:active, -.c31.c31.btn:focus { +.c32.c32.btn:hover, +.c32.c32.btn:active, +.c32.c32.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -284,9 +284,9 @@ exports[`UpdateProperties component > renders as expected 1`] = ` flex-direction: row; } -.c31.c31.btn:hover .text, -.c31.c31.btn:active .text, -.c31.c31.btn:focus .text { +.c32.c32.btn:hover .text, +.c32.c32.btn:active .text, +.c32.c32.btn:focus .text { display: inline; line-height: 2rem; } @@ -310,7 +310,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` border-bottom-left-radius: 0; } -.c29 { +.c30 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -322,7 +322,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` gap: 0.8rem; } -.c29 .form-label { +.c30 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -491,21 +491,25 @@ exports[`UpdateProperties component > renders as expected 1`] = ` font-family: 'BcSans-Bold'; } -.c28 { +.c29 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c26 { +.c27 { min-width: 3rem; min-height: 4.5rem; } -.c27 { +.c28 { font-size: 1.2rem; } +.c26 { + min-height: 4.5rem; +} + .c5.btn.btn-light.Button { padding: 0; border: 0.1rem solid #9F9D9C; @@ -588,7 +592,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` align-items: end; } -.c32 { +.c33 { position: -webkit-sticky; position: sticky; padding-top: 2rem; @@ -1274,7 +1278,7 @@ exports[`UpdateProperties component > renders as expected 1`] = `
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PID: 123-456-789
@@ -1363,7 +1367,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` class="pl-3 col-md-1" >

renders as expected 1`] = ` class="pr-0 text-left col-4" >
@@ -1382,13 +1387,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Received @@ -1401,13 +1406,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
@@ -1418,13 +1423,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
@@ -1472,23 +1477,23 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >

renders as expected 1`] = ` class="pr-0 text-left col-4" >
Dec 18, 2024 @@ -1529,7 +1534,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Jul 29, 2022
@@ -1569,7 +1574,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >

Jul 10, 2024
@@ -1609,13 +1614,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Jul 10, 2025
@@ -1623,10 +1628,10 @@ exports[`AcquisitionView component > renders as expected 1`] = `

renders as expected 1`] = ` class="pr-0 text-left col-4" >
Test ACQ File
@@ -1666,7 +1671,7 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >

legacy file number
@@ -1706,13 +1711,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
Consensual Agreement
@@ -1778,13 +1783,13 @@ exports[`AcquisitionView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
South Coast Region
@@ -1792,10 +1797,10 @@ exports[`AcquisitionView component > renders as expected 1`] = `

renders as expected 1`] = ` class="collapse show" >

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

renders as expected 1`] = ` class="pr-0 text-left col-4" >
test representative comment
diff --git a/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap b/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap index 325b7599b9..1568bbaf0d 100644 --- a/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/disposition/__snapshots__/DispositionView.test.tsx.snap @@ -240,7 +240,7 @@ exports[`DispositionView component > renders as expected 1`] = ` width: 22rem; } -.c30 { +.c29 { overflow: auto; height: 100%; width: 100%; @@ -369,32 +369,32 @@ exports[`DispositionView component > renders as expected 1`] = ` align-items: end; } -.c32 { +.c31 { height: 100%; } -.c32 .tab-content { +.c31 .tab-content { border-radius: 0 0.4rem 0.4rem 0.4rem; overflow-y: auto; background-color: #f2f2f2; } -.c32 .tab-content .tab-pane { +.c31 .tab-content .tab-pane { position: relative; } -.c33 { +.c32 { background-color: white; color: var(--surface-color-primary-button-default); font-size: 1.4rem; border-color: transparent; } -.c33 .nav-tabs { +.c32 .nav-tabs { height: auto; } -.c33 .nav-item { +.c32 .nav-item { color: var(--surface-color-primary-button-default); min-width: 5rem; padding: 0.1rem 1.3rem; @@ -402,7 +402,7 @@ exports[`DispositionView component > renders as expected 1`] = ` margin-left: 0.1rem; } -.c33 .nav-item:hover { +.c32 .nav-item:hover { color: #f2f2f2; background-color: #1e5189; border-color: transparent; @@ -412,7 +412,7 @@ exports[`DispositionView component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c33 .nav-item.active { +.c32 .nav-item.active { border-radius: 0.4rem; color: #f2f2f2; background-color: #053662; @@ -420,7 +420,7 @@ exports[`DispositionView component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c31 { +.c30 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -437,24 +437,24 @@ exports[`DispositionView component > renders as expected 1`] = ` overflow-y: auto; } -.c34 { +.c33 { background-color: #f2f2f2; padding-top: 1rem; } -.c35 { +.c34 { text-align: right; overflow: hidden; } -.c39 { +.c38 { font-weight: bold; color: var(--theme-blue-100); border-bottom: 0.2rem var(--theme-blue-90) solid; margin-bottom: 2.4rem; } -.c36 { +.c35 { margin: 1.6rem; padding: 1.6rem; background-color: white; @@ -462,18 +462,18 @@ exports[`DispositionView component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c38.required::before { +.c37.required::before { content: '*'; position: absolute; top: 0.75rem; left: 0rem; } -.c37 { +.c36 { font-weight: bold; } -.c40 { +.c39 { padding: 0 0.4rem; display: block; } @@ -508,10 +508,9 @@ exports[`DispositionView component > renders as expected 1`] = ` font-size: 1.4rem; } -.c29.selected { - font-weight: bold; +.c24.selected { cursor: default; - line-height: 3rem; + font-weight: bold; } .c28 { @@ -537,7 +536,6 @@ exports[`DispositionView component > renders as expected 1`] = ` } .c28.selected { - font-weight: bold; background-color: #FCBA19; } @@ -949,6 +947,7 @@ exports[`DispositionView component > renders as expected 1`] = ` >
renders as expected 1`] = ` class="pr-0 text-left col-5" >
Road Closure
@@ -1373,7 +1375,7 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
Surplus Declaration
@@ -1413,7 +1415,7 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >

Jun 29, 1917
@@ -1453,13 +1455,13 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
Pending Litigation
@@ -1471,13 +1473,13 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
PLMB
@@ -1489,13 +1491,13 @@ exports[`DispositionView component > renders as expected 1`] = ` class="pr-0 text-left col-5" >
Cannot determine
@@ -1503,10 +1505,10 @@ exports[`DispositionView component > renders as expected 1`] = `

renders as expected 1`] = ` class="pr-0 text-left col-5" >
renders as expected 1`] = ` align-items: end; } -.c30 { +.c29 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -360,32 +360,32 @@ exports[`LeaseView component > renders as expected 1`] = ` overflow-y: auto; } -.c31 { +.c30 { height: 100%; } -.c31 .tab-content { +.c30 .tab-content { border-radius: 0 0.4rem 0.4rem 0.4rem; overflow-y: auto; background-color: #f2f2f2; } -.c31 .tab-content .tab-pane { +.c30 .tab-content .tab-pane { position: relative; } -.c32 { +.c31 { background-color: white; color: var(--surface-color-primary-button-default); font-size: 1.4rem; border-color: transparent; } -.c32 .nav-tabs { +.c31 .nav-tabs { height: auto; } -.c32 .nav-item { +.c31 .nav-item { color: var(--surface-color-primary-button-default); min-width: 5rem; padding: 0.1rem 1.3rem; @@ -393,7 +393,7 @@ exports[`LeaseView component > renders as expected 1`] = ` margin-left: 0.1rem; } -.c32 .nav-item:hover { +.c31 .nav-item:hover { color: #f2f2f2; background-color: #1e5189; border-color: transparent; @@ -403,7 +403,7 @@ exports[`LeaseView component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c32 .nav-item.active { +.c31 .nav-item.active { border-radius: 0.4rem; color: #f2f2f2; background-color: #053662; @@ -411,11 +411,11 @@ exports[`LeaseView component > renders as expected 1`] = ` text-shadow: 0.1rem 0 0 currentColor; } -.c34 { +.c33 { text-align: right; } -.c33 { +.c32 { height: 100%; overflow-y: auto; grid-area: leasecontent; @@ -430,7 +430,7 @@ exports[`LeaseView component > renders as expected 1`] = ` background-color: #f2f2f2; } -.c35 { +.c34 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -458,7 +458,7 @@ exports[`LeaseView component > renders as expected 1`] = ` width: 22rem; } -.c29 { +.c28 { overflow: auto; height: 100%; width: 100%; @@ -494,10 +494,9 @@ exports[`LeaseView component > renders as expected 1`] = ` font-size: 1.4rem; } -.c28.selected { - font-weight: bold; +.c23.selected { cursor: default; - line-height: 3rem; + font-weight: bold; } .c27 { @@ -523,7 +522,6 @@ exports[`LeaseView component > renders as expected 1`] = ` } .c27.selected { - font-weight: bold; background-color: #FCBA19; } @@ -1018,6 +1016,7 @@ exports[`LeaseView component > renders as expected 1`] = ` >

- +
); diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx index 25ad168ea9..645ddf9b8d 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx @@ -21,7 +21,7 @@ describe('ManagementTeamSubForm component', () => { const ref = createRef>(); const utils = render( - {formikProps => } + {formikProps => } , { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx index 5ee765acb9..2563efb9d9 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx @@ -18,7 +18,13 @@ import { WithManagementTeam, } from '../models/ManagementTeamSubFormModel'; -const ManagementTeamSubForm: React.FunctionComponent> = () => { +export interface IManagementTeamSubFormProps { + canEditDetails: boolean; +} + +const ManagementTeamSubForm: React.FunctionComponent = ({ + canEditDetails, +}) => { const { values, setFieldTouched, errors } = useFormikContext(); const { getOptionsByType } = useLookupCodeHelpers(); const { setModalContent, setDisplayModal } = useModalContext(); @@ -43,44 +49,52 @@ const ManagementTeamSubForm: React.FunctionComponent { setFieldTouched(`team.${index}.contact`); }} + disabled={!canEditDetails} /> + + - { - setModalContent({ - ...getDeleteModalProps(), - title: 'Remove Team Member', - message: 'Do you wish to remove this team member?', - okButtonText: 'Yes', - cancelButtonText: 'No', - handleOk: () => { - arrayHelpers.remove(index); - setDisplayModal(false); - }, - handleCancel: () => { - setDisplayModal(false); - }, - }); - setDisplayModal(true); - }} - /> + {canEditDetails && ( + { + setModalContent({ + ...getDeleteModalProps(), + title: 'Remove Team Member', + message: 'Do you wish to remove this team member?', + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: () => { + arrayHelpers.remove(index); + setDisplayModal(false); + }, + handleCancel: () => { + setDisplayModal(false); + }, + }); + setDisplayModal(true); + }} + /> + )} + {isValidId(teamMember.contact?.organizationId) && !isValidId(teamMember.contact?.personId) && ( )} @@ -93,15 +107,17 @@ const ManagementTeamSubForm: React.FunctionComponent )} - { - const member = new ManagementTeamSubFormModel(); - arrayHelpers.push(member); - }} - > - + Add another team member - + {canEditDetails && ( + { + const member = new ManagementTeamSubFormModel(); + arrayHelpers.push(member); + }} + > + + Add another team member + + )} )} /> diff --git a/source/frontend/src/features/mapSideBar/management/tabs/ManagementFileTabs.tsx b/source/frontend/src/features/mapSideBar/management/tabs/ManagementFileTabs.tsx index 86a443a4a2..fc33abdbb2 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/ManagementFileTabs.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/ManagementFileTabs.tsx @@ -2,13 +2,8 @@ import React, { useContext, useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; -import { NoteTypes } from '@/constants'; import { Claims } from '@/constants/claims'; import { FileTabs, FileTabType, TabFileView } from '@/features/mapSideBar/shared/detail/FileTabs'; -import NoteListContainer from '@/features/notes/list/NoteListContainer'; -import NoteListView from '@/features/notes/list/NoteListView'; -import { PropertyNoteSummaryContainer } from '@/features/notes/list/PropertyNoteSummaryContainer'; -import { PropertyNoteSummaryView } from '@/features/notes/list/PropertyNoteSummaryView'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; import { isValidId } from '@/utils'; @@ -18,6 +13,7 @@ import usePathGenerator from '../../shared/sidebarPathGenerator'; import ManagementDocumentsTab from '../../shared/tabs/ManagementDocumentsTab'; import ActivitiesTab from './activities/ActivitiesTab'; import ManagementSummaryView from './fileDetails/detail/ManagementSummaryView'; +import ManagementFileNotesTab from './notes/ManagementFileNotesTab'; export interface IManagementFileTabsProps { managementFile?: ApiGen_Concepts_ManagementFile; @@ -71,7 +67,9 @@ export const ManagementFileTabs: React.FC = ({ if (isValidId(managementFile?.id) && hasClaim(Claims.DOCUMENT_VIEW)) { tabViews.push({ - content: , + content: ( + + ), key: FileTabType.DOCUMENTS, name: 'Documents', }); @@ -80,19 +78,7 @@ export const ManagementFileTabs: React.FC = ({ if (isValidId(managementFile?.id) && hasClaim(Claims.NOTE_VIEW)) { tabViews.push({ content: ( - <> - - - + ), key: FileTabType.NOTES, name: 'Notes', diff --git a/source/frontend/src/features/mapSideBar/management/tabs/__snapshots__/ManagementFileTabs.test.tsx.snap b/source/frontend/src/features/mapSideBar/management/tabs/__snapshots__/ManagementFileTabs.test.tsx.snap index 72d0227625..700dd0f945 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/__snapshots__/ManagementFileTabs.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/management/tabs/__snapshots__/ManagementFileTabs.test.tsx.snap @@ -82,11 +82,6 @@ exports[`ManagementFileTabs component > matches snapshot 1`] = ` border-radius: 0.5rem; } -.c8 { - padding: 0 0.4rem; - display: block; -} - .c6.required::before { content: '*'; position: absolute; @@ -98,6 +93,11 @@ exports[`ManagementFileTabs component > matches snapshot 1`] = ` font-weight: bold; } +.c8 { + padding: 0 0.4rem; + display: block; +} +
diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/ActivitiesTab.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/ActivitiesTab.tsx index 2dd09c4ddf..7cc3a1c1b5 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/ActivitiesTab.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/ActivitiesTab.tsx @@ -10,6 +10,7 @@ import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; import { isValidId } from '@/utils'; +import ManagementStatusUpdateSolver from '../fileDetails/detail/ManagementStatusUpdateSolver'; import AdHocFileActivitiesSummaryContainer from './list/AdHocSummaryActivitiesContainer'; import AdHocSummaryActivitiesView from './list/AdHocSummaryActivitiesListView'; import ManagementFileActivitiesListContainer from './list/ManagementFileActivitiesListContainer'; @@ -24,17 +25,19 @@ export const ActivitiesTab: React.FunctionComponent = ({ ma const pathGenerator = usePathGenerator(); const onAdd = () => { - if (isValidId(managementFile?.id)) { + if (isValidId(managementFile.id)) { pathGenerator.addDetail('management', managementFile.id, 'activities'); } }; + const statusSolver = new ManagementStatusUpdateSolver(managementFile); + return (
- {hasClaim(Claims.MANAGEMENT_EDIT) && ( + {hasClaim(Claims.MANAGEMENT_EDIT) && statusSolver.canEditActivities() && ( Add Activity @@ -50,6 +53,7 @@ export const ActivitiesTab: React.FunctionComponent = ({ ma
void; viewEnabled: boolean; View: React.FunctionComponent>; @@ -23,8 +26,16 @@ export interface IPropertyActivityDetailContainerProps { */ export const FileActivityDetailContainer: React.FunctionComponent< React.PropsWithChildren -> = ({ managementFileId, propertyActivityId, onClose, viewEnabled, View }) => { +> = ({ + managementFileId, + managementActivityId: propertyActivityId, + onClose, + viewEnabled, + View, +}) => { const [show, setShow] = useState(true); + const { file } = useContext(SideBarContext); + const castedFile = file as unknown as ApiGen_Concepts_ManagementFile; const [loadedActivity, setLoadedActivity] = useState( null, @@ -88,6 +99,8 @@ export const FileActivityDetailContainer: React.FunctionComponent< } }, [managementFileId, propertyActivityId, fetchActivity]); + const StatusSolver = new ManagementStatusUpdateSolver(castedFile); + return ( ); diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx index e4650af73a..d8428cadcd 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/detail/FileActivityDetailView.tsx @@ -25,9 +25,11 @@ import { ApiGen_Concepts_PropertyActivityInvoice } from '@/models/api/generated/ export interface IFileActivityDetailViewProps { managementId: number; activity: ApiGen_Concepts_PropertyActivity | null; - onClose: () => void; loading: boolean; show: boolean; + canEditDocuments: boolean; + canEditActivity: boolean; + onClose: () => void; setShow: (show: boolean) => void; } @@ -80,7 +82,7 @@ export const FileActivityDetailView: React.FunctionComponent< - {hasClaim(Claims.MANAGEMENT_EDIT) && ( + {hasClaim(Claims.MANAGEMENT_EDIT) && props.canEditActivity && ( { @@ -122,6 +124,7 @@ export const FileActivityDetailView: React.FunctionComponent< parentId={props.activity?.id.toString() ?? ''} addButtonText="Add a Management Document" relationshipType={ApiGen_CodeTypes_DocumentRelationType.ManagementActivities} + disableAdd={!props.canEditDocuments} /> ); }; diff --git a/source/frontend/src/features/mapSideBar/management/tabs/activities/list/ManagementFileActivitiesListContainer.tsx b/source/frontend/src/features/mapSideBar/management/tabs/activities/list/ManagementFileActivitiesListContainer.tsx index a6ddb5b0f9..059f65ece6 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/activities/list/ManagementFileActivitiesListContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/activities/list/ManagementFileActivitiesListContainer.tsx @@ -10,14 +10,17 @@ import { getDeleteModalProps, useModalContext } from '@/hooks/useModalContext'; import useIsMounted from '@/hooks/util/useIsMounted'; import { ApiGen_Concepts_PropertyActivity } from '@/models/api/generated/ApiGen_Concepts_PropertyActivity'; +import ManagementStatusUpdateSolver from '../../fileDetails/detail/ManagementStatusUpdateSolver'; + export interface IPropertyManagementActivitiesListContainerProps { managementFileId: number; + statusSolver: ManagementStatusUpdateSolver; View: React.FC; } const ManagementFileActivitiesListContainer: React.FunctionComponent< IPropertyManagementActivitiesListContainerProps -> = ({ managementFileId, View }) => { +> = ({ managementFileId, statusSolver, View }) => { const isMounted = useIsMounted(); const { setModalContent, setDisplayModal } = useModalContext(); const [propertyActivities, setPropertyActivities] = useState([]); @@ -62,10 +65,13 @@ const ManagementFileActivitiesListContainer: React.FunctionComponent< pathGenerator.showDetail('management', managementFileId, 'activities', activityId, false); }; + const canEditActivities: boolean = statusSolver?.canEditActivities(); + return ( = ({ isLoading, propertyActivities, sort, onDelete, onView, setSort }) => { +> = ({ isLoading, propertyActivities, sort, canEditActivities, onDelete, onView, setSort }) => { return ( ); }; diff --git a/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver.ts b/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver.ts index 419b1fec30..95f43b9a27 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver.ts +++ b/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver.ts @@ -1,28 +1,52 @@ +import { IUpdateDocumentsStrategy } from '@/features/documents/models/IUpdateDocumentsStrategy'; +import { IUpdateNotesStrategy } from '@/features/notes/models/IUpdateNotesStrategy'; import { ApiGen_CodeTypes_ManagementFileStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_ManagementFileStatusTypes'; import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; -class ManagementStatusUpdateSolver { - private readonly managementFile: ApiGen_Concepts_ManagementFile | null; +class ManagementStatusUpdateSolver implements IUpdateDocumentsStrategy, IUpdateNotesStrategy { + private readonly statusCode: string | null; - constructor(apiModel: ApiGen_Concepts_ManagementFile | undefined | null) { - this.managementFile = apiModel ?? null; + constructor(readonly managementFile: ApiGen_Concepts_ManagementFile | null) { + this.managementFile = managementFile ?? null; + this.statusCode = managementFile ? managementFile.fileStatusTypeCode?.id : null; } - canEditDetails(): boolean { + public isAdminProtected(): boolean { if (this.managementFile === null) { return false; } - const statusCode = this.managementFile.fileStatusTypeCode?.id; - let canEdit = false; - - switch (statusCode) { + let isProtected: boolean; + switch (this.statusCode) { case ApiGen_CodeTypes_ManagementFileStatusTypes.ACTIVE: case ApiGen_CodeTypes_ManagementFileStatusTypes.DRAFT: case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: + case ApiGen_CodeTypes_ManagementFileStatusTypes.THIRDRDPARTY: + case ApiGen_CodeTypes_ManagementFileStatusTypes.CANCELLED: + isProtected = false; + break; + case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: + case ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE: + isProtected = true; + break; + } + + return isProtected; + } + + public canEditDetails(): boolean { + if (!this.managementFile) { + return false; + } + let canEdit: boolean; + + switch (this.statusCode) { + case ApiGen_CodeTypes_ManagementFileStatusTypes.ACTIVE: + case ApiGen_CodeTypes_ManagementFileStatusTypes.DRAFT: case ApiGen_CodeTypes_ManagementFileStatusTypes.THIRDRDPARTY: canEdit = true; break; + case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: case ApiGen_CodeTypes_ManagementFileStatusTypes.CANCELLED: case ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE: @@ -36,22 +60,24 @@ class ManagementStatusUpdateSolver { return canEdit; } - canEditDocuments(): boolean { + public canEditDocuments(): boolean { if (this.managementFile === null) { return false; } + let canEdit: boolean; - const statusCode = this.managementFile.fileStatusTypeCode?.id; - let canEdit = false; - - switch (statusCode) { + switch (this.statusCode) { case ApiGen_CodeTypes_ManagementFileStatusTypes.ACTIVE: case ApiGen_CodeTypes_ManagementFileStatusTypes.DRAFT: - case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: case ApiGen_CodeTypes_ManagementFileStatusTypes.CANCELLED: case ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE: case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: case ApiGen_CodeTypes_ManagementFileStatusTypes.THIRDRDPARTY: + canEdit = true; + break; + case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: + canEdit = false; + break; default: canEdit = true; break; @@ -60,45 +86,45 @@ class ManagementStatusUpdateSolver { return canEdit; } - canEditNotes(): boolean { + public canEditNotes(): boolean { if (this.managementFile === null) { return false; } + let canEditNotes: boolean; - const statusCode = this.managementFile.fileStatusTypeCode?.id; - let canEdit = false; - - switch (statusCode) { + switch (this.statusCode) { case ApiGen_CodeTypes_ManagementFileStatusTypes.ACTIVE: case ApiGen_CodeTypes_ManagementFileStatusTypes.DRAFT: - case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: case ApiGen_CodeTypes_ManagementFileStatusTypes.CANCELLED: case ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE: case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: case ApiGen_CodeTypes_ManagementFileStatusTypes.THIRDRDPARTY: + canEditNotes = true; + break; + case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: + canEditNotes = false; + break; default: - canEdit = true; + canEditNotes = true; break; } - return canEdit; + return canEditNotes; } - canEditProperties(): boolean { - if (this.managementFile === null) { + public canEditActivities(): boolean { + if (!this.managementFile) { return false; } + let canEdit: boolean; - const statusCode = this.managementFile.fileStatusTypeCode?.id; - let canEdit = false; - - switch (statusCode) { + switch (this.statusCode) { case ApiGen_CodeTypes_ManagementFileStatusTypes.ACTIVE: case ApiGen_CodeTypes_ManagementFileStatusTypes.DRAFT: - case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: case ApiGen_CodeTypes_ManagementFileStatusTypes.THIRDRDPARTY: canEdit = true; break; + case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: case ApiGen_CodeTypes_ManagementFileStatusTypes.CANCELLED: case ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE: @@ -112,29 +138,30 @@ class ManagementStatusUpdateSolver { return canEdit; } - isAdminProtected(): boolean { - if (this.managementFile === null) { + public canEditProperties(): boolean { + if (!this.managementFile) { return false; } + let canEdit: boolean; - const statusCode = this.managementFile.fileStatusTypeCode?.id; - let isProtected: boolean; - - switch (statusCode) { + switch (this.statusCode) { case ApiGen_CodeTypes_ManagementFileStatusTypes.ACTIVE: case ApiGen_CodeTypes_ManagementFileStatusTypes.DRAFT: - case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: - case ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE: case ApiGen_CodeTypes_ManagementFileStatusTypes.THIRDRDPARTY: - isProtected = false; + canEdit = true; break; + case ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD: case ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED: case ApiGen_CodeTypes_ManagementFileStatusTypes.CANCELLED: - isProtected = true; + case ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE: + canEdit = false; + break; + default: + canEdit = false; break; } - return isProtected; + return canEdit; } } diff --git a/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.test.tsx b/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.test.tsx index d7bf30db7c..51877e7133 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.test.tsx @@ -75,22 +75,28 @@ describe('ManagementSummaryView component', () => { expect(icon).toBeNull(); }); - it('renders the warning icon for management files in non-editable status', async () => { - const { queryByTitle, queryByTestId } = setup( - { - managementFile: { - ...mockManagementFileResponse(), - fileStatusTypeCode: toTypeCode(ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE), + it.each([ + ['Management File Status is "Completed"', ApiGen_CodeTypes_ManagementFileStatusTypes.COMPLETE], + ['Management File Status is "Archived"', ApiGen_CodeTypes_ManagementFileStatusTypes.ARCHIVED], + ])( + 'renders the warning icon for management files in non-editable status - %s', + async (_: string, fileStatus: ApiGen_CodeTypes_ManagementFileStatusTypes) => { + const { queryByTitle, queryByTestId } = setup( + { + managementFile: { + ...mockManagementFileResponse(), + fileStatusTypeCode: toTypeCode(fileStatus), + }, }, - }, - { claims: [Claims.MANAGEMENT_EDIT] }, - ); - await waitForEffects(); - const editButton = queryByTitle('Edit management file'); - const icon = queryByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); - expect(editButton).toBeNull(); - expect(icon).toBeVisible(); - }); + { claims: [Claims.MANAGEMENT_EDIT] }, + ); + await waitForEffects(); + const editButton = queryByTitle('Edit management file'); + const icon = queryByTestId('tooltip-icon-1-summary-cannot-edit-tooltip'); + expect(editButton).toBeNull(); + expect(icon).toBeVisible(); + }, + ); it('it does not render the warning icon for management files in non-editable status for Admins', async () => { const { queryByTitle, queryByTestId } = setup( diff --git a/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.tsx b/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.tsx index 77e3951a2f..c8decebb01 100644 --- a/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.tsx +++ b/source/frontend/src/features/mapSideBar/management/tabs/fileDetails/detail/ManagementSummaryView.tsx @@ -28,7 +28,7 @@ export const ManagementSummaryView: React.FunctionComponent { - if (keycloak.hasRole(Roles.SYSTEM_ADMINISTRATOR) || statusSolver.canEditProperties()) { + if (keycloak.hasRole(Roles.SYSTEM_ADMINISTRATOR) || !statusSolver.isAdminProtected()) { return true; } return false; @@ -45,14 +45,10 @@ export const ManagementSummaryView: React.FunctionComponent - {keycloak.hasClaim(Claims.MANAGEMENT_EDIT) && - managementFile !== undefined && - canEditDetails() ? ( + {keycloak.hasClaim(Claims.MANAGEMENT_EDIT) && managementFile && canEditDetails() ? ( ) : null} - {keycloak.hasClaim(Claims.MANAGEMENT_EDIT) && - managementFile !== undefined && - !canEditDetails() ? ( + {keycloak.hasClaim(Claims.MANAGEMENT_EDIT) && managementFile && !canEditDetails() ? ( void; +} + +const ManagementFileNotesTab: React.FunctionComponent = ({ + managementFile, + onSuccess, +}) => { + const statusSolver = new ManagementStatusUpdateSolver(managementFile); + + return ( + <> + + + + ); +}; + +export default ManagementFileNotesTab; diff --git a/source/frontend/src/features/mapSideBar/management/update/UpdateManagementContainer.tsx b/source/frontend/src/features/mapSideBar/management/update/UpdateManagementContainer.tsx index a70c142b41..510d8b8000 100644 --- a/source/frontend/src/features/mapSideBar/management/update/UpdateManagementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/update/UpdateManagementContainer.tsx @@ -33,6 +33,8 @@ export const UpdateManagementContainer = React.forwardRef< const dispositionStatusTypes = getByType(API.DISPOSITION_FILE_STATUS_TYPES); + const statusSolver = new ManagementStatusUpdateSolver(managementFile); + const { putManagementFile: { execute: updateManagementFile, loading }, } = useManagementProvider(); @@ -62,6 +64,7 @@ export const UpdateManagementContainer = React.forwardRef< ApiGen_CodeTypes_ManagementFileStatusTypes.DRAFT.toString(), ApiGen_CodeTypes_ManagementFileStatusTypes.HOLD.toString(), ]; + //refresh the map properties if this disposition file was set to a final state. onSuccess( !!managementFile.fileStatusTypeCode?.id && @@ -93,6 +96,7 @@ export const UpdateManagementContainer = React.forwardRef< , diff --git a/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.test.tsx b/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.test.tsx index a54881707e..9968f44afe 100644 --- a/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.test.tsx @@ -6,7 +6,7 @@ import { IAutocompletePrediction } from '@/interfaces/IAutocomplete'; import { mockLookups } from '@/mocks/lookups.mock'; import { mockManagementFileResponse } from '@/mocks/managementFiles.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; -import { act, render, RenderOptions, userEvent, waitForEffects } from '@/utils/test-utils'; +import { act, render, RenderOptions, userEvent, waitFor, waitForEffects } from '@/utils/test-utils'; import { ManagementFormModel } from '../models/ManagementFormModel'; import UpdateManagementForm, { IUpdateManagementFormProps } from './UpdateManagementForm'; @@ -26,9 +26,10 @@ vi.mock('react-visibility-sensor', () => { }; }); +const retrieveProjectProductsFn = vi.fn(); vi.mock('@/hooks/repositories/useProjectProvider'); vi.mocked(useProjectProvider).mockReturnValue({ - retrieveProjectProducts: vi.fn(), + retrieveProjectProducts: retrieveProjectProductsFn, } as unknown as ReturnType); describe('UpdateManagementForm component', () => { @@ -40,6 +41,7 @@ describe('UpdateManagementForm component', () => { initialValues={props.initialValues} onSubmit={props.onSubmit} loading={props.loading} + canEditDetails={props.canEditDetails ?? true} />, { ...renderOptions, @@ -82,12 +84,24 @@ describe('UpdateManagementForm component', () => { }); it('renders as expected', async () => { - const { asFragment } = await setup({ initialValues, loading: false, formikRef: ref, onSubmit }); + const { asFragment } = await setup({ + initialValues, + loading: false, + formikRef: ref, + canEditDetails: true, + onSubmit, + }); expect(asFragment()).toMatchSnapshot(); }); it('renders loading spinner', async () => { - const { getByTestId } = await setup({ initialValues, loading: true, formikRef: ref, onSubmit }); + const { getByTestId } = await setup({ + initialValues, + loading: true, + formikRef: ref, + canEditDetails: true, + onSubmit, + }); expect(getByTestId('filter-backdrop-loading')).toBeVisible(); }); @@ -96,6 +110,7 @@ describe('UpdateManagementForm component', () => { initialValues, loading: false, formikRef: ref, + canEditDetails: true, onSubmit, }); @@ -124,6 +139,7 @@ describe('UpdateManagementForm component', () => { initialValues, loading: false, formikRef: ref, + canEditDetails: true, onSubmit, }); @@ -145,4 +161,37 @@ describe('UpdateManagementForm component', () => { expect(onSubmit).toHaveBeenCalled(); }); + + it('it disables fields when file is not editable', async () => { + retrieveProjectProductsFn.mockResolvedValue([]); + const { container } = await setup({ + initialValues, + loading: false, + formikRef: ref, + canEditDetails: false, + onSubmit, + }); + await waitForEffects(); + + const projectInput = container.querySelector(`#typeahead-project`); + expect(projectInput).toBeDisabled(); + + const productInput = container.querySelector(`select#input-productId`); + expect(productInput).toBeDisabled(); + + const fundingInput = container.querySelector(`#input-fundingTypeCode`); + expect(fundingInput).toBeDisabled(); + + const fileNameInput = container.querySelector(`#input-fileName`); + expect(fileNameInput).toBeDisabled(); + + const legacyFileInput = container.querySelector(`#input-legacyFileNum`); + expect(legacyFileInput).toBeDisabled(); + + const pourposeInput = container.querySelector(`#input-purposeTypeCode`); + expect(pourposeInput).toBeDisabled(); + + const additionalDetailsInput = container.querySelector(`#input-additionalDetails`); + expect(additionalDetailsInput).toBeDisabled(); + }); }); diff --git a/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.tsx b/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.tsx index c1e6784048..916d5ee670 100644 --- a/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/update/UpdateManagementForm.tsx @@ -21,6 +21,7 @@ import { ManagementFormModel } from '../models/ManagementFormModel'; export interface IUpdateManagementFormProps { formikRef: React.Ref>; initialValues: ManagementFormModel; + canEditDetails: boolean; onSubmit: ( values: ManagementFormModel, formikHelpers: FormikHelpers, @@ -31,6 +32,7 @@ export interface IUpdateManagementFormProps { const UpdateManagementForm: React.FC = ({ formikRef, initialValues, + canEditDetails, onSubmit, loading, }) => { @@ -102,9 +104,10 @@ const UpdateManagementForm: React.FC = ({ formikProps.setFieldValue('productId', ''); } }} + disabled={!canEditDetails} /> - {projectProducts !== undefined && ( + {projectProducts && ( + - + +
- +
diff --git a/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx b/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx index 1c881e3bcd..fdfdb87621 100644 --- a/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx @@ -68,11 +68,7 @@ const ProjectTabsContainer: React.FC = ({ if (project?.id && hasClaim(Claims.NOTE_VIEW)) { tabViews.push({ content: ( - + ), key: ProjectTabNames.notes, name: 'Notes', diff --git a/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx b/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx index cd9def782f..a553d26b00 100644 --- a/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx +++ b/source/frontend/src/features/mapSideBar/property/PropertyContainer.tsx @@ -247,7 +247,7 @@ export const PropertyContainer: React.FunctionComponent type={NoteTypes.Property} entityId={composedPropertyState.apiWrapper.response.id} onSuccess={onChildSuccess} - NoteListView={NoteListView} + View={NoteListView} /> void, onDelete: (activityId: number) => void, ) { @@ -109,7 +110,7 @@ export function activityActionColumn( const { hasClaim } = useKeycloakWrapper(); const activityRow = cellProps.row.original; const renderDelete = () => { - if (hasClaim(Claims.MANAGEMENT_DELETE) && exists(onDelete)) { + if (hasClaim(Claims.MANAGEMENT_DELETE) && canEdit && exists(onDelete)) { if ( activityRow?.activityStatusType?.id === PropertyManagementActivityStatusTypes.NOTSTARTED ) { diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx index 7390a00f4c..8cb0d17a75 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx @@ -3,6 +3,7 @@ import { useHistory } from 'react-router-dom'; import { TableSort } from '@/components/Table/TableSort'; import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; +import ManagementStatusUpdateSolver from '@/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver'; import { usePropertyActivityRepository } from '@/hooks/repositories/usePropertyActivityRepository'; import { getDeleteModalProps, useModalContext } from '@/hooks/useModalContext'; import useIsMounted from '@/hooks/util/useIsMounted'; @@ -13,6 +14,7 @@ import { IManagementActivitiesListViewProps } from './ManagementActivitiesListVi import { PropertyActivityRow } from './models/PropertyActivityRow'; export interface IPropertyManagementActivitiesListContainerProps { + statusSolver?: ManagementStatusUpdateSolver; propertyId: number; isAdHoc?: boolean; View: React.FC; @@ -20,7 +22,7 @@ export interface IPropertyManagementActivitiesListContainerProps { const PropertyManagementActivitiesListContainer: React.FunctionComponent< IPropertyManagementActivitiesListContainerProps -> = ({ propertyId, isAdHoc, View }) => { +> = ({ statusSolver, propertyId, isAdHoc, View }) => { const history = useHistory(); const isMounted = useIsMounted(); const { setModalContent, setDisplayModal } = useModalContext(); @@ -64,6 +66,8 @@ const PropertyManagementActivitiesListContainer: React.FunctionComponent< history.push(`/mapview/sidebar/property/${propertyId}/management/activity/${activityId}`); }; + const canEditActivities = !statusSolver || statusSolver?.canEditActivities(); + return ( ); }; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx index e428488631..bdef27bdb1 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx @@ -40,6 +40,7 @@ describe('Activities list view', () => { onView={onView} setSort={setSort} sort={undefined} + canEditActivities={renderOptions?.canEditActivities ?? true} />, { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx index 2c360c09b7..990d02a952 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx @@ -17,6 +17,7 @@ export interface IManagementActivitiesListViewProps { isLoading: boolean; propertyActivities: PropertyActivityRow[]; sort: TableSort; + canEditActivities: boolean; getNavigationUrl?: (row: PropertyActivityRow) => { title: string; url: string }; setSort: React.Dispatch>>; onCreate?: () => void; @@ -28,6 +29,7 @@ const ManagementActivitiesListView: React.FunctionComponent} - onAdd={onCreate} - /> + canEditActivities ? ( + } + onAdd={onCreate} + isAddEnabled={canEditActivities} + /> + ) : ( + 'Activities List' + ) } >
); diff --git a/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx b/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx index a99bef895a..9539e17514 100644 --- a/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/research/tabs/ResearchTabsContainer.tsx @@ -82,7 +82,7 @@ export const ResearchTabsContainer: React.FunctionComponent< type={NoteTypes.Research_File} entityId={researchFile?.id} onSuccess={onChildEntityUpdate} - NoteListView={NoteListView} + View={NoteListView} /> ), key: FileTabType.NOTES, diff --git a/source/frontend/src/features/mapSideBar/router/ManagementFileActivityRouter.tsx b/source/frontend/src/features/mapSideBar/router/ManagementFileActivityRouter.tsx index bb7134ff01..8e0c337d95 100644 --- a/source/frontend/src/features/mapSideBar/router/ManagementFileActivityRouter.tsx +++ b/source/frontend/src/features/mapSideBar/router/ManagementFileActivityRouter.tsx @@ -85,7 +85,7 @@ export const ManagementFileActivityRouter: React.FunctionComponent< customRender={({ match }) => ( { pathGenerator.showDetails( 'management', diff --git a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx index 956c0e5ab2..8ebcbce3db 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx @@ -242,7 +242,7 @@ export const PropertyFileContainer: React.FunctionComponent< type={NoteTypes.Property} entityId={composedProperties.apiWrapper.response.id} onSuccess={props.onChildSuccess} - NoteListView={NoteListView} + View={NoteListView} /> void; } const DocumentsTab: React.FunctionComponent = ({ fileId, relationshipType, + statusSolver, title = 'File Documents', onSuccess, }) => { @@ -19,6 +22,7 @@ const DocumentsTab: React.FunctionComponent = ({ title={title} parentId={fileId.toString()} relationshipType={relationshipType} + statusSolver={statusSolver} onSuccess={onSuccess} /> ); diff --git a/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx b/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx index 347b855222..f6b6efd560 100644 --- a/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx +++ b/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx @@ -1,26 +1,33 @@ import DocumentListContainer from '@/features/documents/list/DocumentListContainer'; import DocumentManagementListContainer from '@/features/documents/list/DocumentManagementListContainer'; import { ApiGen_CodeTypes_DocumentRelationType } from '@/models/api/generated/ApiGen_CodeTypes_DocumentRelationType'; +import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; + +import ManagementStatusUpdateSolver from '../../management/tabs/fileDetails/detail/ManagementStatusUpdateSolver'; interface IManagementDocumentsTabProps { - fileId: number; + managementFile: ApiGen_Concepts_ManagementFile; onSuccess?: () => void; } const ManagementDocumentsTab: React.FunctionComponent = ({ - fileId, + managementFile, onSuccess, }) => { + const statusSolver = new ManagementStatusUpdateSolver(managementFile); + return ( <> void; NoteListView: React.FunctionComponent>; } @@ -23,13 +25,20 @@ export interface INoteSummaryContainerProps { * Container that retrieved a summary of notes from a management file, related to a property. * It retrieves the notes from the management file and displays them in a list. * @param entityId The ID of the entity to retrieve notes for. + * @param statusSolver The Status solver to determine if notes are editable. * @param onSuccess Callback function to be called when the notes are successfully retrieved. * @param NoteListView The component to display the list of notes. * @returns A React component that displays a summary of notes from a management file. */ export const NoteSummaryContainer: React.FunctionComponent< React.PropsWithChildren -> = ({ associationType, entityId, onSuccess, NoteListView }: INoteSummaryContainerProps) => { +> = ({ + associationType, + entityId, + statusSolver, + onSuccess, + NoteListView, +}: INoteSummaryContainerProps) => { const { execute: getAllPropertyAssociations, loading: loadingAssociations } = usePropertyAssociations(); const { @@ -84,6 +93,8 @@ export const NoteSummaryContainer: React.FunctionComponent< const loading = loadingNotes || loadingAssociations; + const editNotesEnabled = !statusSolver || statusSolver?.canEditNotes(); + return ( ); }; diff --git a/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx b/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx index 7756fd028f..30bf653eab 100644 --- a/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx +++ b/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx @@ -35,10 +35,14 @@ export const NoteListView: React.FunctionComponent { - setCurrentNote(note); - openViewNotes(); - }, null), + createNoteActionsColumn( + true, + (note: ApiGen_Concepts_Note) => { + setCurrentNote(note); + openViewNotes(); + }, + null, + ), ]; return ( diff --git a/source/frontend/src/features/notes/list/NoteListContainer.test.tsx b/source/frontend/src/features/notes/list/NoteListContainer.test.tsx index 0be22c6b40..dae73b2c9e 100644 --- a/source/frontend/src/features/notes/list/NoteListContainer.test.tsx +++ b/source/frontend/src/features/notes/list/NoteListContainer.test.tsx @@ -15,6 +15,8 @@ import { import NoteListContainer, { INoteListContainerProps } from './NoteListContainer'; import { NoteListView } from './NoteListView'; +import ManagementStatusUpdateSolver from '@/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver'; +import { IUpdateNotesStrategy } from '../models/IUpdateNotesStrategy'; vi.mock('@/hooks/repositories/useNoteRepository'); const mockGetAllNotesApi = getMockRepositoryObj([]); @@ -35,7 +37,7 @@ describe('Note List Container', () => { type={renderOptions?.type ?? NoteTypes.Acquisition_File} entityId={renderOptions?.entityId ?? 1} onSuccess={renderOptions?.onSuccess ?? onSuccess} - NoteListView={NoteListView} + View={NoteListView} />, { ...renderOptions, diff --git a/source/frontend/src/features/notes/list/NoteListContainer.tsx b/source/frontend/src/features/notes/list/NoteListContainer.tsx index d03313675b..f211b67ab2 100644 --- a/source/frontend/src/features/notes/list/NoteListContainer.tsx +++ b/source/frontend/src/features/notes/list/NoteListContainer.tsx @@ -8,13 +8,15 @@ import { useModalManagement } from '@/hooks/useModalManagement'; import { ApiGen_Concepts_Note } from '@/models/api/generated/ApiGen_Concepts_Note'; import { exists, isValidId } from '@/utils'; +import { IUpdateNotesStrategy } from '../models/IUpdateNotesStrategy'; import { INoteListViewProps } from './NoteListView'; export interface INoteListContainerProps { type: NoteTypes; entityId: number; + View: React.FunctionComponent>; + statusSolver?: IUpdateNotesStrategy | null; onSuccess?: () => void; - NoteListView: React.FunctionComponent>; } /** @@ -22,7 +24,7 @@ export interface INoteListContainerProps { */ export const NoteListContainer: React.FunctionComponent< React.PropsWithChildren -> = ({ type, entityId, onSuccess, NoteListView }: INoteListContainerProps) => { +> = ({ type, entityId, onSuccess, View, statusSolver }: INoteListContainerProps) => { const { getAllNotes: { execute: getAllNotes, loading: loadingNotes, response: notesResponse }, deleteNote: { execute: deleteNote, loading: loadingDeleteNote }, @@ -52,8 +54,10 @@ export const NoteListContainer: React.FunctionComponent< // UI components const loading = loadingNotes || loadingDeleteNote; + const editNotesEnabled = !statusSolver || statusSolver?.canEditNotes(); + return ( - ; + canEditNotes: boolean; openAddNotes?: () => void; closeAddNotes?: () => void; deleteNote?: (type: NoteTypes, noteId: number) => Promise; @@ -52,6 +54,7 @@ export const NoteListView: React.FunctionComponent { + const { hasClaim } = useKeycloakWrapper(); + const { setModalContent, setDisplayModal } = useModalContext(); const columns = [ ...createNoteTableColumns(), createNoteActionsColumn( + canEditNotes, (note: ApiGen_Concepts_Note) => { setCurrentNote(note); openViewNotes(); @@ -100,9 +106,9 @@ export const NoteListView: React.FunctionComponent { + if (hasClaim([Claims.NOTE_ADD]) && canEditNotes) { + return ( } onAdd={openAddNotes} /> - } - title="notes" - isCollapsable - initiallyExpanded - > + ); + } else { + return 'Notes'; + } + }; + + return ( +
= { resu sort={{}} results={results ?? []} setSort={setSort} - columns={[...createNoteTableColumns(), createNoteActionsColumn(onShowDetails, onDelete)]} + columns={[ + ...createNoteTableColumns(), + createNoteActionsColumn(true, onShowDetails, onDelete), + ]} />, { ...rest, diff --git a/source/frontend/src/features/notes/list/NoteResults/columns.tsx b/source/frontend/src/features/notes/list/NoteResults/columns.tsx index 2ecb512005..44e5bfe32e 100644 --- a/source/frontend/src/features/notes/list/NoteResults/columns.tsx +++ b/source/frontend/src/features/notes/list/NoteResults/columns.tsx @@ -44,6 +44,7 @@ export function createNoteTableColumns() { } export const createNoteActionsColumn = ( + canEditNotes: boolean, onShowDetails: (note: ApiGen_Concepts_Note) => void, onDelete: (note: ApiGen_Concepts_Note) => void, ) => ({ @@ -62,6 +63,7 @@ export const createNoteActionsColumn = ( onShowDetails(cellProps.row.original)} title="View Note" /> )} {hasClaim(Claims.NOTE_DELETE) && + canEditNotes && exists(onDelete) && !cellProps.row.original.isSystemGenerated && ( { - setCurrentNote(note); - openViewNotes(); - }, null), + createNoteActionsColumn( + false, + (note: ApiGen_Concepts_Note) => { + setCurrentNote(note); + openViewNotes(); + }, + null, + ), ]; return ( diff --git a/source/frontend/src/features/notes/models/IUpdateNotesStrategy.ts b/source/frontend/src/features/notes/models/IUpdateNotesStrategy.ts new file mode 100644 index 0000000000..7312cc41c1 --- /dev/null +++ b/source/frontend/src/features/notes/models/IUpdateNotesStrategy.ts @@ -0,0 +1,3 @@ +export interface IUpdateNotesStrategy { + canEditNotes(): boolean; +} From a533aa917e9bf24c27c704d889b9496c69e23a6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:44:22 +0000 Subject: [PATCH 22/61] CI: Bump version to v5.11.0-106.9 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 6b3e7aa69c..62f0c1733d 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.8 + 5.11.0-106.9 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index 88773bd20c..a094bffcea 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.8", + "version": "5.11.0-106.9", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 0021a2e824ae1cf489ed372cd7b32a56a5189c28 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Thu, 26 Jun 2025 12:06:30 -0700 Subject: [PATCH 23/61] update ordering logic. --- .../src/features/mapSideBar/shared/FileMenuView.tsx | 8 ++++---- source/frontend/src/utils/mapPropertyUtils.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx index a8cc3e5a0d..3ed7c75c46 100644 --- a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx +++ b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx @@ -88,8 +88,9 @@ const FileMenuView: React.FunctionComponent {labelledProperties.label} - {labelledProperties.properties.map( - (fileProperty: ApiGen_Concepts_FileProperty, index: number) => { + {sortedProperties + .filter(sp => labelledProperties.properties.includes(sp)) + .map((fileProperty: ApiGen_Concepts_FileProperty, index: number) => { const propertyName = getFilePropertyName(fileProperty); return ( ); - }, - )} + })} ); }, diff --git a/source/frontend/src/utils/mapPropertyUtils.ts b/source/frontend/src/utils/mapPropertyUtils.ts index 7147132f56..106cb4597b 100644 --- a/source/frontend/src/utils/mapPropertyUtils.ts +++ b/source/frontend/src/utils/mapPropertyUtils.ts @@ -412,7 +412,7 @@ export function sortFileProperties( ): T[] | null { if (exists(fileProperties)) { return chain(fileProperties) - .orderBy([fp => fp.isActive !== false, fp => fp.displayOrder ?? Infinity, 'asc']) + .orderBy([fp => fp.displayOrder ?? Infinity], ['asc']) .value(); } return null; From 61600ec05bf59a78d87c5e177b21e7e535207d34 Mon Sep 17 00:00:00 2001 From: dfilteau Date: Fri, 27 Jun 2025 16:50:07 -0700 Subject: [PATCH 24/61] Updated Management Activity Type and Subtype Added disabled codes 'UNKNOWN' for backward compatibility. --- ...IMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql | 19 +++++++++++++++++++ ..._PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql | 19 +++++++++++++++++++ .../PSP_PIMS_LATEST/Alter Down/master.sql | 2 +- ..._PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql | 19 +++++++++++++++++++ ...MS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql | 19 +++++++++++++++++++ .../PSP_PIMS_LATEST/Alter Up/master.sql | 2 +- .../135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql | 11 ++++++++++- ...36_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql | 9 +++++++++ ...IMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql | 19 +++++++++++++++++++ ..._PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql | 19 +++++++++++++++++++ .../PSP_PIMS_S105_00/Alter Down/master.sql | 2 +- ..._PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql | 19 +++++++++++++++++++ ...MS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql | 19 +++++++++++++++++++ .../PSP_PIMS_S105_00/Alter Up/master.sql | 2 +- .../135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql | 11 ++++++++++- ...36_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql | 9 +++++++++ 16 files changed, 194 insertions(+), 6 deletions(-) diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql index fe8f1e52e2..5bf40964a3 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -33,6 +34,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql index dfdf089716..2a270f8a22 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -35,6 +36,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql index 546927546d..ee2a0bcab0 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql @@ -1,4 +1,4 @@ --- File generated on 06/20/2025 04:32:41 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql index 157c532464..eb0d7cacb2 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -36,6 +37,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql index 5c496d0dbe..a01aae01fd 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -38,6 +39,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql index 548585d816..9cd87df35d 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql @@ -1,4 +1,4 @@ --- File generated on 06/20/2025 04:32:41 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql index fb3015f7af..2343661867 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql @@ -6,6 +6,7 @@ Author Date Comment Doug Filteau 2023-Sep-11 Initial version. Doug Filteau 2024-Feb-26 Added UTILITYBILL and TAXESLEVIES. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_TYPE @@ -28,7 +29,15 @@ VALUES (N'TRAILMTC', N'Trail Maintenance'), (N'CORRESPOND', N'Correspondence'); GO - + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'Unknown', 1); +GO + -- -------------------------------------------------------------- -- Update the display order. -- -------------------------------------------------------------- diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql index e93592afc2..d87b13a1ca 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql @@ -9,6 +9,7 @@ Doug Filteau 2024-Jul-03 Add/enable PROPADMIN and remove the leading space from WATERANDSEWER. Doug Filteau 2024-Jul-11 Added BYLAWINFRAC. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity subtype. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE @@ -96,6 +97,14 @@ VALUES (N'CORRESPOND', N'CORRESPOND', N'Correspondence'); GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql index fe8f1e52e2..5bf40964a3 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -33,6 +34,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql index dfdf089716..2a270f8a22 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -35,6 +36,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql index 546927546d..ee2a0bcab0 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql @@ -1,4 +1,4 @@ --- File generated on 06/20/2025 04:32:41 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql index 157c532464..eb0d7cacb2 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -36,6 +37,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql index 5c496d0dbe..a01aae01fd 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -38,6 +39,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql index 548585d816..9cd87df35d 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql @@ -1,4 +1,4 @@ --- File generated on 06/20/2025 04:32:41 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql index fb3015f7af..2343661867 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql @@ -6,6 +6,7 @@ Author Date Comment Doug Filteau 2023-Sep-11 Initial version. Doug Filteau 2024-Feb-26 Added UTILITYBILL and TAXESLEVIES. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_TYPE @@ -28,7 +29,15 @@ VALUES (N'TRAILMTC', N'Trail Maintenance'), (N'CORRESPOND', N'Correspondence'); GO - + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'Unknown', 1); +GO + -- -------------------------------------------------------------- -- Update the display order. -- -------------------------------------------------------------- diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql index e93592afc2..d87b13a1ca 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql @@ -9,6 +9,7 @@ Doug Filteau 2024-Jul-03 Add/enable PROPADMIN and remove the leading space from WATERANDSEWER. Doug Filteau 2024-Jul-11 Added BYLAWINFRAC. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity subtype. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE @@ -96,6 +97,14 @@ VALUES (N'CORRESPOND', N'CORRESPOND', N'Correspondence'); GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. From 969a5359f87691349c42e16ac7c94b0fb15eb398 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 02:41:07 +0000 Subject: [PATCH 25/61] CI: Bump version to v5.11.0-106.10 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 62f0c1733d..a4afa5aa10 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.9 + 5.11.0-106.10 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index a094bffcea..4d7a6e5930 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.9", + "version": "5.11.0-106.10", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 5ecd1bf143f78e66a552fc5a52fbf81174852ae0 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Mon, 30 Jun 2025 12:25:25 -0700 Subject: [PATCH 26/61] Added marker to location related searches (#4871) Co-authored-by: Alejandro Sanchez --- .../common/mapFSM/MapStateMachineContext.tsx | 22 +++ .../mapFSM/machineDefinition/mapMachine.ts | 15 ++ .../common/mapFSM/machineDefinition/types.ts | 1 + .../src/components/maps/MapLeafletView.tsx | 7 +- .../src/components/maps/MapSearch.tsx | 14 +- .../maps/leaflet/Layers/InventoryLayer.tsx | 136 ------------------ .../maps/leaflet/Layers/MarkerLayer.tsx | 79 ++++++++++ source/frontend/src/mocks/mapFSM.mock.ts | 3 + 8 files changed, 135 insertions(+), 142 deletions(-) delete mode 100644 source/frontend/src/components/maps/leaflet/Layers/InventoryLayer.tsx create mode 100644 source/frontend/src/components/maps/leaflet/Layers/MarkerLayer.tsx diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index 403d978a51..f8bc731210 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -38,6 +38,7 @@ export interface IMapStateMachineContext { requestedFlyTo: RequestedFlyTo; requestedCenterTo: RequestedCenterTo; mapMarkerSelected: MarkerSelected | null; + mapMarkedLocation: LatLngLiteral | null; mapLocationSelected: LatLngLiteral | null; mapLocationFeatureDataset: LocationFeatureDataset | null; selectedFeatureDataset: SelectedFeatureDataset | null; @@ -78,6 +79,8 @@ export interface IMapStateMachineContext { mapClick: (latlng: LatLngLiteral) => void; mapMarkerClick: (featureSelected: MarkerSelected) => void; + mapMarkLocation: (laLng: LatLngLiteral) => void; + mapClearLocationMark: () => void; setMapSearchCriteria: (searchCriteria: IPropertyFilter) => void; refreshMapProperties: () => void; @@ -228,6 +231,22 @@ export const MapStateMachineProvider: React.FC> }); }, [serviceSend]); + const mapMarkLocation = useCallback( + (latlng: LatLngLiteral) => { + serviceSend({ + type: 'MAP_MARK_LOCATION', + latlng, + }); + }, + [serviceSend], + ); + + const mapClearLocationMark = useCallback(() => { + serviceSend({ + type: 'MAP_CLEAR_MARK_LOCATION', + }); + }, [serviceSend]); + const mapClick = useCallback( (latlng: LatLngLiteral) => { serviceSend({ @@ -468,6 +487,7 @@ export const MapStateMachineProvider: React.FC> requestedCenterTo: state.context.requestedCenterTo, mapMarkerSelected: state.context.mapFeatureSelected, mapLocationSelected: state.context.mapLocationSelected, + mapMarkedLocation: state.context.mapMarkedLocation, selectedFeatureDataset: state.context.selectedFeatureDataset, mapLocationFeatureDataset: state.context.mapLocationFeatureDataset, repositioningFeatureDataset: state.context.repositioningFeatureDataset, @@ -501,6 +521,8 @@ export const MapStateMachineProvider: React.FC> processFitBounds, openSidebar, closeSidebar, + mapMarkLocation, + mapClearLocationMark, requestFlyToLocation, requestCenterToLocation, requestFlyToBounds, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index 4acba39657..ed3fe5fe65 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -265,6 +265,20 @@ const selectedFeatureLoaderStates = { ], target: 'loading', }, + MAP_MARK_LOCATION: { + actions: [ + assign({ + mapMarkedLocation: (_, event: any) => event.latlng, + }), + ], + }, + MAP_CLEAR_MARK_LOCATION: { + actions: [ + assign({ + mapMarkedLocation: () => null, + }), + ], + }, CLOSE_POPUP: { actions: [ assign({ @@ -487,6 +501,7 @@ export const mapMachine = createMachine({ mapLocationSelected: null, mapFeatureSelected: null, mapLocationFeatureDataset: null, + mapMarkedLocation: null, selectedFeatureDataset: null, repositioningFeatureDataset: null, repositioningPropertyIndex: null, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index 61eb656dc5..f2bc912892 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -32,6 +32,7 @@ export type MachineContext = { mapFeatureSelected: MarkerSelected | null; mapLocationSelected: LatLngLiteral | null; mapLocationFeatureDataset: LocationFeatureDataset | null; + mapMarkedLocation: LatLngLiteral | null; selectedFeatureDataset: SelectedFeatureDataset | null; repositioningFeatureDataset: SelectedFeatureDataset | null; repositioningPropertyIndex: number | null; diff --git a/source/frontend/src/components/maps/MapLeafletView.tsx b/source/frontend/src/components/maps/MapLeafletView.tsx index 10b79d5540..d5c9ab9cc4 100644 --- a/source/frontend/src/components/maps/MapLeafletView.tsx +++ b/source/frontend/src/components/maps/MapLeafletView.tsx @@ -33,8 +33,8 @@ import { LegendControl } from './leaflet/Control/Legend/LegendControl'; import { ZoomOutButton } from './leaflet/Control/ZoomOut/ZoomOutButton'; import { LocationPopupContainer } from './leaflet/LayerPopup/LocationPopupContainer'; import { FilePropertiesLayer } from './leaflet/Layers/FilePropertiesLayer'; -import { InventoryLayer } from './leaflet/Layers/InventoryLayer'; import { LeafletLayerListener } from './leaflet/Layers/LeafletLayerListener'; +import { MarkerLayer } from './leaflet/Layers/MarkerLayer'; import { MapEvents } from './leaflet/MapEvents/MapEvents'; import * as Styled from './leaflet/styles'; import { EsriVectorTileLayer } from './leaflet/VectorTileLayer/EsriVectorTileLayer'; @@ -79,6 +79,7 @@ const MapLeafletView: React.FC> = ( return; } timer.current = setTimeout(() => { + mapMachine.mapClearLocationMark(); mapMachine.mapClick(latlng); timer.current = null; }, doubleClickInterval ?? 250); @@ -279,11 +280,11 @@ const MapLeafletView: React.FC> = ( active={mapMachine.isFiltering} /> - + /> {/* Client-side "layer" to highlight file property boundaries (when in the context of a file) */} diff --git a/source/frontend/src/components/maps/MapSearch.tsx b/source/frontend/src/components/maps/MapSearch.tsx index 6208b6a9b0..5e92e47a05 100644 --- a/source/frontend/src/components/maps/MapSearch.tsx +++ b/source/frontend/src/components/maps/MapSearch.tsx @@ -19,8 +19,14 @@ export type MapSearchProps = object; * @param param0 */ const MapSearch: React.FC> = () => { - const { mapSearchCriteria, setMapSearchCriteria, mapClick, requestCenterToLocation } = - useMapStateMachine(); + const { + mapSearchCriteria, + setMapSearchCriteria, + mapClick, + requestCenterToLocation, + mapMarkLocation, + mapClearLocationMark, + } = useMapStateMachine(); const [propertySearchFilter, setPropertySearchFilter] = useState(null); @@ -46,10 +52,12 @@ const MapSearch: React.FC> = () => { latLng = filter.coordinates?.toLatLng(); } if (latLng) { - mapClick(latLng); + mapMarkLocation(latLng); requestCenterToLocation(latLng); + mapClick(latLng); } } else { + mapClearLocationMark(); setPropertySearchFilter(filter); } }; diff --git a/source/frontend/src/components/maps/leaflet/Layers/InventoryLayer.tsx b/source/frontend/src/components/maps/leaflet/Layers/InventoryLayer.tsx deleted file mode 100644 index 2b7a65a0a5..0000000000 --- a/source/frontend/src/components/maps/leaflet/Layers/InventoryLayer.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { BBox } from 'geojson'; -import { LatLngBounds } from 'leaflet'; -import React, { useMemo } from 'react'; -import { useMap } from 'react-leaflet'; -import { tilesInBbox } from 'tiles-in-bbox'; - -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; - -import { PointFeature } from '../../types'; -import PointClusterer from './PointClusterer'; - -export type InventoryLayerProps = { - /** Latitude and Longitude boundary of the layer. */ - bounds: LatLngBounds; - /** Zoom level of the map. */ - zoom: number; - /** Minimum zoom level allowed. */ - minZoom?: number; - /** Maximum zoom level allowed. */ - maxZoom?: number; -}; - -/** - * Get a new instance of a BBox from the specified 'bounds'. - * @param bounds The latitude longitude boundary. - */ -const getBbox = (bounds: LatLngBounds): BBox => { - return [ - bounds.getSouthWest().lng, - bounds.getSouthWest().lat, - bounds.getNorthEast().lng, - bounds.getNorthEast().lat, - ]; -}; - -interface ITilePoint { - // x axis of the tile - x: number; - // y axis of the tile - y: number; - // zoom state of the tile - z: number; -} - -interface ITile { - // Tile point {x, y, z} - point: ITilePoint; - // unique id of the file - key: string; - // bbox of the tile - bbox: string; - // tile data status - processed?: boolean; - // tile data, a list of properties in the tile - datum?: PointFeature[]; - // tile bounds - latlngBounds: LatLngBounds; -} - -/** - * Generate tiles for current bounds and zoom - * @param bounds - * @param zoom - */ -export const getTiles = (bounds: LatLngBounds, zoom: number): ITile[] => { - const bbox = { - bottom: bounds.getSouth(), - left: bounds.getWest(), - top: bounds.getNorth(), - right: bounds.getEast(), - }; - - const tiles = tilesInBbox(bbox, zoom); - - // convert tile x axis to longitude - const tileToLong = (x: number, z: number) => { - return (x / Math.pow(2, z)) * 360 - 180; - }; - - // convert tile y axis to longitude - const tileToLat = (y: number, z: number) => { - const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z); - - return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); - }; - - return tiles.map(({ x, y, z }) => { - const SW_long = tileToLong(x, z); - - const SW_lat = tileToLat(y + 1, z); - - const NE_long = tileToLong(x + 1, z); - - const NE_lat = tileToLat(y, z); - - return { - key: `${x}:${y}:${z}`, - bbox: SW_long + ',' + SW_lat + ',' + NE_long + ',' + NE_lat + ',EPSG:4326', - point: { x, y, z }, - datum: [], - latlngBounds: new LatLngBounds({ lat: SW_lat, lng: SW_long }, { lat: NE_lat, lng: NE_long }), - }; - }); -}; - -/** - * Displays the search results onto a layer with clustering. - * This component makes a request to the PIMS API properties search WFS endpoint. - */ -export const InventoryLayer: React.FC> = ({ - bounds, - zoom, - minZoom, - maxZoom, -}) => { - const mapInstance = useMap(); - const mapMachine = useMapStateMachine(); - - if (!mapInstance) { - throw new Error(' must be used under a leaflet component'); - } - - const bbox = useMemo(() => getBbox(bounds), [bounds]); - - return ( - - ); -}; diff --git a/source/frontend/src/components/maps/leaflet/Layers/MarkerLayer.tsx b/source/frontend/src/components/maps/leaflet/Layers/MarkerLayer.tsx new file mode 100644 index 0000000000..971fbccbd3 --- /dev/null +++ b/source/frontend/src/components/maps/leaflet/Layers/MarkerLayer.tsx @@ -0,0 +1,79 @@ +import { BBox } from 'geojson'; +import { LatLngBounds } from 'leaflet'; +import React, { useMemo } from 'react'; +import { FeatureGroup, Marker, useMap } from 'react-leaflet'; + +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import { exists } from '@/utils'; + +import PointClusterer from './PointClusterer'; +import { getNotOwnerMarkerIcon } from './util'; + +export type InventoryLayerProps = { + /** Latitude and Longitude boundary of the layer. */ + bounds: LatLngBounds; + /** Zoom level of the map. */ + zoom: number; + /** Minimum zoom level allowed. */ + minZoom?: number; + /** Maximum zoom level allowed. */ + maxZoom?: number; +}; + +/** + * Get a new instance of a BBox from the specified 'bounds'. + * @param bounds The latitude longitude boundary. + */ +const getBbox = (bounds: LatLngBounds): BBox => { + return [ + bounds.getSouthWest().lng, + bounds.getSouthWest().lat, + bounds.getNorthEast().lng, + bounds.getNorthEast().lat, + ]; +}; + +/** + * Displays the search results onto a layer with clustering. + * This component makes a request to the PIMS API properties search WFS endpoint. + */ +export const MarkerLayer: React.FC> = ({ + bounds, + zoom, + minZoom, + maxZoom, +}) => { + const mapInstance = useMap(); + const mapMachine = useMapStateMachine(); + + if (!mapInstance) { + throw new Error(' must be used under a leaflet component'); + } + + const bbox = useMemo(() => getBbox(bounds), [bounds]); + + const markedLocation = useMemo( + () => mapMachine.mapMarkedLocation, + [mapMachine.mapMarkedLocation], + ); + + return ( + <> + + + {exists(markedLocation) && ( + + + + )} + + ); +}; diff --git a/source/frontend/src/mocks/mapFSM.mock.ts b/source/frontend/src/mocks/mapFSM.mock.ts index 9550fa3f50..02ecac6473 100644 --- a/source/frontend/src/mocks/mapFSM.mock.ts +++ b/source/frontend/src/mocks/mapFSM.mock.ts @@ -91,4 +91,7 @@ export const mapMachineBaseMock: IMapStateMachineContext = { setMapLayersToRefresh: vi.fn(), setAdvancedSearchCriteria: vi.fn(), setCurrentMapBounds: vi.fn(), + mapMarkedLocation: undefined, + mapMarkLocation: vi.fn(), + mapClearLocationMark: vi.fn(), }; From 3b5b44bbac83d126e096d663fbb896c665fbbe56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:25:52 +0000 Subject: [PATCH 27/61] CI: Bump version to v5.11.0-106.11 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index a4afa5aa10..96120a0e59 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.10 + 5.11.0-106.11 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index 4d7a6e5930..2c1e83a83c 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.10", + "version": "5.11.0-106.11", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 4f796854df495fa8dcc17f411f06eb09bc4561e8 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Mon, 30 Jun 2025 14:44:27 -0700 Subject: [PATCH 28/61] Regenerated snaps --- .../__snapshots__/LayersMenu.test.tsx.snap | 20 +++++++++++++++++++ .../__snapshots__/MapContainer.test.tsx.snap | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap b/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap index bff0d724c2..64b45b594a 100644 --- a/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap +++ b/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap @@ -360,6 +360,26 @@ exports[`LayersMenu View > renders as expected 1`] = `
+
+
+ + +
+
diff --git a/source/frontend/src/features/properties/map/__snapshots__/MapContainer.test.tsx.snap b/source/frontend/src/features/properties/map/__snapshots__/MapContainer.test.tsx.snap index 360f53adfa..69c16e983c 100644 --- a/source/frontend/src/features/properties/map/__snapshots__/MapContainer.test.tsx.snap +++ b/source/frontend/src/features/properties/map/__snapshots__/MapContainer.test.tsx.snap @@ -1245,6 +1245,26 @@ exports[`MapContainer > Renders the map 1`] = `
+
+
+ + +
+
From c5c7472409827cd19f03cbad29dd896cf0df45a6 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Mon, 30 Jun 2025 16:09:12 -0700 Subject: [PATCH 29/61] psp-10487 inability to upload larger files. --- .../frontend/src/hooks/util/useApiRequestWrapper.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/source/frontend/src/hooks/util/useApiRequestWrapper.ts b/source/frontend/src/hooks/util/useApiRequestWrapper.ts index 64f5354cfd..1f3d8ed5ef 100644 --- a/source/frontend/src/hooks/util/useApiRequestWrapper.ts +++ b/source/frontend/src/hooks/util/useApiRequestWrapper.ts @@ -110,7 +110,7 @@ export const useApiRequestWrapper = < ); }); if (returnApiError) { - return axiosError.response.data; + return axiosError.response?.data ?? getApiError(axiosError); } } else if (!throwError) { // If no error handling is provided, fall back to the default PIMS error handler (bomb icon). @@ -122,7 +122,7 @@ export const useApiRequestWrapper = < }), ); if (returnApiError) { - return axiosError.response.data; + return axiosError.response.data ?? getApiError(axiosError); } } @@ -167,3 +167,12 @@ export const useApiRequestWrapper = < requestEndOn, }; }; + +const getApiError = (error: AxiosError): IApiError => ({ + details: error.message, + error: + 'An unknown error occurred, contact your system administrator if you continue to see this error.', + errorCode: error.code, + stackTrace: JSON.stringify(error), + type: 'Unknown', +}); From 712cf8462f5fd5c91ac62db3fbf55fd75d9ce607 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Mon, 30 Jun 2025 16:27:21 -0700 Subject: [PATCH 30/61] handle null/undefined error. --- source/frontend/src/hooks/util/useApiRequestWrapper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/frontend/src/hooks/util/useApiRequestWrapper.ts b/source/frontend/src/hooks/util/useApiRequestWrapper.ts index 1f3d8ed5ef..282c098f13 100644 --- a/source/frontend/src/hooks/util/useApiRequestWrapper.ts +++ b/source/frontend/src/hooks/util/useApiRequestWrapper.ts @@ -169,10 +169,10 @@ export const useApiRequestWrapper = < }; const getApiError = (error: AxiosError): IApiError => ({ - details: error.message, + details: error?.message, error: 'An unknown error occurred, contact your system administrator if you continue to see this error.', - errorCode: error.code, + errorCode: error?.code, stackTrace: JSON.stringify(error), type: 'Unknown', }); From 1b1a7fe10579816112e19c65caaf8ff166fa18d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 05:09:45 +0000 Subject: [PATCH 31/61] CI: Bump version to v5.11.0-106.12 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 96120a0e59..82768f73fd 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.11 + 5.11.0-106.12 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index 2c1e83a83c..c7ffefcfdf 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.11", + "version": "5.11.0-106.12", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From cfb836c9de9372f3defd0c85f6ceefa5883248a1 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Wed, 2 Jul 2025 11:26:20 -0700 Subject: [PATCH 32/61] Fixed properties boundaries not being taken into account --- .../src/features/mapSideBar/shared/FileMenuView.tsx | 6 ++++-- .../shared/update/properties/UpdateProperties.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx index 9bf194742b..cce2649339 100644 --- a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx +++ b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx @@ -58,9 +58,11 @@ const FileMenuView: React.FunctionComponent locationFromFileProperty(fileProp)) .map(geom => getLatLng(geom)) .filter(exists); - const latLngBoudns = latLngBounds(locations); + const bounds = latLngBounds(locations); - mapMachine.requestFlyToBounds(latLngBoudns); + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } } }; diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx index bba118d5b3..ab1d983093 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx @@ -56,7 +56,7 @@ export const UpdateProperties: React.FunctionComponent = const mapMachine = useMapStateMachine(); const fitBoundaries = () => { - const fileProperties = formFile.properties; + const fileProperties = formikRef?.current?.values?.properties; if (exists(fileProperties)) { const locations = fileProperties.map( @@ -64,7 +64,9 @@ export const UpdateProperties: React.FunctionComponent = ); const bounds = geoJSON(locations).getBounds(); - mapMachine.requestFlyToBounds(bounds); + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } } }; From 1f4735476a63eed2e1ba866c78c07f0fb3489009 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:42:19 +0000 Subject: [PATCH 33/61] CI: Bump version to v5.11.0-106.13 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 82768f73fd..52dfb246e3 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.12 + 5.11.0-106.13 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index c7ffefcfdf..10c5b07ad3 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.12", + "version": "5.11.0-106.13", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 17a762dad1ace7efd0e533739496fab3cab4ad89 Mon Sep 17 00:00:00 2001 From: Smith Date: Wed, 2 Jul 2025 16:28:30 -0700 Subject: [PATCH 34/61] psp-10487 increase stability of mayan document upload - refactor logic. --- .../Documents/DocumentQueueController.cs | 4 +- source/backend/api/Pims.Api.csproj | 1 + .../api/Services/DocumentQueueService.cs | 269 ++++++++++-------- .../api/Services/DocumentSyncService.cs | 2 +- .../api/Services/IDocumentQueueService.cs | 2 +- source/backend/api/Startup.cs | 3 + source/backend/dal/Pims.Dal.csproj | 1 + .../Repositories/DocumentQueueRepository.cs | 60 ++-- .../Interfaces/IDocumentQueueRepository.cs | 3 +- .../Services/DocumentQueueService.cs | 8 +- .../api/Services/DocumentQueueServiceTest.cs | 31 +- .../DocumentQueueRepositoryTest.cs | 5 +- .../Services/DocumentQueueServiceTests.cs | 41 ++- 13 files changed, 249 insertions(+), 181 deletions(-) diff --git a/source/backend/api/Areas/Documents/DocumentQueueController.cs b/source/backend/api/Areas/Documents/DocumentQueueController.cs index 3a3fd79efa..4272c788cb 100644 --- a/source/backend/api/Areas/Documents/DocumentQueueController.cs +++ b/source/backend/api/Areas/Documents/DocumentQueueController.cs @@ -62,7 +62,7 @@ public DocumentQueueController(IDocumentQueueService documentQueueService, IMapp [ProducesResponseType(typeof(List), 200)] [SwaggerOperation(Tags = new[] { "document-types" })] [TypeFilter(typeof(NullJsonResultFilter))] - public IActionResult Update(long documentQueueId, [FromBody] DocumentQueueModel documentQueue) + public async Task Update(long documentQueueId, [FromBody] DocumentQueueModel documentQueue) { _logger.LogInformation( "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", @@ -77,7 +77,7 @@ public IActionResult Update(long documentQueueId, [FromBody] DocumentQueueModel throw new BadRequestException("Invalid document queue id."); } - var queuedDocuments = _documentQueueService.Update(_mapper.Map(documentQueue)); + var queuedDocuments = await _documentQueueService.Update(_mapper.Map(documentQueue)); var updatedDocumentQueue = _mapper.Map(queuedDocuments); return new JsonResult(updatedDocumentQueue); } diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 96120a0e59..627820514b 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -17,6 +17,7 @@ + diff --git a/source/backend/api/Services/DocumentQueueService.cs b/source/backend/api/Services/DocumentQueueService.cs index 9c6c744bb1..6039db6876 100644 --- a/source/backend/api/Services/DocumentQueueService.cs +++ b/source/backend/api/Services/DocumentQueueService.cs @@ -10,9 +10,7 @@ using Microsoft.Extensions.Options; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Concepts.Document; -using Pims.Api.Models.Mayan.Document; using Pims.Api.Models.Requests.Document.Upload; -using Pims.Api.Models.Requests.Http; using Pims.Core.Api.Exceptions; using Pims.Core.Api.Services; using Pims.Core.Extensions; @@ -69,7 +67,6 @@ public PimsDocumentQueue GetById(long documentQueueId) { throw new KeyNotFoundException($"Unable to find queued document by id: ${documentQueueId}"); } - return documentQueue; } @@ -86,26 +83,9 @@ public IEnumerable SearchDocumentQueue(DocumentQueueFilter fi var queuedDocuments = _documentQueueRepository.GetAllByFilter(filter); - if (filter.MaxFileSize != null) - { - List documentsBelowMaxFileSize = new List(); - long totalFileSize = 0; - queuedDocuments.ForEach(currentDocument => - { - if (currentDocument.DocumentSize + totalFileSize <= filter.MaxFileSize) - { - totalFileSize += currentDocument.DocumentSize; - documentsBelowMaxFileSize.Add(currentDocument); - } - }); - if (documentsBelowMaxFileSize.Count == 0 && queuedDocuments.Any()) - { - documentsBelowMaxFileSize.Add(queuedDocuments.FirstOrDefault()); - } - this.Logger.LogDebug("returning {length} documents below file size", documentsBelowMaxFileSize.Count); - return documentsBelowMaxFileSize; - } - return queuedDocuments; + return filter.MaxFileSize != null + ? FilterDocumentsByMaxFileSize(queuedDocuments, filter.MaxFileSize.Value) + : queuedDocuments; } /// @@ -114,15 +94,15 @@ public IEnumerable SearchDocumentQueue(DocumentQueueFilter fi /// The document queue object to update. /// The updated document queue object. /// Thrown when the user is not authorized to perform this operation. - public PimsDocumentQueue Update(PimsDocumentQueue documentQueue) + public async Task Update(PimsDocumentQueue documentQueue) { this.Logger.LogInformation("Updating queued document {documentQueueId}", documentQueue.DocumentQueueId); this.Logger.LogDebug("Incoming queued document {document}", documentQueue.Serialize()); this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); - _documentQueueRepository.Update(documentQueue); - _documentQueueRepository.CommitTransaction(); + await _documentQueueRepository.Update(documentQueue); + return documentQueue; } @@ -139,131 +119,183 @@ public async Task PollForDocument(PimsDocumentQueue documentQ this.Logger.LogDebug("Polling queued document {document}", documentQueue.Serialize()); this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); - if (documentQueue.DocumentId == null) + + ValidateDocumentQueueForPolling(documentQueue); + + var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); + ValidateDatabaseDocumentQueueForPolling(databaseDocumentQueue, documentQueue.DocumentQueueId); + + var relatedDocument = _documentRepository.TryGet(documentQueue.DocumentId.Value); + await ValidateRelatedDocumentForPolling(relatedDocument, databaseDocumentQueue); + + return await PollDocumentStatusAsync(databaseDocumentQueue, relatedDocument); + } + + public async Task Upload(PimsDocumentQueue documentQueue) + { + this.Logger.LogInformation("Uploading queued document {documentQueueId}", documentQueue.DocumentQueueId); + this.Logger.LogDebug("Uploading queued document {document}", documentQueue.Serialize()); + + this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); + + var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); + ValidateDatabaseDocumentQueueForUpload(databaseDocumentQueue, documentQueue.DocumentQueueId); + + databaseDocumentQueue.DocProcessStartDt = DateTime.UtcNow; + + if (!ValidateQueuedDocument(databaseDocumentQueue, documentQueue)) { - this.Logger.LogError("polled queued document does not have a document Id {documentQueueId}", documentQueue.DocumentQueueId); - throw new InvalidDataException("DocumentId is required to poll for a document."); + databaseDocumentQueue.MayanError = "Document is invalid."; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + return databaseDocumentQueue; } - var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); - if (databaseDocumentQueue == null) + databaseDocumentQueue = HandleRetryForErroredDocument(databaseDocumentQueue); + + var relatedDocument = _documentRepository.TryGetDocumentRelationships(databaseDocumentQueue.DocumentId.Value); + await ValidateRelatedDocumentForUpload(relatedDocument, databaseDocumentQueue); + + return await UploadDocumentAsync(databaseDocumentQueue, relatedDocument); + } + + private List FilterDocumentsByMaxFileSize(IEnumerable queuedDocuments, long maxFileSize) + { + List documentsBelowMaxFileSize = new(); + long totalFileSize = 0; + + foreach (var currentDocument in queuedDocuments) { - this.Logger.LogError("Unable to find document queue with {id}", documentQueue.DocumentQueueId); - throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueue.DocumentQueueId}"); + if (currentDocument.DocumentSize + totalFileSize <= maxFileSize) + { + totalFileSize += currentDocument.DocumentSize; + documentsBelowMaxFileSize.Add(currentDocument); + } } - else if (databaseDocumentQueue.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.PROCESSING.ToString()) + + if (documentsBelowMaxFileSize.Count == 0 && queuedDocuments.Any()) { - this.Logger.LogError("Document Queue {documentQueueId} is not in valid state, aborting poll.", documentQueue.DocumentQueueId); - return databaseDocumentQueue; + documentsBelowMaxFileSize.Add(queuedDocuments.First()); } - else if (databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PENDING.ToString() || databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.SUCCESS.ToString()) + + this.Logger.LogDebug("Returning {length} documents below file size", documentsBelowMaxFileSize.Count); + return documentsBelowMaxFileSize; + } + + private void ValidateDocumentQueueForPolling(PimsDocumentQueue documentQueue) + { + if (documentQueue.DocumentId == null) { - this.Logger.LogError("Document Queue {documentQueueId} is not in valid state, aborting poll.", documentQueue.DocumentQueueId); - return databaseDocumentQueue; + this.Logger.LogError("Polled queued document does not have a document Id {documentQueueId}", documentQueue.DocumentQueueId); + throw new InvalidDataException("DocumentId is required to poll for a document."); } + } - var relatedDocument = _documentRepository.TryGet(documentQueue.DocumentId.Value); + private void ValidateDatabaseDocumentQueueForPolling(PimsDocumentQueue databaseDocumentQueue, long documentQueueId) + { + if (databaseDocumentQueue == null) + { + this.Logger.LogError("Unable to find document queue with {id}", documentQueueId); + throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueueId}"); + } + if (databaseDocumentQueue.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.PROCESSING.ToString()) + { + this.Logger.LogError("Document Queue {documentQueueId} is not in valid state, aborting poll.", documentQueueId); + throw new InvalidOperationException("Document queue is not in a valid state for polling."); + } + } + + private async Task ValidateRelatedDocumentForPolling(PimsDocument relatedDocument, PimsDocumentQueue databaseDocumentQueue) + { if (relatedDocument?.MayanId == null || relatedDocument?.MayanId < 0) { - this.Logger.LogError("Queued Document {documentQueueId} has no mayan id and is invalid.", documentQueue.DocumentQueueId); + this.Logger.LogError("Queued Document {documentQueueId} has no Mayan ID and is invalid.", databaseDocumentQueue.DocumentQueueId); databaseDocumentQueue.MayanError = "Document does not have a valid MayanId."; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); - return databaseDocumentQueue; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + throw new InvalidDataException("Document does not have a valid MayanId."); } + } - ExternalResponse documentDetailsResponse = await _documentService.GetStorageDocumentDetail(relatedDocument.MayanId.Value); + private async Task PollDocumentStatusAsync(PimsDocumentQueue databaseDocumentQueue, PimsDocument relatedDocument) + { + var documentDetailsResponse = await _documentService.GetStorageDocumentDetail(relatedDocument.MayanId.Value); if (documentDetailsResponse.Status != ExternalResponseStatus.Success || documentDetailsResponse?.Payload == null) { - this.Logger.LogError("Polling for queued document {documentQueueId} failed with status {documentDetailsResponseStatus}", documentQueue.DocumentQueueId, documentDetailsResponse.Status); + this.Logger.LogError("Polling for queued document {documentQueueId} failed with status {documentDetailsResponseStatus}", databaseDocumentQueue.DocumentQueueId, documentDetailsResponse.Status); databaseDocumentQueue.MayanError = "Document Polling failed."; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); return databaseDocumentQueue; } if (documentDetailsResponse.Payload.FileLatest?.Id == null) { - this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file still processing", documentQueue.DocumentQueueId); + this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file still processing", databaseDocumentQueue.DocumentQueueId); } else { - this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file uploaded successfully", documentQueue.DocumentQueueId); - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file uploaded successfully", databaseDocumentQueue.DocumentQueueId); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); } return databaseDocumentQueue; } - /// - /// Uploads the specified document queue. - /// - /// The document queue object containing the document to upload. - /// A task that represents the asynchronous operation. The task result contains the updated document queue object, or null if the upload failed. - /// Thrown when the user is not authorized to perform this operation. - /// Thrown when the document queue does not have a valid document ID or related document. - public async Task Upload(PimsDocumentQueue documentQueue) + private void ValidateDatabaseDocumentQueueForUpload(PimsDocumentQueue databaseDocumentQueue, long documentQueueId) { - this.Logger.LogInformation("Uploading queued document {documentQueueId}", documentQueue.DocumentQueueId); - this.Logger.LogDebug("Uploading queued document {document}", documentQueue.Serialize()); - - this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); - - var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); if (databaseDocumentQueue == null) { - this.Logger.LogError("Unable to find document queue with {id}", documentQueue.DocumentQueueId); - throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueue.DocumentQueueId}"); - } - databaseDocumentQueue.DocProcessStartDt = DateTime.UtcNow; - - bool isValid = ValidateQueuedDocument(databaseDocumentQueue, documentQueue); - if (!isValid) - { - this.Logger.LogDebug("Document Queue {documentQueueId}, invalid, aborting upload.", documentQueue.DocumentQueueId); - databaseDocumentQueue.MayanError = "Document is invalid."; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); - return databaseDocumentQueue; + this.Logger.LogError("Unable to find document queue with {id}", documentQueueId); + throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueueId}"); } + } - // if the document queued for upload is already in an error state, update the retries. - if (databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PIMS_ERROR.ToString() || databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.MAYAN_ERROR.ToString()) + private PimsDocumentQueue HandleRetryForErroredDocument(PimsDocumentQueue databaseDocumentQueue) + { + if (databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PIMS_ERROR.ToString() || + databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.MAYAN_ERROR.ToString()) { - this.Logger.LogDebug("Document Queue {documentQueueId}, previously errored, retrying", documentQueue.DocumentQueueId); + this.Logger.LogDebug("Document Queue {documentQueueId}, previously errored, retrying", databaseDocumentQueue.DocumentQueueId); databaseDocumentQueue.DocProcessRetries = ++databaseDocumentQueue.DocProcessRetries ?? 1; databaseDocumentQueue.DocProcessEndDt = null; } + return databaseDocumentQueue; + } - PimsDocument relatedDocument = null; - relatedDocument = _documentRepository.TryGetDocumentRelationships(databaseDocumentQueue.DocumentId.Value); + private async Task ValidateRelatedDocumentForUpload(PimsDocument relatedDocument, PimsDocumentQueue databaseDocumentQueue) + { if (relatedDocument?.DocumentTypeId == null) { databaseDocumentQueue.MayanError = "Document does not have a valid DocumentType."; this.Logger.LogError("Queued document {documentQueueId} does not have a related PIMS_DOCUMENT {documentId} with valid DocumentType, aborting.", databaseDocumentQueue.DocumentQueueId, relatedDocument?.DocumentId); - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); - return databaseDocumentQueue; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + throw new InvalidDataException("Document does not have a valid DocumentType."); } + } + private async Task UploadDocumentAsync(PimsDocumentQueue databaseDocumentQueue, PimsDocument relatedDocument) + { try { - PimsDocumentTyp documentTyp = _documentTypeRepository.GetById(relatedDocument.DocumentTypeId); // throws KeyNotFoundException if not found. + var documentType = _documentTypeRepository.GetById(relatedDocument.DocumentTypeId); - IFormFile file = null; - using MemoryStream memStream = new(databaseDocumentQueue.Document); - file = new FormFile(memStream, 0, databaseDocumentQueue.Document.Length, relatedDocument.FileName, relatedDocument.FileName); + using var memStream = new MemoryStream(databaseDocumentQueue.Document); + var file = new FormFile(memStream, 0, databaseDocumentQueue.Document.Length, relatedDocument.FileName, relatedDocument.FileName); - DocumentUploadRequest request = new DocumentUploadRequest() + var request = new DocumentUploadRequest { File = file, DocumentStatusCode = relatedDocument.DocumentStatusTypeCode, DocumentTypeId = relatedDocument.DocumentTypeId, - DocumentTypeMayanId = documentTyp.MayanId, + DocumentTypeMayanId = documentType.MayanId, DocumentId = relatedDocument.DocumentId, - DocumentMetadata = databaseDocumentQueue.DocumentMetadata != null ? JsonSerializer.Deserialize>(databaseDocumentQueue.DocumentMetadata) : null, + DocumentMetadata = databaseDocumentQueue.DocumentMetadata != null + ? JsonSerializer.Deserialize>(databaseDocumentQueue.DocumentMetadata) + : null, }; - this.Logger.LogDebug("Document Queue {documentQueueId}, beginning upload.", documentQueue.DocumentQueueId); - DocumentUploadResponse response = await _documentService.UploadDocumentAsync(request, true); - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PROCESSING); // Set the status to processing, as the document is now being uploaded. Must be set after the mayan id is set, so that the poll logic functions correctly. + + this.Logger.LogDebug("Document Queue {documentQueueId}, beginning upload.", databaseDocumentQueue.DocumentQueueId); + var response = await _documentService.UploadDocumentAsync(request, true); if (response.DocumentExternalResponse.Status != ExternalResponseStatus.Success || response?.DocumentExternalResponse?.Payload == null) { @@ -273,42 +305,40 @@ public async Task Upload(PimsDocumentQueue documentQueue) databaseDocumentQueue.DocumentQueueStatusTypeCode, response.DocumentExternalResponse.Status); - databaseDocumentQueue.MayanError = $"Failed to upload document, mayan error: {response.DocumentExternalResponse.Message}"; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.MAYAN_ERROR); + databaseDocumentQueue.MayanError = $"Failed to upload document, Mayan error: {response.DocumentExternalResponse.Message}"; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.MAYAN_ERROR); return databaseDocumentQueue; } - response.MetadataExternalResponse.Where(r => r.Status != ExternalResponseStatus.Success).ForEach(r => this.Logger.LogError("url: ${url} status: ${status} message ${message}", r.Payload.Url, r.Status, r.Message)); // Log any metadata errors, but don't fail the upload. - // Mayan may have already returned a file id from the original upload. If not, this job will remain in the processing state (to be periodically checked for completion in another job). + response.MetadataExternalResponse + .Where(r => r.Status != ExternalResponseStatus.Success) + .ToList() + .ForEach(r => this.Logger.LogError("url: ${url} status: ${status} message ${message}", r.Payload.Url, r.Status, r.Message)); + if (response.DocumentExternalResponse?.Payload?.FileLatest?.Id != null) { - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + } + else + { + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PROCESSING); } } catch (Exception ex) when (ex is BadRequestException || ex is KeyNotFoundException || ex is InvalidDataException || ex is JsonException) { this.Logger.LogError($"Error: {ex.Message}"); databaseDocumentQueue.MayanError = ex.Message; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); } + return databaseDocumentQueue; } - /// - /// Updates the status of the specified document queue. - /// - /// The document queue object to update. - /// The new status type to set for the document queue. - /// - /// This method updates the document queue's status and commits the transaction. - /// If the status is a final state, it also updates the processing end date. - /// - private void UpdateDocumentQueueStatus(PimsDocumentQueue documentQueue, DocumentQueueStatusTypes statusType) + private async Task UpdateDocumentQueueStatus(PimsDocumentQueue documentQueue, DocumentQueueStatusTypes statusType) { documentQueue.DocumentQueueStatusTypeCode = statusType.ToString(); bool removeDocument = false; - // Any final states should update the processing end date. if (statusType != DocumentQueueStatusTypes.PROCESSING && statusType != DocumentQueueStatusTypes.PENDING) { documentQueue.DocProcessEndDt = DateTime.UtcNow; @@ -318,20 +348,10 @@ private void UpdateDocumentQueueStatus(PimsDocumentQueue documentQueue, Document removeDocument = true; } } - _documentQueueRepository.Update(documentQueue, removeDocument); - _documentQueueRepository.CommitTransaction(); + + await _documentQueueRepository.Update(documentQueue, removeDocument); } - /// - /// Validates the queued document against the database document queue. - /// - /// The document queue object from the database. - /// The document queue object to validate against the database. - /// True if the queued document is valid; otherwise, false. - /// - /// This method checks if the status type, process retries, and document content are valid. - /// It also ensures that at least one file document ID is associated with the document. - /// private bool ValidateQueuedDocument(PimsDocumentQueue databaseDocumentQueue, PimsDocumentQueue externalDocument) { if (databaseDocumentQueue.DocumentQueueStatusTypeCode != externalDocument.DocumentQueueStatusTypeCode) @@ -339,16 +359,19 @@ private bool ValidateQueuedDocument(PimsDocumentQueue databaseDocumentQueue, Pim this.Logger.LogError("Requested document queue status: {documentQueueStatusTypeCode} does not match current database status: {documentQueueStatusTypeCode}", externalDocument.DocumentQueueStatusTypeCode, databaseDocumentQueue.DocumentQueueStatusTypeCode); return false; } - else if (databaseDocumentQueue.DocProcessRetries != externalDocument.DocProcessRetries) + + if (databaseDocumentQueue.DocProcessRetries != externalDocument.DocProcessRetries) { this.Logger.LogError("Requested document retries: {documentQueueStatusTypeCode} does not match current database retries: {documentQueueStatusTypeCode}", externalDocument.DocumentQueueStatusTypeCode, databaseDocumentQueue.DocumentQueueStatusTypeCode); return false; } - else if (databaseDocumentQueue.Document == null || databaseDocumentQueue.DocumentId == null) + + if (databaseDocumentQueue.Document == null || databaseDocumentQueue.DocumentId == null) { this.Logger.LogError("Queued document file content is empty, unable to upload."); return false; } + return true; } } diff --git a/source/backend/api/Services/DocumentSyncService.cs b/source/backend/api/Services/DocumentSyncService.cs index be57b73a22..f0dad8e90e 100644 --- a/source/backend/api/Services/DocumentSyncService.cs +++ b/source/backend/api/Services/DocumentSyncService.cs @@ -234,7 +234,7 @@ public ExternalBatchResult SyncPimsToMayan(SyncModel model) IList>> updateTasks = new List>>(); foreach (var pimsDocumentTyp in pimsDocumentTypes) { - var matchingTypeFromMayan = mayanDocumentTypes.Payload.Results.FirstOrDefault(x => x.Id == pimsDocumentTyp.MayanId); + var matchingTypeFromMayan = mayanDocumentTypes?.Payload?.Results?.FirstOrDefault(x => x.Id == pimsDocumentTyp.MayanId); if (matchingTypeFromMayan == null) { createMayanDocumentTypeTasks.Add(new AddDocumentToMayanWithNameTaskWrapper() diff --git a/source/backend/api/Services/IDocumentQueueService.cs b/source/backend/api/Services/IDocumentQueueService.cs index 2a797d7df8..5a73fdb397 100644 --- a/source/backend/api/Services/IDocumentQueueService.cs +++ b/source/backend/api/Services/IDocumentQueueService.cs @@ -14,7 +14,7 @@ public interface IDocumentQueueService public IEnumerable SearchDocumentQueue(DocumentQueueFilter filter); - public PimsDocumentQueue Update(PimsDocumentQueue documentQueue); + public Task Update(PimsDocumentQueue documentQueue); public Task PollForDocument(PimsDocumentQueue documentQueue); diff --git a/source/backend/api/Startup.cs b/source/backend/api/Startup.cs index 19bd18bf3a..624d26a3f0 100644 --- a/source/backend/api/Startup.cs +++ b/source/backend/api/Startup.cs @@ -16,6 +16,8 @@ using HealthChecks.SqlServer; using HealthChecks.UI.Client; using Mapster; +using Medallion.Threading; +using Medallion.Threading.SqlServer; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; @@ -227,6 +229,7 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpClient("Pims.Api.Logging").AddHttpMessageHandler(); services.AddPimsContext(this.Environment, csBuilder.ConnectionString); services.AddPimsDalRepositories(); + services.AddSingleton(new SqlDistributedSynchronizationProvider(csBuilder.ConnectionString)); AddPimsApiRepositories(services); AddPimsApiServices(services); services.AddPimsKeycloakService(); diff --git a/source/backend/dal/Pims.Dal.csproj b/source/backend/dal/Pims.Dal.csproj index 3a76841cb6..d324f41777 100644 --- a/source/backend/dal/Pims.Dal.csproj +++ b/source/backend/dal/Pims.Dal.csproj @@ -11,6 +11,7 @@ + diff --git a/source/backend/dal/Repositories/DocumentQueueRepository.cs b/source/backend/dal/Repositories/DocumentQueueRepository.cs index 7ef007241a..d868b8f918 100644 --- a/source/backend/dal/Repositories/DocumentQueueRepository.cs +++ b/source/backend/dal/Repositories/DocumentQueueRepository.cs @@ -1,13 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; +using DocumentFormat.OpenXml.Office2010.Excel; +using Medallion.Threading; +using Medallion.Threading.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Pims.Api.Models.CodeTypes; using Pims.Core.Extensions; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; namespace Pims.Dal.Repositories { @@ -16,6 +20,7 @@ namespace Pims.Dal.Repositories /// public class DocumentQueueRepository : BaseRepository, IDocumentQueueRepository { + private readonly IDistributedLockProvider _synchronizationProvider; #region Constructors /// @@ -27,9 +32,11 @@ public class DocumentQueueRepository : BaseRepository, IDocumentQu public DocumentQueueRepository( PimsContext dbContext, ClaimsPrincipal user, + IDistributedLockProvider synchronizationProvider, ILogger logger) : base(dbContext, user, logger) { + this._synchronizationProvider = synchronizationProvider; } #endregion @@ -42,7 +49,6 @@ public DocumentQueueRepository( /// public PimsDocumentQueue TryGetById(long documentQueueId) { - return Context.PimsDocumentQueues .AsNoTracking() .FirstOrDefault(dq => dq.DocumentQueueId == documentQueueId); @@ -83,25 +89,35 @@ public PimsDocumentQueue GetByDocumentId(long documentId) /// /// /// - public PimsDocumentQueue Update(PimsDocumentQueue queuedDocument, bool removeDocument = false) + public async Task Update(PimsDocumentQueue queuedDocument, bool removeDocument = false) { - queuedDocument.ThrowIfNull(nameof(queuedDocument)); - var existingQueuedDocument = TryGetById(queuedDocument.DocumentQueueId) ?? throw new KeyNotFoundException($"DocumentQueueId {queuedDocument.DocumentQueueId} not found."); - if (existingQueuedDocument?.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.SUCCESS.ToString() && queuedDocument.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.SUCCESS.ToString()) - { - throw new InvalidOperationException($"DocumentQueueId {queuedDocument.DocumentQueueId} is already completed."); - } - if (!removeDocument) + // Use a distributed lock to ensure that only one process can update the document queue at a time (prevents deadlocks from history triggers). + var @lock = this._synchronizationProvider.CreateLock("DocumentQueueLock"); + await using (await @lock.AcquireAsync()) { - queuedDocument.Document = existingQueuedDocument.Document; + queuedDocument.ThrowIfNull(nameof(queuedDocument)); + var existingQueuedDocument = Context.PimsDocumentQueues + .AsNoTracking() + .FirstOrDefault(dq => dq.DocumentQueueId == queuedDocument.DocumentQueueId) + ?? throw new KeyNotFoundException($"DocumentQueueId {queuedDocument.DocumentQueueId} not found."); + + if (existingQueuedDocument?.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.SUCCESS.ToString() && queuedDocument.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.SUCCESS.ToString()) + { + throw new InvalidOperationException($"DocumentQueueId {queuedDocument.DocumentQueueId} is already completed."); + } + + if (!removeDocument) + { + queuedDocument.Document = existingQueuedDocument.Document; + } + + queuedDocument.MayanError = queuedDocument.MayanError?.Truncate(4000); + queuedDocument.DataSourceTypeCode = existingQueuedDocument.DataSourceTypeCode; // Do not allow the data source to be updated. + Context.Entry(existingQueuedDocument).CurrentValues.SetValues(queuedDocument); + queuedDocument = Context.Update(queuedDocument).Entity; + Context.SaveChanges(); // Force changes to be saved here, in the scope of the lock. + return queuedDocument; } - - queuedDocument.MayanError = queuedDocument.MayanError?.Truncate(4000); - queuedDocument.DataSourceTypeCode = existingQueuedDocument.DataSourceTypeCode; // Do not allow the data source to be updated. - Context.Entry(existingQueuedDocument).CurrentValues.SetValues(queuedDocument); - queuedDocument = Context.Update(queuedDocument).Entity; - - return queuedDocument; } /// @@ -187,7 +203,7 @@ public int DocumentQueueCount(PimsDocumentQueueStatusType pimsDocumentQueueStatu { if (pimsDocumentQueueStatusType == null) { - Context.PimsDocumentQueues.Count(); + return Context.PimsDocumentQueues.Count(); } return Context.PimsDocumentQueues.Count(d => d.DocumentQueueStatusTypeCode == pimsDocumentQueueStatusType.DocumentQueueStatusTypeCode); diff --git a/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs b/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs index 5e57420673..319a7cbe52 100644 --- a/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; @@ -18,7 +19,7 @@ public interface IDocumentQueueRepository : IRepository IEnumerable GetAllByFilter(DocumentQueueFilter filter); - PimsDocumentQueue Update(PimsDocumentQueue queuedDocument, bool removeDocument = false); + Task Update(PimsDocumentQueue queuedDocument, bool removeDocument = false); bool Delete(PimsDocumentQueue queuedDocument); diff --git a/source/backend/scheduler/Services/DocumentQueueService.cs b/source/backend/scheduler/Services/DocumentQueueService.cs index 5f1bf9e14b..f8dd52e3d9 100644 --- a/source/backend/scheduler/Services/DocumentQueueService.cs +++ b/source/backend/scheduler/Services/DocumentQueueService.cs @@ -41,13 +41,19 @@ public DocumentQueueService( public async Task UploadQueuedDocuments() { - var filter = new DocumentQueueFilter() { Quantity = _uploadQueuedDocumentsJobOptions?.CurrentValue?.BatchSize ?? 50, DocumentQueueStatusTypeCodes = new string[] { DocumentQueueStatusTypes.PENDING.ToString() }, MaxFileSize = _uploadQueuedDocumentsJobOptions?.CurrentValue?.MaxFileSize }; + var filter = new DocumentQueueFilter() { Quantity = _uploadQueuedDocumentsJobOptions?.CurrentValue?.BatchSize ?? 50, DocumentQueueStatusTypeCodes = new string[] { DocumentQueueStatusTypes.PENDING.ToString(), DocumentQueueStatusTypes.PROCESSING.ToString() }, MaxFileSize = _uploadQueuedDocumentsJobOptions?.CurrentValue?.MaxFileSize }; var searchResponse = await SearchQueuedDocuments(filter); if (searchResponse?.ScheduledTaskResponseModel != null) { return searchResponse?.ScheduledTaskResponseModel; } + if(searchResponse?.SearchResults?.Payload?.Any(s => s.DocumentQueueStatusType.Id == DocumentQueueStatusTypes.PROCESSING.ToString()) == true) + { + _logger.LogInformation("There are still documents that are actively being processed, skipping upload."); + return new ScheduledTaskResponseModel() { Status = TaskResponseStatusTypes.SKIPPED, Message = "There are still documents that are actively being processed, skipping upload." }; + } + IEnumerable> responses = searchResponse?.SearchResults?.Payload?.Select(qd => { _logger.LogInformation("Uploading Queued document {documentQueueId}", qd.Id); diff --git a/source/backend/tests/api/Services/DocumentQueueServiceTest.cs b/source/backend/tests/api/Services/DocumentQueueServiceTest.cs index a6704c8c2a..4b165da80f 100644 --- a/source/backend/tests/api/Services/DocumentQueueServiceTest.cs +++ b/source/backend/tests/api/Services/DocumentQueueServiceTest.cs @@ -128,7 +128,7 @@ public void SearchDocumentQueue_InvalidPermissions_ThrowsNotAuthorizedException( } [Fact] - public void Update_Success() + public async void Update_Success() { // Arrange var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); @@ -139,12 +139,11 @@ public void Update_Success() documentQueueRepositoryMock.Setup(m => m.CommitTransaction()); // Act - var result = service.Update(documentQueue); + var result = await service.Update(documentQueue); // Assert result.Should().Be(documentQueue); documentQueueRepositoryMock.Verify(m => m.Update(documentQueue, false), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -155,10 +154,10 @@ public void Update_InvalidPermissions_ThrowsNotAuthorizedException() var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; // Act - Action act = () => service.Update(documentQueue); + Func act = async () => await service.Update(documentQueue); // Assert - act.Should().Throw(); + act.Should().ThrowAsync(); } [Fact] @@ -213,12 +212,11 @@ public async Task PollForDocument_RelatedDocumentMayanIdNull_UpdatesStatusToPIMS documentQueueRepositoryMock.Setup(m => m.TryGetById(documentQueue.DocumentQueueId)).Returns(databaseDocumentQueue); // Act - var result = await service.PollForDocument(documentQueue); + Func act = async () => await service.PollForDocument(documentQueue); // Assert - result.Should().Be(databaseDocumentQueue); + await act.Should().ThrowAsync(); documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, false), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -244,7 +242,6 @@ public async Task PollForDocument_GetStorageDocumentDetailFails_UpdatesStatusToP // Assert result.Should().Be(databaseDocumentQueue); documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, false), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -254,7 +251,7 @@ public async Task PollForDocument_FileLatestIdNull_LogsFileStillProcessing() var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; var relatedDocument = new PimsDocument { MayanId = 1 }; - var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PROCESSING.ToString() }; var documentDetailModel = new DocumentDetailModel { FileLatest = null }; var documentDetailsResponse = new ExternalResponse { Status = ExternalResponseStatus.Success, Payload = documentDetailModel }; var documentRepositoryMock = this._helper.GetService>(); @@ -299,7 +296,6 @@ public async Task PollForDocument_FileLatestIdNotNull_UpdatesStatusToSuccess() result.Should().Be(databaseDocumentQueue); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, true), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -364,7 +360,6 @@ public async Task Upload_Success() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } @@ -431,7 +426,6 @@ public async Task Upload_Retry_Success() result.DocProcessRetries.Should().Be(1); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } @@ -462,7 +456,6 @@ public async Task Upload_ValidateQueuedDocumentFails_UpdatesStatusToPIMSError() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Never); } @@ -599,7 +592,6 @@ public async Task Upload_RelatedDocument_MayanId() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PROCESSING.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } @@ -688,13 +680,11 @@ public async Task Upload_DocumentTypeIdNull() documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns((PimsDocument)null); // Act - var result = await service.Upload(documentQueue); + Func act = async () => await service.Upload(documentQueue); + await act.Should().ThrowAsync(); // Assert - result.Should().NotBeNull(); - result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); - documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); + documentQueueRepositoryMock.Verify(x => x.Update(It.Is(p => p.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PIMS_ERROR.ToString()), It.IsAny()), Times.AtLeastOnce); } [Fact] @@ -752,7 +742,6 @@ public async Task Upload_UploadDocumentFails_UpdatesStatusToMayanError() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.MAYAN_ERROR.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } diff --git a/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs b/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs index 05076834e0..ad503e19e4 100644 --- a/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs +++ b/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs @@ -13,6 +13,7 @@ using Pims.Dal.Entities.Models; using System.Security; using Pims.Api.Models.CodeTypes; +using System.Threading.Tasks; namespace Pims.Dal.Test.Repositories { @@ -108,10 +109,10 @@ public void Update_Null() var repository = helper.CreateRepository(user); // Act - Action act = () => repository.Update(null); + Func act = () => repository.Update(null); // Assert - act.Should().Throw(); + act.Should().ThrowAsync(); } #endregion diff --git a/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs b/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs index 01755e57e8..8ee3e35b6f 100644 --- a/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs +++ b/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs @@ -81,7 +81,7 @@ public async Task UploadQueuedDocuments_ErrorStatus_ReturnsError() public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError() { // Arrange - var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -106,7 +106,7 @@ public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError() public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError_UpdatesQueue() { // Arrange - var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -136,7 +136,8 @@ public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError_Updates public async Task UploadQueuedDocuments_SingleDocumentSuccess_ReturnsSuccess() { // Arrange - var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; + var responseDocument = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -146,7 +147,7 @@ public async Task UploadQueuedDocuments_SingleDocumentSuccess_ReturnsSuccess() _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(new ExternalResponse { Status = ExternalResponseStatus.Success, - Payload = document, + Payload = responseDocument, }); // Act @@ -156,12 +157,38 @@ public async Task UploadQueuedDocuments_SingleDocumentSuccess_ReturnsSuccess() result.Status.Should().Be(TaskResponseStatusTypes.SUCCESS); } + [Fact] + public async Task UploadQueuedDocuments_SingleDocumentSkipProcessing() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var responseDocument = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = responseDocument, + }); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.SKIPPED); + } + [Fact] public async Task UploadQueuedDocuments_TwoDocumentsMixedResults_ReturnsPartialSuccess() { // Arrange - var document1 = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; - var document2 = new DocumentQueueModel { Id = 2, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document1 = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; + var document2 = new DocumentQueueModel { Id = 2, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; + var responseDocument1 = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -171,7 +198,7 @@ public async Task UploadQueuedDocuments_TwoDocumentsMixedResults_ReturnsPartialS _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document1)).ReturnsAsync(new ExternalResponse { Status = ExternalResponseStatus.Success, - Payload = document1, + Payload = responseDocument1, }); _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document2)).ReturnsAsync(new ExternalResponse { From 36d8640a35787c3b892a800c7c3c4cc465215e1d Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:08:43 -0700 Subject: [PATCH 35/61] Merge pull request #4873 from devinleighsmith/dev psp-10487 - allow upload of larger files - report errors correctly --- .../frontend/src/hooks/util/useApiRequestWrapper.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/source/frontend/src/hooks/util/useApiRequestWrapper.ts b/source/frontend/src/hooks/util/useApiRequestWrapper.ts index 64f5354cfd..282c098f13 100644 --- a/source/frontend/src/hooks/util/useApiRequestWrapper.ts +++ b/source/frontend/src/hooks/util/useApiRequestWrapper.ts @@ -110,7 +110,7 @@ export const useApiRequestWrapper = < ); }); if (returnApiError) { - return axiosError.response.data; + return axiosError.response?.data ?? getApiError(axiosError); } } else if (!throwError) { // If no error handling is provided, fall back to the default PIMS error handler (bomb icon). @@ -122,7 +122,7 @@ export const useApiRequestWrapper = < }), ); if (returnApiError) { - return axiosError.response.data; + return axiosError.response.data ?? getApiError(axiosError); } } @@ -167,3 +167,12 @@ export const useApiRequestWrapper = < requestEndOn, }; }; + +const getApiError = (error: AxiosError): IApiError => ({ + details: error?.message, + error: + 'An unknown error occurred, contact your system administrator if you continue to see this error.', + errorCode: error?.code, + stackTrace: JSON.stringify(error), + type: 'Unknown', +}); From a952baf887df330d738e1adabe6f6af78b2295af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 00:09:06 +0000 Subject: [PATCH 36/61] CI: Bump version to v5.11.0-106.14 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 52dfb246e3..fc4e413c8e 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.13 + 5.11.0-106.14 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index 10c5b07ad3..bb7d6b04a2 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.13", + "version": "5.11.0-106.14", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From c9f0624e105bb857431f8cb5409cf51831c5cb04 Mon Sep 17 00:00:00 2001 From: devinleighsmith <41091511+devinleighsmith@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:55:33 -0700 Subject: [PATCH 37/61] psp-10487 address major memory useage outliers. (#4880) Co-authored-by: Smith --- source/backend/api/Pims.Api.csproj | 2 ++ .../api/Repositories/Mayan/MayanDocumentRepository.cs | 5 +++-- .../core.api/Middleware/LogRequestMiddleware.cs | 10 +++++----- .../core.api/Middleware/LogResponseMiddleware.cs | 7 ++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index fc4e413c8e..40e27011f8 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -7,6 +7,8 @@ true {16BC0468-78F6-4C91-87DA-7403C919E646} net8.0 + true + true diff --git a/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs b/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs index bb3ff93ef7..4998175b90 100644 --- a/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs +++ b/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs @@ -257,8 +257,9 @@ public async Task> TryUploadDocumentAsync( string authenticationToken = await _authRepository.GetTokenAsync(); byte[] fileData; - using var byteReader = new BinaryReader(file.OpenReadStream()); - fileData = byteReader.ReadBytes((int)file.OpenReadStream().Length); + using Stream stream = file.OpenReadStream(); + using var byteReader = new BinaryReader(stream); + fileData = byteReader.ReadBytes((int)stream.Length); // Add the file data to the content using ByteArrayContent fileBytes = new(fileData); diff --git a/source/backend/core.api/Middleware/LogRequestMiddleware.cs b/source/backend/core.api/Middleware/LogRequestMiddleware.cs index e2aa599eb6..c420b76781 100644 --- a/source/backend/core.api/Middleware/LogRequestMiddleware.cs +++ b/source/backend/core.api/Middleware/LogRequestMiddleware.cs @@ -48,13 +48,13 @@ public async Task Invoke(HttpContext context) { context.Request.EnableBuffering(); await using var requestStream = _recyclableMemoryStreamManager.GetStream(); - await context.Request.Body.CopyToAsync(requestStream); - string body = null; - requestStream.Position = 0; - using (var streamReader = new StreamReader(requestStream)) + if (context.Request.ContentLength < maxStreamLength) { - if (requestStream.Length < maxStreamLength) + await context.Request.Body.CopyToAsync(requestStream); + + requestStream.Position = 0; + using (var streamReader = new StreamReader(requestStream)) { body = streamReader.ReadToEnd(); } diff --git a/source/backend/core.api/Middleware/LogResponseMiddleware.cs b/source/backend/core.api/Middleware/LogResponseMiddleware.cs index 1b9a952295..a180338b30 100644 --- a/source/backend/core.api/Middleware/LogResponseMiddleware.cs +++ b/source/backend/core.api/Middleware/LogResponseMiddleware.cs @@ -62,11 +62,12 @@ private async Task LogResponse(HttpContext context) await _next(context); - context.Response.Body.Seek(0, SeekOrigin.Begin); - using var reader = new StreamReader(context.Response.Body); string body = null; - if (reader.BaseStream.Length < maxStreamLength) + if (context.Response.ContentLength < maxStreamLength) { + context.Response.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(context.Response.Body); + body = await reader.ReadToEndAsync(); } context.Response.Body.Seek(0, SeekOrigin.Begin); From 0b2fba057ba1f558ac875607a5934f7e5c1efab4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 23:55:57 +0000 Subject: [PATCH 38/61] CI: Bump version to v5.11.0-106.15 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 40e27011f8..f6f35cb5c6 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.14 + 5.11.0-106.15 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index bb7d6b04a2..fac02bed1f 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.14", + "version": "5.11.0-106.15", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From b6916566d761abf1f1e89691b894030d0d98e2b8 Mon Sep 17 00:00:00 2001 From: Sue Tairaku <42981334+stairaku@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:06:56 -0700 Subject: [PATCH 39/61] PSP-10565: Adding a Contact in Property via Management tab is not adding a contact (#4877) * PSP-10565 * Deleting unnecessary variables --------- Co-authored-by: Alejandro Sanchez --- .../detail/PropertyContactListView.tsx | 48 ++++++++++++++----- .../mapSideBar/router/FilePropertyRouter.tsx | 19 ++++++++ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx index d08767c371..76731dbbd8 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx @@ -6,6 +6,7 @@ import { Section } from '@/components/common/Section/Section'; import { SectionListHeader } from '@/components/common/SectionListHeader'; import Claims from '@/constants/claims'; import { ApiGen_Concepts_PropertyContact } from '@/models/api/generated/ApiGen_Concepts_PropertyContact'; +import { exists } from '@/utils'; import { InventoryTabNames } from '../../../InventoryTabs'; import { PropertyEditForms } from '../../../PropertyRouter'; @@ -23,7 +24,8 @@ export const PropertyContactListView: React.FunctionComponent { const history = useHistory(); - const match = useRouteMatch<{ propertyId: string }>(); + const matchProperty = useRouteMatch<{ propertyId: string }>(); + const matchPropertyFile = useRouteMatch<{ id: string; menuIndex: string }>(); return (
} onAdd={() => { - const path = generatePath(match.path, { - propertyId: match.params.propertyId, - tab: InventoryTabNames.management, - }); - history.push(`${path}/${PropertyEditForms.UpdateContactContainer}?edit=true`); + if (exists(matchProperty.params.propertyId)) { + const path = generatePath(matchProperty.path, { + propertyId: matchProperty.params.propertyId, + tab: InventoryTabNames.management, + }); + history.push(`${path}/${PropertyEditForms.UpdateContactContainer}?edit=true`); + } else { + const path = generatePath(matchPropertyFile.path, { + id: matchPropertyFile.params.id, + menuIndex: matchPropertyFile.params.menuIndex, + tab: InventoryTabNames.management, + }); + history.push(`${path}/${PropertyEditForms.UpdateContactContainer}?edit=true`); + } }} /> } @@ -48,13 +59,24 @@ export const PropertyContactListView: React.FunctionComponent { - const path = generatePath(match.path, { - propertyId: match.params.propertyId, - tab: InventoryTabNames.management, - }); - history.push( - `${path}/${PropertyEditForms.UpdateContactContainer}/${contactId}?edit=true`, - ); + if (exists(matchProperty.params.propertyId)) { + const path = generatePath(matchProperty.path, { + propertyId: matchProperty.params.propertyId, + tab: InventoryTabNames.management, + }); + history.push( + `${path}/${PropertyEditForms.UpdateContactContainer}/${contactId}?edit=true`, + ); + } else { + const path = generatePath(matchPropertyFile.path, { + id: matchPropertyFile.params.id, + menuIndex: matchPropertyFile.params.menuIndex, + tab: InventoryTabNames.management, + }); + history.push( + `${path}/${PropertyEditForms.UpdateContactContainer}/${contactId}?edit=true`, + ); + } }} handleDelete={onDelete} /> diff --git a/source/frontend/src/features/mapSideBar/router/FilePropertyRouter.tsx b/source/frontend/src/features/mapSideBar/router/FilePropertyRouter.tsx index 26541073a8..030433646d 100644 --- a/source/frontend/src/features/mapSideBar/router/FilePropertyRouter.tsx +++ b/source/frontend/src/features/mapSideBar/router/FilePropertyRouter.tsx @@ -9,9 +9,13 @@ import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTy import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { ApiGen_Concepts_ResearchFileProperty } from '@/models/api/generated/ApiGen_Concepts_ResearchFileProperty'; import { exists, isValidId } from '@/utils'; +import AppRoute from '@/utils/AppRoute'; import { SideBarContext } from '../context/sidebarContext'; +import { PropertyEditForms } from '../property/PropertyRouter'; import { UpdatePropertyDetailsContainer } from '../property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer'; +import { PropertyContactEditContainer } from '../property/tabs/propertyDetailsManagement/update/PropertyContactEditContainer'; +import { PropertyContactEditForm } from '../property/tabs/propertyDetailsManagement/update/PropertyContactEditForm'; import { PropertyManagementUpdateContainer } from '../property/tabs/propertyDetailsManagement/update/summary/PropertyManagementUpdateContainer'; import { PropertyManagementUpdateForm } from '../property/tabs/propertyDetailsManagement/update/summary/PropertyManagementUpdateForm'; import UpdatePropertyForm from '../property/tabs/propertyResearch/update/UpdatePropertyForm'; @@ -108,6 +112,21 @@ export const FilePropertyRouter: React.FC = props => { ref={props.formikRef} /> + ( + + )} + key={PropertyEditForms.UpdateContactContainer} + title="Update Contact" + > ); From a7fc08fefcaf41fa8c42aafefff94d62dc3d0ea6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 01:07:17 +0000 Subject: [PATCH 40/61] CI: Bump version to v5.11.0-106.16 --- source/backend/api/Pims.Api.csproj | 2 +- source/frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index f6f35cb5c6..319d48b2d5 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.15 + 5.11.0-106.16 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/frontend/package.json b/source/frontend/package.json index fac02bed1f..a685524fb8 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.15", + "version": "5.11.0-106.16", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", From 60db0a8ce33e26f28d53782bd6bd0d038dea3499 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Thu, 3 Jul 2025 22:06:16 -0700 Subject: [PATCH 41/61] code review comments. --- .../common/mapFSM/MapStateMachineContext.tsx | 4 +- .../mapSideBar/shared/FileMenuRow.tsx | 77 ------------------- .../mapSideBar/shared/sidebarPathGenerator.ts | 4 +- source/frontend/src/utils/mapPropertyUtils.ts | 24 ------ 4 files changed, 4 insertions(+), 105 deletions(-) delete mode 100644 source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index 4ecbd61729..403d978a51 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -356,7 +356,7 @@ export const MapStateMachineProvider: React.FC> serviceSend({ type: 'FINISH_REPOSITION' }); }, [serviceSend]); - const setFileProperties = useCallback( + const setFilePropertyLocations = useCallback( (locations: LocationBoundaryDataset[]) => { serviceSend({ type: 'SET_FILE_PROPERTY_LOCATIONS', locations }); }, @@ -515,7 +515,7 @@ export const MapStateMachineProvider: React.FC> toggleMapFilterDisplay, toggleMapLayerControl, toggleSidebarDisplay, - setFilePropertyLocations: setFileProperties, + setFilePropertyLocations, setVisiblePimsProperties, setShowDisposed, setShowRetired, diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx deleted file mode 100644 index 9c545b69d4..0000000000 --- a/source/frontend/src/features/mapSideBar/shared/FileMenuRow.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import cx from 'classnames'; -import * as React from 'react'; -import { Col } from 'react-bootstrap'; -import { FaCaretRight } from 'react-icons/fa'; -import styled from 'styled-components'; - -import { LinkButton } from '@/components/common/buttons'; -import { StyledRow } from '@/components/common/HeaderField/styles'; -import { StyledIconWrapper } from '@/components/common/styles'; -import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; -import { getFilePropertyName } from '@/utils/mapPropertyUtils'; - -interface IFileMenuRowProps { - index: number; - currentPropertyIndex: number; - property: ApiGen_Concepts_FileProperty; - onSelectProperty: (propertyId: number) => void; -} - -export const FileMenuRow: React.FunctionComponent = ({ - index, - currentPropertyIndex, - property, - onSelectProperty, -}) => { - const propertyName = getFilePropertyName(property); - return ( - { - if (currentPropertyIndex !== index) { - onSelectProperty(property.id); - } - }} - > - {currentPropertyIndex === index && } - - {property?.isActive !== false ? ( - - {index + 1} - - ) : ( - {index + 1} - )} - - - {currentPropertyIndex === index ? ( - {propertyName.value} - ) : ( - {propertyName.value} - )} - - - ); -}; - -const StyledDisabledIconWrapper = styled.div` - &.selected { - border-color: ${props => props.theme.bcTokens.themeGray110}; - } - border: solid 0.3rem; - border-color: ${props => props.theme.bcTokens.themeGray100}; - font-size: 1.5rem; - border-radius: 20%; - width: 3.25rem; - height: 3.25rem; - padding: 1rem; - display: flex; - justify-content: center; - align-items: center; - color: black; - font-family: 'BCSans-Bold'; -`; - -export default FileMenuRow; diff --git a/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts b/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts index 06f0f573e1..501124783d 100644 --- a/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts +++ b/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts @@ -132,7 +132,7 @@ const usePathGenerator: IPathGenerator = () => { history.push(path); }; - const showFilePropertyIndex = (fileType: string, fileId: number, menuIndex: number) => { + const showFilePropertyId = (fileType: string, fileId: number, menuIndex: number) => { const a = `${sidebarBasePath}/:fileType/:fileId/property/:menuIndex`; const path = generatePath(a, { fileType, @@ -174,7 +174,7 @@ const usePathGenerator: IPathGenerator = () => { editDetails, addDetail, editProperties, - showFilePropertyId: showFilePropertyIndex, + showFilePropertyId, showFilePropertyDetail, }; }; diff --git a/source/frontend/src/utils/mapPropertyUtils.ts b/source/frontend/src/utils/mapPropertyUtils.ts index 106cb4597b..61653f0d15 100644 --- a/source/frontend/src/utils/mapPropertyUtils.ts +++ b/source/frontend/src/utils/mapPropertyUtils.ts @@ -172,30 +172,6 @@ export const getFeatureBoundedCenter = (feature: Feature, address?: string, From b73f13cd865c22c5099d24215f4a01a6e8c42250 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Thu, 3 Jul 2025 23:03:28 -0700 Subject: [PATCH 42/61] code review comments. --- .../api/Services/ManagementFileServiceTest.cs | 2 +- .../ContactInput/ContactInputView.test.tsx | 14 +-- .../DocumentDetailView.test.tsx | 2 +- .../form/ManagementTeamSubForm.test.tsx | 2 +- .../mapSideBar/shared/FileMenuView.tsx | 90 ++++++++++++++++--- 5 files changed, 87 insertions(+), 23 deletions(-) diff --git a/source/backend/tests/api/Services/ManagementFileServiceTest.cs b/source/backend/tests/api/Services/ManagementFileServiceTest.cs index 447564e6c0..8c4997e41d 100644 --- a/source/backend/tests/api/Services/ManagementFileServiceTest.cs +++ b/source/backend/tests/api/Services/ManagementFileServiceTest.cs @@ -1090,7 +1090,7 @@ public void UpdateProperties_DisableProperties_AddsNote() var propertyActivityRepository = this._helper.GetService>(); propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); diff --git a/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx b/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx index 208ed93e4b..3c1acbbe58 100644 --- a/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx +++ b/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx @@ -54,7 +54,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, - canEditDetails: true + canEditDetails: true, }); expect(asFragment()).toMatchSnapshot(); }); @@ -65,7 +65,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, - canEditDetails: true + canEditDetails: true, }); expect(getByText('Select from contacts')).toBeInTheDocument(); }); @@ -77,7 +77,7 @@ describe('ContactInputView component', () => { setShowContactManager: setShowContactManager, onClear: clear, initialValues: { test: { firstName: 'blah', surname: 'blah2', personId: 1 } }, - canEditDetails: true + canEditDetails: true, }); expect(getByText('blah blah2')).toBeInTheDocument(); }); @@ -89,7 +89,7 @@ describe('ContactInputView component', () => { setShowContactManager: setShowContactManager, onClear: clear, initialValues: { test: { organizationName: 'blah org', organizationId: 1 } }, - canEditDetails: true + canEditDetails: true, }); expect(getByText('blah org')).toBeInTheDocument(); }); @@ -100,7 +100,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, - canEditDetails: true + canEditDetails: true, }); const icon = getByTitle('Select Contact'); await act(async () => userEvent.click(icon)); @@ -113,7 +113,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, - canEditDetails: true + canEditDetails: true, }); const icon = queryByTitle('remove'); await act(async () => userEvent.click(icon)); @@ -126,7 +126,7 @@ describe('ContactInputView component', () => { field: 'invalid', setShowContactManager: setShowContactManager, onClear: clear, - canEditDetails: true + canEditDetails: true, }); const icon = queryByTitle('remove'); expect(icon).not.toBeInTheDocument(); diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx index 39b6f20975..76335105f9 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx @@ -128,7 +128,7 @@ describe('DocumentDetailView component', () => { document={renderOptions?.document ?? mockDocument} isLoading={renderOptions?.isLoading ?? false} setIsEditable={vi.fn()} - canEdit={renderOptions?.canEdit ?? true } + canEdit={renderOptions?.canEdit ?? true} />, { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx index 645ddf9b8d..c1022993e9 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx @@ -21,7 +21,7 @@ describe('ManagementTeamSubForm component', () => { const ref = createRef>(); const utils = render( - {formikProps => } + {formikProps => } , { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx index 3ed7c75c46..639c6177dc 100644 --- a/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx +++ b/source/frontend/src/features/mapSideBar/shared/FileMenuView.tsx @@ -1,16 +1,26 @@ import cx from 'classnames'; -import { useMemo } from 'react'; +import { geoJSON, latLngBounds } from 'leaflet'; +import { useCallback, useMemo } from 'react'; import React from 'react'; import { Col, Row } from 'react-bootstrap'; -import { FaCaretRight } from 'react-icons/fa'; +import { FaCaretRight, FaSearchPlus } from 'react-icons/fa'; +import { PiCornersOut } from 'react-icons/pi'; import styled from 'styled-components'; import { RestrictedEditControl } from '@/components/common/buttons'; import { EditPropertiesIcon } from '@/components/common/buttons/EditPropertiesButton'; import { LinkButton } from '@/components/common/buttons/LinkButton'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; -import { exists, getFilePropertyName, sortFileProperties } from '@/utils'; +import { + boundaryFromFileProperty, + exists, + getFilePropertyName, + getLatLng, + locationFromFileProperty, + sortFileProperties, +} from '@/utils'; import { cannotEditMessage } from '../acquisition/common/constants'; @@ -39,6 +49,35 @@ const FileMenuView: React.FunctionComponent !exists(currentFilePropertyId), [currentFilePropertyId]); + const mapMachine = useMapStateMachine(); + + const fitBoundaries = () => { + const fileProperties = file?.fileProperties; + + if (exists(fileProperties)) { + const locations = fileProperties + .map(fileProp => locationFromFileProperty(fileProp)) + .map(geom => getLatLng(geom)) + .filter(exists); + const bounds = latLngBounds(locations); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + } + }; + + const onZoomToProperty = useCallback( + (property: ApiGen_Concepts_FileProperty) => { + const geom = boundaryFromFileProperty(property); + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + }, + [mapMachine], + ); const activeProperties = []; const inactiveProperties = []; @@ -68,16 +107,31 @@ const FileMenuView: React.FunctionComponent - Properties - } - title="Change properties" - toolTipId={`${file?.id ?? 0}-summary-cannot-edit-tooltip`} - editRestrictionMessage={editRestrictionMessage} - onEdit={onEditProperties} - /> + + + Properties + + + } + title="Change properties" + toolTipId={`${file?.id ?? 0}-summary-cannot-edit-tooltip`} + editRestrictionMessage={editRestrictionMessage} + onEdit={onEditProperties} + /> + + + + + + +
@@ -130,6 +184,16 @@ const FileMenuView: React.FunctionComponent{propertyName.value} )} + + ) => { + event.stopPropagation(); + onZoomToProperty(fileProperty); + }} + > + + + ); })} From d07da8773f64215235049142eb002bf9f37dce4f Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Thu, 3 Jul 2025 23:28:47 -0700 Subject: [PATCH 43/61] test updates. --- .../SelectedPropertyRow.test.tsx.snap | 2 +- .../AcquisitionView.test.tsx.snap | 14 +- ...AcquisitionPropertiesSubForm.test.tsx.snap | 4 +- .../DispositionView.test.tsx.snap | 12 +- ...DispositionPropertiesSubForm.test.tsx.snap | 86 +++++---- .../__snapshots__/LeaseView.test.tsx.snap | 12 +- .../ManagementView.test.tsx.snap | 12 +- .../ManagementPropertiesSubForm.test.tsx.snap | 4 +- .../ResearchContainer.test.tsx.snap | 12 +- .../mapSideBar/shared/FileMenuView.tsx | 6 +- .../__snapshots__/FileMenuView.test.tsx.snap | 108 ++++++++++- .../UpdateProperties.test.tsx.snap | 178 ++++++++++-------- 12 files changed, 289 insertions(+), 161 deletions(-) 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 287f1439a4..80f7486b82 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 @@ -436,7 +436,7 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` class="col-auto" >
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PIN: 1111222
@@ -1616,7 +1620,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` class="pl-3 col-md-1" >
+
+
matches snapshot 1`] = ` >
+
+ +
matches snapshot 1`] = ` >
+
+ +
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 415f64a7d2..324213f04d 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 @@ -209,54 +209,54 @@ exports[`UpdateProperties component > renders as expected 1`] = ` margin-right: 0; } -.c31.c31.btn { +.c30.c30.btn { background-color: unset; border: none; } -.c31.c31.btn:hover, -.c31.c31.btn:focus, -.c31.c31.btn:active { +.c30.c30.btn:hover, +.c30.c30.btn:focus, +.c30.c30.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c31.c31.btn svg { +.c30.c30.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c31.c31.btn svg:hover { +.c30.c30.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c31.c31.btn.btn-primary svg { +.c30.c30.btn.btn-primary svg { color: #013366; } -.c31.c31.btn.btn-primary svg:hover { +.c30.c30.btn.btn-primary svg:hover { color: #013366; } -.c31.c31.btn.btn-light svg { +.c30.c30.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c31.c31.btn.btn-light svg:hover { +.c30.c30.btn.btn-light svg:hover { color: #CE3E39; } -.c31.c31.btn.btn-info svg { +.c30.c30.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c31.c31.btn.btn-info svg:hover { +.c30.c30.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c32.c32.btn { +.c31.c31.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -264,13 +264,13 @@ exports[`UpdateProperties component > renders as expected 1`] = ` line-height: unset; } -.c32.c32.btn .text { +.c31.c31.btn .text { display: none; } -.c32.c32.btn:hover, -.c32.c32.btn:active, -.c32.c32.btn:focus { +.c31.c31.btn:hover, +.c31.c31.btn:active, +.c31.c31.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -284,9 +284,9 @@ exports[`UpdateProperties component > renders as expected 1`] = ` flex-direction: row; } -.c32.c32.btn:hover .text, -.c32.c32.btn:active .text, -.c32.c32.btn:focus .text { +.c31.c31.btn:hover .text, +.c31.c31.btn:active .text, +.c31.c31.btn:focus .text { display: inline; line-height: 2rem; } @@ -310,7 +310,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` border-bottom-left-radius: 0; } -.c30 { +.c29 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -322,7 +322,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` gap: 0.8rem; } -.c30 .form-label { +.c29 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -482,31 +482,22 @@ exports[`UpdateProperties component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c25 { - font-size: 1.6rem; - color: #9F9D9C; - border-bottom: 0.2rem solid #606060; - margin-bottom: 0.9rem; - padding-bottom: 0.25rem; - font-family: 'BcSans-Bold'; -} - -.c29 { +.c28 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c27 { +.c26 { min-width: 3rem; min-height: 4.5rem; } -.c28 { +.c27 { font-size: 1.2rem; } -.c26 { +.c25 { min-height: 4.5rem; } @@ -592,7 +583,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` align-items: end; } -.c33 { +.c32 { position: -webkit-sticky; position: sticky; padding-top: 2rem; @@ -1236,7 +1227,43 @@ exports[`UpdateProperties component > renders as expected 1`] = `
- Selected properties +
+
+ Selected Properties +
+
+ +
+
@@ -1244,41 +1271,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` class="collapse show" >
-
- Identifier -
-
- Provide a descriptive name for this land - - - - - -
-
-
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PID: 123-456-789
@@ -1367,7 +1360,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +