Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new observation form for custom observations #70

Merged
merged 34 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2272fa2
Add new observation form for custom observations
Bo-Duke Apr 24, 2024
3133c9a
Test contribution endpoint
Bo-Duke Apr 25, 2024
d228d60
Add stations details
Bo-Duke Apr 25, 2024
168935a
Add Observation details endpoints
Bo-Duke Apr 29, 2024
4c4da7e
Add observation details page
Bo-Duke Apr 29, 2024
d9e72fb
Auto select station in observation form
Bo-Duke Apr 29, 2024
e8a6340
Add pictures support to custom observations
Bo-Duke Apr 29, 2024
ce5858e
Remove file category select
Bo-Duke Apr 29, 2024
1cee24e
Dont show default observation types when custom ones exist
Bo-Duke Apr 29, 2024
f0db884
PR review
Bo-Duke Apr 29, 2024
d5ef7e9
allow specific branch docker build
submarcos Apr 29, 2024
ef7a4a8
Add password support for observation form
Bo-Duke May 2, 2024
f61bd06
Add attribution to map
Bo-Duke May 2, 2024
b4e2d09
Fix grouped layers causing crash
Bo-Duke May 3, 2024
416fa2c
Feat: Add button to redirect user to external page if link on content
babastienne May 3, 2024
9025d05
Use LinkAsButton component
Bo-Duke May 3, 2024
fad7548
5 minutes cache instead of one hour
Bo-Duke May 3, 2024
14de0d0
Add cache revalidation endpoint
Bo-Duke May 3, 2024
dbda3e5
Add bad password error on contribution form
Bo-Duke May 3, 2024
9903e7e
Open external link in new tab
Bo-Duke May 3, 2024
cea0331
feat: add external uri button on Station
babastienne May 7, 2024
b45e2f2
Convert null value from postObservation form to empty string
dtrucs Jul 3, 2024
8915014
Bump @bhch/react-json-form from 2.13.5 to 2.14.1
dtrucs Jul 8, 2024
a5f7ce0
Add missing keys for loops inside jsx
dtrucs Nov 12, 2024
5e87698
Call getObservations in server side
dtrucs Nov 12, 2024
3d76880
Fix 404 for legacy observation details and pois
Bo-Duke Feb 6, 2025
ea8fdf0
PR review fixes
Bo-Duke Feb 6, 2025
c352d9f
Fix typescript showing useless errors
Bo-Duke Feb 6, 2025
51ccc48
Add key prop for JSX item inside loop
dtrucs Feb 10, 2025
1535e40
Parallelize some queries
dtrucs Feb 10, 2025
f56a89e
Remove useless node-fetch package
dtrucs Feb 10, 2025
4e87171
Bump @bhch/react-json-form from 2.14.1 to 2.14.4
dtrucs Feb 11, 2025
34d67ec
Connect each input with its label
dtrucs Feb 11, 2025
ffc3040
Define accept images to input file
dtrucs Feb 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: Release docker image

