From cbdb000d4bdac2923ed91be44f6f1b3a7a8f49b7 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 30 Oct 2025 21:34:45 -0400 Subject: [PATCH] feat: enhance navigation and URL state management - Added clickable "Quick Links" header to navigate to all pages view - Implemented URL normalization to handle multiple section params (tag/folder/quick) with priority rules - Added suppressNextNoteSelection flag to prevent auto-selection for certain quick link actions - Updated URL state service to use setQuickWithMarkdown for consistent navigation state - Improved path normalization to handle backslashes and leading slashes consistently - Added distinct --- .../sidebar/nimbus-sidebar.component.ts | 17 ++-- .../app-shell-nimbus.component.ts | 54 +++++++++--- src/app/services/url-state.service.ts | 87 ++++++++++++++----- vault/titi/Nouveau-markdown.md | 58 +++++++++++++ .../Nouveau-markdown.md.bak} | 0 5 files changed, 177 insertions(+), 39 deletions(-) create mode 100644 vault/titi/Nouveau-markdown.md rename vault/{Nouveau-markdown.md => titi/Nouveau-markdown.md.bak} (100%) diff --git a/src/app/features/sidebar/nimbus-sidebar.component.ts b/src/app/features/sidebar/nimbus-sidebar.component.ts index b43ff8c..f4c6337 100644 --- a/src/app/features/sidebar/nimbus-sidebar.component.ts +++ b/src/app/features/sidebar/nimbus-sidebar.component.ts @@ -62,7 +62,7 @@ import { FilterService } from '../../services/filter.service'; {{ open.quick ? '▾' : '▸' }} - Quick Links + Quick Links
@@ -241,11 +241,16 @@ export class NimbusSidebarComponent implements OnChanges { onHomeClick(event: MouseEvent): void { event.preventDefault(); - this.toggleSection('quick'); - this.quickLinkSelected.emit('all'); - queueMicrotask(async () => { - await this.urlState.filterByKind('markdown'); - }); + this.open = { quick: true, folders: false, tags: false, trash: false, tests: false }; + this.sidebar.open('quick'); + void this.urlState.setQuickWithMarkdown('all'); + } + + onQuickLinksHeaderClick(event: MouseEvent): void { + event.preventDefault(); + this.open = { quick: true, folders: false, tags: false, trash: false, tests: false }; + this.sidebar.open('quick'); + void this.urlState.setQuickWithMarkdown('all'); } onMarkdownPlaygroundClick(): void { diff --git a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts index 8350884..646b088 100644 --- a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts +++ b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts @@ -395,6 +395,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { private flyoutCloseTimer: any = null; tagFilter: string | null = null; quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null = null; + private suppressNextNoteSelection = false; // --- URL State <-> Layout sync --- private mapUrlQuickToInternal(q: string | null): AppShellNimbusLayoutComponent['quickLinkFilter'] { @@ -511,14 +512,24 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.quickLinkFilter = internal; this.folderFilter = null; this.tagFilter = null; - if (!hasNote) this.autoSelectFirstNote(); - if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list'); + if (internal === 'favoris') { + this.suppressNextNoteSelection = true; + } + if (!hasNote && !this.suppressNextNoteSelection) { + this.autoSelectFirstNote(); + } + if (!this.responsive.isDesktop()) { + this.mobileNav.setActiveTab('list'); + } + } else if (!hasNote && !this.suppressNextNoteSelection) { + this.autoSelectFirstNote(); } // Auto-open quick flyout when quick filter is active if (this.hoveredFlyout !== 'quick') { console.log('🎨 Layout - opening quick flyout for quick filter'); this.openFlyout('quick'); } + this.suppressNextNoteSelection = false; } else { // No filters -> show all if (this.folderFilter || this.tagFilter || this.quickLinkFilter) { @@ -527,6 +538,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.quickLinkFilter = null; if (!hasNote) this.autoSelectFirstNote(); } + this.suppressNextNoteSelection = false; // Close any open flyout when no filters if (this.hoveredFlyout) { console.log('🎨 Layout - closing flyout (no active filters)'); @@ -722,6 +734,9 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } onQuickLink(_id: string) { + const suppressAutoSelect = _id === 'all' || _id === 'favorites'; + this.suppressNextNoteSelection = suppressAutoSelect; + if (_id === 'all') { // Show all pages: clear filters and focus list this.folderFilter = null; @@ -735,7 +750,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { this.mobileNav.setActiveTab('list'); } this.scheduleCloseFlyout(150); - this.urlState.showAllAndReset(); + this.urlState.setQuickWithMarkdown('all'); } else if (_id === 'publish') { // Filter by publish: true this.folderFilter = null; @@ -749,7 +764,9 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } this.scheduleCloseFlyout(150); const label = this.mapInternalQuickToUrl('publish'); - if (label) this.urlState.filterByQuickLink(label); + if (label) { + this.urlState.setQuickWithMarkdown(label); + } } else if (_id === 'favorites') { // Filter by favoris: true this.folderFilter = null; @@ -763,7 +780,9 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } this.scheduleCloseFlyout(150); const label = this.mapInternalQuickToUrl('favoris'); - if (label) this.urlState.filterByQuickLink(label); + if (label) { + this.urlState.setQuickWithMarkdown(label); + } } else if (_id === 'templates') { // Filter by template: true this.folderFilter = null; @@ -777,7 +796,9 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } this.scheduleCloseFlyout(150); const label = this.mapInternalQuickToUrl('template'); - if (label) this.urlState.filterByQuickLink(label); + if (label) { + this.urlState.setQuickWithMarkdown(label); + } } else if (_id === 'tasks') { // Filter by task: true this.folderFilter = null; @@ -791,7 +812,9 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } this.scheduleCloseFlyout(150); const label = this.mapInternalQuickToUrl('task'); - if (label) this.urlState.filterByQuickLink(label); + if (label) { + this.urlState.setQuickWithMarkdown(label); + } } else if (_id === 'drafts') { // Filter by draft: true this.folderFilter = null; @@ -805,7 +828,9 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } this.scheduleCloseFlyout(150); const label = this.mapInternalQuickToUrl('draft'); - if (label) this.urlState.filterByQuickLink(label); + if (label) { + this.urlState.setQuickWithMarkdown(label); + } } else if (_id === 'private') { // Filter by private: true this.folderFilter = null; @@ -819,7 +844,9 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } this.scheduleCloseFlyout(150); const label = this.mapInternalQuickToUrl('private'); - if (label) this.urlState.filterByQuickLink(label); + if (label) { + this.urlState.setQuickWithMarkdown(label); + } } else if (_id === 'archive') { // Filter by archive: true this.folderFilter = null; @@ -833,10 +860,15 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit { } this.scheduleCloseFlyout(150); const label = this.mapInternalQuickToUrl('archive'); - if (label) this.urlState.filterByQuickLink(label); + if (label) { + this.urlState.setQuickWithMarkdown(label); + } } // Auto-select first note after filter changes - this.autoSelectFirstNote(); + if (!this.suppressNextNoteSelection) { + this.autoSelectFirstNote(); + } + this.suppressNextNoteSelection = false; } onTagSelected(tagName: string) { diff --git a/src/app/services/url-state.service.ts b/src/app/services/url-state.service.ts index ce33ab9..5c97556 100644 --- a/src/app/services/url-state.service.ts +++ b/src/app/services/url-state.service.ts @@ -1,7 +1,7 @@ import { Injectable, inject, signal, effect, computed, OnDestroy } from '@angular/core'; import { Router, NavigationEnd } from '@angular/router'; import { VaultService } from '../../services/vault.service'; -import { filter, takeUntil, map } from 'rxjs/operators'; +import { filter, takeUntil, map, distinctUntilChanged } from 'rxjs/operators'; import { startWith } from 'rxjs'; import { Subject } from 'rxjs'; import type { Note } from '../../types'; @@ -168,6 +168,22 @@ export class UrlStateService implements OnDestroy { console.log('🌐 UrlStateService - new state:', newState); const previousState = this.currentStateSignal(); + // Normalize when multiple section params (tag/folder/quick) coexist in the raw URL + // Keep only the active one determined by priority, preserve note/search/kind + const rawHasMultiple = (Number(!!params['tag']) + Number(!!params['folder']) + Number(!!params['quick'])) > 1; + if (rawHasMultiple) { + setTimeout(() => { + const active = this.getActiveSection(newState); + if (active) { + this.updateUrl({ + tag: active === 'tag' ? newState.tag! : null, + folder: active === 'folder' ? newState.folder! : null, + quick: active === 'quick' ? newState.quick! : null, + }); + } + }, 0); + } + const changed = this.detectChanges(previousState, newState); console.log('🌐 UrlStateService - changed keys:', changed); if (changed.length > 0) { @@ -208,6 +224,7 @@ export class UrlStateService implements OnDestroy { ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); + this.stateChangeSubject.complete(); } // ======================================== @@ -320,15 +337,16 @@ export class UrlStateService implements OnDestroy { return; } - const note = this.vaultService.allNotes().find(n => n.filePath === trimmed); + const normalized = trimmed.replace(/\\/g, '/').replace(/^\/+/, ''); + const note = this.vaultService.allNotes().find(n => n.filePath === normalized); if (!note && !options?.force) { - console.warn(`Note not found: ${trimmed}`); + console.warn(`Note not found: ${normalized}`); return; } // Mettre à jour l'URL (même si la note n'est pas encore connue localement lorsqu'on force) - await this.updateUrl({ note: trimmed }); + await this.updateUrl({ note: normalized }); } /** @@ -351,7 +369,7 @@ export class UrlStateService implements OnDestroy { */ async filterByFolder(folder: string): Promise { // Vérifier que le dossier existe - const fileTree = this.vaultService.fileTree(); + const fileTree = this.vaultService.fileTree() ?? []; const folderExists = this.folderExistsInTree(fileTree, folder); if (!folderExists) { @@ -382,17 +400,25 @@ export class UrlStateService implements OnDestroy { * Filtrer par quick link */ async filterByQuickLink(quickLink: string): Promise { - const validQuickLinks = ['all', 'All Pages', 'Favoris', 'Publié', 'Modèles', 'Tâches', 'Brouillons', 'Privé', 'Archive', 'Corbeille', 'favorites', 'publish', 'drafts', 'templates', 'tasks', 'private', 'archive']; + const raw = (quickLink ?? '').trim().toLowerCase(); + const mapQuick = new Map([ + ['all', 'all'], ['all pages', 'all'], + ['favoris', 'Favoris'], ['favorites', 'favorites'], + ['publié', 'Publié'], ['publie', 'Publié'], ['publish', 'publish'], + ['modèles', 'Modèles'], ['modeles', 'Modèles'], ['templates', 'templates'], + ['tâches', 'Tâches'], ['taches', 'Tâches'], ['tasks', 'tasks'], + ['brouillons', 'Brouillons'], ['drafts', 'drafts'], + ['privé', 'Privé'], ['prive', 'Privé'], ['private', 'private'], + ['archive', 'Archive'], ['corbeille', 'Corbeille'] + ]); - if (!validQuickLinks.includes(quickLink)) { + const canonical = mapQuick.get(raw); + if (!canonical) { console.warn(`Invalid quick link: ${quickLink}`); return; } - // Si 'All Pages' est sélectionné, on veut supprimer le filtre 'quick'. - const newQuickValue = (quickLink === 'all' || quickLink === 'All Pages') ? null : quickLink; - - // Mettre à jour l'URL + const newQuickValue = canonical === 'all' ? null : canonical; await this.updateUrl({ quick: newQuickValue, note: null, tag: null, folder: null, search: null }); } @@ -439,22 +465,20 @@ export class UrlStateService implements OnDestroy { const normalized = (notePath ?? '').trim() .replace(/\\/g, '/') .replace(/^\/+/, ''); - + if (!normalized) { console.warn('setNote() called with empty path'); return; } - const queryParams: any = { note: normalized }; + const partial: Partial = { note: normalized, tag: null, quick: null }; if (folderPath) { - queryParams.folder = folderPath.replace(/\\/g, '/').replace(/^\/+/, ''); + partial.folder = folderPath.replace(/\\/g, '/').replace(/^\/+/, ''); + } else { + partial.folder = null; } - await this.router.navigate([], { - queryParams, - queryParamsHandling: 'merge', - preserveFragment: true - }); + await this.updateUrl(partial); } /** @@ -483,11 +507,27 @@ export class UrlStateService implements OnDestroy { async resetState(): Promise { await this.router.navigate([], { queryParams: {}, - queryParamsHandling: 'merge', + queryParamsHandling: '', preserveFragment: true }); } + /** + * Appliquer en une seule navigation: quick (ou aucun) + kind=markdown, + * en réinitialisant les autres filtres (note, tag, folder, search). + */ + async setQuickWithMarkdown(quickLabel: string | null): Promise { + const partial: Partial = { + note: null, + tag: null, + folder: null, + search: null, + quick: quickLabel ?? null, + kind: 'markdown', + }; + await this.updateUrl(partial); + } + /** * Générer une URL partageble */ @@ -503,7 +543,9 @@ export class UrlStateService implements OnDestroy { if (stateToShare.search) params.set('search', stateToShare.search); if (stateToShare.kind) params.set('kind', stateToShare.kind); - const baseUrl = window.location.origin + window.location.pathname; + const baseUrl = (typeof window !== 'undefined') + ? window.location.origin + window.location.pathname + : this.router.serializeUrl(this.router.createUrlTree([])); return `${baseUrl}?${params.toString()}`; } @@ -543,7 +585,8 @@ export class UrlStateService implements OnDestroy { */ onStatePropertyChange(property: keyof UrlState) { return this.stateChangeSubject.asObservable().pipe( - filter(event => event.changed.includes(property)) + filter(event => event.changed.includes(property)), + distinctUntilChanged((a, b) => a.current[property] === b.current[property]) ); } diff --git a/vault/titi/Nouveau-markdown.md b/vault/titi/Nouveau-markdown.md new file mode 100644 index 0000000..6c11cee --- /dev/null +++ b/vault/titi/Nouveau-markdown.md @@ -0,0 +1,58 @@ +--- +titre: Nouveau-markdown +auteur: Bruno Charest +creation_date: 2025-10-19T21:42:53-04:00 +modification_date: 2025-10-30T21:24:35-04:00 +catégorie: "" +tags: [] +aliases: [] +status: en-cours +publish: true +favoris: false +template: true +task: true +archive: true +draft: true +private: true +toto: tata +color: "#EF4444" +--- +Allo ceci est un tests +toto + +# Test 1 Markdown + +## Titres + +# Niveau 1 +#tag1 #tag2 #test #test2 + + +# Nouveau-markdown + +## sous-titre +- [ ] allo +- [ ] toto +- [ ] tata + +## sous-titre 2 + +#tag1 #tag2 #tag3 #tag4 + +## sous-titre 3 + +## sous-titre 4 + +## sous-titre 5 +test + +## sous-titre 6 +test + +## sous-titre 7 +test + +## sous-titre 8 + + + diff --git a/vault/Nouveau-markdown.md b/vault/titi/Nouveau-markdown.md.bak similarity index 100% rename from vault/Nouveau-markdown.md rename to vault/titi/Nouveau-markdown.md.bak