Skip to content

Commit

Permalink
feat: mark tasks validated + start conflation UI (#1743)
Browse files Browse the repository at this point in the history
* fix(dialogTaskActions): on lock task for validation, redirect to submission table with filtered taskId

* fix(projectTaskStatus): syle, existingData, feature prop optional

* fix(projectDetails): reorder updateTaskStatus arguments

* fix(submissionTable): btn add to update task as mapped

* fix(dialogTaskActions): if validated show merge with OSM btn & redirect to conflation page on click

* fix(dataConflation): data conflation component and route setup

* feat(taskInfo): conflation task section slicing

* feat(conflationMap): map & legend setup

* fix(button): outline remove from button

* feat(dataConflation): merge OSM modal, taskInfo, conflationMap section add

* fix(taskInfo): style fix

* feat(dataConflation): submissionConflation component add, UI style fix

* feat(submissionConflation): component UI slicing

* fix(modal): show dialogTrigger only if dialogOpen prop present

* fix(dataConflation): UI fix

* feat(mergeAttributes): UI slicing for merge attributes modal

* feat(submissionConflation): add mergeAttributes component

* fix(customTable): remove hover effect & add row bg distinguishable

* feat(dataConflation): skeleton loader add

* fix(mergeAttributes): preventDefault and stopPropagation add to onClick event

* feat(dataConflation): store, slice add

* feat(dataConflation): submissionConflationGeojson service add

* feat(dataConflation): submissionConflationGeojson service fetch

* fix(dataConflation): UI style fix

* feat(conflationMap): display submissionConflation on map

* feat(conflationMap): on map feature click dispatch action to set selectedFeatureOSMId

* feat(submissionConflation): replace actual data with dummy data

* feat(mergeAttributes): replace dummy data with actual data

* feat(dataConflation): setSelectedFeatureOSMId action add

* fix(dataConflation): UI style fix according to feature click

* fix(conflationMap): update accessor key

* fix(dataConflation): style fix

* fix(submissionConflation): remove commented code

* feat(taskInfo): task feature, conflict counts calculation

* fix(dataConflation): remove modal

* fix(dataConflation): loading state add

* fix(mergeAttributes): title add, hover effects add

* fix(updateTaskStatus): update prop type

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(submissionsTable): replace empty string with null

* fix(dialogTaskActions): hide merge data with OSM button

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
NSUWAL123 and pre-commit-ci[bot] committed Aug 29, 2024
1 parent e6c465c commit 9729f53
Show file tree
Hide file tree
Showing 19 changed files with 749 additions and 67 deletions.
19 changes: 19 additions & 0 deletions src/frontend/src/api/DataConflation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import axios from 'axios';
import { DataConflationActions } from '@/store/slices/DataConflationSlice';

export const SubmissionConflationGeojsonService: Function = (url: string) => {
return async (dispatch) => {
const getSubmissionGeojsonConflation = async (url) => {
try {
dispatch(DataConflationActions.SetSubmissionConflationGeojsonLoading(true));
const getSubmissionConflationGeojsonResponse = await axios.get(url);
dispatch(DataConflationActions.SetSubmissionConflationGeojson(getSubmissionConflationGeojsonResponse.data));
dispatch(DataConflationActions.SetSubmissionConflationGeojsonLoading(false));
} catch (error) {
dispatch(DataConflationActions.SetSubmissionConflationGeojsonLoading(false));
}
};

await getSubmissionGeojsonConflation(url);
};
};
54 changes: 30 additions & 24 deletions src/frontend/src/api/ProjectTaskStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,49 @@ import CoreModules from '@/shared/CoreModules';
import { CommonActions } from '@/store/slices/CommonSlice';
import { projectTaskBoundriesType } from '@/models/project/projectModel';

const UpdateTaskStatus = (
export const UpdateTaskStatus = (
url: string,
style: any,
existingData: projectTaskBoundriesType[],
currentProjectId: string,
feature: Record<string, any>,
taskId: number,
taskId: string,
body: any,
params: { project_id: string },
style?: any,
existingData?: projectTaskBoundriesType[],
feature?: Record<string, any>,
) => {
return async (dispatch) => {
const updateTask = async (url: string, body: any, feature: Record<string, any>, params: { project_id: string }) => {
const updateTask = async (
url: string,
body: any,
params: { project_id: string },
feature?: Record<string, any>,
) => {
try {
dispatch(CommonActions.SetLoading(true));

const response = await CoreModules.axios.post(url, body, { params });
dispatch(ProjectActions.UpdateProjectTaskActivity(response.data));

await feature.setStyle(style);
if (feature && style) {
await feature.setStyle(style);

// assign userId to locked_by_user if status is locked_for_mapping or locked_for_validation
const prevProperties = feature.getProperties();
const isTaskLocked = ['LOCKED_FOR_MAPPING', 'LOCKED_FOR_VALIDATION'].includes(response.data.status);
const updatedProperties = { ...prevProperties, locked_by_user: isTaskLocked ? body.id : null };
feature.setProperties(updatedProperties);
// assign userId to locked_by_user if status is locked_for_mapping or locked_for_validation
const prevProperties = feature.getProperties();
const isTaskLocked = ['LOCKED_FOR_MAPPING', 'LOCKED_FOR_VALIDATION'].includes(response.data.status);
const updatedProperties = { ...prevProperties, locked_by_user: isTaskLocked ? body.id : null };
feature.setProperties(updatedProperties);

dispatch(
ProjectActions.UpdateProjectTaskBoundries({
projectId: currentProjectId,
taskId,
locked_by_uid: body?.id,
locked_by_username: body?.username,
task_status: response.data.status,
}),
);
}

dispatch(
ProjectActions.UpdateProjectTaskBoundries({
projectId: currentProjectId,
taskId,
locked_by_uid: body?.id,
locked_by_username: body?.username,
task_status: response.data.status,
}),
);
dispatch(CommonActions.SetLoading(false));
dispatch(
HomeActions.SetSnackBar({
Expand All @@ -60,8 +68,6 @@ const UpdateTaskStatus = (
);
}
};
await updateTask(url, body, feature, params);
await updateTask(url, body, params, feature);
};
};

export default UpdateTaskStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import Accordion from '@/components/common/Accordion';
import useOutsideClick from '@/hooks/useOutsideClick';

const legendArray = [
{ name: 'No conflicts', color: '#40AC8C' },
{ name: 'Tag conflicts', color: '#DB9D35' },
{ name: 'Geometry conflicts', color: '#3C4A5E' },
{ name: 'Both conflicts', color: '#EB8B8B' },
];

const MapLegends = () => {
return (
<div className="fmtm-flex fmtm-flex-col fmtm-gap-1 fmtm-pb-2">
{legendArray?.map((legend) => (
<div className="fmtm-flex fmtm-items-center fmtm-gap-2">
<div
style={{ backgroundColor: legend.color }}
className={`fmtm-w-[0.875rem] fmtm-h-[0.875rem] fmtm-min-w-[0.875rem] fmtm-min-h-[0.875rem]`}
></div>
<p className="fmtm-text-xs fmtm-text-[#484848]">{legend.name}</p>
</div>
))}
</div>
);
};

const MapLegend = () => {
const [legendRef, legendToggle, handleLegendToggle] = useOutsideClick();

return (
<>
<Accordion
hasSeperator={false}
ref={legendRef}
body={<MapLegends />}
header={
<div className="fmtm-flex fmtm-items-center fmtm-gap-1 sm:fmtm-gap-2">
<p className="fmtm-text-base fmtm-font-bold">Legend</p>
</div>
}
onToggle={() => {
handleLegendToggle();
}}
className="fmtm-py-0 !fmtm-pb-0 fmtm-rounded-lg hover:fmtm-bg-gray-50"
collapsed={!legendToggle}
/>
</>
);
};

export default MapLegend;
68 changes: 68 additions & 0 deletions src/frontend/src/components/DataConflation/ConflationMap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { MapContainer as MapComponent, useOLMap } from '@/components/MapComponent/OpenLayersComponent';
import LayerSwitcherControl from '@/components/MapComponent/OpenLayersComponent/LayerSwitcher/index';
import MapLegend from '@/components/DataConflation/ConflationMap/MapLegend';
import Button from '@/components/common/Button';
import { useAppSelector } from '@/types/reduxTypes';
import { VectorLayer } from '@/components/MapComponent/OpenLayersComponent/Layers';
import { useDispatch } from 'react-redux';
import { DataConflationActions } from '@/store/slices/DataConflationSlice';

const ConflationMap = () => {
const dispatch = useDispatch();

const submissionConflationGeojson = useAppSelector((state) => state.dataconflation.submissionConflationGeojson);
const submissionConflationGeojsonLoading = useAppSelector(
(state) => state.dataconflation.submissionConflationGeojsonLoading,
);

const { mapRef, map } = useOLMap({
center: [0, 0],
zoom: 4,
});

return (
<>
<MapComponent
ref={mapRef}
mapInstance={map}
className="map naxatw-relative naxatw-min-h-full !fmtm-h-full fmtm-w-[200px] fmtm-rounded-lg fmtm-overflow-hidden"
>
<VectorLayer
geojson={submissionConflationGeojson}
viewProperties={{
size: map?.getSize(),
padding: [50, 50, 50, 50],
constrainResolution: true,
duration: 2000,
}}
zoomToLayer
mapOnClick={(properties, feature) => {
dispatch(
DataConflationActions.SetSelectedFeatureOSMId(
feature?.getProperties()?.xid
? feature.getProperties().xid
: feature.getProperties()?.osm_id.toString(),
),
);
}}
/>
<LayerSwitcherControl visible="osm" />
<div className="fmtm-absolute fmtm-bottom-20 sm:fmtm-bottom-3 fmtm-left-3 fmtm-z-50 fmtm-rounded-lg">
<MapLegend />
</div>
<div className="fmtm-absolute fmtm-bottom-20 sm:fmtm-top-3 fmtm-right-3 fmtm-z-50 fmtm-rounded-lg fmtm-h-fit">
<Button
btnText="Upload to OSM"
type="button"
btnType="primary"
onClick={() => {}}
disabled={submissionConflationGeojsonLoading}
/>
</div>
</MapComponent>
</>
);
};

export default ConflationMap;
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useState } from 'react';
import { Modal } from '@/components/common/Modal';
import Button from '@/components/common/Button';
import Table, { TableHeader } from '@/components/common/CustomTable';

type mergeAttributesPropType = {
selectedConflateMethod: 'submission_feature' | 'osm_feature' | 'merge_attributes' | '';
setSelectedConflateMethod: (value: '') => void;
submissionTags: Record<string, any>;
osmTags: Record<string, any>;
};

const MergeAttributes = ({
selectedConflateMethod,
setSelectedConflateMethod,
submissionTags,
osmTags,
}: mergeAttributesPropType) => {
const [chosenAttribute, setChosenAttribute] = useState({});

const tableData: any = [];
for (const [key, value] of Object.entries(osmTags)) {
if (submissionTags?.[key] && submissionTags?.[key] !== value) {
tableData.push({ name: key, osm: value, submission: submissionTags?.[key] });
}
}

return (
<>
<Modal
title={<p className="fmtm-text-left">Merge Data With OSM</p>}
description={
<div className="fmtm-mt-1">
<div>
<Table
flag="primarytable"
style={{ maxHeight: '60vh', width: '100%' }}
data={tableData}
onRowClick={() => {}}
isLoading={false}
>
<TableHeader
dataField="Feature Name"
headerClassName="featureHeader"
rowClassName="featureRow"
dataFormat={(row) => <div title={row?.name}>{row?.name}</div>}
/>
<TableHeader
dataField="OSM Tags"
headerClassName="osmHeader"
rowClassName="osmRow !fmtm-p-0"
dataFormat={(row) => (
<div
className={`fmtm-flex fmtm-items-center fmtm-justify-between !fmtm-h-full fmtm-absolute fmtm-top-0 fmtm-left-0 fmtm-w-full fmtm-p-3 ${
chosenAttribute[row?.name] === row?.osm
? 'fmtm-bg-[#9FD5C5]'
: 'hover:fmtm-bg-gray-100 fmtm-duration-200'
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setChosenAttribute((prev) => ({ ...prev, [row?.name]: row?.osm }));
}}
title={row?.osm}
>
<label
htmlFor={row?.name}
className={`fmtm-text-base ${
chosenAttribute[row?.name] === row?.osm ? 'fmtm-text-white' : 'fmtm-text-gray-500'
} fmtm-mb-[2px] fmtm-cursor-pointer fmtm-flex fmtm-items-center fmtm-gap-2`}
>
<p>{row?.osm}</p>
</label>
<input
type="radio"
id={row?.name}
name={row?.name}
value={row?.osm}
className={`fmtm-accent-[#5BAD8C] fmtm-cursor-pointer fmtm-bg-white`}
checked={chosenAttribute[row?.name] === row?.osm}
/>
</div>
)}
/>
<TableHeader
dataField="Submission #457"
headerClassName="submissionHeader"
rowClassName="submissionRow !fmtm-p-0"
dataFormat={(row) => (
<div
className={`fmtm-flex fmtm-items-center fmtm-justify-between !fmtm-h-full fmtm-absolute fmtm-top-0 fmtm-left-0 fmtm-w-full fmtm-p-3 ${
chosenAttribute[row?.name] === row?.submission
? 'fmtm-bg-[#9FD5C5]'
: 'hover:fmtm-bg-gray-100 fmtm-duration-200'
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setChosenAttribute((prev) => ({ ...prev, [row?.name]: row?.submission }));
}}
title={row?.submission}
>
<label
htmlFor={row?.name}
className={`fmtm-text-base ${
chosenAttribute[row?.name] === row?.submission ? 'fmtm-text-white' : 'fmtm-text-gray-500'
} fmtm-mb-[2px] fmtm-cursor-pointer fmtm-flex fmtm-items-center fmtm-gap-2`}
>
<p>{row?.submission}</p>
</label>
<input
type="radio"
id={row?.name}
name={row?.name}
value={row?.submission}
className={`fmtm-accent-[#5BAD8C] fmtm-cursor-pointer`}
checked={chosenAttribute[row?.name] === row?.submission}
/>
</div>
)}
/>
</Table>
</div>
<div className="fmtm-flex fmtm-gap-[0.625rem] fmtm-justify-center fmtm-mt-5">
<Button
btnText="Save"
btnType="primary"
onClick={() => {
setSelectedConflateMethod('');
}}
type="button"
className="!fmtm-rounded fmtm-text-sm !fmtm-py-2 fmtm-font-bold"
/>
</div>
</div>
}
open={selectedConflateMethod === 'merge_attributes'}
onOpenChange={() => setSelectedConflateMethod('')}
className="fmtm-max-w-[70rem] fmtm-rounded-xl"
/>
</>
);
};

export default MergeAttributes;
Loading

0 comments on commit 9729f53

Please sign in to comment.