diff --git a/api/src/handlers.rs b/api/src/handlers.rs index 49a320f92..a0a479199 100644 --- a/api/src/handlers.rs +++ b/api/src/handlers.rs @@ -247,6 +247,34 @@ pub async fn update_project( Ok(StatusCode::NO_CONTENT) } +#[axum_macros::debug_handler] +pub async fn delete_project( + Path(id): Path, + Extension(pool): Extension, + Extension(client): Extension, +) -> Result { + // Delete assets from bucket + let saved_project: Project = sqlx::query_scalar!( + r#"SELECT project as "project: sqlx::types::Json" FROM projects WHERE id = $1"#, + id + ) + .fetch_one(&pool) + .await? + .0; + + if !saved_project.assets.is_empty() { + delete_assets(client, &saved_project.assets).await + } + + // Delete project from database + sqlx::query(r#"DELETE FROM projects WHERE id = $1"#) + .bind(id) + .execute(&pool) + .await?; + + Ok(StatusCode::NO_CONTENT) +} + #[axum_macros::debug_handler] pub async fn update_project_geometries( Path(id): Path, @@ -449,3 +477,29 @@ async fn save_assets(client: Client, project_assets: &Vec) { } } } + +async fn delete_assets(client: Client, project_assets: &Vec) { + let bucket = std::env::var("PROJECTS_S3_BUCKET").unwrap(); + for asset in project_assets { + let permanent_key = format!("assets/saved/{}", asset.key); + + // Check if the file exists in the destination directory + let destination_exists = client + .head_object() + .bucket(&bucket) + .key(&permanent_key) + .send() + .await + .is_ok(); + + if destination_exists { + client + .delete_object() + .bucket(&bucket) + .key(&permanent_key) + .send() + .await + .unwrap(); + } + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs index 069a3f38b..70c0d1ec7 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,6 +1,7 @@ use axum::{ extract::Extension, http::{HeaderValue, Method}, + routing::delete, routing::get, routing::post, routing::put, @@ -48,7 +49,9 @@ pub async fn app(pool: PgPool) -> Router { .route("/api/projects/duplicate", post(handlers::duplicate_project)) .route( "/api/projects/:id", - get(handlers::get_project).put(handlers::update_project), + get(handlers::get_project) + .put(handlers::update_project) + .delete(handlers::delete_project), ) .route( "/api/projects/:id/geometries", diff --git a/ui/locales/app.de.json b/ui/locales/app.de.json index 6f2276a53..6ccb1949c 100644 --- a/ui/locales/app.de.json +++ b/ui/locales/app.de.json @@ -17,6 +17,8 @@ "created_on": "Erstellt am", "dashboard_back_to_topics": "Zurück zu den Themen", "dashboard_by_swisstopo_title": "von swisstopo", + "dashboard_delete_warning_description": "", + "dashboard_delete_warning_title": "", "dashboard_description": "Kurzbeschrieb", "dashboard_modified_title": "Geändert am", "dashboard_my_projects": "Meine Projekte", diff --git a/ui/locales/app.en.json b/ui/locales/app.en.json index 42e01ed34..8db2e60b9 100644 --- a/ui/locales/app.en.json +++ b/ui/locales/app.en.json @@ -17,6 +17,8 @@ "created_on": "Created on", "dashboard_back_to_topics": "Back to overview topics", "dashboard_by_swisstopo_title": "by swisstopo", + "dashboard_delete_warning_description": "The project and its assests will be permanently deleted. This action cannot be undone.", + "dashboard_delete_warning_title": "Are you sure you want to delete?", "dashboard_description": "Description", "dashboard_modified_title": "Modified on", "dashboard_my_projects": "My projects", diff --git a/ui/locales/app.fr.json b/ui/locales/app.fr.json index 8f6f4d978..f15d25452 100644 --- a/ui/locales/app.fr.json +++ b/ui/locales/app.fr.json @@ -17,6 +17,8 @@ "created_on": "Créé le", "dashboard_back_to_topics": "Retour aux thèmes", "dashboard_by_swisstopo_title": "par swisstopo", + "dashboard_delete_warning_description": "", + "dashboard_delete_warning_title": "", "dashboard_description": "Description", "dashboard_modified_title": "Modifié le", "dashboard_my_projects": "Mes projets", diff --git a/ui/locales/app.it.json b/ui/locales/app.it.json index b6b20677e..a944dda20 100644 --- a/ui/locales/app.it.json +++ b/ui/locales/app.it.json @@ -17,6 +17,8 @@ "created_on": "Creato lo", "dashboard_back_to_topics": "Tornare a temi", "dashboard_by_swisstopo_title": "da swisstopo", + "dashboard_delete_warning_description": "", + "dashboard_delete_warning_title": "", "dashboard_description": "Descrizione", "dashboard_modified_title": "Modificato il", "dashboard_my_projects": "I miei progetti", diff --git a/ui/src/api-client.ts b/ui/src/api-client.ts index 2e6cc0bd6..b7507fa1d 100644 --- a/ui/src/api-client.ts +++ b/ui/src/api-client.ts @@ -43,6 +43,20 @@ class ApiClient { }); } + deleteProject(id: string): Promise { + const headers = {}; + addAuthorization(headers, this.token); + + return fetch(`${this.apiUrl}/projects/${id}`, { + method: 'DELETE', + headers: headers, + }) + .then(response => { + this.refreshProjects(); + return response; + }); + } + updateProjectGeometries(id: string, geometries: NgmGeometry[]): Promise { const headers = { 'Content-Type': 'application/json' diff --git a/ui/src/elements/dashboard/ngm-dashboard.ts b/ui/src/elements/dashboard/ngm-dashboard.ts index 254310039..97068762d 100644 --- a/ui/src/elements/dashboard/ngm-dashboard.ts +++ b/ui/src/elements/dashboard/ngm-dashboard.ts @@ -513,6 +513,7 @@ export class NgmDashboard extends LitElementI18n { @onDeselect="${this.deselectTopicOrProject}" @onEdit="${this.onProjectEdit}" @onProjectDuplicated="${(evt: {detail: {project: Project}}) => this.onProjectDuplicated(evt.detail.project)}" + @onProjectDeleted="${() => this.deselectTopicOrProject()}" >`} diff --git a/ui/src/elements/dashboard/ngm-delete-warning-modal.ts b/ui/src/elements/dashboard/ngm-delete-warning-modal.ts new file mode 100644 index 000000000..2e50a7d6f --- /dev/null +++ b/ui/src/elements/dashboard/ngm-delete-warning-modal.ts @@ -0,0 +1,57 @@ +import {html} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import $ from '../../jquery.js'; +import i18next from 'i18next'; +import {LitElementI18n} from '../../i18n.js'; + +import 'fomantic-ui-css/components/dimmer.js'; +import 'fomantic-ui-css/components/modal.js'; + +@customElement('ngm-delete-warning-modal') +export class NgmDeleteWarningModal extends LitElementI18n { + @property({type: Boolean}) + accessor show = false; + element; + + firstUpdated(_changedProperties) { + this.element = $('.ngm-delete-warning-modal.ui.modal').modal({ + centered: true, + onHidden: () => this.show = false, + onApprove: () => this.dispatchEvent(new CustomEvent('onProjectDeleted', {bubbles: true})) + }); + super.firstUpdated(_changedProperties); + } + + updated(changedProperties) { + if (changedProperties.has('show') && this.show) { + this.element.modal('show'); + } else if (!this.show) { + this.element.modal('hide'); + } + super.updated(changedProperties); + } + + render() { + return html` + + `; + } + + createRenderRoot() { + // no shadow dom + return this; + } +} diff --git a/ui/src/elements/dashboard/ngm-project-topic-overview.ts b/ui/src/elements/dashboard/ngm-project-topic-overview.ts index 3a67166f2..44b72e4d1 100644 --- a/ui/src/elements/dashboard/ngm-project-topic-overview.ts +++ b/ui/src/elements/dashboard/ngm-project-topic-overview.ts @@ -1,4 +1,4 @@ -import {customElement, property} from 'lit/decorators.js'; +import {customElement, property, query} from 'lit/decorators.js'; import {LitElementI18n, toLocaleDateString, translated} from '../../i18n'; import {html, PropertyValues} from 'lit'; import i18next from 'i18next'; @@ -12,6 +12,7 @@ import $ from '../../jquery'; import {DEFAULT_PROJECT_COLOR} from '../../constants'; import './ngm-project-geoms-section'; import './ngm-project-assets-section'; +import './ngm-delete-warning-modal'; @customElement('ngm-project-topic-overview') export class NgmProjectTopicOverview extends LitElementI18n { @@ -25,6 +26,8 @@ export class NgmProjectTopicOverview extends LitElementI18n { accessor userEmail: string = ''; @property({type: Number}) accessor selectedViewIndx: number | undefined; + @query('ngm-delete-warning-modal') + accessor deleteWarningModal; shouldUpdate(_changedProperties: PropertyValues): boolean { return this.topicOrProject !== undefined; @@ -42,6 +45,9 @@ export class NgmProjectTopicOverview extends LitElementI18n { const backgroundImage = this.topicOrProject.image?.length ? `url('${this.topicOrProject.image}')` : 'none'; return html` +
${translated(this.topicOrProject.title)} @@ -125,6 +131,11 @@ export class NgmProjectTopicOverview extends LitElementI18n { ${i18next.t('dashboard_share_topic_email')} +
this.deleteWarningModal.show = true}> + ${i18next.t('delete')} +
`; } @@ -138,6 +149,10 @@ export class NgmProjectTopicOverview extends LitElementI18n { this.dispatchEvent(new CustomEvent('onProjectDuplicated', {detail: {project}})); } + async deleteProject() { + await apiClient.deleteProject(this.topicOrProject!.id); + } + async copyLink(viewId?: string) { try { const link = this.getLink(viewId); diff --git a/ui/src/style/ngm-gst-modal.css b/ui/src/style/ngm-gst-modal.css index 6ef31eef2..0ce2688da 100644 --- a/ui/src/style/ngm-gst-modal.css +++ b/ui/src/style/ngm-gst-modal.css @@ -1,9 +1,11 @@ -.ngm-gst-modal.ui.modal embed { +.ngm-gst-modal.ui.modal embed, +.ngm-delete-warning-modal embed { width: 100%; height: 500px; } -.ngm-gst-modal .actions { +.ngm-gst-modal .actions, +.ngm-delete-warning-modal .actions { display: flex; justify-content: end; }