feat(browse): gestion CRUD des dossiers depuis la page Browse
- Ajout boutons Modifier/Supprimer sur chaque tuile de dossier - Supprimer visible uniquement si dossier vide (0 APIs, 0 sous-dossiers) - Modale Angular (sans fr-modal DSFR) pour créer/éditer un dossier - Backend: remove() lève BadRequestException si dossier non vide - Ajout fichiers YAML de démo (OpenAPI Petstore, AsyncAPI User Events) 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,4 +1,4 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, IsNull } from 'typeorm';
|
import { Repository, IsNull } from 'typeorm';
|
||||||
import { CategoryEntity } from './category.entity';
|
import { CategoryEntity } from './category.entity';
|
||||||
@@ -61,20 +61,23 @@ export class CategoriesService {
|
|||||||
|
|
||||||
async remove(id: string): Promise<void> {
|
async remove(id: string): Promise<void> {
|
||||||
await this.findOneOrThrow(id);
|
await this.findOneOrThrow(id);
|
||||||
/* Désassocier les APIs enfants */
|
|
||||||
await this.apiRepo
|
/* Vérifier qu'aucune API n'est rattachée */
|
||||||
.createQueryBuilder()
|
const apiCount = await this.apiRepo.count({ where: { categoryId: id } });
|
||||||
.update(ApiEntryEntity)
|
if (apiCount > 0) {
|
||||||
.set({ categoryId: null })
|
throw new BadRequestException(
|
||||||
.where('category_id = :id', { id })
|
`Ce dossier contient ${apiCount} API(s). Déplacez-les avant de le supprimer.`,
|
||||||
.execute();
|
);
|
||||||
/* Détacher les sous-catégories enfants */
|
}
|
||||||
await this.categoryRepo
|
|
||||||
.createQueryBuilder()
|
/* Vérifier qu'aucun sous-dossier n'existe */
|
||||||
.update(CategoryEntity)
|
const subCount = await this.categoryRepo.count({ where: { parentId: id } });
|
||||||
.set({ parentId: null })
|
if (subCount > 0) {
|
||||||
.where('parent_id = :id', { id })
|
throw new BadRequestException(
|
||||||
.execute();
|
`Ce dossier contient ${subCount} sous-dossier(s). Supprimez-les d'abord.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.categoryRepo.delete(id);
|
await this.categoryRepo.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
101
documentation/files-samples-asyncapi/user-events.yaml
Normal file
101
documentation/files-samples-asyncapi/user-events.yaml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
asyncapi: "3.0.0"
|
||||||
|
info:
|
||||||
|
title: User Events API
|
||||||
|
version: "1.0.0"
|
||||||
|
description: API événementielle pour les actions utilisateurs
|
||||||
|
channels:
|
||||||
|
userSignedUp:
|
||||||
|
address: user.signed.up
|
||||||
|
messages:
|
||||||
|
userSignedUp:
|
||||||
|
$ref: "#/components/messages/UserSignedUp"
|
||||||
|
userLoggedIn:
|
||||||
|
address: user.logged.in
|
||||||
|
messages:
|
||||||
|
userLoggedIn:
|
||||||
|
$ref: "#/components/messages/UserLoggedIn"
|
||||||
|
orderPlaced:
|
||||||
|
address: order.placed
|
||||||
|
messages:
|
||||||
|
orderPlaced:
|
||||||
|
$ref: "#/components/messages/OrderPlaced"
|
||||||
|
operations:
|
||||||
|
onUserSignedUp:
|
||||||
|
action: receive
|
||||||
|
channel:
|
||||||
|
$ref: "#/channels/userSignedUp"
|
||||||
|
onUserLoggedIn:
|
||||||
|
action: receive
|
||||||
|
channel:
|
||||||
|
$ref: "#/channels/userLoggedIn"
|
||||||
|
onOrderPlaced:
|
||||||
|
action: receive
|
||||||
|
channel:
|
||||||
|
$ref: "#/channels/orderPlaced"
|
||||||
|
components:
|
||||||
|
messages:
|
||||||
|
UserSignedUp:
|
||||||
|
name: UserSignedUp
|
||||||
|
title: Utilisateur inscrit
|
||||||
|
summary: Émis lorsqu'un utilisateur crée un compte
|
||||||
|
payload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- userId
|
||||||
|
- email
|
||||||
|
- createdAt
|
||||||
|
properties:
|
||||||
|
userId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
UserLoggedIn:
|
||||||
|
name: UserLoggedIn
|
||||||
|
title: Utilisateur connecté
|
||||||
|
summary: Émis lorsqu'un utilisateur se connecte
|
||||||
|
payload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- userId
|
||||||
|
- loginAt
|
||||||
|
properties:
|
||||||
|
userId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
loginAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
OrderPlaced:
|
||||||
|
name: OrderPlaced
|
||||||
|
title: Commande passée
|
||||||
|
summary: Émis lorsqu'une commande est créée
|
||||||
|
payload:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- orderId
|
||||||
|
- userId
|
||||||
|
- total
|
||||||
|
properties:
|
||||||
|
orderId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
userId:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
total:
|
||||||
|
type: number
|
||||||
|
format: float
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
productId:
|
||||||
|
type: string
|
||||||
|
quantity:
|
||||||
|
type: integer
|
||||||
96
documentation/files-samples-oas/petstore.yaml
Normal file
96
documentation/files-samples-oas/petstore.yaml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
openapi: "3.0.0"
|
||||||
|
info:
|
||||||
|
title: Petstore API
|
||||||
|
version: "1.0.0"
|
||||||
|
description: API de gestion d'une animalerie
|
||||||
|
paths:
|
||||||
|
/pets:
|
||||||
|
get:
|
||||||
|
summary: Liste des animaux
|
||||||
|
operationId: listPets
|
||||||
|
parameters:
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
maximum: 100
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Liste des animaux
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Pet"
|
||||||
|
post:
|
||||||
|
summary: Créer un animal
|
||||||
|
operationId: createPet
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/NewPet"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Animal créé
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Pet"
|
||||||
|
/pets/{id}:
|
||||||
|
get:
|
||||||
|
summary: Récupérer un animal par ID
|
||||||
|
operationId: getPet
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Animal trouvé
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Pet"
|
||||||
|
"404":
|
||||||
|
description: Animal non trouvé
|
||||||
|
delete:
|
||||||
|
summary: Supprimer un animal
|
||||||
|
operationId: deletePet
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Animal supprimé
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Pet:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
NewPet:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
@@ -12,7 +12,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CategoryService } from '../../core/category.service';
|
import { CategoryService } from '../../core/category.service';
|
||||||
import { CategoryBrowseResponse, Category } from '@datacat/shared';
|
import { CategoryBrowseResponse, Category, CategoryWithCounts } from '@datacat/shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-browse',
|
selector: 'app-browse',
|
||||||
@@ -32,15 +32,25 @@ import { CategoryBrowseResponse, Category } from '@datacat/shared';
|
|||||||
<a class="fr-breadcrumb__link" [routerLink]="['/browse', crumb.id]">{{ crumb.name }}</a>
|
<a class="fr-breadcrumb__link" [routerLink]="['/browse', crumb.id]">{{ crumb.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@if (data()?.category) {
|
@if (browseData()?.category) {
|
||||||
<li>
|
<li>
|
||||||
<a class="fr-breadcrumb__link" aria-current="page">{{ data()!.category!.name }}</a>
|
<a class="fr-breadcrumb__link" aria-current="page">{{ browseData()!.category!.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h1 class="fr-h1 fr-mb-4w">{{ data()?.category?.name ?? 'Parcourir le catalogue' }}</h1>
|
<!-- En-tête -->
|
||||||
|
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
||||||
|
<div class="fr-col">
|
||||||
|
<h1 class="fr-h2 fr-mb-0">{{ browseData()?.category?.name ?? 'Catalogue' }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="fr-col-auto">
|
||||||
|
<button class="fr-btn fr-btn--icon-left fr-icon-folder-2-fill" (click)="openCreate()">
|
||||||
|
Nouveau dossier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<div class="fr-callout fr-mb-4w">
|
<div class="fr-callout fr-mb-4w">
|
||||||
@@ -54,12 +64,12 @@ import { CategoryBrowseResponse, Category } from '@datacat/shared';
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!loading() && data()) {
|
@if (!loading() && browseData()) {
|
||||||
<!-- Sous-catégories -->
|
<!-- Sous-catégories -->
|
||||||
@if (data()!.subcategories.length > 0) {
|
@if (browseData()!.subcategories.length > 0) {
|
||||||
<h2 class="fr-h4 fr-mb-3w">Catégories</h2>
|
<h2 class="fr-h4 fr-mb-3w">Catégories</h2>
|
||||||
<div class="fr-grid-row fr-grid-row--gutters fr-mb-4w">
|
<div class="fr-grid-row fr-grid-row--gutters fr-mb-4w">
|
||||||
@for (cat of data()!.subcategories; track cat.id) {
|
@for (cat of browseData()!.subcategories; track cat.id) {
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
<div class="fr-col-12 fr-col-md-4">
|
||||||
<div class="fr-tile fr-enlarge-link fr-tile--sm">
|
<div class="fr-tile fr-enlarge-link fr-tile--sm">
|
||||||
<div class="fr-tile__body">
|
<div class="fr-tile__body">
|
||||||
@@ -73,6 +83,18 @@ import { CategoryBrowseResponse, Category } from '@datacat/shared';
|
|||||||
<p class="fr-tile__detail">
|
<p class="fr-tile__detail">
|
||||||
{{ cat.subcategoryCount }} sous-catégorie(s) · {{ cat.apiCount }} API(s)
|
{{ cat.subcategoryCount }} sous-catégorie(s) · {{ cat.apiCount }} API(s)
|
||||||
</p>
|
</p>
|
||||||
|
<div style="position:relative;z-index:1;margin-top:0.5rem;display:flex;gap:0.25rem;order:5">
|
||||||
|
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-btn--icon-left fr-icon-edit-line"
|
||||||
|
(click)="openEdit(cat); $event.stopPropagation()">
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
@if (cat.apiCount === 0 && cat.subcategoryCount === 0) {
|
||||||
|
<button class="fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-btn--icon-left fr-icon-delete-bin-line"
|
||||||
|
(click)="deleteCategory(cat); $event.stopPropagation()">
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,10 +104,10 @@ import { CategoryBrowseResponse, Category } from '@datacat/shared';
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- APIs -->
|
<!-- APIs -->
|
||||||
@if (data()!.apis.length > 0) {
|
@if (browseData()!.apis.length > 0) {
|
||||||
<h2 class="fr-h4 fr-mb-3w">APIs</h2>
|
<h2 class="fr-h4 fr-mb-3w">APIs</h2>
|
||||||
<div class="fr-grid-row fr-grid-row--gutters">
|
<div class="fr-grid-row fr-grid-row--gutters">
|
||||||
@for (api of data()!.apis; track api.id) {
|
@for (api of browseData()!.apis; track api.id) {
|
||||||
<div class="fr-col-12 fr-col-md-4">
|
<div class="fr-col-12 fr-col-md-4">
|
||||||
<div class="fr-tile fr-enlarge-link fr-tile--sm">
|
<div class="fr-tile fr-enlarge-link fr-tile--sm">
|
||||||
<div class="fr-tile__body">
|
<div class="fr-tile__body">
|
||||||
@@ -109,12 +131,61 @@ import { CategoryBrowseResponse, Category } from '@datacat/shared';
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (data()!.subcategories.length === 0 && data()!.apis.length === 0) {
|
@if (browseData()!.subcategories.length === 0 && browseData()!.apis.length === 0) {
|
||||||
<div class="fr-callout fr-callout--blue-cumulus fr-mb-4w">
|
<div class="fr-callout fr-callout--blue-cumulus fr-mb-4w">
|
||||||
<p class="fr-callout__text">Aucun contenu dans cette catégorie.</p>
|
<p class="fr-callout__text">Aucun contenu dans cette catégorie.</p>
|
||||||
</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>
|
||||||
|
}
|
||||||
|
<div class="fr-input-group fr-mb-2w">
|
||||||
|
<label class="fr-label" for="form-name">
|
||||||
|
Nom <span class="fr-hint-text">Obligatoire</span>
|
||||||
|
</label>
|
||||||
|
<input id="form-name" class="fr-input" type="text"
|
||||||
|
[value]="formName()"
|
||||||
|
(input)="formName.set($any($event.target).value)" />
|
||||||
|
</div>
|
||||||
|
<div class="fr-input-group fr-mb-3w">
|
||||||
|
<label class="fr-label" for="form-desc">
|
||||||
|
Description <span class="fr-hint-text">Optionnel</span>
|
||||||
|
</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>
|
</div>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@@ -123,13 +194,23 @@ export class BrowseComponent implements OnInit {
|
|||||||
private categoryService = inject(CategoryService);
|
private categoryService = inject(CategoryService);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
data = signal<CategoryBrowseResponse | null>(null);
|
browseData = signal<CategoryBrowseResponse | null>(null);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
|
categoryId = signal<string | null>(null);
|
||||||
|
|
||||||
|
/* Signaux pour la modale */
|
||||||
|
showFormModal = signal(false);
|
||||||
|
formMode = signal<'create' | 'edit'>('create');
|
||||||
|
editingCategoryId = signal<string | null>(null);
|
||||||
|
formName = signal('');
|
||||||
|
formDescription = signal('');
|
||||||
|
formError = signal<string | null>(null);
|
||||||
|
formLoading = signal(false);
|
||||||
|
|
||||||
/* Ancêtres = breadcrumb sans le nœud courant (déjà dans le h1) */
|
/* Ancêtres = breadcrumb sans le nœud courant (déjà dans le h1) */
|
||||||
ancestors = computed<Category[]>(() => {
|
ancestors = computed<Category[]>(() => {
|
||||||
const d = this.data();
|
const d = this.browseData();
|
||||||
if (!d || !d.category) return [];
|
if (!d || !d.category) return [];
|
||||||
return d.breadcrumb.slice(0, -1);
|
return d.breadcrumb.slice(0, -1);
|
||||||
});
|
});
|
||||||
@@ -138,16 +219,17 @@ export class BrowseComponent implements OnInit {
|
|||||||
this.route.paramMap
|
this.route.paramMap
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
this.loadBrowse(params.get('id'));
|
this.categoryId.set(params.get('id'));
|
||||||
|
this.loadBrowse();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadBrowse(id: string | null) {
|
loadBrowse() {
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.error.set(null);
|
this.error.set(null);
|
||||||
this.categoryService.browse(id).subscribe({
|
this.categoryService.browse(this.categoryId()).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
this.data.set(data);
|
this.browseData.set(data);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -157,6 +239,69 @@ export class BrowseComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openCreate() {
|
||||||
|
this.formMode.set('create');
|
||||||
|
this.editingCategoryId.set(null);
|
||||||
|
this.formName.set('');
|
||||||
|
this.formDescription.set('');
|
||||||
|
this.formError.set(null);
|
||||||
|
this.showFormModal.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
openEdit(cat: CategoryWithCounts) {
|
||||||
|
this.formMode.set('edit');
|
||||||
|
this.editingCategoryId.set(cat.id);
|
||||||
|
this.formName.set(cat.name);
|
||||||
|
this.formDescription.set(cat.description ?? '');
|
||||||
|
this.formError.set(null);
|
||||||
|
this.showFormModal.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.showFormModal.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitForm() {
|
||||||
|
if (!this.formName().trim()) {
|
||||||
|
this.formError.set('Le nom est obligatoire.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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.categoryId() ?? 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.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCategory(cat: CategoryWithCounts) {
|
||||||
|
if (cat.apiCount > 0 || cat.subcategoryCount > 0) return;
|
||||||
|
if (!confirm(`Supprimer le dossier "${cat.name}" ?`)) return;
|
||||||
|
this.categoryService.delete(cat.id).subscribe({
|
||||||
|
next: () => this.loadBrowse(),
|
||||||
|
error: (err: { error?: { message?: string } }) =>
|
||||||
|
alert(err?.error?.message ?? 'Erreur lors de la suppression.'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
typeBadgeClass(type: string): string {
|
typeBadgeClass(type: string): string {
|
||||||
return type === 'ASYNCAPI'
|
return type === 'ASYNCAPI'
|
||||||
? 'fr-badge fr-badge--sm fr-badge--purple-glycine'
|
? 'fr-badge fr-badge--sm fr-badge--purple-glycine'
|
||||||
|
|||||||
Reference in New Issue
Block a user