diff --git a/front-public/angular.json b/front-public/angular.json index be0490b..872a466 100644 --- a/front-public/angular.json +++ b/front-public/angular.json @@ -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, diff --git a/front-public/package.json b/front-public/package.json index ff9aa45..1ff8371 100644 --- a/front-public/package.json +++ b/front-public/package.json @@ -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" } diff --git a/front-public/src/app/app.config.ts b/front-public/src/app/app.config.ts index 1c7fc6a..a8940e2 100644 --- a/front-public/src/app/app.config.ts +++ b/front-public/src/app/app.config.ts @@ -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])), ], }; diff --git a/front-public/src/app/core/api.service.ts b/front-public/src/app/core/api.service.ts index 79a9378..21b0410 100644 --- a/front-public/src/app/core/api.service.ts +++ b/front-public/src/app/core/api.service.ts @@ -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 { const params: Record = {}; @@ -58,30 +60,23 @@ export class ApiService { } upload(formData: FormData): Observable { - return this.http.post('/api/uploads', formData); + return this.http.post(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 { + return this.http.post(`${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 }>; +} diff --git a/front-public/src/app/core/category.service.ts b/front-public/src/app/core/category.service.ts index e375e79..a032d49 100644 --- a/front-public/src/app/core/category.service.ts +++ b/front-public/src/app/core/category.service.ts @@ -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 { const params: Record = {}; diff --git a/front-public/src/app/core/error.interceptor.ts b/front-public/src/app/core/error.interceptor.ts new file mode 100644 index 0000000..a31aa57 --- /dev/null +++ b/front-public/src/app/core/error.interceptor.ts @@ -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); + }), + ); +}; diff --git a/front-public/src/app/core/error.service.ts b/front-public/src/app/core/error.service.ts new file mode 100644 index 0000000..3ec3055 --- /dev/null +++ b/front-public/src/app/core/error.service.ts @@ -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)}`); + } +} diff --git a/front-public/src/app/pages/upload/upload.component.ts b/front-public/src/app/pages/upload/upload.component.ts index 669126d..8e3ca54 100644 --- a/front-public/src/app/pages/upload/upload.component.ts +++ b/front-public/src/app/pages/upload/upload.component.ts @@ -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(); diff --git a/front-public/src/environments/environment.prod.ts b/front-public/src/environments/environment.prod.ts new file mode 100644 index 0000000..9934449 --- /dev/null +++ b/front-public/src/environments/environment.prod.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiBaseUrl: '/api', +}; diff --git a/front-public/src/environments/environment.ts b/front-public/src/environments/environment.ts new file mode 100644 index 0000000..54f6495 --- /dev/null +++ b/front-public/src/environments/environment.ts @@ -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', +};