feat(browse): tag version sur les tuiles + statut francisé sur la fiche détail
- 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>
This commit is contained in:
@@ -8,16 +8,17 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { ApiService } from '../../core/api.service';
|
import { ApiService } from '../../core/api.service';
|
||||||
import { CategoryService } from '../../core/category.service';
|
import { CategoryService } from '../../core/category.service';
|
||||||
import { ApiEntry, ApiEntryListItem, Category, ApiConvention } from '@datacat/shared';
|
import { ApiEntry, ApiEntryListItem, Category } from '@datacat/shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-api-detail',
|
selector: 'app-api-detail',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterLink],
|
imports: [CommonModule, RouterLink, ReactiveFormsModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="fr-container fr-mt-4w fr-mb-4w">
|
<div class="fr-container fr-mt-4w fr-mb-4w">
|
||||||
@@ -55,310 +56,357 @@ import { ApiEntry, ApiEntryListItem, Category, ApiConvention } from '@datacat/sh
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (api(); as entry) {
|
@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 -->
|
<!-- En-tête -->
|
||||||
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
<div class="fr-mb-3w" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;">
|
||||||
<div class="fr-col">
|
<!-- Gauche : titre + version -->
|
||||||
<h1 class="fr-h2 fr-mb-1w">{{ entry.title }}</h1>
|
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;min-width:0;">
|
||||||
<div class="fr-tags-group">
|
<h1 class="fr-h2 fr-mb-0">{{ entry.title }}</h1>
|
||||||
<span class="fr-tag">{{ entry.type }}</span>
|
@if (versions().length <= 1) {
|
||||||
<span class="fr-tag">v{{ entry.version }}</span>
|
<span class="fr-tag">v{{ entry.version }}</span>
|
||||||
<span [class]="badgeClass(entry.status)">{{ entry.status }}</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>
|
</div>
|
||||||
@if (versions().length > 1) {
|
}
|
||||||
<div class="fr-select-group fr-mt-2w" style="max-width:200px">
|
|
||||||
<label class="fr-label" for="version-select">Version</label>
|
@if (editingInfo()) {
|
||||||
<select class="fr-select" id="version-select" (change)="onVersionChange($event)">
|
<form [formGroup]="infoForm" (ngSubmit)="submitInfo()" novalidate class="fr-mb-3w">
|
||||||
@for (v of versions(); track v.id) {
|
<!-- Convention + Nom -->
|
||||||
<option [value]="v.id" [selected]="v.id === entry.id">{{ v.version }}</option>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Alerte statut PENDING -->
|
<div class="fr-btns-group fr-btns-group--inline fr-btns-group--right">
|
||||||
@if (entry.status === 'PENDING') {
|
<button type="button" class="fr-btn fr-btn--secondary" (click)="editingInfo.set(false)">Annuler</button>
|
||||||
<div class="fr-alert fr-alert--info fr-mb-3w">
|
<button type="submit" class="fr-btn" [disabled]="savingInfo()">
|
||||||
<p class="fr-alert__title">Génération en cours</p>
|
{{ savingInfo() ? 'Enregistrement...' : 'Enregistrer' }}
|
||||||
<p>La documentation est en cours de génération. Cette page se rafraîchit automatiquement.</p>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Alerte statut ERROR -->
|
<!-- Onglet Contacts -->
|
||||||
@if (entry.status === 'ERROR') {
|
@if (activeTab() === 'contacts') {
|
||||||
<div class="fr-alert fr-alert--error fr-mb-3w">
|
@if (!editingContacts()) {
|
||||||
<p class="fr-alert__title">Erreur de génération</p>
|
<div class="fr-card fr-mb-3w">
|
||||||
<p>{{ entry.errorMessage }}</p>
|
<div class="fr-card__body">
|
||||||
</div>
|
<div class="fr-card__content">
|
||||||
}
|
<div class="fr-grid-row fr-grid-row--gutters">
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
<!-- Métadonnées -->
|
<p class="fr-text--bold fr-mb-1w">Contact métier</p>
|
||||||
<div class="fr-card fr-mb-4w">
|
<p class="fr-mb-0">{{ entry.contactFunctionalName }}</p>
|
||||||
<div class="fr-card__body">
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactFunctionalEntity }}</p>
|
||||||
<div class="fr-card__content">
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactFunctionalEmail }}</p>
|
||||||
<h2 class="fr-h5 fr-mb-2w">Informations</h2>
|
</div>
|
||||||
<dl class="fr-grid-row fr-grid-row--gutters">
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
<p class="fr-text--bold fr-mb-1w">Contact technique</p>
|
||||||
<dt class="fr-text--bold">Version</dt>
|
<p class="fr-mb-0">{{ entry.contactTechnicalName }}</p>
|
||||||
<dd>{{ entry.version }}</dd>
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactTechnicalEntity }}</p>
|
||||||
</div>
|
<p class="fr-mb-0 fr-text--sm">{{ entry.contactTechnicalEmail }}</p>
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
</div>
|
||||||
<dt class="fr-text--bold">Type</dt>
|
</div>
|
||||||
<dd>{{ entry.type }}</dd>
|
<div class="fr-mt-2w">
|
||||||
</div>
|
<button class="fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-edit-line" (click)="startEditContacts()">
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
Modifier les contacts
|
||||||
<dt class="fr-text--bold">Fournisseur</dt>
|
</button>
|
||||||
<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>
|
||||||
}
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contacts -->
|
|
||||||
<div class="fr-card fr-mb-4w">
|
|
||||||
<div class="fr-card__body">
|
|
||||||
<div class="fr-card__content">
|
|
||||||
<h2 class="fr-h5 fr-mb-2w">Contacts</h2>
|
|
||||||
<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>
|
</div>
|
||||||
</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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
|
||||||
<a
|
|
||||||
[routerLink]="['/catalog', entry.id, 'docs']"
|
|
||||||
class="fr-btn"
|
|
||||||
[class.fr-btn--disabled]="entry.status !== 'GENERATED'"
|
|
||||||
[attr.aria-disabled]="entry.status !== 'GENERATED' ? 'true' : null"
|
|
||||||
>
|
|
||||||
Voir la documentation
|
|
||||||
</a>
|
|
||||||
<button class="fr-btn fr-btn--secondary" (click)="openEdit()">
|
|
||||||
Modifier
|
|
||||||
</button>
|
|
||||||
<button class="fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line" (click)="onNewVersion()">
|
|
||||||
Nouvelle version
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
[href]="'/api/apis/' + entry.id + '/yaml'"
|
|
||||||
class="fr-btn fr-btn--secondary"
|
|
||||||
download
|
|
||||||
>
|
|
||||||
Télécharger YAML
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
class="fr-btn fr-btn--secondary"
|
|
||||||
(click)="onRegenerate()"
|
|
||||||
[disabled]="regenerating()"
|
|
||||||
>
|
|
||||||
{{ regenerating() ? 'Régénération...' : 'Régénérer' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="fr-btn fr-btn--secondary"
|
|
||||||
(click)="onDelete()"
|
|
||||||
[disabled]="deleting()"
|
|
||||||
>
|
|
||||||
{{ deleting() ? 'Suppression...' : 'Supprimer' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</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:640px;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>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- 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="edit-convention">Convention</label>
|
|
||||||
<select id="edit-convention" class="fr-select" [value]="editConvention()"
|
|
||||||
(change)="editConvention.set($any($event.target).value)">
|
|
||||||
<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]="editNameError()">
|
|
||||||
<label class="fr-label" for="edit-name">Nom de l'API</label>
|
|
||||||
<input id="edit-name" class="fr-input" [class.fr-input--error]="editNameError()" type="text"
|
|
||||||
[value]="editName()"
|
|
||||||
(input)="editName.set($any($event.target).value); editNameError.set(false)"
|
|
||||||
(blur)="editNameError.set(!editName().trim())" />
|
|
||||||
@if (editNameError()) { <p class="fr-error-text">Le nom est obligatoire.</p> }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="fr-hint-text fr-mb-2w">Titre : <strong>{{ editPreviewTitle() }}</strong></p>
|
|
||||||
|
|
||||||
<!-- Catégorie -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Fournisseur -->
|
|
||||||
<div class="fr-input-group fr-mb-2w" [class.fr-input-group--error]="editProviderError()">
|
|
||||||
<label class="fr-label" for="edit-provider">Fournisseur <span style="color:var(--text-default-error)">*</span></label>
|
|
||||||
<input id="edit-provider" class="fr-input" [class.fr-input--error]="editProviderError()" type="text"
|
|
||||||
[value]="editProvider()"
|
|
||||||
(input)="editProvider.set($any($event.target).value); editProviderError.set(false)"
|
|
||||||
(blur)="editProviderError.set(!editProvider().trim())" />
|
|
||||||
@if (editProviderError()) { <p class="fr-error-text">Le fournisseur est obligatoire.</p> }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 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="edit-v-major">Majeure (X)</label>
|
|
||||||
<input id="edit-v-major" class="fr-input" type="number" min="0"
|
|
||||||
[value]="editVersionMajor()"
|
|
||||||
(input)="editVersionMajor.set(+$any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-4">
|
|
||||||
<div class="fr-input-group">
|
|
||||||
<label class="fr-label" for="edit-v-minor">Mineure (Y)</label>
|
|
||||||
<input id="edit-v-minor" class="fr-input" type="number" min="0"
|
|
||||||
[value]="editVersionMinor()"
|
|
||||||
(input)="editVersionMinor.set(+$any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-4">
|
|
||||||
<div class="fr-input-group">
|
|
||||||
<label class="fr-label" for="edit-v-patch">Correctif (Z)</label>
|
|
||||||
<input id="edit-v-patch" class="fr-input" type="number" min="0"
|
|
||||||
[value]="editVersionPatch()"
|
|
||||||
(input)="editVersionPatch.set(+$any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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-2w">
|
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
|
||||||
<div class="fr-input-group">
|
|
||||||
<label class="fr-label" for="edit-cf-name">Nom et prénom</label>
|
|
||||||
<input id="edit-cf-name" class="fr-input" type="text" [value]="editContactFunctionalName()"
|
|
||||||
(input)="editContactFunctionalName.set($any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
|
||||||
<div class="fr-input-group">
|
|
||||||
<label class="fr-label" for="edit-cf-entity">Entité</label>
|
|
||||||
<input id="edit-cf-entity" class="fr-input" type="text" [value]="editContactFunctionalEntity()"
|
|
||||||
(input)="editContactFunctionalEntity.set($any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
|
||||||
<div class="fr-input-group">
|
|
||||||
<label class="fr-label" for="edit-cf-email">Email</label>
|
|
||||||
<input id="edit-cf-email" class="fr-input" type="email" [value]="editContactFunctionalEmail()"
|
|
||||||
(input)="editContactFunctionalEmail.set($any($event.target).value)" />
|
|
||||||
</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">
|
|
||||||
<label class="fr-label" for="edit-ct-name">Nom et prénom</label>
|
|
||||||
<input id="edit-ct-name" class="fr-input" type="text" [value]="editContactTechnicalName()"
|
|
||||||
(input)="editContactTechnicalName.set($any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
|
||||||
<div class="fr-input-group">
|
|
||||||
<label class="fr-label" for="edit-ct-entity">Entité</label>
|
|
||||||
<input id="edit-ct-entity" class="fr-input" type="text" [value]="editContactTechnicalEntity()"
|
|
||||||
(input)="editContactTechnicalEntity.set($any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
|
||||||
<div class="fr-input-group">
|
|
||||||
<label class="fr-label" for="edit-ct-email">Email</label>
|
|
||||||
<input id="edit-ct-email" class="fr-input" type="email" [value]="editContactTechnicalEmail()"
|
|
||||||
(input)="editContactTechnicalEmail.set($any($event.target).value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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 {
|
export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||||
@@ -366,6 +414,7 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
|||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private apiService = inject(ApiService);
|
private apiService = inject(ApiService);
|
||||||
private categoryService = inject(CategoryService);
|
private categoryService = inject(CategoryService);
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
|
||||||
api = signal<ApiEntry | null>(null);
|
api = signal<ApiEntry | null>(null);
|
||||||
versions = signal<ApiEntryListItem[]>([]);
|
versions = signal<ApiEntryListItem[]>([]);
|
||||||
@@ -374,32 +423,41 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
|||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
regenerating = signal(false);
|
regenerating = signal(false);
|
||||||
deleting = signal(false);
|
deleting = signal(false);
|
||||||
|
settingCurrent = signal(false);
|
||||||
/* Modale édition */
|
|
||||||
showEditModal = signal(false);
|
|
||||||
editLoading = signal(false);
|
|
||||||
editError = signal<string | null>(null);
|
|
||||||
editNameError = signal(false);
|
|
||||||
editProviderError = signal(false);
|
|
||||||
categories = signal<Category[]>([]);
|
categories = signal<Category[]>([]);
|
||||||
editConvention = signal<ApiConvention>('CONSULTER');
|
actionsOpen = signal(false);
|
||||||
editName = signal('');
|
|
||||||
editProvider = signal('');
|
|
||||||
editVersionMajor = signal(0);
|
|
||||||
editVersionMinor = signal(0);
|
|
||||||
editVersionPatch = signal(0);
|
|
||||||
editCategoryId = signal<string | null>(null);
|
|
||||||
editDescription = signal('');
|
|
||||||
editContactFunctionalName = signal('');
|
|
||||||
editContactFunctionalEntity = signal('');
|
|
||||||
editContactFunctionalEmail = signal('');
|
|
||||||
editContactTechnicalName = signal('');
|
|
||||||
editContactTechnicalEntity = signal('');
|
|
||||||
editContactTechnicalEmail = signal('');
|
|
||||||
|
|
||||||
editPreviewTitle = computed(() => {
|
backUrl = computed<string[]>(() => {
|
||||||
const labels: Record<ApiConvention, string> = { CONSULTER: 'Consulter', ENREGISTRER: 'Enregistrer', ETRE_NOTIFIE: 'Être notifié' };
|
const crumbs = this.categoryBreadcrumb();
|
||||||
return `${labels[this.editConvention()]} ${this.editName()}`.trim();
|
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 pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -407,12 +465,15 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
|||||||
private id = '';
|
private id = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.categoryService.list().subscribe((res) => this.categories.set(res.items));
|
||||||
this.routeSub = this.route.paramMap.subscribe((params) => {
|
this.routeSub = this.route.paramMap.subscribe((params) => {
|
||||||
this.id = params.get('id') ?? '';
|
this.id = params.get('id') ?? '';
|
||||||
this.api.set(null);
|
this.api.set(null);
|
||||||
this.versions.set([]);
|
this.versions.set([]);
|
||||||
this.error.set(null);
|
this.error.set(null);
|
||||||
this.categoryBreadcrumb.set([]);
|
this.categoryBreadcrumb.set([]);
|
||||||
|
this.editingInfo.set(false);
|
||||||
|
this.editingContacts.set(false);
|
||||||
this.clearPoll();
|
this.clearPoll();
|
||||||
this.loadApi();
|
this.loadApi();
|
||||||
});
|
});
|
||||||
@@ -434,6 +495,21 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(['/upload'], { queryParams: { from: this.api()?.id } });
|
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() {
|
onRegenerate() {
|
||||||
this.regenerating.set(true);
|
this.regenerating.set(true);
|
||||||
this.apiService.regenerate(this.id).subscribe({
|
this.apiService.regenerate(this.id).subscribe({
|
||||||
@@ -446,64 +522,73 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openEdit() {
|
startEditInfo() {
|
||||||
const entry = this.api();
|
const entry = this.api();
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
this.editConvention.set(entry.convention);
|
this.infoForm.patchValue({
|
||||||
this.editName.set(entry.name);
|
convention: entry.convention,
|
||||||
this.editProvider.set(entry.provider);
|
name: entry.name,
|
||||||
this.editVersionMajor.set(entry.versionMajor);
|
provider: entry.provider,
|
||||||
this.editVersionMinor.set(entry.versionMinor);
|
versionMajor: entry.versionMajor,
|
||||||
this.editVersionPatch.set(entry.versionPatch);
|
versionMinor: entry.versionMinor,
|
||||||
this.editCategoryId.set(entry.categoryId ?? null);
|
versionPatch: entry.versionPatch,
|
||||||
this.editDescription.set(entry.description ?? '');
|
description: entry.description ?? '',
|
||||||
this.editContactFunctionalName.set(entry.contactFunctionalName);
|
categoryId: entry.categoryId ?? '',
|
||||||
this.editContactFunctionalEntity.set(entry.contactFunctionalEntity);
|
});
|
||||||
this.editContactFunctionalEmail.set(entry.contactFunctionalEmail);
|
this.editingInfo.set(true);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeEdit() {
|
startEditContacts() {
|
||||||
this.showEditModal.set(false);
|
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() {
|
submitInfo() {
|
||||||
this.editNameError.set(!this.editName().trim());
|
if (this.infoForm.invalid) { this.infoForm.markAllAsTouched(); return; }
|
||||||
this.editProviderError.set(!this.editProvider().trim());
|
const entry = this.api();
|
||||||
if (!this.editName().trim() || !this.editProvider().trim()) return;
|
if (!entry) return;
|
||||||
this.editLoading.set(true);
|
this.savingInfo.set(true);
|
||||||
this.apiService.update(this.id, {
|
const raw = this.infoForm.getRawValue();
|
||||||
convention: this.editConvention(),
|
this.apiService.update(entry.id, {
|
||||||
name: this.editName().trim(),
|
convention: raw.convention ?? 'CONSULTER',
|
||||||
provider: this.editProvider().trim(),
|
name: raw.name ?? '',
|
||||||
versionMajor: this.editVersionMajor(),
|
provider: raw.provider ?? '',
|
||||||
versionMinor: this.editVersionMinor(),
|
versionMajor: raw.versionMajor ?? 0,
|
||||||
versionPatch: this.editVersionPatch(),
|
versionMinor: raw.versionMinor ?? 0,
|
||||||
categoryId: this.editCategoryId() || null,
|
versionPatch: raw.versionPatch ?? 0,
|
||||||
description: this.editDescription().trim() || null,
|
categoryId: raw.categoryId || null,
|
||||||
contactFunctionalName: this.editContactFunctionalName().trim(),
|
description: raw.description || null,
|
||||||
contactFunctionalEntity: this.editContactFunctionalEntity().trim(),
|
|
||||||
contactFunctionalEmail: this.editContactFunctionalEmail().trim(),
|
|
||||||
contactTechnicalName: this.editContactTechnicalName().trim(),
|
|
||||||
contactTechnicalEntity: this.editContactTechnicalEntity().trim(),
|
|
||||||
contactTechnicalEmail: this.editContactTechnicalEmail().trim(),
|
|
||||||
}).subscribe({
|
}).subscribe({
|
||||||
next: (entry) => {
|
next: (updated) => { this.api.set(updated); this.savingInfo.set(false); this.editingInfo.set(false); },
|
||||||
this.api.set(entry);
|
error: () => this.savingInfo.set(false),
|
||||||
this.editLoading.set(false);
|
});
|
||||||
this.showEditModal.set(false);
|
}
|
||||||
},
|
|
||||||
error: (err: { error?: { message?: string } }) => {
|
submitContacts() {
|
||||||
this.editLoading.set(false);
|
if (this.contactForm.invalid) { this.contactForm.markAllAsTouched(); return; }
|
||||||
this.editError.set(err?.error?.message ?? 'Une erreur est survenue.');
|
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';
|
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() {
|
private loadApi() {
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.apiService.get(this.id).subscribe({
|
this.apiService.get(this.id).subscribe({
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- En-tête -->
|
<!-- En-tête -->
|
||||||
|
@if (categoryId()) {
|
||||||
|
<a class="fr-link fr-icon-arrow-left-line fr-link--icon-left fr-mb-2w" style="display:inline-flex;"
|
||||||
|
[routerLink]="backUrl()">Retour</a>
|
||||||
|
}
|
||||||
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
||||||
<div class="fr-col">
|
<div class="fr-col">
|
||||||
<h1 class="fr-h2 fr-mb-0">
|
<h1 class="fr-h2 fr-mb-0">
|
||||||
@@ -146,8 +150,10 @@ import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal
|
|||||||
}
|
}
|
||||||
<p class="fr-tile__detail">
|
<p class="fr-tile__detail">
|
||||||
<span [class]="typeBadgeClass(api.type)">{{ api.type }}</span>
|
<span [class]="typeBadgeClass(api.type)">{{ api.type }}</span>
|
||||||
|
<span class="fr-badge fr-badge--sm">v{{ api.version }}</span>
|
||||||
<span [class]="statusBadgeClass(api.status)">{{ api.status }}</span>
|
@if (api.isCurrent) {
|
||||||
|
<span class="fr-badge fr-badge--success fr-badge--sm">Courante</span>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,7 +221,10 @@ import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal
|
|||||||
}
|
}
|
||||||
<div class="fr-badges-group fr-mt-1w">
|
<div class="fr-badges-group fr-mt-1w">
|
||||||
<span [class]="typeBadgeClass(api.type)">{{ api.type }}</span>
|
<span [class]="typeBadgeClass(api.type)">{{ api.type }}</span>
|
||||||
<span [class]="statusBadgeClass(api.status)">{{ api.status }}</span>
|
<span class="fr-badge fr-badge--sm">v{{ api.version }}</span>
|
||||||
|
@if (api.isCurrent) {
|
||||||
|
<span class="fr-badge fr-badge--success fr-badge--sm">Courante</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,6 +309,12 @@ export class BrowseComponent implements OnInit {
|
|||||||
return d.breadcrumb.slice(0, -1);
|
return d.breadcrumb.slice(0, -1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
backUrl = computed<string[]>(() => {
|
||||||
|
const anc = this.ancestors();
|
||||||
|
if (anc.length > 0) return ['/browse', anc[anc.length - 1].id];
|
||||||
|
return ['/browse'];
|
||||||
|
});
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.paramMap
|
this.route.paramMap
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
@@ -398,9 +413,4 @@ export class BrowseComponent implements OnInit {
|
|||||||
: 'fr-badge fr-badge--sm fr-badge--blue-ecume';
|
: '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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user