From a6d42190188c12b4ab54837f4ae4ba9f258cc50d Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 15 Jan 2025 11:57:45 +0000 Subject: [PATCH 01/24] build: update osm-fieldwork --> 0.18.1 for portuguese xlsform translations --- src/backend/pyproject.toml | 2 +- src/backend/uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index c477306e0..fd03becff 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "sozipfile==0.3.2", "cryptography==43.0.3", "pyjwt==2.9.0", - "osm-fieldwork==0.18.0", + "osm-fieldwork==0.18.1", "osm-login-python==2.0.0", "osm-rawdata==0.4.1", "fmtm-splitter==2.0.0", diff --git a/src/backend/uv.lock b/src/backend/uv.lock index 6b8990200..a5caf865d 100644 --- a/src/backend/uv.lock +++ b/src/backend/uv.lock @@ -568,7 +568,7 @@ requires-dist = [ { name = "httptools", specifier = "==0.6.4" }, { name = "loguru", specifier = "==0.7.2" }, { name = "minio", specifier = "==7.2.9" }, - { name = "osm-fieldwork", specifier = "==0.18.0" }, + { name = "osm-fieldwork", specifier = "==0.18.1" }, { name = "osm-login-python", specifier = "==2.0.0" }, { name = "osm-rawdata", specifier = "==0.4.1" }, { name = "psycopg", extras = ["pool"], specifier = ">=3.2.3" }, @@ -1435,7 +1435,7 @@ wheels = [ [[package]] name = "osm-fieldwork" -version = "0.18.0" +version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1459,9 +1459,9 @@ dependencies = [ { name = "shapely" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/60/a43713da67c39b47460d1d9c598324e71a59c241bd54b69fce6de4168307/osm-fieldwork-0.18.0.tar.gz", hash = "sha256:887fb94b5796bc14a2f8b6ebe0391bb7795bb685f6d9ddee6c9458072d133252", size = 1485366 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/97/aebc77a1399ac391b9852215ed2850d24aeadb01431c0db8549681c56e1a/osm-fieldwork-0.18.1.tar.gz", hash = "sha256:490cc5b72e2fb46fc7f4221f58e197a3d3007e3e8e69ca57ca2283eb06fe98f1", size = 1485728 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/58/8ed93d23d6f74ec92aa4326faef186f983a368aa9f028ad47d4ee6f4972b/osm_fieldwork-0.18.0-py3-none-any.whl", hash = "sha256:bde9c8c4cca9658d100e5993e986cae84b5060f71134182595a4e1896f8a7437", size = 1511469 }, + { url = "https://files.pythonhosted.org/packages/f4/9f/4c4c06ae89d24671d102697aea5ee0e1fef34000247dd82479003e01456b/osm_fieldwork-0.18.1-py3-none-any.whl", hash = "sha256:2454a7786324e511a80934345cf6993ea993b7991b37dd489650ecf128c6821a", size = 1511857 }, ] [[package]] From e5dc618e01a8329aab71066bf15f5fc172395193 Mon Sep 17 00:00:00 2001 From: Sam <78538841+spwoodcock@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:21:14 +0000 Subject: [PATCH 02/24] build(frontend): update pnpm version to latest stable 9.15.4 (#2092) --- src/Dockerfile.ui.debug | 2 +- src/Dockerfile.ui.prod | 2 +- src/frontend/package.json | 2 +- src/mapper/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dockerfile.ui.debug b/src/Dockerfile.ui.debug index c01d3698a..fb7bae09e 100755 --- a/src/Dockerfile.ui.debug +++ b/src/Dockerfile.ui.debug @@ -9,6 +9,6 @@ WORKDIR /app COPY --from=code ./package.json ./pnpm-lock.yaml ./ ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable && corepack use pnpm@9.3.0 +RUN corepack enable && corepack use pnpm@9.15.4 RUN pnpm install ENTRYPOINT ["pnpm", "run", "dev"] diff --git a/src/Dockerfile.ui.prod b/src/Dockerfile.ui.prod index 9eed9fbd0..2471ababb 100644 --- a/src/Dockerfile.ui.prod +++ b/src/Dockerfile.ui.prod @@ -5,7 +5,7 @@ ENV VITE_API_URL=${VITE_API_URL} \ VITE_SYNC_URL=${VITE_SYNC_URL} \ PNPM_HOME="/pnpm" \ PATH="$PATH:/pnpm" -RUN corepack enable && corepack use pnpm@9.3.0 +RUN corepack enable && corepack use pnpm@9.15.4 WORKDIR /app diff --git a/src/frontend/package.json b/src/frontend/package.json index 48b028415..43721dc95 100755 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -105,5 +105,5 @@ "tailwind-merge": "2.3.0", "uuid": "^10.0.0" }, - "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" + "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" } diff --git a/src/mapper/package.json b/src/mapper/package.json index bc9bc8601..92f5cc863 100644 --- a/src/mapper/package.json +++ b/src/mapper/package.json @@ -65,5 +65,5 @@ "peerDependencies": { "@shoelace-style/shoelace": "^2.15.1" }, - "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" + "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" } From 6fda2aa13d95368bbf9466cef728e5077b410267 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 15 Jan 2025 16:31:55 +0000 Subject: [PATCH 03/24] build: add just config for running frontend dev without docker --- contrib/just/start/Justfile | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/contrib/just/start/Justfile b/contrib/just/start/Justfile index 86b2af1c0..d7b220446 100644 --- a/contrib/just/start/Justfile +++ b/contrib/just/start/Justfile @@ -32,7 +32,7 @@ backend: cd {{justfile_directory()}} docker compose up -d api -# Start backend API only +# Start backend API without docker [no-cd] backend-no-docker: #!/usr/bin/env sh @@ -43,11 +43,34 @@ backend-no-docker: OSM_SECRET_KEY="" ENCRYPTION_KEY="" \ uv run uvicorn app.main:api --host 0.0.0.0 --port 8000 -# Start frontend UI only +# Start frontend UI (also starts backend) [no-cd] frontend: docker compose up -d ui +# Start frontend UI without docker, connected to staging +[no-cd] +frontend-dev: + #!/usr/bin/env sh + + cd {{justfile_directory()}}/src/frontend + + pnpm install + VITE_API_URL=https://api.stage.fmtm.hotosm.org \ + pnpm run dev + +# Start mapper frontend UI without docker, connected to staging +[no-cd] +mapper-frontend-dev: + #!/usr/bin/env sh + + cd {{justfile_directory()}}/src/mapper + + pnpm install + VITE_API_URL=https://api.stage.fmtm.hotosm.org \ + VITE_SYNC_URL=https://sync.stage.fmtm.hotosm.org \ + pnpm run dev + # Start FMTM without ODK Central [no-cd] without-central: From 63cd7418c08e61b84c76598098a49ed25ead4b6e Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 15 Jan 2025 16:34:30 +0000 Subject: [PATCH 04/24] docs: update info about starting frontend dev locally (no docker) --- docs/dev/Frontend.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/dev/Frontend.md b/docs/dev/Frontend.md index 3584c6032..029cdd49a 100644 --- a/docs/dev/Frontend.md +++ b/docs/dev/Frontend.md @@ -30,19 +30,17 @@ For details on how to run the API first, please see: ## 2. Start the Frontend locally -### 2A: Navigate to the frontend subdirectory +To run the frontend locally, connected to the staging server as a backend: -`cd src/frontend` - -### 2B: Install dependencies - -`npm install` - -### 2C. Run the project +```bash +just --unstable start frontend-dev +``` -Run the frontend with hot-reloading: `npm run dev` +The mapper frontend can be started with a similar command: -The frontend should now be accessible at: `http://127.0.0.1:` +```bash +just --unstable start mapper-frontend-dev +``` ## Frontend Tips From 5e8753b67a7a832760a36d51f45c4a7e032d7434 Mon Sep 17 00:00:00 2001 From: Sujan Adhikari <109404840+Sujanadh@users.noreply.github.com> Date: Thu, 16 Jan 2025 05:25:17 +0545 Subject: [PATCH 05/24] feat(backend): get api for project's geometry log (#2090) * feat: create get api for project's geometry log * refactor: better naming of endpoints adding proper docstrings --- src/backend/app/db/models.py | 19 ++++++++++++ src/backend/app/projects/project_routes.py | 32 ++++++++++++++++++--- src/backend/app/projects/project_schemas.py | 2 +- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/backend/app/db/models.py b/src/backend/app/db/models.py index 1d7ee7176..d693132a6 100644 --- a/src/backend/app/db/models.py +++ b/src/backend/app/db/models.py @@ -1833,6 +1833,25 @@ async def create( new_geomlog = await cur.fetchone() return new_geomlog + @classmethod + async def all(cls, db: Connection, project_id: int) -> Optional[list[Self]]: + """Retrieve geometry logs from a project.""" + async with db.cursor(row_factory=class_row(cls)) as cur: + await cur.execute( + """ + SELECT * FROM geometrylog WHERE project_id=%(project_id)s; + """, + {"project_id": project_id}, + ) + if cur.rowcount == 0: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f""" + No geometry log with project_id {project_id} + """, + ) + return await cur.fetchall() + @classmethod async def delete( cls, diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index c9d3d7234..56a561a07 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -1068,7 +1068,6 @@ async def update_project( @router.post("/{project_id}/upload-task-boundaries") async def upload_project_task_boundaries( - project_id: int, db: Annotated[Connection, Depends(db_conn)], project_user_dict: Annotated[ProjectUserDict, Depends(project_manager)], task_geojson: UploadFile = File(...), @@ -1084,6 +1083,7 @@ async def upload_project_task_boundaries( Returns: JSONResponse: JSON containing success message. """ + project_id = project_user_dict.get("project").id tasks_featcol = parse_geojson_file_to_featcol(await task_geojson.read()) await check_crs(tasks_featcol) # We only want to allow polygon geometries @@ -1241,7 +1241,6 @@ async def download_project_boundary( @router.get("/{project_id}/download_tasks") async def download_task_boundaries( - project_id: int, db: Annotated[Connection, Depends(db_conn)], project_user: Annotated[ProjectUserDict, Depends(mapper)], ): @@ -1266,7 +1265,7 @@ async def download_task_boundaries( return Response(content=out, headers=headers) -@router.post("/{project_id}/geometries") +@router.post("/{project_id}/geometry/records") async def create_geom_log( geom_log: project_schemas.GeometryLogIn, current_user: Annotated[ProjectUserDict, Depends(mapper)], @@ -1310,7 +1309,32 @@ async def create_geom_log( return geometries -@router.delete("/{project_id}/geometries") +@router.get( + "{project_id}/geometry/records", response_model=list[project_schemas.GeometryLogIn] +) +async def read_geom_logs( + db: Annotated[Connection, Depends(db_conn)], + project_user: Annotated[ProjectUserDict, Depends(mapper)], +): + """Retrieve all geometry logs for a specific project. + + This endpoint fetches geometry records. + - Bad submitted feature and + - new feature drawn in a project + + Args: + db: The database connection. + project_user: The currently authenticated project user details. + + Returns: + list[project_schemas.GeometryLogIn]: A list of geometry log entries. + """ + project_id = project_user.get("project").id + geometries = await DbGeometryLog.all(db, project_id) + return geometries + + +@router.delete("/{project_id}/geometry/records/{geom_id}") async def delete_geom_log( geom_id: str, project_user: Annotated[ diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 4e27ba2fb..1ab6b5a68 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -64,7 +64,7 @@ def parse_input_geometry( if value is None: return None featcol = geojson_to_featcol(value) - return featcol.get("features")[0].get("geometry") + return featcol.get("features")[0] class ProjectInBase(DbProject): From feae2a28b0546d88b0c29826eb62c66284a871be Mon Sep 17 00:00:00 2001 From: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:26:23 +0545 Subject: [PATCH 06/24] feat(frontend): submission table date range filter (#2091) * fix(ISubmissions): update ts type * fix(customDatePicker): add start and end date range * fix(submissionTable): update submitted date with dateRange * fix(customDatePicker): update component name * fix(submissionsTable): pass query strings as object instead of static string * fix(task): update getDownloadProjectSubmission service function * fix(submissionsTable): update initial filter state, clear dateRange on filter clear * fix(projectSubmissionsSkeletonLoader): fix index clash error * fix(submissionsTable): remove useEffect block calculating no of filters applied * fix(select): update ts type * fix(submissionsTable): fallback to empty string if not values present on custom select * fix(index): update datepicker css --- src/frontend/src/api/task.ts | 17 ++--- .../ProjectSubmissionsSkeletonLoader.tsx | 4 +- .../ProjectSubmissions/SubmissionsTable.tsx | 72 +++++++++---------- ...stomDatePicker.tsx => DateRangePicker.tsx} | 24 +++++-- src/frontend/src/components/common/Select.tsx | 2 +- src/frontend/src/index.css | 12 ++-- src/frontend/src/store/types/ISubmissions.ts | 2 +- 7 files changed, 73 insertions(+), 60 deletions(-) rename src/frontend/src/components/common/{CustomDatePicker.tsx => DateRangePicker.tsx} (51%) diff --git a/src/frontend/src/api/task.ts b/src/frontend/src/api/task.ts index 5f2a7dabe..c77f0111b 100644 --- a/src/frontend/src/api/task.ts +++ b/src/frontend/src/api/task.ts @@ -2,11 +2,14 @@ import { AppDispatch } from '@/store/Store'; import CoreModules from '@/shared/CoreModules'; import { TaskActions } from '@/store/slices/TaskSlice'; -export const getDownloadProjectSubmission = (url: string, projectName: string) => { +export const getDownloadProjectSubmission = ( + url: string, + projectName: string, + params: { project_id: string; export_json: boolean; submitted_date_range: string | null }, +) => { return async (dispatch: AppDispatch) => { - const params = new URLSearchParams(url.split('?')[1]); - const isExportJson = params.get('export_json'); - const isJsonOrCsv = isExportJson === 'true' ? 'json' : 'csv'; + const isExportJson = params.export_json; + const isJsonOrCsv = isExportJson ? 'json' : 'csv'; dispatch( TaskActions.GetDownloadProjectSubmissionLoading({ type: isJsonOrCsv, @@ -16,12 +19,10 @@ export const getDownloadProjectSubmission = (url: string, projectName: string) = const getProjectSubmission = async (url: string) => { try { - const response = await CoreModules.axios.get(url, { - responseType: 'blob', - }); + const response = await CoreModules.axios.get(url, { params, responseType: 'blob' }); var a = document.createElement('a'); a.href = window.URL.createObjectURL(response.data); - a.download = isExportJson === 'true' ? `${projectName}.json` : `${projectName}.zip`; + a.download = isExportJson ? `${projectName}.json` : `${projectName}.zip`; a.click(); } catch (error) { } finally { diff --git a/src/frontend/src/components/ProjectSubmissions/ProjectSubmissionsSkeletonLoader.tsx b/src/frontend/src/components/ProjectSubmissions/ProjectSubmissionsSkeletonLoader.tsx index b143545f1..9912f1e91 100644 --- a/src/frontend/src/components/ProjectSubmissions/ProjectSubmissionsSkeletonLoader.tsx +++ b/src/frontend/src/components/ProjectSubmissions/ProjectSubmissionsSkeletonLoader.tsx @@ -29,8 +29,8 @@ export const SubmissionsTableSkeletonLoader = () => { {Array.from({ length: 10 }).map((_, i) => (
- {Array.from({ length: 15 }).map(() => ( - + {Array.from({ length: 15 }).map((_, ind) => ( + ))}
))} diff --git a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx index 21b6d0967..77798be1e 100644 --- a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx +++ b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx @@ -11,7 +11,7 @@ import windowDimention from '@/hooks/WindowDimension'; import Button from '@/components/common/Button'; import { Modal } from '@/components/common/Modal'; import { CustomSelect } from '@/components/common/Select.js'; -import CustomDatePicker from '@/components/common/CustomDatePicker'; +import DateRangePicker from '@/components/common/DateRangePicker'; import Table, { TableHeader } from '@/components/common/CustomTable'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/common/Dropdown'; import { SubmissionsTableSkeletonLoader } from '@/components/ProjectSubmissions/ProjectSubmissionsSkeletonLoader.js'; @@ -39,7 +39,7 @@ const SubmissionsTable = ({ toggleView }) => { task_id: searchParams.get('task_id') ? searchParams?.get('task_id') || null : null, submitted_by: searchParams.get('submitted_by'), review_state: searchParams.get('review_state'), - submitted_date: searchParams.get('submitted_date'), + submitted_date_range: searchParams.get('submitted_date_range'), }; const [filter, setFilter] = useState(initialFilterState); @@ -70,20 +70,25 @@ const SubmissionsTable = ({ toggleView }) => { }; const taskList = projectData[projectIndex]?.taskBoundries; - const [numberOfFilters, setNumberOfFilters] = useState(0); const [paginationPage, setPaginationPage] = useState(1); const [submittedBy, setSubmittedBy] = useState(null); + const [dateRange, setDateRange] = useState<{ start: Date | null; end: Date | null }>({ + start: initialFilterState?.submitted_date_range + ? new Date(initialFilterState.submitted_date_range.split(',')[0]) + : null, + end: initialFilterState?.submitted_date_range + ? new Date(initialFilterState.submitted_date_range.split(',')[1]) + : null, + }); useEffect(() => { - let count = 0; - const filters = Object.keys(filter); - filters?.map((fltr) => { - if (filter[fltr]) { - count = count + 1; - } - }); - setNumberOfFilters(count); - }, [filter]); + if (!dateRange.start || !dateRange.end) return; + + setFilter((prev) => ({ + ...prev, + submitted_date_range: `${format(new Date(dateRange.start as Date), 'yyyy-MM-dd')},${format(new Date(dateRange.end as Date), 'yyyy-MM-dd')}`, + })); + }, [dateRange]); const updatedSubmissionFormFields = submissionFormFields //filter necessary fields only @@ -163,7 +168,8 @@ const SubmissionsTable = ({ toggleView }) => { const clearFilters = () => { setSearchParams({ tab: 'table' }); - setFilter({ task_id: null, submitted_by: null, review_state: null, submitted_date: null }); + setFilter({ task_id: null, submitted_by: null, review_state: null, submitted_date_range: null }); + setDateRange({ start: null, end: null }); }; function getValueByPath(obj: any, path: string) { @@ -203,21 +209,13 @@ const SubmissionsTable = ({ toggleView }) => { }; const handleDownload = (downloadType: 'csv' | 'json') => { - if (downloadType === 'csv') { - dispatch( - getDownloadProjectSubmission( - `${import.meta.env.VITE_API_URL}/submission/download?project_id=${projectId}&export_json=false`, - projectInfo.name!, - ), - ); - } else if (downloadType === 'json') { - dispatch( - getDownloadProjectSubmission( - `${import.meta.env.VITE_API_URL}/submission/download?project_id=${projectId}&export_json=true`, - projectInfo.name!, - ), - ); - } + dispatch( + getDownloadProjectSubmission(`${import.meta.env.VITE_API_URL}/submission/download`, projectInfo.name!, { + project_id: projectId, + submitted_date_range: filter?.submitted_date_range, + export_json: downloadType === 'json', + }), + ); }; const handleTaskMap = async () => { @@ -270,7 +268,7 @@ const SubmissionsTable = ({ toggleView }) => { >

FILTER

{' '}
-

{numberOfFilters}

+

{Object.values(filter).filter((filterObjValue) => filterObjValue).length}

@@ -284,7 +282,7 @@ const SubmissionsTable = ({ toggleView }) => { placeholder="Select" data={taskInfo} dataKey="value" - value={filter?.task_id?.toString() || undefined} + value={filter?.task_id?.toString() || ''} valueKey="task_id" label="task_id" onValueChange={(value) => value && setFilter((prev) => ({ ...prev, task_id: value.toString() }))} @@ -297,7 +295,7 @@ const SubmissionsTable = ({ toggleView }) => { placeholder="Select" data={reviewStateData} dataKey="value" - value={filter?.review_state || undefined} + value={filter?.review_state || ''} valueKey="value" label="label" onValueChange={(value) => @@ -307,13 +305,13 @@ const SubmissionsTable = ({ toggleView }) => { className="fmtm-text-grey-700 fmtm-text-sm !fmtm-mb-0 fmtm-bg-white" /> -
- + - setFilter((prev) => ({ ...prev, submitted_date: format(new Date(date), 'yyyy-MM-dd') })) - } + startDate={dateRange?.start} + endDate={dateRange?.end} + setStartDate={(date) => setDateRange((prev) => ({ ...prev, start: date }))} + setEndDate={(date) => setDateRange((prev) => ({ ...prev, end: date }))} className="fmtm-text-grey-700 fmtm-text-sm !fmtm-mb-0 fmtm-w-full" />
diff --git a/src/frontend/src/components/common/CustomDatePicker.tsx b/src/frontend/src/components/common/DateRangePicker.tsx similarity index 51% rename from src/frontend/src/components/common/CustomDatePicker.tsx rename to src/frontend/src/components/common/DateRangePicker.tsx index a83e524b3..4320539ee 100644 --- a/src/frontend/src/components/common/CustomDatePicker.tsx +++ b/src/frontend/src/components/common/DateRangePicker.tsx @@ -2,22 +2,32 @@ import React from 'react'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; -type CustomDatePickerType = { +type DateRangePickerType = { title: string; className: string; - selectedDate: string | null; - setSelectedDate: (date: Date) => void; + setStartDate: (date: Date | null) => void; + setEndDate: (date: Date | null) => void; + startDate: Date | null; + endDate: Date | null; }; -const CustomDatePicker = ({ title, className, selectedDate, setSelectedDate }: CustomDatePickerType) => { +const DateRangePicker = ({ title, className, setStartDate, setEndDate, startDate, endDate }: DateRangePickerType) => { + const onChange = (dates: [Date | null, Date | null]) => { + const [start, end] = dates; + setStartDate(start); + setEndDate(end); + }; return (
{title && (

{title}

)} setSelectedDate(date)} + selected={startDate} + onChange={onChange} + startDate={startDate} + endDate={endDate} + selectsRange className="fmtm-outline-none fmtm-border-[1px] fmtm-border-gray-300 fmtm-h-[2rem] fmtm-w-full fmtm-z-50 fmtm-px-2 fmtm-text-base fmtm-pt-1 hover" placeholderText="YYYY/MM/DD" dateFormat="yyyy/MM/dd" @@ -26,4 +36,4 @@ const CustomDatePicker = ({ title, className, selectedDate, setSelectedDate }: C ); }; -export default CustomDatePicker; +export default DateRangePicker; diff --git a/src/frontend/src/components/common/Select.tsx b/src/frontend/src/components/common/Select.tsx index 7adbc14d4..074aad807 100644 --- a/src/frontend/src/components/common/Select.tsx +++ b/src/frontend/src/components/common/Select.tsx @@ -97,7 +97,7 @@ interface ICustomSelect { placeholder: string; data: any; dataKey: string; - value?: string | undefined; + value?: string; valueKey: string; label: string; onValueChange?: (value: string | null | number) => void; diff --git a/src/frontend/src/index.css b/src/frontend/src/index.css index 067a7d4d5..f97aa1a9a 100755 --- a/src/frontend/src/index.css +++ b/src/frontend/src/index.css @@ -247,12 +247,16 @@ button { .react-datepicker-popper { z-index: 9000; } -.react-datepicker__day--selected { - background-color: #d73f3e; +.react-datepicker__day--selected, +.react-datepicker__day--in-range { + background-color: #d73f3e !important; +} +.react-datepicker__day--in-selecting-range { + background-color: #df6565 !important; } .react-datepicker__day--selected:hover { - background-color: #bc2c2c; + background-color: #bc2c2c !important; } .react-datepicker__day--keyboard-selected { - background-color: #fcc2c2; + background-color: #fcc2c2 !important; } diff --git a/src/frontend/src/store/types/ISubmissions.ts b/src/frontend/src/store/types/ISubmissions.ts index 0baecd7a1..ed0577e47 100644 --- a/src/frontend/src/store/types/ISubmissions.ts +++ b/src/frontend/src/store/types/ISubmissions.ts @@ -35,7 +35,7 @@ export type filterType = { task_id: string | null; submitted_by: string | null; review_state: string | null; - submitted_date: string | null; + submitted_date_range: string | null; }; type mappedVsValidatedTaskType = { From 274bc182a4d1685409f4ebf4a3cf677aa28753e9 Mon Sep 17 00:00:00 2001 From: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:55:35 +0545 Subject: [PATCH 07/24] feat(mapper): distance constraint add on frontend (#2084) * feat(package): turf/distance package add for distance calculation between two points * fix(types): ProjectData type update * fix(geolocation): reset userLocation coordinate if location disabled * feat(dialog-entities-actions): function add to check validity of distance between user and entity * fix(dialog-entities): fix typo * feat(package): turf/distance package add for distance calculation between two points * fix(types): ProjectData type update * fix(geolocation): reset userLocation coordinate if location disabled * feat(dialog-entities-actions): function add to check validity of distance between user and entity * fix(dialog-entities): fix typo * refactor: update new feature draw warning/error logic * docs: update comment describing the new feat dist constraint * fix(dialog-entities-actions): update distance constraing logic * feat(dialog-entities-actions): show warning dialog on feature map if location enabled, feature far away and geo restrict false --------- Co-authored-by: spwoodcock --- src/mapper/package.json | 1 + src/mapper/pnpm-lock.yaml | 30 +++++ .../components/dialog-entities-actions.svelte | 120 +++++++++++++++++- .../src/lib/components/map/geolocation.svelte | 1 + src/mapper/src/lib/types.ts | 2 + 5 files changed, 152 insertions(+), 2 deletions(-) diff --git a/src/mapper/package.json b/src/mapper/package.json index 92f5cc863..9a3752d2d 100644 --- a/src/mapper/package.json +++ b/src/mapper/package.json @@ -52,6 +52,7 @@ "@turf/bbox": "^7.1.0", "@turf/buffer": "^7.1.0", "@turf/centroid": "^7.1.0", + "@turf/distance": "^7.2.0", "@turf/helpers": "^7.1.0", "@watergis/maplibre-gl-terradraw": "^0.8.1", "drizzle-orm": "^0.35.3", diff --git a/src/mapper/pnpm-lock.yaml b/src/mapper/pnpm-lock.yaml index 636aedb6d..af2ae8c66 100644 --- a/src/mapper/pnpm-lock.yaml +++ b/src/mapper/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@turf/centroid': specifier: ^7.1.0 version: 7.1.0 + '@turf/distance': + specifier: ^7.2.0 + version: 7.2.0 '@turf/helpers': specifier: ^7.1.0 version: 7.1.0 @@ -1447,9 +1450,18 @@ packages: '@turf/clone@7.1.0': resolution: {integrity: sha512-5R9qeWvL7FDdBIbEemd0eCzOStr09oburDvJ1hRiPCFX6rPgzcZBQ0gDmZzoF4AFcNLb5IwknbLZjVLaUGWtFA==} + '@turf/distance@7.2.0': + resolution: {integrity: sha512-HBjjXIgEcD/wJYjv7/6OZj5yoky2oUvTtVeIAqO3lL80XRvoYmVg6vkOIu6NswkerwLDDNT9kl7+BFLJoHbh6Q==} + '@turf/helpers@7.1.0': resolution: {integrity: sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ==} + '@turf/helpers@7.2.0': + resolution: {integrity: sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==} + + '@turf/invariant@7.2.0': + resolution: {integrity: sha512-kV4u8e7Gkpq+kPbAKNC21CmyrXzlbBgFjO1PhrHPgEdNqXqDawoZ3i6ivE3ULJj2rSesCjduUaC/wyvH/sNr2Q==} + '@turf/jsts@2.7.1': resolution: {integrity: sha512-+nwOKme/aUprsxnLSfr2LylV6eL6T1Tuln+4Hl92uwZ8FrmjDRCH5Bi1LJNVfWCiYgk8+5K+t2zDphWNTsIFDA==} @@ -5144,11 +5156,29 @@ snapshots: '@types/geojson': 7946.0.14 tslib: 2.8.1 + '@turf/distance@7.2.0': + dependencies: + '@turf/helpers': 7.2.0 + '@turf/invariant': 7.2.0 + '@types/geojson': 7946.0.14 + tslib: 2.8.1 + '@turf/helpers@7.1.0': dependencies: '@types/geojson': 7946.0.14 tslib: 2.8.1 + '@turf/helpers@7.2.0': + dependencies: + '@types/geojson': 7946.0.14 + tslib: 2.8.1 + + '@turf/invariant@7.2.0': + dependencies: + '@turf/helpers': 7.2.0 + '@types/geojson': 7946.0.14 + tslib: 2.8.1 + '@turf/jsts@2.7.1': dependencies: jsts: 2.7.1 diff --git a/src/mapper/src/lib/components/dialog-entities-actions.svelte b/src/mapper/src/lib/components/dialog-entities-actions.svelte index 991af3721..409747f4f 100644 --- a/src/mapper/src/lib/components/dialog-entities-actions.svelte +++ b/src/mapper/src/lib/components/dialog-entities-actions.svelte @@ -1,9 +1,12 @@ @@ -416,7 +430,7 @@ + + + + + + v0.18.2 for xlsform updates --- src/backend/pyproject.toml | 2 +- src/backend/uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index fd03becff..3fedc51d5 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "sozipfile==0.3.2", "cryptography==43.0.3", "pyjwt==2.9.0", - "osm-fieldwork==0.18.1", + "osm-fieldwork==0.18.2", "osm-login-python==2.0.0", "osm-rawdata==0.4.1", "fmtm-splitter==2.0.0", diff --git a/src/backend/uv.lock b/src/backend/uv.lock index a5caf865d..c8230fd40 100644 --- a/src/backend/uv.lock +++ b/src/backend/uv.lock @@ -568,7 +568,7 @@ requires-dist = [ { name = "httptools", specifier = "==0.6.4" }, { name = "loguru", specifier = "==0.7.2" }, { name = "minio", specifier = "==7.2.9" }, - { name = "osm-fieldwork", specifier = "==0.18.1" }, + { name = "osm-fieldwork", specifier = "==0.18.2" }, { name = "osm-login-python", specifier = "==2.0.0" }, { name = "osm-rawdata", specifier = "==0.4.1" }, { name = "psycopg", extras = ["pool"], specifier = ">=3.2.3" }, @@ -1435,7 +1435,7 @@ wheels = [ [[package]] name = "osm-fieldwork" -version = "0.18.1" +version = "0.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1459,9 +1459,9 @@ dependencies = [ { name = "shapely" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/97/aebc77a1399ac391b9852215ed2850d24aeadb01431c0db8549681c56e1a/osm-fieldwork-0.18.1.tar.gz", hash = "sha256:490cc5b72e2fb46fc7f4221f58e197a3d3007e3e8e69ca57ca2283eb06fe98f1", size = 1485728 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/46/8833f246ee8df7e8c6d4cda682a6d85ef4e8c2716b6787031cc6192d84e6/osm-fieldwork-0.18.2.tar.gz", hash = "sha256:7e2bae57f3a2eb88dd3f91a67f89fbc7fa29ae512bd5c212ba060dc3f0ee825e", size = 1485777 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/9f/4c4c06ae89d24671d102697aea5ee0e1fef34000247dd82479003e01456b/osm_fieldwork-0.18.1-py3-none-any.whl", hash = "sha256:2454a7786324e511a80934345cf6993ea993b7991b37dd489650ecf128c6821a", size = 1511857 }, + { url = "https://files.pythonhosted.org/packages/0b/2d/f9c1bfa56f0ba4a22434c2cdab2ab70e2309368f2921d6218a7ed2a54073/osm_fieldwork-0.18.2-py3-none-any.whl", hash = "sha256:469d210f78b1d391ec5c8ddcbd751365c68beb03424334d5159192464f0744c7", size = 1511940 }, ] [[package]] From 2b1db384ff4d1bd6d640baada060adb9c8847e0c Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 21 Jan 2025 17:52:47 +0000 Subject: [PATCH 14/24] build: update minio to latest version to avoid CVE --- chart/values.yaml | 2 +- docker-compose.development.yml | 2 +- docker-compose.main.yml | 2 +- docker-compose.yml | 2 +- src/backend/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chart/values.yaml b/chart/values.yaml index e875999b9..156cafdad 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -114,7 +114,7 @@ s3: replicas: 1 image: repository: quay.io/minio/minio - tag: RELEASE.2024-12-18T13-15-44Z + tag: RELEASE.2025-01-20T14-49-07Z environment: MINIO_BROWSER: "off" mountPath: /mnt/data diff --git a/docker-compose.development.yml b/docker-compose.development.yml index 850af11f7..46dd1274c 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -178,7 +178,7 @@ services: restart: "on-failure:2" s3: - image: "docker.io/minio/minio:${MINIO_TAG:-RELEASE.2024-12-18T13-15-44Z}" + image: "docker.io/minio/minio:${MINIO_TAG:-RELEASE.2025-01-20T14-49-07Z}" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} diff --git a/docker-compose.main.yml b/docker-compose.main.yml index f9e949f99..ccccbceec 100644 --- a/docker-compose.main.yml +++ b/docker-compose.main.yml @@ -119,7 +119,7 @@ services: restart: "on-failure:2" s3: - image: "docker.io/minio/minio:${MINIO_TAG:-RELEASE.2024-12-18T13-15-44Z}" + image: "docker.io/minio/minio:${MINIO_TAG:-RELEASE.2025-01-20T14-49-07Z}" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} diff --git a/docker-compose.yml b/docker-compose.yml index 4093f941c..96833520f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -225,7 +225,7 @@ services: restart: "on-failure:2" s3: - image: "docker.io/minio/minio:${MINIO_TAG:-RELEASE.2024-12-18T13-15-44Z}" + image: "docker.io/minio/minio:${MINIO_TAG:-RELEASE.2025-01-20T14-49-07Z}" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY:-fmtm} MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:-somelongpassword} diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index f9e76a20b..04da14d68 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -16,7 +16,7 @@ # ARG PYTHON_IMG_TAG=3.12 ARG UV_IMG_TAG=0.5.2 -ARG MINIO_TAG=${MINIO_TAG:-RELEASE.2024-12-18T13-15-44Z} +ARG MINIO_TAG=${MINIO_TAG:-RELEASE.2025-01-20T14-49-07Z} FROM ghcr.io/astral-sh/uv:${UV_IMG_TAG} AS uv FROM docker.io/minio/minio:${MINIO_TAG} AS minio From 2a81bdfa4424c84edb8b57434c31e5fb5bb269c8 Mon Sep 17 00:00:00 2001 From: Anuj Gupta <84966248+Anuj-Gupta4@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:02:15 +0545 Subject: [PATCH 15/24] fix(backend): review state for received on submission table (#2101) * fix(submission): fix review state filter for received * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/backend/app/submissions/submission_routes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/backend/app/submissions/submission_routes.py b/src/backend/app/submissions/submission_routes.py index f9494276e..a297f560f 100644 --- a/src/backend/app/submissions/submission_routes.py +++ b/src/backend/app/submissions/submission_routes.py @@ -303,7 +303,7 @@ async def submission_table( "and __system/submissionDate le {}T23:59:59.999+00:00" ).format(start_date, end_date) - if review_state: + if review_state and review_state != "received": review_filter = f"__system/reviewState eq '{review_state}'" filters["$filter"] = ( f"{filters['$filter']} and {review_filter}" @@ -314,6 +314,12 @@ async def submission_table( data = await submission_crud.get_submission_by_project(project, filters) total_count = data.get("@odata.count", 0) submissions = data.get("value", []) + if review_state == "received": + submissions = [ + sub for sub in submissions if sub["__system"].get("reviewState") is None + ] + total_count = len(submissions) + instance_ids = [ sub["__id"] for sub in submissions From 3fd36659c66cc5eb747602839fb548aade0233e8 Mon Sep 17 00:00:00 2001 From: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com> Date: Wed, 22 Jan 2025 23:44:04 +0545 Subject: [PATCH 16/24] refactor(mapper): relocate entity sync button (#2100) * fix(dialog-entities-actions): remove sync status button from dialog * feat(main): add sync button on bottom right * fix(legend/layer-switcher): update stylings * feat(project): SyncTaskState action add * feat(projectDetailsV2): sync task status on sync button click as well --- src/frontend/src/api/Project.ts | 43 +++++++++++++++++++ src/frontend/src/store/slices/ProjectSlice.ts | 4 ++ src/frontend/src/store/types/IProject.ts | 1 + src/frontend/src/views/ProjectDetailsV2.tsx | 32 ++++++++++++-- .../components/dialog-entities-actions.svelte | 20 --------- .../lib/components/map/layer-switcher.svelte | 4 +- .../src/lib/components/map/legend.svelte | 4 +- src/mapper/src/lib/components/map/main.svelte | 17 ++++++++ 8 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/frontend/src/api/Project.ts b/src/frontend/src/api/Project.ts index 46fd4e15a..da4b8ecc7 100755 --- a/src/frontend/src/api/Project.ts +++ b/src/frontend/src/api/Project.ts @@ -372,3 +372,46 @@ export const GetGeometryLog = (url: string) => { await getProjectActivity(url); }; }; + +export const SyncTaskState = ( + url: string, + params: { project_id: string }, + taskBoundaryFeatures: any, + geojsonStyles: any, +) => { + return async (dispatch: AppDispatch) => { + const syncTaskState = async () => { + try { + dispatch(ProjectActions.SyncTaskStateLoading(true)); + const response: AxiosResponse = await axios.get(url, { params }); + + response.data.map((task) => { + const feature = taskBoundaryFeatures?.find((feature) => feature.getId() === task.id); + const previousProperties = feature.getProperties(); + feature.setProperties({ + ...previousProperties, + task_state: task.task_state, + actioned_by_uid: task.actioned_by_uid, + actioned_by_username: task.actioned_by_username, + }); + + feature.setStyle(geojsonStyles[task.task_state]); + + dispatch( + ProjectActions.UpdateProjectTaskBoundries({ + projectId: params.project_id, + taskId: task.id, + actioned_by_uid: task.actioned_by_uid, + actioned_by_username: task.actioned_by_username, + task_state: task.task_state, + }), + ); + }); + } catch (error) { + } finally { + dispatch(ProjectActions.SyncTaskStateLoading(false)); + } + }; + await syncTaskState(); + }; +}; diff --git a/src/frontend/src/store/slices/ProjectSlice.ts b/src/frontend/src/store/slices/ProjectSlice.ts index ee6dcd2db..380e96662 100755 --- a/src/frontend/src/store/slices/ProjectSlice.ts +++ b/src/frontend/src/store/slices/ProjectSlice.ts @@ -35,6 +35,7 @@ const initialState: ProjectStateTypes = { newGeomFeatureCollection: { type: 'FeatureCollection', features: [] }, badGeomLogList: [], getGeomLogLoading: false, + syncTaskStateLoading: false, }; const ProjectSlice = createSlice({ @@ -170,6 +171,9 @@ const ProjectSlice = createSlice({ SetGeometryLogLoading(state, action: PayloadAction) { state.getGeomLogLoading = action.payload; }, + SyncTaskStateLoading(state, action: PayloadAction) { + state.syncTaskStateLoading = action.payload; + }, }, }); diff --git a/src/frontend/src/store/types/IProject.ts b/src/frontend/src/store/types/IProject.ts index b367bf875..f8275fe30 100644 --- a/src/frontend/src/store/types/IProject.ts +++ b/src/frontend/src/store/types/IProject.ts @@ -40,6 +40,7 @@ export type ProjectStateTypes = { newGeomFeatureCollection: FeatureCollectionType; badGeomLogList: geometryLogResponseType[]; getGeomLogLoading: boolean; + syncTaskStateLoading: boolean; }; type projectCommentsListTypes = { diff --git a/src/frontend/src/views/ProjectDetailsV2.tsx b/src/frontend/src/views/ProjectDetailsV2.tsx index eebc4aeb4..d55e1761e 100644 --- a/src/frontend/src/views/ProjectDetailsV2.tsx +++ b/src/frontend/src/views/ProjectDetailsV2.tsx @@ -3,7 +3,7 @@ import '../../node_modules/ol/ol.css'; import '../styles/home.scss'; import WindowDimension from '@/hooks/WindowDimension'; import ActivitiesPanel from '@/components/ProjectDetailsV2/ActivitiesPanel'; -import { ProjectById, GetEntityStatusList, GetGeometryLog } from '@/api/Project'; +import { ProjectById, GetEntityStatusList, GetGeometryLog, SyncTaskState } from '@/api/Project'; import { ProjectActions } from '@/store/slices/ProjectSlice'; import CustomizedSnackbar from '@/utilities/CustomizedSnackbar'; import { HomeActions } from '@/store/slices/HomeSlice'; @@ -36,6 +36,7 @@ import { Geolocation } from '@/utilfunctions/Geolocation'; import Instructions from '@/components/ProjectDetailsV2/Instructions'; import useDocumentTitle from '@/utilfunctions/useDocumentTitle'; import { Style, Stroke } from 'ol/style'; +import MapStyles from '@/hooks/MapStyles'; const ProjectDetailsV2 = () => { useDocumentTitle('Project Details'); @@ -45,6 +46,8 @@ const ProjectDetailsV2 = () => { const { windowSize } = WindowDimension(); const [divRef, toggle, handleToggle] = useOutsideClick(); + const geojsonStyles = MapStyles(); + const [selectedTaskArea, setSelectedTaskArea] = useState | null>(null); const [selectedTaskFeature, setSelectedTaskFeature] = useState(); const [dataExtractUrl, setDataExtractUrl] = useState(); @@ -69,6 +72,7 @@ const ProjectDetailsV2 = () => { const entityOsmMapLoading = useAppSelector((state) => state?.project?.entityOsmMapLoading); const badGeomFeatureCollection = useAppSelector((state) => state?.project?.badGeomFeatureCollection); const getGeomLogLoading = useAppSelector((state) => state?.project?.getGeomLogLoading); + const syncTaskStateLoading = useAppSelector((state) => state?.project?.syncTaskStateLoading); useEffect(() => { if (state.projectInfo.name) { @@ -259,6 +263,24 @@ const ProjectDetailsV2 = () => { dispatch(GetGeometryLog(`${import.meta.env.VITE_API_URL}/projects/${projectId}/geometry/records`)); }; + const syncTaskState = () => { + const taskBoundaryLayer = map + .getLayers() + .getArray() + .find((layer: any) => layer.get('name') == 'project-area'); + const taskBoundaryFeatures = taskBoundaryLayer.getSource().getFeatures(); + + projectId && + dispatch( + SyncTaskState( + `${import.meta.env.VITE_API_URL}/tasks`, + { project_id: projectId }, + taskBoundaryFeatures, + geojsonStyles, + ), + ); + }; + useEffect(() => { getEntityStatusList(); getGeometryLog(); @@ -299,6 +321,7 @@ const ProjectDetailsV2 = () => { const syncStatus = () => { getEntityStatusList(); getGeometryLog(); + syncTaskState(); }; return ( @@ -462,7 +485,6 @@ const ProjectDetailsV2 = () => { size: map?.getSize(), padding: [50, 50, 50, 50], constrainResolution: true, - duration: 2000, }} layerProperties={{ name: 'project-area' }} mapOnClick={projectClickOnTaskArea} @@ -532,14 +554,16 @@ const ProjectDetailsV2 = () => {

Feature {selectedEntity?.osmid}

- { - await entitiesStore.syncEntityStatus(projectData?.id); - }} - onkeydown={(e: KeyboardEvent) => { - e.key === 'Enter' && {}; - }} - role="button" - tabindex="0" - size="small" - class="link w-fit ml-auto" - disabled={entitiesStore.syncEntityStatusLoading} - > - - SYNC STATUS -

Task Id: {selectedEntity?.task_id}

diff --git a/src/mapper/src/lib/components/map/layer-switcher.svelte b/src/mapper/src/lib/components/map/layer-switcher.svelte index 94f7134ee..88bf34d06 100644 --- a/src/mapper/src/lib/components/map/layer-switcher.svelte +++ b/src/mapper/src/lib/components/map/layer-switcher.svelte @@ -190,13 +190,13 @@ map = new Map({ > Basemap Icon

Base Maps

diff --git a/src/mapper/src/lib/components/map/legend.svelte b/src/mapper/src/lib/components/map/legend.svelte index 5759094cf..738f36df7 100644 --- a/src/mapper/src/lib/components/map/legend.svelte +++ b/src/mapper/src/lib/components/map/legend.svelte @@ -34,11 +34,11 @@

Legend

{#each taskStatuses as taskStatus} diff --git a/src/mapper/src/lib/components/map/main.svelte b/src/mapper/src/lib/components/map/main.svelte index b1185f7d8..149cc89da 100644 --- a/src/mapper/src/lib/components/map/main.svelte +++ b/src/mapper/src/lib/components/map/main.svelte @@ -355,6 +355,23 @@ +
+ await entitiesStore.syncEntityStatus(projectId)} + onkeydown={async (e: KeyboardEvent) => { + e.key === 'Enter' && (await entitiesStore.syncEntityStatus(projectId)); + }} + role="button" + tabindex="0" + > +
+ Date: Thu, 23 Jan 2025 03:03:52 -0500 Subject: [PATCH 17/24] docs: update Backend.md documentation (#2104) docker-compose was replaced by "docker compose" Signed-off-by: Daniel J. Dufour --- docs/dev/Backend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/Backend.md b/docs/dev/Backend.md index 4c6412195..85ff78e1c 100644 --- a/docs/dev/Backend.md +++ b/docs/dev/Backend.md @@ -25,7 +25,7 @@ URLs defined in the docker-compose file and your env file. and ensure that it is running on your local machine. 2. From the command line: navigate to the top level directory of the FMTM project. -3. From the command line run: `docker-compose pull`. +3. From the command line run: `docker compose pull`. This will pull the latest container builds from **main** branch. 4. Make sure you have a `.env` file with all required variables, see [here](../INSTALL.md#2-create-an-env-file). From 247972b2f5c99f7009ac5b6d4c3985d57a785d3e Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Thu, 23 Jan 2025 16:16:42 +0000 Subject: [PATCH 18/24] docs: update user roadmap with current progress --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 26ae12176..b5f27c5f3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ | **CI/CD** | | [![Build and Deploy](https://github.com/hotosm/fmtm/actions/workflows/build_and_deploy.yml/badge.svg?branch=main)](https://github.com/hotosm/fmtm/actions/workflows/build_and_deploy.yml?query=branch%3Amain) [![Build CI Img](https://github.com/hotosm/fmtm/actions/workflows/build_ci_img.yml/badge.svg?branch=development)](https://github.com/hotosm/fmtm/actions/workflows/build_ci_img.yml) [![Build ODK Images](https://github.com/hotosm/fmtm/actions/workflows/build_odk_imgs.yml/badge.svg?branch=development)](https://github.com/hotosm/fmtm/actions/workflows/build_odk_imgs.yml)
[![🔧 Build Proxy Images](https://github.com/hotosm/fmtm/actions/workflows/build_proxy_imgs.yml/badge.svg?branch=development)](https://github.com/hotosm/fmtm/actions/workflows/build_proxy_imgs.yml) [![Publish Docs](https://github.com/hotosm/fmtm/actions/workflows/docs.yml/badge.svg?branch=development)](https://github.com/hotosm/fmtm/actions/workflows/docs.yml) [![pre-commit.ci](https://results.pre-commit.ci/badge/github/hotosm/fmtm/development.svg)](https://results.pre-commit.ci/latest/github/hotosm/fmtm/development) | | :--- | :--- | :--- | -| **Tech Stack** | | ![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) | +| **Tech Stack** | | ![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![Svelte](https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte) ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) | | **Code Style** | | [![Backend Style](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json&labelColor=202235)](https://github.com/astral-sh/ruff) [![Frontend Style](https://img.shields.io/badge/code%20style-prettier-F7B93E?logo=Prettier)](https://github.com/prettier/prettier) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://pre-commit.com) | | **Quality** | | [![Coverage](https://docs.fmtm.dev/coverage.svg)](https://docs.fmtm.dev/coverage.html) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9218/badge)](https://www.bestpractices.dev/projects/9218) | | **Community** | | [![Slack](https://img.shields.io/badge/Slack-Join%20the%20community!-d63f3f?style=for-the-badge&logo=slack&logoColor=d63f3f)](https://slack.hotosm.org) [![All Contributors](https://img.shields.io/github/all-contributors/hotosm/fmtm?color=ee8449&style=flat-square)](#contributors-) | @@ -121,13 +121,14 @@ Alternatively see the [docs](https://docs.fmtm.dev) for various deployment guide |✅| 📱 open ODK Collect with feature already selected | |✅| 📱 live updates during mapping (if online) | |✅| 📱 features turn green once mapped | -|⚙️| 📱 better support for mapping **new** points, lines, polygons | -|⚙️| 📱 navigation and capability for routing to map features | -| | 📱 integrate ODK Web Forms (to avoid switching apps) | +|✅| 📱 better support for mapping **new** points, lines, polygons | +|✅| 📱 navigation and capability for routing to map features | +|⚙️| 📱 integrate ODK Web Forms (to avoid switching apps) | +|⚙️| 🖥️ multiple approaches to task splitting algorithm | +|⚙️| 🖥️ user role management per project | | | 📱 fully offline field mapping | | | 🖥️ organization creation and management | | | 🖥️ simplify project creation with basic / advanced workflows | -| | 🖥️ refinements to task splitting algorithm | | | 🖥️ improvements to the validation criteria and workflow | | | 🖥️ export (+merge) the final data to OpenStreetMap | | | 🖥️ better data visualisation and export options | From 01faae32bbbd57ed05660de1dcfa64d89a5aff25 Mon Sep 17 00:00:00 2001 From: Sujan Adhikari <109404840+Sujanadh@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:22:55 +0545 Subject: [PATCH 19/24] fix(backend): delete submission photos while deleting project to avoid foreignkey constraint (#2112) --- src/backend/app/db/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backend/app/db/models.py b/src/backend/app/db/models.py index fa7e7d35c..e070ae39c 100644 --- a/src/backend/app/db/models.py +++ b/src/backend/app/db/models.py @@ -1357,6 +1357,12 @@ async def update( async def delete(cls, db: Connection, project_id: int) -> bool: """Delete a project and its related data.""" async with db.cursor() as cur: + await cur.execute( + """ + DELETE FROM submission_photos WHERE project_id = %(project_id)s; + """, + {"project_id": project_id}, + ) await cur.execute( """ DELETE FROM background_tasks WHERE project_id = %(project_id)s; From 4a3a277fe174ade77ae94208c95e23f8691f05e4 Mon Sep 17 00:00:00 2001 From: "Daniel J. Dufour" Date: Fri, 24 Jan 2025 04:39:00 -0500 Subject: [PATCH 20/24] build: fix running docker compose up ui-mapper on MacOS by upgrading node version (#2103) * fixed running docker compose up ui-mapper on MacOS by upgrading node image version Signed-off-by: Daniel J. Dufour * Updated Dockerfiles node version to 22 Signed-off-by: Daniel J. Dufour --------- Signed-off-by: Daniel J. Dufour --- src/Dockerfile.ui.debug | 2 +- src/Dockerfile.ui.prod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dockerfile.ui.debug b/src/Dockerfile.ui.debug index fb7bae09e..410bcccc9 100755 --- a/src/Dockerfile.ui.debug +++ b/src/Dockerfile.ui.debug @@ -1,4 +1,4 @@ -FROM docker.io/node:20-slim +FROM docker.io/node:22-slim RUN set -ex \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install \ diff --git a/src/Dockerfile.ui.prod b/src/Dockerfile.ui.prod index 2471ababb..e3e71d0c8 100644 --- a/src/Dockerfile.ui.prod +++ b/src/Dockerfile.ui.prod @@ -1,4 +1,4 @@ -FROM docker.io/node:20-slim AS base +FROM docker.io/node:22-slim AS base ARG VITE_API_URL ARG VITE_SYNC_URL ENV VITE_API_URL=${VITE_API_URL} \ From 14559e1ca8b3be9c715fa4f2917cc9bbe7fc8d4f Mon Sep 17 00:00:00 2001 From: Sam <78538841+spwoodcock@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:53:50 +0000 Subject: [PATCH 21/24] feat(backend): add `integrations` router with API key functionality (external apps) (#2110) * build: add migration for users.api_key field * feat: add integrations router with external app api key access --- src/backend/app/auth/auth_deps.py | 6 +- src/backend/app/db/models.py | 1 + src/backend/app/integrations/__init__.py | 1 + .../app/integrations/integration_crud.py | 50 ++++++++++ .../app/integrations/integration_deps.py | 65 ++++++++++++ .../app/integrations/integration_routes.py | 98 +++++++++++++++++++ src/backend/app/main.py | 2 + src/backend/app/projects/project_routes.py | 1 - src/backend/migrations/005-api-key.sql | 11 +++ .../migrations/init/fmtm_base_schema.sql | 1 + 10 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 src/backend/app/integrations/__init__.py create mode 100644 src/backend/app/integrations/integration_crud.py create mode 100644 src/backend/app/integrations/integration_deps.py create mode 100644 src/backend/app/integrations/integration_routes.py create mode 100644 src/backend/migrations/005-api-key.sql diff --git a/src/backend/app/auth/auth_deps.py b/src/backend/app/auth/auth_deps.py index 4bea8abde..9af69a3d2 100644 --- a/src/backend/app/auth/auth_deps.py +++ b/src/backend/app/auth/auth_deps.py @@ -19,7 +19,7 @@ """Auth dependencies, for restricted routes and cookie handling.""" from time import time -from typing import Optional +from typing import Annotated, Optional import jwt from fastapi import Header, HTTPException, Request, Response @@ -262,7 +262,7 @@ async def refresh_cookies( async def login_required( - request: Request, access_token: str = Header(None) + request: Request, access_token: Annotated[Optional[str], Header()] = None ) -> AuthUser: """Dependency for endpoints requiring login.""" if settings.DEBUG: @@ -277,7 +277,7 @@ async def login_required( async def mapper_login_required( - request: Request, access_token: str = Header(None) + request: Request, access_token: Annotated[Optional[str], Header()] = None ) -> AuthUser: """Dependency for mapper frontend login.""" if settings.DEBUG: diff --git a/src/backend/app/db/models.py b/src/backend/app/db/models.py index e070ae39c..8653a807c 100644 --- a/src/backend/app/db/models.py +++ b/src/backend/app/db/models.py @@ -159,6 +159,7 @@ class DbUser(BaseModel): tasks_validated: Optional[int] = None tasks_invalidated: Optional[int] = None projects_mapped: Optional[list[int]] = None + api_key: Optional[str] = None registered_at: Optional[AwareDatetime] = None # Relationships diff --git a/src/backend/app/integrations/__init__.py b/src/backend/app/integrations/__init__.py new file mode 100644 index 000000000..75d2e0e97 --- /dev/null +++ b/src/backend/app/integrations/__init__.py @@ -0,0 +1 @@ +"""FMTM integrations API, for linking to external services.""" diff --git a/src/backend/app/integrations/integration_crud.py b/src/backend/app/integrations/integration_crud.py new file mode 100644 index 000000000..c647a2fca --- /dev/null +++ b/src/backend/app/integrations/integration_crud.py @@ -0,0 +1,50 @@ +# Copyright (c) Humanitarian OpenStreetMap Team +# +# This file is part of FMTM. +# +# FMTM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# FMTM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with FMTM. If not, see . +# +"""Logic for integration routes.""" + +from secrets import token_urlsafe + +from loguru import logger as log +from psycopg import Connection +from psycopg.rows import class_row + +from app.db.models import DbUser + + +async def generate_api_token( + db: Connection, + user_id: int, +) -> str: + """Generate a new API token for a given user.""" + async with db.cursor(row_factory=class_row(DbUser)) as cur: + await cur.execute( + """ + UPDATE users + SET api_key = %(api_key)s + WHERE id = %(user_id)s + RETURNING *; + """, + {"user_id": user_id, "api_key": token_urlsafe(32)}, + ) + db_user = await cur.fetchone() + if not db_user.api_key: + msg = f"Failed to generate API Key for user ({user_id})" + log.error(msg) + raise ValueError(msg) + + return db_user.api_key diff --git a/src/backend/app/integrations/integration_deps.py b/src/backend/app/integrations/integration_deps.py new file mode 100644 index 000000000..ccfd758bb --- /dev/null +++ b/src/backend/app/integrations/integration_deps.py @@ -0,0 +1,65 @@ +# Copyright (c) Humanitarian OpenStreetMap Team +# +# This file is part of FMTM. +# +# FMTM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# FMTM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with FMTM. If not, see . +# + +"""Integration dependencies, for API token validation.""" + +from typing import Annotated + +from fastapi import ( + Depends, + Header, +) +from fastapi.exceptions import HTTPException +from psycopg import Connection +from psycopg.rows import class_row + +from app.db.database import db_conn +from app.db.enums import HTTPStatus +from app.db.models import DbUser + + +async def valid_api_token( + db: Annotated[Connection, Depends(db_conn)], + x_api_key: Annotated[str, Header()], +) -> DbUser: + """Check the API token is present for an active database user. + + A header X-API-Key must be provided in the request. + + TODO currently this only checks for a valid key, but does not + TODO include checking roles. + TODO If roles other than 'mapper' are required, this should be integrated. + """ + async with db.cursor(row_factory=class_row(DbUser)) as cur: + await cur.execute( + """ + SELECT * + FROM users + WHERE api_key = %(api_key)s + AND is_email_verified = TRUE; + """, + {"api_key": x_api_key}, + ) + db_user = await cur.fetchone() + if not db_user: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, + detail=f"API key invalid: ({x_api_key})", + ) + + return db_user diff --git a/src/backend/app/integrations/integration_routes.py b/src/backend/app/integrations/integration_routes.py new file mode 100644 index 000000000..92a09222c --- /dev/null +++ b/src/backend/app/integrations/integration_routes.py @@ -0,0 +1,98 @@ +# Copyright (c) Humanitarian OpenStreetMap Team +# +# This file is part of FMTM. +# +# FMTM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# FMTM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with FMTM. If not, see . +# +"""Routes to integrate with external apps, via an API key. + +We handle these endpoints separately to minimise the attach surface +possible from misused API keys (so the entire API is not accessible). + +API keys are inherently not as secure as OAuth flow / JWT token combo. +""" + +from typing import Annotated + +from fastapi import ( + APIRouter, + Depends, +) +from fastapi.exceptions import HTTPException +from fastapi.responses import JSONResponse +from psycopg import Connection + +from app.auth.roles import super_admin +from app.central.central_crud import update_entity_mapping_status +from app.central.central_schemas import EntityMappingStatus, EntityMappingStatusIn +from app.db.database import db_conn +from app.db.enums import HTTPStatus +from app.db.models import DbProject, DbUser +from app.integrations.integration_crud import ( + generate_api_token, +) +from app.integrations.integration_deps import valid_api_token +from app.projects.project_deps import get_project + +router = APIRouter( + prefix="/integrations", + tags=["integrations"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/api-token") +async def get_api_token( + current_user: Annotated[DbUser, Depends(super_admin)], + db: Annotated[Connection, Depends(db_conn)], +): + """Generate and return a new API token. + + This can only be accessed once, and is regenerated on + each call to this endpoint. + + Be sure to store it someplace safe, like a password manager. + + NOTE currently requires super admin permission. + """ + try: + api_key = await generate_api_token(db, current_user.id) + except ValueError as e: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(e), + ) from e + return JSONResponse( + status_code=HTTPStatus.OK, + content={"api_key": api_key}, + ) + + +@router.post( + "/webhooks/entity-status", + response_model=EntityMappingStatus, +) +async def update_entity_status( + current_user: Annotated[DbUser, Depends(valid_api_token)], + project: Annotated[DbProject, Depends(get_project)], + entity_details: EntityMappingStatusIn, +): + """Update the status for an Entity.""" + return await update_entity_mapping_status( + project.odk_credentials, + project.odkid, + entity_details.entity_id, + entity_details.label, + entity_details.status, + ) diff --git a/src/backend/app/main.py b/src/backend/app/main.py index c506867be..a525634f6 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -39,6 +39,7 @@ from app.db.database import db_conn, get_db_connection_pool from app.db.enums import HTTPStatus from app.helpers import helper_routes +from app.integrations import integration_routes from app.monitoring import ( add_endpoint_profiler, instrument_app_otel, @@ -144,6 +145,7 @@ def get_application() -> FastAPI: _app.include_router(auth_routes.router) _app.include_router(submission_routes.router) _app.include_router(organisation_routes.router) + _app.include_router(integration_routes.router) _app.include_router(helper_routes.router) return _app diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 06dae5914..79540f437 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -276,7 +276,6 @@ async def get_odk_entity_mapping_status( async def set_odk_entities_mapping_status( project_user: Annotated[ProjectUserDict, Depends(mapper)], entity_details: central_schemas.EntityMappingStatusIn, - db: Annotated[Connection, Depends(db_conn)], ): """Set the ODK entities mapping status, i.e. in progress or complete. diff --git a/src/backend/migrations/005-api-key.sql b/src/backend/migrations/005-api-key.sql new file mode 100644 index 000000000..bf9d11335 --- /dev/null +++ b/src/backend/migrations/005-api-key.sql @@ -0,0 +1,11 @@ +-- ## Migration add an api_key field to the users table + +-- Start a transaction + +BEGIN; + +ALTER TABLE public.users +ADD COLUMN IF NOT EXISTS api_key CHARACTER VARYING; + +-- Commit the transaction +COMMIT; diff --git a/src/backend/migrations/init/fmtm_base_schema.sql b/src/backend/migrations/init/fmtm_base_schema.sql index 10c060a4d..56a2e3558 100644 --- a/src/backend/migrations/init/fmtm_base_schema.sql +++ b/src/backend/migrations/init/fmtm_base_schema.sql @@ -356,6 +356,7 @@ CREATE TABLE public.users ( tasks_validated integer NOT NULL DEFAULT 0, tasks_invalidated integer NOT NULL DEFAULT 0, projects_mapped integer [], + api_key character varying, registered_at timestamp with time zone DEFAULT now() ); ALTER TABLE public.users OWNER TO fmtm; From 23f6c39dec176026758a254b4e8be8b071e8d69f Mon Sep 17 00:00:00 2001 From: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:33:06 +0545 Subject: [PATCH 22/24] refactor(mapper): feature legend & layer-switcher (#2107) * fix(bottom-sheet): adjust z-index * refactor(main): seperate map control symbol and component * refactor(legend): show feature level legend and refactor accordingly * refactor(layer-switcher): remove basemap toggle icon, refactor accordingly * fix(legend): update legend heading --- .../src/lib/components/bottom-sheet.svelte | 2 +- .../lib/components/map/layer-switcher.svelte | 70 ++++++---------- .../src/lib/components/map/legend.svelte | 73 +++++------------ src/mapper/src/lib/components/map/main.svelte | 82 +++++++++++++++++-- 4 files changed, 120 insertions(+), 107 deletions(-) diff --git a/src/mapper/src/lib/components/bottom-sheet.svelte b/src/mapper/src/lib/components/bottom-sheet.svelte index 556f0f94d..0713e4f9d 100644 --- a/src/mapper/src/lib/components/bottom-sheet.svelte +++ b/src/mapper/src/lib/components/bottom-sheet.svelte @@ -91,7 +91,7 @@
diff --git a/src/mapper/src/lib/components/map/layer-switcher.svelte b/src/mapper/src/lib/components/map/layer-switcher.svelte index 88bf34d06..27911037c 100644 --- a/src/mapper/src/lib/components/map/layer-switcher.svelte +++ b/src/mapper/src/lib/components/map/layer-switcher.svelte @@ -24,7 +24,6 @@ map = new Map({ -
(isOpen = false)}> -
(isOpen = !isOpen)} - role="button" - onkeydown={(e) => { - if (e.key === 'Enter') { - isOpen = !isOpen; - } - }} - tabindex="0" - > - Basemap Icon -
-
-

Base Maps

-
- {#each allStyles as style, _} -
selectStyle(style)} - role="button" - onkeydown={(e) => { - if (e.key === 'Enter') selectStyle(style); - }} - tabindex="0" - > - Style Thumbnail - {style.name} -
- {/each} -
+
+

Base Maps

+
+ {#each allStyles as style, _} +
selectStyle(style)} + role="button" + onkeydown={(e) => { + if (e.key === 'Enter') selectStyle(style); + }} + tabindex="0" + > + Style Thumbnail + {style.name} +
+ {/each}
diff --git a/src/mapper/src/lib/components/map/legend.svelte b/src/mapper/src/lib/components/map/legend.svelte index 738f36df7..258dc66ee 100644 --- a/src/mapper/src/lib/components/map/legend.svelte +++ b/src/mapper/src/lib/components/map/legend.svelte @@ -1,62 +1,29 @@ -
(isOpen = false)} class="relative font-barlow"> -
(isOpen = !isOpen)} - role="button" - onkeydown={(e) => { - if (e.key === 'Enter') { - isOpen = !isOpen; - } - }} - tabindex="0" - > - -
-
-

Legend

- {#each taskStatuses as taskStatus} -
- {#if !taskStatus.color} -
- Lock Icon -
- {:else} -
- {/if} -

{taskStatus?.status}

-
- {/each} -
+
+

Legend (Features)

+ {#each taskStatuses as taskStatus} +
+
+

{taskStatus?.status}

+
+ {/each}
- - diff --git a/src/mapper/src/lib/components/map/main.svelte b/src/mapper/src/lib/components/map/main.svelte index 149cc89da..a50964b2b 100644 --- a/src/mapper/src/lib/components/map/main.svelte +++ b/src/mapper/src/lib/components/map/main.svelte @@ -43,6 +43,7 @@ import { projectSetupStep as projectSetupStepEnum, NewGeomTypes } from '$constants/enums.ts'; import { baseLayers, osmStyle, pmtilesStyle } from '$constants/baseLayers.ts'; import { getEntitiesStatusStore } from '$store/entities.svelte.ts'; + import { clickOutside } from '$lib/utils/clickOutside.ts'; type bboxType = [number, number, number, number]; @@ -80,6 +81,8 @@ let projectSetupStep: number | null = $state(null); let lineWidth = $state(1); // Initial line width of the rejected entities let expanding = true; // Whether the line is expanding + let selectedControl: 'layer-switcher' | 'legend' | null = $state(null); + let selectedStyleUrl: string | undefined = $state(undefined); // Trigger adding the PMTiles layer to baselayers, if PmtilesUrl is set let allBaseLayers: maplibregl.StyleSpecification[] = $derived( @@ -371,14 +374,44 @@ tabindex="0" >
- - - +
{ + selectedControl = 'layer-switcher'; + }} + role="button" + onkeydown={(e) => { + if (e.key === 'Enter') { + selectedControl = 'layer-switcher'; + } + }} + tabindex="0" + > + Basemap Icon +
+
(selectedControl = 'legend')} + role="button" + onkeydown={(e) => { + if (e.key === 'Enter') { + selectedControl = 'legend'; + } + }} + tabindex="0" + > + +
@@ -537,3 +570,36 @@
{/if} + +
(selectedControl = null)} + class={`font-barlow flex justify-center !w-[100vw] absolute left-0 z-20 duration-400 ${selectedControl ? 'bottom-[4rem]' : '-bottom-[100%] pointer-events-none'}`} +> +
+
+ (selectedControl = null)} + onkeydown={(e: KeyboardEvent) => { + if (e.key === 'Enter') { + selectedControl = null; + } + }} + role="button" + tabindex="0" + > +
+ (selectedStyleUrl = style)} + isOpen={selectedControl === 'layer-switcher'} + > + +
+
From be6c76a6479706f37fa58b09ab041bc1260c3cd2 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Fri, 24 Jan 2025 13:54:00 +0000 Subject: [PATCH 23/24] =?UTF-8?q?bump:=20version=202024.5.0=20=E2=86=92=20?= =?UTF-8?q?2025.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++++ chart/Chart.yaml | 2 +- src/backend/app/__version__.py | 2 +- src/backend/pyproject.toml | 4 +-- src/frontend/package.json | 2 +- src/mapper/package.json | 2 +- 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0a8e64e..3c99bd8af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## 2025.1.0 (2025-01-24) + +### Feat + +- **backend**: add `integrations` router with API key functionality (external apps) (#2110) +- entity pulse effect on rejected submissions (#2018) +- **mapper**: distance constraint add on frontend (#2084) +- **frontend**: submission table date range filter (#2091) +- **backend**: get api for project's geometry log (#2090) +- capability to draw new polygon and linestring geoms (#2082) +- **backend**: add filters for submission date in submission table and downloads (#2077) +- **backend**: osm-fieldwork --> 0.18.0 (submission filter param + config new feat geom type) +- **geolocation**: comments add +- add submission ids in entities statuses endpoint (#2038) +- consider every additional entities without clipping them with AOI (#2017) +- update the version of osm-fieldwork (#2029) +- **backend**: send org approval message to creator (#2008) +- add submission ids as a dataset property for the entities (#2007) +- update the version of fmtm-splitter 2.0.0 (#1996) +- **mapper**: project details section add to bottom sheet (#1994) +- **mapper**: prompt user to download custom ODK Collect on first load (#1989) + +### Fix + +- **backend**: delete submission photos while deleting project to avoid foreignkey constraint (#2112) +- **backend**: review state for received on submission table (#2101) +- **+page**: remove irrelevant subscribeToEntityStatusUpdates call in onMount +- use task index instead of id in task boundary geojson properties (#2095) +- **mapper**: get entities after page load to speed up first paint (#2051) +- **backend**: get total_tasks count on single project response +- **backend**: various fixes based on sentry error reports (#2053) +- default odk creds when organisation do not have their own during project creation (#2070) +- change geom to geojson in db model +- **generateBasemap**: update tile source option value (#2050) +- **geolocation**: fetch routing api on every 10 seconds +- **dialog-entities-actions**: show alert instead of turning on location on navigation +- **createProjecSlice**: clear additionalFeatureGeojson state after successful project creation (#2041) +- compose file name for backend test stage +- **mapper**: replace task id with index on activities panel (#2002) +- **mapper**: task id with task index on mapper frontend (#1997) +- **backend**: include organisation name in minimal project query results (#1993) +- **frontend**: small fix to reset frontend login if cookie refresh fails +- **mapper**: offline mode button visibility & basemap component TS type error (#1990) +- parse geojson to featcol in generate data extract (#1983) + +### Refactor + +- **mapper**: feature legend & layer-switcher (#2107) +- **mapper**: relocate entity sync button (#2100) +- **frontend**: organization management pages (#2097) +- **frontend**: JS to TS conversion: update useDispatch to useAppDispatch (#2076) +- **frontend**: update TS types on actions (#2054) +- replace incorrect osm libya logo with official osm logo +- **frontend**: terminologies and wording update for users (#1978) +- **mapper**: ts errors on frontend (#2006) +- **mapper**: add mapping guide to sidebar + update links + ## 2024.5.0 (2024-12-11) ### Feat diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 04bdfb10e..be947c54a 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -3,7 +3,7 @@ type: application name: fmtm description: Field Mapping Tasking Manager - coordinated field mapping. version: "0.1.0" -appVersion: "2024.5.0" +appVersion: "2025.1.0" maintainers: - email: sam.woodcock@hotosm.org name: Sam Woodcock diff --git a/src/backend/app/__version__.py b/src/backend/app/__version__.py index ff12fec95..e3fccb90e 100644 --- a/src/backend/app/__version__.py +++ b/src/backend/app/__version__.py @@ -1 +1 @@ -__version__ = "2024.5.0" +__version__ = "2025.1.0" diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 3fedc51d5..7d1fe45c1 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -18,7 +18,7 @@ [project] name = "fmtm" -version = "2024.5.0" +version = "2025.1.0" description = "Field Mapping Tasking Manager - coordinated field mapping." authors = [ {name = "HOTOSM", email = "sysadmin@hotosm.org"}, @@ -127,7 +127,7 @@ asyncio_default_fixture_loop_scope="session" [tool.commitizen] name = "cz_conventional_commits" -version = "2024.5.0" +version = "2025.1.0" version_files = [ "pyproject.toml:version", "app/__version__.py", diff --git a/src/frontend/package.json b/src/frontend/package.json index 43721dc95..d63d87863 100755 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,6 +1,6 @@ { "name": "fmtm", - "version": "2024.5.0", + "version": "2025.1.0", "scripts": { "build": "vite build", "build:dev": "vite build --mode development", diff --git a/src/mapper/package.json b/src/mapper/package.json index 9a3752d2d..9cdb99f0d 100644 --- a/src/mapper/package.json +++ b/src/mapper/package.json @@ -1,6 +1,6 @@ { "name": "fmtm-mapper", - "version": "2024.5.0", + "version": "2025.1.0", "type": "module", "private": true, "scripts": { From 0b03b6506c5af0c4c0ace491afe1a7f776b710e8 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Fri, 24 Jan 2025 14:08:01 +0000 Subject: [PATCH 24/24] build: relock uv.lock after version bump --- src/backend/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/uv.lock b/src/backend/uv.lock index c8230fd40..6bf7aa74b 100644 --- a/src/backend/uv.lock +++ b/src/backend/uv.lock @@ -493,7 +493,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/3e/0d/424de6e5612f1399f [[package]] name = "fmtm" -version = "2024.5.0" +version = "2025.1.0" source = { virtual = "." } dependencies = [ { name = "asgi-lifespan" },