feat: add filter heading display to TaskListView

- Add FilterHeading component to show current saved view name and completion count
- Display 'All' when no saved view is applied, or saved view name when applied
- Include completion count in 'X / Y' format with proper spacing
- Add horizontal divider line below heading for visual separation
- Integrate with TaskListView between FilterBar and task content
- Auto-detect active saved view from persisted state on view load
- Update heading when tasks are refreshed or filters change
- Add proper cleanup on view close

The heading provides immediate visual feedback about:
- What filter/view is currently active
- Task completion progress (completed/total count)

Layout: [FilterBar] -> [Heading + Count] -> [Divider] -> [Task Content]
This commit is contained in:
renatomen 2025-08-17 04:19:17 +00:00
parent bd657010f2
commit 7480d68bcf
8 changed files with 281 additions and 19 deletions

View file

@ -12,6 +12,7 @@ const CSS_FILES = [
'styles/task-inline-widget.css', // Inline task widget for editor with proper BEM scoping
'styles/note-card-bem.css', // NoteCard component with proper BEM scoping
'styles/filter-bar-bem.css', // FilterBar component with proper BEM scoping
'styles/filter-heading.css', // FilterHeading component with proper BEM scoping
'styles/modal-bem.css', // Modal components with proper BEM scoping
'styles/task-modal.css', // Task modal components (Google Keep/Todoist style)
'styles/reminder-modal.css', // Reminder modal component with proper BEM scoping

View file

@ -6,6 +6,7 @@ import { TaskInfo, EVENT_DATA_CHANGED, EVENT_TASK_UPDATED, EVENT_TASK_DELETED, F
import { createTaskCard } from '../ui/TaskCard';
import { ProjectSubtasksService } from '../services/ProjectSubtasksService';
import { FilterBar } from '../ui/FilterBar';
import { FilterHeading } from '../ui/FilterHeading';
import { FilterService } from '../services/FilterService';
import { GroupingUtils } from '../utils/GroupingUtils';
import { GroupCountUtils } from '../utils/GroupCountUtils';
@ -16,6 +17,7 @@ const projectSubtasksUpdateEffect = StateEffect.define<{ forceUpdate?: boolean }
class ProjectSubtasksWidget extends WidgetType {
private groupedTasks: Map<string, TaskInfo[]> = new Map();
private filterBar: FilterBar | null = null;
private filterHeading: FilterHeading | null = null;
private filterService: FilterService;
private currentQuery: FilterQuery;
private savedViewsUnsubscribe: (() => void) | null = null;
@ -268,6 +270,27 @@ class ProjectSubtasksWidget extends WidgetType {
}
}
/**
* Update the filter heading with current saved view and completion count
*/
private updateFilterHeading(): void {
if (!this.filterHeading || !this.filterBar) return;
try {
// Calculate completion stats from current filtered tasks
const allFilteredTasks = Array.from(this.groupedTasks.values()).flat();
const stats = GroupCountUtils.calculateGroupStats(allFilteredTasks, this.plugin);
// Get current saved view from FilterBar
const activeSavedView = (this.filterBar as any).activeSavedView || null;
// Update the filter heading
this.filterHeading.update(activeSavedView, stats.completed, stats.total);
} catch (error) {
console.error('Error updating filter heading in ProjectSubtasksWidget:', error);
}
}
private async applyFiltersAndRender(taskListContainer?: HTMLElement): Promise<void> {
try {
// Apply filters to get grouped tasks

View file

@ -173,10 +173,8 @@ export class FilterBar extends EventEmitter {
this.currentQuery = FilterUtils.deepCloneFilterQuery(query);
// Ensure the updated query has proper structure
this.ensureValidFilterQuery();
// Clear active saved view when query is updated externally (unless loading a saved view)
if (!this.isLoadingSavedView && this.activeSavedView) {
this.clearActiveSavedView();
}
// Detect if the updated query matches any saved view
this.detectActiveSavedView();
this.updateUI();
}
@ -298,6 +296,52 @@ export class FilterBar extends EventEmitter {
updateSavedViews(views: readonly SavedView[]): void {
this.savedViews = views;
this.renderViewSelectorDropdown();
// Check if current query matches any saved view
this.detectActiveSavedView();
}
/**
* Detect if the current query matches any saved view and set activeSavedView accordingly
*/
private detectActiveSavedView(): void {
if (this.isLoadingSavedView) return; // Don't interfere when explicitly loading a view
// Find a saved view that matches the current query
const matchingView = this.savedViews.find(view =>
this.queriesMatch(this.currentQuery, view.query)
);
if (matchingView && this.activeSavedView?.id !== matchingView.id) {
this.activeSavedView = matchingView;
this.updateViewSelectorButtonState();
} else if (!matchingView && this.activeSavedView) {
this.activeSavedView = null;
this.updateViewSelectorButtonState();
}
}
/**
* Check if two queries are functionally equivalent
*/
private queriesMatch(query1: FilterQuery, query2: FilterQuery): boolean {
// Deep comparison of filter queries (excluding sort/group which might differ)
const normalize = (query: FilterQuery) => {
const normalized = FilterUtils.deepCloneFilterQuery(query);
// Remove sort/group for comparison as they might be view-specific
delete (normalized as any).sortKey;
delete (normalized as any).sortDirection;
delete (normalized as any).groupKey;
return normalized;
};
try {
const norm1 = normalize(query1);
const norm2 = normalize(query2);
return JSON.stringify(norm1) === JSON.stringify(norm2);
} catch (error) {
console.warn('Error comparing queries:', error);
return false;
}
}
/**

66
src/ui/FilterHeading.ts Normal file
View file

@ -0,0 +1,66 @@
import { GroupCountUtils } from '../utils/GroupCountUtils';
import { SavedView } from '../types';
/**
* Utility class for creating and managing filter heading displays
* Shows the current saved view name and completion count
*/
export class FilterHeading {
private container: HTMLElement;
private headingElement: HTMLElement | null = null;
private countElement: HTMLElement | null = null;
private dividerElement: HTMLElement | null = null;
constructor(container: HTMLElement) {
this.container = container;
this.render();
}
/**
* Render the filter heading structure
*/
private render(): void {
// Create main heading container
const headingContainer = this.container.createDiv('filter-heading');
// Create heading content wrapper
const headingContent = headingContainer.createDiv('filter-heading__content');
// View name element
this.headingElement = headingContent.createEl('h2', {
cls: 'filter-heading__title',
text: 'All'
});
// Count element
this.countElement = headingContent.createDiv('filter-heading__count agenda-view__item-count');
// Divider line
this.dividerElement = headingContainer.createDiv('filter-heading__divider');
}
/**
* Update the heading with current filter information
*/
update(activeSavedView: SavedView | null, completed: number, total: number): void {
if (!this.headingElement || !this.countElement) return;
// Update view name
const viewName = activeSavedView?.name || 'All';
this.headingElement.textContent = viewName;
// Update count
const countText = GroupCountUtils.formatGroupCount(completed, total).text;
this.countElement.textContent = countText;
}
/**
* Remove the filter heading from the DOM
*/
destroy(): void {
const headingContainer = this.container.querySelector('.filter-heading');
if (headingContainer) {
headingContainer.remove();
}
}
}

View file

@ -18,6 +18,7 @@ import { createTaskCard, updateTaskCard, refreshParentTaskSubtasks } from '../ui
import { createNoteCard } from '../ui/NoteCard';
import { createICSEventCard, updateICSEventCard } from '../ui/ICSCard';
import { FilterBar } from '../ui/FilterBar';
import { FilterHeading } from '../ui/FilterHeading';
import { FilterService } from '../services/FilterService';
export class AgendaView extends ItemView {
@ -33,6 +34,7 @@ export class AgendaView extends ItemView {
// Filter system
private filterBar: FilterBar | null = null;
private filterHeading: FilterHeading | null = null;
private currentQuery: FilterQuery;
// Event listeners
@ -171,6 +173,12 @@ export class AgendaView extends ItemView {
this.filterBar.destroy();
this.filterBar = null;
}
// Clean up FilterHeading
if (this.filterHeading) {
this.filterHeading.destroy();
this.filterHeading = null;
}
// Clean up
this.contentEl.empty();
@ -353,6 +361,9 @@ export class AgendaView extends ItemView {
this.refresh();
});
// Create filter heading
this.filterHeading = new FilterHeading(container);
// Set up view-specific options
this.setupViewOptions();
}
@ -1020,7 +1031,41 @@ export class AgendaView extends ItemView {
indicator.remove();
}
}
/**
* Update the filter heading with current saved view and completion count
*/
private async updateFilterHeading(): Promise<void> {
if (!this.filterHeading || !this.filterBar) return;
try {
// Get all agenda data to calculate completion stats (same logic as renderAgendaContent)
const dates = this.getAgendaDates();
const allTasks: TaskInfo[] = [];
for (const date of dates) {
// Use FilterService's getTasksForDate which properly handles recurring tasks
const tasksForDate = await this.plugin.filterService.getTasksForDate(
date,
this.currentQuery,
this.showOverdueOnToday
);
allTasks.push(...tasksForDate);
}
// Calculate completion stats
const stats = GroupCountUtils.calculateGroupStats(allTasks, this.plugin);
// Get current saved view from FilterBar
const activeSavedView = (this.filterBar as any).activeSavedView || null;
// Update the filter heading
this.filterHeading.update(activeSavedView, stats.completed, stats.total);
} catch (error) {
console.error('Error updating filter heading in AgendaView:', error);
}
}
async refresh() {
const container = this.contentEl.querySelector('.agenda-view') as HTMLElement;
if (container) {
@ -1028,6 +1073,8 @@ export class AgendaView extends ItemView {
this.setupViewOptions();
// Use DOMReconciler for efficient updates
await this.renderAgendaContent(container);
// Update filter heading with current data
this.updateFilterHeading();
}
}

View file

@ -12,7 +12,11 @@ import {
import { perfMonitor } from '../utils/PerformanceMonitor';
import { createTaskCard, updateTaskCard, refreshParentTaskSubtasks } from '../ui/TaskCard';
import { FilterBar } from '../ui/FilterBar';
<<<<<<< HEAD
import { GroupingUtils } from '../utils/GroupingUtils';
=======
import { FilterHeading } from '../ui/FilterHeading';
>>>>>>> b8d0a2a (feat: add filter heading display to TaskListView)
import { GroupCountUtils } from '../utils/GroupCountUtils';
export class TaskListView extends ItemView {
@ -29,6 +33,7 @@ export class TaskListView extends ItemView {
// Filter system
private filterBar: FilterBar | null = null;
private filterHeading: FilterHeading | null = null;
private currentQuery: FilterQuery;
// Task item tracking for dynamic updates
@ -200,6 +205,12 @@ export class TaskListView extends ItemView {
this.filterBar.destroy();
this.filterBar = null;
}
// Clean up FilterHeading
if (this.filterHeading) {
this.filterHeading.destroy();
this.filterHeading = null;
}
this.contentEl.empty();
}
@ -330,8 +341,10 @@ export class TaskListView extends ItemView {
this.plugin.viewStateManager.setFilterState(TASK_LIST_VIEW_TYPE, newQuery);
await this.refreshTasks();
});
// Create filter heading
this.filterHeading = new FilterHeading(container);
// Task list container
const taskList = container.createDiv({ cls: 'task-list' });
@ -355,7 +368,31 @@ export class TaskListView extends ItemView {
this.isTasksLoading = false;
this.updateLoadingState();
}
/**
* Update the filter heading with current saved view and completion count
*/
private async updateFilterHeading(): Promise<void> {
if (!this.filterHeading || !this.filterBar) return;
try {
// Get all filtered tasks to calculate completion stats
const groupedTasks = await this.plugin.filterService.getGroupedTasks(this.currentQuery);
const allTasks = Array.from(groupedTasks.values()).flat();
// Calculate completion stats
const stats = GroupCountUtils.calculateGroupStats(allTasks, this.plugin);
// Get current saved view from FilterBar
const activeSavedView = (this.filterBar as any).activeSavedView || null;
// Update the filter heading
this.filterHeading.update(activeSavedView, stats.completed, stats.total);
} catch (error) {
console.error('Error updating filter heading in TaskListView:', error);
}
}
/**
* Refresh tasks using FilterService
*/
@ -402,6 +439,8 @@ export class TaskListView extends ItemView {
} finally {
this.isTasksLoading = false;
this.updateLoadingState();
// Update filter heading with current data
await this.updateFilterHeading();
}
}

41
styles/filter-heading.css Normal file
View file

@ -0,0 +1,41 @@
/* =================================================================
FILTER HEADING COMPONENT
================================================================= */
/* All FilterHeading styles are scoped under .tasknotes-plugin for proper isolation */
/* Main Container */
.tasknotes-plugin .filter-heading {
margin-top: var(--tn-spacing-lg);
margin-bottom: var(--tn-spacing-lg);
}
/* Content wrapper with heading and count */
.tasknotes-plugin .filter-heading__content {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: var(--tn-spacing-md);
}
/* Heading title */
.tasknotes-plugin .filter-heading__title {
margin: 0;
font-size: var(--font-ui-medium);
font-weight: 600;
color: var(--text-normal);
}
/* Count display */
.tasknotes-plugin .filter-heading__count {
font-weight: 600;
font-size: var(--font-ui-small);
margin-left: 10px;
}
/* Divider line */
.tasknotes-plugin .filter-heading__divider {
height: 1px;
background-color: var(--background-modifier-border);
margin-bottom: var(--tn-spacing-lg);
}

View file

@ -13,20 +13,21 @@
4. task-card-bem.css - BEM TaskCard component with proper scoping
5. note-card-bem.css - BEM NoteCard component with proper scoping
6. filter-bar-bem.css - BEM FilterBar component with proper scoping
7. modal-bem.css - BEM Modal components with proper scoping
7. filter-heading.css - BEM FilterHeading component with proper scoping
8. modal-bem.css - BEM Modal components with proper scoping
=== BEM VIEW FILES ===
8. task-list-view.css - BEM TaskListView component
9. calendar-view.css - BEM CalendarView component
10. kanban-view.css - BEM KanbanView component
11. agenda-view.css - BEM AgendaView component
12. notes-view.css - BEM NotesView component
13. pomodoro-view.css - BEM PomodoroView component
14. pomodoro-stats-view.css - BEM PomodoroStatsView component
15. settings-view.css - BEM SettingsView component
9. task-list-view.css - BEM TaskListView component
10. calendar-view.css - BEM CalendarView component
11. kanban-view.css - BEM KanbanView component
12. agenda-view.css - BEM AgendaView component
13. notes-view.css - BEM NotesView component
14. pomodoro-view.css - BEM PomodoroView component
15. pomodoro-stats-view.css - BEM PomodoroStatsView component
16. settings-view.css - BEM SettingsView component
=== LEGACY SUPPORT ===
16. components.css - Reusable UI components, utilities, and modals
17. components.css - Reusable UI components, utilities, and modals
To build the final styles.css file, run:
npm run build-css