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
This commit is contained in:
parent
1545dbb20a
commit
cbdb000d4b
@ -62,7 +62,7 @@ import { FilterService } from '../../services/filter.service';
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted">{{ open.quick ? '▾' : '▸' }}</span>
|
||||
<span>⚡</span>
|
||||
<span>Quick Links</span>
|
||||
<a href="/" (click)="$event.stopPropagation(); onQuickLinksHeaderClick($event)" class="hover:underline">Quick Links</a>
|
||||
</span>
|
||||
</button>
|
||||
<div *ngIf="open.quick" class="pt-1">
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<void> {
|
||||
// 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<void> {
|
||||
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<string, string>([
|
||||
['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 });
|
||||
}
|
||||
|
||||
@ -445,16 +471,14 @@ export class UrlStateService implements OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryParams: any = { note: normalized };
|
||||
const partial: Partial<UrlState> = { 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<void> {
|
||||
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<void> {
|
||||
const partial: Partial<UrlState> = {
|
||||
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])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
58
vault/titi/Nouveau-markdown.md
Normal file
58
vault/titi/Nouveau-markdown.md
Normal file
@ -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
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user