- 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 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
664 lines
30 KiB
TypeScript
664 lines
30 KiB
TypeScript
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: `
|
|
<div class="fr-container fr-mt-4w fr-mb-4w">
|
|
<!-- Fil d'Ariane -->
|
|
<nav class="fr-breadcrumb fr-mb-3w" aria-label="vous êtes ici">
|
|
<ol class="fr-breadcrumb__list">
|
|
<li>
|
|
<a class="fr-breadcrumb__link" routerLink="/">Accueil</a>
|
|
</li>
|
|
@if (categoryBreadcrumb().length === 0) {
|
|
<li><a class="fr-breadcrumb__link" routerLink="/browse">Parcourir</a></li>
|
|
}
|
|
@for (crumb of categoryBreadcrumb(); track crumb.id) {
|
|
<li><a class="fr-breadcrumb__link" [routerLink]="['/browse', crumb.id]">{{ crumb.name }}</a></li>
|
|
}
|
|
<li>
|
|
<a class="fr-breadcrumb__link" aria-current="page">
|
|
{{ api()?.title ?? 'Chargement...' }}
|
|
</a>
|
|
</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
@if (loading()) {
|
|
<div class="fr-callout fr-mb-4w">
|
|
<p class="fr-callout__text">Chargement...</p>
|
|
</div>
|
|
}
|
|
|
|
@if (error()) {
|
|
<div class="fr-alert fr-alert--error fr-mb-4w">
|
|
<p class="fr-alert__title">Erreur</p>
|
|
<p>{{ error() }}</p>
|
|
</div>
|
|
}
|
|
|
|
@if (api(); as entry) {
|
|
<!-- Retour -->
|
|
<a class="fr-link fr-icon-arrow-left-line fr-link--icon-left fr-mb-2w" style="display:inline-flex;"
|
|
[routerLink]="backUrl()">Retour</a>
|
|
|
|
<!-- En-tête -->
|
|
<div class="fr-mb-3w" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;">
|
|
<!-- Gauche : titre + version -->
|
|
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;min-width:0;">
|
|
<h1 class="fr-h2 fr-mb-0">{{ entry.title }}</h1>
|
|
@if (versions().length <= 1) {
|
|
<span class="fr-tag">v{{ entry.version }}</span>
|
|
} @else {
|
|
<select class="fr-select" id="version-select" style="width:auto;" (change)="onVersionChange($event)">
|
|
@for (v of versions(); track v.id) {
|
|
<option [value]="v.id" [selected]="v.id === entry.id">
|
|
{{ v.version }}{{ v.isCurrent ? ' (courante)' : '' }}
|
|
</option>
|
|
}
|
|
</select>
|
|
}
|
|
</div>
|
|
<!-- Droite : bouton Actions + dropdown -->
|
|
<div style="position:relative;flex-shrink:0;">
|
|
<button class="fr-btn fr-btn--secondary fr-btn--icon-right fr-icon-arrow-down-s-line"
|
|
(click)="actionsOpen.set(!actionsOpen())">
|
|
Actions
|
|
</button>
|
|
@if (actionsOpen()) {
|
|
<div style="position:absolute;right:0;top:calc(100% + 4px);z-index:1000;background:#fff;border:1px solid #ddd;box-shadow:0 4px 12px rgba(0,0,0,.15);min-width:220px;border-radius:4px;overflow:hidden;">
|
|
<a [routerLink]="['/catalog', entry.id, 'docs']"
|
|
(click)="actionsOpen.set(false)"
|
|
style="display:block;padding:0.75rem 1rem;text-decoration:none;color:#161616;border-bottom:1px solid #eee;"
|
|
[style.opacity]="entry.status !== 'GENERATED' ? '0.5' : '1'"
|
|
[style.pointer-events]="entry.status !== 'GENERATED' ? 'none' : 'auto'">
|
|
Voir la documentation
|
|
</a>
|
|
<button style="display:block;width:100%;text-align:left;padding:0.75rem 1rem;background:none;border:none;border-bottom:1px solid #eee;cursor:pointer;color:#161616;"
|
|
(click)="onNewVersion(); actionsOpen.set(false)">
|
|
Nouvelle version
|
|
</button>
|
|
<a [href]="'/api/apis/' + entry.id + '/yaml'"
|
|
download
|
|
(click)="actionsOpen.set(false)"
|
|
style="display:block;padding:0.75rem 1rem;text-decoration:none;color:#161616;border-bottom:1px solid #eee;">
|
|
Télécharger YAML
|
|
</a>
|
|
@if (entry.status !== 'GENERATED') {
|
|
<button style="display:block;width:100%;text-align:left;padding:0.75rem 1rem;background:none;border:none;border-bottom:1px solid #eee;cursor:pointer;color:#161616;"
|
|
[disabled]="regenerating()"
|
|
(click)="onRegenerate(); actionsOpen.set(false)">
|
|
{{ regenerating() ? 'Régénération...' : 'Régénérer' }}
|
|
</button>
|
|
}
|
|
<button style="display:block;width:100%;text-align:left;padding:0.75rem 1rem;background:none;border:none;cursor:pointer;color:#CE0500;"
|
|
[disabled]="deleting()"
|
|
(click)="onDelete(); actionsOpen.set(false)">
|
|
{{ deleting() ? 'Suppression...' : 'Supprimer' }}
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation onglets -->
|
|
<div style="display:flex;border-bottom:2px solid #e5e5e5;margin-bottom:1.5rem;">
|
|
<button (click)="activeTab.set('info')"
|
|
[style.border-bottom]="activeTab()==='info' ? '2px solid #000091' : '2px solid transparent'"
|
|
[style.color]="activeTab()==='info' ? '#000091' : '#3a3a3a'"
|
|
[style.font-weight]="activeTab()==='info' ? '700' : '400'"
|
|
style="padding:0.75rem 1.5rem;background:none;border-top:none;border-left:none;border-right:none;cursor:pointer;margin-bottom:-2px;">
|
|
Informations
|
|
</button>
|
|
<button (click)="activeTab.set('contacts')"
|
|
[style.border-bottom]="activeTab()==='contacts' ? '2px solid #000091' : '2px solid transparent'"
|
|
[style.color]="activeTab()==='contacts' ? '#000091' : '#3a3a3a'"
|
|
[style.font-weight]="activeTab()==='contacts' ? '700' : '400'"
|
|
style="padding:0.75rem 1.5rem;background:none;border-top:none;border-left:none;border-right:none;cursor:pointer;margin-bottom:-2px;">
|
|
Contacts
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Onglet Informations -->
|
|
@if (activeTab() === 'info') {
|
|
@if (!editingInfo()) {
|
|
<div class="fr-card fr-mb-3w">
|
|
<div class="fr-card__body">
|
|
<div class="fr-card__content">
|
|
<dl class="fr-grid-row fr-grid-row--gutters">
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<dt class="fr-text--bold">Version</dt>
|
|
<dd style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
|
{{ entry.version }}
|
|
@if (!entry.isCurrent) {
|
|
<button class="fr-btn fr-btn--sm fr-btn--secondary" [disabled]="settingCurrent()"
|
|
(click)="onSetCurrent()">
|
|
{{ settingCurrent() ? '...' : 'Passer en courante' }}
|
|
</button>
|
|
}
|
|
</dd>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<dt class="fr-text--bold">Type</dt>
|
|
<dd>{{ entry.type }}</dd>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<dt class="fr-text--bold">Statut</dt>
|
|
<dd>
|
|
<span [class]="badgeClass(entry.status)">{{ statusLabel(entry.status) }}</span>
|
|
@if (entry.status === 'ERROR' && entry.errorMessage) {
|
|
<p class="fr-text--sm fr-mt-1w">{{ entry.errorMessage }}</p>
|
|
}
|
|
</dd>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<dt class="fr-text--bold">Fournisseur</dt>
|
|
<dd>{{ entry.provider }}</dd>
|
|
</div>
|
|
@if (entry.description) {
|
|
<div class="fr-col-12">
|
|
<dt class="fr-text--bold">Description</dt>
|
|
<dd>{{ entry.description }}</dd>
|
|
</div>
|
|
}
|
|
<div class="fr-col-12 fr-col-md-6">
|
|
<dt class="fr-text--bold">Créé le</dt>
|
|
<dd>{{ entry.createdAt | date:'dd/MM/yyyy HH:mm' }}</dd>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-6">
|
|
<dt class="fr-text--bold">Mis à jour le</dt>
|
|
<dd>{{ entry.updatedAt | date:'dd/MM/yyyy HH:mm' }}</dd>
|
|
</div>
|
|
</dl>
|
|
<div class="fr-mt-2w">
|
|
<button class="fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-edit-line" (click)="startEditInfo()">
|
|
Modifier les informations
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (editingInfo()) {
|
|
<form [formGroup]="infoForm" (ngSubmit)="submitInfo()" novalidate class="fr-mb-3w">
|
|
<!-- Convention + Nom -->
|
|
<p class="fr-label fr-mb-1w">Titre <span style="color:var(--text-default-error)">*</span></p>
|
|
<div class="fr-grid-row fr-grid-row--gutters fr-mb-1w">
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<div class="fr-select-group">
|
|
<label class="fr-label" for="info-convention">Convention</label>
|
|
<select id="info-convention" class="fr-select" formControlName="convention">
|
|
<option value="CONSULTER">Consulter</option>
|
|
<option value="ENREGISTRER">Enregistrer</option>
|
|
<option value="ETRE_NOTIFIE">Être notifié</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-8">
|
|
<div class="fr-input-group"
|
|
[class.fr-input-group--error]="infoForm.get('name')?.invalid && infoForm.get('name')?.touched">
|
|
<label class="fr-label" for="info-name">Nom de l'API <span style="color:var(--text-default-error)">*</span></label>
|
|
<input id="info-name" class="fr-input" type="text" formControlName="name"
|
|
[class.fr-input--error]="infoForm.get('name')?.invalid && infoForm.get('name')?.touched" />
|
|
@if (infoForm.get('name')?.invalid && infoForm.get('name')?.touched) {
|
|
<p class="fr-error-text">Le nom est obligatoire.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fournisseur -->
|
|
<div class="fr-input-group fr-mb-2w"
|
|
[class.fr-input-group--error]="infoForm.get('provider')?.invalid && infoForm.get('provider')?.touched">
|
|
<label class="fr-label" for="info-provider">Fournisseur <span style="color:var(--text-default-error)">*</span></label>
|
|
<input id="info-provider" class="fr-input" type="text" formControlName="provider"
|
|
[class.fr-input--error]="infoForm.get('provider')?.invalid && infoForm.get('provider')?.touched" />
|
|
@if (infoForm.get('provider')?.invalid && infoForm.get('provider')?.touched) {
|
|
<p class="fr-error-text">Le fournisseur est obligatoire.</p>
|
|
}
|
|
</div>
|
|
|
|
<!-- Version -->
|
|
<p class="fr-label fr-mb-1w">Version <span style="color:var(--text-default-error)">*</span></p>
|
|
<div class="fr-grid-row fr-grid-row--gutters fr-mb-2w">
|
|
<div class="fr-col-4">
|
|
<div class="fr-input-group">
|
|
<label class="fr-label" for="info-v-major">Majeure (X)</label>
|
|
<input id="info-v-major" class="fr-input" type="number" min="0" formControlName="versionMajor" />
|
|
</div>
|
|
</div>
|
|
<div class="fr-col-4">
|
|
<div class="fr-input-group">
|
|
<label class="fr-label" for="info-v-minor">Mineure (Y)</label>
|
|
<input id="info-v-minor" class="fr-input" type="number" min="0" formControlName="versionMinor" />
|
|
</div>
|
|
</div>
|
|
<div class="fr-col-4">
|
|
<div class="fr-input-group">
|
|
<label class="fr-label" for="info-v-patch">Correctif (Z)</label>
|
|
<input id="info-v-patch" class="fr-input" type="number" min="0" formControlName="versionPatch" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="fr-input-group fr-mb-2w">
|
|
<label class="fr-label" for="info-description">Description</label>
|
|
<textarea id="info-description" class="fr-input" rows="3" formControlName="description"></textarea>
|
|
</div>
|
|
|
|
<!-- Catégorie -->
|
|
<div class="fr-select-group fr-mb-3w">
|
|
<label class="fr-label" for="info-category">Catégorie</label>
|
|
<select id="info-category" class="fr-select" formControlName="categoryId">
|
|
<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-btns-group fr-btns-group--inline fr-btns-group--right">
|
|
<button type="button" class="fr-btn fr-btn--secondary" (click)="editingInfo.set(false)">Annuler</button>
|
|
<button type="submit" class="fr-btn" [disabled]="savingInfo()">
|
|
{{ savingInfo() ? 'Enregistrement...' : 'Enregistrer' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
}
|
|
}
|
|
|
|
<!-- Onglet Contacts -->
|
|
@if (activeTab() === 'contacts') {
|
|
@if (!editingContacts()) {
|
|
<div class="fr-card fr-mb-3w">
|
|
<div class="fr-card__body">
|
|
<div class="fr-card__content">
|
|
<div class="fr-grid-row fr-grid-row--gutters">
|
|
<div class="fr-col-12 fr-col-md-6">
|
|
<p class="fr-text--bold fr-mb-1w">Contact métier</p>
|
|
<p class="fr-mb-0">{{ entry.contactFunctionalName }}</p>
|
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactFunctionalEntity }}</p>
|
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactFunctionalEmail }}</p>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-6">
|
|
<p class="fr-text--bold fr-mb-1w">Contact technique</p>
|
|
<p class="fr-mb-0">{{ entry.contactTechnicalName }}</p>
|
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactTechnicalEntity }}</p>
|
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactTechnicalEmail }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="fr-mt-2w">
|
|
<button class="fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-edit-line" (click)="startEditContacts()">
|
|
Modifier les contacts
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (editingContacts()) {
|
|
<form [formGroup]="contactForm" (ngSubmit)="submitContacts()" novalidate class="fr-mb-3w">
|
|
<!-- Contact métier -->
|
|
<p class="fr-label fr-mb-1w">Contact métier <span style="color:var(--text-default-error)">*</span></p>
|
|
<div class="fr-grid-row fr-grid-row--gutters fr-mb-3w">
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<div class="fr-input-group"
|
|
[class.fr-input-group--error]="contactForm.get('contactFunctionalName')?.invalid && contactForm.get('contactFunctionalName')?.touched">
|
|
<label class="fr-label" for="cf-name">Nom et prénom</label>
|
|
<input id="cf-name" class="fr-input" type="text" formControlName="contactFunctionalName" placeholder="Prénom Nom"
|
|
[class.fr-input--error]="contactForm.get('contactFunctionalName')?.invalid && contactForm.get('contactFunctionalName')?.touched" />
|
|
@if (contactForm.get('contactFunctionalName')?.invalid && contactForm.get('contactFunctionalName')?.touched) {
|
|
<p class="fr-error-text">Obligatoire.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<div class="fr-input-group"
|
|
[class.fr-input-group--error]="contactForm.get('contactFunctionalEntity')?.invalid && contactForm.get('contactFunctionalEntity')?.touched">
|
|
<label class="fr-label" for="cf-entity">Entité</label>
|
|
<input id="cf-entity" class="fr-input" type="text" formControlName="contactFunctionalEntity" placeholder="Direction XYZ"
|
|
[class.fr-input--error]="contactForm.get('contactFunctionalEntity')?.invalid && contactForm.get('contactFunctionalEntity')?.touched" />
|
|
@if (contactForm.get('contactFunctionalEntity')?.invalid && contactForm.get('contactFunctionalEntity')?.touched) {
|
|
<p class="fr-error-text">Obligatoire.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<div class="fr-input-group"
|
|
[class.fr-input-group--error]="contactForm.get('contactFunctionalEmail')?.invalid && contactForm.get('contactFunctionalEmail')?.touched">
|
|
<label class="fr-label" for="cf-email">Email</label>
|
|
<input id="cf-email" class="fr-input" type="email" formControlName="contactFunctionalEmail" placeholder="prenom.nom@example.fr"
|
|
[class.fr-input--error]="contactForm.get('contactFunctionalEmail')?.invalid && contactForm.get('contactFunctionalEmail')?.touched" />
|
|
@if (contactForm.get('contactFunctionalEmail')?.invalid && contactForm.get('contactFunctionalEmail')?.touched) {
|
|
<p class="fr-error-text">Email valide requis.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact technique -->
|
|
<p class="fr-label fr-mb-1w">Contact technique <span style="color:var(--text-default-error)">*</span></p>
|
|
<div class="fr-grid-row fr-grid-row--gutters fr-mb-3w">
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<div class="fr-input-group"
|
|
[class.fr-input-group--error]="contactForm.get('contactTechnicalName')?.invalid && contactForm.get('contactTechnicalName')?.touched">
|
|
<label class="fr-label" for="ct-name">Nom et prénom</label>
|
|
<input id="ct-name" class="fr-input" type="text" formControlName="contactTechnicalName" placeholder="Prénom Nom"
|
|
[class.fr-input--error]="contactForm.get('contactTechnicalName')?.invalid && contactForm.get('contactTechnicalName')?.touched" />
|
|
@if (contactForm.get('contactTechnicalName')?.invalid && contactForm.get('contactTechnicalName')?.touched) {
|
|
<p class="fr-error-text">Obligatoire.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<div class="fr-input-group"
|
|
[class.fr-input-group--error]="contactForm.get('contactTechnicalEntity')?.invalid && contactForm.get('contactTechnicalEntity')?.touched">
|
|
<label class="fr-label" for="ct-entity">Entité</label>
|
|
<input id="ct-entity" class="fr-input" type="text" formControlName="contactTechnicalEntity" placeholder="Équipe Technique"
|
|
[class.fr-input--error]="contactForm.get('contactTechnicalEntity')?.invalid && contactForm.get('contactTechnicalEntity')?.touched" />
|
|
@if (contactForm.get('contactTechnicalEntity')?.invalid && contactForm.get('contactTechnicalEntity')?.touched) {
|
|
<p class="fr-error-text">Obligatoire.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="fr-col-12 fr-col-md-4">
|
|
<div class="fr-input-group"
|
|
[class.fr-input-group--error]="contactForm.get('contactTechnicalEmail')?.invalid && contactForm.get('contactTechnicalEmail')?.touched">
|
|
<label class="fr-label" for="ct-email">Email</label>
|
|
<input id="ct-email" class="fr-input" type="email" formControlName="contactTechnicalEmail" placeholder="tech@example.fr"
|
|
[class.fr-input--error]="contactForm.get('contactTechnicalEmail')?.invalid && contactForm.get('contactTechnicalEmail')?.touched" />
|
|
@if (contactForm.get('contactTechnicalEmail')?.invalid && contactForm.get('contactTechnicalEmail')?.touched) {
|
|
<p class="fr-error-text">Email valide requis.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="fr-btns-group fr-btns-group--inline fr-btns-group--right">
|
|
<button type="button" class="fr-btn fr-btn--secondary" (click)="editingContacts.set(false)">Annuler</button>
|
|
<button type="submit" class="fr-btn" [disabled]="savingContacts()">
|
|
{{ savingContacts() ? 'Enregistrement...' : 'Enregistrer' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
}
|
|
}
|
|
|
|
}
|
|
</div>
|
|
`,
|
|
})
|
|
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<ApiEntry | null>(null);
|
|
versions = signal<ApiEntryListItem[]>([]);
|
|
categoryBreadcrumb = signal<Category[]>([]);
|
|
loading = signal(false);
|
|
error = signal<string | null>(null);
|
|
regenerating = signal(false);
|
|
deleting = signal(false);
|
|
settingCurrent = signal(false);
|
|
categories = signal<Category[]>([]);
|
|
actionsOpen = signal(false);
|
|
|
|
backUrl = computed<string[]>(() => {
|
|
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<typeof setInterval> | 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;
|
|
}
|
|
}
|
|
}
|