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

Open
z3n wants to merge 14 commits from chore/quality-refonte into main
6 changed files with 207 additions and 137 deletions
Showing only changes of commit c38adf3a7c - Show all commits

View File

@@ -3,14 +3,15 @@ import {
signal, signal,
computed, computed,
inject, inject,
DestroyRef,
OnInit, OnInit,
OnDestroy, OnDestroy,
ChangeDetectionStrategy, ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Subscription } from 'rxjs';
import { ApiService } from '../../core/api.service'; import { ApiService } from '../../core/api.service';
import { CategoryService } from '../../core/category.service'; import { CategoryService } from '../../core/category.service';
import { ApiEntry, ApiEntryListItem, Category } from '@datacat/shared'; import { ApiEntry, ApiEntryListItem, Category } from '@datacat/shared';
@@ -416,6 +417,7 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService); private apiService = inject(ApiService);
private categoryService = inject(CategoryService); private categoryService = inject(CategoryService);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
private destroyRef = inject(DestroyRef);
api = signal<ApiEntry | null>(null); api = signal<ApiEntry | null>(null);
versions = signal<ApiEntryListItem[]>([]); versions = signal<ApiEntryListItem[]>([]);
@@ -462,26 +464,29 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
}); });
private pollInterval: ReturnType<typeof setInterval> | null = null; private pollInterval: ReturnType<typeof setInterval> | null = null;
private routeSub: Subscription | null = null;
private id = ''; private id = '';
ngOnInit() { ngOnInit() {
this.categoryService.list().subscribe((res) => this.categories.set(res.items)); this.categoryService
this.routeSub = this.route.paramMap.subscribe((params) => { .list()
this.id = params.get('id') ?? ''; .pipe(takeUntilDestroyed(this.destroyRef))
this.api.set(null); .subscribe((res) => this.categories.set(res.items));
this.versions.set([]); this.route.paramMap
this.error.set(null); .pipe(takeUntilDestroyed(this.destroyRef))
this.categoryBreadcrumb.set([]); .subscribe((params) => {
this.editingInfo.set(false); this.id = params.get('id') ?? '';
this.editingContacts.set(false); this.api.set(null);
this.clearPoll(); this.versions.set([]);
this.loadApi(); this.error.set(null);
}); this.categoryBreadcrumb.set([]);
this.editingInfo.set(false);
this.editingContacts.set(false);
this.clearPoll();
this.loadApi();
});
} }
ngOnDestroy() { ngOnDestroy() {
this.routeSub?.unsubscribe();
this.clearPoll(); this.clearPoll();
} }
@@ -498,29 +503,38 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
onSetCurrent() { onSetCurrent() {
this.settingCurrent.set(true); this.settingCurrent.set(true);
this.apiService.setCurrent(this.id).subscribe({ this.apiService
next: (entry) => { .setCurrent(this.id)
this.api.set(entry); .pipe(takeUntilDestroyed(this.destroyRef))
this.settingCurrent.set(false); .subscribe({
this.apiService.getVersions(this.id).subscribe({ next: (entry) => {
next: (v) => this.versions.set(v), this.api.set(entry);
error: () => {}, this.settingCurrent.set(false);
}); this.apiService
}, .getVersions(this.id)
error: () => this.settingCurrent.set(false), .pipe(takeUntilDestroyed(this.destroyRef))
}); .subscribe({
next: (v) => this.versions.set(v),
error: () => {},
});
},
error: () => this.settingCurrent.set(false),
});
} }
onRegenerate() { onRegenerate() {
this.regenerating.set(true); this.regenerating.set(true);
this.apiService.regenerate(this.id).subscribe({ this.apiService
next: (entry) => { .regenerate(this.id)
this.api.set(entry); .pipe(takeUntilDestroyed(this.destroyRef))
this.regenerating.set(false); .subscribe({
this.startPollIfPending(); next: (entry) => {
}, this.api.set(entry);
error: () => this.regenerating.set(false), this.regenerating.set(false);
}); this.startPollIfPending();
},
error: () => this.regenerating.set(false),
});
} }
startEditInfo() { startEditInfo() {
@@ -568,10 +582,12 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
versionPatch: raw.versionPatch ?? 0, versionPatch: raw.versionPatch ?? 0,
categoryId: raw.categoryId || null, categoryId: raw.categoryId || null,
description: raw.description || null, description: raw.description || null,
}).subscribe({ })
next: (updated) => { this.api.set(updated); this.savingInfo.set(false); this.editingInfo.set(false); }, .pipe(takeUntilDestroyed(this.destroyRef))
error: () => this.savingInfo.set(false), .subscribe({
}); next: (updated) => { this.api.set(updated); this.savingInfo.set(false); this.editingInfo.set(false); },
error: () => this.savingInfo.set(false),
});
} }
submitContacts() { submitContacts() {
@@ -587,10 +603,12 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
contactTechnicalName: raw.contactTechnicalName ?? '', contactTechnicalName: raw.contactTechnicalName ?? '',
contactTechnicalEntity: raw.contactTechnicalEntity ?? '', contactTechnicalEntity: raw.contactTechnicalEntity ?? '',
contactTechnicalEmail: raw.contactTechnicalEmail ?? '', contactTechnicalEmail: raw.contactTechnicalEmail ?? '',
}).subscribe({ })
next: (updated) => { this.api.set(updated); this.savingContacts.set(false); this.editingContacts.set(false); }, .pipe(takeUntilDestroyed(this.destroyRef))
error: () => this.savingContacts.set(false), .subscribe({
}); next: (updated) => { this.api.set(updated); this.savingContacts.set(false); this.editingContacts.set(false); },
error: () => this.savingContacts.set(false),
});
} }
onDelete() { onDelete() {
@@ -599,16 +617,19 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
/* Calculer la cible de redirection avant la suppression */ /* Calculer la cible de redirection avant la suppression */
const otherVersions = this.versions().filter((v) => v.id !== this.id); const otherVersions = this.versions().filter((v) => v.id !== this.id);
const redirectTarget = otherVersions.find((v) => v.isCurrent) ?? otherVersions[0]; const redirectTarget = otherVersions.find((v) => v.isCurrent) ?? otherVersions[0];
this.apiService.delete(this.id).subscribe({ this.apiService
next: () => { .delete(this.id)
if (redirectTarget) { .pipe(takeUntilDestroyed(this.destroyRef))
this.router.navigate(['/catalog', redirectTarget.id]); .subscribe({
} else { next: () => {
this.router.navigate(this.backUrl()); if (redirectTarget) {
} this.router.navigate(['/catalog', redirectTarget.id]);
}, } else {
error: () => this.deleting.set(false), this.router.navigate(this.backUrl());
}); }
},
error: () => this.deleting.set(false),
});
} }
badgeClass(status: string): string { badgeClass(status: string): string {
@@ -627,27 +648,36 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
private loadApi() { private loadApi() {
this.loading.set(true); this.loading.set(true);
this.apiService.get(this.id).subscribe({ this.apiService
next: (entry) => { .get(this.id)
this.api.set(entry); .pipe(takeUntilDestroyed(this.destroyRef))
this.loading.set(false); .subscribe({
this.startPollIfPending(); next: (entry) => {
this.apiService.getVersions(this.id).subscribe({ this.api.set(entry);
next: (v) => this.versions.set(v), this.loading.set(false);
error: () => {}, this.startPollIfPending();
}); this.apiService
if (entry.categoryId) { .getVersions(this.id)
this.categoryService.browse(entry.categoryId).subscribe({ .pipe(takeUntilDestroyed(this.destroyRef))
next: (data) => this.categoryBreadcrumb.set(data.breadcrumb), .subscribe({
error: () => {}, next: (v) => this.versions.set(v),
}); error: () => {},
} });
}, if (entry.categoryId) {
error: (err) => { this.categoryService
this.error.set(err.message ?? 'Erreur lors du chargement'); .browse(entry.categoryId)
this.loading.set(false); .pipe(takeUntilDestroyed(this.destroyRef))
}, .subscribe({
}); next: (data) => this.categoryBreadcrumb.set(data.breadcrumb),
error: () => {},
});
}
},
error: (err) => {
this.error.set(err.message ?? 'Erreur lors du chargement');
this.loading.set(false);
},
});
} }
private startPollIfPending() { private startPollIfPending() {
@@ -658,12 +688,15 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
} }
private pollStatus() { private pollStatus() {
this.apiService.get(this.id).subscribe({ this.apiService
next: (entry) => { .get(this.id)
this.api.set(entry); .pipe(takeUntilDestroyed(this.destroyRef))
if (entry.status !== 'PENDING') this.clearPoll(); .subscribe({
}, next: (entry) => {
}); this.api.set(entry);
if (entry.status !== 'PENDING') this.clearPoll();
},
});
} }
private clearPoll() { private clearPoll() {

View File

@@ -327,16 +327,19 @@ export class BrowseComponent implements OnInit {
loadBrowse() { loadBrowse() {
this.loading.set(true); this.loading.set(true);
this.error.set(null); this.error.set(null);
this.categoryService.browse(this.categoryId()).subscribe({ this.categoryService
next: (data) => { .browse(this.categoryId())
this.browseData.set(data); .pipe(takeUntilDestroyed(this.destroyRef))
this.loading.set(false); .subscribe({
}, next: (data) => {
error: () => { this.browseData.set(data);
this.error.set('Erreur lors du chargement des données.'); this.loading.set(false);
this.loading.set(false); },
}, error: () => {
}); this.error.set('Erreur lors du chargement des données.');
this.loading.set(false);
},
});
} }
openCreate() { openCreate() {
@@ -385,7 +388,9 @@ export class BrowseComponent implements OnInit {
forkJoin({ forkJoin({
apis: this.apiService.list({ search: raw || undefined, page: this.searchPage(), limit: this.searchLimit }), apis: this.apiService.list({ search: raw || undefined, page: this.searchPage(), limit: this.searchLimit }),
categories: this.categoryService.list(raw || undefined), categories: this.categoryService.list(raw || undefined),
}).subscribe({ })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: ({ apis, categories }) => { next: ({ apis, categories }) => {
this.searchResults.set(apis.items); this.searchResults.set(apis.items);
this.searchTotal.set(apis.total); this.searchTotal.set(apis.total);

View File

@@ -4,9 +4,11 @@ import {
computed, computed,
effect, effect,
inject, inject,
DestroyRef,
OnInit, OnInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@@ -154,6 +156,7 @@ import { ApiEntryListItem, ApiType } from '@datacat/shared';
}) })
export class CatalogComponent implements OnInit { export class CatalogComponent implements OnInit {
private apiService = inject(ApiService); private apiService = inject(ApiService);
private destroyRef = inject(DestroyRef);
apis = signal<ApiEntryListItem[]>([]); apis = signal<ApiEntryListItem[]>([]);
loading = signal(false); loading = signal(false);
@@ -221,6 +224,7 @@ export class CatalogComponent implements OnInit {
page: this.page(), page: this.page(),
limit: this.limit, limit: this.limit,
}) })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: (res) => { next: (res) => {
this.apis.set(res.items); this.apis.set(res.items);

View File

@@ -3,10 +3,11 @@ import {
signal, signal,
computed, computed,
inject, inject,
DestroyRef,
OnInit, OnInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
} from '@angular/core'; } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { Router, RouterLink, ActivatedRoute } from '@angular/router'; import { Router, RouterLink, ActivatedRoute } from '@angular/router';
@@ -507,6 +508,7 @@ export class UploadComponent implements OnInit {
private categoryService = inject(CategoryService); private categoryService = inject(CategoryService);
private router = inject(Router); private router = inject(Router);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private destroyRef = inject(DestroyRef);
step = signal(1); step = signal(1);
selectedFiles = signal<File[]>([]); selectedFiles = signal<File[]>([]);
@@ -628,24 +630,33 @@ export class UploadComponent implements OnInit {
const preselectedCategoryId = params.get('categoryId'); const preselectedCategoryId = params.get('categoryId');
const fromId = params.get('from'); const fromId = params.get('from');
this.categoryService.list().subscribe({ this.categoryService
next: (res) => { .list()
this.categories.set(res.items); .pipe(takeUntilDestroyed(this.destroyRef))
if (preselectedCategoryId) { .subscribe({
this.metaForm.patchValue({ categoryId: preselectedCategoryId }); next: (res) => {
} this.categories.set(res.items);
}, if (preselectedCategoryId) {
}); this.metaForm.patchValue({ categoryId: preselectedCategoryId });
}
},
});
if (fromId) { if (fromId) {
this.apiService.get(fromId).subscribe({ this.apiService
next: (api) => this.prefillFromApi(api), .get(fromId)
error: () => {}, .pipe(takeUntilDestroyed(this.destroyRef))
}); .subscribe({
this.apiService.getVersions(fromId).subscribe({ next: (api) => this.prefillFromApi(api),
next: (versions) => this.existingVersions.set(versions), error: () => {},
error: () => {}, });
}); this.apiService
.getVersions(fromId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (versions) => this.existingVersions.set(versions),
error: () => {},
});
} }
} }
@@ -748,10 +759,13 @@ export class UploadComponent implements OnInit {
formData.append('files', f); formData.append('files', f);
} }
this.apiService.validate(formData).subscribe({ this.apiService
next: (result) => { .validate(formData)
this.validating.set(false); .pipe(takeUntilDestroyed(this.destroyRef))
this.validationDone.set(true); .subscribe({
next: (result) => {
this.validating.set(false);
this.validationDone.set(true);
if (!result.valid) { if (!result.valid) {
this.validationErrors.set(result.errors); this.validationErrors.set(result.errors);
/* Ne pas réactiver le type s'il est verrouillé par fromApi */ /* Ne pas réactiver le type s'il est verrouillé par fromApi */
@@ -826,16 +840,19 @@ export class UploadComponent implements OnInit {
formData.append('contactTechnicalEntity', raw.contactTechnicalEntity ?? ''); formData.append('contactTechnicalEntity', raw.contactTechnicalEntity ?? '');
formData.append('contactTechnicalEmail', raw.contactTechnicalEmail ?? ''); formData.append('contactTechnicalEmail', raw.contactTechnicalEmail ?? '');
this.apiService.upload(formData).subscribe({ this.apiService
next: (entry) => { .upload(formData)
this.uploading.set(false); .pipe(takeUntilDestroyed(this.destroyRef))
this.router.navigate(['/catalog', entry.id]); .subscribe({
}, next: (entry) => {
error: (err) => { this.uploading.set(false);
this.uploading.set(false); this.router.navigate(['/catalog', entry.id]);
this.uploadError.set(err.error?.message ?? "Erreur lors de l'import"); },
}, error: (err) => {
}); this.uploading.set(false);
this.uploadError.set(err.error?.message ?? "Erreur lors de l'import");
},
});
} }
private async detectMainFileClient(files: File[]): Promise<void> { private async detectMainFileClient(files: File[]): Promise<void> {

View File

@@ -1,4 +1,5 @@
import { Component, signal, inject, ChangeDetectionStrategy, input, output } from '@angular/core'; import { Component, signal, inject, DestroyRef, ChangeDetectionStrategy, input, output } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CategoryService } from '../../core/category.service'; import { CategoryService } from '../../core/category.service';
import { CategoryWithCounts } from '@datacat/shared'; import { CategoryWithCounts } from '@datacat/shared';
@@ -49,6 +50,7 @@ import { CategoryWithCounts } from '@datacat/shared';
}) })
export class CategoryDeleteModalComponent { export class CategoryDeleteModalComponent {
private categoryService = inject(CategoryService); private categoryService = inject(CategoryService);
private destroyRef = inject(DestroyRef);
category = input.required<CategoryWithCounts>(); category = input.required<CategoryWithCounts>();
confirmed = output<void>(); confirmed = output<void>();
@@ -65,15 +67,18 @@ export class CategoryDeleteModalComponent {
const cat = this.category(); const cat = this.category();
this.deleteLoading.set(true); this.deleteLoading.set(true);
this.deleteError.set(null); this.deleteError.set(null);
this.categoryService.delete(cat.id).subscribe({ this.categoryService
next: () => { .delete(cat.id)
this.deleteLoading.set(false); .pipe(takeUntilDestroyed(this.destroyRef))
this.confirmed.emit(); .subscribe({
}, next: () => {
error: (err: { error?: { message?: string } }) => { this.deleteLoading.set(false);
this.deleteLoading.set(false); this.confirmed.emit();
this.deleteError.set(err?.error?.message ?? 'Erreur lors de la suppression.'); },
}, error: (err: { error?: { message?: string } }) => {
}); this.deleteLoading.set(false);
this.deleteError.set(err?.error?.message ?? 'Erreur lors de la suppression.');
},
});
} }
} }

View File

@@ -3,11 +3,13 @@ import {
signal, signal,
computed, computed,
inject, inject,
DestroyRef,
OnInit, OnInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
input, input,
output, output,
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CategoryService } from '../../core/category.service'; import { CategoryService } from '../../core/category.service';
import { Category } from '@datacat/shared'; import { Category } from '@datacat/shared';
@@ -79,6 +81,7 @@ import { Category } from '@datacat/shared';
}) })
export class CategoryFormModalComponent implements OnInit { export class CategoryFormModalComponent implements OnInit {
private categoryService = inject(CategoryService); private categoryService = inject(CategoryService);
private destroyRef = inject(DestroyRef);
mode = input<'create' | 'edit'>('create'); mode = input<'create' | 'edit'>('create');
category = input<Category | null>(null); category = input<Category | null>(null);
@@ -133,10 +136,13 @@ export class CategoryFormModalComponent implements OnInit {
this.formDescription.set(cat.description ?? ''); this.formDescription.set(cat.description ?? '');
} else { } else {
this.formParentId.set(this.parentId()); this.formParentId.set(this.parentId());
this.categoryService.list().subscribe({ this.categoryService
next: (res) => this.allCategories.set(res.items), .list()
error: () => this.allCategories.set([]), .pipe(takeUntilDestroyed(this.destroyRef))
}); .subscribe({
next: (res) => this.allCategories.set(res.items),
error: () => this.allCategories.set([]),
});
} }
} }
@@ -159,7 +165,7 @@ export class CategoryFormModalComponent implements OnInit {
? this.categoryService.create({ name, description, parentId: this.formParentId() ?? undefined }) ? this.categoryService.create({ name, description, parentId: this.formParentId() ?? undefined })
: this.categoryService.update(this.editingCategoryId()!, { name, description }); : this.categoryService.update(this.editingCategoryId()!, { name, description });
obs.subscribe({ obs.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: () => { next: () => {
this.formLoading.set(false); this.formLoading.set(false);
this.saved.emit(); this.saved.emit();