feat(api-detail): isCurrent + setCurrent + màj doc et seeder
- Shared types : ajout isCurrent sur ApiEntry et ApiEntryListItem - Backend entity : colonne is_current (boolean, default true) - Backend service : isCurrent calculé à la création, setCurrent() marque une version comme courante (siblings → false) - Backend controller : POST /api/apis/:id/set-current - Frontend ApiService : méthode setCurrent() - Browse/upload : affichage badge "Courante" via isCurrent - Seeder : données de démo mises à jour - Documentation : api-reference, architecture, local-setup, CLAUDE.md 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:
@@ -32,17 +32,32 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
|
||||
<h1 class="fr-h2 fr-mb-4w">Importer une API</h1>
|
||||
|
||||
<!-- Stepper -->
|
||||
<div class="fr-stepper fr-mb-4w">
|
||||
<h2 class="fr-stepper__title">
|
||||
{{ stepTitles[step() - 1] }}
|
||||
<span class="fr-stepper__state">Étape {{ step() }} sur 3</span>
|
||||
</h2>
|
||||
<div
|
||||
class="fr-stepper__steps"
|
||||
[attr.data-fr-current-step]="step()"
|
||||
data-fr-steps="3"
|
||||
></div>
|
||||
<!-- 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 -->
|
||||
@@ -120,7 +135,7 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Étape 2 : Métadonnées -->
|
||||
<!-- Étape 2 : Informations -->
|
||||
@if (step() === 2) {
|
||||
<form [formGroup]="metaForm" (ngSubmit)="goToStep3()" novalidate>
|
||||
|
||||
@@ -270,6 +285,19 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
</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(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">
|
||||
@@ -347,16 +375,16 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
</div>
|
||||
|
||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
||||
<button type="submit" class="fr-btn" [disabled]="metaForm.invalid">Suivant</button>
|
||||
<button type="button" class="fr-btn fr-btn--secondary" (click)="step.set(1)">
|
||||
<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 3 : Confirmation -->
|
||||
@if (step() === 3) {
|
||||
<!-- Étape 4 : Confirmation -->
|
||||
@if (step() === 4) {
|
||||
<div class="fr-card fr-mb-4w">
|
||||
<div class="fr-card__body">
|
||||
<div class="fr-card__content">
|
||||
@@ -407,6 +435,22 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
<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>
|
||||
@@ -427,7 +471,7 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
|
||||
>
|
||||
{{ uploading() ? 'Import en cours...' : 'Importer' }}
|
||||
</button>
|
||||
<button class="fr-btn fr-btn--secondary" (click)="step.set(2)" [disabled]="uploading()">
|
||||
<button class="fr-btn fr-btn--secondary" (click)="step.set(3)" [disabled]="uploading()">
|
||||
Retour
|
||||
</button>
|
||||
</div>
|
||||
@@ -456,10 +500,21 @@ export class UploadComponent implements OnInit {
|
||||
|
||||
readonly stepTitles = [
|
||||
'Choisir le fichier',
|
||||
'Renseigner les métadonnées',
|
||||
'Informations',
|
||||
'Contacts',
|
||||
"Confirmer l'import",
|
||||
];
|
||||
|
||||
private readonly INFO_FIELDS = [
|
||||
'convention', 'name', 'provider', 'type',
|
||||
'versionMajor', 'versionMinor', 'versionPatch',
|
||||
];
|
||||
|
||||
private readonly CONTACT_FIELDS = [
|
||||
'contactFunctionalName', 'contactFunctionalEntity', 'contactFunctionalEmail',
|
||||
'contactTechnicalName', 'contactTechnicalEntity', 'contactTechnicalEmail',
|
||||
];
|
||||
|
||||
metaForm = this.fb.group({
|
||||
convention: ['CONSULTER', Validators.required],
|
||||
name: ['', Validators.required],
|
||||
@@ -609,6 +664,24 @@ export class UploadComponent implements OnInit {
|
||||
this.step.set(2);
|
||||
}
|
||||
|
||||
goToStep3() {
|
||||
const invalid = this.INFO_FIELDS.some((f) => this.metaForm.get(f)?.invalid);
|
||||
if (invalid) {
|
||||
this.INFO_FIELDS.forEach((f) => this.metaForm.get(f)?.markAsTouched());
|
||||
return;
|
||||
}
|
||||
this.step.set(3);
|
||||
}
|
||||
|
||||
goToStep4() {
|
||||
const invalid = this.CONTACT_FIELDS.some((f) => this.metaForm.get(f)?.invalid);
|
||||
if (invalid) {
|
||||
this.CONTACT_FIELDS.forEach((f) => this.metaForm.get(f)?.markAsTouched());
|
||||
return;
|
||||
}
|
||||
this.step.set(4);
|
||||
}
|
||||
|
||||
private runValidation(files: File[]): void {
|
||||
this.validating.set(true);
|
||||
this.validationDone.set(false);
|
||||
@@ -651,14 +724,6 @@ export class UploadComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
goToStep3() {
|
||||
if (this.metaForm.invalid) {
|
||||
this.metaForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
this.step.set(3);
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const files = this.selectedFiles();
|
||||
if (files.length === 0 || this.metaForm.invalid) return;
|
||||
|
||||
Reference in New Issue
Block a user