Refonte qualité (shared / back / front) #1
@@ -57,7 +57,13 @@
|
|||||||
{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
|
{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
|
||||||
{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
|
{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all",
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@datacat/shared": "workspace:*",
|
"@datacat/shared": "workspace:*",
|
||||||
|
"@gouvfr/dsfr": "^1.13.0",
|
||||||
"@angular/animations": "^19.0.0",
|
"@angular/animations": "^19.0.0",
|
||||||
"@angular/common": "^19.0.0",
|
"@angular/common": "^19.0.0",
|
||||||
"@angular/compiler": "^19.0.0",
|
"@angular/compiler": "^19.0.0",
|
||||||
@@ -28,7 +29,6 @@
|
|||||||
"@angular/cli": "^19.0.0",
|
"@angular/cli": "^19.0.0",
|
||||||
"@angular/compiler-cli": "^19.0.0",
|
"@angular/compiler-cli": "^19.0.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@gouvfr/dsfr": "^1.13.0",
|
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"sass": "^1.83.0"
|
"sass": "^1.83.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
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 { routes } from './app.routes';
|
||||||
|
import { errorInterceptor } from './core/error.interceptor';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes, withComponentInputBinding()),
|
provideRouter(routes, withComponentInputBinding()),
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptors([errorInterceptor])),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ApiService {
|
export class ApiService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private baseUrl = '/api/apis';
|
private baseUrl = `${environment.apiBaseUrl}/apis`;
|
||||||
|
private uploadsUrl = `${environment.apiBaseUrl}/uploads`;
|
||||||
|
|
||||||
list(query?: ApiListQuery): Observable<ApiListResponse> {
|
list(query?: ApiListQuery): Observable<ApiListResponse> {
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
@@ -58,12 +60,18 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upload(formData: FormData): Observable<ApiEntry> {
|
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<{
|
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;
|
valid: boolean;
|
||||||
type: 'ASYNCAPI' | 'OPENAPI' | null;
|
type: ApiType | null;
|
||||||
mainFile: string | null;
|
mainFile: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
versionMajor: number | null;
|
versionMajor: number | null;
|
||||||
@@ -71,17 +79,4 @@ export class ApiService {
|
|||||||
versionPatch: number | null;
|
versionPatch: number | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
errors: Array<{ file: string; message: string }>;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import {
|
|||||||
CategoryListResponse,
|
CategoryListResponse,
|
||||||
CategoryBrowseResponse,
|
CategoryBrowseResponse,
|
||||||
} from '@datacat/shared';
|
} from '@datacat/shared';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CategoryService {
|
export class CategoryService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private baseUrl = '/api/categories';
|
private baseUrl = `${environment.apiBaseUrl}/categories`;
|
||||||
|
|
||||||
list(search?: string): Observable<CategoryListResponse> {
|
list(search?: string): Observable<CategoryListResponse> {
|
||||||
const params: Record<string, string> = {};
|
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 */
|
/* 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(() => {
|
previewTitle = computed(() => {
|
||||||
const values = this.formValues();
|
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