- Added UrlStateService to sync app state with URL parameters for note selection, tags, folders, and search - Implemented URL state effects in AppComponent to handle navigation from URL parameters - Updated sidebar and layout components to reflect URL state changes in UI - Added URL state updates when navigating via note selection, tag clicks, and search - Modified note sharing to use URL parameters instead of route paths - Added auto-opening of relevant
		
			
				
	
	
		
			621 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			621 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # UrlStateService - Guide d'Intégration Complet
 | |
| 
 | |
| ## 📋 Table des matières
 | |
| 
 | |
| 1. [Vue d'ensemble](#vue-densemble)
 | |
| 2. [Installation](#installation)
 | |
| 3. [Intégration dans les composants](#intégration-dans-les-composants)
 | |
| 4. [Exemples d'URL](#exemples-durl)
 | |
| 5. [Gestion des erreurs](#gestion-des-erreurs)
 | |
| 6. [Cas d'usage avancés](#cas-dusage-avancés)
 | |
| 7. [API Complète](#api-complète)
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Vue d'ensemble
 | |
| 
 | |
| Le `UrlStateService` synchronise l'état de l'interface avec l'URL, permettant:
 | |
| 
 | |
| - ✅ **Deep-linking**: Ouvrir une note directement via URL
 | |
| - ✅ **Partage de liens**: Générer des URLs partageables
 | |
| - ✅ **Restauration d'état**: Retrouver l'état après rechargement
 | |
| - ✅ **Filtrage persistant**: Tags, dossiers, quick links via URL
 | |
| - ✅ **Recherche persistante**: Termes de recherche dans l'URL
 | |
| 
 | |
| ### Architecture
 | |
| 
 | |
| ```
 | |
| URL (query params)
 | |
|     ↓
 | |
| UrlStateService (parsing + validation)
 | |
|     ↓
 | |
| Angular Signals (currentState, activeTag, etc.)
 | |
|     ↓
 | |
| Composants (NotesListComponent, NoteViewComponent, etc.)
 | |
| ```
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Installation
 | |
| 
 | |
| ### 1. Service déjà créé
 | |
| 
 | |
| Le service est disponible à:
 | |
| ```
 | |
| src/app/services/url-state.service.ts
 | |
| ```
 | |
| 
 | |
| ### 2. Injection dans AppComponent
 | |
| 
 | |
| ```typescript
 | |
| import { Component, inject } from '@angular/core';
 | |
| import { UrlStateService } from './services/url-state.service';
 | |
| 
 | |
| @Component({
 | |
|   selector: 'app-root',
 | |
|   standalone: true,
 | |
|   template: `...`
 | |
| })
 | |
| export class AppComponent {
 | |
|   private urlStateService = inject(UrlStateService);
 | |
|   
 | |
|   // Le service est automatiquement initialisé et écoute les changements d'URL
 | |
| }
 | |
| ```
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Intégration dans les composants
 | |
| 
 | |
| ### NotesListComponent - Synchroniser les filtres avec l'URL
 | |
| 
 | |
| ```typescript
 | |
| import { Component, inject, effect } from '@angular/core';
 | |
| import { UrlStateService } from '../../services/url-state.service';
 | |
| 
 | |
| @Component({
 | |
|   selector: 'app-notes-list',
 | |
|   standalone: true,
 | |
|   template: `...`
 | |
| })
 | |
| export class NotesListComponent {
 | |
|   private urlState = inject(UrlStateService);
 | |
|   
 | |
|   // Signaux dérivés de l'URL
 | |
|   activeTag = this.urlState.activeTag;
 | |
|   activeFolder = this.urlState.activeFolder;
 | |
|   activeQuickLink = this.urlState.activeQuickLink;
 | |
|   activeSearch = this.urlState.activeSearch;
 | |
|   
 | |
|   constructor() {
 | |
|     // Écouter les changements d'état
 | |
|     effect(() => {
 | |
|       const state = this.urlState.currentState();
 | |
|       
 | |
|       // Réagir aux changements
 | |
|       if (state.tag) {
 | |
|         console.log('Tag filter applied:', state.tag);
 | |
|         this.applyTagFilter(state.tag);
 | |
|       }
 | |
|       
 | |
|       if (state.folder) {
 | |
|         console.log('Folder filter applied:', state.folder);
 | |
|         this.applyFolderFilter(state.folder);
 | |
|       }
 | |
|       
 | |
|       if (state.search) {
 | |
|         console.log('Search applied:', state.search);
 | |
|         this.applySearch(state.search);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   // Mettre à jour l'URL quand l'utilisateur change de vue
 | |
|   onTagClick(tag: string): void {
 | |
|     this.urlState.filterByTag(tag);
 | |
|   }
 | |
|   
 | |
|   onFolderClick(folder: string): void {
 | |
|     this.urlState.filterByFolder(folder);
 | |
|   }
 | |
|   
 | |
|   onQuickLinkClick(quickLink: string): void {
 | |
|     this.urlState.filterByQuickLink(quickLink);
 | |
|   }
 | |
|   
 | |
|   onSearch(searchTerm: string): void {
 | |
|     this.urlState.updateSearch(searchTerm);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### NoteViewComponent - Ouvrir une note via URL
 | |
| 
 | |
| ```typescript
 | |
| import { Component, inject, effect } from '@angular/core';
 | |
| import { UrlStateService } from '../../services/url-state.service';
 | |
| import { VaultService } from '../../../services/vault.service';
 | |
| 
 | |
| @Component({
 | |
|   selector: 'app-note-view',
 | |
|   standalone: true,
 | |
|   template: `
 | |
|     <div *ngIf="currentNote() as note" class="note-view">
 | |
|       <h1>{{ note.title }}</h1>
 | |
|       <div [innerHTML]="note.content"></div>
 | |
|     </div>
 | |
|   `
 | |
| })
 | |
| export class NoteViewComponent {
 | |
|   private urlState = inject(UrlStateService);
 | |
|   private vault = inject(VaultService);
 | |
|   
 | |
|   // Signal de la note actuelle depuis l'URL
 | |
|   currentNote = this.urlState.currentNote;
 | |
|   
 | |
|   constructor() {
 | |
|     // Charger la note quand l'URL change
 | |
|     effect(async () => {
 | |
|       const note = this.currentNote();
 | |
|       if (note) {
 | |
|         // Charger le contenu complet si nécessaire
 | |
|         await this.vault.ensureNoteLoadedByPath(note.filePath);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   // Ouvrir une note en mettant à jour l'URL
 | |
|   openNote(notePath: string): void {
 | |
|     this.urlState.openNote(notePath);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### FoldersSidebarComponent - Synchroniser la sélection avec l'URL
 | |
| 
 | |
| ```typescript
 | |
| import { Component, inject } from '@angular/core';
 | |
| import { UrlStateService } from '../../services/url-state.service';
 | |
| 
 | |
| @Component({
 | |
|   selector: 'app-folders-sidebar',
 | |
|   standalone: true,
 | |
|   template: `
 | |
|     <div class="folders-list">
 | |
|       <button *ngFor="let folder of folders"
 | |
|               [class.active]="urlState.isFolderActive(folder.path)"
 | |
|               (click)="selectFolder(folder.path)">
 | |
|         {{ folder.name }}
 | |
|       </button>
 | |
|     </div>
 | |
|   `
 | |
| })
 | |
| export class FoldersSidebarComponent {
 | |
|   urlState = inject(UrlStateService);
 | |
|   
 | |
|   selectFolder(folderPath: string): void {
 | |
|     this.urlState.filterByFolder(folderPath);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### TagsComponent - Synchroniser les tags avec l'URL
 | |
| 
 | |
| ```typescript
 | |
| import { Component, inject } from '@angular/core';
 | |
| import { UrlStateService } from '../../services/url-state.service';
 | |
| 
 | |
| @Component({
 | |
|   selector: 'app-tags-view',
 | |
|   standalone: true,
 | |
|   template: `
 | |
|     <div class="tags-list">
 | |
|       <button *ngFor="let tag of tags"
 | |
|               [class.active]="urlState.isTagActive(tag.name)"
 | |
|               (click)="selectTag(tag.name)">
 | |
|         #{{ tag.name }}
 | |
|       </button>
 | |
|     </div>
 | |
|   `
 | |
| })
 | |
| export class TagsComponent {
 | |
|   urlState = inject(UrlStateService);
 | |
|   
 | |
|   selectTag(tagName: string): void {
 | |
|     this.urlState.filterByTag(tagName);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Exemples d'URL
 | |
| 
 | |
| ### 1. Ouvrir une note spécifique
 | |
| 
 | |
| ```
 | |
| https://app.example.com/viewer?note=Docs/Architecture.md
 | |
| ```
 | |
| 
 | |
| **Résultat**: Ouvre la note `Docs/Architecture.md` dans la vue note
 | |
| 
 | |
| ### 2. Filtrer par tag
 | |
| 
 | |
| ```
 | |
| https://app.example.com/viewer?tag=Ideas
 | |
| ```
 | |
| 
 | |
| **Résultat**: Affiche toutes les notes avec le tag `Ideas`
 | |
| 
 | |
| ### 3. Filtrer par dossier
 | |
| 
 | |
| ```
 | |
| https://app.example.com/viewer?folder=Notes/Meetings
 | |
| ```
 | |
| 
 | |
| **Résultat**: Affiche toutes les notes du dossier `Notes/Meetings`
 | |
| 
 | |
| ### 4. Afficher un quick link
 | |
| 
 | |
| ```
 | |
| https://app.example.com/viewer?quick=Favoris
 | |
| ```
 | |
| 
 | |
| **Résultat**: Affiche les notes marquées comme favoris
 | |
| 
 | |
| ### 5. Rechercher
 | |
| 
 | |
| ```
 | |
| https://app.example.com/viewer?search=performance
 | |
| ```
 | |
| 
 | |
| **Résultat**: Affiche les résultats de recherche pour "performance"
 | |
| 
 | |
| ### 6. Combinaisons
 | |
| 
 | |
| ```
 | |
| https://app.example.com/viewer?note=Docs/Architecture.md&search=performance
 | |
| ```
 | |
| 
 | |
| **Résultat**: Ouvre la note et met en surbrillance les occurrences de "performance"
 | |
| 
 | |
| ```
 | |
| https://app.example.com/viewer?folder=Notes/Meetings&tag=Important
 | |
| ```
 | |
| 
 | |
| **Résultat**: Affiche les notes du dossier `Notes/Meetings` avec le tag `Important`
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Gestion des erreurs
 | |
| 
 | |
| ### Note introuvable
 | |
| 
 | |
| ```typescript
 | |
| async openNote(notePath: string): Promise<void> {
 | |
|   try {
 | |
|     await this.urlState.openNote(notePath);
 | |
|   } catch (error) {
 | |
|     console.error('Note not found:', notePath);
 | |
|     // Afficher un message d'erreur à l'utilisateur
 | |
|     this.toast.error(`Note introuvable: ${notePath}`);
 | |
|     // Réinitialiser l'état
 | |
|     this.urlState.resetState();
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Tag inexistant
 | |
| 
 | |
| ```typescript
 | |
| async filterByTag(tag: string): Promise<void> {
 | |
|   try {
 | |
|     await this.urlState.filterByTag(tag);
 | |
|   } catch (error) {
 | |
|     console.error('Tag not found:', tag);
 | |
|     this.toast.warning(`Tag inexistant: ${tag}`);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Dossier inexistant
 | |
| 
 | |
| ```typescript
 | |
| async filterByFolder(folder: string): Promise<void> {
 | |
|   try {
 | |
|     await this.urlState.filterByFolder(folder);
 | |
|   } catch (error) {
 | |
|     console.error('Folder not found:', folder);
 | |
|     this.toast.warning(`Dossier inexistant: ${folder}`);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Cas d'usage avancés
 | |
| 
 | |
| ### 1. Générer un lien de partage
 | |
| 
 | |
| ```typescript
 | |
| // Copier l'URL actuelle
 | |
| async shareCurrentState(): Promise<void> {
 | |
|   try {
 | |
|     await this.urlState.copyCurrentUrlToClipboard();
 | |
|     this.toast.success('Lien copié!');
 | |
|   } catch (error) {
 | |
|     this.toast.error('Erreur lors de la copie');
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Générer une URL personnalisée
 | |
| generateShareUrl(note: Note): string {
 | |
|   return this.urlState.generateShareUrl({ note: note.filePath });
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### 2. Écouter les changements d'état
 | |
| 
 | |
| ```typescript
 | |
| constructor() {
 | |
|   // Écouter tous les changements
 | |
|   this.urlState.stateChange$.subscribe(event => {
 | |
|     console.log('État précédent:', event.previous);
 | |
|     console.log('Nouvel état:', event.current);
 | |
|     console.log('Propriétés changées:', event.changed);
 | |
|   });
 | |
|   
 | |
|   // Écouter un changement spécifique
 | |
|   this.urlState.onStatePropertyChange('note').subscribe(event => {
 | |
|     console.log('Note changée:', event.current.note);
 | |
|   });
 | |
|   
 | |
|   this.urlState.onStatePropertyChange('tag').subscribe(event => {
 | |
|     console.log('Tag changé:', event.current.tag);
 | |
|   });
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### 3. Restaurer l'état après rechargement
 | |
| 
 | |
| ```typescript
 | |
| constructor() {
 | |
|   // L'état est automatiquement restauré depuis l'URL
 | |
|   effect(() => {
 | |
|     const state = this.urlState.currentState();
 | |
|     
 | |
|     // Restaurer la vue
 | |
|     if (state.note) {
 | |
|       this.openNote(state.note);
 | |
|     } else if (state.tag) {
 | |
|       this.filterByTag(state.tag);
 | |
|     } else if (state.folder) {
 | |
|       this.filterByFolder(state.folder);
 | |
|     } else if (state.quick) {
 | |
|       this.filterByQuickLink(state.quick);
 | |
|     }
 | |
|   });
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### 4. Historique de navigation
 | |
| 
 | |
| ```typescript
 | |
| // Obtenir l'état précédent
 | |
| const previousState = this.urlState.getPreviousState();
 | |
| 
 | |
| // Revenir à l'état précédent
 | |
| if (previousState.note) {
 | |
|   this.urlState.openNote(previousState.note);
 | |
| } else if (previousState.tag) {
 | |
|   this.urlState.filterByTag(previousState.tag);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### 5. Réinitialiser l'état
 | |
| 
 | |
| ```typescript
 | |
| // Retour à la vue par défaut
 | |
| resetToDefault(): void {
 | |
|   this.urlState.resetState();
 | |
| }
 | |
| ```
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## API Complète
 | |
| 
 | |
| ### Signaux (Computed)
 | |
| 
 | |
| ```typescript
 | |
| // État actuel
 | |
| currentState: Signal<UrlState>
 | |
| 
 | |
| // État précédent
 | |
| previousState: Signal<UrlState>
 | |
| 
 | |
| // Note actuellement ouverte
 | |
| currentNote: Signal<Note | null>
 | |
| 
 | |
| // Tag actif
 | |
| activeTag: Signal<string | null>
 | |
| 
 | |
| // Dossier actif
 | |
| activeFolder: Signal<string | null>
 | |
| 
 | |
| // Quick link actif
 | |
| activeQuickLink: Signal<string | null>
 | |
| 
 | |
| // Terme de recherche actif
 | |
| activeSearch: Signal<string | null>
 | |
| ```
 | |
| 
 | |
| ### Méthodes
 | |
| 
 | |
| #### Navigation
 | |
| 
 | |
| ```typescript
 | |
| // Ouvrir une note
 | |
| async openNote(notePath: string): Promise<void>
 | |
| 
 | |
| // Filtrer par tag
 | |
| async filterByTag(tag: string): Promise<void>
 | |
| 
 | |
| // Filtrer par dossier
 | |
| async filterByFolder(folder: string): Promise<void>
 | |
| 
 | |
| // Filtrer par quick link
 | |
| async filterByQuickLink(quickLink: string): Promise<void>
 | |
| 
 | |
| // Mettre à jour la recherche
 | |
| async updateSearch(searchTerm: string): Promise<void>
 | |
| 
 | |
| // Réinitialiser l'état
 | |
| async resetState(): Promise<void>
 | |
| ```
 | |
| 
 | |
| #### Vérification
 | |
| 
 | |
| ```typescript
 | |
| // Vérifier si une note est ouverte
 | |
| isNoteOpen(notePath: string): boolean
 | |
| 
 | |
| // Vérifier si un tag est actif
 | |
| isTagActive(tag: string): boolean
 | |
| 
 | |
| // Vérifier si un dossier est actif
 | |
| isFolderActive(folder: string): boolean
 | |
| 
 | |
| // Vérifier si un quick link est actif
 | |
| isQuickLinkActive(quickLink: string): boolean
 | |
| ```
 | |
| 
 | |
| #### Partage
 | |
| 
 | |
| ```typescript
 | |
| // Générer une URL partageble
 | |
| generateShareUrl(state?: Partial<UrlState>): string
 | |
| 
 | |
| // Copier l'URL actuelle
 | |
| async copyCurrentUrlToClipboard(): Promise<void>
 | |
| ```
 | |
| 
 | |
| #### État
 | |
| 
 | |
| ```typescript
 | |
| // Obtenir l'état actuel
 | |
| getState(): UrlState
 | |
| 
 | |
| // Obtenir l'état précédent
 | |
| getPreviousState(): UrlState
 | |
| ```
 | |
| 
 | |
| ### Observables
 | |
| 
 | |
| ```typescript
 | |
| // Observable des changements d'état
 | |
| stateChange$: Observable<UrlStateChangeEvent>
 | |
| 
 | |
| // Observable des changements d'une propriété
 | |
| onStatePropertyChange(property: keyof UrlState): Observable<UrlStateChangeEvent>
 | |
| ```
 | |
| 
 | |
| ### Types
 | |
| 
 | |
| ```typescript
 | |
| interface UrlState {
 | |
|   note?: string;        // Chemin de la note
 | |
|   tag?: string;         // Tag de filtrage
 | |
|   folder?: string;      // Dossier de filtrage
 | |
|   quick?: string;       // Quick link de filtrage
 | |
|   search?: string;      // Terme de recherche
 | |
| }
 | |
| 
 | |
| interface UrlStateChangeEvent {
 | |
|   previous: UrlState;
 | |
|   current: UrlState;
 | |
|   changed: (keyof UrlState)[];
 | |
| }
 | |
| ```
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Checklist d'intégration
 | |
| 
 | |
| - [ ] Service `UrlStateService` créé
 | |
| - [ ] Service injecté dans `AppComponent`
 | |
| - [ ] `NotesListComponent` synchronise les filtres avec l'URL
 | |
| - [ ] `NoteViewComponent` ouvre les notes via URL
 | |
| - [ ] `FoldersSidebarComponent` synchronise la sélection avec l'URL
 | |
| - [ ] `TagsComponent` synchronise les tags avec l'URL
 | |
| - [ ] Gestion des erreurs implémentée
 | |
| - [ ] Tests unitaires écrits
 | |
| - [ ] Documentation mise à jour
 | |
| - [ ] Déploiement en production
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Troubleshooting
 | |
| 
 | |
| ### L'URL ne change pas quand je change de vue
 | |
| 
 | |
| **Solution**: Vérifiez que vous appelez les méthodes du service:
 | |
| ```typescript
 | |
| // ❌ Mauvais
 | |
| this.currentTag = 'Ideas';
 | |
| 
 | |
| // ✅ Correct
 | |
| await this.urlState.filterByTag('Ideas');
 | |
| ```
 | |
| 
 | |
| ### La note n'est pas trouvée
 | |
| 
 | |
| **Solution**: Vérifiez le chemin exact:
 | |
| ```typescript
 | |
| // Afficher tous les chemins disponibles
 | |
| console.log(this.vault.allNotes().map(n => n.filePath));
 | |
| 
 | |
| // Utiliser le bon chemin
 | |
| await this.urlState.openNote('Docs/Architecture.md');
 | |
| ```
 | |
| 
 | |
| ### L'état n'est pas restauré après rechargement
 | |
| 
 | |
| **Solution**: Assurez-vous que le service est injecté dans `AppComponent`:
 | |
| ```typescript
 | |
| export class AppComponent {
 | |
|   private urlStateService = inject(UrlStateService);
 | |
|   // Le service s'initialise automatiquement
 | |
| }
 | |
| ```
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Performance
 | |
| 
 | |
| - ✅ Utilise Angular Signals pour les mises à jour réactives
 | |
| - ✅ Pas de polling, écoute les changements d'URL natifs
 | |
| - ✅ Décodage/encodage URI optimisé
 | |
| - ✅ Gestion automatique du cycle de vie
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Sécurité
 | |
| 
 | |
| - ✅ Validation des chemins de notes
 | |
| - ✅ Validation des tags existants
 | |
| - ✅ Validation des dossiers existants
 | |
| - ✅ Encodage URI pour les caractères spéciaux
 | |
| - ✅ Pas d'exécution de code depuis l'URL
 | |
| 
 | |
| ---
 | |
| 
 | |
| ## Prochaines étapes
 | |
| 
 | |
| 1. **Tests unitaires**: Créer des tests pour chaque méthode
 | |
| 2. **Tests E2E**: Tester les flux complets avec Playwright
 | |
| 3. **Monitoring**: Tracker les URLs les plus utilisées
 | |
| 4. **Analytics**: Analyser les patterns de navigation
 | |
| 5. **Optimisation**: Compresser les URLs longues si nécessaire
 | |
| 
 |