Skip to content

Commit a3c6f61

Browse files
Option to delete newly created entity (#2506)
* feat(messages): translations add * refactor(+page): pass user_sub as key to pass user identifier for who created the new geom * feat(messages): traslations add * feat(dialog-entities-actions): workflow to delete newly created entities * feat(entities): delete new entity api add * refactor(dialog-entities-actions): style add * feat: translations * feat(dialog-entities-actions): only allow entity creator to delete the entity * refactor(entities): if error, show toast * refactor(dialog-entities-actions): allow ready and opened_in_odk to be deleted * feat(featureSelectionPopup): option for pm to delete newly created entity via project details page * feat(project): delete geom api service add * refactor(projectSlice): add geom_id on new geom geojson properties * refactor(project): refactor delete entity api func * refactor(projectDetails): fix styles * feat(featureSelectionPopup): entity delete modal * feat(project): entity delete state * fix(projectDetails): add setSelectedTaskFeature prop * feat(+page): pass user_sub as entity feature property * feat(messages): translations add * refactor(+page): pass user_sub as key to pass user identifier for who created the new geom * feat(messages): traslations add * feat(dialog-entities-actions): workflow to delete newly created entities * feat(entities): delete new entity api add * refactor(dialog-entities-actions): style add * feat: translations * feat(dialog-entities-actions): only allow entity creator to delete the entity * refactor(entities): if error, show toast * refactor(dialog-entities-actions): allow ready and opened_in_odk to be deleted * feat(featureSelectionPopup): option for pm to delete newly created entity via project details page * feat(project): delete geom api service add * refactor(projectDetails): fix styles * feat(featureSelectionPopup): entity delete modal * feat(project): entity delete state * fix(projectDetails): add setSelectedTaskFeature prop * feat(+page): pass user_sub as entity feature property * refactor: replace is_new with created_by entity field in code * fix(mapper): minor tweaks after rebase * build: add migration for entities is_new --> created_by field * refactor(entities): update delete new entities api * refactor(dialog-entities-actions): remove geom_id, update user_sub key to created_by * fix(+page): remove duplicate loginStore initialization * refactor(entities): manually sync status after entity deletion --------- Co-authored-by: spwoodcock <[email protected]>
1 parent 003e2e5 commit a3c6f61

File tree

27 files changed

+623
-163
lines changed

27 files changed

+623
-163
lines changed

src/backend/app/central/central_crud.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,8 @@ async def get_entities_data(
739739
odk_id: int,
740740
dataset_name: str = "features",
741741
fields: str = (
742-
"__system/updatedAt, osm_id, status, task_id, submission_ids, is_new, geometry"
742+
"__system/updatedAt, osm_id, status, task_id, submission_ids, "
743+
"geometry, created_by"
743744
),
744745
filter_date: Optional[datetime] = None,
745746
) -> list:

src/backend/app/central/central_schemas.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class NameTypeMapping:
131131
NameTypeMapping(name="timestamp", type="datetime"),
132132
NameTypeMapping(name="status", type="string"),
133133
NameTypeMapping(name="submission_ids", type="string"),
134-
NameTypeMapping(name="is_new", type="string"),
134+
NameTypeMapping(name="created_by", type="string"),
135135
]
136136

137137
RESERVED_KEYS = {
@@ -198,7 +198,7 @@ class EntityProperties(BaseModel):
198198
changeset: Optional[str] = None
199199
timestamp: Optional[str] = None
200200
status: Optional[str] = None
201-
is_new: Optional[str] = None
201+
created_by: Optional[str] = None
202202

203203
@computed_field
204204
@property
@@ -262,8 +262,8 @@ class EntityMappingStatus(EntityOsmID, EntityTaskID):
262262
updatedAt: Optional[str | datetime] = Field(exclude=True) # noqa: N815
263263
status: Optional[EntityState] = None
264264
submission_ids: Optional[str] = None
265-
is_new: Optional[bool] = None
266265
geometry: Optional[str] = None
266+
created_by: Optional[str] = None
267267

268268
@computed_field
269269
@property
@@ -276,14 +276,6 @@ def updated_at(self) -> Optional[str | datetime]:
276276
)
277277
return self.updatedAt
278278

