From a723e40bcbfacf4c3682d7d29a8795fb15ecf9db Mon Sep 17 00:00:00 2001 From: Tuukka Kataja Date: Thu, 26 Oct 2023 16:49:10 +0300 Subject: [PATCH] [projectObject] allow adding geometry on new projectObject creation --- backend/src/router/projectObject.ts | 63 ++++++++++++------- .../src/views/ProjectObject/ProjectObject.tsx | 15 ++++- .../views/ProjectObject/ProjectObjectForm.tsx | 37 ++++++----- shared/src/language/fi.json | 1 + shared/src/schema/projectObject.ts | 1 + 5 files changed, 79 insertions(+), 38 deletions(-) diff --git a/backend/src/router/projectObject.ts b/backend/src/router/projectObject.ts index bf36c6af..3ba16be3 100644 --- a/backend/src/router/projectObject.ts +++ b/backend/src/router/projectObject.ts @@ -10,6 +10,7 @@ import { TRPC } from '@backend/router'; import { nonEmptyString } from '@shared/schema/common'; import { + UpdateGeometry, UpdateProjectObject, UpsertProjectObject, dbProjectObjectSchema, @@ -303,9 +304,48 @@ export async function upsertProjectObject( await updateObjectUsages(tx, { ...projectObject, id: upsertResult.id }); await updateObjectRoles(tx, { ...projectObject, id: upsertResult.id }); + if (projectObject.geom) { + updateProjectObjectGeometry( + tx, + { + id: upsertResult.id, + features: projectObject.geom, + }, + userId + ); + } + return upsertResult; } +async function updateProjectObjectGeometry( + tx: DatabaseTransactionConnection, + input: UpdateGeometry, + userId: string +) { + const { id, features } = input; + + await addAuditEvent(tx, { + eventType: 'projectObject.updateGeometry', + eventData: input, + eventUser: userId, + }); + + return tx.one(sql.type(updateGeometryResultSchema)` + WITH featureCollection AS ( + SELECT ST_Collect( + ST_GeomFromGeoJSON(value->'geometry') + ) AS resultGeom + FROM jsonb_array_elements(${features}::jsonb) + ) + UPDATE app.project_object + SET geom = featureCollection.resultGeom + FROM featureCollection + WHERE id = ${id} + RETURNING id, ST_AsGeoJSON(geom) AS geom + `); +} + export const createProjectObjectRouter = (t: TRPC) => t.router({ upsert: t.procedure.input(upsertProjectObjectSchema).mutation(async ({ input, ctx }) => { @@ -316,27 +356,8 @@ export const createProjectObjectRouter = (t: TRPC) => }), updateGeometry: t.procedure.input(updateGeometrySchema).mutation(async ({ input, ctx }) => { - const { id, features } = input; - - return getPool().transaction(async (tx) => { - await addAuditEvent(tx, { - eventType: 'projectObject.updateGeometry', - eventData: input, - eventUser: ctx.user.id, - }); - return tx.one(sql.type(updateGeometryResultSchema)` - WITH featureCollection AS ( - SELECT ST_Collect( - ST_GeomFromGeoJSON(value->'geometry') - ) AS resultGeom - FROM jsonb_array_elements(${features}::jsonb) - ) - UPDATE app.project_object - SET geom = featureCollection.resultGeom - FROM featureCollection - WHERE id = ${id} - RETURNING id, ST_AsGeoJSON(geom) AS geom - `); + return await getPool().transaction(async (tx) => { + return await updateProjectObjectGeometry(tx, input, ctx.user.id); }); }), diff --git a/frontend/src/views/ProjectObject/ProjectObject.tsx b/frontend/src/views/ProjectObject/ProjectObject.tsx index 499c46ac..d4904f20 100644 --- a/frontend/src/views/ProjectObject/ProjectObject.tsx +++ b/frontend/src/views/ProjectObject/ProjectObject.tsx @@ -3,7 +3,7 @@ import { Assignment, Euro, Map } from '@mui/icons-material'; import { Box, Breadcrumbs, Chip, Paper, Tab, Tabs, Typography } from '@mui/material'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import { ReactElement, useMemo } from 'react'; +import { ReactElement, useMemo, useState } from 'react'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; @@ -82,6 +82,7 @@ export function ProjectObject(props: Props) { projectObjectId: string; tabView?: TabView; }; + const projectObjectId = routeParams?.projectObjectId; const tabView = routeParams.tabView || 'default'; const tabs = projectObjectTabs(routeParams.projectId, props.projectType, projectObjectId); @@ -95,6 +96,8 @@ export function ProjectObject(props: Props) { { enabled: Boolean(projectObjectId) } ); + const [geom, setGeom] = useState(null); + const tr = useTranslations(); const notify = useNotifications(); const geometryUpdate = trpc.projectObject.updateGeometry.useMutation({ @@ -181,6 +184,7 @@ export function ProjectObject(props: Props) { projectId={routeParams.projectId} projectType={props.projectType} projectObject={projectObject.data} + geom={geom} /> {/* @@ -215,6 +219,7 @@ export function ProjectObject(props: Props) { > {tabs.map((tab) => ( { - geometryUpdate.mutate({ id: projectObjectId, features: features }); + if (!projectObject.data) { + setGeom(features); + } else { + geometryUpdate.mutate({ id: projectObjectId, features }); + } }} /> diff --git a/frontend/src/views/ProjectObject/ProjectObjectForm.tsx b/frontend/src/views/ProjectObject/ProjectObjectForm.tsx index 691eb575..d38fcb25 100644 --- a/frontend/src/views/ProjectObject/ProjectObjectForm.tsx +++ b/frontend/src/views/ProjectObject/ProjectObjectForm.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import { zodResolver } from '@hookform/resolvers/zod'; import { AddCircle, Edit, Save, Undo } from '@mui/icons-material'; -import { Box, Button, InputAdornment, TextField } from '@mui/material'; +import { Alert, Box, Button, InputAdornment, TextField } from '@mui/material'; import { useQueryClient } from '@tanstack/react-query'; import dayjs from 'dayjs'; import { useAtomValue } from 'jotai'; @@ -25,7 +25,6 @@ import { SapWBSSelect } from '@frontend/views/ProjectObject/SapWBSSelect'; import { UpsertProjectObject, newProjectObjectSchema, - updateProjectObjectSchema, upsertProjectObjectSchema, } from '@shared/schema/projectObject'; @@ -38,6 +37,7 @@ interface Props { projectId: string; projectType: ProjectTypePath; projectObject?: UpsertProjectObject | null; + geom?: string | null; } export function ProjectObjectForm(props: Props) { @@ -138,7 +138,9 @@ export function ProjectObjectForm(props: Props) { }, }); - const onSubmit = (data: UpsertProjectObject) => projectObjectUpsert.mutate(data); + const onSubmit = (data: UpsertProjectObject) => { + projectObjectUpsert.mutate({ ...data, geom: props.geom }); + }; return ( @@ -330,17 +332,24 @@ export function ProjectObjectForm(props: Props) { /> {!props.projectObject && ( - + <> + {(!props.geom || props.geom === '[]') && ( + + {tr('projectObjectForm.infoNoGeom')} + + )} + + )} {props.projectObject && editing && ( diff --git a/shared/src/language/fi.json b/shared/src/language/fi.json index 873366d0..a9e1535b 100644 --- a/shared/src/language/fi.json +++ b/shared/src/language/fi.json @@ -232,6 +232,7 @@ "projectObject.notifyGeometryUpdateTitle": "Kohteen sijainti päivitetty", "projectObject.notifyGeometryUpdateFailedTitle": "Virhe tallentaessa kohteen sijaintia", "projectObject.noTasks": "Kohteella ei ole tehtäviä", + "projectObjectForm.infoNoGeom": "Olet luomassa kohdetta ilman aluetta", "projectObjectForm.createBtnLabel": "Luo kohde", "projectObjectForm.saveBtnLabel": "Tallenna", "sessionExpiredWarning.infoText": "Istuntosi on vanhentunut. Jatkaaksesi keskeneräisiä töitäsi, uusi istunto.", diff --git a/shared/src/schema/projectObject.ts b/shared/src/schema/projectObject.ts index a93285bf..44118088 100644 --- a/shared/src/schema/projectObject.ts +++ b/shared/src/schema/projectObject.ts @@ -29,6 +29,7 @@ export const newProjectObjectSchema = z.object({ height: z.coerce.number().optional().nullable(), objectUserRoles: z.array(projectObjectUserRoleSchema), budgetUpdate: partialBudgetUpdateSchema.optional().nullable(), + geom: z.string().optional().nullable(), }); export const updateProjectObjectSchema = newProjectObjectSchema.partial().extend({