mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: add collapsible groups to subtask widget
- Extract reusable GroupingUtils for shared group functionality - Implement collapsible group headers with toggle buttons in subtask widget - Add state persistence using SUBTASK_WIDGET_VIEW_TYPE identifier - Wire expand/collapse all functionality via FilterBar events - Apply consistent CSS styling matching TaskListView groups - Refactor TaskListView to use GroupingUtils for consistency Ensures subtask widget groups behave identically to TaskListView with proper state persistence, accessibility, and visual consistency.
This commit is contained in:
parent
a1abe71a8c
commit
edf29a252e
7 changed files with 424 additions and 85 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -52,3 +52,4 @@ external/tasknotes-e2e/.e2e/
|
|||
C/
|
||||
|
||||
.copy-files.local
|
||||
.augment
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Decoration, DecorationSet, EditorView, PluginSpec, PluginValue, ViewPlugin, ViewUpdate, WidgetType } from '@codemirror/view';
|
||||
import { Extension, RangeSetBuilder, StateEffect } from '@codemirror/state';
|
||||
import { TFile, editorLivePreviewField, editorInfoField, EventRef } from 'obsidian';
|
||||
import { TFile, editorLivePreviewField, editorInfoField, EventRef, setIcon } from 'obsidian';
|
||||
import TaskNotesPlugin from '../main';
|
||||
import { TaskInfo, EVENT_DATA_CHANGED, EVENT_TASK_UPDATED, EVENT_TASK_DELETED, FilterQuery } from '../types';
|
||||
import { TaskInfo, EVENT_DATA_CHANGED, EVENT_TASK_UPDATED, EVENT_TASK_DELETED, FilterQuery, SUBTASK_WIDGET_VIEW_TYPE } from '../types';
|
||||
import { createTaskCard } from '../ui/TaskCard';
|
||||
import { ProjectSubtasksService } from '../services/ProjectSubtasksService';
|
||||
import { FilterBar } from '../ui/FilterBar';
|
||||
import { FilterService } from '../services/FilterService';
|
||||
import { GroupingUtils } from '../utils/GroupingUtils';
|
||||
|
||||
// Define a state effect for project subtasks updates
|
||||
const projectSubtasksUpdateEffect = StateEffect.define<{ forceUpdate?: boolean }>();
|
||||
|
|
@ -23,7 +24,7 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
constructor(private plugin: TaskNotesPlugin, private tasks: TaskInfo[], private notePath: string, private version: number = 0) {
|
||||
super();
|
||||
// Create note-specific view type identifier
|
||||
this.viewType = `project-subtasks:${notePath}`;
|
||||
this.viewType = `${SUBTASK_WIDGET_VIEW_TYPE}:${notePath}`;
|
||||
|
||||
// Initialize with ungrouped tasks
|
||||
this.groupedTasks.set('all', [...tasks]);
|
||||
|
|
@ -218,7 +219,38 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
this.filterBar.updateSavedViews(updatedViews);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Wire expand/collapse all functionality
|
||||
this.filterBar.on('expandAllGroups', () => {
|
||||
const key = this.currentQuery.groupKey || 'none';
|
||||
GroupingUtils.expandAllGroups(this.viewType, key, this.plugin);
|
||||
// Update DOM
|
||||
if (this.taskListContainer) {
|
||||
this.taskListContainer.querySelectorAll('.task-group').forEach(section => {
|
||||
section.classList.remove('is-collapsed');
|
||||
const list = (section as HTMLElement).querySelector('.task-cards') as HTMLElement | null;
|
||||
if (list) list.style.display = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.filterBar.on('collapseAllGroups', () => {
|
||||
const key = this.currentQuery.groupKey || 'none';
|
||||
const groupNames: string[] = [];
|
||||
if (this.taskListContainer) {
|
||||
this.taskListContainer.querySelectorAll('.task-group').forEach(section => {
|
||||
const name = (section as HTMLElement).dataset.group;
|
||||
if (name) {
|
||||
groupNames.push(name);
|
||||
section.classList.add('is-collapsed');
|
||||
const list = (section as HTMLElement).querySelector('.task-cards') as HTMLElement | null;
|
||||
if (list) list.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
GroupingUtils.collapseAllGroups(this.viewType, key, groupNames, this.plugin);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing filter bar for subtasks:', error);
|
||||
}
|
||||
|
|
@ -282,25 +314,85 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
taskListContainer.appendChild(taskCard);
|
||||
});
|
||||
} else {
|
||||
// Render grouped tasks with group headers
|
||||
// Render grouped tasks with collapsible group headers
|
||||
for (const [groupKey, tasks] of this.groupedTasks.entries()) {
|
||||
if (tasks.length === 0) continue;
|
||||
|
||||
// Create group header
|
||||
const groupHeader = taskListContainer.createEl('div', {
|
||||
cls: 'project-note-subtasks__group-header'
|
||||
|
||||
// Create group section
|
||||
const groupSection = taskListContainer.createEl('div', {
|
||||
cls: 'project-note-subtasks__group-section task-group'
|
||||
});
|
||||
|
||||
groupSection.setAttribute('data-group', groupKey);
|
||||
|
||||
const groupingKey = this.currentQuery.groupKey || 'none';
|
||||
const collapsedInitially = GroupingUtils.isGroupCollapsed(this.viewType, groupingKey, groupKey, this.plugin);
|
||||
|
||||
// Create group header with toggle functionality
|
||||
const groupHeader = groupSection.createEl('div', {
|
||||
cls: 'project-note-subtasks__group-header task-group-header'
|
||||
});
|
||||
|
||||
// Create toggle button first
|
||||
const toggleBtn = groupHeader.createEl('button', {
|
||||
cls: 'task-group-toggle',
|
||||
attr: { 'aria-label': 'Toggle group' }
|
||||
});
|
||||
try {
|
||||
setIcon(toggleBtn, 'chevron-right');
|
||||
} catch (_) {}
|
||||
const svg = toggleBtn.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.classList.add('chevron');
|
||||
svg.setAttr('width', '16');
|
||||
svg.setAttr('height', '16');
|
||||
} else {
|
||||
toggleBtn.textContent = '▸';
|
||||
toggleBtn.addClass('chevron-text');
|
||||
}
|
||||
|
||||
// Add group title
|
||||
groupHeader.createEl('h4', {
|
||||
cls: 'project-note-subtasks__group-title',
|
||||
text: this.getGroupDisplayName(groupKey, tasks.length)
|
||||
text: GroupingUtils.getGroupDisplayName(groupKey, tasks.length, this.plugin)
|
||||
});
|
||||
|
||||
|
||||
// Create group container
|
||||
const groupContainer = taskListContainer.createEl('div', {
|
||||
cls: 'project-note-subtasks__group'
|
||||
const groupContainer = groupSection.createEl('div', {
|
||||
cls: 'project-note-subtasks__group task-cards'
|
||||
});
|
||||
|
||||
|
||||
// Apply initial collapsed state
|
||||
if (collapsedInitially) {
|
||||
groupSection.classList.add('is-collapsed');
|
||||
groupContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add click handlers for expand/collapse
|
||||
groupHeader.addEventListener('click', (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('a')) return; // Ignore link clicks
|
||||
|
||||
const willCollapse = !groupSection.classList.contains('is-collapsed');
|
||||
GroupingUtils.setGroupCollapsed(this.viewType, groupingKey, groupKey, willCollapse, this.plugin);
|
||||
groupSection.classList.toggle('is-collapsed', willCollapse);
|
||||
groupContainer.style.display = willCollapse ? 'none' : '';
|
||||
toggleBtn.setAttribute('aria-expanded', String(!willCollapse));
|
||||
});
|
||||
|
||||
toggleBtn.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const willCollapse = !groupSection.classList.contains('is-collapsed');
|
||||
GroupingUtils.setGroupCollapsed(this.viewType, groupingKey, groupKey, willCollapse, this.plugin);
|
||||
groupSection.classList.toggle('is-collapsed', willCollapse);
|
||||
groupContainer.style.display = willCollapse ? 'none' : '';
|
||||
toggleBtn.setAttribute('aria-expanded', String(!willCollapse));
|
||||
});
|
||||
|
||||
// Set initial ARIA state
|
||||
toggleBtn.setAttribute('aria-expanded', String(!collapsedInitially));
|
||||
|
||||
// Render tasks in this group
|
||||
tasks.forEach(task => {
|
||||
const taskCard = createTaskCard(task, this.plugin, {
|
||||
|
|
@ -311,7 +403,7 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
showRecurringControls: true,
|
||||
groupByDate: false
|
||||
});
|
||||
|
||||
|
||||
taskCard.classList.add('project-note-subtasks__task');
|
||||
groupContainer.appendChild(taskCard);
|
||||
});
|
||||
|
|
@ -326,26 +418,7 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
}
|
||||
|
||||
private getGroupDisplayName(groupKey: string, taskCount: number): string {
|
||||
// Handle different group types with user-friendly names
|
||||
switch (groupKey) {
|
||||
case 'none':
|
||||
case 'all':
|
||||
return `All Tasks (${taskCount})`;
|
||||
case 'No Status':
|
||||
return `No Status (${taskCount})`;
|
||||
case 'No Priority':
|
||||
return `No Priority (${taskCount})`;
|
||||
case 'No Context':
|
||||
return `No Context (${taskCount})`;
|
||||
case 'No Project':
|
||||
return `No Project (${taskCount})`;
|
||||
case 'No Due Date':
|
||||
return `No Due Date (${taskCount})`;
|
||||
case 'No Scheduled Date':
|
||||
return `No Scheduled Date (${taskCount})`;
|
||||
default:
|
||||
return `${groupKey} (${taskCount})`;
|
||||
}
|
||||
return GroupingUtils.getGroupDisplayName(groupKey, taskCount, this.plugin);
|
||||
}
|
||||
|
||||
private createNewSubtask(): void {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const AGENDA_VIEW_TYPE = 'tasknotes-agenda-view';
|
|||
export const POMODORO_VIEW_TYPE = 'tasknotes-pomodoro-view';
|
||||
export const POMODORO_STATS_VIEW_TYPE = 'tasknotes-pomodoro-stats-view';
|
||||
export const KANBAN_VIEW_TYPE = 'tasknotes-kanban-view';
|
||||
export const SUBTASK_WIDGET_VIEW_TYPE = 'tasknotes-subtask-widget-view';
|
||||
|
||||
// Event types
|
||||
export const EVENT_DATE_SELECTED = 'date-selected';
|
||||
|
|
|
|||
103
src/utils/GroupingUtils.ts
Normal file
103
src/utils/GroupingUtils.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import TaskNotesPlugin from '../main';
|
||||
|
||||
/**
|
||||
* Utility functions for task grouping functionality
|
||||
* Shared between TaskListView and subtask widget
|
||||
*/
|
||||
export class GroupingUtils {
|
||||
|
||||
/**
|
||||
* Format group name for display with proper labels
|
||||
*/
|
||||
static formatGroupName(groupName: string, plugin: TaskNotesPlugin): string {
|
||||
// Check if it's a priority value
|
||||
const priorityConfig = plugin.priorityManager.getPriorityConfig(groupName);
|
||||
if (priorityConfig) {
|
||||
return `${priorityConfig.label} priority`;
|
||||
}
|
||||
|
||||
// Check if it's a status value
|
||||
const statusConfig = plugin.statusManager.getStatusConfig(groupName);
|
||||
if (statusConfig) {
|
||||
return statusConfig.label;
|
||||
}
|
||||
|
||||
switch (groupName) {
|
||||
case 'all':
|
||||
return 'All tasks';
|
||||
case 'no-status':
|
||||
return 'No status assigned';
|
||||
case 'No Status':
|
||||
return 'No Status';
|
||||
case 'No Priority':
|
||||
return 'No Priority';
|
||||
case 'No Context':
|
||||
return 'No Context';
|
||||
case 'No Project':
|
||||
return 'No Project';
|
||||
case 'No Due Date':
|
||||
return 'No Due Date';
|
||||
case 'No Scheduled Date':
|
||||
return 'No Scheduled Date';
|
||||
default:
|
||||
return groupName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group display name with task count
|
||||
*/
|
||||
static getGroupDisplayName(groupKey: string, taskCount: number, plugin: TaskNotesPlugin): string {
|
||||
const formattedName = GroupingUtils.formatGroupName(groupKey, plugin);
|
||||
return `${formattedName} (${taskCount})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a group should be collapsed initially
|
||||
*/
|
||||
static isGroupCollapsed(viewType: string, groupingKey: string, groupName: string, plugin: TaskNotesPlugin): boolean {
|
||||
try {
|
||||
const prefs = plugin.viewStateManager.getViewPreferences<any>(viewType) || {};
|
||||
const collapsed = prefs.collapsedGroups || {};
|
||||
return !!collapsed?.[groupingKey]?.[groupName];
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the collapsed state for a group
|
||||
*/
|
||||
static setGroupCollapsed(viewType: string, groupingKey: string, groupName: string, collapsed: boolean, plugin: TaskNotesPlugin): void {
|
||||
const prefs = plugin.viewStateManager.getViewPreferences<any>(viewType) || {};
|
||||
const next = { ...prefs };
|
||||
if (!next.collapsedGroups) next.collapsedGroups = {};
|
||||
if (!next.collapsedGroups[groupingKey]) next.collapsedGroups[groupingKey] = {};
|
||||
next.collapsedGroups[groupingKey][groupName] = collapsed;
|
||||
plugin.viewStateManager.setViewPreferences(viewType, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all groups for a specific view and grouping key
|
||||
*/
|
||||
static expandAllGroups(viewType: string, groupingKey: string, plugin: TaskNotesPlugin): void {
|
||||
const prefs = plugin.viewStateManager.getViewPreferences<any>(viewType) || {};
|
||||
const next = { ...(prefs.collapsedGroups || {}) } as Record<string, Record<string, boolean>>;
|
||||
next[groupingKey] = {};
|
||||
plugin.viewStateManager.setViewPreferences(viewType, { ...prefs, collapsedGroups: next });
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse all groups for a specific view and grouping key
|
||||
*/
|
||||
static collapseAllGroups(viewType: string, groupingKey: string, groupNames: string[], plugin: TaskNotesPlugin): void {
|
||||
const prefs = plugin.viewStateManager.getViewPreferences<any>(viewType) || {};
|
||||
const next = { ...(prefs.collapsedGroups || {}) } as Record<string, Record<string, boolean>>;
|
||||
const collapsed: Record<string, boolean> = {};
|
||||
groupNames.forEach(name => {
|
||||
collapsed[name] = true;
|
||||
});
|
||||
next[groupingKey] = collapsed;
|
||||
plugin.viewStateManager.setViewPreferences(viewType, { ...prefs, collapsedGroups: next });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { TFile, ItemView, WorkspaceLeaf, EventRef, Notice, setIcon } from 'obsidian';
|
||||
import TaskNotesPlugin from '../main';
|
||||
import {
|
||||
TASK_LIST_VIEW_TYPE,
|
||||
TaskInfo,
|
||||
import {
|
||||
TASK_LIST_VIEW_TYPE,
|
||||
TaskInfo,
|
||||
EVENT_DATA_CHANGED,
|
||||
EVENT_TASK_UPDATED,
|
||||
FilterQuery,
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { perfMonitor } from '../utils/PerformanceMonitor';
|
||||
import { createTaskCard, updateTaskCard, refreshParentTaskSubtasks } from '../ui/TaskCard';
|
||||
import { FilterBar } from '../ui/FilterBar';
|
||||
import { GroupingUtils } from '../utils/GroupingUtils';
|
||||
|
||||
export class TaskListView extends ItemView {
|
||||
plugin: TaskNotesPlugin;
|
||||
|
|
@ -270,10 +271,7 @@ export class TaskListView extends ItemView {
|
|||
// Wire expand/collapse all (as in preview-all)
|
||||
this.filterBar.on('expandAllGroups', () => {
|
||||
const key = this.currentQuery.groupKey || 'none';
|
||||
const prefs = this.plugin.viewStateManager.getViewPreferences<any>(TASK_LIST_VIEW_TYPE) || {};
|
||||
const next = { ...(prefs.collapsedGroups || {}) } as Record<string, Record<string, boolean>>;
|
||||
next[key] = {};
|
||||
this.plugin.viewStateManager.setViewPreferences(TASK_LIST_VIEW_TYPE, { ...prefs, collapsedGroups: next });
|
||||
GroupingUtils.expandAllGroups(TASK_LIST_VIEW_TYPE, key, this.plugin);
|
||||
// Update DOM
|
||||
this.contentEl.querySelectorAll('.task-group').forEach(section => {
|
||||
section.classList.remove('is-collapsed');
|
||||
|
|
@ -283,18 +281,17 @@ export class TaskListView extends ItemView {
|
|||
});
|
||||
this.filterBar.on('collapseAllGroups', () => {
|
||||
const key = this.currentQuery.groupKey || 'none';
|
||||
const prefs = this.plugin.viewStateManager.getViewPreferences<any>(TASK_LIST_VIEW_TYPE) || {};
|
||||
const next = { ...(prefs.collapsedGroups || {}) } as Record<string, Record<string, boolean>>;
|
||||
const collapsed: Record<string, boolean> = {};
|
||||
const groupNames: string[] = [];
|
||||
this.contentEl.querySelectorAll('.task-group').forEach(section => {
|
||||
const name = (section as HTMLElement).dataset.group;
|
||||
if (name) collapsed[name] = true;
|
||||
section.classList.add('is-collapsed');
|
||||
const list = (section as HTMLElement).querySelector('.task-cards') as HTMLElement | null;
|
||||
if (list) list.style.display = 'none';
|
||||
if (name) {
|
||||
groupNames.push(name);
|
||||
section.classList.add('is-collapsed');
|
||||
const list = (section as HTMLElement).querySelector('.task-cards') as HTMLElement | null;
|
||||
if (list) list.style.display = 'none';
|
||||
}
|
||||
});
|
||||
next[key] = collapsed;
|
||||
this.plugin.viewStateManager.setViewPreferences(TASK_LIST_VIEW_TYPE, { ...prefs, collapsedGroups: next });
|
||||
GroupingUtils.collapseAllGroups(TASK_LIST_VIEW_TYPE, key, groupNames, this.plugin);
|
||||
});
|
||||
|
||||
// Get saved views for the FilterBar
|
||||
|
|
@ -566,22 +563,11 @@ export class TaskListView extends ItemView {
|
|||
|
||||
// Persist and restore collapsed state per grouping key and group name
|
||||
private isGroupCollapsed(groupingKey: string, groupName: string): boolean {
|
||||
try {
|
||||
const prefs = this.plugin.viewStateManager.getViewPreferences<any>(TASK_LIST_VIEW_TYPE) || {};
|
||||
const collapsed = prefs.collapsedGroups || {};
|
||||
return !!collapsed?.[groupingKey]?.[groupName];
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return GroupingUtils.isGroupCollapsed(TASK_LIST_VIEW_TYPE, groupingKey, groupName, this.plugin);
|
||||
}
|
||||
|
||||
private setGroupCollapsed(groupingKey: string, groupName: string, collapsed: boolean): void {
|
||||
const prefs = this.plugin.viewStateManager.getViewPreferences<any>(TASK_LIST_VIEW_TYPE) || {};
|
||||
const next = { ...prefs };
|
||||
if (!next.collapsedGroups) next.collapsedGroups = {};
|
||||
if (!next.collapsedGroups[groupingKey]) next.collapsedGroups[groupingKey] = {};
|
||||
next.collapsedGroups[groupingKey][groupName] = collapsed;
|
||||
this.plugin.viewStateManager.setViewPreferences(TASK_LIST_VIEW_TYPE, next);
|
||||
GroupingUtils.setGroupCollapsed(TASK_LIST_VIEW_TYPE, groupingKey, groupName, collapsed, this.plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -650,26 +636,7 @@ export class TaskListView extends ItemView {
|
|||
* Format group name for display
|
||||
*/
|
||||
private formatGroupName(groupName: string): string {
|
||||
// Check if it's a priority value
|
||||
const priorityConfig = this.plugin.priorityManager.getPriorityConfig(groupName);
|
||||
if (priorityConfig) {
|
||||
return `${priorityConfig.label} priority`;
|
||||
}
|
||||
|
||||
// Check if it's a status value
|
||||
const statusConfig = this.plugin.statusManager.getStatusConfig(groupName);
|
||||
if (statusConfig) {
|
||||
return statusConfig.label;
|
||||
}
|
||||
|
||||
switch (groupName) {
|
||||
case 'all':
|
||||
return 'All tasks';
|
||||
case 'no-status':
|
||||
return 'No status assigned';
|
||||
default:
|
||||
return groupName;
|
||||
}
|
||||
return GroupingUtils.formatGroupName(groupName, this.plugin);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,11 @@
|
|||
.tasknotes-plugin .project-note-subtasks__group-header {
|
||||
margin: 1em 0 0.5em 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--tn-spacing-xs, 0.5em);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__group-header:first-child {
|
||||
|
|
@ -218,6 +223,7 @@
|
|||
margin: 0;
|
||||
padding: 0.3em 0;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__group {
|
||||
|
|
@ -227,6 +233,55 @@
|
|||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Collapsible group functionality */
|
||||
.tasknotes-plugin .project-note-subtasks__group-section {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__group-section.is-collapsed .task-cards {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Toggle button styles (match TaskListView) */
|
||||
.tasknotes-plugin .task-group-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.2em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-group-toggle:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-group-toggle svg,
|
||||
.tasknotes-plugin .task-group-toggle .chevron,
|
||||
.tasknotes-plugin .task-group-toggle .chevron path {
|
||||
color: var(--text-normal);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-group-toggle svg,
|
||||
.tasknotes-plugin .task-group-toggle .chevron path {
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
/* Chevron rotation for expanded state */
|
||||
.tasknotes-plugin .task-group:not(.is-collapsed) .task-group-toggle svg,
|
||||
.tasknotes-plugin .task-group:not(.is-collapsed) .task-group-toggle .chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .task-group.is-collapsed .task-group-toggle svg,
|
||||
.tasknotes-plugin .task-group.is-collapsed .task-group-toggle .chevron {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__task {
|
||||
/* Task card wrapper */
|
||||
margin-bottom: 0.3em;
|
||||
|
|
|
|||
139
tests/unit/utils/GroupingUtils.test.ts
Normal file
139
tests/unit/utils/GroupingUtils.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { GroupingUtils } from '../../../src/utils/GroupingUtils';
|
||||
import { TASK_LIST_VIEW_TYPE, SUBTASK_WIDGET_VIEW_TYPE } from '../../../src/types';
|
||||
|
||||
// Mock TaskNotesPlugin
|
||||
const mockPlugin = {
|
||||
priorityManager: {
|
||||
getPriorityConfig: jest.fn((priority: string) => {
|
||||
if (priority === 'high') return { label: 'High' };
|
||||
if (priority === 'medium') return { label: 'Medium' };
|
||||
return null;
|
||||
})
|
||||
},
|
||||
statusManager: {
|
||||
getStatusConfig: jest.fn((status: string) => {
|
||||
if (status === 'todo') return { label: 'To Do' };
|
||||
if (status === 'done') return { label: 'Done' };
|
||||
return null;
|
||||
})
|
||||
},
|
||||
viewStateManager: {
|
||||
getViewPreferences: jest.fn(() => ({})),
|
||||
setViewPreferences: jest.fn()
|
||||
}
|
||||
} as any;
|
||||
|
||||
describe('GroupingUtils', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('formatGroupName', () => {
|
||||
it('should format priority group names', () => {
|
||||
const result = GroupingUtils.formatGroupName('high', mockPlugin);
|
||||
expect(result).toBe('High priority');
|
||||
});
|
||||
|
||||
it('should format status group names', () => {
|
||||
const result = GroupingUtils.formatGroupName('todo', mockPlugin);
|
||||
expect(result).toBe('To Do');
|
||||
});
|
||||
|
||||
it('should handle special group names', () => {
|
||||
expect(GroupingUtils.formatGroupName('all', mockPlugin)).toBe('All tasks');
|
||||
expect(GroupingUtils.formatGroupName('no-status', mockPlugin)).toBe('No status assigned');
|
||||
expect(GroupingUtils.formatGroupName('No Status', mockPlugin)).toBe('No Status');
|
||||
});
|
||||
|
||||
it('should return original name for unknown groups', () => {
|
||||
const result = GroupingUtils.formatGroupName('custom-group', mockPlugin);
|
||||
expect(result).toBe('custom-group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupDisplayName', () => {
|
||||
it('should include task count in display name', () => {
|
||||
const result = GroupingUtils.getGroupDisplayName('high', 5, mockPlugin);
|
||||
expect(result).toBe('High priority (5)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('group collapse state management', () => {
|
||||
it('should check if group is collapsed', () => {
|
||||
mockPlugin.viewStateManager.getViewPreferences.mockReturnValue({
|
||||
collapsedGroups: {
|
||||
'status': {
|
||||
'todo': true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = GroupingUtils.isGroupCollapsed(TASK_LIST_VIEW_TYPE, 'status', 'todo', mockPlugin);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-collapsed groups', () => {
|
||||
mockPlugin.viewStateManager.getViewPreferences.mockReturnValue({});
|
||||
|
||||
const result = GroupingUtils.isGroupCollapsed(TASK_LIST_VIEW_TYPE, 'status', 'todo', mockPlugin);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should set group collapsed state', () => {
|
||||
mockPlugin.viewStateManager.getViewPreferences.mockReturnValue({});
|
||||
|
||||
GroupingUtils.setGroupCollapsed(SUBTASK_WIDGET_VIEW_TYPE, 'status', 'todo', true, mockPlugin);
|
||||
|
||||
expect(mockPlugin.viewStateManager.setViewPreferences).toHaveBeenCalledWith(
|
||||
SUBTASK_WIDGET_VIEW_TYPE,
|
||||
{
|
||||
collapsedGroups: {
|
||||
'status': {
|
||||
'todo': true
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should expand all groups', () => {
|
||||
mockPlugin.viewStateManager.getViewPreferences.mockReturnValue({
|
||||
collapsedGroups: {
|
||||
'status': {
|
||||
'todo': true,
|
||||
'done': true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
GroupingUtils.expandAllGroups(TASK_LIST_VIEW_TYPE, 'status', mockPlugin);
|
||||
|
||||
expect(mockPlugin.viewStateManager.setViewPreferences).toHaveBeenCalledWith(
|
||||
TASK_LIST_VIEW_TYPE,
|
||||
{
|
||||
collapsedGroups: {
|
||||
'status': {}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should collapse all groups', () => {
|
||||
mockPlugin.viewStateManager.getViewPreferences.mockReturnValue({});
|
||||
|
||||
GroupingUtils.collapseAllGroups(TASK_LIST_VIEW_TYPE, 'status', ['todo', 'done'], mockPlugin);
|
||||
|
||||
expect(mockPlugin.viewStateManager.setViewPreferences).toHaveBeenCalledWith(
|
||||
TASK_LIST_VIEW_TYPE,
|
||||
{
|
||||
collapsedGroups: {
|
||||
'status': {
|
||||
'todo': true,
|
||||
'done': true
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue