Skip to content

Commit

Permalink
feat: entity pulse effect on rejected submissions (#2018)
Browse files Browse the repository at this point in the history
* fix(submissions): update entity status on submission approve or reject

* fix(flatgeobuf-layer): update ts type

* feat(main): layer representing entities having issues pulsing the entity outlines

* fix(getTaskStatusStyle): update feature style

* fix(taskSelectionPopup): clear selecetd task on task popup close

* fix(projectModel): update outline type

* fix(vectorLayer): processGeojson prop function add

* feat(projectDetailsV2): pulse rejected entities

* fix(extractGeojsonFromObject): export convertCoordinateStringToFeatuer function

* fix(submissionModel): ts types add

* feat(submissionService): post geometry service add

* feat(commonUtils): add isEmpty function

* fix(ISubmissions): add feature type to updateReviewStatusModal type

* fix(submission): convert javaRosa string to feature and pass to reviewStateModal

* feat(updateReviewStatusModal): dispatch post bad geometry if submission marked as hasIssues

* fix(entities): getBadGeomStream, subscribeToBadGeom function add

* feat(+page): dispatch subscribeToBadGeom

* fix(entities): update variable and function names, filter new and bad geoms on frontend

* fix(+page): update function names

* fix(entities): get newGeomList func add

* fix(main): remove flatgeobuf layer for displaying rejected entities

* fix(submissionModel): update geometryLogType key

* fix(updateReviewStatusModal): udpate geom key to geojson, convert task_id to number, fix ts type

* fix(extractGeojsonFromObject): convert javaRosa geom to polygon instead of lineString

* fix(entities): store bad and new geoms in featurecollectionn

* fix(entities): update type and initial state

* feat(main):: layer add to display bad geometries

* fix(updateReviewStatusModal): update post geometry post api url

* fix(entities): update geometry to feature

* fix(project_routes): add missing slash at the beginning of the route

* fix(projectModel): geometryLog response type add

* fix(projectSlice): setGeometryLog reducer function add

* fix(IProject): project state types update

* fix(project): getGeometryLog service function add

* fix(projectDetailsV2): show highlighting rejected layer based on featureCollection instead of fgbURL and entity status mapping

* fix(themeSlice): update color

* feat(enums): add submission_status enums

* feat(submissionDetails): add submission status on card

* fix(ProjectDetailsV2): update stroke opacity and refactor status sync function

* fix(geometryLog): return geometry record id

* feat(submissionService): delete geometry record service add

* fix(project): update ts type

* fix(projectSlice): new state to store badGeomList

* fix(project): on geometry log error clear geometries

* fix(ProjectDetailsV2): handle missing layer for bad entities and update layer style application

* fix(updateReviewStatusModal): on submission approve or previously rejected entity, delete bad geometry record
  • Loading branch information
NSUWAL123 authored Jan 20, 2025
1 parent 75b8e16 commit 1d02cd4
Show file tree
Hide file tree
Showing 26 changed files with 441 additions and 76 deletions.
1 change: 1 addition & 0 deletions src/backend/app/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,7 @@ def slugify(name: Optional[str]) -> Optional[str]:
class DbGeometryLog(BaseModel):
"""Table geometry log."""

id: Optional[UUID] = None
geojson: dict
status: GeomStatus
project_id: Optional[int] = None
Expand Down
2 changes: 1 addition & 1 deletion src/backend/app/projects/project_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,7 +1310,7 @@ async def create_geom_log(


@router.get(
"{project_id}/geometry/records", response_model=list[project_schemas.GeometryLogIn]
"/{project_id}/geometry/records", response_model=list[project_schemas.GeometryLogIn]
)
async def read_geom_logs(
db: Annotated[Connection, Depends(db_conn)],
Expand Down
1 change: 1 addition & 0 deletions src/backend/app/projects/project_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
class GeometryLogIn(BaseModel):
"""Geometry log insert."""

id: Optional[UUID] = None
status: GeomStatus
geojson: dict
project_id: Optional[int] = None
Expand Down
21 changes: 20 additions & 1 deletion src/frontend/src/api/Project.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AxiosResponse } from 'axios';
import axios, { AxiosResponse } from 'axios';
import { ProjectActions } from '@/store/slices/ProjectSlice';
import { CommonActions } from '@/store/slices/CommonSlice';
import CoreModules from '@/shared/CoreModules';
import { task_state, task_event } from '@/types/enums';
import {
EntityOsmMap,
geometryLogResponseType,
projectDashboardDetailTypes,
projectInfoType,
projectTaskBoundriesType,
Expand Down Expand Up @@ -353,3 +354,21 @@ export const DownloadSubmissionGeojson = (url: string, projectName: string) => {
await downloadSubmissionGeojson(url);
};
};

export const GetGeometryLog = (url: string) => {
return async (dispatch: AppDispatch) => {
const getProjectActivity = async (url: string) => {
try {
dispatch(ProjectActions.SetGeometryLogLoading(true));
const response: AxiosResponse<geometryLogResponseType[]> = await axios.get(url);
dispatch(ProjectActions.SetGeometryLog(response.data));
} catch (error) {
// error means no geometry log present for the project
dispatch(ProjectActions.SetGeometryLog([]));
} finally {
dispatch(ProjectActions.SetGeometryLogLoading(false));
}
};
await getProjectActivity(url);
};
};
46 changes: 45 additions & 1 deletion src/frontend/src/api/SubmissionService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AxiosResponse } from 'axios';
import axios, { AxiosResponse } from 'axios';
import {
submissionContributorsTypes,
submissionFormFieldsTypes,
submissionTableDataTypes,
updateReviewStateType,
validatedMappedType,
geometryLogType,
} from '@/models/submission/submissionModel';
import CoreModules from '@/shared/CoreModules';
import { CommonActions } from '@/store/slices/CommonSlice';
Expand Down Expand Up @@ -107,3 +108,46 @@ export const MappedVsValidatedTaskService = (url: string) => {
await MappedVsValidatedTask(url);
};
};

// post bad and new geometries
export const PostGeometry = (url: string, payload: geometryLogType) => {
return async (dispatch: AppDispatch) => {
const postGeometry = async () => {
try {
await CoreModules.axios.post(url, payload);
} catch (error) {
dispatch(
CommonActions.SetSnackBar({
open: true,
message: 'Failed to post geometry.',
variant: 'error',
duration: 2000,
}),
);
}
};

await postGeometry();
};
};

export const DeleteGeometry = (url: string) => {
return async (dispatch: AppDispatch) => {
const deleteGeometry = async () => {
try {
await axios.delete(url);
} catch (error) {
dispatch(
CommonActions.SetSnackBar({
open: true,
message: 'Failed to delete geometry.',
variant: 'error',
duration: 2000,
}),
);
}
};

await deleteGeometry();
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const VectorLayer = ({
layerProperties,
rotation,
getAOIArea,
processGeojson,
}) => {
const [vectorLayer, setVectorLayer] = useState(null);
useEffect(() => {
Expand Down Expand Up @@ -185,8 +186,7 @@ const VectorLayer = ({

async function loadFgbRemote(filterExtent = true, extractGeomCol = true) {
this.clear();
const filteredFeatures = [];

let filteredFeatures = [];
for await (let feature of FGBGeoJson.deserialize(fgbUrl, fgbBoundingBox(fgbExtent.getExtent()))) {
if (extractGeomCol && feature.geometry.type === 'GeometryCollection') {
// Extract first geom from geomcollection
Expand All @@ -209,6 +209,11 @@ const VectorLayer = ({
filteredFeatures.push(extractGeom);
}
}
// Process Geojson if needed i.e. filter, modify, etc
// ex: in our use case we are filtering only rejected entities
if (processGeojson) {
filteredFeatures = processGeojson(filteredFeatures);
}
this.addFeatures(filteredFeatures);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ const TaskSelectionPopup = ({ taskId, body, feature }: TaskSelectionPopupPropTyp
<AssetModules.CloseIcon
style={{ width: '20px' }}
className="hover:fmtm-text-primaryRed"
onClick={() => dispatch(ProjectActions.ToggleTaskModalStatus(false))}
onClick={() => {
dispatch(ProjectActions.ToggleTaskModalStatus(false));
dispatch(CoreModules.TaskActions.SetSelectedTask(null));
}}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { SubmissionFormFieldsService, SubmissionTableService } from '@/api/Submi
import filterParams from '@/utilfunctions/filterParams';
import { camelToFlat } from '@/utilfunctions/commonUtils';
import useDocumentTitle from '@/utilfunctions/useDocumentTitle';
import { convertCoordinateStringToFeature } from '@/utilfunctions/extractGeojsonFromObject';

const SubmissionsTable = ({ toggleView }) => {
useDocumentTitle('Submission Table');
Expand Down Expand Up @@ -489,6 +490,9 @@ const SubmissionsTable = ({ toggleView }) => {
taskId: row?.task_id,
projectId: projectId,
reviewState: row?.__system?.reviewState,
entity_id: row?.feature,
label: row?.meta?.entity?.label,
feature: convertCoordinateStringToFeature(row?.xlocation),
taskUid: taskUid?.toString() || null,
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import React, { useEffect, useState } from 'react';
import { Modal } from '@/components/common/Modal';
import { SubmissionActions } from '@/store/slices/SubmissionSlice';
import { reviewListType } from '@/models/submission/submissionModel';
import { UpdateReviewStateService } from '@/api/SubmissionService';
import { DeleteGeometry, PostGeometry, UpdateReviewStateService } from '@/api/SubmissionService';
import TextArea from '../common/TextArea';
import Button from '../common/Button';
import { GetGeometryLog, PostProjectComments, UpdateEntityState } from '@/api/Project';
import { entity_state } from '@/types/enums';
import { useAppDispatch, useAppSelector } from '@/types/reduxTypes';
import { PostProjectComments } from '@/api/Project';
import { task_event } from '@/types/enums';
import { featureType } from '@/store/types/ISubmissions';

// Note these id values must be camelCase to match what ODK Central requires
const reviewList: reviewListType[] = [
Expand All @@ -23,12 +25,6 @@ const reviewList: reviewListType[] = [
className: 'fmtm-bg-[#E9DFCF] fmtm-text-[#D99F00] fmtm-border-[#D99F00]',
hoverClass: 'hover:fmtm-text-[#D99F00] hover:fmtm-border-[#D99F00]',
},
{
id: 'rejected',
title: 'Rejected',
className: 'fmtm-bg-[#E8D5D5] fmtm-text-[#D73F37] fmtm-border-[#D73F37]',
hoverClass: 'hover:fmtm-text-[#D73F37] hover:fmtm-border-[#D73F37]',
},
];

const UpdateReviewStatusModal = () => {
Expand All @@ -37,17 +33,26 @@ const UpdateReviewStatusModal = () => {
const [reviewStatus, setReviewStatus] = useState('');
const updateReviewStatusModal = useAppSelector((state) => state.submission.updateReviewStatusModal);
const updateReviewStateLoading = useAppSelector((state) => state.submission.updateReviewStateLoading);
const badGeomLogList = useAppSelector((state) => state?.project?.badGeomLogList);

useEffect(() => {
setReviewStatus(updateReviewStatusModal.reviewState);
}, [updateReviewStatusModal.reviewState]);

useEffect(() => {
if (!updateReviewStatusModal.projectId) return;
dispatch(
GetGeometryLog(`${import.meta.env.VITE_API_URL}/projects/${updateReviewStatusModal.projectId}/geometry/records`),
);
}, [updateReviewStatusModal.projectId]);

const handleStatusUpdate = async () => {
if (
!updateReviewStatusModal.instanceId ||
!updateReviewStatusModal.projectId ||
!updateReviewStatusModal.taskId ||
!updateReviewStatusModal?.taskUid
!updateReviewStatusModal.entity_id ||
!updateReviewStatusModal.taskUid
) {
return;
}
Expand All @@ -62,7 +67,55 @@ const UpdateReviewStatusModal = () => {
},
),
);

// post bad geometry if submission is marked as hasIssues
if (reviewStatus === 'hasIssues') {
const badFeature = {
...(updateReviewStatusModal.feature as featureType),
properties: {
entity_id: updateReviewStatusModal.entity_id,
task_id: updateReviewStatusModal.taskUid,
instance_id: updateReviewStatusModal.instanceId,
},
};

dispatch(
PostGeometry(
`${import.meta.env.VITE_API_URL}/projects/${updateReviewStatusModal.projectId}/geometry/records`,
{
status: 'BAD',
geojson: badFeature,
project_id: updateReviewStatusModal.projectId,
task_id: +updateReviewStatusModal.taskUid,
},
),
);
}

// delete bad geometry if the entity previously has rejected submission and current submission is marked as approved
if (reviewStatus === 'approved') {
const badGeomId = badGeomLogList.find(
(geom) => geom.geojson.properties.entity_id === updateReviewStatusModal.entity_id,
)?.id;
dispatch(
DeleteGeometry(
`${import.meta.env.VITE_API_URL}/projects/${updateReviewStatusModal.projectId}/geometry/records/${badGeomId}`,
),
);
}

dispatch(
UpdateEntityState(
`${import.meta.env.VITE_API_URL}/projects/${updateReviewStatusModal.projectId}/entity/status`,
{
entity_id: updateReviewStatusModal.entity_id,
status: reviewStatus === 'approved' ? entity_state['SURVEY_SUBMITTED'] : entity_state['MARKED_BAD'],
label: updateReviewStatusModal.label,
},
),
);
}

if (noteComments.trim().length > 0) {
dispatch(
PostProjectComments(
Expand All @@ -84,6 +137,9 @@ const UpdateReviewStatusModal = () => {
taskId: null,
reviewState: '',
taskUid: null,
entity_id: null,
label: null,
feature: null,
}),
);
dispatch(SubmissionActions.UpdateReviewStateLoading(false));
Expand All @@ -96,11 +152,11 @@ const UpdateReviewStatusModal = () => {
<h2 className="!fmtm-text-lg fmtm-font-archivo fmtm-tracking-wide">Update Review Status</h2>
</div>
}
className="!fmtm-w-fit !fmtm-outline-none fmtm-rounded-xl"
className="!fmtm-w-[23rem] !fmtm-outline-none fmtm-rounded-xl"
description={
<div className="fmtm-mt-9">
<div className="fmtm-mb-4">
<div className="fmtm-flex fmtm-justify-between fmtm-gap-2">
<div className="fmtm-flex fmtm-gap-2">
{reviewList.map((reviewBtn) => (
<button
key={reviewBtn.id}
Expand Down Expand Up @@ -136,6 +192,9 @@ const UpdateReviewStatusModal = () => {
taskId: null,
reviewState: '',
taskUid: null,
entity_id: null,
label: null,
feature: null,
}),
);
}}
Expand All @@ -162,6 +221,9 @@ const UpdateReviewStatusModal = () => {
taskId: null,
reviewState: '',
taskUid: null,
entity_id: null,
label: null,
feature: null,
}),
);
}}
Expand Down
15 changes: 15 additions & 0 deletions src/frontend/src/models/project/projectModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,18 @@ export type EntityOsmMap = {
updated_at: string;
submission_ids: string | null;
};

export type geometryLogResponseType = {
id: string;
status: 'BAD' | 'NEW';
geojson: {
type: 'Feature';
geometry: {
type: string;
coordinates: number[][][];
};
properties: Record<string, any>;
};
project_id: number;
task_id: number;
};
18 changes: 17 additions & 1 deletion src/frontend/src/models/submission/submissionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,23 @@ export type reviewListType = {
};

export type formSubmissionType = { date: string; count: number; label: string };
export type validatedMappedType = { date: string; validated: number; mapped: number };
export type validatedMappedType = { date: string; Validated: number; Mapped: number; label: string };

type featureType = {
type: 'Feature';
geometry: Partial<{
type: string;
coordinates: any[];
}>;
properties: Record<string, any>;
};

export type geometryLogType = {
status: 'NEW' | 'BAD';
geojson: featureType;
project_id: number;
task_id: number;
};

export type updateReviewStateType = {
instanceId: string;
Expand Down
Loading

0 comments on commit 1d02cd4

Please sign in to comment.