mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
feat(search): include note content in global search and support folde… (#8044)
* 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
This commit is contained in:
parent
e4ded3aeee
commit
c9cb8dddfa
9 changed files with 375 additions and 23 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<!---->
|
||||
@for (note of notes; track note.id) {
|
||||
<div
|
||||
[id]="'n-' + note.id"
|
||||
cdkDrag
|
||||
[cdkDragStartDelay]="dragDelayForTouch()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@
|
|||
class="issue-icon"
|
||||
></mat-icon>
|
||||
}
|
||||
@if (item.isNote) {
|
||||
<mat-icon class="issue-icon">comment</mat-icon>
|
||||
}
|
||||
@if (item.isArchiveTask) {
|
||||
<mat-icon class="archive-icon">archive</mat-icon>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { signal, WritableSignal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NoteService } from '../../features/note/note.service';
|
||||
import { Note } from '../../features/note/note.model';
|
||||
import { LayoutService } from '../../core-ui/layout/layout.service';
|
||||
import { MenuTreeService } from '../../features/menu-tree/menu-tree.service';
|
||||
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { TaskService } from '../../features/tasks/task.service';
|
||||
import { ProjectService } from '../../features/project/project.service';
|
||||
|
|
@ -55,15 +62,23 @@ describe('SearchPageComponent', () => {
|
|||
let archivedTasks: Task[];
|
||||
let projectList$: BehaviorSubject<Project[]>;
|
||||
let tags$: BehaviorSubject<Tag[]>;
|
||||
let notes$: BehaviorSubject<Note[]>;
|
||||
let projectFolderMapSignal: WritableSignal<Map<string, string>>;
|
||||
let tagFolderMapSignal: WritableSignal<Map<string, string>>;
|
||||
|
||||
let taskServiceSpy: jasmine.SpyObj<TaskService>;
|
||||
let navigateToTaskServiceSpy: jasmine.SpyObj<NavigateToTaskService>;
|
||||
let routerSpy: jasmine.SpyObj<Router>;
|
||||
let layoutServiceSpy: jasmine.SpyObj<LayoutService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
allTasks$ = new BehaviorSubject<Task[]>([]);
|
||||
archivedTasks = [];
|
||||
projectList$ = new BehaviorSubject<Project[]>([createProject()]);
|
||||
tags$ = new BehaviorSubject<Tag[]>([createTag()]);
|
||||
notes$ = new BehaviorSubject<Note[]>([]);
|
||||
projectFolderMapSignal = signal<Map<string, string>>(new Map());
|
||||
tagFolderMapSignal = signal<Map<string, string>>(new Map());
|
||||
|
||||
taskServiceSpy = jasmine.createSpyObj('TaskService', ['getArchivedTasks'], {
|
||||
allTasks$,
|
||||
|
|
@ -75,6 +90,13 @@ describe('SearchPageComponent', () => {
|
|||
]);
|
||||
navigateToTaskServiceSpy.navigate.and.returnValue(Promise.resolve());
|
||||
|
||||
routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||
routerSpy.navigate.and.returnValue(Promise.resolve(true));
|
||||
|
||||
layoutServiceSpy = jasmine.createSpyObj('LayoutService', ['toggleNotes'], {
|
||||
isShowNotes: signal(false),
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SearchPageComponent, NoopAnimationsModule, TranslateModule.forRoot()],
|
||||
providers: [
|
||||
|
|
@ -88,6 +110,16 @@ describe('SearchPageComponent', () => {
|
|||
provide: NavigateToTaskService,
|
||||
useValue: navigateToTaskServiceSpy,
|
||||
},
|
||||
{ provide: NoteService, useValue: { notes$ } },
|
||||
{ provide: Router, useValue: routerSpy },
|
||||
{ provide: LayoutService, useValue: layoutServiceSpy },
|
||||
{
|
||||
provide: MenuTreeService,
|
||||
useValue: {
|
||||
projectFolderMap: projectFolderMapSignal,
|
||||
tagFolderMap: tagFolderMapSignal,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(SearchPageComponent, {
|
||||
|
|
@ -384,4 +416,113 @@ describe('SearchPageComponent', () => {
|
|||
expect(latestResults[0].searchText).toContain('hello world');
|
||||
expect(latestResults[0].searchText).toContain('some notes');
|
||||
}));
|
||||
|
||||
it('should find notes by content (case-insensitive)', fakeAsync(() => {
|
||||
notes$.next([
|
||||
{
|
||||
id: 'n1',
|
||||
content: 'Check out this important note\nSecond line',
|
||||
created: 1000,
|
||||
} as any,
|
||||
]);
|
||||
initAndFlush();
|
||||
typeAndFlush('IMPORTANT');
|
||||
expect(latestResults.length).toBe(1);
|
||||
expect(latestResults[0].id).toBe('n1');
|
||||
expect(latestResults[0].title).toBe('Check out this important note');
|
||||
expect(latestResults[0].isNote).toBe(true);
|
||||
}));
|
||||
|
||||
it('should navigate to note correctly in navigateToItem', fakeAsync(() => {
|
||||
const item = {
|
||||
id: 'n1',
|
||||
isNote: true,
|
||||
projectId: 'proj-1',
|
||||
} as SearchItem;
|
||||
component.navigateToItem(item);
|
||||
tick();
|
||||
expect(routerSpy.navigate).toHaveBeenCalledWith(['/project/proj-1/tasks'], {
|
||||
queryParams: { focusItem: 'n1' },
|
||||
});
|
||||
expect(layoutServiceSpy.toggleNotes).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should navigate to Today notes correctly in navigateToItem when projectId is null', fakeAsync(() => {
|
||||
const item = {
|
||||
id: 'n2',
|
||||
isNote: true,
|
||||
projectId: null,
|
||||
} as SearchItem;
|
||||
component.navigateToItem(item);
|
||||
tick();
|
||||
expect(routerSpy.navigate).toHaveBeenCalledWith(['/tag/TODAY/tasks'], {
|
||||
queryParams: { focusItem: 'n2' },
|
||||
});
|
||||
expect(layoutServiceSpy.toggleNotes).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should include full folder path in context tag title for task', fakeAsync(() => {
|
||||
projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
|
||||
projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
|
||||
allTasks$.next([createTask({ id: 't1', title: 'Folder Task', projectId: 'proj-1' })]);
|
||||
initAndFlush();
|
||||
typeAndFlush('Folder');
|
||||
expect(latestResults.length).toBe(1);
|
||||
expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
|
||||
}));
|
||||
|
||||
it('should include full folder path in context tag title for note', fakeAsync(() => {
|
||||
projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
|
||||
projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
|
||||
notes$.next([
|
||||
{
|
||||
id: 'n1',
|
||||
content: 'Folder Note Content',
|
||||
projectId: 'proj-1',
|
||||
created: 1000,
|
||||
} as any,
|
||||
]);
|
||||
initAndFlush();
|
||||
typeAndFlush('Folder Note');
|
||||
expect(latestResults.length).toBe(1);
|
||||
expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
|
||||
}));
|
||||
|
||||
it('should update folder path reactively when projectFolderMap changes after init', fakeAsync(() => {
|
||||
projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
|
||||
allTasks$.next([createTask({ id: 't1', title: 'Folder Task', projectId: 'proj-1' })]);
|
||||
initAndFlush();
|
||||
typeAndFlush('Folder');
|
||||
expect(latestResults[0].ctx.title).toBe('My Project');
|
||||
|
||||
projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
|
||||
fixture.detectChanges();
|
||||
tick(150); // combineLatest debounce
|
||||
expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
|
||||
}));
|
||||
|
||||
it('should update folder path reactively for ARCHIVED tasks when projectFolderMap changes after init', fakeAsync(() => {
|
||||
const archiveTask = createTask({
|
||||
id: 'archived-1',
|
||||
title: 'Old Task',
|
||||
projectId: 'proj-1',
|
||||
});
|
||||
taskServiceSpy.getArchivedTasks.and.returnValue(Promise.resolve([archiveTask]));
|
||||
projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
|
||||
|
||||
// Recreate component so the new archive promise is used
|
||||
fixture = TestBed.createComponent(SearchPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
initAndFlush();
|
||||
typeAndFlush('Old');
|
||||
|
||||
expect(latestResults.length).toBe(1);
|
||||
expect(latestResults[0].isArchiveTask).toBe(true);
|
||||
expect(latestResults[0].ctx.title).toBe('My Project');
|
||||
|
||||
projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
|
||||
fixture.detectChanges();
|
||||
tick(150); // combineLatest debounce
|
||||
expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,12 +6,17 @@ import {
|
|||
OnInit,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { T } from '../../t.const';
|
||||
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { debounceTime, filter, map, startWith, withLatestFrom } from 'rxjs/operators';
|
||||
import { debounceTime, filter, map, startWith } from 'rxjs/operators';
|
||||
import { TaskService } from '../../features/tasks/task.service';
|
||||
import { DEFAULT_TAG } from '../../features/tag/tag.const';
|
||||
import { DEFAULT_TAG, TODAY_TAG } from '../../features/tag/tag.const';
|
||||
import { NoteService } from '../../features/note/note.service';
|
||||
import { Note } from '../../features/note/note.model';
|
||||
import { Router } from '@angular/router';
|
||||
import { LayoutService } from '../../core-ui/layout/layout.service';
|
||||
import { Project } from '../../features/project/project.model';
|
||||
import { Tag } from '../../features/tag/tag.model';
|
||||
import { ProjectService } from '../../features/project/project.service';
|
||||
|
|
@ -32,6 +37,7 @@ import { MatFormField, MatLabel } from '@angular/material/form-field';
|
|||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DialogViewArchivedTaskComponent } from '../../features/tasks/dialog-view-archived-task/dialog-view-archived-task.component';
|
||||
import { Log } from '../../core/log';
|
||||
import { MenuTreeService } from '../../features/menu-tree/menu-tree.service';
|
||||
|
||||
const MAX_RESULTS = 50;
|
||||
|
||||
|
|
@ -61,6 +67,10 @@ export class SearchPageComponent implements OnInit {
|
|||
private _tagService = inject(TagService);
|
||||
private _navigateToTaskService = inject(NavigateToTaskService);
|
||||
private _matDialog = inject(MatDialog);
|
||||
private _noteService = inject(NoteService);
|
||||
private _router = inject(Router);
|
||||
private _layoutService = inject(LayoutService);
|
||||
private _menuTreeService = inject(MenuTreeService);
|
||||
|
||||
readonly inputEl = viewChild.required<ElementRef>('inputEl');
|
||||
|
||||
|
|
@ -69,26 +79,71 @@ export class SearchPageComponent implements OnInit {
|
|||
filteredResults$: Observable<SearchItem[]> = new Observable();
|
||||
|
||||
private _cachedArchiveItems: SearchItem[] | null = null;
|
||||
private _archiveCacheInputs?: {
|
||||
archiveTasks: Task[];
|
||||
projects: Project[];
|
||||
tags: Tag[];
|
||||
projectFolderMap: Map<string, string>;
|
||||
tagFolderMap: Map<string, string>;
|
||||
};
|
||||
|
||||
private _searchableItems$: Observable<SearchItem[]> = combineLatest([
|
||||
this._taskService.allTasks$,
|
||||
this._taskService.getArchivedTasks(),
|
||||
this._noteService.notes$,
|
||||
this._projectService.list$,
|
||||
this._tagService.tags$,
|
||||
toObservable(this._menuTreeService.projectFolderMap),
|
||||
toObservable(this._menuTreeService.tagFolderMap),
|
||||
]).pipe(
|
||||
withLatestFrom(this._projectService.list$, this._tagService.tags$),
|
||||
map(([[allTasks, archiveTasks], projects, tags]) => {
|
||||
if (!this._cachedArchiveItems) {
|
||||
this._cachedArchiveItems = this._mapTasksToSearchItems(
|
||||
true,
|
||||
archiveTasks,
|
||||
projects,
|
||||
tags,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...this._mapTasksToSearchItems(false, allTasks, projects, tags),
|
||||
...this._cachedArchiveItems,
|
||||
];
|
||||
}),
|
||||
map(
|
||||
([
|
||||
allTasks,
|
||||
archiveTasks,
|
||||
notes,
|
||||
projects,
|
||||
tags,
|
||||
projectFolderMap,
|
||||
tagFolderMap,
|
||||
]) => {
|
||||
if (
|
||||
!this._cachedArchiveItems ||
|
||||
this._archiveCacheInputs?.archiveTasks !== archiveTasks ||
|
||||
this._archiveCacheInputs?.projects !== projects ||
|
||||
this._archiveCacheInputs?.tags !== tags ||
|
||||
this._archiveCacheInputs?.projectFolderMap !== projectFolderMap ||
|
||||
this._archiveCacheInputs?.tagFolderMap !== tagFolderMap
|
||||
) {
|
||||
this._archiveCacheInputs = {
|
||||
archiveTasks,
|
||||
projects,
|
||||
tags,
|
||||
projectFolderMap,
|
||||
tagFolderMap,
|
||||
};
|
||||
this._cachedArchiveItems = this._mapTasksToSearchItems(
|
||||
true,
|
||||
archiveTasks,
|
||||
projects,
|
||||
tags,
|
||||
projectFolderMap,
|
||||
tagFolderMap,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...this._mapTasksToSearchItems(
|
||||
false,
|
||||
allTasks,
|
||||
projects,
|
||||
tags,
|
||||
projectFolderMap,
|
||||
tagFolderMap,
|
||||
),
|
||||
...this._mapNotesToSearchItems(notes, projects, projectFolderMap),
|
||||
...this._cachedArchiveItems,
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
private _mapTasksToSearchItems(
|
||||
|
|
@ -96,6 +151,8 @@ export class SearchPageComponent implements OnInit {
|
|||
tasks: Task[],
|
||||
projects: Project[],
|
||||
tags: Tag[],
|
||||
projectFolderMap: Map<string, string>,
|
||||
tagFolderMap: Map<string, string>,
|
||||
): SearchItem[] {
|
||||
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
||||
const projectMap = new Map(projects.map((p) => [p.id, p]));
|
||||
|
|
@ -118,18 +175,76 @@ export class SearchPageComponent implements OnInit {
|
|||
timeSpentOnDay: task.timeSpentOnDay,
|
||||
created: task.created,
|
||||
issueType: task.issueType || null,
|
||||
ctx: this._getContextIcon(task, projectMap, tagMap, tagId),
|
||||
ctx: this._getContextIcon(
|
||||
task,
|
||||
projectMap,
|
||||
tagMap,
|
||||
tagId,
|
||||
projectFolderMap,
|
||||
tagFolderMap,
|
||||
),
|
||||
isArchiveTask,
|
||||
isDone: !!task.isDone,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _mapNotesToSearchItems(
|
||||
notes: Note[],
|
||||
projects: Project[],
|
||||
projectFolderMap: Map<string, string>,
|
||||
): SearchItem[] {
|
||||
const projectMap = new Map(projects.map((p) => [p.id, p]));
|
||||
|
||||
return notes.map((note) => {
|
||||
const title = note.content ? note.content.split('\n')[0] : 'Note';
|
||||
const ctx = note.projectId
|
||||
? projectMap.get(note.projectId) || {
|
||||
...DEFAULT_TAG,
|
||||
icon: 'comment',
|
||||
color: 'black',
|
||||
}
|
||||
: TODAY_TAG;
|
||||
|
||||
let ctxTitle = ctx.title;
|
||||
if (note.projectId) {
|
||||
const folderPath = projectFolderMap.get(note.projectId);
|
||||
if (folderPath) {
|
||||
ctxTitle = `${folderPath.replace(/ › /g, ' > ')} > ${ctx.title}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: note.id,
|
||||
title,
|
||||
taskNotes: note.content || '',
|
||||
searchText: (note.content || '').toLowerCase(),
|
||||
projectId: note.projectId || null,
|
||||
parentId: null,
|
||||
parentTitle: null,
|
||||
tagId: note.projectId ? '' : TODAY_TAG.id,
|
||||
timeSpentOnDay: {},
|
||||
created: note.created,
|
||||
issueType: null,
|
||||
ctx: {
|
||||
...ctx,
|
||||
title: ctxTitle,
|
||||
icon: (ctx as Tag).icon || (note.projectId && 'list') || 'comment',
|
||||
} as Tag | Project,
|
||||
isArchiveTask: false,
|
||||
isDone: false,
|
||||
isNote: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _getContextIcon(
|
||||
task: Task,
|
||||
projectMap: Map<string, Project>,
|
||||
tagMap: Map<string, Tag>,
|
||||
tagId: string,
|
||||
projectFolderMap: Map<string, string>,
|
||||
tagFolderMap: Map<string, string>,
|
||||
): Tag | Project {
|
||||
let context: Tag | Project | undefined = task.projectId
|
||||
? projectMap.get(task.projectId)
|
||||
|
|
@ -140,8 +255,22 @@ export class SearchPageComponent implements OnInit {
|
|||
context = { ...DEFAULT_TAG, icon: 'help_outline', color: 'black' };
|
||||
}
|
||||
|
||||
let title = context.title;
|
||||
if (task.projectId) {
|
||||
const folderPath = projectFolderMap.get(task.projectId);
|
||||
if (folderPath) {
|
||||
title = `${folderPath.replace(/ › /g, ' > ')} > ${context.title}`;
|
||||
}
|
||||
} else if (tagId && tagId !== TODAY_TAG.id) {
|
||||
const folderPath = tagFolderMap.get(tagId);
|
||||
if (folderPath) {
|
||||
title = `${folderPath.replace(/ › /g, ' > ')} > ${context.title}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...context,
|
||||
title,
|
||||
icon: (context as Tag).icon || (task.projectId && 'list') || null,
|
||||
};
|
||||
}
|
||||
|
|
@ -177,7 +306,18 @@ export class SearchPageComponent implements OnInit {
|
|||
|
||||
navigateToItem(item: SearchItem): void {
|
||||
if (!item) return;
|
||||
this._navigateToTaskService.navigate(item.id, item.isArchiveTask).then(() => {});
|
||||
if (item.isNote) {
|
||||
const path = item.projectId
|
||||
? `/project/${item.projectId}/tasks`
|
||||
: `/tag/TODAY/tasks`;
|
||||
this._router.navigate([path], { queryParams: { focusItem: item.id } }).then(() => {
|
||||
if (!this._layoutService.isShowNotes()) {
|
||||
this._layoutService.toggleNotes();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._navigateToTaskService.navigate(item.id, item.isArchiveTask).then(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
clearSearch(): void {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export interface SearchItem {
|
|||
searchText: string;
|
||||
// jira only
|
||||
titleHighlighted?: string;
|
||||
isNote?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchQueryParams {
|
||||
|
|
|
|||
|
|
@ -1125,9 +1125,9 @@
|
|||
"START": "Work Start"
|
||||
},
|
||||
"SEARCH_BAR": {
|
||||
"INFO": "Start typing to search for tasks",
|
||||
"NO_RESULTS": "No tasks found matching your search",
|
||||
"PLACEHOLDER": "Search for task or task notes"
|
||||
"INFO": "Start typing to search for tasks or notes",
|
||||
"NO_RESULTS": "No tasks or notes found matching your search",
|
||||
"PLACEHOLDER": "Search for task, task notes or project notes"
|
||||
},
|
||||
"SIMPLE_COUNTER": {
|
||||
"D_CONFIRM_REMOVE": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue