- 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
|
|
|