chore: delete unused trash explorer documentation files
This commit is contained in:
@@ -659,6 +659,19 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
// Initialize theme from storage
|
||||
this.themeService.initFromStorage();
|
||||
|
||||
// Initialize vault with metadata-first approach (Phase 1)
|
||||
this.vaultService.initializeVault().then(() => {
|
||||
console.log(`[AppComponent] Vault initialized with ${this.vaultService.getNotesCount()} notes`);
|
||||
this.logService.log('VAULT_INITIALIZED', {
|
||||
notesCount: this.vaultService.getNotesCount()
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('[AppComponent] Failed to initialize vault:', error);
|
||||
this.logService.log('VAULT_INIT_FAILED', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}, 'error');
|
||||
});
|
||||
|
||||
// Log app start
|
||||
this.logService.log('APP_START', {
|
||||
viewport: {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Pagination Configuration
|
||||
* Centralized configuration for pagination and virtual scrolling
|
||||
*/
|
||||
|
||||
export const PAGINATION_CONFIG = {
|
||||
// Page size for API requests (items per page)
|
||||
// Must be <= 500 (server-side limit)
|
||||
PAGE_SIZE: 100,
|
||||
|
||||
// Virtual scroll item height in pixels
|
||||
// Must match the actual height of rendered items
|
||||
ITEM_HEIGHT: 60,
|
||||
|
||||
// Number of items to preload before reaching the end
|
||||
// Higher = more preloading, lower = less memory
|
||||
PRELOAD_THRESHOLD: 20,
|
||||
|
||||
// Maximum page size allowed by server
|
||||
MAX_PAGE_SIZE: 500,
|
||||
|
||||
// Minimum page size
|
||||
MIN_PAGE_SIZE: 10,
|
||||
|
||||
// API endpoint for paginated metadata
|
||||
PAGINATED_METADATA_ENDPOINT: '/api/vault/metadata/paginated',
|
||||
|
||||
// Debounce delay for search input (ms)
|
||||
SEARCH_DEBOUNCE_MS: 300,
|
||||
|
||||
// Cache TTL in milliseconds (not currently used, but available for future optimization)
|
||||
CACHE_TTL_MS: 5 * 60 * 1000, // 5 minutes
|
||||
|
||||
// Enable logging for debugging
|
||||
DEBUG: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Get effective page size based on configuration
|
||||
*/
|
||||
export function getEffectivePageSize(): number {
|
||||
const size = PAGINATION_CONFIG.PAGE_SIZE;
|
||||
return Math.max(
|
||||
PAGINATION_CONFIG.MIN_PAGE_SIZE,
|
||||
Math.min(size, PAGINATION_CONFIG.MAX_PAGE_SIZE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preload threshold as number of items
|
||||
*/
|
||||
export function getPreloadThreshold(): number {
|
||||
return PAGINATION_CONFIG.PRELOAD_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get virtual scroll item height
|
||||
*/
|
||||
export function getItemHeight(): number {
|
||||
return PAGINATION_CONFIG.ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug message if debugging is enabled
|
||||
*/
|
||||
export function debugLog(message: string, data?: any): void {
|
||||
if (PAGINATION_CONFIG.DEBUG) {
|
||||
console.log(`[Pagination] ${message}`, data || '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { Component, EventEmitter, Output, input, signal, computed, effect, inject, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
import { PaginationService, NoteMetadata } from '../../services/pagination.service';
|
||||
import { TagFilterStore } from '../../core/stores/tag-filter.store';
|
||||
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-paginated-notes-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ScrollingModule, ScrollableOverlayDirective],
|
||||
template: `
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Search and filters header -->
|
||||
<div class="p-2 border-b border-border dark:border-gray-800 space-y-2">
|
||||
<div *ngIf="activeTag() as t" class="flex items-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-surface1 dark:bg-card text-main dark:text-main px-2 py-1">
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
|
||||
Filtre: #{{ t }}
|
||||
</span>
|
||||
<button type="button" (click)="clearTagFilter()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-surface2/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="quickLinkFilter() && getQuickLinkDisplay(quickLinkFilter()) as ql" class="flex items-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-surface1 dark:bg-card text-main dark:text-main px-2 py-1">
|
||||
{{ ql.icon }} {{ ql.name }}
|
||||
</span>
|
||||
<button type="button" (click)="clearQuickLinkFilter.emit()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-surface2/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<input type="text"
|
||||
[value]="query()"
|
||||
(input)="onQuery($any($event.target).value)"
|
||||
placeholder="Rechercher..."
|
||||
class="w-full rounded border border-border dark:border-border bg-card dark:bg-main px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
</div>
|
||||
|
||||
<!-- Virtual scroll viewport -->
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<cdk-virtual-scroll-viewport
|
||||
#viewport
|
||||
itemSize="60"
|
||||
class="h-full w-full"
|
||||
(scrolledIndexChange)="onScroll($event)"
|
||||
appScrollableOverlay>
|
||||
|
||||
<ul class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<!-- Virtual items -->
|
||||
<li *cdkVirtualFor="let note of paginatedNotes(); trackBy: trackByFn"
|
||||
class="p-3 hover:bg-surface1 dark:hover:bg-card cursor-pointer transition-colors min-h-[60px] flex flex-col justify-center"
|
||||
[class.selected]="note.id === selectedNoteId()"
|
||||
(click)="selectNote(note)">
|
||||
<div class="text-sm font-semibold truncate">{{ note.title }}</div>
|
||||
<div class="text-xs text-muted truncate">{{ note.filePath }}</div>
|
||||
</li>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<li *ngIf="isLoadingMore()" class="p-4 text-center text-muted min-h-[60px] flex items-center justify-center">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<div class="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
||||
<span>Chargement...</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- End of list indicator -->
|
||||
<li *ngIf="!hasMore() && totalLoaded() > 0" class="p-4 text-center text-muted min-h-[60px] flex items-center justify-center">
|
||||
{{ totalLoaded() }} notes chargées
|
||||
</li>
|
||||
|
||||
<!-- Empty state -->
|
||||
<li *ngIf="totalLoaded() === 0 && !isLoadingMore()" class="p-4 text-center text-muted min-h-[60px] flex items-center justify-center">
|
||||
Aucune note trouvée
|
||||
</li>
|
||||
</ul>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.notes-list-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.note-item.selected {
|
||||
background-color: var(--surface1);
|
||||
border-left: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
cdk-virtual-scroll-viewport {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
||||
private paginationService = inject(PaginationService);
|
||||
private store = inject(TagFilterStore);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@ViewChild(CdkVirtualScrollViewport) viewport?: CdkVirtualScrollViewport;
|
||||
|
||||
// Inputs
|
||||
folderFilter = input<string | null>(null);
|
||||
query = input<string>('');
|
||||
tagFilter = input<string | null>(null);
|
||||
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
|
||||
|
||||
// Outputs
|
||||
@Output() openNote = new EventEmitter<string>();
|
||||
@Output() queryChange = new EventEmitter<string>();
|
||||
@Output() clearQuickLinkFilter = new EventEmitter<void>();
|
||||
|
||||
// Local state
|
||||
private q = signal('');
|
||||
selectedNoteId = signal<string | null>(null);
|
||||
activeTag = signal<string | null>(null);
|
||||
|
||||
// Pagination state
|
||||
paginatedNotes = this.paginationService.allItems;
|
||||
isLoadingMore = this.paginationService.isLoadingMore;
|
||||
hasMore = this.paginationService.hasMore;
|
||||
totalLoaded = this.paginationService.totalLoaded;
|
||||
canLoadMore = this.paginationService.canLoadMore;
|
||||
|
||||
// Effects
|
||||
private syncQuery = effect(() => {
|
||||
this.q.set(this.query() || '');
|
||||
});
|
||||
|
||||
private syncTagFromStore = effect(() => {
|
||||
const inputTag = this.tagFilter();
|
||||
if (inputTag !== null && inputTag !== undefined) {
|
||||
this.activeTag.set(inputTag || null);
|
||||
return;
|
||||
}
|
||||
this.activeTag.set(this.store.get());
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
// Load initial page
|
||||
this.paginationService.loadInitial();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
// Handle virtual scroll
|
||||
onScroll(index: number) {
|
||||
const items = this.paginatedNotes();
|
||||
// Load more when approaching the end (20 items before the end)
|
||||
if (index > items.length - 20 && this.canLoadMore()) {
|
||||
this.paginationService.loadNextPage();
|
||||
}
|
||||
}
|
||||
|
||||
// Select a note
|
||||
selectNote(note: NoteMetadata) {
|
||||
this.selectedNoteId.set(note.id);
|
||||
this.openNote.emit(note.id);
|
||||
}
|
||||
|
||||
// Search
|
||||
onQuery(v: string) {
|
||||
this.q.set(v);
|
||||
this.queryChange.emit(v);
|
||||
// Trigger search with pagination
|
||||
this.paginationService.search(v);
|
||||
}
|
||||
|
||||
// Clear tag filter
|
||||
clearTagFilter(): void {
|
||||
this.activeTag.set(null);
|
||||
if (this.tagFilter() == null) {
|
||||
this.store.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Track by function for virtual scroll
|
||||
trackByFn(index: number, item: NoteMetadata): string {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
// Quick link display
|
||||
getQuickLinkDisplay(quickLink: string): { icon: string; name: string } | null {
|
||||
const displays: Record<string, { icon: string; name: string }> = {
|
||||
'favoris': { icon: '❤️', name: 'Favoris' },
|
||||
'publish': { icon: '🌐', name: 'Publish' },
|
||||
'draft': { icon: '📝', name: 'Draft' },
|
||||
'template': { icon: '📑', name: 'Template' },
|
||||
'task': { icon: '🗒️', name: 'Task' },
|
||||
'private': { icon: '🔒', name: 'Private' },
|
||||
'archive': { icon: '🗃️', name: 'Archive' }
|
||||
};
|
||||
return displays[quickLink] || null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Injectable, signal, computed, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface NoteMetadata {
|
||||
id: string;
|
||||
title: string;
|
||||
filePath: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PaginationResponse {
|
||||
items: NoteMetadata[];
|
||||
nextCursor: number | null;
|
||||
hasMore: boolean;
|
||||
total: number;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PaginationService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
// State management
|
||||
private pages = signal<Map<number, NoteMetadata[]>>(new Map());
|
||||
private currentCursor = signal<number | null>(null);
|
||||
private hasMorePages = signal(true);
|
||||
private isLoading = signal(false);
|
||||
private searchTerm = signal('');
|
||||
private totalItems = signal(0);
|
||||
|
||||
// Computed properties
|
||||
readonly allItems = computed(() => {
|
||||
const pages = this.pages();
|
||||
const result: NoteMetadata[] = [];
|
||||
for (const page of pages.values()) {
|
||||
result.push(...page);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
readonly totalLoaded = computed(() => this.allItems().length);
|
||||
readonly canLoadMore = computed(() => this.hasMorePages() && !this.isLoading());
|
||||
readonly isLoadingMore = this.isLoading;
|
||||
readonly hasMore = this.hasMorePages;
|
||||
|
||||
// Load initial page
|
||||
async loadInitial(search = ''): Promise<void> {
|
||||
this.searchTerm.set(search);
|
||||
this.pages.set(new Map());
|
||||
this.currentCursor.set(null);
|
||||
this.hasMorePages.set(true);
|
||||
this.totalItems.set(0);
|
||||
|
||||
await this.loadNextPage();
|
||||
}
|
||||
|
||||
// Load next page
|
||||
async loadNextPage(): Promise<void> {
|
||||
if (this.isLoading() || !this.hasMorePages()) return;
|
||||
|
||||
this.isLoading.set(true);
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
limit: 100,
|
||||
search: this.searchTerm()
|
||||
};
|
||||
|
||||
if (this.currentCursor() !== null) {
|
||||
params.cursor = this.currentCursor();
|
||||
}
|
||||
|
||||
const response = await firstValueFrom(
|
||||
this.http.get<PaginationResponse>('/api/vault/metadata/paginated', { params })
|
||||
);
|
||||
|
||||
// Add page to cache
|
||||
const pageIndex = this.pages().size;
|
||||
this.pages.update(pages => {
|
||||
const newPages = new Map(pages);
|
||||
newPages.set(pageIndex, response.items);
|
||||
return newPages;
|
||||
});
|
||||
|
||||
// Update state
|
||||
this.currentCursor.set(response.nextCursor);
|
||||
this.hasMorePages.set(response.hasMore);
|
||||
this.totalItems.set(response.total);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PaginationService] Failed to load page:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Search with new term
|
||||
async search(term: string): Promise<void> {
|
||||
await this.loadInitial(term);
|
||||
}
|
||||
|
||||
// Invalidate cache (after file changes)
|
||||
invalidateCache(): void {
|
||||
this.pages.set(new Map());
|
||||
this.currentCursor.set(null);
|
||||
this.hasMorePages.set(true);
|
||||
this.totalItems.set(0);
|
||||
}
|
||||
|
||||
// Get current search term
|
||||
getSearchTerm(): string {
|
||||
return this.searchTerm();
|
||||
}
|
||||
|
||||
// Get total items count
|
||||
getTotalItems(): number {
|
||||
return this.totalItems();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Re-export the main VaultService from src/services/vault.service.ts
|
||||
export { VaultService } from '../../services/vault.service';
|
||||
@@ -15,7 +15,10 @@ export type LogEvent =
|
||||
| 'THEME_CHANGE'
|
||||
| 'THEME_MODE_CHANGE'
|
||||
| 'LANGUAGE_CHANGE'
|
||||
| 'HELP_PAGE_OPEN';
|
||||
| 'HELP_PAGE_OPEN'
|
||||
| 'VAULT_INITIALIZED'
|
||||
| 'VAULT_INIT_FAILED'
|
||||
| 'NOTE_SELECTED';
|
||||
|
||||
export interface LogContext {
|
||||
route?: string;
|
||||
|
||||
@@ -174,6 +174,56 @@ export class VaultService implements OnDestroy {
|
||||
this.observeVaultEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize vault with metadata-first approach (Phase 1 optimization)
|
||||
* Loads only metadata initially, then loads full content on-demand
|
||||
*/
|
||||
async initializeVault(): Promise<void> {
|
||||
try {
|
||||
console.time('[VaultService] initializeVault');
|
||||
|
||||
// Load metadata only (fast)
|
||||
const metadata = await firstValueFrom(
|
||||
this.http.get<any[]>('/api/vault/metadata')
|
||||
);
|
||||
|
||||
console.timeEnd('[VaultService] initializeVault');
|
||||
console.log(`[VaultService] Loaded metadata for ${metadata.length} notes`);
|
||||
|
||||
// Process metadata to build indices and fast tree
|
||||
this.processMetadataForInitialization(metadata);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[VaultService] Failed to initialize vault with metadata-first:', error);
|
||||
// Fallback to regular refresh
|
||||
this.refreshNotes();
|
||||
}
|
||||
}
|
||||
|
||||
private processMetadataForInitialization(metadata: any[]): void {
|
||||
// Build indices for fast lookups
|
||||
for (const item of metadata) {
|
||||
const slugId = this.buildSlugIdFromPath(item.filePath);
|
||||
this.idToPathIndex.set(item.id, item.filePath);
|
||||
this.slugIdToPathIndex.set(slugId, item.filePath);
|
||||
|
||||
const fileMeta: FileMetadata = {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
path: item.filePath,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
};
|
||||
this.metaByPathIndex.set(item.filePath, fileMeta);
|
||||
}
|
||||
|
||||
// Rebuild fast file tree with metadata
|
||||
this.loadFastFileTree();
|
||||
|
||||
// Start observing vault events
|
||||
this.observeVaultEvents();
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
this.vaultEventsSubscription?.unsubscribe();
|
||||
this.vaultEventsSubscription = null;
|
||||
@@ -192,6 +242,13 @@ export class VaultService implements OnDestroy {
|
||||
return this.notesMap().get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of loaded notes
|
||||
*/
|
||||
getNotesCount(): number {
|
||||
return this.notesMap().size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the built-in help note content into the in-memory notes map.
|
||||
* This loads the help content from the embedded file.
|
||||
|
||||
Reference in New Issue
Block a user