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:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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])),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
18
front-public/src/app/core/error.interceptor.ts
Normal file
18
front-public/src/app/core/error.interceptor.ts
Normal 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);
|
||||
}),
|
||||
);
|
||||
};
|
||||
32
front-public/src/app/core/error.service.ts
Normal file
32
front-public/src/app/core/error.service.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
4
front-public/src/environments/environment.prod.ts
Normal file
4
front-public/src/environments/environment.prod.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiBaseUrl: '/api',
|
||||
};
|
||||
5
front-public/src/environments/environment.ts
Normal file
5
front-public/src/environments/environment.ts
Normal 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',
|
||||
};
|
||||
Reference in New Issue
Block a user