on:
push:
branches:
- main
- new_observation_types
release:
types: [created]

Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '@bhch/react-json-form';
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"dependencies": {
"@20tab/react-leaflet-resetview": "^1.1.0",
"@bhch/react-json-form": "^2.14.4",
"@hookform/resolvers": "^3.1.0",
"@radix-ui/react-accordion": "^1.1.1",
"@radix-ui/react-dialog": "^1.0.4",
Expand All @@ -38,6 +39,7 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-leaflet": "^4.2.1",
"react-modal": "^3.16.1",
"sharp": "^0.32.6",
"slugify": "^1.6.6",
"tailwind-merge": "^2.2.2",
Expand Down
129 changes: 129 additions & 0 deletions src/api/customObservations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { JSONSchema } from 'json-schema-yup-transformer/dist/schema';

import { Attachement } from './settings';

export type Observation = {
id: number;
label: string;
description: string;
json_schema_form: JSONSchema;
stations: number[];
password_required?: boolean;
};

export type ObservationDetails = {
values?: { id: string; value: any; label?: string }[];
id: string;
contributedAt?: string;
label?: string;
description?: string;
attachments?: Attachement[];
geometry?: {
coordinates: number[];
};
};

type ObservationListItem = {
id: number;
contributed_at: string;
attachments: Attachement[];
};

async function fetchObservations(): Promise<Observation[]> {
const res = await fetch(
`${process.env.apiHost}/api/portal/fr/${process.env.portal}/custom-contribution-types/`,
{
next: { revalidate: 5 * 60, tags: ['admin', 'contributions'] },
headers: {
Accept: 'application/json',
},
},
);
if (res.status < 200 || res.status > 299) {
return [];
}
return res.json();
}

async function fetchObservation(id: string): Promise<Observation | null> {
const res = await fetch(
`${process.env.apiHost}/api/portal/fr/${process.env.portal}/custom-contribution-types/${id}`,
{
next: { revalidate: 5 * 60, tags: ['admin', 'contributions'] },
headers: {
Accept: 'application/json',
},
},
);
if (res.status < 200 || res.status > 299) {
return null;
}
return res.json();
}

async function fetchObservationDetails(
id: string,
): Promise<ObservationListItem[] | null> {
const res = await fetch(
`${process.env.apiHost}/api/portal/fr/${process.env.portal}/custom-contribution-types/${id}/contributions`,
{
next: { revalidate: 5 * 60, tags: ['admin', 'contributions'] },
headers: {
Accept: 'application/json',
},
},
);
if (res.status < 200 || res.status > 299) {
return null;
}
return res.json();
}

export async function getObservationDetails(
type: string,
id: string,
): Promise<ObservationDetails | null> {
const [schema, detailsList] = await Promise.all([
fetchObservation(type),
fetchObservationDetails(type),
]);
const values = detailsList?.find(detail => String(detail.id) === id);

if (!values) return null;

const details = {
values: Object.entries(values)
.filter(([key]) => schema?.json_schema_form.properties?.[key])
.map(([key, value]) => ({
id: key,
value,
label: (schema?.json_schema_form.properties?.[key] as any)?.title,
})),
id,
contributedAt: values.contributed_at,
label: schema?.label,
description: schema?.description,
attachments: values.attachments,
};

return details;
}

export async function getObservation(id: string) {
const observation = await fetchObservation(id);
if (!observation || !observation.json_schema_form) return null;
return {
...observation,
json_schema_form: {
...observation.json_schema_form,
properties: {
...observation.json_schema_form.properties,
},
},
};
}

export async function getObservations() {
const observations = await fetchObservations();
return observations;
}
13 changes: 10 additions & 3 deletions src/api/details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getDetailsUrl } from './settings';

