import { Component, signal, computed, inject, OnInit, OnDestroy, 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 } from '@datacat/shared'; @Component({ selector: 'app-api-detail', standalone: true, imports: [CommonModule, RouterLink, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@if (loading()) {

Chargement...

} @if (error()) {

Erreur

{{ error() }}

} @if (api(); as entry) { Retour

{{ entry.title }}

@if (versions().length <= 1) { v{{ entry.version }} } @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 (editingInfo()) {

Titre *

@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 (activeTab() === 'contacts') { @if (!editingContacts()) {

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.

}
} } }
`, }) export class ApiDetailComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private router = inject(Router); private apiService = inject(ApiService); private categoryService = inject(CategoryService); private fb = inject(FormBuilder); api = signal(null); versions = signal([]); categoryBreadcrumb = signal([]); loading = signal(false); error = signal(null); regenerating = signal(false); deleting = signal(false); settingCurrent = signal(false); categories = signal([]); actionsOpen = signal(false); 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; private routeSub: Subscription | null = null; 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(); }); } ngOnDestroy() { this.routeSub?.unsubscribe(); this.clearPoll(); } onVersionChange(event: Event): void { const id = (event.target as HTMLSelectElement).value; if (id && id !== this.api()?.id) { this.router.navigate(['/catalog', id]); } } onNewVersion(): void { 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({ next: (entry) => { this.api.set(entry); this.regenerating.set(false); this.startPollIfPending(); }, error: () => this.regenerating.set(false), }); } startEditInfo() { const entry = this.api(); if (!entry) return; 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); } 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); } 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: (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), }); } onDelete() { if (!confirm('Supprimer cette API ?')) return; this.deleting.set(true); this.apiService.delete(this.id).subscribe({ next: () => this.router.navigate(['/catalog']), error: () => this.deleting.set(false), }); } badgeClass(status: string): string { if (status === 'GENERATED') return 'fr-badge fr-badge--success'; if (status === 'ERROR') return 'fr-badge fr-badge--error'; 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({ next: (entry) => { this.api.set(entry); this.loading.set(false); this.startPollIfPending(); this.apiService.getVersions(this.id).subscribe({ next: (v) => this.versions.set(v), error: () => {}, }); if (entry.categoryId) { this.categoryService.browse(entry.categoryId).subscribe({ next: (data) => this.categoryBreadcrumb.set(data.breadcrumb), error: () => {}, }); } }, error: (err) => { this.error.set(err.message ?? 'Erreur lors du chargement'); this.loading.set(false); }, }); } private startPollIfPending() { this.clearPoll(); if (this.api()?.status === 'PENDING') { this.pollInterval = setInterval(() => this.pollStatus(), 3000); } } private pollStatus() { this.apiService.get(this.id).subscribe({ next: (entry) => { this.api.set(entry); if (entry.status !== 'PENDING') this.clearPoll(); }, }); } private clearPoll() { if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } } }