refactor: fix bugs et réduire duplication + extraire modales BrowseComponent
- fix(dto): ajouter @IsEmail() sur contactFunctionalEmail/TechnicalEmail dans UpdateApiEntryDto - fix(docs): corriger stringify des erreurs (String(error) → error.message) - refactor(mapper): extraire toApiEntryListItem() dans api-entry.mapper.ts partagé - refactor(categories): supprimer CONVENTION_LABELS et toApiEntryListItem locaux dupliqués - refactor(browse): extraire CategoryFormModalComponent et CategoryDeleteModalComponent - refactor(api-detail): convertir editPreviewTitle() en computed() Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Component,
|
||||
signal,
|
||||
computed,
|
||||
inject,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
@@ -381,10 +382,10 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||
editContactTechnicalEntity = signal('');
|
||||
editContactTechnicalEmail = signal('');
|
||||
|
||||
editPreviewTitle() {
|
||||
editPreviewTitle = computed(() => {
|
||||
const labels: Record<ApiConvention, string> = { CONSULTER: 'Consulter', ENREGISTRER: 'Enregistrer', ETRE_NOTIFIE: 'Être notifié' };
|
||||
return `${labels[this.editConvention()]} ${this.editName()}`.trim();
|
||||
}
|
||||
});
|
||||
|
||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private id = '';
|
||||
|
||||
@@ -15,11 +15,13 @@ import { CategoryService } from '../../core/category.service';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { CategoryBrowseResponse, Category, CategoryWithCounts, ApiEntryListItem } from '@datacat/shared';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { CategoryFormModalComponent } from '../../shared/category-form-modal/category-form-modal.component';
|
||||
import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal/category-delete-modal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-browse',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
imports: [CommonModule, RouterLink, CategoryFormModalComponent, CategoryDeleteModalComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="fr-container fr-mt-4w fr-mb-4w">
|
||||
@@ -238,110 +240,27 @@ import { forkJoin } from 'rxjs';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Modale de suppression -->
|
||||
@if (showDeleteModal()) {
|
||||
<div style="position:fixed;inset:0;z-index:9999;background:rgba(22,22,22,.64);display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;overflow-y:auto"
|
||||
role="dialog" aria-modal="true" aria-labelledby="delete-modal-title">
|
||||
<div style="background:white;width:calc(100% - 2rem);max-width:480px;margin:0 1rem 2rem">
|
||||
<div class="fr-p-4w">
|
||||
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
||||
<div class="fr-col">
|
||||
<h2 id="delete-modal-title" class="fr-h3 fr-mb-0">Supprimer le dossier</h2>
|
||||
</div>
|
||||
<div class="fr-col-auto">
|
||||
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm"
|
||||
(click)="showDeleteModal.set(false)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="fr-text--lead fr-mb-1w">
|
||||
Êtes-vous sûr de vouloir supprimer le dossier
|
||||
<strong>« {{ deletingCategory()?.name }} »</strong> ?
|
||||
</p>
|
||||
<p class="fr-text--sm fr-text--mention-grey fr-mb-3w">
|
||||
Cette action est irréversible.
|
||||
</p>
|
||||
@if (deleteError()) {
|
||||
<div class="fr-alert fr-alert--error fr-mb-2w">
|
||||
<p>{{ deleteError() }}</p>
|
||||
</div>
|
||||
}
|
||||
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline">
|
||||
<button class="fr-btn fr-btn--secondary" (click)="showDeleteModal.set(false)"
|
||||
[disabled]="deleteLoading()">Annuler</button>
|
||||
<button class="fr-btn fr-btn--error" (click)="confirmDelete()"
|
||||
[disabled]="deleteLoading()">
|
||||
{{ deleteLoading() ? 'Suppression...' : 'Supprimer' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Modale création / édition (overlay Angular, sans fr-modal pour éviter l'interférence du JS DSFR) -->
|
||||
@if (showFormModal()) {
|
||||
<div style="position:fixed;inset:0;z-index:9999;background:rgba(22,22,22,.64);display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;overflow-y:auto"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||
<div style="background:white;width:calc(100% - 2rem);max-width:540px;margin:0 1rem 2rem">
|
||||
<div class="fr-p-4w">
|
||||
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
||||
<div class="fr-col">
|
||||
<h2 id="modal-title" class="fr-h3 fr-mb-0">
|
||||
{{ formMode() === 'create' ? 'Nouveau dossier' : 'Modifier le dossier' }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="fr-col-auto">
|
||||
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm"
|
||||
(click)="closeModal()">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (formError()) {
|
||||
<div class="fr-alert fr-alert--error fr-mb-2w">
|
||||
<p>{{ formError() }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (formMode() === 'create') {
|
||||
<div class="fr-select-group fr-mb-2w">
|
||||
<label class="fr-label" for="form-parent">Emplacement</label>
|
||||
<select id="form-parent" class="fr-select"
|
||||
(change)="formParentId.set($any($event.target).value || null)">
|
||||
<option value="" [selected]="!formParentId()"></option>
|
||||
@for (opt of categoriesForSelect(); track opt.id) {
|
||||
<option [value]="opt.id" [selected]="opt.id === formParentId()">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
<div class="fr-input-group fr-mb-2w" [class.fr-input-group--error]="formNameError()">
|
||||
<label class="fr-label" for="form-name">
|
||||
Nom <span style="color:var(--text-default-error)">*</span>
|
||||
</label>
|
||||
<input id="form-name" class="fr-input" [class.fr-input--error]="formNameError()" type="text"
|
||||
[value]="formName()"
|
||||
(input)="formName.set($any($event.target).value); formNameError.set(false)"
|
||||
(blur)="formNameError.set(!formName().trim())" />
|
||||
@if (formNameError()) { <p class="fr-error-text">Le nom est obligatoire.</p> }
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-3w">
|
||||
<label class="fr-label" for="form-desc">
|
||||
Description
|
||||
</label>
|
||||
<textarea id="form-desc" class="fr-input" rows="3"
|
||||
[value]="formDescription()"
|
||||
(input)="formDescription.set($any($event.target).value)"></textarea>
|
||||
</div>
|
||||
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline">
|
||||
<button class="fr-btn fr-btn--secondary" (click)="closeModal()">Annuler</button>
|
||||
<button class="fr-btn" (click)="submitForm()" [disabled]="formLoading()">
|
||||
{{ formMode() === 'create' ? 'Créer' : 'Enregistrer' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Modale de suppression -->
|
||||
@if (deletingCategory()) {
|
||||
<app-category-delete-modal
|
||||
[category]="deletingCategory()!"
|
||||
(confirmed)="onDeleteConfirmed()"
|
||||
(cancelled)="deletingCategory.set(null)"
|
||||
/>
|
||||
}
|
||||
|
||||
<!-- Modale création / édition -->
|
||||
@if (showFormModal()) {
|
||||
<app-category-form-modal
|
||||
[mode]="formMode()"
|
||||
[category]="editingCategory()"
|
||||
[parentId]="categoryId()"
|
||||
(saved)="onFormSaved()"
|
||||
(closed)="showFormModal.set(false)"
|
||||
/>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class BrowseComponent implements OnInit {
|
||||
@@ -369,64 +288,18 @@ export class BrowseComponent implements OnInit {
|
||||
|
||||
private searchDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/* Signaux pour la modale création / édition */
|
||||
/* Signaux pour les modales */
|
||||
showFormModal = signal(false);
|
||||
formMode = signal<'create' | 'edit'>('create');
|
||||
editingCategoryId = signal<string | null>(null);
|
||||
formName = signal('');
|
||||
formDescription = signal('');
|
||||
formParentId = signal<string | null>(null);
|
||||
formError = signal<string | null>(null);
|
||||
formNameError = signal(false);
|
||||
formLoading = signal(false);
|
||||
allCategories = signal<Category[]>([]);
|
||||
|
||||
/* Signaux pour la modale de suppression */
|
||||
showDeleteModal = signal(false);
|
||||
editingCategory = signal<Category | null>(null);
|
||||
deletingCategory = signal<CategoryWithCounts | null>(null);
|
||||
deleteLoading = signal(false);
|
||||
deleteError = signal<string | null>(null);
|
||||
|
||||
/* Ancêtres = breadcrumb sans le nœud courant (déjà dans le h1) */
|
||||
ancestors = computed<Category[]>(() => {
|
||||
const d = this.browseData();
|
||||
if (!d || !d.category) return [];
|
||||
return d.breadcrumb.slice(0, -1);
|
||||
});
|
||||
|
||||
/* Options du select "Emplacement" avec indentation type └ similaire au formulaire d'import */
|
||||
categoriesForSelect = computed<{ id: string; label: string }[]>(() => {
|
||||
const cats = this.allCategories();
|
||||
const map = new Map(cats.map((c) => [c.id, c]));
|
||||
|
||||
/* Chemin complet pour le tri */
|
||||
const getSortPath = (id: string, depth = 0): string => {
|
||||
if (depth > 10) return '';
|
||||
const cat = map.get(id);
|
||||
if (!cat) return '';
|
||||
if (!cat.parentId) return cat.name;
|
||||
return `${getSortPath(cat.parentId, depth + 1)} / ${cat.name}`;
|
||||
};
|
||||
|
||||
/* Profondeur pour l'indentation */
|
||||
const getDepth = (id: string, depth = 0): number => {
|
||||
if (depth > 10) return depth;
|
||||
const cat = map.get(id);
|
||||
if (!cat?.parentId) return depth;
|
||||
return getDepth(cat.parentId, depth + 1);
|
||||
};
|
||||
|
||||
return cats
|
||||
.map((c) => {
|
||||
const depth = getDepth(c.id);
|
||||
const indent = '\u00a0\u00a0'.repeat(depth);
|
||||
const prefix = depth > 0 ? `${indent}└\u00a0` : '';
|
||||
return { id: c.id, label: `${prefix}${c.name}`, _sort: getSortPath(c.id) };
|
||||
})
|
||||
.sort((a, b) => a._sort.localeCompare(b._sort, 'fr'))
|
||||
.map(({ id, label }) => ({ id, label }));
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.route.paramMap
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
@@ -453,92 +326,28 @@ export class BrowseComponent implements OnInit {
|
||||
|
||||
openCreate() {
|
||||
this.formMode.set('create');
|
||||
this.editingCategoryId.set(null);
|
||||
this.formName.set('');
|
||||
this.formDescription.set('');
|
||||
this.formParentId.set(this.categoryId());
|
||||
this.formError.set(null);
|
||||
this.formNameError.set(false);
|
||||
/* Charger les catégories d'abord, puis afficher la modale pour que la pré-sélection fonctionne */
|
||||
this.categoryService.list().subscribe({
|
||||
next: (res) => {
|
||||
this.allCategories.set(res.items);
|
||||
this.showFormModal.set(true);
|
||||
},
|
||||
error: () => {
|
||||
this.allCategories.set([]);
|
||||
this.showFormModal.set(true);
|
||||
},
|
||||
});
|
||||
this.editingCategory.set(null);
|
||||
this.showFormModal.set(true);
|
||||
}
|
||||
|
||||
openEdit(cat: Category) {
|
||||
this.formMode.set('edit');
|
||||
this.editingCategoryId.set(cat.id);
|
||||
this.formName.set(cat.name);
|
||||
this.formDescription.set(cat.description ?? '');
|
||||
this.formError.set(null);
|
||||
this.formNameError.set(false);
|
||||
this.editingCategory.set(cat);
|
||||
this.showFormModal.set(true);
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.showFormModal.set(false);
|
||||
}
|
||||
|
||||
submitForm() {
|
||||
if (!this.formName().trim()) {
|
||||
this.formNameError.set(true);
|
||||
return;
|
||||
}
|
||||
this.formNameError.set(false);
|
||||
this.formLoading.set(true);
|
||||
const name = this.formName().trim();
|
||||
const description = this.formDescription().trim() || undefined;
|
||||
|
||||
const obs =
|
||||
this.formMode() === 'create'
|
||||
? this.categoryService.create({
|
||||
name,
|
||||
description,
|
||||
parentId: this.formParentId() ?? undefined,
|
||||
})
|
||||
: this.categoryService.update(this.editingCategoryId()!, { name, description });
|
||||
|
||||
obs.subscribe({
|
||||
next: () => {
|
||||
this.formLoading.set(false);
|
||||
this.showFormModal.set(false);
|
||||
this.loadBrowse();
|
||||
},
|
||||
error: (err: { error?: { message?: string } }) => {
|
||||
this.formLoading.set(false);
|
||||
this.formError.set(err?.error?.message ?? 'Une erreur est survenue.');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openDelete(cat: CategoryWithCounts) {
|
||||
this.deletingCategory.set(cat);
|
||||
this.deleteError.set(null);
|
||||
this.showDeleteModal.set(true);
|
||||
}
|
||||
|
||||
confirmDelete() {
|
||||
const cat = this.deletingCategory();
|
||||
if (!cat) return;
|
||||
this.deleteLoading.set(true);
|
||||
this.categoryService.delete(cat.id).subscribe({
|
||||
next: () => {
|
||||
this.deleteLoading.set(false);
|
||||
this.showDeleteModal.set(false);
|
||||
this.loadBrowse();
|
||||
},
|
||||
error: (err: { error?: { message?: string } }) => {
|
||||
this.deleteLoading.set(false);
|
||||
this.deleteError.set(err?.error?.message ?? 'Erreur lors de la suppression.');
|
||||
},
|
||||
});
|
||||
onFormSaved() {
|
||||
this.showFormModal.set(false);
|
||||
this.loadBrowse();
|
||||
}
|
||||
|
||||
onDeleteConfirmed() {
|
||||
this.deletingCategory.set(null);
|
||||
this.loadBrowse();
|
||||
}
|
||||
|
||||
onSearchChange(value: string) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, signal, inject, ChangeDetectionStrategy, input, output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CategoryService } from '../../core/category.service';
|
||||
import { CategoryWithCounts } from '@datacat/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-delete-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="position:fixed;inset:0;z-index:9999;background:rgba(22,22,22,.64);display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;overflow-y:auto"
|
||||
role="dialog" aria-modal="true" aria-labelledby="delete-modal-title">
|
||||
<div style="background:white;width:calc(100% - 2rem);max-width:480px;margin:0 1rem 2rem">
|
||||
<div class="fr-p-4w">
|
||||
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
||||
<div class="fr-col">
|
||||
<h2 id="delete-modal-title" class="fr-h3 fr-mb-0">Supprimer le dossier</h2>
|
||||
</div>
|
||||
<div class="fr-col-auto">
|
||||
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm"
|
||||
(click)="cancel()">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="fr-text--lead fr-mb-1w">
|
||||
Êtes-vous sûr de vouloir supprimer le dossier
|
||||
<strong>« {{ category().name }} »</strong> ?
|
||||
</p>
|
||||
<p class="fr-text--sm fr-text--mention-grey fr-mb-3w">
|
||||
Cette action est irréversible.
|
||||
</p>
|
||||
@if (deleteError()) {
|
||||
<div class="fr-alert fr-alert--error fr-mb-2w">
|
||||
<p>{{ deleteError() }}</p>
|
||||
</div>
|
||||
}
|
||||
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline">
|
||||
<button class="fr-btn fr-btn--secondary" (click)="cancel()"
|
||||
[disabled]="deleteLoading()">Annuler</button>
|
||||
<button class="fr-btn fr-btn--error" (click)="confirmDelete()"
|
||||
[disabled]="deleteLoading()">
|
||||
{{ deleteLoading() ? 'Suppression...' : 'Supprimer' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class CategoryDeleteModalComponent {
|
||||
private categoryService = inject(CategoryService);
|
||||
|
||||
category = input.required<CategoryWithCounts>();
|
||||
confirmed = output<void>();
|
||||
cancelled = output<void>();
|
||||
|
||||
deleteLoading = signal(false);
|
||||
deleteError = signal<string | null>(null);
|
||||
|
||||
cancel() {
|
||||
this.cancelled.emit();
|
||||
}
|
||||
|
||||
confirmDelete() {
|
||||
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.');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
Component,
|
||||
signal,
|
||||
computed,
|
||||
inject,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CategoryService } from '../../core/category.service';
|
||||
import { Category } from '@datacat/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-form-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="position:fixed;inset:0;z-index:9999;background:rgba(22,22,22,.64);display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;overflow-y:auto"
|
||||
role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||
<div style="background:white;width:calc(100% - 2rem);max-width:540px;margin:0 1rem 2rem">
|
||||
<div class="fr-p-4w">
|
||||
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
||||
<div class="fr-col">
|
||||
<h2 id="modal-title" class="fr-h3 fr-mb-0">
|
||||
{{ mode() === 'create' ? 'Nouveau dossier' : 'Modifier le dossier' }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="fr-col-auto">
|
||||
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm"
|
||||
(click)="close()">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (formError()) {
|
||||
<div class="fr-alert fr-alert--error fr-mb-2w">
|
||||
<p>{{ formError() }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (mode() === 'create') {
|
||||
<div class="fr-select-group fr-mb-2w">
|
||||
<label class="fr-label" for="form-parent">Emplacement</label>
|
||||
<select id="form-parent" class="fr-select"
|
||||
(change)="formParentId.set($any($event.target).value || null)">
|
||||
<option value="" [selected]="!formParentId()"></option>
|
||||
@for (opt of categoriesForSelect(); track opt.id) {
|
||||
<option [value]="opt.id" [selected]="opt.id === formParentId()">{{ opt.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
<div class="fr-input-group fr-mb-2w" [class.fr-input-group--error]="formNameError()">
|
||||
<label class="fr-label" for="form-name">
|
||||
Nom <span style="color:var(--text-default-error)">*</span>
|
||||
</label>
|
||||
<input id="form-name" class="fr-input" [class.fr-input--error]="formNameError()" type="text"
|
||||
[value]="formName()"
|
||||
(input)="formName.set($any($event.target).value); formNameError.set(false)"
|
||||
(blur)="formNameError.set(!formName().trim())" />
|
||||
@if (formNameError()) { <p class="fr-error-text">Le nom est obligatoire.</p> }
|
||||
</div>
|
||||
<div class="fr-input-group fr-mb-3w">
|
||||
<label class="fr-label" for="form-desc">Description</label>
|
||||
<textarea id="form-desc" class="fr-input" rows="3"
|
||||
[value]="formDescription()"
|
||||
(input)="formDescription.set($any($event.target).value)"></textarea>
|
||||
</div>
|
||||
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline">
|
||||
<button class="fr-btn fr-btn--secondary" (click)="close()">Annuler</button>
|
||||
<button class="fr-btn" (click)="submit()" [disabled]="formLoading()">
|
||||
{{ mode() === 'create' ? 'Créer' : 'Enregistrer' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class CategoryFormModalComponent implements OnInit {
|
||||
private categoryService = inject(CategoryService);
|
||||
|
||||
mode = input<'create' | 'edit'>('create');
|
||||
category = input<Category | null>(null);
|
||||
parentId = input<string | null>(null);
|
||||
saved = output<void>();
|
||||
closed = output<void>();
|
||||
|
||||
formName = signal('');
|
||||
formDescription = signal('');
|
||||
formParentId = signal<string | null>(null);
|
||||
formError = signal<string | null>(null);
|
||||
formNameError = signal(false);
|
||||
formLoading = signal(false);
|
||||
allCategories = signal<Category[]>([]);
|
||||
editingCategoryId = signal<string | null>(null);
|
||||
|
||||
categoriesForSelect = computed<{ id: string; label: string }[]>(() => {
|
||||
const cats = this.allCategories();
|
||||
const map = new Map(cats.map((c) => [c.id, c]));
|
||||
|
||||
const getSortPath = (id: string, depth = 0): string => {
|
||||
if (depth > 10) return '';
|
||||
const cat = map.get(id);
|
||||
if (!cat) return '';
|
||||
if (!cat.parentId) return cat.name;
|
||||
return `${getSortPath(cat.parentId, depth + 1)} / ${cat.name}`;
|
||||
};
|
||||
|
||||
const getDepth = (id: string, depth = 0): number => {
|
||||
if (depth > 10) return depth;
|
||||
const cat = map.get(id);
|
||||
if (!cat?.parentId) return depth;
|
||||
return getDepth(cat.parentId, depth + 1);
|
||||
};
|
||||
|
||||
return cats
|
||||
.map((c) => {
|
||||
const depth = getDepth(c.id);
|
||||
const indent = '\u00a0\u00a0'.repeat(depth);
|
||||
const prefix = depth > 0 ? `${indent}└\u00a0` : '';
|
||||
return { id: c.id, label: `${prefix}${c.name}`, _sort: getSortPath(c.id) };
|
||||
})
|
||||
.sort((a, b) => a._sort.localeCompare(b._sort, 'fr'))
|
||||
.map(({ id, label }) => ({ id, label }));
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
const cat = this.category();
|
||||
if (this.mode() === 'edit' && cat) {
|
||||
this.editingCategoryId.set(cat.id);
|
||||
this.formName.set(cat.name);
|
||||
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([]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closed.emit();
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.formName().trim()) {
|
||||
this.formNameError.set(true);
|
||||
return;
|
||||
}
|
||||
this.formNameError.set(false);
|
||||
this.formLoading.set(true);
|
||||
const name = this.formName().trim();
|
||||
const description = this.formDescription().trim() || undefined;
|
||||
|
||||
const obs =
|
||||
this.mode() === 'create'
|
||||
? this.categoryService.create({ name, description, parentId: this.formParentId() ?? undefined })
|
||||
: this.categoryService.update(this.editingCategoryId()!, { name, description });
|
||||
|
||||
obs.subscribe({
|
||||
next: () => {
|
||||
this.formLoading.set(false);
|
||||
this.saved.emit();
|
||||
},
|
||||
error: (err: { error?: { message?: string } }) => {
|
||||
this.formLoading.set(false);
|
||||
this.formError.set(err?.error?.message ?? 'Une erreur est survenue.');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user