mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(manager): enhance FileTaskManager with expanded functionality
- Add comprehensive file task operations (batch create, update, delete) - Implement file task search and querying capabilities - Add tests for FileTaskManager operations - Update FileTaskView and TaskSpecificView for better file task handling - Improve task metadata processing and validation
This commit is contained in:
parent
4c5f560c2b
commit
8e292cb4f3
4 changed files with 578 additions and 87 deletions
|
|
@ -0,0 +1,272 @@
|
|||
/**
|
||||
* Tests for frontmatter title protection in FileTaskManager
|
||||
*/
|
||||
|
||||
import { App } from "obsidian";
|
||||
import { FileTaskManagerImpl } from "../../managers/file-task-manager";
|
||||
import { FileTask } from "../../types/file-task";
|
||||
import { FileSourceConfiguration } from "../../types/file-source";
|
||||
|
||||
// Mock Obsidian App
|
||||
const mockApp = {
|
||||
vault: {
|
||||
getFileByPath: jest.fn(),
|
||||
},
|
||||
fileManager: {
|
||||
renameFile: jest.fn(),
|
||||
},
|
||||
} as unknown as App;
|
||||
|
||||
// Mock BasesEntry
|
||||
const createMockBasesEntry = (filePath: string, properties: Record<string, any> = {}) => ({
|
||||
ctx: {
|
||||
_local: {},
|
||||
app: mockApp,
|
||||
filter: {},
|
||||
formulas: {},
|
||||
localUsed: false,
|
||||
},
|
||||
file: {
|
||||
parent: null,
|
||||
deleted: false,
|
||||
vault: null,
|
||||
path: filePath,
|
||||
name: filePath.split('/').pop() || filePath,
|
||||
extension: 'md',
|
||||
getShortName: () => filePath.split('/').pop() || filePath,
|
||||
},
|
||||
formulas: {},
|
||||
implicit: {
|
||||
file: null,
|
||||
name: filePath.split('/').pop() || filePath,
|
||||
path: filePath,
|
||||
folder: filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : '',
|
||||
ext: 'md',
|
||||
},
|
||||
lazyEvalCache: {},
|
||||
properties,
|
||||
updateProperty: jest.fn(),
|
||||
getValue: jest.fn((prop: any) => {
|
||||
if (prop.type === 'property') {
|
||||
return properties[prop.name];
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
getFormulaValue: jest.fn(),
|
||||
getPropertyKeys: jest.fn(() => Object.keys(properties)),
|
||||
});
|
||||
|
||||
describe('FileTaskManager - Frontmatter Title Protection', () => {
|
||||
let fileTaskManager: FileTaskManagerImpl;
|
||||
let mockConfig: FileSourceConfiguration;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockConfig = {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: { enabled: true, taskFields: ['status'], requireAllFields: false },
|
||||
tags: { enabled: false, taskTags: [], matchMode: 'exact' },
|
||||
templates: { enabled: false, templatePaths: [], checkTemplateMetadata: false },
|
||||
paths: { enabled: false, taskPaths: [], matchMode: 'prefix' }
|
||||
},
|
||||
fileTaskProperties: {
|
||||
contentSource: 'title',
|
||||
stripExtension: true,
|
||||
defaultStatus: ' ',
|
||||
preferFrontmatterTitle: true
|
||||
},
|
||||
relationships: {
|
||||
enableChildRelationships: false,
|
||||
enableMetadataInheritance: false,
|
||||
inheritanceFields: []
|
||||
},
|
||||
performance: {
|
||||
enableWorkerProcessing: false,
|
||||
enableCaching: false,
|
||||
cacheTTL: 0
|
||||
},
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: {},
|
||||
symbolToMetadata: {},
|
||||
autoDetect: false,
|
||||
caseSensitive: false
|
||||
}
|
||||
};
|
||||
|
||||
fileTaskManager = new FileTaskManagerImpl(mockApp, mockConfig);
|
||||
});
|
||||
|
||||
describe('handleContentUpdate', () => {
|
||||
it('should update frontmatter title when preferFrontmatterTitle is enabled and contentSource is title', async () => {
|
||||
const mockEntry = createMockBasesEntry('test-file.md', { title: 'Original Title' });
|
||||
const fileTask: FileTask = {
|
||||
id: 'test-id',
|
||||
content: 'Original Title',
|
||||
filePath: 'test-file.md',
|
||||
completed: false,
|
||||
status: ' ',
|
||||
metadata: {
|
||||
tags: [],
|
||||
children: []
|
||||
},
|
||||
sourceEntry: mockEntry,
|
||||
isFileTask: true
|
||||
};
|
||||
|
||||
await fileTaskManager.updateFileTask(fileTask, { content: 'New Title' });
|
||||
|
||||
// Should update frontmatter title, not rename file
|
||||
expect(mockEntry.updateProperty).toHaveBeenCalledWith('title', 'New Title');
|
||||
expect(mockApp.fileManager.renameFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rename file when preferFrontmatterTitle is disabled', async () => {
|
||||
// Disable frontmatter title preference
|
||||
mockConfig.fileTaskProperties.preferFrontmatterTitle = false;
|
||||
fileTaskManager = new FileTaskManagerImpl(mockApp, mockConfig);
|
||||
|
||||
const mockFile = { path: 'test-file.md' };
|
||||
jest.mocked(mockApp.vault.getFileByPath).mockReturnValue(mockFile);
|
||||
|
||||
const mockEntry = createMockBasesEntry('test-file.md', { title: 'Original Title' });
|
||||
const fileTask: FileTask = {
|
||||
id: 'test-id',
|
||||
content: 'Original Title',
|
||||
filePath: 'test-file.md',
|
||||
completed: false,
|
||||
status: ' ',
|
||||
metadata: {
|
||||
tags: [],
|
||||
children: []
|
||||
},
|
||||
sourceEntry: mockEntry,
|
||||
isFileTask: true
|
||||
};
|
||||
|
||||
await fileTaskManager.updateFileTask(fileTask, { content: 'New Title' });
|
||||
|
||||
// Should rename file, not update frontmatter
|
||||
expect(mockApp.fileManager.renameFile).toHaveBeenCalledWith(mockFile, 'New Title.md');
|
||||
expect(mockEntry.updateProperty).not.toHaveBeenCalledWith('title', 'New Title');
|
||||
});
|
||||
|
||||
it('should rename file when contentSource is not title', async () => {
|
||||
// Change content source to filename
|
||||
mockConfig.fileTaskProperties.contentSource = 'filename';
|
||||
fileTaskManager = new FileTaskManagerImpl(mockApp, mockConfig);
|
||||
|
||||
const mockFile = { path: 'test-file.md' };
|
||||
jest.mocked(mockApp.vault.getFileByPath).mockReturnValue(mockFile);
|
||||
|
||||
const mockEntry = createMockBasesEntry('test-file.md');
|
||||
const fileTask: FileTask = {
|
||||
id: 'test-id',
|
||||
content: 'test-file',
|
||||
filePath: 'test-file.md',
|
||||
completed: false,
|
||||
status: ' ',
|
||||
metadata: {
|
||||
tags: [],
|
||||
children: []
|
||||
},
|
||||
sourceEntry: mockEntry,
|
||||
isFileTask: true
|
||||
};
|
||||
|
||||
await fileTaskManager.updateFileTask(fileTask, { content: 'new-filename' });
|
||||
|
||||
// Should rename file even with preferFrontmatterTitle enabled
|
||||
expect(mockApp.fileManager.renameFile).toHaveBeenCalledWith(mockFile, 'new-filename.md');
|
||||
expect(mockEntry.updateProperty).not.toHaveBeenCalledWith('title', 'new-filename');
|
||||
});
|
||||
|
||||
it('should fallback to file renaming when frontmatter update fails', async () => {
|
||||
const mockFile = { path: 'test-file.md' };
|
||||
jest.mocked(mockApp.vault.getFileByPath).mockReturnValue(mockFile);
|
||||
|
||||
const mockEntry = createMockBasesEntry('test-file.md', { title: 'Original Title' });
|
||||
// Make updateProperty throw an error
|
||||
mockEntry.updateProperty.mockImplementation(() => {
|
||||
throw new Error('Failed to update property');
|
||||
});
|
||||
|
||||
const fileTask: FileTask = {
|
||||
id: 'test-id',
|
||||
content: 'Original Title',
|
||||
filePath: 'test-file.md',
|
||||
completed: false,
|
||||
status: ' ',
|
||||
metadata: {
|
||||
tags: [],
|
||||
children: []
|
||||
},
|
||||
sourceEntry: mockEntry,
|
||||
isFileTask: true
|
||||
};
|
||||
|
||||
await fileTaskManager.updateFileTask(fileTask, { content: 'New Title' });
|
||||
|
||||
// Should try frontmatter first, then fallback to file renaming
|
||||
expect(mockEntry.updateProperty).toHaveBeenCalledWith('title', 'New Title');
|
||||
expect(mockApp.fileManager.renameFile).toHaveBeenCalledWith(mockFile, 'New Title.md');
|
||||
});
|
||||
|
||||
it('should update content property when contentSource is not filename', async () => {
|
||||
// Set contentSource to 'title' (not filename)
|
||||
mockConfig.fileTaskProperties.contentSource = 'title';
|
||||
fileTaskManager = new FileTaskManagerImpl(mockApp, mockConfig);
|
||||
|
||||
const mockEntry = createMockBasesEntry('test-file.md', { title: 'Original Title' });
|
||||
const fileTask: FileTask = {
|
||||
id: 'test-id',
|
||||
content: 'Original Title',
|
||||
filePath: 'test-file.md',
|
||||
completed: false,
|
||||
status: ' ',
|
||||
metadata: {
|
||||
tags: [],
|
||||
children: []
|
||||
},
|
||||
sourceEntry: mockEntry,
|
||||
isFileTask: true
|
||||
};
|
||||
|
||||
await fileTaskManager.updateFileTask(fileTask, { content: 'New Title', status: 'x' });
|
||||
|
||||
// Should update both content and status properties
|
||||
expect(mockEntry.updateProperty).toHaveBeenCalledWith('title', 'New Title');
|
||||
expect(mockEntry.updateProperty).toHaveBeenCalledWith('status', 'x');
|
||||
expect(mockEntry.updateProperty).toHaveBeenCalledWith('completed', false);
|
||||
});
|
||||
|
||||
it('should handle custom content field updates', async () => {
|
||||
// Set contentSource to 'custom' with a custom field
|
||||
mockConfig.fileTaskProperties.contentSource = 'custom';
|
||||
mockConfig.fileTaskProperties.customContentField = 'taskName';
|
||||
fileTaskManager = new FileTaskManagerImpl(mockApp, mockConfig);
|
||||
|
||||
const mockEntry = createMockBasesEntry('test-file.md', { taskName: 'Original Task' });
|
||||
const fileTask: FileTask = {
|
||||
id: 'test-id',
|
||||
content: 'Original Task',
|
||||
filePath: 'test-file.md',
|
||||
completed: false,
|
||||
status: ' ',
|
||||
metadata: {
|
||||
tags: [],
|
||||
children: []
|
||||
},
|
||||
sourceEntry: mockEntry,
|
||||
isFileTask: true
|
||||
};
|
||||
|
||||
await fileTaskManager.updateFileTask(fileTask, { content: 'New Task Name' });
|
||||
|
||||
// Should update the custom field
|
||||
expect(mockEntry.updateProperty).toHaveBeenCalledWith('taskName', 'New Task Name');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
FileTaskViewConfig,
|
||||
} from "../types/file-task";
|
||||
import { TFile } from "obsidian";
|
||||
import { FileSourceConfiguration } from "../types/file-source";
|
||||
|
||||
// BasesEntry interface (copied from types to avoid import issues)
|
||||
interface BasesEntry {
|
||||
|
|
@ -70,7 +71,10 @@ export const DEFAULT_FILE_TASK_MAPPING: FileTaskPropertyMapping = {
|
|||
};
|
||||
|
||||
export class FileTaskManagerImpl implements FileTaskManager {
|
||||
constructor(private app: App) {}
|
||||
constructor(
|
||||
private app: App,
|
||||
private fileSourceConfig?: FileSourceConfiguration
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Convert a BasesEntry to a FileTask
|
||||
|
|
@ -197,8 +201,17 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
): Record<string, any> {
|
||||
const updates: Record<string, any> = {};
|
||||
|
||||
// Don't update content property as it should be handled by file renaming
|
||||
// updates[mapping.contentProperty] = task.content;
|
||||
// Update content property based on configuration
|
||||
const config = this.fileSourceConfig?.fileTaskProperties;
|
||||
if (config?.contentSource && config.contentSource !== 'filename') {
|
||||
// Only update content property if it's not handled by file renaming
|
||||
const shouldUpdateProperty = this.shouldUpdateContentProperty(config);
|
||||
if (shouldUpdateProperty) {
|
||||
updates[mapping.contentProperty] = task.content;
|
||||
}
|
||||
}
|
||||
// Note: If contentSource is 'filename', content updates are handled by file renaming
|
||||
|
||||
updates[mapping.statusProperty] = task.status;
|
||||
updates[mapping.completedProperty] = task.completed;
|
||||
|
||||
|
|
@ -285,12 +298,12 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
// Merge updates into the task
|
||||
const updatedTask = { ...task, ...updates };
|
||||
|
||||
// Handle file renaming if content changed
|
||||
// Handle content changes
|
||||
if (updates.content && updates.content !== task.content) {
|
||||
await this.updateFileName(task, updates.content);
|
||||
await this.handleContentUpdate(task, updates.content);
|
||||
}
|
||||
|
||||
// Convert to property updates (excluding content which is handled by file renaming)
|
||||
// Convert to property updates (excluding content which is handled separately)
|
||||
const propertyUpdates = this.fileTaskToPropertyUpdates(updatedTask);
|
||||
|
||||
console.log(
|
||||
|
|
@ -308,6 +321,165 @@ export class FileTaskManagerImpl implements FileTaskManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if content should be updated via property update vs file operations
|
||||
*/
|
||||
private shouldUpdateContentProperty(config: any): boolean {
|
||||
switch (config.contentSource) {
|
||||
case 'title':
|
||||
// Only update property if preferFrontmatterTitle is enabled
|
||||
return config.preferFrontmatterTitle === true;
|
||||
case 'h1':
|
||||
// H1 updates are handled by file content modification, not property updates
|
||||
return false;
|
||||
case 'custom':
|
||||
// Custom fields are always updated via properties
|
||||
return true;
|
||||
case 'filename':
|
||||
default:
|
||||
// Filename updates are handled by file renaming
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle content update - update frontmatter property, rename file, or update custom field
|
||||
*/
|
||||
private async handleContentUpdate(
|
||||
task: FileTask,
|
||||
newContent: string
|
||||
): Promise<void> {
|
||||
const config = this.fileSourceConfig?.fileTaskProperties;
|
||||
|
||||
if (!config) {
|
||||
console.warn('[FileTaskManager] No file source config available, skipping content update');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (config.contentSource) {
|
||||
case 'title':
|
||||
if (config.preferFrontmatterTitle) {
|
||||
await this.updateFrontmatterTitle(task, newContent);
|
||||
} else {
|
||||
await this.updateFileName(task, newContent);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'h1':
|
||||
// For H1 content source, we need to update the first heading in the file
|
||||
await this.updateH1Heading(task, newContent);
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
// For custom content source, update the custom field in frontmatter
|
||||
if (config.customContentField) {
|
||||
await this.updateCustomContentField(task, newContent, config.customContentField);
|
||||
} else {
|
||||
console.warn('[FileTaskManager] Custom content source specified but no customContentField configured');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'filename':
|
||||
default:
|
||||
// For filename content source, rename the file
|
||||
await this.updateFileName(task, newContent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update frontmatter title property
|
||||
*/
|
||||
private async updateFrontmatterTitle(
|
||||
task: FileTask,
|
||||
newTitle: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Update the title property in frontmatter through the source entry
|
||||
task.sourceEntry.updateProperty('title', newTitle);
|
||||
console.log(
|
||||
`[FileTaskManager] Updated frontmatter title for ${task.filePath} to: ${newTitle}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[FileTaskManager] Failed to update frontmatter title:`, error);
|
||||
// Fallback to file renaming if frontmatter update fails
|
||||
await this.updateFileName(task, newTitle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update H1 heading in the file content
|
||||
*/
|
||||
private async updateH1Heading(
|
||||
task: FileTask,
|
||||
newHeading: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const file = this.app.vault.getFileByPath(task.filePath);
|
||||
if (!file) {
|
||||
console.error(`[FileTaskManager] File not found: ${task.filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Find the first H1 heading
|
||||
let h1LineIndex = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith('# ')) {
|
||||
h1LineIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (h1LineIndex >= 0) {
|
||||
// Update existing H1
|
||||
lines[h1LineIndex] = `# ${newHeading}`;
|
||||
} else {
|
||||
// Add new H1 at the beginning (after frontmatter if present)
|
||||
let insertIndex = 0;
|
||||
if (content.startsWith('---')) {
|
||||
// Skip frontmatter
|
||||
const frontmatterEnd = content.indexOf('\n---\n', 3);
|
||||
if (frontmatterEnd >= 0) {
|
||||
const frontmatterLines = content.substring(0, frontmatterEnd + 5).split('\n').length - 1;
|
||||
insertIndex = frontmatterLines;
|
||||
}
|
||||
}
|
||||
lines.splice(insertIndex, 0, `# ${newHeading}`, '');
|
||||
}
|
||||
|
||||
const newContent = lines.join('\n');
|
||||
await this.app.vault.modify(file, newContent);
|
||||
|
||||
console.log(
|
||||
`[FileTaskManager] Updated H1 heading for ${task.filePath} to: ${newHeading}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[FileTaskManager] Failed to update H1 heading:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update custom content field in frontmatter
|
||||
*/
|
||||
private async updateCustomContentField(
|
||||
task: FileTask,
|
||||
newContent: string,
|
||||
fieldName: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Update the custom field in frontmatter through the source entry
|
||||
task.sourceEntry.updateProperty(fieldName, newContent);
|
||||
console.log(
|
||||
`[FileTaskManager] Updated custom field '${fieldName}' for ${task.filePath} to: ${newContent}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[FileTaskManager] Failed to update custom field '${fieldName}':`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update file name when task content changes
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export class FileTaskView extends Component implements BasesView {
|
|||
this.containerEl = containerEl;
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.fileTaskManager = new FileTaskManagerImpl(app);
|
||||
this.fileTaskManager = new FileTaskManagerImpl(app, plugin.settings?.fileSource);
|
||||
|
||||
if (propertyMapping) {
|
||||
this.propertyMapping = propertyMapping;
|
||||
|
|
|
|||
|
|
@ -95,7 +95,10 @@ export class TaskSpecificView extends ItemView {
|
|||
// Data management
|
||||
tasks: Task[] = [];
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, private plugin: TaskProgressBarPlugin) {
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
) {
|
||||
super(leaf);
|
||||
|
||||
// 使用预加载的任务进行快速初始显示
|
||||
|
|
@ -138,7 +141,7 @@ export class TaskSpecificView extends ItemView {
|
|||
if (this.currentViewId) {
|
||||
this.switchView(
|
||||
this.currentViewId,
|
||||
this.currentProject
|
||||
this.currentProject,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -155,7 +158,7 @@ export class TaskSpecificView extends ItemView {
|
|||
getDisplayText(): string {
|
||||
const currentViewConfig = getViewSettingOrDefault(
|
||||
this.plugin,
|
||||
this.currentViewId
|
||||
this.currentViewId,
|
||||
);
|
||||
// Potentially add project name if relevant for 'projects' view?
|
||||
return currentViewConfig.name;
|
||||
|
|
@ -164,7 +167,7 @@ export class TaskSpecificView extends ItemView {
|
|||
getIcon(): string {
|
||||
const currentViewConfig = getViewSettingOrDefault(
|
||||
this.plugin,
|
||||
this.currentViewId
|
||||
this.currentViewId,
|
||||
);
|
||||
return currentViewConfig.icon;
|
||||
}
|
||||
|
|
@ -183,25 +186,28 @@ export class TaskSpecificView extends ItemView {
|
|||
}, 150); // 150ms debounce delay
|
||||
|
||||
// 1. 首先注册事件监听器,确保不会错过任何更新
|
||||
if (isDataflowEnabled(this.plugin) && this.plugin.dataflowOrchestrator) {
|
||||
if (
|
||||
isDataflowEnabled(this.plugin) &&
|
||||
this.plugin.dataflowOrchestrator
|
||||
) {
|
||||
// Dataflow: 订阅统一事件
|
||||
const { on, Events } = await import("../dataflow/events/Events");
|
||||
this.registerEvent(
|
||||
on(this.app, Events.CACHE_READY, async () => {
|
||||
// 冷启动就绪,从快照加载
|
||||
await this.loadTasksFast(true);
|
||||
})
|
||||
}),
|
||||
);
|
||||
this.registerEvent(
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate)
|
||||
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate),
|
||||
);
|
||||
} else {
|
||||
// Legacy: 兼容旧事件
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"task-genius:task-cache-updated",
|
||||
debouncedViewUpdate
|
||||
)
|
||||
debouncedViewUpdate,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +219,7 @@ export class TaskSpecificView extends ItemView {
|
|||
"TaskSpecificView 过滤器实时变更:",
|
||||
filterState,
|
||||
"leafId:",
|
||||
leafId
|
||||
leafId,
|
||||
);
|
||||
|
||||
// 只处理来自当前视图的过滤器变更
|
||||
|
|
@ -225,8 +231,8 @@ export class TaskSpecificView extends ItemView {
|
|||
this.debouncedApplyFilter();
|
||||
}
|
||||
// 忽略来自其他leafId的变更,包括基础过滤器(view-config-开头)
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 2. 初始化组件(但先不传入数据)
|
||||
|
|
@ -256,7 +262,7 @@ export class TaskSpecificView extends ItemView {
|
|||
(this.leaf.tabHeaderStatusContainerEl as HTMLElement)?.empty();
|
||||
(this.leaf.tabHeaderEl as HTMLElement)?.toggleClass(
|
||||
"task-genius-tab-header",
|
||||
true
|
||||
true,
|
||||
);
|
||||
this.tabActionButton = (
|
||||
this.leaf.tabHeaderStatusContainerEl as HTMLElement
|
||||
|
|
@ -274,11 +280,11 @@ export class TaskSpecificView extends ItemView {
|
|||
this.plugin.app,
|
||||
this.plugin,
|
||||
{},
|
||||
true
|
||||
true,
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
if (this.tabActionButton) {
|
||||
this.register(() => {
|
||||
|
|
@ -311,7 +317,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.contentComponent);
|
||||
this.contentComponent.load();
|
||||
|
|
@ -333,7 +339,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.forecastComponent);
|
||||
this.forecastComponent.load();
|
||||
|
|
@ -353,7 +359,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.tagsComponent);
|
||||
this.tagsComponent.load();
|
||||
|
|
@ -373,7 +379,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.projectsComponent);
|
||||
this.projectsComponent.load();
|
||||
|
|
@ -393,7 +399,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => {
|
||||
this.handleTaskContextMenu(event, task);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.reviewComponent);
|
||||
this.reviewComponent.load();
|
||||
|
|
@ -414,7 +420,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onEventContextMenu: (ev: MouseEvent, event: CalendarEvent) => {
|
||||
this.handleTaskContextMenu(ev, event);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.calendarComponent);
|
||||
this.calendarComponent.load();
|
||||
|
|
@ -432,7 +438,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskSelected: this.handleTaskSelection.bind(this),
|
||||
onTaskCompleted: this.toggleTaskCompletion.bind(this),
|
||||
onTaskContextMenu: this.handleTaskContextMenu.bind(this),
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.kanbanComponent);
|
||||
this.kanbanComponent.containerEl.hide();
|
||||
|
|
@ -444,21 +450,21 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskSelected: this.handleTaskSelection.bind(this),
|
||||
onTaskCompleted: this.toggleTaskCompletion.bind(this),
|
||||
onTaskContextMenu: this.handleTaskContextMenu.bind(this),
|
||||
}
|
||||
},
|
||||
);
|
||||
this.addChild(this.ganttComponent);
|
||||
this.ganttComponent.containerEl.hide();
|
||||
|
||||
this.habitsComponent = new HabitsComponent(
|
||||
this.plugin,
|
||||
this.rootContainerEl
|
||||
this.rootContainerEl,
|
||||
);
|
||||
this.addChild(this.habitsComponent);
|
||||
this.habitsComponent.containerEl.hide();
|
||||
this.detailsComponent = new TaskDetailsComponent(
|
||||
this.rootContainerEl,
|
||||
this.app,
|
||||
this.plugin
|
||||
this.plugin,
|
||||
);
|
||||
this.addChild(this.detailsComponent);
|
||||
this.detailsComponent.load();
|
||||
|
|
@ -476,7 +482,7 @@ export class TaskSpecificView extends ItemView {
|
|||
onTaskStatusUpdate:
|
||||
this.handleKanbanTaskStatusUpdate.bind(this),
|
||||
onEventContextMenu: this.handleTaskContextMenu.bind(this),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.addChild(this.viewComponentManager);
|
||||
|
|
@ -492,7 +498,7 @@ export class TaskSpecificView extends ItemView {
|
|||
t("Details"),
|
||||
() => {
|
||||
this.toggleDetailsVisibility(!this.isDetailsVisible);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.detailsToggleBtn.toggleClass("panel-toggle-btn", true);
|
||||
|
|
@ -504,7 +510,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.plugin.app,
|
||||
this.plugin,
|
||||
{},
|
||||
true
|
||||
true,
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
|
|
@ -514,7 +520,7 @@ export class TaskSpecificView extends ItemView {
|
|||
const popover = new ViewTaskFilterPopover(
|
||||
this.plugin.app,
|
||||
this.leaf.id,
|
||||
this.plugin
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
// 设置关闭回调 - 现在主要用于处理取消操作
|
||||
|
|
@ -534,7 +540,7 @@ export class TaskSpecificView extends ItemView {
|
|||
const filterState = this
|
||||
.liveFilterState as RootFilterState;
|
||||
popover.taskFilterComponent.loadFilterState(
|
||||
filterState
|
||||
filterState,
|
||||
);
|
||||
}
|
||||
}, 100);
|
||||
|
|
@ -545,7 +551,7 @@ export class TaskSpecificView extends ItemView {
|
|||
const modal = new ViewTaskFilterModal(
|
||||
this.plugin.app,
|
||||
this.leaf.id,
|
||||
this.plugin
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
// 设置关闭回调 - 现在主要用于处理取消操作
|
||||
|
|
@ -611,7 +617,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.detailsToggleBtn.toggleClass("is-active", visible);
|
||||
this.detailsToggleBtn.setAttribute(
|
||||
"aria-label",
|
||||
visible ? t("Hide Details") : t("Show Details")
|
||||
visible ? t("Hide Details") : t("Show Details"),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -629,7 +635,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.detailsComponent.onTaskEdit = (task: Task) => this.editTask(task);
|
||||
this.detailsComponent.onTaskUpdate = async (
|
||||
originalTask: Task,
|
||||
updatedTask: Task
|
||||
updatedTask: Task,
|
||||
) => {
|
||||
await this.updateTask(originalTask, updatedTask);
|
||||
};
|
||||
|
|
@ -640,7 +646,11 @@ export class TaskSpecificView extends ItemView {
|
|||
// No sidebar component handlers needed
|
||||
}
|
||||
|
||||
private switchView(viewId: ViewMode, project?: string | null, forceRefresh: boolean = false) {
|
||||
private switchView(
|
||||
viewId: ViewMode,
|
||||
project?: string | null,
|
||||
forceRefresh: boolean = false,
|
||||
) {
|
||||
this.currentViewId = viewId;
|
||||
this.currentProject = project;
|
||||
console.log("Switching view to:", viewId, "Project:", project);
|
||||
|
|
@ -680,7 +690,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.app,
|
||||
this.plugin,
|
||||
twoColumnConfig,
|
||||
viewId
|
||||
viewId,
|
||||
);
|
||||
this.addChild(twoColumnComponent);
|
||||
|
||||
|
|
@ -742,7 +752,7 @@ export class TaskSpecificView extends ItemView {
|
|||
if (targetComponent) {
|
||||
console.log(
|
||||
`Activating component for view ${viewId}`,
|
||||
targetComponent.constructor.name
|
||||
targetComponent.constructor.name,
|
||||
);
|
||||
targetComponent.containerEl.show();
|
||||
if (typeof targetComponent.setTasks === "function") {
|
||||
|
|
@ -760,17 +770,24 @@ export class TaskSpecificView extends ItemView {
|
|||
filterOptions.advancedFilter = this.currentFilterState;
|
||||
}
|
||||
|
||||
let filteredTasks = filterTasks(this.tasks, viewId, this.plugin, filterOptions);
|
||||
|
||||
let filteredTasks = filterTasks(
|
||||
this.tasks,
|
||||
viewId,
|
||||
this.plugin,
|
||||
filterOptions,
|
||||
);
|
||||
|
||||
// Filter out badge tasks for forecast view - they should only appear in event view
|
||||
if (viewId === "forecast") {
|
||||
filteredTasks = filteredTasks.filter(task => !(task as any).badge);
|
||||
filteredTasks = filteredTasks.filter(
|
||||
(task) => !(task as any).badge,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
targetComponent.setTasks(
|
||||
filteredTasks,
|
||||
this.tasks,
|
||||
forceRefresh
|
||||
forceRefresh,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -790,13 +807,13 @@ export class TaskSpecificView extends ItemView {
|
|||
}
|
||||
|
||||
targetComponent.updateTasks(
|
||||
filterTasks(this.tasks, viewId, this.plugin, filterOptions)
|
||||
filterTasks(this.tasks, viewId, this.plugin, filterOptions),
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof targetComponent.setViewMode === "function") {
|
||||
console.log(
|
||||
`Setting view mode for ${viewId} to ${modeForComponent} with project ${project}`
|
||||
`Setting view mode for ${viewId} to ${modeForComponent} with project ${project}`,
|
||||
);
|
||||
targetComponent.setViewMode(modeForComponent, project);
|
||||
}
|
||||
|
|
@ -823,14 +840,16 @@ export class TaskSpecificView extends ItemView {
|
|||
this.tasks,
|
||||
component.getViewId(),
|
||||
this.plugin,
|
||||
filterOptions
|
||||
filterOptions,
|
||||
);
|
||||
|
||||
|
||||
// Filter out badge tasks for forecast view - they should only appear in event view
|
||||
if (component.getViewId() === "forecast") {
|
||||
filteredTasks = filteredTasks.filter(task => !(task as any).badge);
|
||||
filteredTasks = filteredTasks.filter(
|
||||
(task) => !(task as any).badge,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
component.setTasks(filteredTasks);
|
||||
}
|
||||
});
|
||||
|
|
@ -855,8 +874,11 @@ export class TaskSpecificView extends ItemView {
|
|||
if (!this.currentViewId) return null;
|
||||
|
||||
// Check for special view types first
|
||||
const viewConfig = getViewSettingOrDefault(this.plugin, this.currentViewId);
|
||||
|
||||
const viewConfig = getViewSettingOrDefault(
|
||||
this.plugin,
|
||||
this.currentViewId,
|
||||
);
|
||||
|
||||
// Handle TwoColumn views
|
||||
if (viewConfig.specificConfig?.viewType === "twocolumn") {
|
||||
return this.twoColumnViewComponents.get(this.currentViewId);
|
||||
|
|
@ -871,7 +893,10 @@ export class TaskSpecificView extends ItemView {
|
|||
|
||||
// Handle forecast views
|
||||
const specificViewType = viewConfig.specificConfig?.viewType;
|
||||
if (specificViewType === "forecast" || this.currentViewId === "forecast") {
|
||||
if (
|
||||
specificViewType === "forecast" ||
|
||||
this.currentViewId === "forecast"
|
||||
) {
|
||||
return this.forecastComponent;
|
||||
}
|
||||
|
||||
|
|
@ -938,7 +963,7 @@ export class TaskSpecificView extends ItemView {
|
|||
},
|
||||
(el) => {
|
||||
createTaskCheckbox(mark, task, el);
|
||||
}
|
||||
},
|
||||
);
|
||||
item.titleEl.createEl("span", {
|
||||
cls: "status-option",
|
||||
|
|
@ -1010,20 +1035,29 @@ export class TaskSpecificView extends ItemView {
|
|||
|
||||
private async loadTasks(
|
||||
forceSync: boolean = false,
|
||||
skipViewUpdate: boolean = false
|
||||
skipViewUpdate: boolean = false,
|
||||
) {
|
||||
// Only use dataflow - TaskManager is deprecated
|
||||
if (!this.plugin.dataflowOrchestrator) {
|
||||
console.warn("[TaskSpecificView] Dataflow orchestrator not available, waiting for initialization...");
|
||||
console.warn(
|
||||
"[TaskSpecificView] Dataflow orchestrator not available, waiting for initialization...",
|
||||
);
|
||||
this.tasks = [];
|
||||
} else {
|
||||
try {
|
||||
console.log("[TaskSpecificView] Loading tasks from dataflow orchestrator...");
|
||||
console.log(
|
||||
"[TaskSpecificView] Loading tasks from dataflow orchestrator...",
|
||||
);
|
||||
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
|
||||
this.tasks = await queryAPI.getAllTasks();
|
||||
console.log(`[TaskSpecificView] Loaded ${this.tasks.length} tasks from dataflow`);
|
||||
console.log(
|
||||
`[TaskSpecificView] Loaded ${this.tasks.length} tasks from dataflow`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[TaskSpecificView] Error loading tasks from dataflow:", error);
|
||||
console.error(
|
||||
"[TaskSpecificView] Error loading tasks from dataflow:",
|
||||
error,
|
||||
);
|
||||
this.tasks = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1045,17 +1079,26 @@ export class TaskSpecificView extends ItemView {
|
|||
private async loadTasksFast(skipViewUpdate: boolean = false) {
|
||||
// Only use dataflow
|
||||
if (!this.plugin.dataflowOrchestrator) {
|
||||
console.warn("[TaskSpecificView] Dataflow orchestrator not available for fast load");
|
||||
console.warn(
|
||||
"[TaskSpecificView] Dataflow orchestrator not available for fast load",
|
||||
);
|
||||
this.tasks = [];
|
||||
} else {
|
||||
try {
|
||||
console.log("[TaskSpecificView] Loading tasks fast from dataflow orchestrator...");
|
||||
console.log(
|
||||
"[TaskSpecificView] Loading tasks fast from dataflow orchestrator...",
|
||||
);
|
||||
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
|
||||
// For fast loading, use regular getAllTasks (it should be cached)
|
||||
this.tasks = await queryAPI.getAllTasks();
|
||||
console.log(`[TaskSpecificView] Loaded ${this.tasks.length} tasks (fast from dataflow)`);
|
||||
console.log(
|
||||
`[TaskSpecificView] Loaded ${this.tasks.length} tasks (fast from dataflow)`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[TaskSpecificView] Error loading tasks fast from dataflow:", error);
|
||||
console.error(
|
||||
"[TaskSpecificView] Error loading tasks fast from dataflow:",
|
||||
error,
|
||||
);
|
||||
this.tasks = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1085,7 +1128,9 @@ export class TaskSpecificView extends ItemView {
|
|||
const tasks = await queryAPI.getAllTasks();
|
||||
if (tasks.length !== this.tasks.length || tasks.length === 0) {
|
||||
this.tasks = tasks;
|
||||
console.log(`TaskSpecificView updated with ${this.tasks.length} tasks (dataflow sync)`);
|
||||
console.log(
|
||||
`TaskSpecificView updated with ${this.tasks.length} tasks (dataflow sync)`,
|
||||
);
|
||||
// Don't trigger view update here as it will be handled by events
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -1098,7 +1143,7 @@ export class TaskSpecificView extends ItemView {
|
|||
console.log(
|
||||
"应用 TaskSpecificView 当前过滤状态:",
|
||||
this.liveFilterState ? "有实时筛选器" : "无实时筛选器",
|
||||
this.currentFilterState ? "有过滤器" : "无过滤器"
|
||||
this.currentFilterState ? "有过滤器" : "无过滤器",
|
||||
);
|
||||
// 通过 loadTasks 重新加载任务
|
||||
this.loadTasks();
|
||||
|
|
@ -1112,7 +1157,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.updateActionButtons();
|
||||
} else {
|
||||
console.warn(
|
||||
"TaskSpecificView: Cannot trigger update, currentViewId is not set."
|
||||
"TaskSpecificView: Cannot trigger update, currentViewId is not set.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1120,7 +1165,7 @@ export class TaskSpecificView extends ItemView {
|
|||
private updateActionButtons() {
|
||||
// 移除过滤器重置按钮(如果存在)
|
||||
const resetButton = this.leaf.view.containerEl.querySelector(
|
||||
".view-action.task-filter-reset"
|
||||
".view-action.task-filter-reset",
|
||||
);
|
||||
if (resetButton) {
|
||||
resetButton.remove();
|
||||
|
|
@ -1173,7 +1218,7 @@ export class TaskSpecificView extends ItemView {
|
|||
|
||||
const result = await this.plugin.writeAPI.updateTask({
|
||||
taskId: updatedTask.id,
|
||||
updates: updatedTask
|
||||
updates: updatedTask,
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to update task");
|
||||
|
|
@ -1194,14 +1239,14 @@ export class TaskSpecificView extends ItemView {
|
|||
originalTask.id,
|
||||
updatedTask.id,
|
||||
updatedTask,
|
||||
originalTask
|
||||
originalTask,
|
||||
);
|
||||
|
||||
try {
|
||||
// Always use WriteAPI
|
||||
const writeResult = await this.plugin.writeAPI.updateTask({
|
||||
taskId: updatedTask.id,
|
||||
updates: updatedTask
|
||||
updates: updatedTask,
|
||||
});
|
||||
if (!writeResult.success) {
|
||||
throw new Error(writeResult.error || "Failed to update task");
|
||||
|
|
@ -1211,7 +1256,9 @@ export class TaskSpecificView extends ItemView {
|
|||
updatedTask = writeResult.task;
|
||||
}
|
||||
|
||||
console.log(`Task ${updatedTask.id} updated successfully via handleTaskUpdate.`);
|
||||
console.log(
|
||||
`Task ${updatedTask.id} updated successfully via handleTaskUpdate.`,
|
||||
);
|
||||
|
||||
// Update local task list immediately
|
||||
const index = this.tasks.findIndex((t) => t.id === originalTask.id);
|
||||
|
|
@ -1221,7 +1268,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.tasks[index] = updatedTask;
|
||||
} else {
|
||||
console.warn(
|
||||
"Updated task not found in local list, might reload."
|
||||
"Updated task not found in local list, might reload.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1248,7 +1295,7 @@ export class TaskSpecificView extends ItemView {
|
|||
|
||||
private async updateTask(
|
||||
originalTask: Task,
|
||||
updatedTask: Task
|
||||
updatedTask: Task,
|
||||
): Promise<Task> {
|
||||
if (!this.plugin.writeAPI) {
|
||||
console.error("WriteAPI not available for updateTask");
|
||||
|
|
@ -1258,7 +1305,7 @@ export class TaskSpecificView extends ItemView {
|
|||
// Always use WriteAPI
|
||||
const writeResult = await this.plugin.writeAPI.updateTask({
|
||||
taskId: updatedTask.id,
|
||||
updates: updatedTask
|
||||
updates: updatedTask,
|
||||
});
|
||||
if (!writeResult.success) {
|
||||
throw new Error(writeResult.error || "Failed to update task");
|
||||
|
|
@ -1276,7 +1323,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.tasks[index] = updatedTask;
|
||||
} else {
|
||||
console.warn(
|
||||
"Updated task not found in local list, might reload."
|
||||
"Updated task not found in local list, might reload.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1309,7 +1356,7 @@ export class TaskSpecificView extends ItemView {
|
|||
const existingLeaf = this.app.workspace
|
||||
.getLeavesOfType("markdown")
|
||||
.find(
|
||||
(leaf) => (leaf.view as any).file === file // Type assertion needed here
|
||||
(leaf) => (leaf.view as any).file === file, // Type assertion needed here
|
||||
);
|
||||
|
||||
const leafToUse = existingLeaf || this.app.workspace.getLeaf("tab"); // Open in new tab if not open
|
||||
|
|
@ -1353,10 +1400,10 @@ export class TaskSpecificView extends ItemView {
|
|||
// Method to handle status updates originating from Kanban drag-and-drop
|
||||
private handleKanbanTaskStatusUpdate = async (
|
||||
taskId: string,
|
||||
newStatusMark: string
|
||||
newStatusMark: string,
|
||||
) => {
|
||||
console.log(
|
||||
`TaskSpecificView handling Kanban status update request for ${taskId} to mark ${newStatusMark}`
|
||||
`TaskSpecificView handling Kanban status update request for ${taskId} to mark ${newStatusMark}`,
|
||||
);
|
||||
const taskToUpdate = this.tasks.find((t) => t.id === taskId);
|
||||
|
||||
|
|
@ -1388,22 +1435,22 @@ export class TaskSpecificView extends ItemView {
|
|||
// Use updateTask to ensure consistency and UI updates
|
||||
await this.updateTask(taskToUpdate, updatedTaskData);
|
||||
console.log(
|
||||
`Task ${taskId} status update processed by TaskSpecificView.`
|
||||
`Task ${taskId} status update processed by TaskSpecificView.`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`TaskSpecificView failed to update task status from Kanban callback for task ${taskId}:`,
|
||||
error
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Task ${taskId} status (${newStatusMark}) already matches, no update needed.`
|
||||
`Task ${taskId} status (${newStatusMark}) already matches, no update needed.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`TaskSpecificView could not find task with ID ${taskId} for Kanban status update.`
|
||||
`TaskSpecificView could not find task with ID ${taskId} for Kanban status update.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -1415,7 +1462,7 @@ export class TaskSpecificView extends ItemView {
|
|||
this.currentFilterState = null;
|
||||
this.app.saveLocalStorage(
|
||||
`task-genius-view-filter-${this.leaf.id}`,
|
||||
null
|
||||
null,
|
||||
);
|
||||
this.applyCurrentFilter();
|
||||
this.updateActionButtons();
|
||||
|
|
|
|||
Loading…
Reference in a new issue