feat: enhance note list views with rich metadata display
- Added timestamp, author, and description fields to both compact and detailed view modes - Implemented thumbnail support in detailed view with automatic image extraction from note content - Improved auto-selection behavior when filters change using reactive signals for more reliable note navigation
This commit is contained in:
parent
7331077ffa
commit
7be20d05e0
@ -192,21 +192,26 @@ import { AIToolsService } from '../../services/ai-tools.service';
|
|||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="title text-sm truncate">{{ n.title }}</div>
|
<div class="title text-sm truncate">{{ n.title }}</div>
|
||||||
<div class="meta text-xs truncate">{{ n.filePath }}</div>
|
<div class="meta text-xs truncate">{{ n.filePath }}</div>
|
||||||
|
<div *ngIf="getUpdatedTimestamp(n) as ts" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">🕓 {{ formatDateTime(ts) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detailed View -->
|
<!-- Detailed View -->
|
||||||
<div *ngIf="state.viewMode() === 'detailed'" class="note-inner flex items-start gap-2 space-y-0">
|
<div *ngIf="state.viewMode() === 'detailed'" class="note-inner flex flex-col sm:flex-row items-start gap-4">
|
||||||
<span class="note-color-dot flex-shrink-0 mt-1" [style.backgroundColor]="getNoteColor(n)" aria-hidden="true"></span>
|
<div class="flex items-start gap-2 min-w-0 flex-1">
|
||||||
<span class="flex-shrink-0 mt-0.5" title="Type">{{ typeIcon(n) }}</span>
|
<span class="note-color-dot flex-shrink-0 mt-1" [style.backgroundColor]="getNoteColor(n)" aria-hidden="true"></span>
|
||||||
<div class="min-w-0 flex-1 space-y-1.5">
|
<span class="flex-shrink-0 mt-0.5" title="Type">{{ typeIcon(n) }}</span>
|
||||||
<div class="title text-sm truncate">{{ n.title }}</div>
|
<div class="min-w-0 flex-1 space-y-1.5">
|
||||||
<div class="meta text-xs truncate">{{ n.filePath }}</div>
|
<div class="title text-sm truncate">{{ n.title }}</div>
|
||||||
<div class="excerpt text-xs">
|
<div class="meta text-xs truncate">{{ n.filePath }}</div>
|
||||||
<span *ngIf="n.frontmatter?.status">Status: {{ n.frontmatter.status }}</span>
|
<div *ngIf="getAuthor(n) as a" class="text-xs text-gray-500 dark:text-gray-400 truncate">✍️ {{ a }}</div>
|
||||||
<span *ngIf="n.mtime" class="ml-2">{{ formatDate(n.mtime) }}</span>
|
<div *ngIf="getDescription(n) as d" class="text-xs text-muted truncate">{{ d }}</div>
|
||||||
|
<div *ngIf="getUpdatedTimestamp(n) as ts" class="text-xs text-gray-500 dark:text-gray-400">🕓 {{ formatDateTime(ts) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-shrink-0 sm:ml-4 mt-2 sm:mt-0" *ngIf="getThumbnailSrc(n) as imgSrc">
|
||||||
|
<img [src]="imgSrc" alt="" class="rounded-md shadow-sm object-cover w-24 h-24" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -1049,6 +1054,75 @@ export class NotesListComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUpdatedTimestamp(n: Note): number | null {
|
||||||
|
try {
|
||||||
|
const m = Number(n.mtime || 0);
|
||||||
|
if (m && !Number.isNaN(m)) return m;
|
||||||
|
} catch {}
|
||||||
|
const parse = (s?: string) => {
|
||||||
|
if (!s) return 0;
|
||||||
|
const t = Date.parse(s);
|
||||||
|
return Number.isFinite(t) ? t : 0;
|
||||||
|
};
|
||||||
|
const u = parse(n.updatedAt);
|
||||||
|
if (u) return u;
|
||||||
|
const c = parse(n.createdAt);
|
||||||
|
return c || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDateTime(ts: number): string {
|
||||||
|
try {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return d.toLocaleString('fr-FR', { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
|
} catch { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthor(n: Note): string | null {
|
||||||
|
const fm = (n.frontmatter || {}) as any;
|
||||||
|
const v = (fm.author ?? fm.auteur ?? n.author) as any;
|
||||||
|
return (typeof v === 'string' && v.trim().length > 0) ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(n: Note): string | null {
|
||||||
|
const fm = (n.frontmatter || {}) as any;
|
||||||
|
const v = (fm.description ?? fm.desc) as any;
|
||||||
|
return (typeof v === 'string' && v.trim().length > 0) ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractFirstImageFromContent(n: Note): string | null {
|
||||||
|
const content = (n.rawContent ?? n.content ?? '').toString();
|
||||||
|
if (!content) return null;
|
||||||
|
try {
|
||||||
|
const embed = content.match(/!\[\[(.*?)\]\]/);
|
||||||
|
if (embed && embed[1]) {
|
||||||
|
const name = embed[1].trim();
|
||||||
|
if (name) {
|
||||||
|
const notePath = (n.filePath || n.originalPath || '').replace(/\\/g, '/');
|
||||||
|
return `/api/attachments/resolve?name=${encodeURIComponent(name)}¬e=${encodeURIComponent(notePath)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const md = content.match(/!\[[^\]]*\]\(([^\)\s]+)(?:\s+\"[^\"]*\")?\)/);
|
||||||
|
if (md && md[1]) {
|
||||||
|
const p = md[1].trim();
|
||||||
|
if (/^(data:|https?:)/i.test(p)) return p;
|
||||||
|
if (p.startsWith('/')) return `/vault/${encodeURI(p.replace(/^\/+/, ''))}`;
|
||||||
|
const notePath = (n.filePath || n.originalPath || '').replace(/\\/g, '/');
|
||||||
|
return `/api/attachments/resolve?name=${encodeURIComponent(p)}¬e=${encodeURIComponent(notePath)}`;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getThumbnailSrc(n: Note): string | null {
|
||||||
|
try {
|
||||||
|
const kind = this.fileTypes.getViewerType(n.filePath, n.rawContent ?? n.content);
|
||||||
|
if (kind === 'image') {
|
||||||
|
return `/vault/${encodeURI(n.filePath)}`;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return this.extractFirstImageFromContent(n);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Multiple Selection Methods ============
|
// ============ Multiple Selection Methods ============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -138,16 +138,25 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="title text-sm truncate">{{ note.title }}</div>
|
<div class="title text-sm truncate">{{ note.title }}</div>
|
||||||
<div class="meta text-xs truncate">{{ note.filePath }}</div>
|
<div class="meta text-xs truncate">{{ note.filePath }}</div>
|
||||||
|
<div *ngIf="getUpdatedTimestampById(note) as ts" class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">🕓 {{ formatDateTime(ts) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detailed View -->
|
<!-- Detailed View -->
|
||||||
<div *ngIf="state.viewMode() === 'detailed'" class="note-inner flex items-start gap-2 space-y-0">
|
<div *ngIf="state.viewMode() === 'detailed'" class="note-inner flex flex-col sm:flex-row items-start gap-4">
|
||||||
<span class="note-color-dot flex-shrink-0 mt-1" [style.backgroundColor]="getNoteColorById(note.id)" aria-hidden="true"></span>
|
<div class="flex items-start gap-2 min-w-0 flex-1">
|
||||||
<span class="flex-shrink-0 mt-0.5" title="Type">{{ typeIcon(note.filePath) }}</span>
|
<span class="note-color-dot flex-shrink-0 mt-1" [style.backgroundColor]="getNoteColorById(note.id)" aria-hidden="true"></span>
|
||||||
<div class="min-w-0 flex-1 space-y-1.5">
|
<span class="flex-shrink-0 mt-0.5" title="Type">{{ typeIcon(note.filePath) }}</span>
|
||||||
<div class="title text-sm truncate">{{ note.title }}</div>
|
<div class="min-w-0 flex-1 space-y-1.5">
|
||||||
<div class="meta text-xs truncate">{{ note.filePath }}</div>
|
<div class="title text-sm truncate">{{ note.title }}</div>
|
||||||
|
<div class="meta text-xs truncate">{{ note.filePath }}</div>
|
||||||
|
<div *ngIf="getAuthorById(note.id) as a" class="text-xs text-gray-500 dark:text-gray-400 truncate">✍️ {{ a }}</div>
|
||||||
|
<div *ngIf="getDescriptionById(note.id) as d" class="text-xs text-muted truncate">{{ d }}</div>
|
||||||
|
<div *ngIf="getUpdatedTimestampById(note) as ts" class="text-xs text-gray-500 dark:text-gray-400">🕓 {{ formatDateTime(ts) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 sm:ml-4 mt-2 sm:mt-0" *ngIf="getThumbnailSrcById(note) as imgSrc">
|
||||||
|
<img [src]="imgSrc" alt="" class="rounded-md shadow-sm object-cover w-24 h-24" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -1030,6 +1039,82 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
|||||||
} catch { return '📎'; }
|
} catch { return '📎'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metadata helpers used by Comfortable/Detailed modes
|
||||||
|
getUpdatedTimestampById(meta: NoteMetadata): number | null {
|
||||||
|
try {
|
||||||
|
const full = this.getFullNoteById(meta.id) as any;
|
||||||
|
const m = Number(full?.mtime || 0);
|
||||||
|
if (m && !Number.isNaN(m)) return m;
|
||||||
|
} catch {}
|
||||||
|
const parse = (s?: string) => {
|
||||||
|
if (!s) return 0;
|
||||||
|
const t = Date.parse(s);
|
||||||
|
return Number.isFinite(t) ? t : 0;
|
||||||
|
};
|
||||||
|
const u = parse(meta.updatedAt);
|
||||||
|
if (u) return u;
|
||||||
|
const c = parse(meta.createdAt);
|
||||||
|
return c || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDateTime(ts: number): string {
|
||||||
|
try {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return d.toLocaleString('fr-FR', { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
|
} catch { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorById(id: string): string | null {
|
||||||
|
const full = this.getFullNoteById(id) as any;
|
||||||
|
const fm = (full?.frontmatter || {}) as any;
|
||||||
|
const v = (fm.author ?? fm.auteur ?? full?.author) as any;
|
||||||
|
return (typeof v === 'string' && v.trim().length > 0) ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescriptionById(id: string): string | null {
|
||||||
|
const full = this.getFullNoteById(id) as any;
|
||||||
|
const fm = (full?.frontmatter || {}) as any;
|
||||||
|
const v = (fm.description ?? fm.desc) as any;
|
||||||
|
return (typeof v === 'string' && v.trim().length > 0) ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractFirstImageFromFull(full: any): string | null {
|
||||||
|
if (!full) return null;
|
||||||
|
const content = String(full.rawContent ?? full.content ?? '');
|
||||||
|
if (!content) return null;
|
||||||
|
try {
|
||||||
|
const embed = content.match(/!\[\[(.*?)\]\]/);
|
||||||
|
if (embed && embed[1]) {
|
||||||
|
const name = embed[1].trim();
|
||||||
|
if (name) {
|
||||||
|
const notePath = String(full.filePath || full.originalPath || '').replace(/\\/g, '/');
|
||||||
|
return `/api/attachments/resolve?name=${encodeURIComponent(name)}¬e=${encodeURIComponent(notePath)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const md = content.match(/!\[[^\]]*\]\(([^\)\s]+)(?:\s+\"[^\"]*\")?\)/);
|
||||||
|
if (md && md[1]) {
|
||||||
|
const p = md[1].trim();
|
||||||
|
if (/^(data:|https?:)/i.test(p)) return p;
|
||||||
|
if (p.startsWith('/')) return `/vault/${encodeURI(p.replace(/^\/+/, ''))}`;
|
||||||
|
const notePath = String(full.filePath || full.originalPath || '').replace(/\\/g, '/');
|
||||||
|
return `/api/attachments/resolve?name=${encodeURIComponent(p)}¬e=${encodeURIComponent(notePath)}`;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getThumbnailSrcById(meta: NoteMetadata): string | null {
|
||||||
|
try {
|
||||||
|
const full = this.getFullNoteById(meta.id) as any;
|
||||||
|
const path = full?.filePath || meta.filePath || '';
|
||||||
|
const kind = this.fileTypes.getViewerType(path, full?.rawContent ?? full?.content ?? '');
|
||||||
|
if (kind === 'image') {
|
||||||
|
return `/vault/${encodeURI(path)}`;
|
||||||
|
}
|
||||||
|
return this.extractFirstImageFromFull(full);
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
// Sort/View menus
|
// Sort/View menus
|
||||||
toggleSortMenu(): void { this.sortMenuOpen.set(!this.sortMenuOpen()); this.viewModeMenuOpen.set(false); }
|
toggleSortMenu(): void { this.sortMenuOpen.set(!this.sortMenuOpen()); this.viewModeMenuOpen.set(false); }
|
||||||
toggleViewModeMenu(): void { this.viewModeMenuOpen.set(!this.viewModeMenuOpen()); this.sortMenuOpen.set(false); }
|
toggleViewModeMenu(): void { this.viewModeMenuOpen.set(!this.viewModeMenuOpen()); this.sortMenuOpen.set(false); }
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, HostListener, Input, Output, inject, effect, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
import { Component, EventEmitter, HostListener, Input, Output, inject, effect, signal, computed, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { UiModeService } from '../../shared/services/ui-mode.service';
|
import { UiModeService } from '../../shared/services/ui-mode.service';
|
||||||
import { ResponsiveService } from '../../shared/services/responsive.service';
|
import { ResponsiveService } from '../../shared/services/responsive.service';
|
||||||
@ -431,6 +431,22 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | 'bookmarks' | null = null;
|
quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | 'bookmarks' | null = null;
|
||||||
private suppressNextNoteSelection = false;
|
private suppressNextNoteSelection = false;
|
||||||
|
|
||||||
|
// Signaux pour tracker les filtres et déclencher la sélection automatique
|
||||||
|
private filterStateSignal = signal<{
|
||||||
|
tag: string | null;
|
||||||
|
folder: string | null;
|
||||||
|
quick: string | null;
|
||||||
|
search: string;
|
||||||
|
}>({
|
||||||
|
tag: null,
|
||||||
|
folder: null,
|
||||||
|
quick: null,
|
||||||
|
search: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signal pour forcer un recalcul de la liste filtrée
|
||||||
|
private filterChangeCounter = signal(0);
|
||||||
|
|
||||||
// --- URL State <-> Layout sync ---
|
// --- URL State <-> Layout sync ---
|
||||||
private mapUrlQuickToInternal(q: string | null): AppShellNimbusLayoutComponent['quickLinkFilter'] {
|
private mapUrlQuickToInternal(q: string | null): AppShellNimbusLayoutComponent['quickLinkFilter'] {
|
||||||
switch ((q || '').toLowerCase()) {
|
switch ((q || '').toLowerCase()) {
|
||||||
@ -525,7 +541,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = norm || null;
|
this.tagFilter = norm || null;
|
||||||
this.folderFilter = null;
|
this.folderFilter = null;
|
||||||
this.quickLinkFilter = null;
|
this.quickLinkFilter = null;
|
||||||
if (!hasNote) this.autoSelectFirstNote();
|
if (!hasNote) this.notifyFilterChange();
|
||||||
if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list');
|
if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
// Auto-open tags flyout when tag filter is active
|
// Auto-open tags flyout when tag filter is active
|
||||||
@ -538,7 +554,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.folderFilter = folder || null;
|
this.folderFilter = folder || null;
|
||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = null;
|
this.quickLinkFilter = null;
|
||||||
if (!hasNote) this.autoSelectFirstNote();
|
if (!hasNote) this.notifyFilterChange();
|
||||||
if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list');
|
if (!this.responsive.isDesktop()) this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
// Auto-open folders flyout when folder filter is active
|
// Auto-open folders flyout when folder filter is active
|
||||||
@ -552,12 +568,12 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.quickLinkFilter = internal;
|
this.quickLinkFilter = internal;
|
||||||
this.folderFilter = null;
|
this.folderFilter = null;
|
||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.autoSelectFirstNote();
|
this.notifyFilterChange();
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.autoSelectFirstNote();
|
this.notifyFilterChange();
|
||||||
}
|
}
|
||||||
// Auto-open quick flyout when quick filter is active
|
// Auto-open quick flyout when quick filter is active
|
||||||
if (this.hoveredFlyout !== 'quick') {
|
if (this.hoveredFlyout !== 'quick') {
|
||||||
@ -571,7 +587,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.folderFilter = null;
|
this.folderFilter = null;
|
||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = null;
|
this.quickLinkFilter = null;
|
||||||
this.autoSelectFirstNote();
|
this.notifyFilterChange();
|
||||||
}
|
}
|
||||||
this.suppressNextNoteSelection = false;
|
this.suppressNextNoteSelection = false;
|
||||||
// Close any open flyout when no filters
|
// Close any open flyout when no filters
|
||||||
@ -589,11 +605,58 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Effect pour sélection automatique quand les filtres changent
|
||||||
|
private _autoSelectEffect = effect(() => {
|
||||||
|
// Écouter le signal de changement de filtre
|
||||||
|
const filterState = this.filterStateSignal();
|
||||||
|
const counter = this.filterChangeCounter();
|
||||||
|
|
||||||
|
// Éviter l'exécution lors de l'initialisation
|
||||||
|
if (counter === 0) return;
|
||||||
|
|
||||||
|
console.log('🎯 Auto-select effect triggered:', { filterState, counter });
|
||||||
|
|
||||||
|
// Utiliser queueMicrotask pour s'assurer que la liste est mise à jour
|
||||||
|
queueMicrotask(() => {
|
||||||
|
this.autoSelectFirstNote(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Méthode helper pour notifier les changements de filtres
|
||||||
|
private notifyFilterChange() {
|
||||||
|
const currentState = {
|
||||||
|
tag: this.tagFilter,
|
||||||
|
folder: this.folderFilter,
|
||||||
|
quick: this.quickLinkFilter,
|
||||||
|
search: this.listQuery
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🔔 Notifying filter change:', currentState);
|
||||||
|
|
||||||
|
// Mettre à jour le signal d'état
|
||||||
|
this.filterStateSignal.set(currentState);
|
||||||
|
|
||||||
|
// Incrémenter le compteur pour déclencher l'effect
|
||||||
|
this.filterChangeCounter.update(c => c + 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-select first note when filters change
|
// Auto-select first note when filters change
|
||||||
private autoSelectFirstNote() {
|
private autoSelectFirstNote(forceSelection = false) {
|
||||||
const filteredNotes = this.getFilteredNotes();
|
const filteredNotes = this.getFilteredNotes();
|
||||||
if (filteredNotes.length > 0 && filteredNotes[0].id !== this.selectedNoteId) {
|
console.log('🎯 autoSelectFirstNote called:', {
|
||||||
this.noteSelected.emit(filteredNotes[0].id);
|
forceSelection,
|
||||||
|
notesCount: filteredNotes.length,
|
||||||
|
firstNoteId: filteredNotes[0]?.id,
|
||||||
|
selectedNoteId: this.selectedNoteId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredNotes.length > 0) {
|
||||||
|
const firstNote = filteredNotes[0];
|
||||||
|
// Forcer la sélection si demandé, ou si la note est différente
|
||||||
|
if (forceSelection || firstNote.id !== this.selectedNoteId) {
|
||||||
|
console.log('✅ Emitting noteSelected for:', firstNote.title, firstNote.id);
|
||||||
|
this.noteSelected.emit(firstNote.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,8 +748,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
|
|
||||||
onQueryChange(query: string) {
|
onQueryChange(query: string) {
|
||||||
this.listQuery = query;
|
this.listQuery = query;
|
||||||
// Auto-select first note on any list change including search updates
|
// Notifier le changement pour déclencher la sélection automatique
|
||||||
this.autoSelectFirstNote();
|
this.notifyFilterChange();
|
||||||
// Sync URL search term
|
// Sync URL search term
|
||||||
this.urlState.updateSearch(query);
|
this.urlState.updateSearch(query);
|
||||||
}
|
}
|
||||||
@ -747,6 +810,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
|
|
||||||
onClearFolderFromList() {
|
onClearFolderFromList() {
|
||||||
this.folderFilter = null;
|
this.folderFilter = null;
|
||||||
|
this.notifyFilterChange();
|
||||||
this.urlState.clearFolderFilter();
|
this.urlState.clearFolderFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -807,7 +871,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
try { this.filters.clearKinds(); } catch {}
|
try { this.filters.clearKinds(); } catch {}
|
||||||
try { this.filters.clearTags(); } catch {}
|
try { this.filters.clearTags(); } catch {}
|
||||||
this.autoSelectFirstNote();
|
// Notifier le changement pour déclencher la sélection automatique
|
||||||
|
this.notifyFilterChange();
|
||||||
if (this.responsive.isMobile() || this.responsive.isTablet()) {
|
if (this.responsive.isMobile() || this.responsive.isTablet()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
@ -829,7 +894,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
try { this.filters.clearKinds(); } catch {}
|
try { this.filters.clearKinds(); } catch {}
|
||||||
try { this.filters.clearTags(); } catch {}
|
try { this.filters.clearTags(); } catch {}
|
||||||
this.autoSelectFirstNote();
|
// Notifier le changement pour déclencher la sélection automatique
|
||||||
|
this.notifyFilterChange();
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
this.mobileNav.sidebarOpen.set(false);
|
this.mobileNav.sidebarOpen.set(false);
|
||||||
if (path) {
|
if (path) {
|
||||||
@ -987,8 +1053,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.urlState.setQuickWithMarkdown(label);
|
this.urlState.setQuickWithMarkdown(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Auto-select first note after filter changes
|
// Notifier le changement pour déclencher la sélection automatique
|
||||||
this.autoSelectFirstNote();
|
this.notifyFilterChange();
|
||||||
this.suppressNextNoteSelection = false;
|
this.suppressNextNoteSelection = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1000,8 +1066,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
// Clear other filters and search to focus on tag results
|
// Clear other filters and search to focus on tag results
|
||||||
this.quickLinkFilter = null;
|
this.quickLinkFilter = null;
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
// Auto-select first note after filter changes
|
// Notifier le changement pour déclencher la sélection automatique
|
||||||
this.autoSelectFirstNote();
|
this.notifyFilterChange();
|
||||||
// Ensure the list is visible: exit fullscreen if active
|
// Ensure the list is visible: exit fullscreen if active
|
||||||
if (this.noteFullScreen) {
|
if (this.noteFullScreen) {
|
||||||
this.noteFullScreen = false;
|
this.noteFullScreen = false;
|
||||||
@ -1104,6 +1170,6 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = null;
|
this.quickLinkFilter = null;
|
||||||
this.listQuery = '';
|
this.listQuery = '';
|
||||||
this.autoSelectFirstNote();
|
this.notifyFilterChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
---
|
---
|
||||||
titre: test
|
titre: "test"
|
||||||
auteur: Bruno Charest
|
auteur: "Bruno Charest"
|
||||||
creation_date: 2025-09-25T07:45:20-04:00
|
creation_date: "2025-09-25T07:45:20-04:00"
|
||||||
modification_date: 2025-11-02T12:07:38-04:00
|
modification_date: "2025-11-02T12:07:38-04:00"
|
||||||
catégorie: ""
|
tags: [""]
|
||||||
tags: []
|
status: "en-cours"
|
||||||
aliases:
|
|
||||||
- ""
|
|
||||||
status: en-cours
|
|
||||||
publish: true
|
publish: true
|
||||||
favoris: false
|
favoris: false
|
||||||
template: true
|
template: true
|
||||||
@ -15,14 +12,14 @@ task: true
|
|||||||
archive: true
|
archive: true
|
||||||
draft: true
|
draft: true
|
||||||
private: true
|
private: true
|
||||||
first_name: Bruno
|
first_name: "Bruno"
|
||||||
birth_date: 2025-06-18
|
birth_date: "2025-06-18"
|
||||||
email: bruno.charest@gmail.com
|
email: "bruno.charest@gmail.com"
|
||||||
number: "12345"
|
number: "12345"
|
||||||
todo: false
|
todo: false
|
||||||
url: https://google.com
|
url: "https://google.com"
|
||||||
image: https://images.unsplash.com/photo-1675789652575-0a5d2425b6c2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80
|
image: "https://images.unsplash.com/photo-1675789652575-0a5d2425b6c2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"
|
||||||
color: "#64748B"
|
color: "#22C55E"
|
||||||
---
|
---
|
||||||
# Test 1 Markdown
|
# Test 1 Markdown
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ publish: false
|
|||||||
favoris: true
|
favoris: true
|
||||||
template: false
|
template: false
|
||||||
task: false
|
task: false
|
||||||
archive: false
|
archive: true
|
||||||
draft: false
|
draft: false
|
||||||
private: false
|
private: false
|
||||||
description: "Stargate Atlantis: une expédition militaire et scientifique découvre la cité mythique d'Atlantis dans la galaxie de Pégase et affronte les Wraiths."
|
description: "Stargate Atlantis: une expédition militaire et scientifique découvre la cité mythique d'Atlantis dans la galaxie de Pégase et affronte les Wraiths."
|
||||||
|
|||||||
@ -12,24 +12,9 @@ archive: false
|
|||||||
draft: true
|
draft: true
|
||||||
private: false
|
private: false
|
||||||
titre: ""
|
titre: ""
|
||||||
Les Compléments Alimentaires: "Un Guide Général"
|
|
||||||
catégorie: ""
|
|
||||||
readOnly: false
|
readOnly: false
|
||||||
description: "Les Compléments Alimentaires : Un Guide Général Dans notre quête constante de bien-être et de..."
|
description: "Les Compléments Alimentaires : Un Guide Général Dans notre quête constante de bien-être et de..."
|
||||||
tags:
|
color: "#00AEEF"
|
||||||
- supplments
|
|
||||||
- tag2
|
|
||||||
- configuration
|
|
||||||
- accueil
|
|
||||||
- home
|
|
||||||
- markdown
|
|
||||||
- bruno
|
|
||||||
- tag4
|
|
||||||
- tag3
|
|
||||||
- test
|
|
||||||
- tag1
|
|
||||||
- test2
|
|
||||||
- tagtag
|
|
||||||
---
|
---
|
||||||
## Les Compléments Alimentaires : Un Guide Général
|
## Les Compléments Alimentaires : Un Guide Général
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user