From 6b545d5e0cfded85cc78cc915da5f70317386090 Mon Sep 17 00:00:00 2001 From: Sharala-Perumal <80914899+Sharala-Perumal@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:45:27 -0800 Subject: [PATCH 1/4] PIMS-2233: Update winston and few dev dependencies to latest versions in express-api (#2861) --- express-api/package.json | 12 ++++++------ express-api/tests/integration/users/users.test.ts | 2 +- express-api/tests/testUtils/factories.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/express-api/package.json b/express-api/package.json index 4ee3cae67..12fbbeb55 100644 --- a/express-api/package.json +++ b/express-api/package.json @@ -39,13 +39,13 @@ "tsconfig-paths": "4.2.0", "typeorm": "0.3.20", "typeorm-naming-strategies": "4.1.0", - "winston": "3.16.0", + "winston": "3.17.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "3.23.3" }, "devDependencies": { - "@eslint/js": "9.11.1", - "@faker-js/faker": "9.0.1", + "@eslint/js": "9.14.0", + "@faker-js/faker": "9.2.0", "@types/compression": "1.7.4", "@types/cookie-parser": "1.4.5", "@types/cors": "2.8.15", @@ -53,13 +53,13 @@ "@types/jest": "29.5.10", "@types/morgan": "1.9.9", "@types/multer": "1.4.11", - "@types/node": "22.7.4", + "@types/node": "22.9.0", "@types/nunjucks": "3.2.6", "@types/supertest": "6.0.2", "@types/swagger-jsdoc": "6.0.4", "@types/swagger-ui-express": "4.1.6", "cross-env": "7.0.3", - "eslint": "9.11.1", + "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.2.1", "jest": "29.7.0", @@ -69,6 +69,6 @@ "ts-jest": "29.2.0", "tsc-alias": "1.8.8", "typescript": "5.6.2", - "typescript-eslint": "8.8.0" + "typescript-eslint": "8.14.0" } } diff --git a/express-api/tests/integration/users/users.test.ts b/express-api/tests/integration/users/users.test.ts index 12355e1d2..7216e4c72 100644 --- a/express-api/tests/integration/users/users.test.ts +++ b/express-api/tests/integration/users/users.test.ts @@ -22,7 +22,7 @@ const mockUser: IUser = { middleName: faker.person.middleName(), lastName: faker.person.lastName(), email: faker.internet.email(), - username: faker.internet.userName(), + username: faker.internet.username(), position: 'Tester', }; const TOKEN = ''; diff --git a/express-api/tests/testUtils/factories.ts b/express-api/tests/testUtils/factories.ts index 50f37bebd..a4c036f4e 100644 --- a/express-api/tests/testUtils/factories.ts +++ b/express-api/tests/testUtils/factories.ts @@ -144,7 +144,7 @@ export const produceUser = (props?: Partial): User => { MiddleName: faker.person.middleName(), LastName: faker.person.lastName(), Email: faker.internet.email(), - Username: faker.internet.userName(), + Username: faker.internet.username(), Position: 'Tester', Note: '', LastLogin: faker.date.anytime(), From 700a86a210d08f24231caa3a7823dbb4acd99782 Mon Sep 17 00:00:00 2001 From: Sharala-Perumal <80914899+Sharala-Perumal@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:51:01 -0800 Subject: [PATCH 2/4] PIMS-2234: Update react-router-dom, typescript-eslint and few dev dependencies to latest versions in react-app (#2862) --- react-app/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/react-app/package.json b/react-app/package.json index 7b5f67bb1..bb6b73a5d 100644 --- a/react-app/package.json +++ b/react-app/package.json @@ -34,17 +34,17 @@ "react-leaflet": "4.2.1", "react-leaflet-draw": "0.20.4", "react-plugin": "3.0.0-alpha.4", - "react-router-dom": "6.27.0", + "react-router-dom": "6.28.0", "supercluster": "8.0.1", - "typescript-eslint": "8.13.0", + "typescript-eslint": "8.14.0", "use-supercluster": "1.2.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "3.23.8" }, "devDependencies": { - "@babel/preset-env": "7.25.2", - "@babel/preset-react": "7.24.1", - "@testing-library/jest-dom": "6.5.0", + "@babel/preset-env": "7.26.0", + "@babel/preset-react": "7.25.9", + "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.0.0", "@types/geojson": "7946.0.14", "@types/jest": "29.5.11", @@ -52,7 +52,7 @@ "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@vitejs/plugin-react": "4.3.0", - "eslint": "9.11.1", + "eslint": "9.14.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.2.1", "eslint-plugin-react": "7.37.1", @@ -64,6 +64,6 @@ "ts-node": "10.9.2", "typescript": "5.6.2", "vite": "5.4.6", - "vite-tsconfig-paths": "5.0.1" + "vite-tsconfig-paths": "5.1.2" } } From 3ecdadc45a10a57ef298fdf52748a6a752c9927e Mon Sep 17 00:00:00 2001 From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:43:34 -0800 Subject: [PATCH 3/4] PIMS-2220 Project Properties Map (#2863) --- react-app/src/App.tsx | 11 +++- .../src/components/map/InventoryLayer.tsx | 12 +++- react-app/src/components/map/MapLayers.tsx | 21 +++++-- react-app/src/components/map/ParcelMap.tsx | 53 ++++++++++++------ .../map/clusterPopup/ClusterPopup.tsx | 55 +++++++++++++------ .../src/components/map/sidebar/MapSidebar.tsx | 4 +- .../DisposalPropertiesSimpleTable.tsx | 19 ++++++- .../src/components/projects/ProjectDetail.tsx | 25 +++++++++ .../src/components/projects/ProjectDialog.tsx | 2 - .../components/property/PropertyDetail.tsx | 2 +- .../convertProjectPropertyToPropertyGeo.ts | 36 ++++++++++++ 11 files changed, 193 insertions(+), 47 deletions(-) create mode 100644 react-app/src/utilities/convertProjectPropertyToPropertyGeo.ts diff --git a/react-app/src/App.tsx b/react-app/src/App.tsx index 3b21b2e4e..121bd9ac8 100644 --- a/react-app/src/App.tsx +++ b/react-app/src/App.tsx @@ -49,7 +49,16 @@ const Router = () => { const showMap = () => ( - + ); diff --git a/react-app/src/components/map/InventoryLayer.tsx b/react-app/src/components/map/InventoryLayer.tsx index 6b0e21297..76466f5f2 100644 --- a/react-app/src/components/map/InventoryLayer.tsx +++ b/react-app/src/components/map/InventoryLayer.tsx @@ -194,6 +194,14 @@ export const InventoryLayer = (props: InventoryLayerProps) => { }), }); + // Converts mouse event on clusters to a usable Point + const convertEventToPoint = (e: MouseEvent): Point => { + return { + x: e.clientX, + y: e.clientY, + } as Point; + }; + return ( <> {/* For all cluster objects */} @@ -208,7 +216,7 @@ export const InventoryLayer = (props: InventoryLayerProps) => { eventHandlers={{ click: () => zoomOnCluster(property), mouseover: (e) => { - openClusterPopup(property, e.containerPoint); + openClusterPopup(property, convertEventToPoint(e.originalEvent)); }, mouseout: cancelOpenPopup, }} @@ -232,7 +240,7 @@ export const InventoryLayer = (props: InventoryLayerProps) => { }, ); }, - mouseover: (e) => openClusterPopup(property, e.containerPoint), + mouseover: (e) => openClusterPopup(property, convertEventToPoint(e.originalEvent)), mouseout: cancelOpenPopup, }} /> diff --git a/react-app/src/components/map/MapLayers.tsx b/react-app/src/components/map/MapLayers.tsx index 3ba55db56..f1b0d7fda 100644 --- a/react-app/src/components/map/MapLayers.tsx +++ b/react-app/src/components/map/MapLayers.tsx @@ -97,12 +97,25 @@ interface MapLayersProps { const MapLayers = (props: MapLayersProps) => { const { hideControls } = props; // If layer control is hidden, must still return a default tileset to use + // Also showing parcel boundaries if (hideControls) { + const parcelBoundaryLayer = LAYER_CONFIGS.landOwnership.find( + (layer) => layer.name === 'Parcel Boundaries', + ); return ( - + <> + + + ); } return ( diff --git a/react-app/src/components/map/ParcelMap.tsx b/react-app/src/components/map/ParcelMap.tsx index 04cc668ba..9e97db563 100644 --- a/react-app/src/components/map/ParcelMap.tsx +++ b/react-app/src/components/map/ParcelMap.tsx @@ -34,6 +34,9 @@ type ParcelMapProps = { hideControls?: boolean; defaultZoom?: number; defaultLocation?: LatLngExpression; + overrideProperties?: PropertyGeo[]; + showClusterPopup?: boolean; + showSideBar?: boolean; } & PropsWithChildren; export const SelectedMarkerContext = createContext(null); @@ -130,10 +133,13 @@ const ParcelMap = (props: ParcelMapProps) => { loadProperties = false, popupSize, scrollOnClick, - zoomOnScroll = true, + zoomOnScroll = false, hideControls = false, defaultLocation, defaultZoom, + overrideProperties, + showClusterPopup = false, + showSideBar = false, } = props; // To access map outside of MapContainer @@ -167,6 +173,13 @@ const ParcelMap = (props: ParcelMapProps) => { } }, [data, isLoading]); + // If override properties were supplied, set them. + useEffect(() => { + if (overrideProperties) { + setProperties(overrideProperties); + } + }, [overrideProperties]); + // Loops through any array and pairs it down to a flat list of its base elements // Used here for breaking shape geography down to bounds coordinates const extractLowestElements: (arr: any[]) => [number, number][] = (arr) => { @@ -292,14 +305,16 @@ const ParcelMap = (props: ParcelMapProps) => { ], ), { - paddingBottomRight: [500, 0], // Padding for map sidebar + paddingBottomRight: showSideBar ? [500, 0] : [0, 0], // Padding for map sidebar }, ); } }, [properties]); + // Reference to containing div to help centre cluster popups + const mapBoxRef = useRef(); return ( - + {loadProperties ? : <>} { mapEventsDisabled={mapEventsDisabled} /> - {loadProperties ? ( + {loadProperties || overrideProperties ? ( { ))} {props.children} - {loadProperties ? ( - <> - - - + {loadProperties && showSideBar ? ( + + ) : ( + <> + )} + {(loadProperties || overrideProperties) && showClusterPopup ? ( + ) : ( <> )} diff --git a/react-app/src/components/map/clusterPopup/ClusterPopup.tsx b/react-app/src/components/map/clusterPopup/ClusterPopup.tsx index 9d646f3a6..89eb9f858 100644 --- a/react-app/src/components/map/clusterPopup/ClusterPopup.tsx +++ b/react-app/src/components/map/clusterPopup/ClusterPopup.tsx @@ -7,7 +7,7 @@ import { formatNumber, pidFormatter } from '@/utilities/formatters'; import { ArrowCircleLeft, ArrowCircleRight } from '@mui/icons-material'; import { Box, Grid, IconButton, Typography } from '@mui/material'; import { Point } from 'leaflet'; -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useRef } from 'react'; export interface PopupState { open: boolean; @@ -23,6 +23,7 @@ export interface PopupState { interface ClusterPopupProps { popupState: PopupState; setPopupState: (stateUpdates: Partial) => void; + boundingBox?: DOMRect; } /** @@ -34,46 +35,60 @@ interface ClusterPopupProps { * @returns {JSX.Element} A React component representing the ClusterPopup. */ const ClusterPopup = (props: ClusterPopupProps) => { - const { popupState, setPopupState } = props; + const { popupState, setPopupState, boundingBox } = props; const { getLookupValueById } = useContext(LookupContext); + // Backups for when surrounding box isn't initialized + const within = boundingBox ?? { + x: 0, + y: 0, + height: 0, + width: 0, + right: 0, + left: 0, + top: 0, + bottom: 0, + }; + /** * The following block of code determines which direction and position the popup should open with. - * Depending on the screen size, it determines the quadrant of the mouse event and choses a position and offset. + * Depending on the map size, it determines the quadrant of the mouse event and choses a position and offset. */ - const screenCentre = { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 }; // -100 to account for the side menu being open + const mapCentre = { x: within.width / 2 - 100, y: within.height / 2 }; // -100 to account for the side menu being open + const mousePositionOnMap = popupState.position; + let offset: { x: number; y: number } = { x: 0, y: 0 }; - // Depending on how many properties are available, y displacement changes. 1 = -60, 2 = -180, else -220 + // Depending on how many properties are available, y displacement changes. 1 = -170, 2 = -260, else -300 const bottomYOffset = - popupState.properties.length < 3 ? (popupState.properties.length === 2 ? -180 : -60) : -220; + popupState.properties.length < 3 ? (popupState.properties.length === 2 ? -260 : -170) : -300; // Determine quadrant and set offset - const leftXOffset = 5; - const rightXOffset = -415; - const topYOffset = 80; + const leftXOffset = 0; + const rightXOffset = -400; + const topYOffset = 0; switch (true) { // Top-left quadant - case popupState.position.x <= screenCentre.x && popupState.position.y <= screenCentre.y: + case mousePositionOnMap.x <= mapCentre.x && mousePositionOnMap.y <= mapCentre.y: offset = { x: leftXOffset, y: topYOffset, }; break; // Top-right quadrant - case popupState.position.x > screenCentre.x && popupState.position.y <= screenCentre.y: + case mousePositionOnMap.x > mapCentre.x && mousePositionOnMap.y <= mapCentre.y: offset = { x: rightXOffset, y: topYOffset, }; break; // Bottom-left quadrant - case popupState.position.x <= screenCentre.x && popupState.position.y > screenCentre.y: + case mousePositionOnMap.x <= mapCentre.x && mousePositionOnMap.y > mapCentre.y: offset = { x: leftXOffset, y: bottomYOffset, }; break; // Bottom-right quadrant - case popupState.position.x > screenCentre.x && popupState.position.y > screenCentre.y: + case mousePositionOnMap.x > mapCentre.x && mousePositionOnMap.y > mapCentre.y: offset = { x: rightXOffset, y: bottomYOffset, @@ -95,16 +110,18 @@ const ClusterPopup = (props: ClusterPopupProps) => { } }, [popupState.pageIndex]); + const popupRef = useRef(); return ( { ? property.properties.Name.match(/^\d*$/) || property.properties.Name == '' ? property.properties.Address1 : property.properties.Name - : pidFormatter(property.properties.PID) ?? String(property.properties.PIN) + : property.properties.PID != null && property.properties.PID != 0 + ? pidFormatter(property.properties.PID) + : String(property.properties.PIN) } content={[ property.properties.Address1, diff --git a/react-app/src/components/map/sidebar/MapSidebar.tsx b/react-app/src/components/map/sidebar/MapSidebar.tsx index b1f21434d..31095cb08 100644 --- a/react-app/src/components/map/sidebar/MapSidebar.tsx +++ b/react-app/src/components/map/sidebar/MapSidebar.tsx @@ -153,7 +153,9 @@ const MapSidebar = (props: MapSidebarProps) => { ? property.properties.Name.match(/^\d*$/) || property.properties.Name == '' ? property.properties.Address1 : property.properties.Name - : pidFormatter(property.properties.PID) ?? String(property.properties.PIN) + : property.properties.PID != null && property.properties.PID != 0 + ? pidFormatter(property.properties.PID) + : String(property.properties.PIN) } content={[ property.properties.Address1, diff --git a/react-app/src/components/projects/DisposalPropertiesSimpleTable.tsx b/react-app/src/components/projects/DisposalPropertiesSimpleTable.tsx index be78f1638..3e2dce11b 100644 --- a/react-app/src/components/projects/DisposalPropertiesSimpleTable.tsx +++ b/react-app/src/components/projects/DisposalPropertiesSimpleTable.tsx @@ -1,15 +1,17 @@ import { PropertyTypes } from '@/constants/propertyTypes'; import { Agency } from '@/hooks/api/useAgencyApi'; import { formatMoney, pidFormatter } from '@/utilities/formatters'; -import { Box, Typography } from '@mui/material'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { Box, Typography, useTheme } from '@mui/material'; +import { DataGrid, GridCellParams, GridColDef } from '@mui/x-data-grid'; import React from 'react'; +import { Link } from 'react-router-dom'; interface IDisposalPropertiesTable { rows: Record[]; } const DisposalPropertiesTable = (props: IDisposalPropertiesTable) => { + const theme = useTheme(); const columns: GridColDef[] = [ { field: 'PropertyType', @@ -23,6 +25,19 @@ const DisposalPropertiesTable = (props: IDisposalPropertiesTable) => { row.PropertyTypeId === PropertyTypes.BUILDING && row.Address1 ? row.Address1 : pidFormatter(row.PID) ?? row.PIN, + renderCell: (params: GridCellParams) => { + const urlType = params.row.PropertyTypeId === 0 ? 'parcel' : 'building'; + return ( + + {String(params.value)} + + ); + }, }, { field: 'Agency', diff --git a/react-app/src/components/projects/ProjectDetail.tsx b/react-app/src/components/projects/ProjectDetail.tsx index 068d3a0d6..fe08f77f1 100644 --- a/react-app/src/components/projects/ProjectDetail.tsx +++ b/react-app/src/components/projects/ProjectDetail.tsx @@ -52,6 +52,9 @@ import { getStatusString } from '@/constants/chesNotificationStatus'; import { NoteTypes } from '@/constants/noteTypes'; import EnhancedReferralDates from './EnhancedReferralDates'; import { NotificationType } from '@/constants/notificationTypes'; +import ParcelMap from '@/components/map/ParcelMap'; +import { PropertyGeo } from '@/hooks/api/usePropertiesApi'; +import { convertProjectPropertyToPropertyGeo } from '@/utilities/convertProjectPropertyToPropertyGeo'; interface IProjectDetail { onClose: () => void; @@ -117,6 +120,19 @@ const ProjectDetail = (props: IProjectDetail) => { return notificationItems.some((n) => types.includes(n.TemplateId)); }, [notifications]); + // Store the map properties for this project + const [mapProperties, setMapProperties] = useState([]); + // When data changes, refresh the map properties + useEffect(() => { + if (data?.parsedBody) { + setMapProperties( + data.parsedBody.ProjectProperties.map((property) => + convertProjectPropertyToPropertyGeo(property, data.parsedBody), + ), + ); + } + }, [data]); + const { ungroupedAgencies, agencyOptions } = useGroupedAgenciesApi(); interface IStatusHistoryStruct { Notes: Array; @@ -351,6 +367,15 @@ const ProjectDetail = (props: IProjectDetail) => { /> )} + { const requireNotificationAcknowledge = approvedStatus == status && status !== initialValues?.StatusId; const isAdmin = pimsUser.hasOneOfRoles([Roles.ADMIN]); - console.log('project form values', projectFormMethods.getValues()); return ( { }} onConfirm={async () => { const isValid = await projectFormMethods.trigger(); - console.log('lookupData and isValid', lookupData, isValid); if (lookupData && isValid) { const values = projectFormMethods.getValues(); submit(+initialValues.Id, { diff --git a/react-app/src/components/property/PropertyDetail.tsx b/react-app/src/components/property/PropertyDetail.tsx index e353169ee..c73df96a3 100644 --- a/react-app/src/components/property/PropertyDetail.tsx +++ b/react-app/src/components/property/PropertyDetail.tsx @@ -354,7 +354,7 @@ const PropertyDetail = (props: IPropertyDetail) => { height={'500px'} mapRef={map} movable={false} - zoomable={false} + zoomable={true} zoomOnScroll={false} popupSize="small" hideControls diff --git a/react-app/src/utilities/convertProjectPropertyToPropertyGeo.ts b/react-app/src/utilities/convertProjectPropertyToPropertyGeo.ts new file mode 100644 index 000000000..9a751ccb1 --- /dev/null +++ b/react-app/src/utilities/convertProjectPropertyToPropertyGeo.ts @@ -0,0 +1,36 @@ +import { ProjectProperty, Project } from '@/hooks/api/useProjectsApi'; +import { PropertyGeo } from '@/hooks/api/usePropertiesApi'; + +/** + * Converts a ProjectProperty object into a PropertyGeo object. + * + * @param projectProperty - The ProjectProperty object containing details about the property. + * @param project - Optional Project object providing additional project-related information. + * @returns A PropertyGeo object representing the geographical and property details. + */ +export const convertProjectPropertyToPropertyGeo = ( + projectProperty: ProjectProperty, + project?: Project, +): PropertyGeo => { + const property = projectProperty.Parcel ?? projectProperty.Building; + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [property.Location.x, property.Location.y], + }, + properties: { + Id: property.Id, + Location: property.Location, + PropertyTypeId: property.PropertyTypeId, + ClassificationId: property.ClassificationId, + Name: property.Name, + AdministrativeAreaId: property.AdministrativeAreaId, + AgencyId: property.AgencyId, + PID: property.PID, + PIN: property.PIN, + Address1: property.Address1, + ProjectStatusId: project?.StatusId, + }, + } as PropertyGeo; +}; From b0ccefafb89970c4e33f521f6eb930e5fe4a25cd Mon Sep 17 00:00:00 2001 From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com> Date: Mon, 18 Nov 2024 08:33:03 -0800 Subject: [PATCH 4/4] BCA connect-src Fix (#2864) --- react-app/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-app/nginx.conf b/react-app/nginx.conf index 92293338f..f7b1eac47 100644 --- a/react-app/nginx.conf +++ b/react-app/nginx.conf @@ -27,7 +27,7 @@ server { style-src 'self' 'unsafe-inline' https://unpkg.com https://cdnjs.cloudflare.com; font-src 'self'; img-src 'self' data: tile.openstreetmap.org https://maps.googleapis.com https://unpkg.com https://cdnjs.cloudflare.com https://openmaps.gov.bc.ca https://server.arcgisonline.com; - connect-src 'self' https://openmaps.gov.bc.ca; + connect-src 'self' https://openmaps.gov.bc.ca https://test.apps.gov.bc.ca https://apps.gov.bc.ca; manifest-src 'self'; form-action 'self'; ";