From c9cb8dddfaf727d678b1e09c6a965018eef7ae47 Mon Sep 17 00:00:00 2001 From: Maikel Hajiabadi <89179997+hajiboy95@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:18:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(search):=20include=20note=20content=20in?= =?UTF-8?q?=20global=20search=20and=20support=20folde=E2=80=A6=20(#8044)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(search): include note content in global search and support folder path disambiguation * fix(i18n): revert accidental changes to de.json * fix(notes): implement robust retry mechanism for focusing notes * fix(search): improve reactivity for folder path labels and archive cache invalidation * fix(notes): prevent stale focus retry loops * test(search): add regression spec for folder path reactivity * fix(notes): track and cancel stale focusItem retry loops * test(search): add regression test for archive cache invalidation on folder map changes * fix(search): improve note highlighting cleanup and remove debug log --- docs/wiki/4.10-Task-Notes.md | 2 +- .../features/note/notes/notes.component.html | 1 + .../features/note/notes/notes.component.scss | 8 + .../features/note/notes/notes.component.ts | 58 ++++++ .../search-page/search-page.component.html | 3 + .../search-page/search-page.component.spec.ts | 141 ++++++++++++++ .../search-page/search-page.component.ts | 178 ++++++++++++++++-- .../pages/search-page/search-page.model.ts | 1 + src/assets/i18n/en.json | 6 +- 9 files changed, 375 insertions(+), 23 deletions(-) diff --git a/docs/wiki/4.10-Task-Notes.md b/docs/wiki/4.10-Task-Notes.md index 4517932dba..55f1cb84be 100644 --- a/docs/wiki/4.10-Task-Notes.md +++ b/docs/wiki/4.10-Task-Notes.md @@ -28,7 +28,7 @@ The note system balances structure and flexibility so notes can serve as both pl 2. **Markdown flexibility** — Notes support standard markdown: headings, lists, bold, links, and so on. You can write free-form text or use list syntax that the app can treat as a checklist. Rendered checklist items can be toggled from the checkbox or item text, while links keep their normal link behavior. You are not locked into a fixed form. -3. **Context-aware storage** — Notes are stored as simple text attached to a specific task (or, for project notes, to a project). That keeps them easy to find and keeps the data model simple—no heavy schema, just content tied to the right context. +3. **Context-aware storage** — Notes are stored as simple text attached to a specific task (or, for project notes, to a project). That keeps them easy to find and keeps the data model simple—no heavy schema, just content tied to the right context. Both project notes and task notes can also be found via the global search (Shift+F), allowing you to quickly jump to and highlight note content across different projects. 4. **Minimal schema** — The note model stays minimal: content, optional image reference, and basic metadata. That avoids over-structuring and keeps notes fast to use for ad hoc capture while still supporting templates and checklists when you want them. diff --git a/src/app/features/note/notes/notes.component.html b/src/app/features/note/notes/notes.component.html index 6216d8b955..c44a654b48 100644 --- a/src/app/features/note/notes/notes.component.html +++ b/src/app/features/note/notes/notes.component.html @@ -32,6 +32,7 @@ @for (note of notes; track note.id) {
diff --git a/src/app/features/note/notes/notes.component.scss b/src/app/features/note/notes/notes.component.scss index 2f58f8bde2..4c0bee2d3b 100644 --- a/src/app/features/note/notes/notes.component.scss +++ b/src/app/features/note/notes/notes.component.scss @@ -45,3 +45,11 @@ note { display: flex; z-index: 2; } + +.highlight-searched-item note ::ng-deep .note { + outline: 2px solid var(--c-accent) !important; + box-shadow: var(--whiteframe-shadow-6dp) !important; + transition: + outline 0.2s ease, + box-shadow 0.2s ease; +} diff --git a/src/app/features/note/notes/notes.component.ts b/src/app/features/note/notes/notes.component.ts index 486a8ad875..5ddad4ca8b 100644 --- a/src/app/features/note/notes/notes.component.ts +++ b/src/app/features/note/notes/notes.component.ts @@ -5,7 +5,9 @@ import { inject, viewChild, OnInit, + DestroyRef, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NoteService } from '../note.service'; import { MatButton } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; @@ -25,6 +27,8 @@ import { HISTORY_STATE } from '../../../app.constants'; import { dragDelayForTouch } from '../../../util/input-intent'; import { LayoutService } from 'src/app/core-ui/layout/layout.service'; import { IS_MOBILE } from 'src/app/util/is-mobile'; +import { ActivatedRoute } from '@angular/router'; +import { distinctUntilChanged, map } from 'rxjs/operators'; @Component({ selector: 'notes', @@ -47,6 +51,11 @@ export class NotesComponent implements OnInit { workContextService = inject(WorkContextService); private _matDialog = inject(MatDialog); private _layoutService = inject(LayoutService); + private _activatedRoute = inject(ActivatedRoute); + private _destroyRef = inject(DestroyRef); + + private _focusToken?: object; + private _focusNoteTimeout?: number; T: typeof T = T; isElementWasAdded: boolean = false; @@ -93,6 +102,55 @@ export class NotesComponent implements OnInit { window.history.pushState({ [HISTORY_STATE.NOTES]: true }, ''); } } + + this._destroyRef.onDestroy(() => { + this._focusToken = undefined; + window.clearTimeout(this._focusNoteTimeout); + }); + + this._activatedRoute.queryParams + .pipe( + map((params) => params.focusItem), + distinctUntilChanged(), + takeUntilDestroyed(this._destroyRef), + ) + .subscribe((focusItem) => { + if (focusItem) { + this._focusNote(focusItem); + } else { + this._focusToken = undefined; + window.clearTimeout(this._focusNoteTimeout); + } + }); + } + + private _focusNote(noteId: string): void { + document.querySelectorAll('.highlight-searched-item').forEach((el) => { + el.classList.remove('highlight-searched-item'); + }); + const id = `n-${noteId}`; + const startTime = Date.now(); + const timeout = 4000; + const token = {}; + this._focusToken = token; + window.clearTimeout(this._focusNoteTimeout); + + const tryFocus = (): void => { + if (this._focusToken !== token) { + return; + } + const el = document.getElementById(id); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + el.classList.add('highlight-searched-item'); + setTimeout(() => { + el.classList.remove('highlight-searched-item'); + }, 3000); + } else if (Date.now() - startTime < timeout) { + this._focusNoteTimeout = window.setTimeout(tryFocus, 100); + } + }; + tryFocus(); } @HostListener('window:popstate') diff --git a/src/app/pages/search-page/search-page.component.html b/src/app/pages/search-page/search-page.component.html index 7e3d31b59f..add8ec3c2d 100644 --- a/src/app/pages/search-page/search-page.component.html +++ b/src/app/pages/search-page/search-page.component.html @@ -57,6 +57,9 @@ class="issue-icon" > } + @if (item.isNote) { + comment + } @if (item.isArchiveTask) { archive