feat: suppression images + gestion tags (ajout/suppression)

Backend:
- ImagoClient::delete_image() — DELETE /api/v1/images/{id}
- ImagoClient::update_tags() — PUT /api/v1/images/{id}/tags
- Route DELETE /api/images/{id}
- Route PUT /api/images/{id}/tags

Frontend:
- Bouton 🗑️ sur chaque carte (visible au hover)
- Boîte de confirmation avant suppression
- Tags cliquables avec × pour suppression
- Bouton + tag pour ajouter un tag (Enter pour valider)
- Bouton Supprimer + tags éditables dans la modale
- Suppression + modif tags aussi dans la modale

⚠️ Nécessite redéploiement Imago pour le endpoint PUT /tags
This commit is contained in:
2026-06-23 15:20:26 -04:00
parent 8256d4c362
commit 1ec0d4fbe8
3 changed files with 238 additions and 10 deletions
+65 -2
View File
@@ -1,7 +1,7 @@
//! Routes API REST du dashboard — proxy vers Imago v2
use axum::{
Router, routing::{get, post},
Router, routing::{get, post, put, delete},
extract::{Multipart, Path, Query, State},
http::HeaderMap,
response::IntoResponse,
@@ -97,7 +97,8 @@ pub fn api_routes(state: AppState) -> Router {
.route("/api/upload", post(upload_image))
.route("/api/images", get(list_images))
.route("/api/images/search", get(search_images))
.route("/api/images/{id}", get(get_image))
.route("/api/images/{id}", get(get_image).delete(delete_image))
.route("/api/images/{id}/tags", put(update_tags))
.route("/api/images/{id}/thumbnail", get(get_thumbnail))
.route("/api/stats", get(stats))
.with_state(state)
@@ -447,6 +448,68 @@ async fn get_thumbnail(
}
}
async fn delete_image(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> impl IntoResponse {
let img_id: i64 = match id.parse() {
Ok(n) => n,
Err(_) => return error_response(400, "ID invalide"),
};
let client = match get_client(&state, &headers) {
Ok(c) => c,
Err(e) => return error_response(500, &format!("Erreur client : {}", e)),
};
match client.delete_image(img_id).await {
Ok(()) => (
axum::http::StatusCode::OK,
Json(serde_json::json!({"deleted": true, "id": img_id})),
),
Err(e) => {
let err_msg = format!("{:#}", e);
let status = if err_msg.contains("403") || err_msg.contains("401") { 403 } else { 500 };
error_response(status, &format!("Échec suppression : {}", err_msg))
}
}
}
/// Requête de mise à jour des tags
#[derive(Deserialize)]
pub struct TagsUpdate {
pub tags: Vec<String>,
}
async fn update_tags(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
Json(body): Json<TagsUpdate>,
) -> impl IntoResponse {
let img_id: i64 = match id.parse() {
Ok(n) => n,
Err(_) => return error_response(400, "ID invalide"),
};
let client = match get_client(&state, &headers) {
Ok(c) => c,
Err(e) => return error_response(500, &format!("Erreur client : {}", e)),
};
match client.update_tags(img_id, body.tags.clone()).await {
Ok(()) => (
axum::http::StatusCode::OK,
Json(serde_json::json!({"updated": true, "id": img_id, "tags": body.tags})),
),
Err(e) => {
let err_msg = format!("{:#}", e);
error_response(500, &format!("Échec mise à jour tags : {}", err_msg))
}
}
}
async fn stats(
State(state): State<Arc<AppState>>,
headers: HeaderMap,