front: stoppe les fuites de souscriptions RxJS (takeUntilDestroyed)
Toutes les souscriptions des composants passent par takeUntilDestroyed(this.destroyRef) : browse, catalog, api-detail, upload, category-form-modal, category-delete-modal. api-detail abandonne le pattern manuel Subscription/ngOnDestroy au profit de DestroyRef (ngOnDestroy ne garde que le clearPoll du setInterval). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ApiEntry | null>(null);
|
||||
versions = signal<ApiEntryListItem[]>([]);
|
||||
@@ -462,26 +464,29 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
private pollInterval: ReturnType<typeof setInterval> | 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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ApiEntryListItem[]>([]);
|
||||
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);
|
||||
|
||||
@@ -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<File[]>([]);
|
||||
@@ -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<void> {
|
||||
|
||||
Reference in New Issue
Block a user