front: composant <app-breadcrumb> mutualisé (dédup markup DSFR)

Extrait le fil d'Ariane DSFR dupliqué dans browse/api-detail/upload vers un
composant présentationnel piloté par une liste d'items. Chaque page construit
ses items (computed pour browse/api-detail, statique pour upload).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
z3n
2026-06-28 21:17:18 +00:00
parent 15e05cc46c
commit f64efda8a3
4 changed files with 71 additions and 48 deletions

View File

@@ -14,34 +14,17 @@ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ApiService } from '../../core/api.service';
import { CategoryService } from '../../core/category.service';
import { BreadcrumbComponent, BreadcrumbItem } from '../../shared/breadcrumb/breadcrumb.component';
import { ApiEntry, ApiEntryListItem, Category } from '@datacat/shared';
@Component({
selector: 'app-api-detail',
standalone: true,
imports: [CommonModule, RouterLink, ReactiveFormsModule],
imports: [CommonModule, RouterLink, ReactiveFormsModule, BreadcrumbComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="fr-container fr-mt-4w fr-mb-4w">
<!-- Fil d'Ariane -->
<nav class="fr-breadcrumb fr-mb-3w" aria-label="vous êtes ici">
<ol class="fr-breadcrumb__list">
<li>
<a class="fr-breadcrumb__link" routerLink="/">Accueil</a>
</li>
@if (categoryBreadcrumb().length === 0) {
<li><a class="fr-breadcrumb__link" routerLink="/browse">Parcourir</a></li>
}
@for (crumb of categoryBreadcrumb(); track crumb.id) {
<li><a class="fr-breadcrumb__link" [routerLink]="['/browse', crumb.id]">{{ crumb.name }}</a></li>
}
<li>
<a class="fr-breadcrumb__link" aria-current="page">
{{ api()?.title ?? 'Chargement...' }}
</a>
</li>
</ol>
</nav>
<app-breadcrumb [items]="breadcrumbItems()" />
@if (loading()) {
<div class="fr-callout fr-mb-4w">
@@ -436,6 +419,15 @@ export class ApiDetailComponent implements OnInit, OnDestroy {
return ['/browse'];
});
breadcrumbItems = computed<BreadcrumbItem[]>(() => {
const crumbs = this.categoryBreadcrumb();
const items: BreadcrumbItem[] = [{ label: 'Accueil', link: '/' }];
if (crumbs.length === 0) items.push({ label: 'Parcourir', link: '/browse' });
for (const crumb of crumbs) items.push({ label: crumb.name, link: ['/browse', crumb.id] });
items.push({ label: this.api()?.title ?? 'Chargement...' });
return items;
});
/* Onglets */
activeTab = signal<'info' | 'contacts'>('info');
editingInfo = signal(false);

View File

@@ -17,32 +17,16 @@ import { CategoryBrowseResponse, Category, CategoryWithCounts, ApiEntryListItem
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';
import { BreadcrumbComponent, BreadcrumbItem } from '../../shared/breadcrumb/breadcrumb.component';
@Component({
selector: 'app-browse',
standalone: true,
imports: [CommonModule, RouterLink, CategoryFormModalComponent, CategoryDeleteModalComponent],
imports: [CommonModule, RouterLink, CategoryFormModalComponent, CategoryDeleteModalComponent, BreadcrumbComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="fr-container fr-mt-4w fr-mb-4w">
<!-- Fil d'Ariane -->
<nav class="fr-breadcrumb fr-mb-3w" aria-label="vous êtes ici">
<ol class="fr-breadcrumb__list">
<li>
<a class="fr-breadcrumb__link" routerLink="/">Accueil</a>
</li>
@for (crumb of ancestors(); track crumb.id) {
<li>
<a class="fr-breadcrumb__link" [routerLink]="['/browse', crumb.id]">{{ crumb.name }}</a>
</li>
}
@if (browseData()?.category) {
<li>
<a class="fr-breadcrumb__link" aria-current="page">{{ browseData()!.category!.name }}</a>
</li>
}
</ol>
</nav>
<app-breadcrumb [items]="breadcrumbItems()" />
<!-- En-tête -->
@if (categoryId()) {
@@ -309,6 +293,16 @@ export class BrowseComponent implements OnInit {
return d.breadcrumb.slice(0, -1);
});
breadcrumbItems = computed<BreadcrumbItem[]>(() => {
const items: BreadcrumbItem[] = [{ label: 'Accueil', link: '/' }];
for (const crumb of this.ancestors()) {
items.push({ label: crumb.name, link: ['/browse', crumb.id] });
}
const current = this.browseData()?.category;
if (current) items.push({ label: current.name });
return items;
});
backUrl = computed<string[]>(() => {
const anc = this.ancestors();
if (anc.length > 0) return ['/browse', anc[anc.length - 1].id];

View File

@@ -13,23 +13,17 @@ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { Router, RouterLink, ActivatedRoute } from '@angular/router';
import { ApiService } from '../../core/api.service';
import { CategoryService } from '../../core/category.service';
import { BreadcrumbComponent, BreadcrumbItem } from '../../shared/breadcrumb/breadcrumb.component';
import { ApiEntry, ApiEntryListItem, Category, API_CONVENTION_LABELS, ApiConvention } from '@datacat/shared';
@Component({
selector: 'app-upload',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink],
imports: [CommonModule, ReactiveFormsModule, RouterLink, BreadcrumbComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="fr-container fr-mt-4w fr-mb-4w">
<!-- Fil d'Ariane -->
<nav class="fr-breadcrumb fr-mb-3w" aria-label="vous êtes ici">
<ol class="fr-breadcrumb__list">
<li><a class="fr-breadcrumb__link" routerLink="/browse">Parcourir</a></li>
<li><a class="fr-breadcrumb__link" routerLink="/catalog">Catalogue</a></li>
<li><a class="fr-breadcrumb__link" aria-current="page">Importer une API</a></li>
</ol>
</nav>
<app-breadcrumb [items]="breadcrumbItems" />
<h1 class="fr-h2 fr-mb-4w">Importer une API</h1>
@@ -511,6 +505,12 @@ export class UploadComponent implements OnInit {
private destroyRef = inject(DestroyRef);
step = signal(1);
readonly breadcrumbItems: BreadcrumbItem[] = [
{ label: 'Parcourir', link: '/browse' },
{ label: 'Catalogue', link: '/catalog' },
{ label: 'Importer une API' },
];
private readonly maxFileSize = 5 * 1024 * 1024; // 5 Mo
selectedFiles = signal<File[]>([]);
detectedType = signal<'ASYNCAPI' | 'OPENAPI' | null>(null);

View File

@@ -0,0 +1,37 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
/** Un maillon du fil d'Ariane. `link` absent => élément courant (aria-current). */
export interface BreadcrumbItem {
label: string;
link?: string | unknown[];
}
/**
* Fil d'Ariane DSFR générique (présentationnel). Chaque page construit sa liste
* d'items et la passe ; le dernier item sans `link` est marqué comme courant.
*/
@Component({
selector: 'app-breadcrumb',
standalone: true,
imports: [RouterLink],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nav class="fr-breadcrumb fr-mb-3w" aria-label="vous êtes ici">
<ol class="fr-breadcrumb__list">
@for (item of items(); track $index) {
<li>
@if (item.link) {
<a class="fr-breadcrumb__link" [routerLink]="item.link">{{ item.label }}</a>
} @else {
<a class="fr-breadcrumb__link" aria-current="page">{{ item.label }}</a>
}
</li>
}
</ol>
</nav>
`,
})
export class BreadcrumbComponent {
items = input.required<BreadcrumbItem[]>();
}