- 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
		
			
				
	
	
	
		
			14 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	
			14 KiB
		
	
	
	
	
	
	
	
UrlStateService - Guide d'Intégration Complet
📋 Table des matières
- Vue d'ensemble
- Installation
- Intégration dans les composants
- Exemples d'URL
- Gestion des erreurs
- Cas d'usage avancés
- 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
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
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
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
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
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
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
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
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
// 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
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
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
// 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
// Retour à la vue par défaut
resetToDefault(): void {
  this.urlState.resetState();
}
API Complète
Signaux (Computed)
// É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
// 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
// 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
// Générer une URL partageble
generateShareUrl(state?: Partial<UrlState>): string
// Copier l'URL actuelle
async copyCurrentUrlToClipboard(): Promise<void>
État
// Obtenir l'état actuel
getState(): UrlState
// Obtenir l'état précédent
getPreviousState(): UrlState
Observables
// Observable des changements d'état
stateChange$: Observable<UrlStateChangeEvent>
// Observable des changements d'une propriété
onStatePropertyChange(property: keyof UrlState): Observable<UrlStateChangeEvent>
Types
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 UrlStateServicecréé
- Service injecté dans AppComponent
- NotesListComponentsynchronise les filtres avec l'URL
- NoteViewComponentouvre les notes via URL
- FoldersSidebarComponentsynchronise la sélection avec l'URL
- TagsComponentsynchronise 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:
// ❌ Mauvais
this.currentTag = 'Ideas';
// ✅ Correct
await this.urlState.filterByTag('Ideas');
La note n'est pas trouvée
Solution: Vérifiez le chemin exact:
// 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:
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
- Tests unitaires: Créer des tests pour chaque méthode
- Tests E2E: Tester les flux complets avec Playwright
- Monitoring: Tracker les URLs les plus utilisées
- Analytics: Analyser les patterns de navigation
- Optimisation: Compresser les URLs longues si nécessaire