feat: add quick links filtering and frontmatter enrichment on file load

This commit is contained in:
2025-10-19 20:47:18 -04:00
parent 8cccf83f9a
commit 0f7610bed1
51 changed files with 4078 additions and 647 deletions
+23 -2
View File
@@ -50,6 +50,7 @@ export class NotesListComponent {
folderFilter = input<string | null>(null); // like "folder/subfolder"
query = input<string>('');
tagFilter = input<string | null>(null);
quickLinkFilter = input<'favoris' | 'template' | 'task' | 'draft' | 'archive' | null>(null);
@Output() openNote = new EventEmitter<string>();
@Output() queryChange = new EventEmitter<string>();
@@ -61,18 +62,38 @@ export class NotesListComponent {
filtered = computed(() => {
const q = (this.q() || '').toLowerCase().trim();
const folder = (this.folderFilter() || '').toLowerCase();
const folder = (this.folderFilter() || '').toLowerCase().replace(/^\/+|\/+$/g, '');
const tag = (this.tagFilter() || '').toLowerCase();
const quickLink = this.quickLinkFilter();
let list = this.notes();
if (folder) {
list = list.filter(n => (n.originalPath || '').toLowerCase().startsWith(folder));
if (folder === '.trash') {
// All files anywhere under .trash (including subfolders)
list = list.filter(n => {
const filePath = (n.filePath || n.originalPath || '').toLowerCase().replace(/\\/g, '/');
return filePath.includes('/.trash/');
});
} else {
list = list.filter(n => {
const originalPath = (n.originalPath || '').toLowerCase().replace(/^\/+|\/+$/g, '');
return originalPath === folder || originalPath.startsWith(folder + '/');
});
}
}
if (tag) {
list = list.filter(n => Array.isArray(n.tags) && n.tags.some(t => (t || '').toLowerCase() === tag));
}
// Apply Quick Link filter (favoris, template, task)
if (quickLink) {
list = list.filter(n => {
const frontmatter = n.frontmatter || {};
return frontmatter[quickLink] === true;
});
}
// Apply query if present
if (q) {
list = list.filter(n => {
@@ -1,26 +1,71 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VaultService } from '../../../services/vault.service';
import { BadgeCountComponent } from '../../shared/ui/badge-count.component';
interface QuickLinkCountsUi {
all: number;
favorites: number;
templates: number;
tasks: number;
drafts: number;
archive: number;
}
@Component({
selector: 'app-quick-links',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, BadgeCountComponent],
template: `
<div class="p-3">
<ul class="text-sm">
<li><button (click)="select('all')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>🗂️</span> <span>All pages</span></button></li>
<li><button (click)="select('favorites')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>❤️</span> <span>Favorites</span></button></li>
<li><button (click)="select('templates')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>📑</span> <span>Templates</span></button></li>
<li><button (click)="select('import')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>⬇️</span> <span>Import</span></button></li>
<li><button (click)="select('tasks')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>🗒️</span> <span>Tasks</span></button></li>
<li><button (click)="select('chat')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>💬</span> <span>Chat</span></button></li>
<li><button (click)="select('activity')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>🕒</span> <span>Activity Panel</span></button></li>
<li><button (click)="select('portal')" class="w-full text-left px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"><span>📰</span> <span>Portal</span></button></li>
<ul class="text-sm space-y-1">
<li>
<button (click)="select('all')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗂️</span> <span>All pages</span></span>
<app-badge-count class="ml-auto" [count]="counts().all" color="slate"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>❤️</span> <span>Favorites</span></span>
<app-badge-count class="ml-auto" [count]="counts().favorites" color="rose"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('templates')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>📑</span> <span>Templates</span></span>
<app-badge-count class="ml-auto" [count]="counts().templates" color="amber"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('tasks')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗒️</span> <span>Tasks</span></span>
<app-badge-count class="ml-auto" [count]="counts().tasks" color="indigo"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('drafts')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>📝</span> <span>Drafts</span></span>
<app-badge-count class="ml-auto" [count]="counts().drafts" color="emerald"></app-badge-count>
</button>
</li>
<li>
<button (click)="select('archive')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
<span class="flex items-center gap-2"><span>🗃️</span> <span>Archive</span></span>
<app-badge-count class="ml-auto" [count]="counts().archive" color="stone"></app-badge-count>
</button>
</li>
</ul>
</div>
`
})
export class QuickLinksComponent {
private vault = inject(VaultService);
@Output() quickLinkSelected = new EventEmitter<string>();
select(id: string) { this.quickLinkSelected.emit(id); }
counts = () => this.vault.counts() as QuickLinkCountsUi;
select(id: string) {
this.quickLinkSelected.emit(id);
}
}
@@ -6,11 +6,13 @@ import { QuickLinksComponent } from '../quick-links/quick-links.component';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import type { VaultNode, TagInfo } from '../../../types';
import { environment } from '../../../environments/environment';
import { VaultService } from '../../../services/vault.service';
import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explorer.component';
@Component({
selector: 'app-sidebar-drawer',
standalone: true,
imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective],
imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective, TrashExplorerComponent],
template: `
<aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-white dark:bg-gray-900 shadow-2xl z-40 transform transition-all duration-300 ease-out flex flex-col"
[class.-translate-x-full]="!mobileNav.sidebarOpen()"
@@ -90,11 +92,18 @@ import { environment } from '../../../environments/environment';
<!-- Trash accordion -->
<section class="border-b border-gray-200 dark:border-gray-800">
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
(click)="open.trash = !open.trash">
<span>Trash</span>
(click)="open.trash = !open.trash; onFolder('.trash')">
<span class="flex items-center gap-2">Trash</span>
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
</button>
<div *ngIf="open.trash" class="px-3 py-3 text-sm text-gray-500 dark:text-gray-400">Empty</div>
<div *ngIf="open.trash" class="px-1 py-2">
<ng-container *ngIf="trashHasContent(); else emptyTrash">
<app-trash-explorer (folderSelected)="onFolder($event)"></app-trash-explorer>
</ng-container>
<ng-template #emptyTrash>
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div>
</ng-template>
</div>
</section>
</div>
@@ -116,6 +125,7 @@ import { environment } from '../../../environments/environment';
export class AppSidebarDrawerComponent {
mobileNav = inject(MobileNavService);
env = environment;
private vault = inject(VaultService);
@Input() nodes: VaultNode[] = [];
@Input() selectedNoteId: string | null = null;
@@ -154,4 +164,9 @@ export class AppSidebarDrawerComponent {
this.markdownPlaygroundSelected.emit();
this.mobileNav.sidebarOpen.set(false);
}
trashNotes = () => this.vault.trashNotes();
trashCount = () => this.vault.counts().trash;
trashHasContent = () => (this.vault.trashTree() || []).length > 0;
trackNoteId = (_: number, n: { id: string }) => n.id;
}
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component';
@@ -6,6 +6,7 @@ import { QuickLinksComponent } from '../quick-links/quick-links.component';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import type { VaultNode, TagInfo } from '../../../types';
import { environment } from '../../../environments/environment';
import { VaultService } from '../../../services/vault.service';
@Component({
selector: 'app-nimbus-sidebar',
@@ -58,7 +59,13 @@ import { environment } from '../../../environments/environment';
<span class="text-xs text-gray-500">{{ open.folders ? '▾' : '▸' }}</span>
</button>
<div *ngIf="open.folders" class="px-1 py-1">
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="folderSelected.emit($event)" (fileSelected)="fileSelected.emit($event)"></app-file-explorer>
<app-file-explorer
[nodes]="effectiveFileTree"
[selectedNoteId]="selectedNoteId"
[foldersOnly]="true"
(folderSelected)="folderSelected.emit($event)"
(fileSelected)="fileSelected.emit($event)">
</app-file-explorer>
</div>
</section>
@@ -85,11 +92,25 @@ import { environment } from '../../../environments/environment';
<!-- Trash accordion -->
<section class="border-b border-gray-200 dark:border-gray-800">
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800"
(click)="open.trash = !open.trash">
<span>Trash</span>
(click)="open.trash = !open.trash; folderSelected.emit('.trash')">
<span class="flex items-center gap-2">Trash</span>
<span class="text-xs text-gray-500">{{ open.trash ? '▾' : '▸' }}</span>
</button>
<div *ngIf="open.trash" class="px-3 py-3 text-sm text-gray-500 dark:text-gray-400">Empty</div>
<div *ngIf="open.trash" class="px-1 py-2">
<ng-container *ngIf="trashHasContent(); else emptyTrash">
<app-file-explorer
[nodes]="vault.trashTree()"
[selectedNoteId]="selectedNoteId"
[foldersOnly]="true"
[useTrashCounts]="true"
(folderSelected)="folderSelected.emit($event)"
(fileSelected)="fileSelected.emit($event)">
</app-file-explorer>
</ng-container>
<ng-template #emptyTrash>
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div>
</ng-template>
</div>
</section>
</div>
@@ -113,10 +134,16 @@ export class NimbusSidebarComponent {
env = environment;
open = { quick: true, folders: true, tags: false, trash: false, tests: true };
private vault = inject(VaultService);
onQuickLink(id: string) { this.quickLinkSelected.emit(id); }
onMarkdownPlaygroundClick(): void {
this.markdownPlaygroundSelected.emit();
}
trashNotes = () => this.vault.trashNotes();
trashCount = () => this.vault.counts().trash;
trashHasContent = () => (this.vault.trashTree() || []).length > 0;
trackNoteId = (_: number, n: { id: string }) => n.id;
}