279-
@field_validator("is_new", mode="before")
280-
@classmethod
281-
def emoji_to_bool(cls, value: str, info: ValidationInfo) -> bool:
282-
"""Convert ✅ emoji to True values."""
283-
if value == "✅":
284-
return True
285-
return False
286-
287279

288280
class EntityMappingStatusIn(BaseModel):
289281
"""Update the mapping status for an Entity."""

src/backend/app/db/models.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,8 +2019,11 @@ class DbOdkEntities(BaseModel):
20192019
task_id: Optional[int]
20202020
osm_id: int
20212021
submission_ids: str
2022-
is_new: bool
2022+
# NOTE geometry is only set if the geom is 'new' or 'bad'
20232023
geometry: str
2024+
# NOTE previous we had field is_new, replaced by created_by
2025+
# NOTE as is_new is implicitly true if created_by is set
2026+
created_by: str
20242027

20252028
@classmethod
20262029
async def upsert(
@@ -2053,7 +2056,7 @@ async def upsert(
20532056
sql = """
20542057
INSERT INTO public.odk_entities
20552058
(entity_id, status, project_id, task_id,
2056-
osm_id, submission_ids, is_new, geometry)
2059+
osm_id, submission_ids, geometry, created_by)
20572060
VALUES
20582061
"""
20592062

@@ -2069,8 +2072,8 @@ async def upsert(
20692072
f"%({entity_index}_task_id)s, "
20702073
f"%({entity_index}_osm_id)s, "
20712074
f"%({entity_index}_submission_ids)s, "
2072-
f"%({entity_index}_is_new)s, "
2073-
f"%({entity_index}_geometry)s)"
2075+
f"%({entity_index}_geometry)s, "
2076+
f"%({entity_index}_created_by)s)"
20742077
)
20752078
data[f"{entity_index}_entity_id"] = entity["id"]
20762079
data[f"{entity_index}_status"] = EntityState(int(entity["status"])).name
@@ -2081,15 +2084,15 @@ async def upsert(
20812084
)
20822085
data[f"{entity_index}_osm_id"] = entity["osm_id"]
20832086
data[f"{entity_index}_submission_ids"] = entity["submission_ids"]
2084-
data[f"{entity_index}_is_new"] = (
2085-
True if entity["is_new"] == "✅" else False
2087+
data[f"{entity_index}_osm_id"] = entity["osm_id"]
2088+
# Only copy geometry if new geom (created_by), or marked bad
2089+
should_include_geom = (
2090+
entity["status"] == "6" or entity["created_by"] != ""
20862091
)
2087-
# Only copy geometry if new geom, or marked bad
20882092
data[f"{entity_index}_geometry"] = (
2089-
entity["geometry"]
2090-
if entity["status"] == "6" or entity["is_new"] == "✅"
2091-
else ""
2093+
entity["geometry"] if should_include_geom else ""
20922094
)
2095+
data[f"{entity_index}_created_by"] = entity["created_by"]
20932096

20942097
sql += (
20952098
", ".join(values)
@@ -2099,8 +2102,8 @@ async def upsert(
20992102
task_id = EXCLUDED.task_id,
21002103
osm_id = EXCLUDED.osm_id,
21012104
submission_ids = EXCLUDED.submission_ids,
2102-
is_new = EXCLUDED.is_new,
2103-
geometry = EXCLUDED.geometry
2105+
geometry = EXCLUDED.geometry,
2106+
created_by = EXCLUDED.created_by
21042107
RETURNING True;
21052108
"""
21062109
)

src/backend/app/db/postgis_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ def add_required_geojson_properties(
340340
or properties.get("fid") # fid is typical from tools like QGIS
341341
# Random id
342342
# NOTE 32-bit int is max supported by standard postgres Integer
343-
# 0 to 1073741823
343+
# 0 to 1073741823 (collision chance is extremely low for ≤20k entities)
344344
or getrandbits(30)
345345
)
346346

@@ -361,7 +361,7 @@ def add_required_geojson_properties(
361361
properties.setdefault("changeset", 1)
362362
properties.setdefault("timestamp", str(current_date))
363363
properties.setdefault("submission_ids", "")
364-
properties.setdefault("is_new", "")
364+
properties.setdefault("created_by", "")
365365

366366
feature["properties"] = properties
367367

src/backend/app/projects/project_crud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ async def generate_project_files(
476476
if first_feature and "properties" in first_feature: # Check if properties exist
477477
# FIXME perhaps this should be done in the SQL code?
478478
entity_properties = list(first_feature["properties"].keys())
479-
for field in ["submission_ids", "is_new"]:
479+
for field in ["submission_ids", "created_by"]:
480480
if field not in entity_properties:
481481
entity_properties.append(field)
482482

src/backend/packages/osm-fieldwork/osm_fieldwork/form_components/mandatory_fields.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ def _get_mandatory_fields(
191191
"save_to": "submission_ids",
192192
},
193193
# FIXME probably add logic to take `new_feature` field
194-
# and set the is_new entity property if not null?
194+
# and set the created_by entity property to the injected
195+
# username field?
195196
])
196197
if need_verification_fields:
197198
fields.append(add_label_translations({

src/frontend/src/api/Project.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ import { TaskActions } from '@/store/slices/TaskSlice';
1414
import { AppDispatch } from '@/store/Store';
1515
import { featureType } from '@/store/types/IProject';
1616

17+
const VITE_API_URL = import.meta.env.VITE_API_URL;
18+
1719
export const ProjectById = (projectId: string) => {
1820
return async (dispatch: AppDispatch) => {
1921
const fetchProjectById = async (projectId: string) => {
2022
try {
2123
dispatch(ProjectActions.SetProjectDetialsLoading(true));
22-
const project = await CoreModules.axios.get(
23-
`${import.meta.env.VITE_API_URL}/projects/${projectId}?project_id=${projectId}`,
24-
);
24+
const project = await CoreModules.axios.get(`${VITE_API_URL}/projects/${projectId}?project_id=${projectId}`);
2525
const projectResp: projectInfoType = project.data;
2626
const persistingValues = projectResp.tasks.map((data) => {
2727
return {
@@ -162,7 +162,7 @@ export const GenerateProjectTiles = (url: string, projectId: string, data: objec
162162
const generateProjectTiles = async (url: string, projectId: string) => {
163163
try {
164164
await CoreModules.axios.post(url, data);
165-
dispatch(GetTilesList(`${import.meta.env.VITE_API_URL}/projects/${projectId}/tiles`));
165+
dispatch(GetTilesList(`${VITE_API_URL}/projects/${projectId}/tiles`));
166166
dispatch(ProjectActions.SetGenerateProjectTilesLoading(false));
167167
} catch (error) {
168168
dispatch(ProjectActions.SetGenerateProjectTilesLoading(false));
@@ -317,6 +317,45 @@ export const UpdateEntityState = (url: string, payload: { entity_id: string; sta
317317
};
318318
};
319319

320+
export const GetGeometryLog = (url: string) => {
321+
return async (dispatch: AppDispatch) => {
322+
const getProjectActivity = async (url: string) => {
323+
try {
324+
dispatch(ProjectActions.SetGeometryLogLoading(true));
325+
const response: AxiosResponse<geometryLogResponseType[]> = await axios.get(url);
326+
dispatch(ProjectActions.SetGeometryLog(response.data));
327+
} catch (error) {
328+
// error means no geometry log present for the project
329+
dispatch(ProjectActions.SetGeometryLog([]));
330+
} finally {
331+
dispatch(ProjectActions.SetGeometryLogLoading(false));
332+
}
333+
};
334+
await getProjectActivity(url);
335+
};
336+
};
337+
338+
export const DeleteEntity = (url: string, project_id: number, entity_id: string) => {
339+
return async (dispatch: AppDispatch) => {
340+
const deleteEntity = async () => {
341+
try {
342+
dispatch(ProjectActions.SetIsEntityDeleting({ [entity_id]: true }));
343+
await axios.delete(url, { params: { project_id } });
344+
dispatch(ProjectActions.RemoveNewEntity(entity_id));
345+
} catch (error) {
346+
dispatch(
347+
CommonActions.SetSnackBar({
348+
message: error?.response?.data?.detail || 'Failed to delete entity',
349+
}),
350+
);
351+
} finally {
352+
dispatch(ProjectActions.SetIsEntityDeleting({ [entity_id]: false }));
353+
}
354+
};
355+
await deleteEntity();
356+
};
357+
};
358+
320359
export const SyncTaskState = (
321360
url: string,
322361
params: { project_id: string },

src/frontend/src/components/MapLegends.tsx

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const MapLegends = ({ defaultTheme }: { defaultTheme: any }) => {
7070
};
7171

7272
return (
73-
<div className="fmtm-absolute fmtm-bottom-24 md:fmtm-bottom-10 fmtm-left-3 fmtm-z-50">
73+
<div className="fmtm-absolute fmtm-bottom-24 md:fmtm-bottom-10 fmtm-left-3 fmtm-z-[45]">
7474
<DropdownMenu modal={false} open={toggleLegend}>
7575
<DropdownMenuTrigger className="fmtm-outline-none" onClick={() => setToggleLegend(true)}>
7676
<Tooltip title="Legend" placement="right" arrow>
@@ -81,28 +81,26 @@ const MapLegends = ({ defaultTheme }: { defaultTheme: any }) => {
8181
</div>
8282
</Tooltip>
8383
</DropdownMenuTrigger>
84-
<DropdownMenuPortal>
85-
<DropdownMenuContent
86-
className="fmtm-border-none fmtm-z-[60] fmtm-bg-white fmtm-p-2"
87-
align="start"
88-
sideOffset={-25}
89-
>
90-
<div className="fmtm-flex fmtm-items-center fmtm-justify-between">
91-
<p className="fmtm-body-sm-semibold fmtm-mb-2">Legend</p>
92-
<div
93-
className="fmtm-p-1 hover:fmtm-bg-grey-200 fmtm-rounded-full fmtm-w-4 fmtm-h-4 fmtm-flex fmtm-items-center fmtm-justify-center fmtm-duration-200"
94-
onClick={() => setToggleLegend(false)}
95-
>
96-
<AssetModules.ExpandMoreIcon className="!fmtm-text-sm fmtm-cursor-pointer" />
97-
</div>
98-
</div>
99-
<div className="fmtm-flex fmtm-flex-col fmtm-gap-1">
100-
{MapDetails.map((data, index) => {
101-
return <LegendListItem data={data} key={index} />;
102-
})}
84+
<DropdownMenuContent
85+
className="fmtm-border-none fmtm-bg-white fmtm-p-2 fmtm-z-[45]"
86+
align="start"
87+
sideOffset={-25}
88+
>
89+
<div className="fmtm-flex fmtm-items-center fmtm-justify-between">
90+
<p className="fmtm-body-sm-semibold fmtm-mb-2">Legend</p>
91+
<div
92+
className="fmtm-p-1 hover:fmtm-bg-grey-200 fmtm-rounded-full fmtm-w-4 fmtm-h-4 fmtm-flex fmtm-items-center fmtm-justify-center fmtm-duration-200"
93+
onClick={() => setToggleLegend(false)}
94+
>
95+
<AssetModules.ExpandMoreIcon className="!fmtm-text-sm fmtm-cursor-pointer" />
10396
</div>
104-
</DropdownMenuContent>
105-
</DropdownMenuPortal>
97+
</div>
98+
<div className="fmtm-flex fmtm-flex-col fmtm-gap-1">
99+
{MapDetails.map((data, index) => {
100+
return <LegendListItem data={data} key={index} />;
101+
})}
102+
</div>
103+
</DropdownMenuContent>
106104
</DropdownMenu>
107105
</div>
108106
);

0 commit comments

Comments
 (0)