diff --git a/front-public/src/app/pages/api-detail/api-detail.component.ts b/front-public/src/app/pages/api-detail/api-detail.component.ts index 563d3e6..28d5bfd 100644 --- a/front-public/src/app/pages/api-detail/api-detail.component.ts +++ b/front-public/src/app/pages/api-detail/api-detail.component.ts @@ -3,14 +3,15 @@ import { signal, computed, inject, + DestroyRef, OnInit, OnDestroy, ChangeDetectionStrategy, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { Subscription } from 'rxjs'; import { ApiService } from '../../core/api.service'; import { CategoryService } from '../../core/category.service'; import { ApiEntry, ApiEntryListItem, Category } from '@datacat/shared'; @@ -416,6 +417,7 @@ export class ApiDetailComponent implements OnInit, OnDestroy { private apiService = inject(ApiService); private categoryService = inject(CategoryService); private fb = inject(FormBuilder); + private destroyRef = inject(DestroyRef); api = signal(null); versions = signal([]); @@ -462,26 +464,29 @@ export class ApiDetailComponent implements OnInit, OnDestroy { }); private pollInterval: ReturnType | null = null; - private routeSub: Subscription | null = null; private id = ''; ngOnInit() { - this.categoryService.list().subscribe((res) => this.categories.set(res.items)); - this.routeSub = this.route.paramMap.subscribe((params) => { - this.id = params.get('id') ?? ''; - this.api.set(null); - this.versions.set([]); - this.error.set(null); - this.categoryBreadcrumb.set([]); - this.editingInfo.set(false); - this.editingContacts.set(false); - this.clearPoll(); - this.loadApi(); - }); + this.categoryService + .list() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((res) => this.categories.set(res.items)); + this.route.paramMap + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((params) => { + this.id = params.get('id') ?? ''; + this.api.set(null); + this.versions.set([]); + this.error.set(null); + this.categoryBreadcrumb.set([]); + this.editingInfo.set(false); + this.editingContacts.set(false); + this.clearPoll(); + this.loadApi(); + }); } ngOnDestroy() { - this.routeSub?.unsubscribe(); this.clearPoll(); } @@ -498,29 +503,38 @@ export class ApiDetailComponent implements OnInit, OnDestroy { onSetCurrent() { this.settingCurrent.set(true); - this.apiService.setCurrent(this.id).subscribe({ - next: (entry) => { - this.api.set(entry); - this.settingCurrent.set(false); - this.apiService.getVersions(this.id).subscribe({ - next: (v) => this.versions.set(v), - error: () => {}, - }); - }, - error: () => this.settingCurrent.set(false), - }); + this.apiService + .setCurrent(this.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (entry) => { + this.api.set(entry); + this.settingCurrent.set(false); + this.apiService + .getVersions(this.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (v) => this.versions.set(v), + error: () => {}, + }); + }, + error: () => this.settingCurrent.set(false), + }); } onRegenerate() { this.regenerating.set(true); - this.apiService.regenerate(this.id).subscribe({ - next: (entry) => { - this.api.set(entry); - this.regenerating.set(false); - this.startPollIfPending(); - }, - error: () => this.regenerating.set(false), - }); + this.apiService + .regenerate(this.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (entry) => { + this.api.set(entry); + this.regenerating.set(false); + this.startPollIfPending(); + }, + error: () => this.regenerating.set(false), + }); } startEditInfo() { @@ -568,10 +582,12 @@ export class ApiDetailComponent implements OnInit, OnDestroy { versionPatch: raw.versionPatch ?? 0, categoryId: raw.categoryId || null, description: raw.description || null, - }).subscribe({ - next: (updated) => { this.api.set(updated); this.savingInfo.set(false); this.editingInfo.set(false); }, - error: () => this.savingInfo.set(false), - }); + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (updated) => { this.api.set(updated); this.savingInfo.set(false); this.editingInfo.set(false); }, + error: () => this.savingInfo.set(false), + }); } submitContacts() { @@ -587,10 +603,12 @@ export class ApiDetailComponent implements OnInit, OnDestroy { contactTechnicalName: raw.contactTechnicalName ?? '', contactTechnicalEntity: raw.contactTechnicalEntity ?? '', contactTechnicalEmail: raw.contactTechnicalEmail ?? '', - }).subscribe({ - next: (updated) => { this.api.set(updated); this.savingContacts.set(false); this.editingContacts.set(false); }, - error: () => this.savingContacts.set(false), - }); + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (updated) => { this.api.set(updated); this.savingContacts.set(false); this.editingContacts.set(false); }, + error: () => this.savingContacts.set(false), + }); } onDelete() { @@ -599,16 +617,19 @@ export class ApiDetailComponent implements OnInit, OnDestroy { /* Calculer la cible de redirection avant la suppression */ const otherVersions = this.versions().filter((v) => v.id !== this.id); const redirectTarget = otherVersions.find((v) => v.isCurrent) ?? otherVersions[0]; - this.apiService.delete(this.id).subscribe({ - next: () => { - if (redirectTarget) { - this.router.navigate(['/catalog', redirectTarget.id]); - } else { - this.router.navigate(this.backUrl()); - } - }, - error: () => this.deleting.set(false), - }); + this.apiService + .delete(this.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + if (redirectTarget) { + this.router.navigate(['/catalog', redirectTarget.id]); + } else { + this.router.navigate(this.backUrl()); + } + }, + error: () => this.deleting.set(false), + }); } badgeClass(status: string): string { @@ -627,27 +648,36 @@ export class ApiDetailComponent implements OnInit, OnDestroy { private loadApi() { this.loading.set(true); - this.apiService.get(this.id).subscribe({ - next: (entry) => { - this.api.set(entry); - this.loading.set(false); - this.startPollIfPending(); - this.apiService.getVersions(this.id).subscribe({ - next: (v) => this.versions.set(v), - error: () => {}, - }); - if (entry.categoryId) { - this.categoryService.browse(entry.categoryId).subscribe({ - next: (data) => this.categoryBreadcrumb.set(data.breadcrumb), - error: () => {}, - }); - } - }, - error: (err) => { - this.error.set(err.message ?? 'Erreur lors du chargement'); - this.loading.set(false); - }, - }); + this.apiService + .get(this.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (entry) => { + this.api.set(entry); + this.loading.set(false); + this.startPollIfPending(); + this.apiService + .getVersions(this.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (v) => this.versions.set(v), + error: () => {}, + }); + if (entry.categoryId) { + this.categoryService + .browse(entry.categoryId) + .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() { @@ -658,12 +688,15 @@ export class ApiDetailComponent implements OnInit, OnDestroy { } private pollStatus() { - this.apiService.get(this.id).subscribe({ - next: (entry) => { - this.api.set(entry); - if (entry.status !== 'PENDING') this.clearPoll(); - }, - }); + this.apiService + .get(this.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (entry) => { + this.api.set(entry); + if (entry.status !== 'PENDING') this.clearPoll(); + }, + }); } private clearPoll() { diff --git a/front-public/src/app/pages/browse/browse.component.ts b/front-public/src/app/pages/browse/browse.component.ts index 99a45a9..8434a1b 100644 --- a/front-public/src/app/pages/browse/browse.component.ts +++ b/front-public/src/app/pages/browse/browse.component.ts @@ -327,16 +327,19 @@ export class BrowseComponent implements OnInit { loadBrowse() { this.loading.set(true); this.error.set(null); - this.categoryService.browse(this.categoryId()).subscribe({ - next: (data) => { - this.browseData.set(data); - this.loading.set(false); - }, - error: () => { - this.error.set('Erreur lors du chargement des données.'); - this.loading.set(false); - }, - }); + this.categoryService + .browse(this.categoryId()) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (data) => { + this.browseData.set(data); + this.loading.set(false); + }, + error: () => { + this.error.set('Erreur lors du chargement des données.'); + this.loading.set(false); + }, + }); } openCreate() { @@ -385,7 +388,9 @@ export class BrowseComponent implements OnInit { forkJoin({ apis: this.apiService.list({ search: raw || undefined, page: this.searchPage(), limit: this.searchLimit }), categories: this.categoryService.list(raw || undefined), - }).subscribe({ + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ next: ({ apis, categories }) => { this.searchResults.set(apis.items); this.searchTotal.set(apis.total); diff --git a/front-public/src/app/pages/catalog/catalog.component.ts b/front-public/src/app/pages/catalog/catalog.component.ts index 5d64934..bbe7390 100644 --- a/front-public/src/app/pages/catalog/catalog.component.ts +++ b/front-public/src/app/pages/catalog/catalog.component.ts @@ -4,9 +4,11 @@ import { computed, effect, inject, + DestroyRef, OnInit, ChangeDetectionStrategy, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; @@ -154,6 +156,7 @@ import { ApiEntryListItem, ApiType } from '@datacat/shared'; }) export class CatalogComponent implements OnInit { private apiService = inject(ApiService); + private destroyRef = inject(DestroyRef); apis = signal([]); loading = signal(false); @@ -221,6 +224,7 @@ export class CatalogComponent implements OnInit { page: this.page(), limit: this.limit, }) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (res) => { this.apis.set(res.items); diff --git a/front-public/src/app/pages/upload/upload.component.ts b/front-public/src/app/pages/upload/upload.component.ts index 8e3ca54..3a5752f 100644 --- a/front-public/src/app/pages/upload/upload.component.ts +++ b/front-public/src/app/pages/upload/upload.component.ts @@ -3,10 +3,11 @@ import { signal, computed, inject, + DestroyRef, OnInit, ChangeDetectionStrategy, } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { Router, RouterLink, ActivatedRoute } from '@angular/router'; @@ -507,6 +508,7 @@ export class UploadComponent implements OnInit { private categoryService = inject(CategoryService); private router = inject(Router); private route = inject(ActivatedRoute); + private destroyRef = inject(DestroyRef); step = signal(1); selectedFiles = signal([]); @@ -628,24 +630,33 @@ export class UploadComponent implements OnInit { const preselectedCategoryId = params.get('categoryId'); const fromId = params.get('from'); - this.categoryService.list().subscribe({ - next: (res) => { - this.categories.set(res.items); - if (preselectedCategoryId) { - this.metaForm.patchValue({ categoryId: preselectedCategoryId }); - } - }, - }); + this.categoryService + .list() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (res) => { + this.categories.set(res.items); + if (preselectedCategoryId) { + this.metaForm.patchValue({ categoryId: preselectedCategoryId }); + } + }, + }); if (fromId) { - this.apiService.get(fromId).subscribe({ - next: (api) => this.prefillFromApi(api), - error: () => {}, - }); - this.apiService.getVersions(fromId).subscribe({ - next: (versions) => this.existingVersions.set(versions), - error: () => {}, - }); + this.apiService + .get(fromId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (api) => this.prefillFromApi(api), + 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); } - this.apiService.validate(formData).subscribe({ - next: (result) => { - this.validating.set(false); - this.validationDone.set(true); + this.apiService + .validate(formData) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (result) => { + this.validating.set(false); + this.validationDone.set(true); if (!result.valid) { this.validationErrors.set(result.errors); /* 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('contactTechnicalEmail', raw.contactTechnicalEmail ?? ''); - this.apiService.upload(formData).subscribe({ - next: (entry) => { - this.uploading.set(false); - this.router.navigate(['/catalog', entry.id]); - }, - error: (err) => { - this.uploading.set(false); - this.uploadError.set(err.error?.message ?? "Erreur lors de l'import"); - }, - }); + this.apiService + .upload(formData) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (entry) => { + this.uploading.set(false); + this.router.navigate(['/catalog', entry.id]); + }, + error: (err) => { + this.uploading.set(false); + this.uploadError.set(err.error?.message ?? "Erreur lors de l'import"); + }, + }); } private async detectMainFileClient(files: File[]): Promise { diff --git a/front-public/src/app/shared/category-delete-modal/category-delete-modal.component.ts b/front-public/src/app/shared/category-delete-modal/category-delete-modal.component.ts index d902f38..7c3eebb 100644 --- a/front-public/src/app/shared/category-delete-modal/category-delete-modal.component.ts +++ b/front-public/src/app/shared/category-delete-modal/category-delete-modal.component.ts @@ -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 { CategoryService } from '../../core/category.service'; import { CategoryWithCounts } from '@datacat/shared'; @@ -49,6 +50,7 @@ import { CategoryWithCounts } from '@datacat/shared'; }) export class CategoryDeleteModalComponent { private categoryService = inject(CategoryService); + private destroyRef = inject(DestroyRef); category = input.required(); confirmed = output(); @@ -65,15 +67,18 @@ export class CategoryDeleteModalComponent { const cat = this.category(); this.deleteLoading.set(true); this.deleteError.set(null); - this.categoryService.delete(cat.id).subscribe({ - next: () => { - this.deleteLoading.set(false); - this.confirmed.emit(); - }, - error: (err: { error?: { message?: string } }) => { - this.deleteLoading.set(false); - this.deleteError.set(err?.error?.message ?? 'Erreur lors de la suppression.'); - }, - }); + this.categoryService + .delete(cat.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.deleteLoading.set(false); + this.confirmed.emit(); + }, + error: (err: { error?: { message?: string } }) => { + this.deleteLoading.set(false); + this.deleteError.set(err?.error?.message ?? 'Erreur lors de la suppression.'); + }, + }); } } diff --git a/front-public/src/app/shared/category-form-modal/category-form-modal.component.ts b/front-public/src/app/shared/category-form-modal/category-form-modal.component.ts index 9d58d20..a8d14cf 100644 --- a/front-public/src/app/shared/category-form-modal/category-form-modal.component.ts +++ b/front-public/src/app/shared/category-form-modal/category-form-modal.component.ts @@ -3,11 +3,13 @@ import { signal, computed, inject, + DestroyRef, OnInit, ChangeDetectionStrategy, input, output, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { CategoryService } from '../../core/category.service'; import { Category } from '@datacat/shared'; @@ -79,6 +81,7 @@ import { Category } from '@datacat/shared'; }) export class CategoryFormModalComponent implements OnInit { private categoryService = inject(CategoryService); + private destroyRef = inject(DestroyRef); mode = input<'create' | 'edit'>('create'); category = input(null); @@ -133,10 +136,13 @@ export class CategoryFormModalComponent implements OnInit { this.formDescription.set(cat.description ?? ''); } else { this.formParentId.set(this.parentId()); - this.categoryService.list().subscribe({ - next: (res) => this.allCategories.set(res.items), - error: () => this.allCategories.set([]), - }); + this.categoryService + .list() + .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.update(this.editingCategoryId()!, { name, description }); - obs.subscribe({ + obs.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: () => { this.formLoading.set(false); this.saved.emit();