Skip to content

Commit 7abc899

Browse files
committed
fix: improve search UX with no-results feedback and hide empty groups
- Add "No tasks match..." message when search returns no results - Hide empty groups in TaskListView when search filtering - Persist search term across view re-renders - Remove placeholder integration tests that weren't testing anything
1 parent 26c2bbe commit 7abc899

File tree

6 files changed

+106
-212
lines changed

6 files changed

+106
-212
lines changed

docs/releases/unreleased.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ Example:
2424
2525
-->
2626

27+
## Added
28+
29+
- (#1207) Added inline search box to Bases views (Task List, Kanban, Calendar)
30+
- Enable via "Enable search box" toggle in view settings
31+
- Searches across title, status, priority, tags, contexts, projects, and visible custom properties
32+
- Press Escape or click × to clear search
33+
- Thanks to @renatomen for the PR
34+
2735
## Fixed
2836

2937
- (#1165) Fixed Kanban view grouping by list properties (contexts, tags, projects) treating multiple values as a single combined column

src/bases/BasesViewBase.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,12 @@ export abstract class BasesViewBase extends Component {
358358
* Requires enableSearch to be true and will only create the UI once.
359359
*/
360360
protected setupSearch(container: HTMLElement): void {
361-
// Idempotency: if search UI is already created, do nothing
361+
// Idempotency: if search UI is already created, restore value and return
362362
if (this.searchBox) {
363+
// Restore search term if it was cleared during re-render
364+
if (this.currentSearchTerm && this.searchBox.getValue() !== this.currentSearchTerm) {
365+
this.searchBox.setValue(this.currentSearchTerm);
366+
}
363367
return;
364368
}
365369
if (!this.enableSearch) {
@@ -399,6 +403,11 @@ export abstract class BasesViewBase extends Component {
399403
);
400404
this.searchBox.render();
401405

406+
// Restore search term if view is being re-initialized with existing search
407+
if (this.currentSearchTerm) {
408+
this.searchBox.setValue(this.currentSearchTerm);
409+
}
410+
402411
// Register cleanup using Component lifecycle
403412
this.register(() => {
404413
if (this.searchBox) {
@@ -455,6 +464,35 @@ export abstract class BasesViewBase extends Component {
455464
return filtered;
456465
}
457466

467+
/**
468+
* Check if we're currently filtering with no results.
469+
* Returns true if search is active and produced no matches.
470+
*/
471+
protected isSearchWithNoResults(filteredTasks: TaskInfo[], originalCount: number): boolean {
472+
return this.currentSearchTerm.length > 0 && filteredTasks.length === 0 && originalCount > 0;
473+
}
474+
475+
/**
476+
* Render "no results" message for search.
477+
* Call this when search produces no matches.
478+
*/
479+
protected renderSearchNoResults(container: HTMLElement): void {
480+
const noResultsEl = document.createElement("div");
481+
noResultsEl.className = "tn-search-no-results";
482+
483+
const textEl = document.createElement("div");
484+
textEl.className = "tn-search-no-results__text";
485+
textEl.textContent = `No tasks match "${this.currentSearchTerm}"`;
486+
487+
const hintEl = document.createElement("div");
488+
hintEl.className = "tn-search-no-results__hint";
489+
hintEl.textContent = "Try a different search term or clear the search";
490+
491+
noResultsEl.appendChild(textEl);
492+
noResultsEl.appendChild(hintEl);
493+
container.appendChild(noResultsEl);
494+
}
495+
458496
// Abstract methods that subclasses must implement
459497

460498
/**

src/bases/KanbanView.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,12 @@ export class KanbanView extends BasesViewBase {
119119
this.boardEl.empty();
120120

121121
if (filteredTasks.length === 0) {
122-
this.renderEmptyState();
122+
// Show "no results" if search returned empty but we had tasks
123+
if (this.isSearchWithNoResults(filteredTasks, taskNotes.length)) {
124+
this.renderSearchNoResults(this.boardEl);
125+
} else {
126+
this.renderEmptyState();
127+
}
123128
return;
124129
}
125130

src/bases/TaskListView.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ export class TaskListView extends BasesViewBase {
225225
// Apply search filter
226226
const filteredTasks = this.applySearchFilter(taskNotes);
227227

228+
// Show "no results" if search returned empty but we had tasks
229+
if (this.isSearchWithNoResults(filteredTasks, taskNotes.length)) {
230+
this.clearAllTaskElements();
231+
if (this.itemsContainer) {
232+
this.renderSearchNoResults(this.itemsContainer);
233+
}
234+
return;
235+
}
236+
228237
// Note: taskNotes are already sorted by Bases according to sort configuration
229238
// No manual sorting needed - Bases provides pre-sorted data
230239

@@ -376,6 +385,10 @@ export class TaskListView extends BasesViewBase {
376385
const primaryKey = this.dataAdapter.convertGroupKeyToString(group.key);
377386
const groupPaths = new Set(group.entries.map((e: any) => e.file.path));
378387
const groupTasks = taskNotes.filter((t) => groupPaths.has(t.path));
388+
389+
// Skip groups with no matching tasks (e.g., after search filtering)
390+
if (groupTasks.length === 0) continue;
391+
379392
const isPrimaryCollapsed = this.collapsedGroups.has(primaryKey);
380393

381394
// Add primary header
@@ -441,6 +454,15 @@ export class TaskListView extends BasesViewBase {
441454
// Apply search filter
442455
const filteredTasks = this.applySearchFilter(taskNotes);
443456

457+
// Show "no results" if search returned empty but we had tasks
458+
if (this.isSearchWithNoResults(filteredTasks, taskNotes.length)) {
459+
this.clearAllTaskElements();
460+
if (this.itemsContainer) {
461+
this.renderSearchNoResults(this.itemsContainer);
462+
}
463+
return;
464+
}
465+
444466
const targetDate = new Date();
445467
this.currentTargetDate = targetDate;
446468
const cardOptions = this.getCardOptions(targetDate);
@@ -518,6 +540,15 @@ export class TaskListView extends BasesViewBase {
518540
// Apply search filter
519541
const filteredTasks = this.applySearchFilter(taskNotes);
520542

543+
// Show "no results" if search returned empty but we had tasks
544+
if (this.isSearchWithNoResults(filteredTasks, taskNotes.length)) {
545+
this.clearAllTaskElements();
546+
if (this.itemsContainer) {
547+
this.renderSearchNoResults(this.itemsContainer);
548+
}
549+
return;
550+
}
551+
521552
const targetDate = new Date();
522553
this.currentTargetDate = targetDate;
523554
const cardOptions = this.getCardOptions(targetDate);

styles/search-box.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,25 @@
9595
outline-offset: 2px;
9696
}
9797

98+
/* No results state */
99+
.tn-search-no-results {
100+
display: flex;
101+
flex-direction: column;
102+
align-items: center;
103+
justify-content: center;
104+
padding: var(--size-4-8, 32px) var(--size-4-4, 16px);
105+
text-align: center;
106+
color: var(--text-muted);
107+
}
108+
109+
.tn-search-no-results__text {
110+
font-size: var(--font-ui-medium, 14px);
111+
color: var(--text-normal);
112+
margin-bottom: var(--size-4-2, 8px);
113+
}
114+
115+
.tn-search-no-results__hint {
116+
font-size: var(--font-ui-small, 13px);
117+
color: var(--text-muted);
118+
}
119+

tests/integration/TaskListView-search.integration.test.ts

Lines changed: 0 additions & 210 deletions
This file was deleted.

0 commit comments

Comments
 (0)