feat: enhance navigation and filtering behavior
- Added clickable app title links in header and sidebar that reset view to all markdown files - Fixed quicklink filtering to use correct frontmatter boolean keys (favoris, publish, draft, etc.) - Updated sidebar behavior to auto-open Quick Links section when viewing markdown files - Added filter clearing when switching between views to prevent conflicting filters - Modified URL state handling to preserve kind parameter when navigating - Enforced markdown-only
This commit is contained in:
parent
58b22a47c9
commit
1545dbb20a
29
e2e/quicklinks-sync.spec.ts
Normal file
29
e2e/quicklinks-sync.spec.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick Links → Search/Filters → Notes-list sync
|
||||||
|
* - One click on a Quick Link shows the filter pill above the search immediately
|
||||||
|
* - Notes-list updates accordingly
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe('Quick Links state sync', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:4200');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('one-click Favoris shows pill above search', async ({ page }) => {
|
||||||
|
// Open Quick Links if inside an accordion in desktop left sidebar
|
||||||
|
await page.getByText('Quick Links').first().click({ trial: true }).catch(() => {});
|
||||||
|
|
||||||
|
// Click on Favoris in Quick Links
|
||||||
|
await page.getByText('Favoris', { exact: true }).first().click();
|
||||||
|
|
||||||
|
// Expect a pill with label "Favoris" to appear above the search input in the list panel
|
||||||
|
const pill = page.locator('app-filter-badge .label', { hasText: 'Favoris' });
|
||||||
|
await expect(pill).toBeVisible({ timeout: 3000 });
|
||||||
|
|
||||||
|
// URL should contain quick=Favoris
|
||||||
|
await expect.poll(async () => new URL(page.url()).searchParams.get('quick')).toBe('Favoris');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -318,7 +318,9 @@
|
|||||||
<div class="min-w-0 flex flex-col gap-1">
|
<div class="min-w-0 flex flex-col gap-1">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<span class="inline-flex items-center rounded-lg border border-border bg-bg-muted/70 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-text-muted">{{ vaultName() }}</span>
|
<span class="inline-flex items-center rounded-lg border border-border bg-bg-muted/70 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-text-muted">{{ vaultName() }}</span>
|
||||||
<h1 class="text-lg font-semibold leading-tight text-text-main">ObsiWatcher</h1>
|
<h1 class="text-lg font-semibold leading-tight text-text-main">
|
||||||
|
<a href="/" (click)="onAppTitleClick($event)" class="hover:underline">ObsiWatcher</a>
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@if (selectedNoteBreadcrumb().length > 0) {
|
@if (selectedNoteBreadcrumb().length > 0) {
|
||||||
<p class="truncate text-xs text-text-muted">{{ selectedNoteBreadcrumb().join(' / ') }}</p>
|
<p class="truncate text-xs text-text-muted">{{ selectedNoteBreadcrumb().join(' / ') }}</p>
|
||||||
|
|||||||
@ -884,6 +884,16 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAppTitleClick(event: MouseEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
this.isSidebarOpen.set(true);
|
||||||
|
this.activeView.set('files');
|
||||||
|
queueMicrotask(async () => {
|
||||||
|
await this.urlState.showAllAndReset();
|
||||||
|
await this.urlState.filterByKind('markdown');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onCalendarSearchErrorChange(message: string | null): void {
|
onCalendarSearchErrorChange(message: string | null): void {
|
||||||
this.calendarSearchError.set(message);
|
this.calendarSearchError.set(message);
|
||||||
if (message) {
|
if (message) {
|
||||||
|
|||||||
@ -663,13 +663,14 @@ export class NotesListComponent {
|
|||||||
|
|
||||||
private mapInternalQuickToFrontmatter(id: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null): string | null {
|
private mapInternalQuickToFrontmatter(id: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null): string | null {
|
||||||
switch (id || '') {
|
switch (id || '') {
|
||||||
case 'favoris': return 'Favoris';
|
// Match actual frontmatter boolean keys used in the vault service and writers
|
||||||
case 'publish': return 'Publié';
|
case 'favoris': return 'favoris';
|
||||||
case 'draft': return 'Brouillons';
|
case 'publish': return 'publish';
|
||||||
case 'template': return 'Modèles';
|
case 'draft': return 'draft';
|
||||||
case 'task': return 'Tâches';
|
case 'template': return 'template';
|
||||||
case 'private': return 'Privé';
|
case 'task': return 'task';
|
||||||
case 'archive': return 'Archive';
|
case 'private': return 'private';
|
||||||
|
case 'archive': return 'archive';
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -720,9 +721,13 @@ export class NotesListComponent {
|
|||||||
|
|
||||||
if (quickLink) {
|
if (quickLink) {
|
||||||
const fmKey = this.mapInternalQuickToFrontmatter(quickLink);
|
const fmKey = this.mapInternalQuickToFrontmatter(quickLink);
|
||||||
|
// Enforce Markdown-only when a Quick Link is active and match frontmatter boolean flag
|
||||||
list = list.filter(n => {
|
list = list.filter(n => {
|
||||||
const frontmatter = n.frontmatter || {} as any;
|
const frontmatter = n.frontmatter || ({} as any);
|
||||||
return fmKey ? frontmatter[fmKey] === true : false;
|
return (
|
||||||
|
this.matchesKind(n, 'markdown') &&
|
||||||
|
(fmKey ? frontmatter[fmKey] === true : false)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,10 +20,10 @@ import { FilterService } from '../../services/filter.service';
|
|||||||
<div class="h-full flex flex-col overflow-hidden select-none">
|
<div class="h-full flex flex-col overflow-hidden select-none">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
|
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<a href="/" (click)="onHomeClick($event)" class="flex items-center gap-2 min-w-0 text-inherit no-underline">
|
||||||
<img src="assets/favicon.svg" alt="ObsiViewer" class="h-6 w-6 flex-shrink-0" />
|
<img src="assets/favicon.svg" alt="ObsiViewer" class="h-6 w-6 flex-shrink-0" />
|
||||||
<div class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</div>
|
<span class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</span>
|
||||||
</div>
|
</a>
|
||||||
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" title="Hide Sidebar">⟨⟨</button>
|
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" title="Hide Sidebar">⟨⟨</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ export class NimbusSidebarComponent implements OnChanges {
|
|||||||
@Output() aboutSelected = new EventEmitter<void>();
|
@Output() aboutSelected = new EventEmitter<void>();
|
||||||
|
|
||||||
env = environment;
|
env = environment;
|
||||||
open = { quick: true, folders: true, tags: false, trash: false, tests: false };
|
open = { quick: true, folders: false, tags: false, trash: false, tests: false };
|
||||||
private vault = inject(VaultService);
|
private vault = inject(VaultService);
|
||||||
urlState = inject(UrlStateService);
|
urlState = inject(UrlStateService);
|
||||||
private sidebar = inject(SidebarStateService);
|
private sidebar = inject(SidebarStateService);
|
||||||
@ -239,6 +239,15 @@ export class NimbusSidebarComponent implements OnChanges {
|
|||||||
|
|
||||||
onQuickLink(id: string) { this.quickLinkSelected.emit(id); }
|
onQuickLink(id: string) { this.quickLinkSelected.emit(id); }
|
||||||
|
|
||||||
|
onHomeClick(event: MouseEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
this.toggleSection('quick');
|
||||||
|
this.quickLinkSelected.emit('all');
|
||||||
|
queueMicrotask(async () => {
|
||||||
|
await this.urlState.filterByKind('markdown');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMarkdownPlaygroundClick(): void {
|
onMarkdownPlaygroundClick(): void {
|
||||||
this.markdownPlaygroundSelected.emit();
|
this.markdownPlaygroundSelected.emit();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { TestExcalidrawPageComponent } from '../../features/tests/test-excalidra
|
|||||||
import { ParametersPage } from '../../features/parameters/parameters.page';
|
import { ParametersPage } from '../../features/parameters/parameters.page';
|
||||||
import { AboutPanelComponent } from '../../features/about/about-panel.component';
|
import { AboutPanelComponent } from '../../features/about/about-panel.component';
|
||||||
import { UrlStateService } from '../../services/url-state.service';
|
import { UrlStateService } from '../../services/url-state.service';
|
||||||
|
import { FilterService } from '../../services/filter.service';
|
||||||
import { NoteInfoModalComponent } from '../../features/note-info/note-info-modal.component';
|
import { NoteInfoModalComponent } from '../../features/note-info/note-info-modal.component';
|
||||||
import { NoteInfoModalService } from '../../services/note-info-modal.service';
|
import { NoteInfoModalService } from '../../services/note-info-modal.service';
|
||||||
import { InPageSearchService } from '../../shared/search/in-page-search.service';
|
import { InPageSearchService } from '../../shared/search/in-page-search.service';
|
||||||
@ -67,7 +68,11 @@ import { InPageSearchOverlayComponent } from '../../shared/search/in-page-search
|
|||||||
[selectedNoteId]="selectedNoteId"
|
[selectedNoteId]="selectedNoteId"
|
||||||
[tags]="tags"
|
[tags]="tags"
|
||||||
[quickLinkFilter]="quickLinkFilter"
|
[quickLinkFilter]="quickLinkFilter"
|
||||||
[forceOpenSection]="tagFilter ? 'tags' : (folderFilter ? 'folders' : (quickLinkFilter ? 'quick' : null))"
|
[forceOpenSection]="
|
||||||
|
(!tagFilter && !folderFilter && !quickLinkFilter && urlState.activeKind() === 'markdown')
|
||||||
|
? 'quick'
|
||||||
|
: (tagFilter ? 'tags' : (folderFilter ? 'folders' : (quickLinkFilter ? 'quick' : null)))
|
||||||
|
"
|
||||||
(toggleSidebarRequest)="toggleSidebarRequest.emit()"
|
(toggleSidebarRequest)="toggleSidebarRequest.emit()"
|
||||||
(folderSelected)="onFolderSelected($event)"
|
(folderSelected)="onFolderSelected($event)"
|
||||||
(fileSelected)="noteSelected.emit($event)"
|
(fileSelected)="noteSelected.emit($event)"
|
||||||
@ -341,6 +346,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
responsive = inject(ResponsiveService);
|
responsive = inject(ResponsiveService);
|
||||||
mobileNav = inject(MobileNavService);
|
mobileNav = inject(MobileNavService);
|
||||||
urlState = inject(UrlStateService);
|
urlState = inject(UrlStateService);
|
||||||
|
filters = inject(FilterService);
|
||||||
noteInfo = inject(NoteInfoModalService);
|
noteInfo = inject(NoteInfoModalService);
|
||||||
inPageSearch = inject(InPageSearchService);
|
inPageSearch = inject(InPageSearchService);
|
||||||
|
|
||||||
@ -722,17 +728,22 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = null;
|
this.quickLinkFilter = null;
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
// Clear local filters (kinds, cumulative tags) to avoid conflicting filters
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
this.scheduleCloseFlyout(150);
|
this.scheduleCloseFlyout(150);
|
||||||
this.urlState.resetState();
|
this.urlState.showAllAndReset();
|
||||||
} else if (_id === 'publish') {
|
} else if (_id === 'publish') {
|
||||||
// Filter by publish: true
|
// Filter by publish: true
|
||||||
this.folderFilter = null;
|
this.folderFilter = null;
|
||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = 'publish';
|
this.quickLinkFilter = 'publish';
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
@ -745,6 +756,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = 'favoris';
|
this.quickLinkFilter = 'favoris';
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
@ -757,6 +770,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = 'template';
|
this.quickLinkFilter = 'template';
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
@ -769,6 +784,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = 'task';
|
this.quickLinkFilter = 'task';
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
@ -781,6 +798,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = 'draft';
|
this.quickLinkFilter = 'draft';
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
@ -793,6 +812,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = 'private';
|
this.quickLinkFilter = 'private';
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
@ -805,6 +826,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = 'archive';
|
this.quickLinkFilter = 'archive';
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -189,7 +189,8 @@ export class UrlStateService implements OnDestroy {
|
|||||||
folder: nextSection === 'folder' ? newState.folder! : null,
|
folder: nextSection === 'folder' ? newState.folder! : null,
|
||||||
quick: nextSection === 'quick' ? newState.quick! : null,
|
quick: nextSection === 'quick' ? newState.quick! : null,
|
||||||
search: null,
|
search: null,
|
||||||
kind: 'all',
|
// Preserve provided kind if any; fallback to 'all'
|
||||||
|
kind: newState.kind ?? 'all',
|
||||||
note: null,
|
note: null,
|
||||||
};
|
};
|
||||||
this.updateUrl(partial);
|
this.updateUrl(partial);
|
||||||
@ -381,15 +382,18 @@ export class UrlStateService implements OnDestroy {
|
|||||||
* Filtrer par quick link
|
* Filtrer par quick link
|
||||||
*/
|
*/
|
||||||
async filterByQuickLink(quickLink: string): Promise<void> {
|
async filterByQuickLink(quickLink: string): Promise<void> {
|
||||||
const validQuickLinks = ['Favoris', 'Publié', 'Modèles', 'Tâches', 'Brouillons', 'Privé', 'Archive', 'Corbeille'];
|
const validQuickLinks = ['all', 'All Pages', 'Favoris', 'Publié', 'Modèles', 'Tâches', 'Brouillons', 'Privé', 'Archive', 'Corbeille', 'favorites', 'publish', 'drafts', 'templates', 'tasks', 'private', 'archive'];
|
||||||
|
|
||||||
if (!validQuickLinks.includes(quickLink)) {
|
if (!validQuickLinks.includes(quickLink)) {
|
||||||
console.warn(`Invalid quick link: ${quickLink}`);
|
console.warn(`Invalid quick link: ${quickLink}`);
|
||||||
return;
|
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
|
// Mettre à jour l'URL
|
||||||
await this.updateUrl({ quick: quickLink, note: null, tag: null, folder: null, search: null });
|
await this.updateUrl({ quick: newQuickValue, note: null, tag: null, folder: null, search: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -460,15 +464,15 @@ export class UrlStateService implements OnDestroy {
|
|||||||
const currentState = this.currentStateSignal();
|
const currentState = this.currentStateSignal();
|
||||||
const newState = { ...currentState, ...partialState };
|
const newState = { ...currentState, ...partialState };
|
||||||
|
|
||||||
// Nettoyer les propriétés undefined
|
// Nettoyer les propriétés undefined ou null
|
||||||
const cleanState = Object.fromEntries(
|
const cleanState = Object.fromEntries(
|
||||||
Object.entries(newState).filter(([_, v]) => v !== undefined)
|
Object.entries(newState).filter(([_, v]) => v !== undefined && v !== null)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Naviguer avec les nouveaux queryParams
|
// Naviguer avec les nouveaux queryParams, en remplaçant les anciens.
|
||||||
await this.router.navigate([], {
|
await this.router.navigate([], {
|
||||||
queryParams: cleanState,
|
queryParams: cleanState,
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: '', // Remplacer au lieu de fusionner
|
||||||
preserveFragment: true
|
preserveFragment: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user