front: extrait les templates inline vers des fichiers .html
upload (900->427 l.) et api-detail (700->330 l.) : le template inline géant passe en templateUrl. Aucun changement de comportement (build + eslint a11y + runtime OK). Composants bien plus navigables ; les .html sont désormais lintés par angular-eslint (a11y). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
369
front-public/src/app/pages/api-detail/api-detail.component.html
Normal file
369
front-public/src/app/pages/api-detail/api-detail.component.html
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
<div class="fr-container fr-mt-4w fr-mb-4w">
|
||||||
|
<app-breadcrumb [items]="breadcrumbItems()" />
|
||||||
|
|
||||||
|
@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 Voir doc + Actions dropdown -->
|
||||||
|
<div style="display:flex;align-items:center;gap:0.5rem;flex-shrink:0;">
|
||||||
|
<a class="fr-btn fr-btn--icon-left fr-icon-eye-line"
|
||||||
|
[routerLink]="entry.status === 'GENERATED' ? ['/catalog', entry.id, 'docs'] : null"
|
||||||
|
[style.opacity]="entry.status !== 'GENERATED' ? '0.5' : '1'"
|
||||||
|
[style.pointer-events]="entry.status !== 'GENERATED' ? 'none' : 'auto'">
|
||||||
|
Voir la documentation
|
||||||
|
</a>
|
||||||
|
<div style="position:relative;">
|
||||||
|
<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;">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
@@ -22,377 +22,7 @@ import { ApiEntry, ApiEntryListItem, Category } from '@datacat/shared';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterLink, ReactiveFormsModule, BreadcrumbComponent],
|
imports: [CommonModule, RouterLink, ReactiveFormsModule, BreadcrumbComponent],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
templateUrl: "./api-detail.component.html",
|
||||||
<div class="fr-container fr-mt-4w fr-mb-4w">
|
|
||||||
<app-breadcrumb [items]="breadcrumbItems()" />
|
|
||||||
|
|
||||||
@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 Voir doc + Actions dropdown -->
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;flex-shrink:0;">
|
|
||||||
<a class="fr-btn fr-btn--icon-left fr-icon-eye-line"
|
|
||||||
[routerLink]="entry.status === 'GENERATED' ? ['/catalog', entry.id, 'docs'] : null"
|
|
||||||
[style.opacity]="entry.status !== 'GENERATED' ? '0.5' : '1'"
|
|
||||||
[style.pointer-events]="entry.status !== 'GENERATED' ? 'none' : 'auto'">
|
|
||||||
Voir la documentation
|
|
||||||
</a>
|
|
||||||
<div style="position:relative;">
|
|
||||||
<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;">
|
|
||||||
<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>
|
|
||||||
</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 {
|
export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
|
|||||||
472
front-public/src/app/pages/upload/upload.component.html
Normal file
472
front-public/src/app/pages/upload/upload.component.html
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
<div class="fr-container fr-mt-4w fr-mb-4w">
|
||||||
|
<app-breadcrumb [items]="breadcrumbItems" />
|
||||||
|
|
||||||
|
<h1 class="fr-h2 fr-mb-4w">Importer une API</h1>
|
||||||
|
|
||||||
|
<!-- Stepper numéroté -->
|
||||||
|
<div class="fr-mb-4w" style="display:flex;align-items:flex-start;">
|
||||||
|
@for (title of stepTitles; track $index) {
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;flex:none;min-width:6rem;">
|
||||||
|
<div
|
||||||
|
[style.background]="step() > $index + 1 ? '#1f8d49' : step() === $index + 1 ? '#000091' : '#e5e5e5'"
|
||||||
|
[style.color]="step() >= $index + 1 ? 'white' : '#666'"
|
||||||
|
style="width:2rem;height:2rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.875rem;transition:background 0.2s;">
|
||||||
|
@if (step() > $index + 1) {
|
||||||
|
<span class="fr-icon-check-line" aria-hidden="true" style="font-size:0.875rem"></span>
|
||||||
|
} @else {
|
||||||
|
{{ $index + 1 }}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span style="font-size:0.75rem;margin-top:0.4rem;text-align:center;line-height:1.2;"
|
||||||
|
[style.font-weight]="step() === $index + 1 ? '700' : '400'"
|
||||||
|
[style.color]="step() === $index + 1 ? '#000091' : step() > $index + 1 ? '#1f8d49' : '#666'">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if ($index < stepTitles.length - 1) {
|
||||||
|
<div style="flex:1;height:2px;margin-top:1rem;transition:background 0.2s;"
|
||||||
|
[style.background]="step() > $index + 1 ? '#1f8d49' : '#e5e5e5'">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Étape 1 : Fichier(s) YAML -->
|
||||||
|
@if (step() === 1) {
|
||||||
|
<div class="fr-upload-group fr-mb-4w">
|
||||||
|
<label class="fr-label" for="file-input">
|
||||||
|
Fichier(s) YAML
|
||||||
|
<span class="fr-hint-text">
|
||||||
|
Formats acceptés : .yaml, .yml — Sélectionnez plusieurs fichiers si votre spec utilise des $ref
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="file-input"
|
||||||
|
class="fr-upload"
|
||||||
|
type="file"
|
||||||
|
accept=".yaml,.yml"
|
||||||
|
multiple
|
||||||
|
(change)="onFileChange($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (fileError()) {
|
||||||
|
<div class="fr-alert fr-alert--error fr-mb-3w">
|
||||||
|
<p>{{ fileError() }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (selectedFiles().length > 0) {
|
||||||
|
<div class="fr-mb-3w">
|
||||||
|
@for (file of selectedFiles(); track file.name) {
|
||||||
|
<div class="fr-callout fr-callout--blue-cumulus fr-mb-1w" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;">
|
||||||
|
<span>
|
||||||
|
<strong>{{ file.name }}</strong>
|
||||||
|
({{ (file.size / 1024).toFixed(1) }} Ko)
|
||||||
|
</span>
|
||||||
|
@if (file.name === detectedMainFilename()) {
|
||||||
|
<span class="fr-badge fr-badge--success fr-badge--sm">Principal</span>
|
||||||
|
} @else if (selectedFiles().length > 1) {
|
||||||
|
<span class="fr-badge fr-badge--blue-cumulus fr-badge--sm">Secondaire</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (validating()) {
|
||||||
|
<div class="fr-callout fr-mb-3w">
|
||||||
|
<p>Validation en cours...</p>
|
||||||
|
</div>
|
||||||
|
} @else if (validationDone()) {
|
||||||
|
@if (validationErrors().length > 0) {
|
||||||
|
<div class="fr-alert fr-alert--error fr-mb-3w">
|
||||||
|
<p class="fr-alert__title">Fichier(s) invalide(s)</p>
|
||||||
|
@for (err of validationErrors(); track err.file) {
|
||||||
|
<p>{{ err.file ? err.file + ' : ' : '' }}{{ err.message }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="fr-alert fr-alert--success fr-mb-3w">
|
||||||
|
<p class="fr-alert__title">Fichier(s) valide(s)</p>
|
||||||
|
<p>Le fichier a été validé avec succès.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="fr-btns-group fr-btns-group--inline-sm">
|
||||||
|
<button
|
||||||
|
class="fr-btn"
|
||||||
|
(click)="goToStep2()"
|
||||||
|
[disabled]="!canProceedFromStep1()"
|
||||||
|
>
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
<a routerLink="/catalog" class="fr-btn fr-btn--secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Étape 2 : Informations -->
|
||||||
|
@if (step() === 2) {
|
||||||
|
<!-- Bannière nouvelle version -->
|
||||||
|
@if (fromApi(); as origin) {
|
||||||
|
<div class="fr-callout fr-callout--blue-cumulus fr-mb-3w">
|
||||||
|
<p class="fr-callout__text">
|
||||||
|
Nouvelle version de : <strong>{{ origin.title }}</strong> (v{{ origin.version }})
|
||||||
|
— le nom et la convention sont verrouillés.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form [formGroup]="metaForm" (ngSubmit)="goToStep3()" novalidate>
|
||||||
|
|
||||||
|
<!-- Titre : 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" [class.fr-select-group--error]="conventionInvalid()">
|
||||||
|
<label class="fr-label" for="convention-input">Convention</label>
|
||||||
|
<select
|
||||||
|
id="convention-input"
|
||||||
|
class="fr-select"
|
||||||
|
[class.fr-select--error]="conventionInvalid()"
|
||||||
|
formControlName="convention"
|
||||||
|
>
|
||||||
|
<option value="CONSULTER">Consulter</option>
|
||||||
|
<option value="ENREGISTRER">Enregistrer</option>
|
||||||
|
<option value="ETRE_NOTIFIE">Être notifié</option>
|
||||||
|
</select>
|
||||||
|
@if (conventionInvalid()) {
|
||||||
|
<p class="fr-error-text">La convention est obligatoire.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-12 fr-col-md-8">
|
||||||
|
<div class="fr-input-group" [class.fr-input-group--error]="nameInvalid()">
|
||||||
|
<label class="fr-label" for="name-input">Nom de l'API</label>
|
||||||
|
<input
|
||||||
|
id="name-input"
|
||||||
|
class="fr-input"
|
||||||
|
[class.fr-input--error]="nameInvalid()"
|
||||||
|
formControlName="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Référentiel Adresses"
|
||||||
|
/>
|
||||||
|
@if (nameInvalid()) {
|
||||||
|
<p class="fr-error-text">Le nom est obligatoire.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="fr-hint-text fr-mb-3w">Titre complet : <strong>{{ previewTitle() }}</strong></p>
|
||||||
|
|
||||||
|
<!-- Catégorie -->
|
||||||
|
<div class="fr-select-group fr-mb-3w">
|
||||||
|
<label class="fr-label" for="category-input">
|
||||||
|
Catégorie
|
||||||
|
</label>
|
||||||
|
<select id="category-input" class="fr-select" formControlName="categoryId">
|
||||||
|
<option value="">— Sans catégorie —</option>
|
||||||
|
@for (cat of categories(); track cat.id) {
|
||||||
|
<option [value]="cat.id">{{ categoryLabel(cat) }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fournisseur -->
|
||||||
|
<div class="fr-input-group fr-mb-3w" [class.fr-input-group--error]="providerInvalid()">
|
||||||
|
<label class="fr-label" for="provider-input">
|
||||||
|
Fournisseur <span style="color:var(--text-default-error)">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="provider-input"
|
||||||
|
class="fr-input"
|
||||||
|
[class.fr-input--error]="providerInvalid()"
|
||||||
|
formControlName="provider"
|
||||||
|
type="text"
|
||||||
|
placeholder="DINUM, Direction XYZ..."
|
||||||
|
/>
|
||||||
|
@if (providerInvalid()) {
|
||||||
|
<p class="fr-error-text">Le fournisseur est obligatoire.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Type -->
|
||||||
|
<div class="fr-select-group fr-mb-3w">
|
||||||
|
<label class="fr-label" for="type-input">
|
||||||
|
Type <span style="color:var(--text-default-error)">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="type-input" class="fr-select" formControlName="type">
|
||||||
|
<option value="ASYNCAPI">AsyncAPI</option>
|
||||||
|
<option value="OPENAPI">OpenAPI</option>
|
||||||
|
</select>
|
||||||
|
@if (detectedType()) {
|
||||||
|
<p class="fr-hint-text">Détecté automatiquement depuis le fichier YAML</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="fr-input-group fr-mb-3w">
|
||||||
|
<label class="fr-label" for="description-input">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description-input"
|
||||||
|
class="fr-input"
|
||||||
|
formControlName="description"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Description de l'API..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Version X.Y.Z -->
|
||||||
|
<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-1w">
|
||||||
|
<div class="fr-col-4">
|
||||||
|
<div class="fr-input-group" [class.fr-input-group--error]="versionMajorInvalid()">
|
||||||
|
<label class="fr-label" for="version-major-input">Majeure (X)</label>
|
||||||
|
<input
|
||||||
|
id="version-major-input"
|
||||||
|
class="fr-input"
|
||||||
|
[class.fr-input--error]="versionMajorInvalid()"
|
||||||
|
formControlName="versionMajor"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-4">
|
||||||
|
<div class="fr-input-group" [class.fr-input-group--error]="versionMinorInvalid()">
|
||||||
|
<label class="fr-label" for="version-minor-input">Mineure (Y)</label>
|
||||||
|
<input
|
||||||
|
id="version-minor-input"
|
||||||
|
class="fr-input"
|
||||||
|
[class.fr-input--error]="versionMinorInvalid()"
|
||||||
|
formControlName="versionMinor"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-4">
|
||||||
|
<div class="fr-input-group" [class.fr-input-group--error]="versionPatchInvalid()">
|
||||||
|
<label class="fr-label" for="version-patch-input">Correctif (Z)</label>
|
||||||
|
<input
|
||||||
|
id="version-patch-input"
|
||||||
|
class="fr-input"
|
||||||
|
[class.fr-input--error]="versionPatchInvalid()"
|
||||||
|
formControlName="versionPatch"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (versionAlreadyExists()) {
|
||||||
|
<div class="fr-alert fr-alert--warning fr-mb-3w">
|
||||||
|
<p class="fr-alert__title">Version déjà existante</p>
|
||||||
|
<p>
|
||||||
|
La version {{ formValues().versionMajor }}.{{ formValues().versionMinor }}.{{ formValues().versionPatch }}
|
||||||
|
existe déjà pour cette API. Choisissez un numéro de version différent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="fr-btns-group fr-btns-group--inline-sm">
|
||||||
|
<button type="submit" class="fr-btn" [disabled]="versionAlreadyExists()">Suivant</button>
|
||||||
|
<button type="button" class="fr-btn fr-btn--secondary" (click)="step.set(1)">
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Étape 3 : Contacts -->
|
||||||
|
@if (step() === 3) {
|
||||||
|
<form [formGroup]="metaForm" (ngSubmit)="goToStep4()" novalidate>
|
||||||
|
|
||||||
|
<!-- 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]="fieldInvalid('contactFunctionalName')">
|
||||||
|
<label class="fr-label" for="cf-name-input">Nom et prénom</label>
|
||||||
|
<input id="cf-name-input" class="fr-input"
|
||||||
|
[class.fr-input--error]="fieldInvalid('contactFunctionalName')"
|
||||||
|
formControlName="contactFunctionalName" type="text" placeholder="Prénom Nom" />
|
||||||
|
@if (fieldInvalid('contactFunctionalName')) {
|
||||||
|
<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]="fieldInvalid('contactFunctionalEntity')">
|
||||||
|
<label class="fr-label" for="cf-entity-input">Entité</label>
|
||||||
|
<input id="cf-entity-input" class="fr-input"
|
||||||
|
[class.fr-input--error]="fieldInvalid('contactFunctionalEntity')"
|
||||||
|
formControlName="contactFunctionalEntity" type="text" placeholder="Direction XYZ" />
|
||||||
|
@if (fieldInvalid('contactFunctionalEntity')) {
|
||||||
|
<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]="fieldInvalid('contactFunctionalEmail')">
|
||||||
|
<label class="fr-label" for="cf-email-input">Email</label>
|
||||||
|
<input id="cf-email-input" class="fr-input"
|
||||||
|
[class.fr-input--error]="fieldInvalid('contactFunctionalEmail')"
|
||||||
|
formControlName="contactFunctionalEmail" type="email" placeholder="prenom.nom@example.fr" />
|
||||||
|
@if (fieldInvalid('contactFunctionalEmail')) {
|
||||||
|
<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-4w">
|
||||||
|
<div class="fr-col-12 fr-col-md-4">
|
||||||
|
<div class="fr-input-group" [class.fr-input-group--error]="fieldInvalid('contactTechnicalName')">
|
||||||
|
<label class="fr-label" for="ct-name-input">Nom et prénom</label>
|
||||||
|
<input id="ct-name-input" class="fr-input"
|
||||||
|
[class.fr-input--error]="fieldInvalid('contactTechnicalName')"
|
||||||
|
formControlName="contactTechnicalName" type="text" placeholder="Prénom Nom" />
|
||||||
|
@if (fieldInvalid('contactTechnicalName')) {
|
||||||
|
<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]="fieldInvalid('contactTechnicalEntity')">
|
||||||
|
<label class="fr-label" for="ct-entity-input">Entité</label>
|
||||||
|
<input id="ct-entity-input" class="fr-input"
|
||||||
|
[class.fr-input--error]="fieldInvalid('contactTechnicalEntity')"
|
||||||
|
formControlName="contactTechnicalEntity" type="text" placeholder="Équipe Technique" />
|
||||||
|
@if (fieldInvalid('contactTechnicalEntity')) {
|
||||||
|
<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]="fieldInvalid('contactTechnicalEmail')">
|
||||||
|
<label class="fr-label" for="ct-email-input">Email</label>
|
||||||
|
<input id="ct-email-input" class="fr-input"
|
||||||
|
[class.fr-input--error]="fieldInvalid('contactTechnicalEmail')"
|
||||||
|
formControlName="contactTechnicalEmail" type="email" placeholder="tech@example.fr" />
|
||||||
|
@if (fieldInvalid('contactTechnicalEmail')) {
|
||||||
|
<p class="fr-error-text">Email valide requis.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fr-btns-group fr-btns-group--inline-sm">
|
||||||
|
<button type="submit" class="fr-btn">Suivant</button>
|
||||||
|
<button type="button" class="fr-btn fr-btn--secondary" (click)="step.set(2)">
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Étape 4 : Confirmation -->
|
||||||
|
@if (step() === 4) {
|
||||||
|
<div class="fr-card fr-mb-4w">
|
||||||
|
<div class="fr-card__body">
|
||||||
|
<div class="fr-card__content">
|
||||||
|
<h2 class="fr-h5 fr-mb-3w">Récapitulatif</h2>
|
||||||
|
<dl class="fr-grid-row fr-grid-row--gutters">
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
|
<dt class="fr-text--bold">Fichier{{ selectedFiles().length > 1 ? 's' : '' }}</dt>
|
||||||
|
<dd>
|
||||||
|
@if (selectedFiles().length === 0) {
|
||||||
|
<span class="fr-badge fr-badge--warning fr-badge--sm">Aucun fichier</span>
|
||||||
|
} @else if (selectedFiles().length === 1) {
|
||||||
|
{{ selectedFiles()[0].name }}
|
||||||
|
} @else {
|
||||||
|
@for (f of selectedFiles(); track f.name) {
|
||||||
|
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||||
|
{{ f.name }}
|
||||||
|
@if (f.name === detectedMainFilename()) {
|
||||||
|
<span class="fr-badge fr-badge--success fr-badge--sm">Principal</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
|
<dt class="fr-text--bold">Type</dt>
|
||||||
|
<dd>{{ displayType() }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-12">
|
||||||
|
<dt class="fr-text--bold">Titre</dt>
|
||||||
|
<dd>{{ previewTitle() }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
|
<dt class="fr-text--bold">Version</dt>
|
||||||
|
<dd>{{ metaForm.value.versionMajor }}.{{ metaForm.value.versionMinor }}.{{ metaForm.value.versionPatch }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
|
<dt class="fr-text--bold">Fournisseur</dt>
|
||||||
|
<dd>{{ metaForm.value.provider }}</dd>
|
||||||
|
</div>
|
||||||
|
@if (metaForm.value.categoryId) {
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
|
<dt class="fr-text--bold">Catégorie</dt>
|
||||||
|
<dd>{{ selectedCategoryName() }}</dd>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (metaForm.value.description) {
|
||||||
|
<div class="fr-col-12">
|
||||||
|
<dt class="fr-text--bold">Description</dt>
|
||||||
|
<dd>{{ metaForm.value.description }}</dd>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
|
<dt class="fr-text--bold">Contact métier</dt>
|
||||||
|
<dd>
|
||||||
|
{{ metaForm.value.contactFunctionalName }}<br>
|
||||||
|
<span class="fr-text--sm">{{ metaForm.value.contactFunctionalEntity }}</span><br>
|
||||||
|
<span class="fr-text--sm">{{ metaForm.value.contactFunctionalEmail }}</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-12 fr-col-md-6">
|
||||||
|
<dt class="fr-text--bold">Contact technique</dt>
|
||||||
|
<dd>
|
||||||
|
{{ metaForm.value.contactTechnicalName }}<br>
|
||||||
|
<span class="fr-text--sm">{{ metaForm.value.contactTechnicalEntity }}</span><br>
|
||||||
|
<span class="fr-text--sm">{{ metaForm.value.contactTechnicalEmail }}</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (uploadError()) {
|
||||||
|
<div class="fr-alert fr-alert--error fr-mb-3w">
|
||||||
|
<p class="fr-alert__title">Erreur lors de l'import</p>
|
||||||
|
<p>{{ uploadError() }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="fr-btns-group fr-btns-group--inline-sm">
|
||||||
|
<button
|
||||||
|
class="fr-btn"
|
||||||
|
(click)="onSubmit()"
|
||||||
|
[disabled]="uploading()"
|
||||||
|
>
|
||||||
|
{{ uploading() ? 'Import en cours...' : 'Importer' }}
|
||||||
|
</button>
|
||||||
|
<button class="fr-btn fr-btn--secondary" (click)="step.set(3)" [disabled]="uploading()">
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -21,480 +21,7 @@ import { ApiEntry, ApiEntryListItem, Category, API_CONVENTION_LABELS, ApiConvent
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReactiveFormsModule, RouterLink, BreadcrumbComponent],
|
imports: [CommonModule, ReactiveFormsModule, RouterLink, BreadcrumbComponent],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
templateUrl: "./upload.component.html",
|
||||||
<div class="fr-container fr-mt-4w fr-mb-4w">
|
|
||||||
<app-breadcrumb [items]="breadcrumbItems" />
|
|
||||||
|
|
||||||
<h1 class="fr-h2 fr-mb-4w">Importer une API</h1>
|
|
||||||
|
|
||||||
<!-- Stepper numéroté -->
|
|
||||||
<div class="fr-mb-4w" style="display:flex;align-items:flex-start;">
|
|
||||||
@for (title of stepTitles; track $index) {
|
|
||||||
<div style="display:flex;flex-direction:column;align-items:center;flex:none;min-width:6rem;">
|
|
||||||
<div
|
|
||||||
[style.background]="step() > $index + 1 ? '#1f8d49' : step() === $index + 1 ? '#000091' : '#e5e5e5'"
|
|
||||||
[style.color]="step() >= $index + 1 ? 'white' : '#666'"
|
|
||||||
style="width:2rem;height:2rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.875rem;transition:background 0.2s;">
|
|
||||||
@if (step() > $index + 1) {
|
|
||||||
<span class="fr-icon-check-line" aria-hidden="true" style="font-size:0.875rem"></span>
|
|
||||||
} @else {
|
|
||||||
{{ $index + 1 }}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<span style="font-size:0.75rem;margin-top:0.4rem;text-align:center;line-height:1.2;"
|
|
||||||
[style.font-weight]="step() === $index + 1 ? '700' : '400'"
|
|
||||||
[style.color]="step() === $index + 1 ? '#000091' : step() > $index + 1 ? '#1f8d49' : '#666'">
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
@if ($index < stepTitles.length - 1) {
|
|
||||||
<div style="flex:1;height:2px;margin-top:1rem;transition:background 0.2s;"
|
|
||||||
[style.background]="step() > $index + 1 ? '#1f8d49' : '#e5e5e5'">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Étape 1 : Fichier(s) YAML -->
|
|
||||||
@if (step() === 1) {
|
|
||||||
<div class="fr-upload-group fr-mb-4w">
|
|
||||||
<label class="fr-label" for="file-input">
|
|
||||||
Fichier(s) YAML
|
|
||||||
<span class="fr-hint-text">
|
|
||||||
Formats acceptés : .yaml, .yml — Sélectionnez plusieurs fichiers si votre spec utilise des $ref
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="file-input"
|
|
||||||
class="fr-upload"
|
|
||||||
type="file"
|
|
||||||
accept=".yaml,.yml"
|
|
||||||
multiple
|
|
||||||
(change)="onFileChange($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (fileError()) {
|
|
||||||
<div class="fr-alert fr-alert--error fr-mb-3w">
|
|
||||||
<p>{{ fileError() }}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (selectedFiles().length > 0) {
|
|
||||||
<div class="fr-mb-3w">
|
|
||||||
@for (file of selectedFiles(); track file.name) {
|
|
||||||
<div class="fr-callout fr-callout--blue-cumulus fr-mb-1w" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;">
|
|
||||||
<span>
|
|
||||||
<strong>{{ file.name }}</strong>
|
|
||||||
({{ (file.size / 1024).toFixed(1) }} Ko)
|
|
||||||
</span>
|
|
||||||
@if (file.name === detectedMainFilename()) {
|
|
||||||
<span class="fr-badge fr-badge--success fr-badge--sm">Principal</span>
|
|
||||||
} @else if (selectedFiles().length > 1) {
|
|
||||||
<span class="fr-badge fr-badge--blue-cumulus fr-badge--sm">Secondaire</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (validating()) {
|
|
||||||
<div class="fr-callout fr-mb-3w">
|
|
||||||
<p>Validation en cours...</p>
|
|
||||||
</div>
|
|
||||||
} @else if (validationDone()) {
|
|
||||||
@if (validationErrors().length > 0) {
|
|
||||||
<div class="fr-alert fr-alert--error fr-mb-3w">
|
|
||||||
<p class="fr-alert__title">Fichier(s) invalide(s)</p>
|
|
||||||
@for (err of validationErrors(); track err.file) {
|
|
||||||
<p>{{ err.file ? err.file + ' : ' : '' }}{{ err.message }}</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div class="fr-alert fr-alert--success fr-mb-3w">
|
|
||||||
<p class="fr-alert__title">Fichier(s) valide(s)</p>
|
|
||||||
<p>Le fichier a été validé avec succès.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
|
||||||
<button
|
|
||||||
class="fr-btn"
|
|
||||||
(click)="goToStep2()"
|
|
||||||
[disabled]="!canProceedFromStep1()"
|
|
||||||
>
|
|
||||||
Suivant
|
|
||||||
</button>
|
|
||||||
<a routerLink="/catalog" class="fr-btn fr-btn--secondary">Annuler</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Étape 2 : Informations -->
|
|
||||||
@if (step() === 2) {
|
|
||||||
<!-- Bannière nouvelle version -->
|
|
||||||
@if (fromApi(); as origin) {
|
|
||||||
<div class="fr-callout fr-callout--blue-cumulus fr-mb-3w">
|
|
||||||
<p class="fr-callout__text">
|
|
||||||
Nouvelle version de : <strong>{{ origin.title }}</strong> (v{{ origin.version }})
|
|
||||||
— le nom et la convention sont verrouillés.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<form [formGroup]="metaForm" (ngSubmit)="goToStep3()" novalidate>
|
|
||||||
|
|
||||||
<!-- Titre : 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" [class.fr-select-group--error]="conventionInvalid()">
|
|
||||||
<label class="fr-label" for="convention-input">Convention</label>
|
|
||||||
<select
|
|
||||||
id="convention-input"
|
|
||||||
class="fr-select"
|
|
||||||
[class.fr-select--error]="conventionInvalid()"
|
|
||||||
formControlName="convention"
|
|
||||||
>
|
|
||||||
<option value="CONSULTER">Consulter</option>
|
|
||||||
<option value="ENREGISTRER">Enregistrer</option>
|
|
||||||
<option value="ETRE_NOTIFIE">Être notifié</option>
|
|
||||||
</select>
|
|
||||||
@if (conventionInvalid()) {
|
|
||||||
<p class="fr-error-text">La convention est obligatoire.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-8">
|
|
||||||
<div class="fr-input-group" [class.fr-input-group--error]="nameInvalid()">
|
|
||||||
<label class="fr-label" for="name-input">Nom de l'API</label>
|
|
||||||
<input
|
|
||||||
id="name-input"
|
|
||||||
class="fr-input"
|
|
||||||
[class.fr-input--error]="nameInvalid()"
|
|
||||||
formControlName="name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Référentiel Adresses"
|
|
||||||
/>
|
|
||||||
@if (nameInvalid()) {
|
|
||||||
<p class="fr-error-text">Le nom est obligatoire.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="fr-hint-text fr-mb-3w">Titre complet : <strong>{{ previewTitle() }}</strong></p>
|
|
||||||
|
|
||||||
<!-- Catégorie -->
|
|
||||||
<div class="fr-select-group fr-mb-3w">
|
|
||||||
<label class="fr-label" for="category-input">
|
|
||||||
Catégorie
|
|
||||||
</label>
|
|
||||||
<select id="category-input" class="fr-select" formControlName="categoryId">
|
|
||||||
<option value="">— Sans catégorie —</option>
|
|
||||||
@for (cat of categories(); track cat.id) {
|
|
||||||
<option [value]="cat.id">{{ categoryLabel(cat) }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fournisseur -->
|
|
||||||
<div class="fr-input-group fr-mb-3w" [class.fr-input-group--error]="providerInvalid()">
|
|
||||||
<label class="fr-label" for="provider-input">
|
|
||||||
Fournisseur <span style="color:var(--text-default-error)">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="provider-input"
|
|
||||||
class="fr-input"
|
|
||||||
[class.fr-input--error]="providerInvalid()"
|
|
||||||
formControlName="provider"
|
|
||||||
type="text"
|
|
||||||
placeholder="DINUM, Direction XYZ..."
|
|
||||||
/>
|
|
||||||
@if (providerInvalid()) {
|
|
||||||
<p class="fr-error-text">Le fournisseur est obligatoire.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Type -->
|
|
||||||
<div class="fr-select-group fr-mb-3w">
|
|
||||||
<label class="fr-label" for="type-input">
|
|
||||||
Type <span style="color:var(--text-default-error)">*</span>
|
|
||||||
</label>
|
|
||||||
<select id="type-input" class="fr-select" formControlName="type">
|
|
||||||
<option value="ASYNCAPI">AsyncAPI</option>
|
|
||||||
<option value="OPENAPI">OpenAPI</option>
|
|
||||||
</select>
|
|
||||||
@if (detectedType()) {
|
|
||||||
<p class="fr-hint-text">Détecté automatiquement depuis le fichier YAML</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div class="fr-input-group fr-mb-3w">
|
|
||||||
<label class="fr-label" for="description-input">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="description-input"
|
|
||||||
class="fr-input"
|
|
||||||
formControlName="description"
|
|
||||||
rows="3"
|
|
||||||
placeholder="Description de l'API..."
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Version X.Y.Z -->
|
|
||||||
<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-1w">
|
|
||||||
<div class="fr-col-4">
|
|
||||||
<div class="fr-input-group" [class.fr-input-group--error]="versionMajorInvalid()">
|
|
||||||
<label class="fr-label" for="version-major-input">Majeure (X)</label>
|
|
||||||
<input
|
|
||||||
id="version-major-input"
|
|
||||||
class="fr-input"
|
|
||||||
[class.fr-input--error]="versionMajorInvalid()"
|
|
||||||
formControlName="versionMajor"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
placeholder="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-4">
|
|
||||||
<div class="fr-input-group" [class.fr-input-group--error]="versionMinorInvalid()">
|
|
||||||
<label class="fr-label" for="version-minor-input">Mineure (Y)</label>
|
|
||||||
<input
|
|
||||||
id="version-minor-input"
|
|
||||||
class="fr-input"
|
|
||||||
[class.fr-input--error]="versionMinorInvalid()"
|
|
||||||
formControlName="versionMinor"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-4">
|
|
||||||
<div class="fr-input-group" [class.fr-input-group--error]="versionPatchInvalid()">
|
|
||||||
<label class="fr-label" for="version-patch-input">Correctif (Z)</label>
|
|
||||||
<input
|
|
||||||
id="version-patch-input"
|
|
||||||
class="fr-input"
|
|
||||||
[class.fr-input--error]="versionPatchInvalid()"
|
|
||||||
formControlName="versionPatch"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (versionAlreadyExists()) {
|
|
||||||
<div class="fr-alert fr-alert--warning fr-mb-3w">
|
|
||||||
<p class="fr-alert__title">Version déjà existante</p>
|
|
||||||
<p>
|
|
||||||
La version {{ formValues().versionMajor }}.{{ formValues().versionMinor }}.{{ formValues().versionPatch }}
|
|
||||||
existe déjà pour cette API. Choisissez un numéro de version différent.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
|
||||||
<button type="submit" class="fr-btn" [disabled]="versionAlreadyExists()">Suivant</button>
|
|
||||||
<button type="button" class="fr-btn fr-btn--secondary" (click)="step.set(1)">
|
|
||||||
Retour
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Étape 3 : Contacts -->
|
|
||||||
@if (step() === 3) {
|
|
||||||
<form [formGroup]="metaForm" (ngSubmit)="goToStep4()" novalidate>
|
|
||||||
|
|
||||||
<!-- 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]="fieldInvalid('contactFunctionalName')">
|
|
||||||
<label class="fr-label" for="cf-name-input">Nom et prénom</label>
|
|
||||||
<input id="cf-name-input" class="fr-input"
|
|
||||||
[class.fr-input--error]="fieldInvalid('contactFunctionalName')"
|
|
||||||
formControlName="contactFunctionalName" type="text" placeholder="Prénom Nom" />
|
|
||||||
@if (fieldInvalid('contactFunctionalName')) {
|
|
||||||
<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]="fieldInvalid('contactFunctionalEntity')">
|
|
||||||
<label class="fr-label" for="cf-entity-input">Entité</label>
|
|
||||||
<input id="cf-entity-input" class="fr-input"
|
|
||||||
[class.fr-input--error]="fieldInvalid('contactFunctionalEntity')"
|
|
||||||
formControlName="contactFunctionalEntity" type="text" placeholder="Direction XYZ" />
|
|
||||||
@if (fieldInvalid('contactFunctionalEntity')) {
|
|
||||||
<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]="fieldInvalid('contactFunctionalEmail')">
|
|
||||||
<label class="fr-label" for="cf-email-input">Email</label>
|
|
||||||
<input id="cf-email-input" class="fr-input"
|
|
||||||
[class.fr-input--error]="fieldInvalid('contactFunctionalEmail')"
|
|
||||||
formControlName="contactFunctionalEmail" type="email" placeholder="prenom.nom@example.fr" />
|
|
||||||
@if (fieldInvalid('contactFunctionalEmail')) {
|
|
||||||
<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-4w">
|
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
|
||||||
<div class="fr-input-group" [class.fr-input-group--error]="fieldInvalid('contactTechnicalName')">
|
|
||||||
<label class="fr-label" for="ct-name-input">Nom et prénom</label>
|
|
||||||
<input id="ct-name-input" class="fr-input"
|
|
||||||
[class.fr-input--error]="fieldInvalid('contactTechnicalName')"
|
|
||||||
formControlName="contactTechnicalName" type="text" placeholder="Prénom Nom" />
|
|
||||||
@if (fieldInvalid('contactTechnicalName')) {
|
|
||||||
<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]="fieldInvalid('contactTechnicalEntity')">
|
|
||||||
<label class="fr-label" for="ct-entity-input">Entité</label>
|
|
||||||
<input id="ct-entity-input" class="fr-input"
|
|
||||||
[class.fr-input--error]="fieldInvalid('contactTechnicalEntity')"
|
|
||||||
formControlName="contactTechnicalEntity" type="text" placeholder="Équipe Technique" />
|
|
||||||
@if (fieldInvalid('contactTechnicalEntity')) {
|
|
||||||
<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]="fieldInvalid('contactTechnicalEmail')">
|
|
||||||
<label class="fr-label" for="ct-email-input">Email</label>
|
|
||||||
<input id="ct-email-input" class="fr-input"
|
|
||||||
[class.fr-input--error]="fieldInvalid('contactTechnicalEmail')"
|
|
||||||
formControlName="contactTechnicalEmail" type="email" placeholder="tech@example.fr" />
|
|
||||||
@if (fieldInvalid('contactTechnicalEmail')) {
|
|
||||||
<p class="fr-error-text">Email valide requis.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
|
||||||
<button type="submit" class="fr-btn">Suivant</button>
|
|
||||||
<button type="button" class="fr-btn fr-btn--secondary" (click)="step.set(2)">
|
|
||||||
Retour
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Étape 4 : Confirmation -->
|
|
||||||
@if (step() === 4) {
|
|
||||||
<div class="fr-card fr-mb-4w">
|
|
||||||
<div class="fr-card__body">
|
|
||||||
<div class="fr-card__content">
|
|
||||||
<h2 class="fr-h5 fr-mb-3w">Récapitulatif</h2>
|
|
||||||
<dl class="fr-grid-row fr-grid-row--gutters">
|
|
||||||
<div class="fr-col-12 fr-col-md-6">
|
|
||||||
<dt class="fr-text--bold">Fichier{{ selectedFiles().length > 1 ? 's' : '' }}</dt>
|
|
||||||
<dd>
|
|
||||||
@if (selectedFiles().length === 0) {
|
|
||||||
<span class="fr-badge fr-badge--warning fr-badge--sm">Aucun fichier</span>
|
|
||||||
} @else if (selectedFiles().length === 1) {
|
|
||||||
{{ selectedFiles()[0].name }}
|
|
||||||
} @else {
|
|
||||||
@for (f of selectedFiles(); track f.name) {
|
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
||||||
{{ f.name }}
|
|
||||||
@if (f.name === detectedMainFilename()) {
|
|
||||||
<span class="fr-badge fr-badge--success fr-badge--sm">Principal</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-6">
|
|
||||||
<dt class="fr-text--bold">Type</dt>
|
|
||||||
<dd>{{ displayType() }}</dd>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12">
|
|
||||||
<dt class="fr-text--bold">Titre</dt>
|
|
||||||
<dd>{{ previewTitle() }}</dd>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-6">
|
|
||||||
<dt class="fr-text--bold">Version</dt>
|
|
||||||
<dd>{{ metaForm.value.versionMajor }}.{{ metaForm.value.versionMinor }}.{{ metaForm.value.versionPatch }}</dd>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-6">
|
|
||||||
<dt class="fr-text--bold">Fournisseur</dt>
|
|
||||||
<dd>{{ metaForm.value.provider }}</dd>
|
|
||||||
</div>
|
|
||||||
@if (metaForm.value.categoryId) {
|
|
||||||
<div class="fr-col-12 fr-col-md-6">
|
|
||||||
<dt class="fr-text--bold">Catégorie</dt>
|
|
||||||
<dd>{{ selectedCategoryName() }}</dd>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (metaForm.value.description) {
|
|
||||||
<div class="fr-col-12">
|
|
||||||
<dt class="fr-text--bold">Description</dt>
|
|
||||||
<dd>{{ metaForm.value.description }}</dd>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="fr-col-12 fr-col-md-6">
|
|
||||||
<dt class="fr-text--bold">Contact métier</dt>
|
|
||||||
<dd>
|
|
||||||
{{ metaForm.value.contactFunctionalName }}<br>
|
|
||||||
<span class="fr-text--sm">{{ metaForm.value.contactFunctionalEntity }}</span><br>
|
|
||||||
<span class="fr-text--sm">{{ metaForm.value.contactFunctionalEmail }}</span>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="fr-col-12 fr-col-md-6">
|
|
||||||
<dt class="fr-text--bold">Contact technique</dt>
|
|
||||||
<dd>
|
|
||||||
{{ metaForm.value.contactTechnicalName }}<br>
|
|
||||||
<span class="fr-text--sm">{{ metaForm.value.contactTechnicalEntity }}</span><br>
|
|
||||||
<span class="fr-text--sm">{{ metaForm.value.contactTechnicalEmail }}</span>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (uploadError()) {
|
|
||||||
<div class="fr-alert fr-alert--error fr-mb-3w">
|
|
||||||
<p class="fr-alert__title">Erreur lors de l'import</p>
|
|
||||||
<p>{{ uploadError() }}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
|
||||||
<button
|
|
||||||
class="fr-btn"
|
|
||||||
(click)="onSubmit()"
|
|
||||||
[disabled]="uploading()"
|
|
||||||
>
|
|
||||||
{{ uploading() ? 'Import en cours...' : 'Importer' }}
|
|
||||||
</button>
|
|
||||||
<button class="fr-btn fr-btn--secondary" (click)="step.set(3)" [disabled]="uploading()">
|
|
||||||
Retour
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
})
|
})
|
||||||
export class UploadComponent implements OnInit {
|
export class UploadComponent implements OnInit {
|
||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
|
|||||||
Reference in New Issue
Block a user