Skip to content

Commit 4e8cd2e

Browse files
authored
Merge pull request #1207 from renatomen/feat/bases-views-inline-search
feat: Add in-view (instant) search box to Bases views
2 parents 6a533c5 + db27df4 commit 4e8cd2e

File tree

13 files changed

+1486
-11
lines changed

13 files changed

+1486
-11
lines changed

build-css.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const CSS_FILES = [
1313
'styles/note-card-bem.css', // NoteCard component with proper BEM scoping
1414
'styles/filter-bar-bem.css', // FilterBar component with proper BEM scoping
1515
'styles/filter-heading.css', // FilterHeading component with proper BEM scoping
16+
'styles/search-box.css', // SearchBox component with proper BEM scoping
1617
'styles/modal-bem.css', // Modal components with proper BEM scoping
1718
'styles/task-modal.css', // Task modal components (Google Keep/Todoist style)
1819
'styles/reminder-modal.css', // Reminder modal component with proper BEM scoping
1.4 MB
Loading

src/bases/BasesViewBase.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { PropertyMappingService } from "./PropertyMappingService";
55
import { TaskInfo, EVENT_TASK_UPDATED } from "../types";
66
import { convertInternalToUserProperties } from "../utils/propertyMapping";
77
import { DEFAULT_INTERNAL_VISIBLE_PROPERTIES } from "../settings/defaults";
8+
import { SearchBox } from "./components/SearchBox";
9+
import { TaskSearchFilter } from "./TaskSearchFilter";
810

911
/**
1012
* Abstract base class for all TaskNotes Bases views.
@@ -25,6 +27,12 @@ export abstract class BasesViewBase extends Component {
2527
protected taskUpdateListener: any = null;
2628
protected updateDebounceTimer: number | null = null;
2729

30+
// Search functionality (opt-in via enableSearch flag)
31+
protected enableSearch = false;
32+
protected searchBox: SearchBox | null = null;
33+
protected searchFilter: TaskSearchFilter | null = null;
34+
protected currentSearchTerm = "";
35+
2836
constructor(controller: any, containerEl: HTMLElement, plugin: TaskNotesPlugin) {
2937
// Call Component constructor
3038
super();
@@ -344,6 +352,109 @@ export abstract class BasesViewBase extends Component {
344352
return visibleProperties;
345353
}
346354

355+
/**
356+
* Initialize search functionality for this view.
357+
* Call this from render() in subclasses that want search.
358+
* Requires enableSearch to be true and will only create the UI once.
359+
*/
360+
protected setupSearch(container: HTMLElement): void {
361+
// Idempotency: if search UI is already created, do nothing
362+
if (this.searchBox) {
363+
return;
364+
}
365+
if (!this.enableSearch) {
366+
return;
367+
}
368+
369+
// Create search container
370+
const searchContainer = document.createElement("div");
371+
searchContainer.className = "tn-search-container";
372+
373+
// Insert search container at the top of the container so it appears above
374+
// the main items/content (e.g., the task list). This keeps the search box
375+
// visible while the list itself can scroll independently.
376+
if (container.firstChild) {
377+
container.insertBefore(searchContainer, container.firstChild);
378+
} else {
379+
container.appendChild(searchContainer);
380+
}
381+
382+
// Initialize search filter with visible properties (if available)
383+
// Config might not be available yet during initial setup
384+
let visibleProperties: string[] = [];
385+
try {
386+
if (this.config) {
387+
visibleProperties = this.getVisibleProperties();
388+
}
389+
} catch (e) {
390+
console.debug(`[${this.type}] Could not get visible properties during search setup:`, e);
391+
}
392+
this.searchFilter = new TaskSearchFilter(visibleProperties);
393+
394+
// Initialize search box
395+
this.searchBox = new SearchBox(
396+
searchContainer,
397+
(term) => this.handleSearch(term),
398+
300 // 300ms debounce
399+
);
400+
this.searchBox.render();
401+
402+
// Register cleanup using Component lifecycle
403+
this.register(() => {
404+
if (this.searchBox) {
405+
this.searchBox.destroy();
406+
this.searchBox = null;
407+
}
408+
this.searchFilter = null;
409+
this.currentSearchTerm = "";
410+
});
411+
}
412+
413+
/**
414+
* Handle search term changes.
415+
* Subclasses can override for custom behavior.
416+
* Includes performance monitoring for search operations.
417+
*/
418+
protected handleSearch(term: string): void {
419+
const startTime = performance.now();
420+
this.currentSearchTerm = term;
421+
422+
// Re-render with filtered tasks
423+
this.render();
424+
425+
const filterTime = performance.now() - startTime;
426+
427+
// Log slow searches for performance monitoring
428+
if (filterTime > 200) {
429+
console.warn(
430+
`[${this.type}] Slow search: ${filterTime.toFixed(2)}ms for search term "${term}"`
431+
);
432+
}
433+
}
434+
435+
/**
436+
* Apply search filter to tasks.
437+
* Returns filtered tasks or original if no search term.
438+
*/
439+
protected applySearchFilter(tasks: TaskInfo[]): TaskInfo[] {
440+
if (!this.searchFilter || !this.currentSearchTerm) {
441+
return tasks;
442+
}
443+
444+
const startTime = performance.now();
445+
const filtered = this.searchFilter.filterTasks(tasks, this.currentSearchTerm);
446+
const filterTime = performance.now() - startTime;
447+
448+
// Log filter performance for monitoring
449+
if (filterTime > 100) {
450+
console.warn(
451+
`[${this.type}] Filter operation took ${filterTime.toFixed(2)}ms for ${tasks.length} tasks`
452+
);
453+
}
454+
455+
return filtered;
456+
}
457+
347458
// Abstract methods that subclasses must implement
348459

349460
/**

src/bases/CalendarView.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,11 @@ export class CalendarView extends BasesViewBase {
352352
this.microsoftCalendarToggles.set(cal.id, this.config.get(key) ?? true);
353353
}
354354
}
355+
356+
// Read enableSearch toggle (default: false for backward compatibility)
357+
const enableSearchValue = this.config.get('enableSearch');
358+
this.enableSearch = (enableSearchValue as boolean) ?? false;
359+
355360
// Mark config as successfully loaded
356361
this.configLoaded = true;
357362

@@ -373,11 +378,19 @@ export class CalendarView extends BasesViewBase {
373378
this.readViewOptions();
374379
}
375380

381+
// Now that config is loaded, setup search (idempotent: will only create once)
382+
if (this.rootElement) {
383+
this.setupSearch(this.rootElement);
384+
}
385+
376386
try {
377387
// Extract tasks from Bases
378388
const dataItems = this.dataAdapter.extractDataItems();
379389
const taskNotes = await identifyTaskNotesFromBasesData(dataItems, this.plugin);
380-
this.currentTasks = taskNotes;
390+
391+
// Apply search filter
392+
const filteredTasks = this.applySearchFilter(taskNotes);
393+
this.currentTasks = filteredTasks;
381394

382395
// Build Bases entry mapping for task enrichment
383396
this.basesEntryByPath.clear();

src/bases/KanbanView.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ export class KanbanView extends BasesViewBase {
7575
const columnOrderStr = (this.config.get('columnOrder') as string) || '{}';
7676
this.columnOrders = JSON.parse(columnOrderStr);
7777

78+
// Read enableSearch toggle (default: false for backward compatibility)
79+
const enableSearchValue = this.config.get('enableSearch');
80+
this.enableSearch = (enableSearchValue as boolean) ?? false;
81+
7882
// Mark config as successfully loaded
7983
this.configLoaded = true;
8084
} catch (e) {
@@ -92,15 +96,23 @@ export class KanbanView extends BasesViewBase {
9296
this.readViewOptions();
9397
}
9498

99+
// Now that config is loaded, setup search (idempotent: will only create once)
100+
if (this.rootElement) {
101+
this.setupSearch(this.rootElement);
102+
}
103+
95104
try {
96105
const dataItems = this.dataAdapter.extractDataItems();
97106
const taskNotes = await identifyTaskNotesFromBasesData(dataItems, this.plugin);
98107

108+
// Apply search filter
109+
const filteredTasks = this.applySearchFilter(taskNotes);
110+
99111
// Clear board and cleanup scrollers
100112
this.destroyColumnScrollers();
101113
this.boardEl.empty();
102114

103-
if (taskNotes.length === 0) {
115+
if (filteredTasks.length === 0) {
104116
this.renderEmptyState();
105117
return;
106118
}
@@ -118,11 +130,11 @@ export class KanbanView extends BasesViewBase {
118130
}
119131

120132
// Group tasks
121-
const groups = this.groupTasks(taskNotes, groupByPropertyId, pathToProps);
133+
const groups = this.groupTasks(filteredTasks, groupByPropertyId, pathToProps);
122134

123135
// Render swimlanes if configured
124136
if (this.swimLanePropertyId) {
125-
await this.renderWithSwimLanes(groups, taskNotes, pathToProps, groupByPropertyId);
137+
await this.renderWithSwimLanes(groups, filteredTasks, pathToProps, groupByPropertyId);
126138
} else {
127139
await this.renderFlat(groups);
128140
}

src/bases/TaskListView.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { parseDateToUTC } from "../utils/dateUtils";
1717

1818
export class TaskListView extends BasesViewBase {
1919
type = "tasknoteTaskList";
20+
2021
private itemsContainer: HTMLElement | null = null;
2122
private currentTaskElements = new Map<string, HTMLElement>();
2223
private lastRenderWasGrouped = false;
@@ -32,6 +33,7 @@ export class TaskListView extends BasesViewBase {
3233
private collapsedSubGroups = new Set<string>(); // Track collapsed sub-group keys
3334
private subGroupPropertyId: string | null = null; // Property ID for sub-grouping
3435
private configLoaded = false; // Track if we've successfully loaded config
36+
3537
/**
3638
* Threshold for enabling virtual scrolling in task list view.
3739
* Virtual scrolling activates when total items (tasks + group headers) >= 100.
@@ -64,11 +66,15 @@ export class TaskListView extends BasesViewBase {
6466
private readViewOptions(): void {
6567
// Guard: config may not be set yet if called too early
6668
if (!this.config || typeof this.config.get !== 'function') {
69+
console.debug('[TaskListView] Config not available yet in readViewOptions');
6770
return;
6871
}
6972

7073
try {
7174
this.subGroupPropertyId = this.config.getAsPropertyId('subGroup');
75+
// Read enableSearch toggle (default: false for backward compatibility)
76+
const enableSearchValue = this.config.get('enableSearch');
77+
this.enableSearch = (enableSearchValue as boolean) ?? false;
7278
// Mark config as successfully loaded
7379
this.configLoaded = true;
7480
} catch (e) {
@@ -105,6 +111,11 @@ export class TaskListView extends BasesViewBase {
105111
this.readViewOptions();
106112
}
107113

114+
// Now that config is loaded, setup search (idempotent: will only create once)
115+
if (this.rootElement) {
116+
this.setupSearch(this.rootElement);
117+
}
118+
108119
try {
109120
// Skip rendering if we have no data yet (prevents flickering during data updates)
110121
if (!this.data?.data) {
@@ -211,6 +222,9 @@ export class TaskListView extends BasesViewBase {
211222
private async renderFlat(taskNotes: TaskInfo[]): Promise<void> {
212223
const visibleProperties = this.getVisibleProperties();
213224

225+
// Apply search filter
226+
const filteredTasks = this.applySearchFilter(taskNotes);
227+
214228
// Note: taskNotes are already sorted by Bases according to sort configuration
215229
// No manual sorting needed - Bases provides pre-sorted data
216230

@@ -219,8 +233,8 @@ export class TaskListView extends BasesViewBase {
219233

220234
const cardOptions = this.getCardOptions(targetDate);
221235

222-
// Decide whether to use virtual scrolling
223-
const shouldUseVirtualScrolling = taskNotes.length >= this.VIRTUAL_SCROLL_THRESHOLD;
236+
// Decide whether to use virtual scrolling based on filtered task count
237+
const shouldUseVirtualScrolling = filteredTasks.length >= this.VIRTUAL_SCROLL_THRESHOLD;
224238

225239
if (shouldUseVirtualScrolling && !this.useVirtualScrolling) {
226240
// Switch to virtual scrolling
@@ -233,9 +247,9 @@ export class TaskListView extends BasesViewBase {
233247
}
234248

235249
if (this.useVirtualScrolling) {
236-
await this.renderFlatVirtual(taskNotes, visibleProperties, cardOptions);
250+
await this.renderFlatVirtual(filteredTasks, visibleProperties, cardOptions);
237251
} else {
238-
await this.renderFlatNormal(taskNotes, visibleProperties, cardOptions);
252+
await this.renderFlatNormal(filteredTasks, visibleProperties, cardOptions);
239253
}
240254
}
241255

@@ -423,13 +437,17 @@ export class TaskListView extends BasesViewBase {
423437
*/
424438
private async renderGroupedBySubProperty(taskNotes: TaskInfo[]): Promise<void> {
425439
const visibleProperties = this.getVisibleProperties();
440+
441+
// Apply search filter
442+
const filteredTasks = this.applySearchFilter(taskNotes);
443+
426444
const targetDate = new Date();
427445
this.currentTargetDate = targetDate;
428446
const cardOptions = this.getCardOptions(targetDate);
429447

430448
// Group tasks by sub-property
431449
const pathToProps = this.buildPathToPropsMap();
432-
const groupedTasks = this.groupTasksBySubProperty(taskNotes, this.subGroupPropertyId!, pathToProps);
450+
const groupedTasks = this.groupTasksBySubProperty(filteredTasks, this.subGroupPropertyId!, pathToProps);
433451

434452
// Build flat items array (treat sub-groups as primary groups)
435453
type RenderItem =
@@ -497,12 +515,15 @@ export class TaskListView extends BasesViewBase {
497515
const visibleProperties = this.getVisibleProperties();
498516
const groups = this.dataAdapter.getGroupedData();
499517

518+
// Apply search filter
519+
const filteredTasks = this.applySearchFilter(taskNotes);
520+
500521
const targetDate = new Date();
501522
this.currentTargetDate = targetDate;
502523
const cardOptions = this.getCardOptions(targetDate);
503524

504525
// Build flattened list of items using shared method
505-
const items = this.buildGroupedRenderItems(groups, taskNotes);
526+
const items = this.buildGroupedRenderItems(groups, filteredTasks);
506527

507528
// Use virtual scrolling if we have many items
508529
const shouldUseVirtualScrolling = items.length >= this.VIRTUAL_SCROLL_THRESHOLD;
@@ -718,10 +739,11 @@ export class TaskListView extends BasesViewBase {
718739
* Override from Component base class.
719740
*/
720741
onunload(): void {
721-
// Component.register() calls will be automatically cleaned up
742+
// Component.register() calls will be automatically cleaned up (including search cleanup)
722743
// We just need to clean up view-specific state
723744
this.unregisterContainerListeners();
724745
this.destroyVirtualScroller();
746+
725747
this.currentTaskElements.clear();
726748
this.itemsContainer = null;
727749
this.lastRenderWasGrouped = false;

0 commit comments

Comments
 (0)