feat(status): ajouter statut NO_YAML pour les entrées sans fichier YAML

- shared: ajoute 'NO_YAML' à ApiStatus
- back/apis: status initial dérivé de yamlContent (PENDING si YAML, NO_YAML sinon)
- back/uploads: supprime la contrainte "fichier requis", gère le cas sans fichier
- front/api-detail: badgeClass/statusLabel gèrent NO_YAML (badge warning "Sans fichier")
- front/catalog: badgeClass ajoute le case NO_YAML → badge warning
- front/upload: fichier optionnel (canProceedFromStep1 retourne true si 0 fichier)

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:
z3n
2026-06-24 08:23:03 +00:00
parent f326efb7b0
commit 3836db1e1f
6 changed files with 34 additions and 10 deletions

View File

@@ -41,7 +41,7 @@ export class ApisService {
description: dto.description ?? null, description: dto.description ?? null,
type: dto.type, type: dto.type,
yamlContent: dto.yamlContent, yamlContent: dto.yamlContent,
status: 'PENDING', status: dto.yamlContent ? 'PENDING' : 'NO_YAML',
categoryId: dto.categoryId ?? null, categoryId: dto.categoryId ?? null,
isCurrent, isCurrent,
contactFunctionalName: dto.contactFunctionalName, contactFunctionalName: dto.contactFunctionalName,

View File

@@ -205,7 +205,6 @@ export class UploadsController {
contactTechnicalEmail: string; contactTechnicalEmail: string;
}, },
) { ) {
if (!files || files.length === 0) throw new BadRequestException('YAML file is required');
if (!body.name) throw new BadRequestException('name is required'); if (!body.name) throw new BadRequestException('name is required');
if (!body.convention) throw new BadRequestException('convention is required'); if (!body.convention) throw new BadRequestException('convention is required');
@@ -218,7 +217,10 @@ export class UploadsController {
let yamlContent: string; let yamlContent: string;
let detectedType: ApiType | null = null; let detectedType: ApiType | null = null;
if (files.length === 1) { if (!files || files.length === 0) {
/* Pas de fichier : entrée créée sans YAML (YAML pourra être ajouté plus tard) */
yamlContent = '';
} else if (files.length === 1) {
/* Cas simple : un seul fichier */ /* Cas simple : un seul fichier */
yamlContent = fs.readFileSync(files[0].path, 'utf-8'); yamlContent = fs.readFileSync(files[0].path, 'utf-8');
@@ -260,8 +262,10 @@ export class UploadsController {
contactTechnicalEmail: body.contactTechnicalEmail, contactTechnicalEmail: body.contactTechnicalEmail,
}); });
/* Fire-and-forget : génération asynchrone */ /* Fire-and-forget : génération asynchrone (seulement si YAML fourni) */
if (yamlContent) {
this.docsService.generate(entry.id).catch(console.error); this.docsService.generate(entry.id).catch(console.error);
}
return entry; return entry;
} }

View File

@@ -596,8 +596,17 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
onDelete() { onDelete() {
if (!confirm('Supprimer cette API ?')) return; if (!confirm('Supprimer cette API ?')) return;
this.deleting.set(true); this.deleting.set(true);
/* Calculer la cible de redirection avant la suppression */
const otherVersions = this.versions().filter((v) => v.id !== this.id);
const redirectTarget = otherVersions.find((v) => v.isCurrent) ?? otherVersions[0];
this.apiService.delete(this.id).subscribe({ this.apiService.delete(this.id).subscribe({
next: () => this.router.navigate(['/catalog']), next: () => {
if (redirectTarget) {
this.router.navigate(['/catalog', redirectTarget.id]);
} else {
this.router.navigate(this.backUrl());
}
},
error: () => this.deleting.set(false), error: () => this.deleting.set(false),
}); });
} }
@@ -605,12 +614,14 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
badgeClass(status: string): string { badgeClass(status: string): string {
if (status === 'GENERATED') return 'fr-badge fr-badge--success'; if (status === 'GENERATED') return 'fr-badge fr-badge--success';
if (status === 'ERROR') return 'fr-badge fr-badge--error'; if (status === 'ERROR') return 'fr-badge fr-badge--error';
if (status === 'NO_YAML') return 'fr-badge fr-badge--warning';
return 'fr-badge fr-badge--info'; return 'fr-badge fr-badge--info';
} }
statusLabel(status: string): string { statusLabel(status: string): string {
if (status === 'GENERATED') return 'Générée'; if (status === 'GENERATED') return 'Générée';
if (status === 'ERROR') return 'Erreur'; if (status === 'ERROR') return 'Erreur';
if (status === 'NO_YAML') return 'Sans fichier';
return 'En attente'; return 'En attente';
} }

