mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
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:
parent
bd657010f2
commit
7480d68bcf
8 changed files with 281 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
66
src/ui/FilterHeading.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
41
styles/filter-heading.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue