feat(api-detail): modale d'édition des métadonnées d'une API
- Bouton "Modifier" sur la page détail ouvre une modale avec tous les champs éditables (titre, version, catégorie, description, docs, contacts, droits d'accès) - ApiService.update() étendu à tous les champs du UpdateApiEntryDto - Remplacement des labels "Obligatoire"/"Optionnel" par un astérisque rouge sur les champs requis dans tous les formulaires 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:
@@ -9,7 +9,8 @@ import {
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { ApiEntry } from '@datacat/shared';
|
||||
import { CategoryService } from '../../core/category.service';
|
||||
import { ApiEntry, Category } from '@datacat/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'app-api-detail',
|
||||
@@ -196,6 +197,9 @@ import { ApiEntry } from '@datacat/shared';
|
||||
>
|
||||
Voir la documentation
|
||||
</a>
|
||||
<button class="fr-btn fr-btn--secondary" (click)="openEdit()">
|
||||
Modifier
|
||||
</button>
|
||||
<a
|
||||
[href]="'/api/apis/' + entry.id + '/yaml'"
|
||||
class="fr-btn fr-btn--secondary"
|
||||
@@ -220,12 +224,86 @@ import { ApiEntry } from '@datacat/shared';
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Modale édition -->
|
||||
@if (showEditModal()) {
|
||||
<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="edit-modal-title">
|
||||
<div style="background:white;width:calc(100% - 2rem);max-width:600px;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="edit-modal-title" class="fr-h3 fr-mb-0">Modifier l'API</h2>
|
||||
</div>
|
||||
<div class="fr-col-auto">
|
||||
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm" (click)="closeEdit()">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (editError()) {
|
||||
<div class="fr-alert fr-alert--error fr-mb-2w"><p>{{ editError() }}</p></div>
|
||||
}
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-title">Titre <span style="color:var(--text-default-error)">*</span></label>
|
||||
<input id="edit-title" class="fr-input" type="text" [value]="editTitle()" (input)="editTitle.set($any($event.target).value)" />
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-version">Version <span style="color:var(--text-default-error)">*</span></label>
|
||||
<input id="edit-version" class="fr-input" type="text" [value]="editVersion()" (input)="editVersion.set($any($event.target).value)" />
|
||||
</div>
|
||||
<div class="fr-select-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-category">Catégorie</label>
|
||||
<select id="edit-category" class="fr-select" [value]="editCategoryId()" (change)="editCategoryId.set($any($event.target).value || null)">
|
||||
<option value="">— Sans catégorie —</option>
|
||||
@for (cat of categories(); track cat.id) {
|
||||
<option [value]="cat.id">{{ cat.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-description">Description</label>
|
||||
<textarea id="edit-description" class="fr-input" rows="3" [value]="editDescription()" (input)="editDescription.set($any($event.target).value)"></textarea>
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-func-doc">Documentation fonctionnelle</label>
|
||||
<textarea id="edit-func-doc" class="fr-input" rows="3" [value]="editFunctionalDoc()" (input)="editFunctionalDoc.set($any($event.target).value)"></textarea>
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-tech-doc">Documentation technique</label>
|
||||
<textarea id="edit-tech-doc" class="fr-input" rows="3" [value]="editTechnicalDoc()" (input)="editTechnicalDoc.set($any($event.target).value)"></textarea>
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-ext-url">URL documentation externe</label>
|
||||
<input id="edit-ext-url" class="fr-input" type="text" [value]="editExternalDocUrl()" (input)="editExternalDocUrl.set($any($event.target).value)" />
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-contact-func">Contact fonctionnel</label>
|
||||
<input id="edit-contact-func" class="fr-input" type="text" [value]="editContactFunctional()" (input)="editContactFunctional.set($any($event.target).value)" />
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-2w">
|
||||
<label class="fr-label" for="edit-contact-tech">Contact technique</label>
|
||||
<input id="edit-contact-tech" class="fr-input" type="text" [value]="editContactTechnical()" (input)="editContactTechnical.set($any($event.target).value)" />
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-3w">
|
||||
<label class="fr-label" for="edit-access">Droits d'accès</label>
|
||||
<textarea id="edit-access" class="fr-input" rows="3" [value]="editAccessRights()" (input)="editAccessRights.set($any($event.target).value)"></textarea>
|
||||
</div>
|
||||
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline">
|
||||
<button class="fr-btn fr-btn--secondary" (click)="closeEdit()">Annuler</button>
|
||||
<button class="fr-btn" (click)="submitEdit()" [disabled]="editLoading()">
|
||||
{{ editLoading() ? 'Enregistrement...' : 'Enregistrer' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private apiService = inject(ApiService);
|
||||
private categoryService = inject(CategoryService);
|
||||
|
||||
api = signal<ApiEntry | null>(null);
|
||||
loading = signal(false);
|
||||
@@ -233,6 +311,22 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||
regenerating = signal(false);
|
||||
deleting = signal(false);
|
||||
|
||||
/* Modale édition */
|
||||
showEditModal = signal(false);
|
||||
editLoading = signal(false);
|
||||
editError = signal<string | null>(null);
|
||||
categories = signal<Category[]>([]);
|
||||
editTitle = signal('');
|
||||
editVersion = signal('');
|
||||
editCategoryId = signal<string | null>(null);
|
||||
editDescription = signal('');
|
||||
editFunctionalDoc = signal('');
|
||||
editTechnicalDoc = signal('');
|
||||
editExternalDocUrl = signal('');
|
||||
editContactFunctional = signal('');
|
||||
editContactTechnical = signal('');
|
||||
editAccessRights = signal('');
|
||||
|
||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private id = '';
|
||||
|
||||
@@ -257,6 +351,58 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
openEdit() {
|
||||
const entry = this.api();
|
||||
if (!entry) return;
|
||||
this.editTitle.set(entry.title);
|
||||
this.editVersion.set(entry.version);
|
||||
this.editCategoryId.set(entry.categoryId ?? null);
|
||||
this.editDescription.set(entry.description ?? '');
|
||||
this.editFunctionalDoc.set(entry.functionalDoc ?? '');
|
||||
this.editTechnicalDoc.set(entry.technicalDoc ?? '');
|
||||
this.editExternalDocUrl.set(entry.externalDocUrl ?? '');
|
||||
this.editContactFunctional.set(entry.contactFunctional ?? '');
|
||||
this.editContactTechnical.set(entry.contactTechnical ?? '');
|
||||
this.editAccessRights.set(entry.accessRights ?? '');
|
||||
this.editError.set(null);
|
||||
this.categoryService.list().subscribe((res) => this.categories.set(res.items));
|
||||
this.showEditModal.set(true);
|
||||
}
|
||||
|
||||
closeEdit() {
|
||||
this.showEditModal.set(false);
|
||||
}
|
||||
|
||||
submitEdit() {
|
||||
if (!this.editTitle().trim() || !this.editVersion().trim()) {
|
||||
this.editError.set('Le titre et la version sont obligatoires.');
|
||||
return;
|
||||
}
|
||||
this.editLoading.set(true);
|
||||
this.apiService.update(this.id, {
|
||||
title: this.editTitle().trim(),
|
||||
version: this.editVersion().trim(),
|
||||
categoryId: this.editCategoryId() || null,
|
||||
description: this.editDescription().trim() || undefined,
|
||||
functionalDoc: this.editFunctionalDoc().trim() || null,
|
||||
technicalDoc: this.editTechnicalDoc().trim() || null,
|
||||
externalDocUrl: this.editExternalDocUrl().trim() || null,
|
||||
contactFunctional: this.editContactFunctional().trim() || null,
|
||||
contactTechnical: this.editContactTechnical().trim() || null,
|
||||
accessRights: this.editAccessRights().trim() || null,
|
||||
}).subscribe({
|
||||
next: (entry) => {
|
||||
this.api.set(entry);
|
||||
this.editLoading.set(false);
|
||||
this.showEditModal.set(false);
|
||||
},
|
||||
error: (err: { error?: { message?: string } }) => {
|
||||
this.editLoading.set(false);
|
||||
this.editError.set(err?.error?.message ?? 'Une erreur est survenue.');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onDelete() {
|
||||
if (!confirm('Supprimer cette API ?')) return;
|
||||
this.deleting.set(true);
|
||||
|
||||
Reference in New Issue
Block a user