feat: enhance note creation and navigation UX
- Added note creation event handlers to sync selection, scrolling and focus across components - Improved context menu styling with better opacity and z-index handling - Updated mobile layout to hide search input on larger screens - Fixed focus timing issues in move-note-to-folder component - Added URL state management for newly created notes - Removed unused archived note files - Added proper note ID tracking and scroll-into-view behavior - Enhanced error
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Output, computed, signal, effect, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, EventEmitter, Output, computed, signal, effect, inject, ChangeDetectionStrategy, ViewChild, ElementRef } from '@angular/core';
|
||||
import { input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import type { Note } from '../../../types';
|
||||
@@ -9,6 +9,7 @@ import { NoteCreationService } from '../../services/note-creation.service';
|
||||
import { NoteContextMenuComponent } from '../../../components/note-context-menu/note-context-menu.component';
|
||||
import { WarningPanelComponent } from '../../components/warning-panel/warning-panel.component';
|
||||
import { NoteContextMenuService } from '../../services/note-context-menu.service';
|
||||
import { UrlStateService } from '../../services/url-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notes-list',
|
||||
@@ -117,7 +118,7 @@ import { NoteContextMenuService } from '../../services/note-context-menu.service
|
||||
</div>
|
||||
|
||||
<!-- List Container -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay>
|
||||
<div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay #listContainer>
|
||||
<!-- Empty state when a folder is selected but no notes match -->
|
||||
<div *ngIf="(folderFilter() && filtered().length === 0)" class="h-full min-h-[240px] flex items-center justify-center text-center p-6">
|
||||
<div>
|
||||
@@ -133,10 +134,12 @@ import { NoteContextMenuService } from '../../services/note-context-menu.service
|
||||
<ul class="notes-list">
|
||||
<li *ngFor="let n of filtered()"
|
||||
class="note-row cursor-pointer"
|
||||
[class.active]="selectedId() === n.id"
|
||||
[class.active]="(selectedId() === n.id) || (pendingSelectId() === n.id)"
|
||||
[ngClass]="getListItemClasses()"
|
||||
[ngStyle]="getNoteGradientStyle(n)"
|
||||
[attr.data-note-path]="n.filePath"
|
||||
[attr.data-note-id]="n.id"
|
||||
tabindex="-1"
|
||||
(click)="openNote.emit(n.id)"
|
||||
(contextmenu)="openContextMenu($event, n)">
|
||||
<!-- Compact View -->
|
||||
@@ -373,11 +376,15 @@ export class NotesListComponent {
|
||||
@Output() queryChange = new EventEmitter<string>();
|
||||
@Output() clearQuickLinkFilter = new EventEmitter<void>();
|
||||
@Output() noteCreated = new EventEmitter<string>();
|
||||
@Output() noteCreatedAndSelected = new EventEmitter<{ id: string; filePath: string }>();
|
||||
|
||||
private store = inject(TagFilterStore);
|
||||
readonly state = inject(NotesListStateService);
|
||||
private noteCreationService = inject(NoteCreationService);
|
||||
readonly contextMenuService = inject(NoteContextMenuService);
|
||||
@ViewChild('listContainer') listContainer?: ElementRef<HTMLElement>;
|
||||
private urlState = inject(UrlStateService);
|
||||
private pendingSelectId = signal<string | null>(null);
|
||||
|
||||
// Delete warning modal state
|
||||
deleteWarningOpen = signal<boolean>(false);
|
||||
@@ -435,6 +442,45 @@ export class NotesListComponent {
|
||||
}, 10);
|
||||
});
|
||||
|
||||
private scrollToSelectedEffect = effect(() => {
|
||||
const id = this.selectedId() || this.pendingSelectId();
|
||||
if (!id) return;
|
||||
const host = this.listContainer?.nativeElement;
|
||||
if (!host) return;
|
||||
|
||||
const tryFocus = () => {
|
||||
const el = host.querySelector(`li[data-note-id="${id}"]`) as HTMLElement | null;
|
||||
if (el) {
|
||||
try { el.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch {}
|
||||
try { el.focus({ preventScroll: true } as any); } catch {}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Attempt after microtask, then a few RAF retries to wait for DOM
|
||||
queueMicrotask(() => {
|
||||
if (tryFocus()) {
|
||||
// If parent hasn't yet reflected selection, keep local pending highlight
|
||||
return;
|
||||
}
|
||||
let attempts = 4;
|
||||
const raf = () => {
|
||||
if (tryFocus()) return;
|
||||
if (--attempts > 0) requestAnimationFrame(raf);
|
||||
};
|
||||
requestAnimationFrame(raf);
|
||||
});
|
||||
});
|
||||
|
||||
private clearPendingWhenSynced = effect(() => {
|
||||
const sel = this.selectedId();
|
||||
const pending = this.pendingSelectId();
|
||||
if (sel && pending && sel === pending) {
|
||||
this.pendingSelectId.set(null);
|
||||
}
|
||||
});
|
||||
|
||||
private syncTagFromStore = effect(() => {
|
||||
const inputTag = this.tagFilter();
|
||||
if (inputTag !== null && inputTag !== undefined) {
|
||||
@@ -603,8 +649,27 @@ export class NotesListComponent {
|
||||
|
||||
this.noteCreationService.createNote(fileName, folderPath)
|
||||
.then(response => {
|
||||
if (!response?.filePath || !response?.id) {
|
||||
console.error('Invalid response from note creation:', response);
|
||||
this.state.setRequestStats(false, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Set URL immediately with note + folder
|
||||
this.urlState.setNote(response.filePath, folderPath).catch(err => {
|
||||
console.error('Failed to update URL:', err);
|
||||
});
|
||||
|
||||
// 2) Local optimistic selection
|
||||
this.pendingSelectId.set(response.id);
|
||||
|
||||
// 3) Emit events for parent to handle scroll + focus
|
||||
this.noteCreated.emit(response.id);
|
||||
this.openNote.emit(response.id);
|
||||
this.noteCreatedAndSelected.emit({
|
||||
id: response.id,
|
||||
filePath: response.filePath
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to create note:', error);
|
||||
|
||||
+4
-1
@@ -132,7 +132,7 @@ export class MoveNoteToFolderComponent {
|
||||
|
||||
queueMicrotask(() => {
|
||||
this.computeMenuPosition();
|
||||
this.focusSearchField();
|
||||
setTimeout(() => this.focusSearchField(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -308,6 +308,9 @@ export class MoveNoteToFolderComponent {
|
||||
}
|
||||
|
||||
private focusSearchField(): void {
|
||||
if (!this.showMenu()) {
|
||||
return;
|
||||
}
|
||||
const el = this.searchField?.nativeElement;
|
||||
if (el) {
|
||||
el.focus();
|
||||
|
||||
@@ -70,6 +70,7 @@ import { UrlStateService } from '../../services/url-state.service';
|
||||
(testsPanelSelected)="onTestsPanelSelected()"
|
||||
(helpPageSelected)="onHelpPageSelected()"
|
||||
(aboutSelected)="onAboutSelected()"
|
||||
(noteCreated)="onNoteCreated($event)"
|
||||
/>
|
||||
</aside>
|
||||
</ng-container>
|
||||
@@ -185,6 +186,8 @@ import { UrlStateService } from '../../services/url-state.service';
|
||||
(openNote)="onOpenNote($event)"
|
||||
(queryChange)="onQueryChange($event)"
|
||||
(clearQuickLinkFilter)="onClearQuickLinkFilter()"
|
||||
(noteCreated)="onNoteCreated($event)"
|
||||
(noteCreatedAndSelected)="onNoteCreatedAndSelected($event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -260,7 +263,7 @@ import { UrlStateService } from '../../services/url-state.service';
|
||||
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (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" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" [selectedId]="selectedNoteId" (queryChange)="onQueryChange($event)" (openNote)="onOpenNote($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()"></app-notes-list>
|
||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" [selectedId]="selectedNoteId" (queryChange)="onQueryChange($event)" (openNote)="onOpenNote($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()" (noteCreated)="onNoteCreated($event)" (noteCreatedAndSelected)="onNoteCreatedAndSelected($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-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
|
||||
@@ -368,6 +371,8 @@ export class AppShellNimbusLayoutComponent {
|
||||
@Output() navigateHeading = new EventEmitter<string>();
|
||||
@Output() searchTermChange = new EventEmitter<string>();
|
||||
@Output() searchOptionsChange = new EventEmitter<any>();
|
||||
@Output() noteCreated = new EventEmitter<string>();
|
||||
@Output() noteCreatedAndSelected = new EventEmitter<{ id: string; filePath: string }>();
|
||||
@Output() markdownPlaygroundSelected = new EventEmitter<void>();
|
||||
@Output() parametersOpened = new EventEmitter<void>();
|
||||
@Output() helpPageRequested = new EventEmitter<void>();
|
||||
@@ -632,6 +637,14 @@ export class AppShellNimbusLayoutComponent {
|
||||
}
|
||||
}
|
||||
|
||||
onNoteCreated(noteId: string) {
|
||||
this.noteCreated.emit(noteId);
|
||||
}
|
||||
|
||||
onNoteCreatedAndSelected(event: { id: string; filePath: string }) {
|
||||
this.noteCreatedAndSelected.emit(event);
|
||||
}
|
||||
|
||||
onNoteSelectedMobile(noteId: string) {
|
||||
if (!noteId) return;
|
||||
this.noteSelected.emit(noteId);
|
||||
|
||||
@@ -348,6 +348,32 @@ export class UrlStateService implements OnDestroy {
|
||||
await this.updateUrl({ search: searchTerm || undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* Définir la note et optionnellement le dossier (pour création de note)
|
||||
* Utilise merge pour conserver les autres paramètres (search, etc.)
|
||||
*/
|
||||
async setNote(notePath: string, folderPath?: string): Promise<void> {
|
||||
const normalized = (notePath ?? '').trim()
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/+/, '');
|
||||
|
||||
if (!normalized) {
|
||||
console.warn('setNote() called with empty path');
|
||||
return;
|
||||
}
|
||||
|
||||
const queryParams: any = { note: normalized };
|
||||
if (folderPath) {
|
||||
queryParams.folder = folderPath.replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
await this.router.navigate([], {
|
||||
queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
preserveFragment: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour l'URL avec le nouvel état
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user