feat(browse): modale de suppression + crayon d'édition sur le titre

- Remplace le confirm() natif par une vraie modale de confirmation avec gestion d'erreur inline
- Ajoute un bouton crayon dans le <h1> pour éditer la catégorie courante (visible uniquement dans /browse/:id)
- Signature openEdit() assouplie pour accepter Category (au lieu de CategoryWithCounts)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
z3n
2026-06-11 14:50:16 +00:00
parent b9d45c0c3c
commit e939d592f8

View File

@@ -45,7 +45,17 @@ import { forkJoin } from 'rxjs';
<!-- En-tête -->
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
<div class="fr-col">
<h1 class="fr-h2 fr-mb-0">{{ browseData()?.category?.name ?? 'Catalogue' }}</h1>
<h1 class="fr-h2 fr-mb-0">
{{ browseData()?.category?.name ?? 'Catalogue' }}
@if (browseData()?.category) {
<button
title="Modifier ce dossier"
style="background:none;border:none;cursor:pointer;padding:0 0.25rem;vertical-align:middle;opacity:0.6"
(click)="openEdit(browseData()!.category!)">
<span class="fr-icon-edit-line" aria-hidden="true" style="font-size:0.875rem"></span>
</button>
}
</h1>
</div>
<div class="fr-col-auto" style="display:flex;gap:0.5rem">
<a class="fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-upload-2-line"
@@ -99,7 +109,7 @@ import { forkJoin } from 'rxjs';
@if (cat.apiCount === 0 && cat.subcategoryCount === 0) {
<button title="Supprimer"
style="position:relative;z-index:1;background:none;border:none;cursor:pointer;padding:0 0.25rem;vertical-align:middle;opacity:0.6"
(click)="deleteCategory(cat); $event.stopPropagation()">
(click)="openDelete(cat); $event.stopPropagation()">
<span class="fr-icon-delete-bin-line" aria-hidden="true" style="font-size:0.75rem"></span>
</button>
}
@@ -229,6 +239,46 @@ import { forkJoin } from 'rxjs';
}
}
<!-- Modale de suppression -->
@if (showDeleteModal()) {
<div style="position:fixed;inset:0;z-index:9999;background:rgba(22,22,22,.64);display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;overflow-y:auto"
role="dialog" aria-modal="true" aria-labelledby="delete-modal-title">
<div style="background:white;width:calc(100% - 2rem);max-width:480px;margin:0 1rem 2rem">
<div class="fr-p-4w">
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
<div class="fr-col">
<h2 id="delete-modal-title" class="fr-h3 fr-mb-0">Supprimer le dossier</h2>
</div>
<div class="fr-col-auto">
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm"
(click)="showDeleteModal.set(false)">✕</button>
</div>
</div>
<p class="fr-text--lead fr-mb-1w">
Êtes-vous sûr de vouloir supprimer le dossier
<strong>« {{ deletingCategory()?.name }} »</strong> ?
</p>
<p class="fr-text--sm fr-text--mention-grey fr-mb-3w">
Cette action est irréversible.
</p>
@if (deleteError()) {
<div class="fr-alert fr-alert--error fr-mb-2w">
<p>{{ deleteError() }}</p>
</div>
}
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline">
<button class="fr-btn fr-btn--secondary" (click)="showDeleteModal.set(false)"
[disabled]="deleteLoading()">Annuler</button>
<button class="fr-btn fr-btn--error" (click)="confirmDelete()"
[disabled]="deleteLoading()">
{{ deleteLoading() ? 'Suppression...' : 'Supprimer' }}
</button>
</div>
</div>
</div>
</div>
}
<!-- Modale création / édition (overlay Angular, sans fr-modal pour éviter l'interférence du JS DSFR) -->
@if (showFormModal()) {
<div style="position:fixed;inset:0;z-index:9999;background:rgba(22,22,22,.64);display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;overflow-y:auto"
@@ -319,7 +369,7 @@ export class BrowseComponent implements OnInit {
private searchDebounce: ReturnType<typeof setTimeout> | null = null;
/* Signaux pour la modale */
/* Signaux pour la modale création / édition */
showFormModal = signal(false);
formMode = signal<'create' | 'edit'>('create');
editingCategoryId = signal<string | null>(null);
@@ -331,6 +381,12 @@ export class BrowseComponent implements OnInit {
formLoading = signal(false);
allCategories = signal<Category[]>([]);
/* Signaux pour la modale de suppression */
showDeleteModal = signal(false);
deletingCategory = signal<CategoryWithCounts | null>(null);
deleteLoading = signal(false);
deleteError = signal<string | null>(null);
/* Ancêtres = breadcrumb sans le nœud courant (déjà dans le h1) */
ancestors = computed<Category[]>(() => {
const d = this.browseData();
@@ -416,7 +472,7 @@ export class BrowseComponent implements OnInit {
});
}
openEdit(cat: CategoryWithCounts) {
openEdit(cat: Category) {
this.formMode.set('edit');
this.editingCategoryId.set(cat.id);
this.formName.set(cat.name);
@@ -462,13 +518,26 @@ export class BrowseComponent implements OnInit {
});
}
deleteCategory(cat: CategoryWithCounts) {
if (cat.apiCount > 0 || cat.subcategoryCount > 0) return;
if (!confirm(`Supprimer le dossier "${cat.name}" ?`)) return;
openDelete(cat: CategoryWithCounts) {
this.deletingCategory.set(cat);
this.deleteError.set(null);
this.showDeleteModal.set(true);
}
confirmDelete() {
const cat = this.deletingCategory();
if (!cat) return;
this.deleteLoading.set(true);
this.categoryService.delete(cat.id).subscribe({
next: () => this.loadBrowse(),
error: (err: { error?: { message?: string } }) =>
alert(err?.error?.message ?? 'Erreur lors de la suppression.'),
next: () => {
this.deleteLoading.set(false);
this.showDeleteModal.set(false);
this.loadBrowse();
},
error: (err: { error?: { message?: string } }) => {
this.deleteLoading.set(false);
this.deleteError.set(err?.error?.message ?? 'Erreur lors de la suppression.');
},
});
}