View File

@@ -208,6 +208,7 @@ export class CatalogComponent implements OnInit {
const base = 'fr-badge'; const base = 'fr-badge';
if (status === 'GENERATED') return `${base} fr-badge--success`; if (status === 'GENERATED') return `${base} fr-badge--success`;
if (status === 'ERROR') return `${base} fr-badge--error`; if (status === 'ERROR') return `${base} fr-badge--error`;
if (status === 'NO_YAML') return `${base} fr-badge--warning`;
return `${base} fr-badge--info`; return `${base} fr-badge--info`;
} }

View File

@@ -393,7 +393,9 @@ import { ApiEntry, Category, API_CONVENTION_LABELS, ApiConvention } from '@datac
<div class="fr-col-12 fr-col-md-6"> <div class="fr-col-12 fr-col-md-6">
<dt class="fr-text--bold">Fichier{{ selectedFiles().length > 1 ? 's' : '' }}</dt> <dt class="fr-text--bold">Fichier{{ selectedFiles().length > 1 ? 's' : '' }}</dt>
<dd> <dd>
@if (selectedFiles().length === 1) { @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 }} {{ selectedFiles()[0].name }}
} @else { } @else {
@for (f of selectedFiles(); track f.name) { @for (f of selectedFiles(); track f.name) {
@@ -568,7 +570,7 @@ export class UploadComponent implements OnInit {
return !!(this.metaForm.get('versionPatch')?.invalid && this.metaForm.get('versionPatch')?.touched); return !!(this.metaForm.get('versionPatch')?.invalid && this.metaForm.get('versionPatch')?.touched);
} }
canProceedFromStep1 = computed(() => { canProceedFromStep1 = computed(() => {
if (this.selectedFiles().length === 0) return false; if (this.selectedFiles().length === 0) return true; // fichier optionnel
if (this.validating()) return false; if (this.validating()) return false;
return this.validationDone() && this.validationErrors().length === 0; return this.validationDone() && this.validationErrors().length === 0;
}); });
@@ -627,6 +629,7 @@ export class UploadComponent implements OnInit {
onFileChange(event: Event) { onFileChange(event: Event) {
this.fileError.set(null); this.fileError.set(null);
this.uploadError.set(null);
this.detectedType.set(null); this.detectedType.set(null);
this.detectedMainFilename.set(null); this.detectedMainFilename.set(null);
this.validationErrors.set([]); this.validationErrors.set([]);
@@ -725,7 +728,12 @@ export class UploadComponent implements OnInit {
onSubmit() { onSubmit() {
const files = this.selectedFiles(); const files = this.selectedFiles();
this.metaForm.markAllAsTouched(); this.metaForm.markAllAsTouched();
if (files.length === 0 || this.metaForm.invalid) return; if (this.metaForm.invalid) {
/* Naviguer vers l'étape avec les erreurs pour les rendre visibles */
const infoInvalid = this.INFO_FIELDS.some((f) => this.metaForm.get(f)?.invalid);
this.step.set(infoInvalid ? 2 : 3);
return;
}
this.uploading.set(true); this.uploading.set(true);
this.uploadError.set(null); this.uploadError.set(null);

View File

@@ -1,6 +1,6 @@
export type ApiType = 'ASYNCAPI' | 'OPENAPI'; export type ApiType = 'ASYNCAPI' | 'OPENAPI';
export type ApiStatus = 'PENDING' | 'GENERATED' | 'ERROR'; export type ApiStatus = 'PENDING' | 'GENERATED' | 'ERROR' | 'NO_YAML';
export type ApiConvention = 'CONSULTER' | 'ENREGISTRER' | 'ETRE_NOTIFIE'; export type ApiConvention = 'CONSULTER' | 'ENREGISTRER' | 'ETRE_NOTIFIE';