feat: add quick links filtering and frontmatter enrichment on file load
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
[notes]="vault.allNotes()"
|
||||
[folderFilter]="folderFilter"
|
||||
[tagFilter]="tagFilter"
|
||||
[quickLinkFilter]="quickLinkFilter"
|
||||
[query]="listQuery"
|
||||
(openNote)="onOpenNote($event)"
|
||||
(queryChange)="listQuery = $event"
|
||||
@@ -161,7 +162,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
|
||||
</div>
|
||||
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
|
||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
|
||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
|
||||
</div>
|
||||
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
|
||||
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
|
||||
@@ -185,7 +186,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
|
||||
@if (mobileNav.activeTab() === 'list') {
|
||||
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
|
||||
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
|
||||
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -260,6 +261,7 @@ export class AppShellNimbusLayoutComponent {
|
||||
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'trash' | null = null;
|
||||
private flyoutCloseTimer: any = null;
|
||||
tagFilter: string | null = null;
|
||||
quickLinkFilter: 'favoris' | 'template' | 'task' | 'draft' | 'archive' | null = null;
|
||||
|
||||
toggleNoteFullScreen(): void {
|
||||
this.noteFullScreen = !this.noteFullScreen;
|
||||
@@ -316,11 +318,61 @@ export class AppShellNimbusLayoutComponent {
|
||||
// Show all pages: clear filters and focus list
|
||||
this.folderFilter = null;
|
||||
this.tagFilter = null;
|
||||
this.quickLinkFilter = null;
|
||||
this.listQuery = '';
|
||||
if (!this.responsive.isDesktop()) {
|
||||
this.mobileNav.setActiveTab('list');
|
||||
}
|
||||
this.scheduleCloseFlyout(150);
|
||||
} else if (_id === 'favorites') {
|
||||
// Filter by favoris: true
|
||||
this.folderFilter = null;
|
||||
this.tagFilter = null;
|
||||
this.quickLinkFilter = 'favoris';
|
||||
this.listQuery = '';
|
||||
if (!this.responsive.isDesktop()) {
|
||||
this.mobileNav.setActiveTab('list');
|
||||
}
|
||||
this.scheduleCloseFlyout(150);
|
||||
} else if (_id === 'templates') {
|
||||
// Filter by template: true
|
||||
this.folderFilter = null;
|
||||
this.tagFilter = null;
|
||||
this.quickLinkFilter = 'template';
|
||||
this.listQuery = '';
|
||||
if (!this.responsive.isDesktop()) {
|
||||
this.mobileNav.setActiveTab('list');
|
||||
}
|
||||
this.scheduleCloseFlyout(150);
|
||||
} else if (_id === 'tasks') {
|
||||
// Filter by task: true
|
||||
this.folderFilter = null;
|
||||
this.tagFilter = null;
|
||||
this.quickLinkFilter = 'task';
|
||||
this.listQuery = '';
|
||||
if (!this.responsive.isDesktop()) {
|
||||
this.mobileNav.setActiveTab('list');
|
||||
}
|
||||
this.scheduleCloseFlyout(150);
|
||||
} else if (_id === 'drafts') {
|
||||
// Filter by draft: true
|
||||
this.folderFilter = null;
|
||||
this.tagFilter = null;
|
||||
this.quickLinkFilter = 'draft';
|
||||
this.listQuery = '';
|
||||
if (!this.responsive.isDesktop()) {
|
||||
this.mobileNav.setActiveTab('list');
|
||||
}
|
||||
this.scheduleCloseFlyout(150);
|
||||
} else if (_id === 'archive') {
|
||||
// Filter by archive: true
|
||||
this.folderFilter = null;
|
||||
this.tagFilter = null;
|
||||
this.quickLinkFilter = 'archive';
|
||||
this.listQuery = '';
|
||||
if (!this.responsive.isDesktop()) {
|
||||
this.mobileNav.setActiveTab('list');
|
||||
}
|
||||
// If flyout is open, keep it or close? Close gracefully
|
||||
this.scheduleCloseFlyout(150);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Component, ChangeDetectionStrategy, input, output, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { VaultNode, VaultFile, VaultFolder } from '../../../../types';
|
||||
import { VaultService } from '../../../../services/vault.service';
|
||||
import { BadgeCountComponent } from '../../../../app/shared/ui/badge-count.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-trash-explorer',
|
||||
template: `
|
||||
<ul class="space-y-0.5">
|
||||
@for(node of nodes(); track node.path) {
|
||||
<li>
|
||||
@if (isFolder(node)) {
|
||||
@let folder = node;
|
||||
<div>
|
||||
<div
|
||||
(click)="onFolderClick(folder)"
|
||||
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>
|
||||
<span class="font-semibold truncate">{{ folder.name }}</span>
|
||||
<app-badge-count class="ml-auto" [count]="folderCount(folder.path)" color="slate"></app-badge-count>
|
||||
</div>
|
||||
@if (folder.isOpen) {
|
||||
<div class="pl-5">
|
||||
<app-trash-explorer
|
||||
[nodes]="folder.children"
|
||||
[selectedNoteId]="selectedNoteId()"
|
||||
[foldersOnly]="foldersOnly()"
|
||||
(folderSelected)="folderSelected.emit($event)"
|
||||
(fileSelected)="onFileSelected($event)"
|
||||
></app-trash-explorer>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
@if (!foldersOnly()) {
|
||||
@let file = node;
|
||||
<div
|
||||
(click)="onFileSelected(file.id)"
|
||||
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm ml-5"
|
||||
[class.bg-obs-l-bg-main]="selectedNoteId() === file.id"
|
||||
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id"
|
||||
[class.hover:bg-slate-500/10]="selectedNoteId() !== file.id"
|
||||
[class.dark:hover:bg-slate-200/10]="selectedNoteId() !== file.id"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ file.name }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, BadgeCountComponent],
|
||||
})
|
||||
|
||||
export class TrashExplorerComponent {
|
||||
nodes = input.required<VaultNode[]>();
|
||||
selectedNoteId = input<string | null>(null);
|
||||
foldersOnly = input<boolean>(false);
|
||||
fileSelected = output<string>();
|
||||
folderSelected = output<string>();
|
||||
|
||||
private vaultService = inject(VaultService);
|
||||
|
||||
folderCount(path: string): number {
|
||||
const counts = this.vaultService.folderCounts();
|
||||
return counts[path] ?? 0;
|
||||
}
|
||||
|
||||
onFileSelected(noteId: string) {
|
||||
if(noteId) {
|
||||
this.fileSelected.emit(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
onFolderClick(folder: VaultFolder) {
|
||||
this.toggleFolder(folder);
|
||||
if (folder?.path) {
|
||||
this.folderSelected.emit(folder.path);
|
||||
}
|
||||
}
|
||||
|
||||
toggleFolder(folder: VaultFolder) {
|
||||
this.vaultService.toggleFolder(folder.path);
|
||||
}
|
||||
|
||||
isFolder(node: VaultNode): node is VaultFolder {
|
||||
return node.type === 'folder';
|
||||
}
|
||||
}
|
||||
// export class TrashExplorerComponent {
|
||||
// // align with FileExplorer API (even si non utilisés ici)
|
||||
// selectedNoteId = input<string | null>(null);
|
||||
// foldersOnly = input<boolean>(false);
|
||||
// fileSelected = output<string>();
|
||||
// folderSelected = output<string>();
|
||||
|
||||
// private vaultService = inject(VaultService);
|
||||
|
||||
// /** Arborescence limitée au dossier .trash */
|
||||
// get nodes(): VaultNode[] {
|
||||
// const tree = this.vaultService.trashTree();
|
||||
// try { console.debug('[TrashExplorer] .trash tree', tree); } catch {}
|
||||
// return tree ?? [];
|
||||
// }
|
||||
|
||||
// /** Compte les éléments d’un dossier de la corbeille */
|
||||
// trashFolderCount(path: string): number {
|
||||
// const counts = this.vaultService.trashFolderCounts();
|
||||
// const raw = (path || '').replace(/\\/g, '/');
|
||||
// const norm = raw.replace(/^\/+|\/+$/g, '').toLowerCase();
|
||||
// return (counts?.[norm] ?? counts?.[raw] ?? counts?.[raw.toLowerCase()] ?? 0);
|
||||
// }
|
||||
|
||||
// onFileSelected(noteId: string) {
|
||||
// if (noteId) {
|
||||
// this.fileSelected.emit(noteId);
|
||||
// }
|
||||
// }
|
||||
|
||||
// onFolderClick(folder: VaultFolder) {
|
||||
// this.toggleFolder(folder);
|
||||
// if (folder?.path) {
|
||||
// this.folderSelected.emit(folder.path);
|
||||
// }
|
||||
// }
|
||||
|
||||
// toggleFolder(folder: VaultFolder) {
|
||||
// if (!folder?.path) return;
|
||||
// this.vaultService.toggleFolder(folder.path);
|
||||
// }
|
||||
|
||||
// isFolder(node: VaultNode): node is VaultFolder {
|
||||
// return node?.type === 'folder';
|
||||
// }
|
||||
|
||||
// trackPath(_: number, n: VaultNode) {
|
||||
// return n.path;
|
||||
// }
|
||||
|
||||
// getFolderLabel(folder: VaultFolder): string {
|
||||
// const name = (folder?.name || '').trim();
|
||||
// if (name) return name;
|
||||
// const path = (folder?.path || '').replace(/\\/g, '/');
|
||||
// const last = path.split('/').filter(Boolean).pop();
|
||||
// return last || '(unnamed)';
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-badge-count',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<span
|
||||
class="inline-flex items-center justify-center w-6 h-6 rounded-full text-xs font-semibold text-white"
|
||||
[ngClass]="bgClass"
|
||||
aria-label="count"
|
||||
>
|
||||
{{ count }}
|
||||
</span>
|
||||
`,
|
||||
})
|
||||
export class BadgeCountComponent {
|
||||
@Input() count = 0;
|
||||
@Input() color: 'slate'|'rose'|'amber'|'indigo'|'emerald'|'stone'|'zinc' = 'slate';
|
||||
|
||||
get bgClass() {
|
||||
return {
|
||||
'bg-slate-600': this.color === 'slate',
|
||||
'bg-rose-600': this.color === 'rose',
|
||||
'bg-amber-600': this.color === 'amber',
|
||||
'bg-indigo-600': this.color === 'indigo',
|
||||
'bg-emerald-600': this.color === 'emerald',
|
||||
'bg-stone-600': this.color === 'stone',
|
||||
'bg-zinc-700': this.color === 'zinc',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Component, ChangeDetectionStrategy, input, output, inject } from '@angu
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { VaultNode, VaultFile, VaultFolder } from '../../types';
|
||||
import { VaultService } from '../../services/vault.service';
|
||||
import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-explorer',
|
||||
@@ -9,18 +10,19 @@ import { VaultService } from '../../services/vault.service';
|
||||
<ul class="space-y-0.5">
|
||||
@for(node of nodes(); track node.path) {
|
||||
<li>
|
||||
@if (isFolder(node)) {
|
||||
@if (isFolder(node) && node.name !== '.trash') {
|
||||
@let folder = node;
|
||||
<div>
|
||||
<div
|
||||
(click)="onFolderClick(folder)"
|
||||
class="flex items-center cursor-pointer p-2 rounded my-0.5 text-sm hover:bg-obs-l-bg-main dark:hover:bg-obs-d-bg-main"
|
||||
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>
|
||||
<span class="font-semibold truncate">{{ folder.name }}</span>
|
||||
<app-badge-count class="ml-auto" [count]="folderCount(folder.path)" color="slate"></app-badge-count>
|
||||
</div>
|
||||
@if (folder.isOpen) {
|
||||
<div class="pl-5">
|
||||
@@ -39,11 +41,11 @@ import { VaultService } from '../../services/vault.service';
|
||||
@let file = node;
|
||||
<div
|
||||
(click)="onFileSelected(file.id)"
|
||||
class="flex items-center cursor-pointer p-2 rounded my-0.5 text-sm ml-5"
|
||||
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm ml-5"
|
||||
[class.bg-obs-l-bg-main]="selectedNoteId() === file.id"
|
||||
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id"
|
||||
[class.hover:bg-obs-l-bg-main]="selectedNoteId() !== file.id"
|
||||
[class.dark:hover:bg-obs-d-bg-main]="selectedNoteId() !== file.id"
|
||||
[class.hover:bg-slate-500/10]="selectedNoteId() !== file.id"
|
||||
[class.dark:hover:bg-slate-200/10]="selectedNoteId() !== file.id"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
@@ -57,17 +59,30 @@ import { VaultService } from '../../services/vault.service';
|
||||
</ul>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, BadgeCountComponent],
|
||||
})
|
||||
export class FileExplorerComponent {
|
||||
nodes = input.required<VaultNode[]>();
|
||||
selectedNoteId = input<string | null>(null);
|
||||
foldersOnly = input<boolean>(false);
|
||||
useTrashCounts = input<boolean>(false);
|
||||
fileSelected = output<string>();
|
||||
folderSelected = output<string>();
|
||||
|
||||
private vaultService = inject(VaultService);
|
||||
|
||||
folderCount(path: string): number {
|
||||
if (this.useTrashCounts()) {
|
||||
const counts = this.vaultService.trashFolderCounts();
|
||||
const raw = (path || '').replace(/\\/g, '/');
|
||||
const norm = raw.replace(/^\/+|\/+$/g, '').toLowerCase();
|
||||
return (counts[norm] ?? counts[raw] ?? counts[raw.toLowerCase()] ?? 0);
|
||||
} else {
|
||||
const counts = this.vaultService.folderCounts();
|
||||
return counts[path] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
onFileSelected(noteId: string) {
|
||||
if(noteId) {
|
||||
this.fileSelected.emit(noteId);
|
||||
|
||||
+1071
-584
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ export interface NoteFrontmatter {
|
||||
status?: string;
|
||||
publish?: boolean;
|
||||
favoris?: boolean;
|
||||
template?: boolean;
|
||||
task?: boolean;
|
||||
archive?: boolean;
|
||||
draft?: boolean;
|
||||
private?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user