From eb07ca085fe4a5f25f8d2ce76c46823eeecad41b Mon Sep 17 00:00:00 2001 From: z3n Date: Wed, 17 Jun 2026 14:50:51 +0000 Subject: [PATCH] =?UTF-8?q?feat(browse):=20tag=20version=20sur=20les=20tui?= =?UTF-8?q?les=20+=20statut=20francis=C3=A9=20sur=20la=20fiche=20d=C3=A9ta?= =?UTF-8?q?il?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Supprime le badge statut (GENERATED/PENDING/ERROR) des tuiles browse et cartes search - Ajoute un badge version (ex: v1.2.0) sur les tuiles et cartes - Traduit le statut en français sur la fiche détail (Générée / En attente / Erreur) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- .../pages/api-detail/api-detail.component.ts | 821 ++++++++++-------- .../src/app/pages/browse/browse.component.ts | 26 +- 2 files changed, 474 insertions(+), 373 deletions(-) diff --git a/front-public/src/app/pages/api-detail/api-detail.component.ts b/front-public/src/app/pages/api-detail/api-detail.component.ts index e92543a..d5e825a 100644 --- a/front-public/src/app/pages/api-detail/api-detail.component.ts +++ b/front-public/src/app/pages/api-detail/api-detail.component.ts @@ -8,16 +8,17 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { Subscription } from 'rxjs'; import { ApiService } from '../../core/api.service'; import { CategoryService } from '../../core/category.service'; -import { ApiEntry, ApiEntryListItem, Category, ApiConvention } from '@datacat/shared'; +import { ApiEntry, ApiEntryListItem, Category } from '@datacat/shared'; @Component({ selector: 'app-api-detail', standalone: true, - imports: [CommonModule, RouterLink], + imports: [CommonModule, RouterLink, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@@ -55,310 +56,357 @@ import { ApiEntry, ApiEntryListItem, Category, ApiConvention } from '@datacat/sh } @if (api(); as entry) { + + Retour + -
-
-

{{ entry.title }}

-
- {{ entry.type }} +
+ +
+

{{ entry.title }}

+ @if (versions().length <= 1) { v{{ entry.version }} - {{ entry.status }} + } @else { + + } +
+ +
+ + @if (actionsOpen()) { +
+ + Voir la documentation + + + + Télécharger YAML + + @if (entry.status !== 'GENERATED') { + + } + +
+ } +
+
+ + +
+ + +
+ + + @if (activeTab() === 'info') { + @if (!editingInfo()) { +
+
+
+
+
+
Version
+
+ {{ entry.version }} + @if (!entry.isCurrent) { + + } +
+
+
+
Type
+
{{ entry.type }}
+
+
+
Statut
+
+ {{ statusLabel(entry.status) }} + @if (entry.status === 'ERROR' && entry.errorMessage) { +

{{ entry.errorMessage }}

+ } +
+
+
+
Fournisseur
+
{{ entry.provider }}
+
+ @if (entry.description) { +
+
Description
+
{{ entry.description }}
+
+ } +
+
Créé le
+
{{ entry.createdAt | date:'dd/MM/yyyy HH:mm' }}
+
+
+
Mis à jour le
+
{{ entry.updatedAt | date:'dd/MM/yyyy HH:mm' }}
+
+
+
+ +
+
+
- @if (versions().length > 1) { -
- - + + + + +
+
+
+
+ + + @if (infoForm.get('name')?.invalid && infoForm.get('name')?.touched) { +

Le nom est obligatoire.

+ } +
+
+
+ + +
+ + + @if (infoForm.get('provider')?.invalid && infoForm.get('provider')?.touched) { +

Le fournisseur est obligatoire.

+ } +
+ + +

Version *

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+ + +
+ + +
+ +
- } -
-
- - @if (entry.status === 'PENDING') { -
-

Génération en cours

-

La documentation est en cours de génération. Cette page se rafraîchit automatiquement.

-
+
+ + +
+ + } } - - @if (entry.status === 'ERROR') { -
-

Erreur de génération

-

{{ entry.errorMessage }}

-
- } - - -
-
-
-

Informations

-
-
-
Version
-
{{ entry.version }}
-
-
-
Type
-
{{ entry.type }}
-
-
-
Fournisseur
-
{{ entry.provider }}
-
- @if (entry.description) { -
-
Description
-
{{ entry.description }}
+ + @if (activeTab() === 'contacts') { + @if (!editingContacts()) { +
+
+
+
+
+

Contact métier

+

{{ entry.contactFunctionalName }}

+

{{ entry.contactFunctionalEntity }}

+

{{ entry.contactFunctionalEmail }}

+
+
+

Contact technique

+

{{ entry.contactTechnicalName }}

+

{{ entry.contactTechnicalEntity }}

+

{{ entry.contactTechnicalEmail }}

+
+
+
+
- } -
-
Créé le
-
{{ entry.createdAt | date:'dd/MM/yyyy HH:mm' }}
-
-
-
Mis à jour le
-
{{ entry.updatedAt | date:'dd/MM/yyyy HH:mm' }}
-
-
-
-
-
- - -
-
-
-

Contacts

-
-
-

Contact métier

-

{{ entry.contactFunctionalName }}

-

{{ entry.contactFunctionalEntity }}

-

{{ entry.contactFunctionalEmail }}

-
-
-

Contact technique

-

{{ entry.contactTechnicalName }}

-

{{ entry.contactTechnicalEntity }}

-

{{ entry.contactTechnicalEmail }}

-
-
+ } + + @if (editingContacts()) { +
+ +

Contact métier *

+
+
+
+ + + @if (contactForm.get('contactFunctionalName')?.invalid && contactForm.get('contactFunctionalName')?.touched) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (contactForm.get('contactFunctionalEntity')?.invalid && contactForm.get('contactFunctionalEntity')?.touched) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (contactForm.get('contactFunctionalEmail')?.invalid && contactForm.get('contactFunctionalEmail')?.touched) { +

Email valide requis.

+ } +
+
+
+ + +

Contact technique *

+
+
+
+ + + @if (contactForm.get('contactTechnicalName')?.invalid && contactForm.get('contactTechnicalName')?.touched) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (contactForm.get('contactTechnicalEntity')?.invalid && contactForm.get('contactTechnicalEntity')?.touched) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (contactForm.get('contactTechnicalEmail')?.invalid && contactForm.get('contactTechnicalEmail')?.touched) { +

Email valide requis.

+ } +
+
+
+ +
+ + +
+
+ } + } - -
- - Voir la documentation - - - - - Télécharger YAML - - - -
} - - - @if (showEditModal()) { -
-
-
-
-
-

Modifier l'API

-
-
- -
-
- @if (editError()) { -

{{ editError() }}

- } - - -

Titre *

-
-
-
- - -
-
-
-
- - - @if (editNameError()) {

Le nom est obligatoire.

} -
-
-
-

Titre : {{ editPreviewTitle() }}

- - -
- - -
- - -
- - - @if (editProviderError()) {

Le fournisseur est obligatoire.

} -
- - -
- - -
- - -

Version *

-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -

Contact métier *

-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -

Contact technique *

-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- -
- - -
-
-
-
- } `, }) export class ApiDetailComponent implements OnInit, OnDestroy { @@ -366,6 +414,7 @@ export class ApiDetailComponent implements OnInit, OnDestroy { private router = inject(Router); private apiService = inject(ApiService); private categoryService = inject(CategoryService); + private fb = inject(FormBuilder); api = signal(null); versions = signal([]); @@ -374,32 +423,41 @@ export class ApiDetailComponent implements OnInit, OnDestroy { error = signal(null); regenerating = signal(false); deleting = signal(false); - - /* Modale édition */ - showEditModal = signal(false); - editLoading = signal(false); - editError = signal(null); - editNameError = signal(false); - editProviderError = signal(false); + settingCurrent = signal(false); categories = signal([]); - editConvention = signal('CONSULTER'); - editName = signal(''); - editProvider = signal(''); - editVersionMajor = signal(0); - editVersionMinor = signal(0); - editVersionPatch = signal(0); - editCategoryId = signal(null); - editDescription = signal(''); - editContactFunctionalName = signal(''); - editContactFunctionalEntity = signal(''); - editContactFunctionalEmail = signal(''); - editContactTechnicalName = signal(''); - editContactTechnicalEntity = signal(''); - editContactTechnicalEmail = signal(''); + actionsOpen = signal(false); - editPreviewTitle = computed(() => { - const labels: Record = { CONSULTER: 'Consulter', ENREGISTRER: 'Enregistrer', ETRE_NOTIFIE: 'Être notifié' }; - return `${labels[this.editConvention()]} ${this.editName()}`.trim(); + backUrl = computed(() => { + const crumbs = this.categoryBreadcrumb(); + if (crumbs.length > 0) return ['/browse', crumbs[crumbs.length - 1].id]; + return ['/browse']; + }); + + /* Onglets */ + activeTab = signal<'info' | 'contacts'>('info'); + editingInfo = signal(false); + editingContacts = signal(false); + savingInfo = signal(false); + savingContacts = signal(false); + + infoForm = this.fb.group({ + convention: ['CONSULTER', Validators.required], + name: ['', Validators.required], + provider: ['', Validators.required], + versionMajor: [0, [Validators.required, Validators.min(0)]], + versionMinor: [0, [Validators.required, Validators.min(0)]], + versionPatch: [0, [Validators.required, Validators.min(0)]], + description: [''], + categoryId: [''], + }); + + contactForm = this.fb.group({ + contactFunctionalName: ['', Validators.required], + contactFunctionalEntity: ['', Validators.required], + contactFunctionalEmail: ['', [Validators.required, Validators.email]], + contactTechnicalName: ['', Validators.required], + contactTechnicalEntity: ['', Validators.required], + contactTechnicalEmail: ['', [Validators.required, Validators.email]], }); private pollInterval: ReturnType | null = null; @@ -407,12 +465,15 @@ export class ApiDetailComponent implements OnInit, OnDestroy { private id = ''; ngOnInit() { + this.categoryService.list().subscribe((res) => this.categories.set(res.items)); this.routeSub = this.route.paramMap.subscribe((params) => { this.id = params.get('id') ?? ''; this.api.set(null); this.versions.set([]); this.error.set(null); this.categoryBreadcrumb.set([]); + this.editingInfo.set(false); + this.editingContacts.set(false); this.clearPoll(); this.loadApi(); }); @@ -434,6 +495,21 @@ export class ApiDetailComponent implements OnInit, OnDestroy { this.router.navigate(['/upload'], { queryParams: { from: this.api()?.id } }); } + onSetCurrent() { + this.settingCurrent.set(true); + this.apiService.setCurrent(this.id).subscribe({ + next: (entry) => { + this.api.set(entry); + this.settingCurrent.set(false); + this.apiService.getVersions(this.id).subscribe({ + next: (v) => this.versions.set(v), + error: () => {}, + }); + }, + error: () => this.settingCurrent.set(false), + }); + } + onRegenerate() { this.regenerating.set(true); this.apiService.regenerate(this.id).subscribe({ @@ -446,64 +522,73 @@ export class ApiDetailComponent implements OnInit, OnDestroy { }); } - openEdit() { + startEditInfo() { const entry = this.api(); if (!entry) return; - this.editConvention.set(entry.convention); - this.editName.set(entry.name); - this.editProvider.set(entry.provider); - this.editVersionMajor.set(entry.versionMajor); - this.editVersionMinor.set(entry.versionMinor); - this.editVersionPatch.set(entry.versionPatch); - this.editCategoryId.set(entry.categoryId ?? null); - this.editDescription.set(entry.description ?? ''); - this.editContactFunctionalName.set(entry.contactFunctionalName); - this.editContactFunctionalEntity.set(entry.contactFunctionalEntity); - this.editContactFunctionalEmail.set(entry.contactFunctionalEmail); - this.editContactTechnicalName.set(entry.contactTechnicalName); - this.editContactTechnicalEntity.set(entry.contactTechnicalEntity); - this.editContactTechnicalEmail.set(entry.contactTechnicalEmail); - this.editError.set(null); - this.editNameError.set(false); - this.editProviderError.set(false); - this.categoryService.list().subscribe((res) => this.categories.set(res.items)); - this.showEditModal.set(true); + this.infoForm.patchValue({ + convention: entry.convention, + name: entry.name, + provider: entry.provider, + versionMajor: entry.versionMajor, + versionMinor: entry.versionMinor, + versionPatch: entry.versionPatch, + description: entry.description ?? '', + categoryId: entry.categoryId ?? '', + }); + this.editingInfo.set(true); } - closeEdit() { - this.showEditModal.set(false); + startEditContacts() { + const entry = this.api(); + if (!entry) return; + this.contactForm.patchValue({ + contactFunctionalName: entry.contactFunctionalName, + contactFunctionalEntity: entry.contactFunctionalEntity, + contactFunctionalEmail: entry.contactFunctionalEmail, + contactTechnicalName: entry.contactTechnicalName, + contactTechnicalEntity: entry.contactTechnicalEntity, + contactTechnicalEmail: entry.contactTechnicalEmail, + }); + this.editingContacts.set(true); } - submitEdit() { - this.editNameError.set(!this.editName().trim()); - this.editProviderError.set(!this.editProvider().trim()); - if (!this.editName().trim() || !this.editProvider().trim()) return; - this.editLoading.set(true); - this.apiService.update(this.id, { - convention: this.editConvention(), - name: this.editName().trim(), - provider: this.editProvider().trim(), - versionMajor: this.editVersionMajor(), - versionMinor: this.editVersionMinor(), - versionPatch: this.editVersionPatch(), - categoryId: this.editCategoryId() || null, - description: this.editDescription().trim() || null, - contactFunctionalName: this.editContactFunctionalName().trim(), - contactFunctionalEntity: this.editContactFunctionalEntity().trim(), - contactFunctionalEmail: this.editContactFunctionalEmail().trim(), - contactTechnicalName: this.editContactTechnicalName().trim(), - contactTechnicalEntity: this.editContactTechnicalEntity().trim(), - contactTechnicalEmail: this.editContactTechnicalEmail().trim(), + submitInfo() { + if (this.infoForm.invalid) { this.infoForm.markAllAsTouched(); return; } + const entry = this.api(); + if (!entry) return; + this.savingInfo.set(true); + const raw = this.infoForm.getRawValue(); + this.apiService.update(entry.id, { + convention: raw.convention ?? 'CONSULTER', + name: raw.name ?? '', + provider: raw.provider ?? '', + versionMajor: raw.versionMajor ?? 0, + versionMinor: raw.versionMinor ?? 0, + versionPatch: raw.versionPatch ?? 0, + categoryId: raw.categoryId || null, + description: raw.description || 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.'); - }, + next: (updated) => { this.api.set(updated); this.savingInfo.set(false); this.editingInfo.set(false); }, + error: () => this.savingInfo.set(false), + }); + } + + submitContacts() { + if (this.contactForm.invalid) { this.contactForm.markAllAsTouched(); return; } + const entry = this.api(); + if (!entry) return; + this.savingContacts.set(true); + const raw = this.contactForm.getRawValue(); + this.apiService.update(entry.id, { + contactFunctionalName: raw.contactFunctionalName ?? '', + contactFunctionalEntity: raw.contactFunctionalEntity ?? '', + contactFunctionalEmail: raw.contactFunctionalEmail ?? '', + contactTechnicalName: raw.contactTechnicalName ?? '', + contactTechnicalEntity: raw.contactTechnicalEntity ?? '', + contactTechnicalEmail: raw.contactTechnicalEmail ?? '', + }).subscribe({ + next: (updated) => { this.api.set(updated); this.savingContacts.set(false); this.editingContacts.set(false); }, + error: () => this.savingContacts.set(false), }); } @@ -522,6 +607,12 @@ export class ApiDetailComponent implements OnInit, OnDestroy { return 'fr-badge fr-badge--info'; } + statusLabel(status: string): string { + if (status === 'GENERATED') return 'Générée'; + if (status === 'ERROR') return 'Erreur'; + return 'En attente'; + } + private loadApi() { this.loading.set(true); this.apiService.get(this.id).subscribe({ diff --git a/front-public/src/app/pages/browse/browse.component.ts b/front-public/src/app/pages/browse/browse.component.ts index af0e815..578b419 100644 --- a/front-public/src/app/pages/browse/browse.component.ts +++ b/front-public/src/app/pages/browse/browse.component.ts @@ -45,6 +45,10 @@ import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal + @if (categoryId()) { + Retour + }

@@ -146,8 +150,10 @@ import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal }

{{ api.type }} -   - {{ api.status }} +  v{{ api.version }} + @if (api.isCurrent) { +  Courante + }

@@ -215,7 +221,10 @@ import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal }
{{ api.type }} - {{ api.status }} + v{{ api.version }} + @if (api.isCurrent) { + Courante + }
@@ -300,6 +309,12 @@ export class BrowseComponent implements OnInit { return d.breadcrumb.slice(0, -1); }); + backUrl = computed(() => { + const anc = this.ancestors(); + if (anc.length > 0) return ['/browse', anc[anc.length - 1].id]; + return ['/browse']; + }); + ngOnInit() { this.route.paramMap .pipe(takeUntilDestroyed(this.destroyRef)) @@ -398,9 +413,4 @@ export class BrowseComponent implements OnInit { : 'fr-badge fr-badge--sm fr-badge--blue-ecume'; } - statusBadgeClass(status: string): string { - if (status === 'GENERATED') return 'fr-badge fr-badge--sm fr-badge--success'; - if (status === 'ERROR') return 'fr-badge fr-badge--sm fr-badge--error'; - return 'fr-badge fr-badge--sm fr-badge--info'; - } }