From 7480d68bcfae140701a5ec726d31f78d42429c21 Mon Sep 17 00:00:00 2001 From: renatomen Date: Sun, 17 Aug 2025 04:19:17 +0000 Subject: [PATCH] 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] --- build-css.mjs | 1 + src/editor/ProjectNoteDecorations.ts | 23 ++++++++++ src/ui/FilterBar.ts | 52 ++++++++++++++++++++-- src/ui/FilterHeading.ts | 66 ++++++++++++++++++++++++++++ src/views/AgendaView.ts | 49 ++++++++++++++++++++- src/views/TaskListView.ts | 45 +++++++++++++++++-- styles/filter-heading.css | 41 +++++++++++++++++ styles/index.css | 23 +++++----- 8 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 src/ui/FilterHeading.ts create mode 100644 styles/filter-heading.css diff --git a/build-css.mjs b/build-css.mjs index a0388bfd..c6d371c2 100644 --- a/build-css.mjs +++ b/build-css.mjs @@ -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 diff --git a/src/editor/ProjectNoteDecorations.ts b/src/editor/ProjectNoteDecorations.ts index 9f430e46..2614a139 100644 --- a/src/editor/ProjectNoteDecorations.ts +++ b/src/editor/ProjectNoteDecorations.ts @@ -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 = 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 { try { // Apply filters to get grouped tasks diff --git a/src/ui/FilterBar.ts b/src/ui/FilterBar.ts index 85ade09e..e697098c 100644 --- a/src/ui/FilterBar.ts +++ b/src/ui/FilterBar.ts @@ -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; + } } /** diff --git a/src/ui/FilterHeading.ts b/src/ui/FilterHeading.ts new file mode 100644 index 00000000..cd13b5d6 --- /dev/null +++ b/src/ui/FilterHeading.ts @@ -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(); + } + } +} diff --git a/src/views/AgendaView.ts b/src/views/AgendaView.ts index b02c1c78..44206837 100644 --- a/src/views/AgendaView.ts +++ b/src/views/AgendaView.ts @@ -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 { + 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(); } } diff --git a/src/views/TaskListView.ts b/src/views/TaskListView.ts index 9b9ed75a..af29b192 100644 --- a/src/views/TaskListView.ts +++ b/src/views/TaskListView.ts @@ -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 { + 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(); } } diff --git a/styles/filter-heading.css b/styles/filter-heading.css new file mode 100644 index 00000000..28c17063 --- /dev/null +++ b/styles/filter-heading.css @@ -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); +} diff --git a/styles/index.css b/styles/index.css index d7290d61..69d8090e 100644 --- a/styles/index.css +++ b/styles/index.css @@ -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