front: config env, error interceptor fonctionnel, DSFR en deps, fix build

- src/environments/{environment,environment.prod}.ts + fileReplacements :
  baseUrl API configurable au lieu de '/api' hardcodé dans les services
- ErrorService + errorInterceptor (HttpInterceptorFn) ; app.config passe à
  withInterceptors([...]) (remplace withInterceptorsFromDi legacy)
- @gouvfr/dsfr déplacé en dependencies (chargé au runtime)
- api.service: extrait ValidateResponse (dédup du type inline)
- fix: formValues passe en protected (utilisé dans le template -> build NG1
  cassé, le conteneur servait un ancien bundle)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
z3n
2026-06-26 12:43:06 +00:00
parent f7f706e1e1
commit 34a83233f2
10 changed files with 93 additions and 31 deletions

View File

@@ -57,7 +57,13 @@
{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
],
"outputHashing": "all"
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"development": {
"optimization": false,

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@datacat/shared": "workspace:*",
"@gouvfr/dsfr": "^1.13.0",
"@angular/animations": "^19.0.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
@@ -28,7 +29,6 @@
"@angular/cli": "^19.0.0",
"@angular/compiler-cli": "^19.0.0",
"@types/node": "^22.0.0",
"@gouvfr/dsfr": "^1.13.0",
"typescript": "~5.8.3",
"sass": "^1.83.0"
}

View File

@@ -1,12 +1,13 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { errorInterceptor } from './core/error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptorsFromDi()),
provideHttpClient(withInterceptors([errorInterceptor])),
],
};

View File

@@ -1,12 +1,14 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ApiEntry, ApiEntryListItem, ApiListResponse, ApiListQuery } from '@datacat/shared';
import { ApiEntry, ApiEntryListItem, ApiListResponse, ApiListQuery, ApiType } from '@datacat/shared';
import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
private baseUrl = '/api/apis';
private baseUrl = `${environment.apiBaseUrl}/apis`;
private uploadsUrl = `${environment.apiBaseUrl}/uploads`;
list(query?: ApiListQuery): Observable<ApiListResponse> {
const params: Record<string, string> = {};
@@ -58,30 +60,23 @@ export class ApiService {
}
upload(formData: FormData): Observable<ApiEntry> {
return this.http.post<ApiEntry>('/api/uploads', formData);
return this.http.post<ApiEntry>(this.uploadsUrl, formData);
}
validate(formData: FormData): Observable<{
valid: boolean;
type: 'ASYNCAPI' | 'OPENAPI' | null;
mainFile: string | null;
name: string | null;
versionMajor: number | null;
versionMinor: number | null;
versionPatch: number | null;
description: string | null;
errors: Array<{ file: string; message: string }>;
}> {
return this.http.post<{
valid: boolean;
type: 'ASYNCAPI' | 'OPENAPI' | null;
mainFile: string | null;
name: string | null;
versionMajor: number | null;
versionMinor: number | null;
versionPatch: number | null;
description: string | null;
errors: Array<{ file: string; message: string }>;
}>('/api/uploads/validate', formData);
validate(formData: FormData): Observable<ValidateResponse> {
return this.http.post<ValidateResponse>(`${this.uploadsUrl}/validate`, formData);
}
}
/** Réponse de l'endpoint de validation d'upload (POST /uploads/validate). */
export interface ValidateResponse {
valid: boolean;
type: ApiType | null;
mainFile: string | null;
name: string | null;
versionMajor: number | null;
versionMinor: number | null;
versionPatch: number | null;
description: string | null;
errors: Array<{ file: string; message: string }>;
}

View File

@@ -6,11 +6,12 @@ import {
CategoryListResponse,
CategoryBrowseResponse,
} from '@datacat/shared';
import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' })
export class CategoryService {
private http = inject(HttpClient);
private baseUrl = '/api/categories';
private baseUrl = `${environment.apiBaseUrl}/categories`;
list(search?: string): Observable<CategoryListResponse> {
const params: Record<string, string> = {};

View File

@@ -0,0 +1,18 @@
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { ErrorService } from './error.service';
import { catchError, throwError } from 'rxjs';
/**
* Interceptor HTTP fonctionnel : journalise et normalise les erreurs réseau en
* un message lisible (via ErrorService), puis relaie l'erreur aux appelants.
*/
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const errorService = inject(ErrorService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
errorService.log(req.method, req.url, error);
return throwError(() => error);
}),
);
};

View File

@@ -0,0 +1,32 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
/**
* Centralise la traduction d'une erreur HTTP en message lisible et son
* journal. Les composants appellent `toMessage(err)` plutôt que de
* réimplémenter chacun leur extraction de message.
*/
@Injectable({ providedIn: 'root' })
export class ErrorService {
/** Extrait un message lisible depuis une erreur HTTP (ou autre). */
toMessage(error: unknown, fallback = 'Une erreur est survenue.'): string {
if (error instanceof HttpErrorResponse) {
const body = error.error;
if (typeof body === 'string' && body.trim()) return body;
if (body && typeof body === 'object') {
const message = (body as { message?: unknown }).message;
if (typeof message === 'string' && message) return message;
if (Array.isArray(message) && message.length) return message.join(', ');
}
if (error.status === 0) return 'Serveur injoignable. Vérifiez votre connexion.';
return error.message || fallback;
}
if (error instanceof Error && error.message) return error.message;
return fallback;
}
/** Journalise une erreur réseau (appelé par l'interceptor). */
log(method: string, url: string, error: HttpErrorResponse): void {
console.error(`[HTTP ${error.status}] ${method} ${url}${this.toMessage(error)}`);
}
}

View File

@@ -560,7 +560,7 @@ export class UploadComponent implements OnInit {
});
/* Convertit valueChanges en signal pour que les computed() trackent les changements du formulaire */
private readonly formValues = toSignal(this.metaForm.valueChanges, { initialValue: this.metaForm.value });
protected readonly formValues = toSignal(this.metaForm.valueChanges, { initialValue: this.metaForm.value });
previewTitle = computed(() => {
const values = this.formValues();

View File

@@ -0,0 +1,4 @@
export const environment = {
production: true,
apiBaseUrl: '/api',
};

View File

@@ -0,0 +1,5 @@
export const environment = {
production: false,
// Préfixe des appels API (proxifié vers le back en dev, cf. proxy.conf*.json).
apiBaseUrl: '/api',
};