async function fetchDetails(url: string) {
const res = await fetch(`${process.env.apiHost}${url}`, {
next: { revalidate: 60 * 60 },
next: { revalidate: 5 * 60, tags: ['details', 'admin'] },
headers: {
Accept: 'application/json',
},
Expand All @@ -26,9 +26,16 @@ export async function getDetails(path: string, id: number) {
}
let details = null;
try {
details = await fetchDetails(`${endpoint}${id}`);
details = await fetchDetails(
`/api/portal/fr/${process.env.portal}/${path}/${id}`,
);
} catch (e) {
// notfound
try {
details = await fetchDetails(`/api/portal/fr/${path}/${id}`);
} catch (e) {
return null;
}
return details;
}
return details;
}
2 changes: 1 addition & 1 deletion src/api/geojson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GeoJSON } from 'geojson';

async function fetchGeoJSON(url: string) {
const res = await fetch(`${process.env.apiHost}${url}`, {
next: { revalidate: 60 * 60 },
next: { revalidate: 20 * 60, tags: ['admin', 'geojson'] },
});
if (res.status < 200 || res.status > 299) {
return null;
Expand Down
49 changes: 39 additions & 10 deletions src/api/observations.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import {
JSONSchema,
JSONSchemaType,
} from 'json-schema-yup-transformer/dist/schema';

import { getCorrespondingPath } from '@/lib/utils';

import { Attachement } from './settings';

export type PostObservationProps = {
geom: string;
properties: string;
files: string;
};

export type ObservationDetails = {
id: number;
type: string;
category: string;
description: string;
attachments?: Attachement[];
};

async function fetchObservation() {
const res = await fetch(
`${process.env.apiHost}/api/portal/fr/${process.env.portal}/contributions/json_schema/`,
{
next: { revalidate: 60 * 60 },
next: { revalidate: 5 * 60, tags: ['admin', 'observations'] },
headers: {
Accept: 'application/json',
},
},
);
if (res.status < 200 || res.status > 299) {
return {};
}
return res.json();
}

async function fetchObservationDetails(id: string) {
const res = await fetch(
`${process.env.apiHost}/api/portal/fr/${process.env.portal}/contributions/${id}/`,
{
next: { revalidate: 5 * 60, tags: ['admin', 'observations'] },
headers: {
Accept: 'application/json',
},
Expand Down Expand Up @@ -79,10 +100,10 @@ async function postObservation(props: PostObservationProps) {
}
}

function observationAdapter(json: JSONSchema, path: string) {
function observationAdapter(json: any, path: string) {
const { category, ...jsonProperties } = json.properties;
const { then: type } = json.allOf.find(
(item: JSONSchemaType) =>
const { then: type } = json.allOf?.find(
(item: any) =>
item.if.properties.category?.const === getCorrespondingPath(path),
);
return {
Expand All @@ -103,12 +124,20 @@ function observationAdapter(json: JSONSchema, path: string) {
...jsonProperties,
...type.properties,
},
required: [...json.required, 'lng', 'lat'].filter(
required: [...(json.required ?? []), 'lng', 'lat'].filter(
item => item !== 'category',
),
};
}

export async function getObservationDetails(
id: string,
): Promise<ObservationDetails | null> {
const observationDetails = fetchObservationDetails(id);

return observationDetails;
}

export async function getObservationJsonSchema(path: string) {
const rawObservation = await fetchObservation();
return observationAdapter(rawObservation, path);
Expand Down
2 changes: 1 addition & 1 deletion src/api/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Menu, getMenuSettings } from './settings';

async function fetchDetails(url: string) {
const res = await fetch(`${process.env.apiHost}${url}`, {
next: { revalidate: 60 * 60 },
next: { revalidate: 5 * 60, tags: ['admin', 'staticpages'] },
headers: {
Accept: 'application/json',
},
Expand Down
2 changes: 1 addition & 1 deletion src/api/poi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function fetchPois({ params }: FetchPoisProps = {}): Promise<Poi[]> {
headers: {
Accept: 'application/json',
},
next: { revalidate: 60 * 60 },
next: { revalidate: 5 * 60, tags: ['admin', 'pois'] },
});
if (res.status < 200 || res.status > 299) {
return [];
Expand Down
56 changes: 56 additions & 0 deletions src/api/postObservation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
async function postObservation(
props: { [key: string]: string | Blob },
id: string,
formData: FormData,
) {
const body = new FormData();

Object.entries(props).forEach(([key, value]) => {
// `null` in formData is converted to `'null'`
// We convert them to an empty string to avoid it
const nextValue = value !== null ? value : '';
body.append(key, nextValue);
});

Array.from({ length: 5 }).forEach((_, index) => {
const number = index + 1;
const file = formData.get(`file${number}-file`) as File;
if (file && file.size > 0) {
body.append(`file${number}`, file);
}
});

try {
const res = await fetch(
`${process.env.apiHost}/api/portal/fr/${process.env.portal}/custom-contribution-types/${id}/contributions/`,
{
method: 'POST',
body,
},
).catch(errorServer => {
throw Error(errorServer);
});
if (res.status > 499) {
throw Error(res.statusText);
}
const json = await res.json();

if (res.status < 200 || res.status > 299) {
return { error: true, message: json };
}
return { error: false, message: json };
} catch (error) {
let message = 'Unknown Error';
if (error instanceof Error) message = error.message;
return { error: true, message };
}
}

export async function handleSubmitCustomObservation(
body: { [key: string]: string | Blob },
id: string,
formData: FormData,
) {
'use server';
return await postObservation(body, id, formData);
}
6 changes: 4 additions & 2 deletions src/api/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ type BaseLayers = {
id: number;
label: string;
url: string;
attribution: string;
control: {
attribution: string;
};
}[];

export type RawLayer = {
Expand Down Expand Up @@ -156,7 +158,7 @@ export async function fetchSettings(): Promise<RawSettings> {
const res = await fetch(
`${process.env.apiHost}/api/portal/fr/portal/${process.env.portal}/`,
{
next: { revalidate: 60 * 60 },
next: { revalidate: 5 * 60, tags: ['admin', 'settings'] },
headers: {
Accept: 'application/json',
},
Expand Down
Loading