Refonte qualité (shared / back / front) #1

Open
z3n wants to merge 14 commits from chore/quality-refonte into main
10 changed files with 93 additions and 31 deletions
Showing only changes of commit 34a83233f2 - Show all commits

View File

@@ -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,

View File

@@ -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"
} }

View File

@@ -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])),
], ],
}; };

View File

@@ -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,30 +60,23 @@ 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> {
valid: boolean; return this.http.post<ValidateResponse>(`${this.uploadsUrl}/validate`, formData);
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);
} }
} }
/** 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, 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> = {};

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 */ /* 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();

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',
};