refactor(bases): simplify view registration and upgrade to Obsidian 1.10

- Replace complex ViewManager with streamlined registration approach
- Consolidate multiple specialized view classes into unified TaskBasesView
- Upgrade Obsidian API from 1.8.7 to 1.10.0 for latest features
- Add kanban config override support for Bases view integration
- Update file task property mappings to align with dataview standards
- Remove deprecated Bases API version checking from VersionManager
This commit is contained in:
Quorafind 2025-10-08 21:12:17 +08:00
parent 791f2cd9a9
commit 5583f4678c
21 changed files with 2282 additions and 5682 deletions

View file

@ -62,7 +62,6 @@
"jest-environment-jsdom": "^29.5.0",
"jszip": "^3.10.1",
"monkey-around": "^3.0.0",
"obsidian": "^1.8.7",
"release-it": "^19.0.4",
"semver": "^7.7.2",
"ts-jest": "^29.1.0",
@ -76,6 +75,7 @@
"date-fns": "^4.1.0",
"localforage": "^1.10.0",
"rrule": "^2.8.1",
"obsidian": "^1.10.0",
"obsidian-daily-notes-interface": "^0.9.4",
"sortablejs": "^1.15.6",
"regexp-match-indices": "^1.0.2"

View file

@ -23,6 +23,9 @@ importers:
localforage:
specifier: ^1.10.0
version: 1.10.0
obsidian:
specifier: ^1.10.0
version: 1.10.0(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
obsidian-daily-notes-interface:
specifier: ^0.9.4
version: 0.9.4(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
@ -108,9 +111,6 @@ importers:
monkey-around:
specifier: ^3.0.0
version: 3.0.0
obsidian:
specifier: ^1.8.7
version: 1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
release-it:
specifier: ^19.0.4
version: 19.0.4(@types/node@16.18.126)
@ -2489,18 +2489,18 @@ packages:
resolution: {integrity: sha512-PILoRtZUB5wEeGnDQAPMlkVlXwDYoxkLR8Wl4STU2zLNwhcq9kKvQexiXi7sfjGlpTnL+LeAOfEVWyeVndneKg==}
hasBin: true
obsidian@1.8.7:
resolution: {integrity: sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==}
obsidian@1.10.0:
resolution: {integrity: sha512-F7hhnmGRQD1TanDPFT//LD3iKNUVd7N8sKL7flCCHRszfTxpDJ39j3T7LHbcGpyid906i6lD5oO+cnfLBzJMKw==}
peerDependencies:
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
'@codemirror/state': 6.5.0
'@codemirror/view': 6.38.1
obsidian@https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/403d1e0e70a3d3dca0cf2f032d0e3baff138973a:
resolution: {tarball: https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/403d1e0e70a3d3dca0cf2f032d0e3baff138973a}
version: 1.8.7
obsidian@https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/3a2161f440361a0c5466d5ebc4569998933e665b:
resolution: {tarball: https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/3a2161f440361a0c5466d5ebc4569998933e665b}
version: 1.10.0
peerDependencies:
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
'@codemirror/state': 6.5.0
'@codemirror/view': 6.38.1
ohash@2.0.11:
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
@ -5969,20 +5969,20 @@ snapshots:
obsidian-daily-notes-interface@0.9.4(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
dependencies:
obsidian: https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/403d1e0e70a3d3dca0cf2f032d0e3baff138973a(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
obsidian: https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/3a2161f440361a0c5466d5ebc4569998933e665b(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)
tslib: 2.1.0
transitivePeerDependencies:
- '@codemirror/state'
- '@codemirror/view'
obsidian@1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
obsidian@1.10.0(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.7
'@types/codemirror': 5.60.8
moment: 2.29.4
obsidian@https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/403d1e0e70a3d3dca0cf2f032d0e3baff138973a(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
obsidian@https://codeload.github.com/obsidianmd/obsidian-api/tar.gz/3a2161f440361a0c5466d5ebc4569998933e665b(@codemirror/state@6.5.2)(@codemirror/view@6.36.7):
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.7

View file

@ -75,6 +75,7 @@ export class KanbanComponent extends Component {
label: "Priority (High to Low)",
};
private hideEmptyColumns: boolean = false;
private configOverride: Partial<KanbanSpecificConfig> | null = null; // Configuration override from Bases view
constructor(
app: App,
@ -101,6 +102,34 @@ export class KanbanComponent extends Component {
this.params = params;
}
/**
* Set configuration override from Bases view config
*/
public setConfigOverride(config: Partial<KanbanSpecificConfig> | null): void {
const isChanged = this.hasConfigOverrideChanged(config);
this.configOverride = config;
console.log('[Kanban] setConfigOverride received', config);
if (isChanged) {
// Refresh derived state from effective config (sort/hide/column order)
this.loadKanbanConfig();
const eff = this.getEffectiveKanbanConfig();
console.log('[Kanban] effective config after override', eff);
if (this.columnContainerEl) {
// Rebuild columns with the new configuration so changes like groupBy
// take effect immediately without requiring a data refresh.
this.applyFiltersAndRender();
}
}
}
private getEffectiveKanbanConfig(): KanbanSpecificConfig | undefined {
const pluginConfig = this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
)?.specificConfig as KanbanSpecificConfig;
return this.configOverride ? {...(pluginConfig ?? {}), ...this.configOverride} : pluginConfig;
}
override onload() {
super.onload();
this.containerEl.empty();
@ -143,6 +172,27 @@ export class KanbanComponent extends Component {
console.log("KanbanComponent unloaded.");
}
private hasConfigOverrideChanged(
nextConfig: Partial<KanbanSpecificConfig> | null
): boolean {
if (!this.configOverride && !nextConfig) {
return false;
}
if (!this.configOverride || !nextConfig) {
return true;
}
try {
return (
JSON.stringify(this.configOverride) !== JSON.stringify(nextConfig)
);
} catch (error) {
console.warn("Failed to compare kanban config overrides:", error);
return true;
}
}
private renderControls(containerEl: HTMLElement) {
// Create a controls container for sort and toggle controls
const controlsContainer = containerEl.createDiv({
@ -482,9 +532,9 @@ export class KanbanComponent extends Component {
this.columns.forEach((col) => this.removeChild(col));
this.columns = [];
const kanbanConfig = this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
)?.specificConfig as KanbanSpecificConfig;
// Resolve effective config (Bases override wins over plugin settings)
const kanbanConfig = this.getEffectiveKanbanConfig();
console.log('[Kanban] renderColumns effective config', kanbanConfig);
const groupBy = kanbanConfig?.groupBy || "status";
@ -746,8 +796,10 @@ export class KanbanComponent extends Component {
}
private updateColumnVisibility() {
const effective = this.getEffectiveKanbanConfig();
const hideEmpty = effective?.hideEmptyColumns ?? this.hideEmptyColumns;
this.columns.forEach((column) => {
if (this.hideEmptyColumns && column.isEmpty()) {
if (hideEmpty && column.isEmpty()) {
column.setVisible(false);
} else {
column.setVisible(true);
@ -885,7 +937,8 @@ export class KanbanComponent extends Component {
(v) => v.id === this.currentViewId
)?.specificConfig as KanbanSpecificConfig;
const groupBy = kanbanConfig?.groupBy || "status";
const groupByPlugin = kanbanConfig?.groupBy || "status";
const groupBy = (this.getEffectiveKanbanConfig()?.groupBy) || groupByPlugin;
if (groupBy === "status") {
// Handle status-based grouping (original logic)
@ -903,16 +956,18 @@ export class KanbanComponent extends Component {
);
}
} else {
const effectiveCustomColumns = (this.getEffectiveKanbanConfig()?.customColumns) || kanbanConfig?.customColumns;
// Handle property-based grouping
const targetValue = this.getColumnValueFromTitle(
targetColumnTitle,
groupBy,
kanbanConfig?.customColumns
effectiveCustomColumns
);
const sourceValue = this.getColumnValueFromTitle(
sourceColumnTitle,
groupBy,
kanbanConfig?.customColumns
effectiveCustomColumns
);
console.log(
`Kanban requesting ${groupBy} update for task ${taskId} from ${sourceValue} to value: ${targetValue}`
@ -937,9 +992,7 @@ export class KanbanComponent extends Component {
}
private loadKanbanConfig() {
const kanbanConfig = this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
)?.specificConfig as KanbanSpecificConfig;
const kanbanConfig = this.getEffectiveKanbanConfig();
if (kanbanConfig) {
this.hideEmptyColumns = kanbanConfig.hideEmptyColumns || false;
@ -951,6 +1004,11 @@ export class KanbanComponent extends Component {
kanbanConfig.defaultSortOrder || "desc"
),
};
console.log('[Kanban] loadKanbanConfig applied', {
hideEmptyColumns: this.hideEmptyColumns,
defaultSortField: this.sortOption.field,
defaultSortOrder: this.sortOption.order,
});
}
// Load saved column order
@ -1488,9 +1546,7 @@ export class KanbanComponent extends Component {
// Column order management methods
private getColumnOrderKey(): string {
const kanbanConfig = this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
)?.specificConfig as KanbanSpecificConfig;
const kanbanConfig = this.getEffectiveKanbanConfig();
const groupBy = kanbanConfig?.groupBy || "status";
return `kanban-column-order-${this.currentViewId}-${groupBy}`;
}

View file

@ -43,7 +43,7 @@ export class TaskListItemComponent extends Component {
this.element = createEl("div", {
cls: "task-item",
attr: { "data-task-id": this.task.id },
attr: {"data-task-id": this.task.id},
});
this.settings = this.plugin.settings;
@ -237,7 +237,7 @@ export class TaskListItemComponent extends Component {
if (sanitizedPriority) {
classes.push(`priority-${sanitizedPriority}`);
}
const priorityEl = createDiv({ cls: classes });
const priorityEl = createDiv({cls: classes});
// Priority icon based on level
let icon = "•";
@ -399,10 +399,10 @@ export class TaskListItemComponent extends Component {
dateText = this.settings.useRelativeTimeForDate
? getRelativeTimeString(date)
: date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
year: "numeric",
month: "long",
day: "numeric",
});
}
if (cssClass) {
@ -422,14 +422,14 @@ export class TaskListItemComponent extends Component {
type === "due"
? "dueDate"
: type === "scheduled"
? "scheduledDate"
: type === "start"
? "startDate"
: type === "cancelled"
? "cancelledDate"
: type === "completed"
? "completedDate"
: null;
? "scheduledDate"
: type === "start"
? "startDate"
: type === "cancelled"
? "cancelledDate"
: type === "completed"
? "completedDate"
: null;
if (fieldType) {
this.getInlineEditor().showMetadataEditor(
@ -618,7 +618,7 @@ export class TaskListItemComponent extends Component {
// Create the add metadata button
const addBtn = addButtonContainer.createEl("button", {
cls: "add-metadata-btn",
attr: { "aria-label": "Add metadata" },
attr: {"aria-label": "Add metadata"},
});
setIcon(addBtn, "plus");
@ -636,19 +636,19 @@ export class TaskListItemComponent extends Component {
const menu = new Menu();
const availableFields = [
{ key: "project", label: "Project", icon: "folder" },
{ key: "tags", label: "Tags", icon: "tag" },
{ key: "context", label: "Context", icon: "at-sign" },
{ key: "dueDate", label: "Due Date", icon: "calendar" },
{ key: "startDate", label: "Start Date", icon: "play" },
{ key: "scheduledDate", label: "Scheduled Date", icon: "clock" },
{ key: "cancelledDate", label: "Cancelled Date", icon: "x" },
{ key: "completedDate", label: "Completed Date", icon: "check" },
{ key: "priority", label: "Priority", icon: "alert-triangle" },
{ key: "recurrence", label: "Recurrence", icon: "repeat" },
{ key: "onCompletion", label: "On Completion", icon: "flag" },
{ key: "dependsOn", label: "Depends On", icon: "link" },
{ key: "id", label: "Task ID", icon: "hash" },
{key: "project", label: "Project", icon: "folder"},
{key: "tags", label: "Tags", icon: "tag"},
{key: "context", label: "Context", icon: "at-sign"},
{key: "dueDate", label: "Due Date", icon: "calendar"},
{key: "startDate", label: "Start Date", icon: "play"},
{key: "scheduledDate", label: "Scheduled Date", icon: "clock"},
{key: "cancelledDate", label: "Cancelled Date", icon: "x"},
{key: "completedDate", label: "Completed Date", icon: "check"},
{key: "priority", label: "Priority", icon: "alert-triangle"},
{key: "recurrence", label: "Recurrence", icon: "repeat"},
{key: "onCompletion", label: "On Completion", icon: "flag"},
{key: "dependsOn", label: "Depends On", icon: "link"},
{key: "id", label: "Task ID", icon: "hash"},
];
// Filter out fields that already have values

View file

@ -7,7 +7,6 @@ import {
Notice,
Platform,
Plugin,
requireApiVersion,
} from "obsidian";
import { taskProgressBarExtension } from "./editor-extensions/ui-widgets/progress-bar-widget";
import { taskTimerExtension } from "./editor-extensions/date-time/task-timer";
@ -87,7 +86,6 @@ import { sortTasksInDocument } from "./commands/sortTaskCommands";
import { taskGutterExtension } from "./editor-extensions/task-operations/gutter-marker";
import { autoDateManagerExtension } from "./editor-extensions/date-time/date-manager";
import { taskMarkCleanupExtension } from "./editor-extensions/task-operations/mark-cleanup";
import { ViewManager } from "./pages/ViewManager";
import { IcsManager } from "./managers/ics-manager";
import { FluentIntegration } from "./components/features/fluent/FluentIntegration";
import { ObsidianUriHandler } from "./utils/ObsidianUriHandler";
@ -98,6 +96,7 @@ import { OnboardingConfigManager } from "./managers/onboarding-manager";
import { OnCompletionManager } from "./managers/completion-manager";
import { SettingsChangeDetector } from "./services/settings-change-detector";
import { ONBOARDING_VIEW_TYPE, OnboardingView, } from "./components/features/onboarding/OnboardingView";
import { registerTaskGeniusBasesViews } from "@/pages/bases/registerBasesViews";
import { TaskTimerExporter } from "./services/timer-export-service";
import { TaskTimerManager } from "./managers/timer-manager";
import { McpServerManager } from "./mcp/McpServerManager";
@ -177,21 +176,10 @@ export default class TaskProgressBarPlugin extends Plugin {
// V2 Integration instance
v2Integration?: FluentIntegration;
// Uninstaller for workspace drag-and-drop monkey patch
private uninstallWorkspaceDnD?: () => void;
async onload() {
console.time("[TPB] onload");
await this.loadSettings();
if (
requireApiVersion("1.9.10") &&
this.settings.betaTest?.enableBaseView
) {
const viewManager = new ViewManager(this.app, this);
this.addChild(viewManager);
}
// Initialize version manager first
this.versionManager = new VersionManager(this.app, this);
this.addChild(this.versionManager);
@ -203,10 +191,6 @@ export default class TaskProgressBarPlugin extends Plugin {
// Initialize global suggest manager
this.globalSuggestManager = new SuggestManager(this.app, this);
// Initialize workspace manager
const {WorkspaceManager} = await import(
"@/components/features/fluent/managers/WorkspaceManager"
);
this.workspaceManager = new WorkspaceManager(this);
await this.workspaceManager.migrateToV2();
this.workspaceManager.ensureDefaultWorkspaceInvariant();
@ -269,6 +253,14 @@ export default class TaskProgressBarPlugin extends Plugin {
})
);
// Register Bases views if Bases plugin is available
try {
registerTaskGeniusBasesViews(this);
} catch (error) {
console.log("Failed to register Bases views:", error);
// Not critical if Bases plugin is not installed
}
// Add a command to open the TaskView
this.addCommand({
id: "open-task-genius-view",
@ -1509,6 +1501,8 @@ export default class TaskProgressBarPlugin extends Plugin {
this.globalSuggestManager.cleanup();
}
// Bases views are automatically unregistered by Obsidian when plugin unloads
// Clean up dataflow orchestrator (experimental)
if (this.dataflowOrchestrator) {
this.dataflowOrchestrator.cleanup().catch((error) => {
@ -1689,8 +1683,9 @@ export default class TaskProgressBarPlugin extends Plugin {
try {
const {workspace} = this.app;
const viewType = this.settings.fluentView?.enableFluent ? FLUENT_TASK_VIEW : TASK_VIEW_TYPE;
// Check if view is already open
const existingLeaves = workspace.getLeavesOfType(TASK_VIEW_TYPE);
const existingLeaves = workspace.getLeavesOfType(viewType);
if (existingLeaves.length > 0) {
// If view is already open, just reveal the first one
@ -1705,8 +1700,8 @@ export default class TaskProgressBarPlugin extends Plugin {
// Otherwise, create a new leaf and open the view
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({type: TASK_VIEW_TYPE});
workspace.revealLeaf(leaf);
await leaf.setViewState({type: viewType});
await workspace.revealLeaf(leaf);
} finally {
this.isActivatingView = false;
}

View file

@ -44,12 +44,16 @@ interface BasesEntry {
};
lazyEvalCache: Record<string, any>;
properties: Record<string, any>;
getValue(prop: {
type: "property" | "file" | "formula";
name: string;
}): any;
updateProperty(key: string, value: any): void;
getFormulaValue(formula: string): any;
getPropertyKeys(): string[];
}
@ -58,11 +62,11 @@ export const DEFAULT_FILE_TASK_MAPPING: FileTaskPropertyMapping = {
contentProperty: "title",
statusProperty: "status",
completedProperty: "completed",
createdDateProperty: "created", // dataview standard: created
startDateProperty: "start", // dataview standard: start
scheduledDateProperty: "scheduled", // dataview standard: scheduled
dueDateProperty: "due", // dataview standard: due
completedDateProperty: "completion", // dataview standard: completion
createdDateProperty: "createdDate", // dataview standard: created
startDateProperty: "startDate", // dataview standard: start
scheduledDateProperty: "scheduledDate", // dataview standard: scheduled
dueDateProperty: "dueDate", // dataview standard: due
completedDateProperty: "completionDate", // dataview standard: completion
recurrenceProperty: "repeat", // dataview standard: repeat
tagsProperty: "tags",
projectProperty: "project",
@ -174,7 +178,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
// Combine dates with time components to create enhanced datetime objects
const enhancedDates = this.combineTimestampsWithTimeComponents(
{ startDate, dueDate, scheduledDate, completedDate },
{startDate, dueDate, scheduledDate, completedDate},
enhancedMetadata.timeComponents
);
@ -189,21 +193,21 @@ export class FileTaskManagerImpl implements FileTaskManager {
children: [], // File tasks don't have children by default
// Optional properties
...(createdDate && { createdDate }),
...(startDate && { startDate }),
...(scheduledDate && { scheduledDate }),
...(dueDate && { dueDate }),
...(completedDate && { completedDate }),
...(recurrence && { recurrence }),
...(project && { project }),
...(context && { context }),
...(priority && { priority }),
...(estimatedTime && { estimatedTime }),
...(actualTime && { actualTime }),
...(createdDate && {createdDate}),
...(startDate && {startDate}),
...(scheduledDate && {scheduledDate}),
...(dueDate && {dueDate}),
...(completedDate && {completedDate}),
...(recurrence && {recurrence}),
...(project && {project}),
...(context && {context}),
...(priority && {priority}),
...(estimatedTime && {estimatedTime}),
...(actualTime && {actualTime}),
// Enhanced time components
...enhancedMetadata,
...(enhancedDates && { enhancedDates }),
...(enhancedDates && {enhancedDates}),
},
sourceEntry: entry,
isFileTask: true,
@ -320,17 +324,17 @@ export class FileTaskManagerImpl implements FileTaskManager {
updates: Partial<FileTask>
): Promise<void> {
// Merge updates into the task
const updatedTask = { ...task, ...updates };
const updatedTask = {...task, ...updates};
let contentHandledSeparately = false;
// Handle content changes - re-extract time components if content changed
if (updates.content && updates.content !== task.content) {
await this.handleContentUpdate(task, updates.content);
contentHandledSeparately = true;
// Re-extract time components from updated content
const enhancedMetadata = this.extractTimeComponents(updates.content);
// Update the task's metadata with new time components
if (enhancedMetadata.timeComponents) {
updatedTask.metadata = {
@ -671,7 +675,8 @@ export class FileTaskManagerImpl implements FileTaskManager {
name: propertyName,
});
if (value !== null && value !== undefined) return String(value);
} catch {}
} catch {
}
// 2) Fallback: direct properties/frontmatter/note.data
try {
const anyEntry: any = entry as any;
@ -693,7 +698,8 @@ export class FileTaskManagerImpl implements FileTaskManager {
) {
return String(anyEntry.note.data[propertyName]);
}
} catch {}
} catch {
}
return undefined;
}
@ -849,7 +855,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
try {
// Parse time components from content
const { timeComponents, errors, warnings } = this.timeParsingService.parseTimeComponents(content);
const {timeComponents, errors, warnings} = this.timeParsingService.parseTimeComponents(content);
// Log warnings if any
if (warnings.length > 0) {
@ -927,13 +933,13 @@ export class FileTaskManagerImpl implements FileTaskManager {
enhancedDates.scheduledDateTime = combineDateTime(dates.scheduledDate, timeComponents.scheduledTime);
}
// If we have a due date but the time component is scheduledTime (common with "at" keyword),
// If we have a due date but the time component is scheduledTime (common with "at" keyword),
// create dueDateTime using scheduledTime
if (dates.dueDate && !timeComponents.dueTime && timeComponents.scheduledTime) {
enhancedDates.dueDateTime = combineDateTime(dates.dueDate, timeComponents.scheduledTime);
}
// If we have a scheduled date but the time component is dueTime,
// If we have a scheduled date but the time component is dueTime,
// create scheduledDateTime using dueTime
if (dates.scheduledDate && !timeComponents.scheduledTime && timeComponents.dueTime) {
enhancedDates.scheduledDateTime = combineDateTime(dates.scheduledDate, timeComponents.dueTime);

View file

@ -355,35 +355,4 @@ export class VersionManager extends Component {
canWrite,
};
}
/**
* Check if the current Obsidian version supports a specific API version
*/
public isObsidianVersionSupported(requiredVersion: string): boolean {
try {
return requireApiVersion(requiredVersion)
} catch (error) {
console.error("Error checking Obsidian version support:", error);
return false;
}
}
/**
* Check if the new Bases API (registerBasesView) is supported
*/
public isNewBasesApiSupported(): boolean {
try {
// Check if Obsidian version is 1.9.10 or higher
const hasVersionSupport = this.isObsidianVersionSupported("1.9.10");
// Check if the plugin has the registerBasesView method
const hasMethodSupport =
typeof (this.plugin as any).registerBasesView === "function";
return hasVersionSupport && hasMethodSupport;
} catch (error) {
console.error("Error checking new Bases API support:", error);
return false;
}
}
}

View file

@ -1,573 +0,0 @@
/**
* View Manager
*
*/
import { App, Component } from "obsidian";
import { FileTaskView } from "./bases/FileTaskView";
import { InboxBasesView } from "./bases/InboxBasesView";
import { FlaggedBasesView } from "./bases/FlaggedBasesView";
import { ProjectBasesView } from "./bases/ProjectBasesView";
import { TagsBasesView } from "./bases/TagsBasesView";
import TaskProgressBarPlugin from "../index";
import "../styles/base-view.css";
import { requireApiVersion } from "obsidian";
import { BasesPlugin, BasesViewRegistration, BaseView } from "@/types/bases";
export class ViewManager extends Component {
private app: App;
private basesPlugin: BasesPlugin | null = null;
private registeredViews: Set<string> = new Set();
private plugin: TaskProgressBarPlugin;
constructor(app: App, plugin: TaskProgressBarPlugin) {
super();
this.app = app;
this.plugin = plugin;
}
/**
* Bases
*/
private getBasesPlugin(): BasesPlugin | null {
try {
// 使用你提供的方法获取插件
const internalPlugins = (this.app as any).internalPlugins?.plugins;
if (internalPlugins && internalPlugins["bases"]) {
this.basesPlugin = internalPlugins["bases"].instance;
console.log(
"[ViewManager] Bases plugin found via internalPlugins"
);
return this.basesPlugin;
}
console.warn("[ViewManager] Bases plugin not found");
return null;
} catch (error) {
console.error("[ViewManager] Error getting Bases plugin:", error);
return null;
}
}
/**
* Check if the new Bases API (registerBasesView) is supported
*/
private isNewBasesApiSupported(): boolean {
try {
// Check if plugin has the new method
const hasPluginMethod =
typeof (this.plugin as any).registerBasesView === "function";
// Check version via VersionManager if available
const versionManager = this.plugin.versionManager;
const hasVersionSupport = versionManager
? versionManager.isNewBasesApiSupported()
: false;
console.log(
`[ViewManager] New Bases API support - Plugin method: ${hasPluginMethod}, Version support: ${hasVersionSupport}`
);
return hasPluginMethod || hasVersionSupport;
} catch (error) {
console.error(
"[ViewManager] Error checking new Bases API support:",
error
);
return false;
}
}
/**
*
*/
async initialize(): Promise<boolean> {
console.log("[ViewManager] Initializing...");
const basesPlugin = this.getBasesPlugin();
console.log(basesPlugin);
if (!basesPlugin) {
console.error(
"[ViewManager] Cannot initialize without Bases plugin"
);
return false;
}
try {
// 注册所有自定义视图
await this.registerAllViews();
console.log("[ViewManager] Initialization completed successfully");
return true;
} catch (error) {
console.error("[ViewManager] Initialization failed:", error);
return false;
}
}
/**
*
*/
private async registerAllViews(): Promise<void> {
// 注册文件任务视图
await this.registerFileTaskView();
// 注册专门的视图
await this.registerInboxView();
await this.registerFlaggedView();
await this.registerProjectsView();
await this.registerTagsView();
// 在这里可以注册更多视图
// await this.registerTimelineView();
// await this.registerKanbanView();
}
/**
* Resolve container element from different parameter formats in Bases API
*/
private resolveContainerEl(arg1: any, arg2?: any): HTMLElement {
const candidates = [arg1, arg2];
for (const c of candidates) {
if (!c) continue;
if (c instanceof HTMLElement) return c;
if (typeof c === "object") {
if ((c as any).containerEl instanceof HTMLElement)
return (c as any).containerEl;
if ((c as any).viewContainerEl instanceof HTMLElement)
return (c as any).viewContainerEl;
if (typeof (c as any).createDiv === "function")
return c as any as HTMLElement;
}
}
console.warn(
"[ViewManager] resolveContainerEl: Could not resolve container element from",
arg1,
arg2
);
// Fallback to detached container to avoid hard failure; the view can still render
return document.createElement("div");
}
/**
*
*/
private async registerFileTaskView(): Promise<void> {
const viewId = "task-genius-view";
if (this.registeredViews.has(viewId)) {
console.log(`[ViewManager] View ${viewId} already registered`);
return;
}
try {
const self = this;
const factory = function (container: any): any {
const containerEl = self.resolveContainerEl(
container,
(arguments as any)[1]
);
console.log(
`[ViewManager] Creating ${viewId} instance`,
container,
(arguments as any)[1]
);
return new FileTaskView(containerEl, self.app, self.plugin);
};
await this.registerView(
viewId,
factory as any,
"Task Genius View",
"task-genius"
);
} catch (error) {
console.error(
`[ViewManager] Failed to register view ${viewId}:`,
error
);
throw error;
}
}
/**
*
*/
private async registerInboxView(): Promise<void> {
const viewId = "inbox-bases-view";
if (this.registeredViews.has(viewId)) {
console.log(`[ViewManager] View ${viewId} already registered`);
return;
}
try {
const self = this;
const factory = function (container: any): any {
const containerEl = self.resolveContainerEl(
container,
(arguments as any)[1]
);
console.log(
`[ViewManager] Creating ${viewId} instance`,
container,
(arguments as any)[1]
);
return new InboxBasesView(containerEl, self.app, self.plugin);
};
await this.registerView(
viewId,
factory as any,
"Inbox Tasks",
"inbox"
);
} catch (error) {
console.error(
`[ViewManager] Failed to register view ${viewId}:`,
error
);
throw error;
}
}
/**
*
*/
private async registerFlaggedView(): Promise<void> {
const viewId = "flagged-bases-view";
if (this.registeredViews.has(viewId)) {
console.log(`[ViewManager] View ${viewId} already registered`);
return;
}
try {
const self = this;
const factory = function (container: any): any {
const containerEl = self.resolveContainerEl(
container,
(arguments as any)[1]
);
console.log(
`[ViewManager] Creating ${viewId} instance`,
container,
(arguments as any)[1]
);
return new FlaggedBasesView(containerEl, self.app, self.plugin);
};
await this.registerView(
viewId,
factory as any,
"Flagged Tasks",
"flag"
);
} catch (error) {
console.error(
`[ViewManager] Failed to register view ${viewId}:`,
error
);
throw error;
}
}
/**
*
*/
private async registerProjectsView(): Promise<void> {
const viewId = "projects-bases-view";
if (this.registeredViews.has(viewId)) {
console.log(`[ViewManager] View ${viewId} already registered`);
return;
}
try {
const self = this;
const factory = function (container: any): any {
const containerEl = self.resolveContainerEl(
container,
(arguments as any)[1]
);
console.log(
`[ViewManager] Creating ${viewId} instance`,
container,
(arguments as any)[1]
);
return new ProjectBasesView(containerEl, self.app, self.plugin);
};
await this.registerView(
viewId,
factory as any,
"Project Tasks",
"folders"
);
} catch (error) {
console.error(
`[ViewManager] Failed to register view ${viewId}:`,
error
);
throw error;
}
}
/**
*
*/
private async registerTagsView(): Promise<void> {
const viewId = "tags-bases-view";
if (this.registeredViews.has(viewId)) {
console.log(`[ViewManager] View ${viewId} already registered`);
return;
}
try {
const self = this;
const factory = function (container: any): any {
const containerEl = self.resolveContainerEl(
container,
(arguments as any)[1]
);
console.log(
`[ViewManager] Creating ${viewId} instance`,
container,
(arguments as any)[1]
);
return new TagsBasesView(containerEl, self.app, self.plugin);
};
await this.registerView(
viewId,
factory as any,
"Tagged Tasks",
"tag"
);
} catch (error) {
console.error(
`[ViewManager] Failed to register view ${viewId}:`,
error
);
throw error;
}
}
/**
*
*/
private async registerView(
viewId: string,
factory: (container: HTMLElement) => any,
name: string,
icon: string
): Promise<void> {
// Check if new API is supported
if (this.isNewBasesApiSupported()) {
console.log(
`[ViewManager] Using legacy registerView API for ${viewId}`
);
// Use legacy bases plugin registration method
if (!this.basesPlugin) {
throw new Error(
"Bases plugin not available for legacy registration"
);
}
// Create view registration configuration
const viewConfig: BasesViewRegistration = {
name: name,
icon: icon,
factory: factory,
};
// Try to register with config first, fallback to factory only
// Register view is handled by plugin itself(Will help remove the need to register view in bases plugin)
try {
this.plugin.registerBasesView(viewId, viewConfig);
} catch (configError) {
console.warn(
`[ViewManager] Config registration failed, trying factory-only registration:`,
configError
);
this.basesPlugin.registerView(viewId, factory);
}
this.registeredViews.add(viewId);
console.log(
`[ViewManager] Successfully registered view using legacy API: ${viewId}`
);
} else if (requireApiVersion("1.9.0")) {
console.log(
`[ViewManager] Using new registerBasesView API for ${viewId}`
);
// Use new plugin-level registration method
// Method is used between 1.9.0 and 1.9.3
const success = (this.plugin as any).registerBasesView(
viewId,
factory
);
if (success) {
this.registeredViews.add(viewId);
console.log(
`[ViewManager] Successfully registered view using new API: ${viewId}`
);
} else {
throw new Error("New API registration returned false");
}
}
}
/**
*
*/
unregisterView(viewId: string): void {
try {
if (!this.registeredViews.has(viewId)) {
console.log(
`[ViewManager] View ${viewId} not registered, skipping unregistration`
);
return;
}
// For new API, the cleanup is handled automatically by the plugin
if (this.isNewBasesApiSupported()) {
console.log(
`[ViewManager] View ${viewId} registered with new API, cleanup handled automatically`
);
} else {
// For legacy API, manually unregister from bases plugin
if (this.basesPlugin) {
this.basesPlugin.deregisterView(viewId);
console.log(
`[ViewManager] Manually unregistered view from bases plugin: ${viewId}`
);
}
}
this.registeredViews.delete(viewId);
console.log(`[ViewManager] Unregistered view: ${viewId}`);
} catch (error) {
console.error(
`[ViewManager] Failed to unregister view ${viewId}:`,
error
);
}
}
/**
*
*/
unregisterAllViews(): void {
console.log("[ViewManager] Unregistering all views...");
// Create a copy of the set to avoid modification during iteration
const viewsToUnregister = Array.from(this.registeredViews);
for (const viewId of viewsToUnregister) {
this.unregisterView(viewId);
}
console.log("[ViewManager] All views unregistered");
}
/**
*
*/
getRegisteredViews(): string[] {
return Array.from(this.registeredViews);
}
/**
*
*/
isViewRegistered(viewId: string): boolean {
return this.registeredViews.has(viewId);
}
/**
* Bases
*/
getAvailableViewTypes(): string[] {
if (!this.basesPlugin) {
return [];
}
try {
return this.basesPlugin.getViewTypes();
} catch (error) {
console.error("[ViewManager] Error getting view types:", error);
return [];
}
}
/**
*
*/
createViewInstance(
viewId: string,
container: HTMLElement
): BaseView | null {
if (!this.basesPlugin) {
console.error("[ViewManager] Bases plugin not available");
return null;
}
try {
const factory = this.basesPlugin.getViewFactory(viewId);
if (factory) {
return factory(container);
} else {
console.error(
`[ViewManager] No factory found for view: ${viewId}`
);
return null;
}
} catch (error) {
console.error(
`[ViewManager] Error creating view instance ${viewId}:`,
error
);
return null;
}
}
/**
*
*/
getStatus(): {
basesPluginAvailable: boolean;
registeredViewsCount: number;
registeredViews: string[];
availableViewTypes: string[];
usingNewApi: boolean;
apiVersion: string;
} {
const usingNewApi = this.isNewBasesApiSupported();
return {
basesPluginAvailable: !!this.basesPlugin,
registeredViewsCount: this.registeredViews.size,
registeredViews: this.getRegisteredViews(),
availableViewTypes: this.getAvailableViewTypes(),
usingNewApi: usingNewApi,
apiVersion: usingNewApi ? "1.9.3+" : "legacy",
};
}
onload(): void {
this.initialize();
}
/**
* Component unload handler
*/
onunload(): void {
console.log("[ViewManager] Unloading...");
this.unregisterAllViews();
super.onunload();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,353 +0,0 @@
/**
* Flagged Bases View
* Specialized view for flagged and high priority tasks
*/
import { App, Menu } from "obsidian";
import { BaseTaskBasesView } from "./BaseTaskBasesView";
import { ContentComponent } from "@/components/features/task/view/content";
import TaskProgressBarPlugin from "@/index";
import { filterTasks } from "@/utils/task/task-filter-utils";
import { t } from "@/translations/helper";
import { Task } from "@/types/task";
export class FlaggedBasesView extends BaseTaskBasesView {
type = "flagged-bases-view";
private contentComponent: ContentComponent;
private isLoaded = false;
constructor(
containerEl: HTMLElement,
app: App,
plugin: TaskProgressBarPlugin
) {
super(containerEl, app, plugin, "flagged");
this.initializeComponents();
}
private initializeComponents(): void {
// Create content component for flagged tasks
this.contentComponent = new ContentComponent(
this.containerEl,
this.app,
this.plugin,
{
onTaskSelected: (task) => {
console.log("[FlaggedBasesView] Task selected:", task);
this.handleTaskSelection(task);
},
onTaskCompleted: (task) => {
console.log("[FlaggedBasesView] Task completed:", task);
this.handleTaskCompletionLocal(task);
},
onTaskContextMenu: (event, task) => {
console.log("[FlaggedBasesView] Task context menu:", task);
this.handleTaskContextMenu(event, task);
},
}
);
this.addChild(this.contentComponent);
}
// Abstract method implementations
protected onConfigUpdated(): void {
console.log(
"[FlaggedBasesView] onConfigUpdated called, isLoaded:",
this.isLoaded
);
if (this.isLoaded) {
// Convert data again in case configuration affects data processing
this.convertEntriesToTasks();
this.updateFlaggedTasks();
}
}
protected onDataUpdated(): void {
// Handle data updates - convert data and update tasks
console.log(
"[FlaggedBasesView] onDataUpdated called, isLoaded:",
this.isLoaded
);
// Force convert entries to tasks to get latest data
this.convertEntriesToTasks();
// Then update the view
this.updateFlaggedTasks();
}
protected onDisplay(): void {
this.containerEl.removeClass("is-loading");
if (this.tasks.length === 0) {
this.showEmptyState();
} else {
this.hideEmptyState();
}
}
protected onViewLoad(): void {
this.contentComponent.load();
this.isLoaded = true;
this.updateFlaggedTasks();
}
protected onViewUnload(): void {
this.isLoaded = false;
}
protected onViewResize(): void {
// Content component handles its own resize
}
protected getCustomActions(): Array<{
name: string;
callback: () => void;
icon: string;
}> {
return [
{
name: t("Set Priority"),
icon: "flag",
callback: () => {
this.openPrioritySelector();
},
},
{
name: t("Clear Flags"),
icon: "flag-off",
callback: () => {
this.clearAllFlags();
},
},
{
name: t("Filter by Priority"),
icon: "filter",
callback: () => {
this.openPriorityFilter();
},
},
];
}
protected getEditMenuItems(): Array<{
displayName: string;
component: (container: HTMLElement) => any;
}> {
return [
{
displayName: "Priority Settings",
component: (container: HTMLElement) => {
return this.createPrioritySettingsComponent(container);
},
},
];
}
private updateFlaggedTasks(): void {
console.log(
"[FlaggedBasesView] updateFlaggedTasks called, isLoaded:",
this.isLoaded
);
if (!this.isLoaded) {
console.log(
"[FlaggedBasesView] View not loaded yet, skipping update"
);
return;
}
console.log(
"[FlaggedBasesView] Processing",
this.tasks.length,
"total tasks"
);
try {
// Filter tasks for flagged view (high priority and flagged tasks)
const flaggedTasks = filterTasks(
this.tasks,
"flagged",
this.plugin
);
console.log(
`[FlaggedBasesView] Filtered ${flaggedTasks.length} flagged tasks from ${this.tasks.length} total tasks`
);
// Sort by priority (highest first)
flaggedTasks.sort((a, b) => {
const priorityA = a.metadata.priority || 0;
const priorityB = b.metadata.priority || 0;
return priorityB - priorityA;
});
// Update content component with filtered tasks
this.contentComponent.setTasks(flaggedTasks, this.tasks);
this.contentComponent.setViewMode("flagged");
console.log(
`[FlaggedBasesView] Successfully updated ContentComponent with ${flaggedTasks.length} flagged tasks`
);
} catch (error) {
console.error(
"[FlaggedBasesView] Error updating flagged tasks:",
error
);
this.showErrorState("Failed to update flagged tasks");
}
}
private async handleTaskCompletionLocal(task: Task): Promise<void> {
// Use base class method for task completion
try {
await super.handleTaskCompletion(task);
// Trigger refresh after completion
setTimeout(() => {
this.refreshTasks();
}, 100);
} catch (error) {
console.error(
"[FlaggedBasesView] Error handling task completion:",
error
);
}
}
/**
* Handle task context menu
*/
private handleTaskContextMenu(event: MouseEvent, task: Task): void {
const menu = new Menu();
menu.addItem((item: any) => {
item.setTitle(t("Complete"));
item.setIcon("check-square");
item.onClick(() => {
this.handleTaskCompletionLocal(task);
});
})
.addSeparator()
.addItem((item: any) => {
item.setTitle(t("Edit"));
item.setIcon("pencil");
item.onClick(() => {
this.handleTaskSelection(task); // Open details view for editing
});
})
.addItem((item: any) => {
item.setTitle(t("Edit in File"));
item.setIcon("file-edit");
item.onClick(() => {
this.handleTaskEdit(task);
});
});
menu.showAtMouseEvent(event);
}
private openPrioritySelector(): void {
console.log("[FlaggedBasesView] Opening priority selector");
// This would open a priority selection modal
}
private clearAllFlags(): void {
console.log("[FlaggedBasesView] Clearing all flags");
// This would clear flags from all visible tasks
}
private openPriorityFilter(): void {
console.log("[FlaggedBasesView] Opening priority filter");
// This would open a priority filter modal
}
private createPrioritySettingsComponent(container: HTMLElement): any {
const settingsEl = container.createDiv({
cls: "flagged-view-settings",
});
settingsEl.createEl("h3", {
text: "Priority Settings",
});
const optionsEl = settingsEl.createDiv({
cls: "settings-options",
});
// Minimum priority threshold
const thresholdEl = optionsEl.createDiv({
cls: "setting-item",
});
thresholdEl.createEl("label", {
text: "Minimum priority for flagged view:",
});
const thresholdInput = thresholdEl.createEl("input", {
type: "number",
value: "3",
});
thresholdInput.min = "0";
thresholdInput.max = "10";
// Show completed flagged tasks
const completedEl = optionsEl.createDiv({
cls: "setting-item",
});
completedEl.createEl("label", {
text: "Show completed flagged tasks",
});
const completedToggle = completedEl.createEl("input", {
type: "checkbox",
});
return settingsEl;
}
private showEmptyState(): void {
this.hideEmptyState();
const emptyEl = this.createEmptyContainer("No flagged tasks found");
emptyEl.addClass("flagged-empty-state");
const helpEl = emptyEl.createDiv({
cls: "flagged-empty-help",
});
helpEl.createEl("p", {
text: "Tasks with high priority (3+) or flagged tags will appear here.",
});
const helpText = helpEl.createEl("div", {
cls: "flagged-help-text",
});
helpText.createEl("p", {
text: "To flag a task:",
});
const helpList = helpText.createEl("ul");
helpList.createEl("li", {
text: "Set priority to 3 or higher",
});
helpList.createEl("li", {
text: "Add #flagged tag to the task",
});
}
private hideEmptyState(): void {
const emptyEl = this.containerEl.querySelector(".flagged-empty-state");
if (emptyEl) {
emptyEl.remove();
}
}
private showErrorState(message: string): void {
this.containerEl.empty();
this.createErrorContainer(message);
}
}

View file

@ -1,334 +0,0 @@
/**
* Inbox Bases View
* Specialized view for inbox tasks (tasks without projects)
*/
import { App, Menu } from "obsidian";
import { BaseTaskBasesView } from "./BaseTaskBasesView";
import { ContentComponent } from "@/components/features/task/view/content";
import TaskProgressBarPlugin from "@/index";
import { filterTasks } from "@/utils/task/task-filter-utils";
import { t } from "@/translations/helper";
import { Task } from "@/types/task";
export class InboxBasesView extends BaseTaskBasesView {
type = "inbox-bases-view";
private contentComponent: ContentComponent;
private isLoaded = false;
constructor(
containerEl: HTMLElement,
app: App,
plugin: TaskProgressBarPlugin
) {
super(containerEl, app, plugin, "inbox");
this.initializeComponents();
}
private initializeComponents(): void {
// Create content component for inbox tasks
this.contentComponent = new ContentComponent(
this.containerEl,
this.app,
this.plugin,
{
onTaskSelected: (task) => {
// Handle task selection using base class method
console.log("[InboxBasesView] Task selected:", task);
this.handleTaskSelection(task);
},
onTaskCompleted: (task) => {
// Handle task completion using base class method
console.log("[InboxBasesView] Task completed:", task);
this.handleTaskCompletionLocal(task);
},
onTaskContextMenu: (event, task) => {
// Handle context menu
console.log("[InboxBasesView] Task context menu:", task);
this.handleTaskContextMenu(event, task);
},
}
);
this.addChild(this.contentComponent);
}
// Abstract method implementations
protected onConfigUpdated(): void {
// Handle configuration updates
console.log(
"[InboxBasesView] onConfigUpdated called, isLoaded:",
this.isLoaded
);
if (this.isLoaded) {
// Convert data again in case configuration affects data processing
this.convertEntriesToTasks();
this.updateInboxTasks();
}
}
protected onDataUpdated(): void {
// Handle data updates - convert data and update tasks
console.log(
"[InboxBasesView] onDataUpdated called, isLoaded:",
this.isLoaded
);
// Force convert entries to tasks to get latest data
this.convertEntriesToTasks();
// Then update the view
this.updateInboxTasks();
}
protected onDisplay(): void {
// Display the view
this.containerEl.removeClass("is-loading");
if (this.tasks.length === 0) {
this.showEmptyState();
} else {
this.hideEmptyState();
}
}
protected onViewLoad(): void {
// Load the view
this.contentComponent.load();
this.isLoaded = true;
this.updateInboxTasks();
}
protected onViewUnload(): void {
// Unload the view
this.isLoaded = false;
// Component cleanup is handled by parent
}
protected onViewResize(): void {
// Handle view resize
// Content component handles its own resize
}
protected getCustomActions(): Array<{
name: string;
callback: () => void;
icon: string;
}> {
return [
{
name: t("Quick Capture"),
icon: "plus",
callback: () => {
// Open quick capture modal
this.openQuickCapture();
},
},
{
name: t("Filter"),
icon: "filter",
callback: () => {
// Open filter options
this.openFilterOptions();
},
},
];
}
protected getEditMenuItems(): Array<{
displayName: string;
component: (container: HTMLElement) => any;
}> {
return [
{
displayName: "View Settings",
component: (container: HTMLElement) => {
return this.createViewSettingsComponent(container);
},
},
];
}
private updateInboxTasks(): void {
console.log(
"[InboxBasesView] updateInboxTasks called, isLoaded:",
this.isLoaded
);
if (!this.isLoaded) {
console.log(
"[InboxBasesView] View not loaded yet, skipping update"
);
return;
}
console.log("[InboxBasesView] Raw data:", this.data);
console.log("[InboxBasesView] Converted tasks:", this.tasks);
try {
// Filter tasks for inbox view (tasks without projects)
const inboxTasks = filterTasks(this.tasks, "inbox", this.plugin);
console.log(
`[InboxBasesView] Filtered ${inboxTasks.length} inbox tasks from ${this.tasks.length} total tasks`
);
// Update content component with filtered tasks
this.contentComponent.setTasks(inboxTasks, this.tasks);
this.contentComponent.setViewMode("inbox");
console.log(
`[InboxBasesView] Successfully updated ContentComponent with ${inboxTasks.length} inbox tasks`
);
} catch (error) {
console.error(
"[InboxBasesView] Error updating inbox tasks:",
error
);
this.showErrorState("Failed to update inbox tasks");
}
}
private async handleTaskCompletionLocal(task: Task): Promise<void> {
// Use base class method for task completion
try {
await super.handleTaskCompletion(task);
// Trigger refresh after completion
setTimeout(() => {
this.refreshTasks();
}, 100);
} catch (error) {
console.error(
"[InboxBasesView] Error handling task completion:",
error
);
}
}
/**
* Handle task context menu
*/
private handleTaskContextMenu(event: MouseEvent, task: Task): void {
const menu = new Menu();
menu.addItem((item: any) => {
item.setTitle(t("Complete"));
item.setIcon("check-square");
item.onClick(() => {
this.handleTaskCompletionLocal(task);
});
})
.addSeparator()
.addItem((item: any) => {
item.setTitle(t("Edit"));
item.setIcon("pencil");
item.onClick(() => {
this.handleTaskSelection(task); // Open details view for editing
});
})
.addItem((item: any) => {
item.setTitle(t("Edit in File"));
item.setIcon("file-edit");
item.onClick(() => {
this.handleTaskEdit(task);
});
});
menu.showAtMouseEvent(event);
}
private openQuickCapture(): void {
// Open quick capture modal
try {
const {
QuickCaptureModal,
} = require("../components/QuickCaptureModal");
const modal = new QuickCaptureModal(
this.app,
this.plugin,
{},
true
);
modal.open();
} catch (error) {
console.error(
"[InboxBasesView] Error opening quick capture:",
error
);
}
}
private openFilterOptions(): void {
// Open filter options
console.log("[InboxBasesView] Opening filter options");
// This could open a filter modal or popover
}
private createViewSettingsComponent(container: HTMLElement): any {
// Create view settings component
const settingsEl = container.createDiv({
cls: "inbox-view-settings",
});
settingsEl.createEl("h3", {
text: "Inbox View Settings",
});
// Add settings options here
const optionsEl = settingsEl.createDiv({
cls: "settings-options",
});
optionsEl.createEl("label", {
text: "Show completed tasks",
});
const toggleEl = optionsEl.createEl("input", {
type: "checkbox",
});
// Add more settings as needed
return settingsEl;
}
private showEmptyState(): void {
// Remove any existing empty state
this.hideEmptyState();
// Create empty state
const emptyEl = this.createEmptyContainer("No inbox tasks found");
emptyEl.addClass("inbox-empty-state");
// Add helpful message
const helpEl = emptyEl.createDiv({
cls: "inbox-empty-help",
});
helpEl.createEl("p", {
text: "Tasks without projects will appear here.",
});
const captureBtn = helpEl.createEl("button", {
cls: "inbox-capture-btn",
text: "Create Task",
});
captureBtn.addEventListener("click", () => {
this.openQuickCapture();
});
}
private hideEmptyState(): void {
const emptyEl = this.containerEl.querySelector(".inbox-empty-state");
if (emptyEl) {
emptyEl.remove();
}
}
private showErrorState(message: string): void {
this.containerEl.empty();
this.createErrorContainer(message);
}
}

View file

@ -1,373 +0,0 @@
/**
* Projects Bases View
* Specialized view for project-based task management
*/
import { App, Menu } from "obsidian";
import { BaseTaskBasesView } from "./BaseTaskBasesView";
import { ProjectsComponent } from "@/components/features/task/view/projects";
import TaskProgressBarPlugin from "@/index";
import { filterTasks } from "@/utils/task/task-filter-utils";
import { t } from "@/translations/helper";
import { Task } from "@/types/task";
export class ProjectBasesView extends BaseTaskBasesView {
type = "projects-bases-view";
private projectsComponent: ProjectsComponent;
private isLoaded = false;
constructor(
containerEl: HTMLElement,
app: App,
plugin: TaskProgressBarPlugin
) {
super(containerEl, app, plugin, "projects");
this.initializeComponents();
}
private initializeComponents(): void {
// Create projects component for project tasks
this.projectsComponent = new ProjectsComponent(
this.containerEl,
this.app,
this.plugin,
{
onTaskSelected: (task) => {
console.log("[ProjectBasesView] Task selected:", task);
this.handleTaskSelection(task);
},
onTaskCompleted: (task) => {
console.log("[ProjectBasesView] Task completed:", task);
this.handleTaskCompletionLocal(task);
},
onTaskContextMenu: (event, task) => {
console.log("[ProjectBasesView] Task context menu:", task);
this.handleTaskContextMenu(event, task);
},
}
);
this.addChild(this.projectsComponent);
}
// Abstract method implementations
protected onConfigUpdated(): void {
console.log(
"[ProjectBasesView] onConfigUpdated called, isLoaded:",
this.isLoaded
);
if (this.isLoaded) {
// Convert data again in case configuration affects data processing
this.convertEntriesToTasks();
this.updateProjectTasks();
}
}
protected onDataUpdated(): void {
// Handle data updates - convert data and update tasks
console.log(
"[ProjectBasesView] onDataUpdated called, isLoaded:",
this.isLoaded
);
console.log("[ProjectBasesView] this.data type:", typeof this.data);
console.log("[ProjectBasesView] this.data:", this.data);
// Force convert entries to tasks to get latest data
try {
this.convertEntriesToTasks();
} catch (error) {
console.error("[ProjectBasesView] Error converting entries to tasks:", error);
this.tasks = [];
}
// Then update the view
this.updateProjectTasks();
}
protected onDisplay(): void {
this.containerEl.removeClass("is-loading");
if (this.tasks.length === 0) {
this.showEmptyState();
} else {
this.hideEmptyState();
}
}
protected onViewLoad(): void {
this.projectsComponent.load();
this.isLoaded = true;
this.updateProjectTasks();
}
protected onViewUnload(): void {
this.isLoaded = false;
}
protected onViewResize(): void {
// Projects component handles its own resize
}
protected getCustomActions(): Array<{
name: string;
callback: () => void;
icon: string;
}> {
return [
{
name: t("New Project"),
icon: "folder-plus",
callback: () => {
this.createNewProject();
},
},
{
name: t("Archive Completed"),
icon: "archive",
callback: () => {
this.archiveCompletedProjects();
},
},
{
name: t("Project Statistics"),
icon: "bar-chart",
callback: () => {
this.showProjectStatistics();
},
},
];
}
protected getEditMenuItems(): Array<{
displayName: string;
component: (container: HTMLElement) => any;
}> {
return [
{
displayName: "Project Settings",
component: (container: HTMLElement) => {
return this.createProjectSettingsComponent(container);
},
},
];
}
private updateProjectTasks(): void {
console.log(
"[ProjectBasesView] updateProjectTasks called, isLoaded:",
this.isLoaded
);
if (!this.isLoaded) {
console.log(
"[ProjectBasesView] View not loaded yet, skipping update"
);
return;
}
console.log(
"[ProjectBasesView] Processing",
this.tasks.length,
"total tasks"
);
try {
// Filter tasks for projects view (tasks with projects)
const projectTasks = filterTasks(
this.tasks,
"projects",
this.plugin
);
console.log(
`[ProjectBasesView] Filtered ${projectTasks.length} project tasks from ${this.tasks.length} total tasks`
);
// Update projects component with filtered tasks
this.projectsComponent.setTasks(projectTasks);
console.log(
`[ProjectBasesView] Successfully updated ProjectsComponent with ${projectTasks.length} project tasks`
);
} catch (error) {
console.error(
"[ProjectBasesView] Error updating project tasks:",
error
);
this.showErrorState("Failed to update project tasks");
}
}
private async handleTaskCompletionLocal(task: Task): Promise<void> {
// Use base class method for task completion
try {
await super.handleTaskCompletion(task);
// Trigger refresh after completion
setTimeout(() => {
this.refreshTasks();
}, 100);
} catch (error) {
console.error(
"[ProjectBasesView] Error handling task completion:",
error
);
}
}
/**
* Handle task context menu
*/
private handleTaskContextMenu(event: MouseEvent, task: Task): void {
const menu = new Menu();
menu.addItem((item: any) => {
item.setTitle(t("Complete"));
item.setIcon("check-square");
item.onClick(() => {
this.handleTaskCompletionLocal(task);
});
})
.addSeparator()
.addItem((item: any) => {
item.setTitle(t("Edit"));
item.setIcon("pencil");
item.onClick(() => {
this.handleTaskSelection(task); // Open details view for editing
});
})
.addItem((item: any) => {
item.setTitle(t("Edit in File"));
item.setIcon("file-edit");
item.onClick(() => {
this.handleTaskEdit(task);
});
});
menu.showAtMouseEvent(event);
}
private createNewProject(): void {
console.log("[ProjectBasesView] Creating new project");
// This would open a new project creation modal
}
private archiveCompletedProjects(): void {
console.log("[ProjectBasesView] Archiving completed projects");
// This would archive all completed projects
}
private showProjectStatistics(): void {
console.log("[ProjectBasesView] Showing project statistics");
// This would show project completion statistics
}
private createProjectSettingsComponent(container: HTMLElement): any {
const settingsEl = container.createDiv({
cls: "projects-view-settings",
});
settingsEl.createEl("h3", {
text: "Project Settings",
});
const optionsEl = settingsEl.createDiv({
cls: "settings-options",
});
// Show project hierarchy
const hierarchyEl = optionsEl.createDiv({
cls: "setting-item",
});
hierarchyEl.createEl("label", {
text: "Show project hierarchy",
});
const hierarchyToggle = hierarchyEl.createEl("input", {
type: "checkbox",
});
hierarchyToggle.checked = true;
// Group by project
const groupingEl = optionsEl.createDiv({
cls: "setting-item",
});
groupingEl.createEl("label", {
text: "Group tasks by project",
});
const groupingToggle = groupingEl.createEl("input", {
type: "checkbox",
});
groupingToggle.checked = true;
// Show completed projects
const completedEl = optionsEl.createDiv({
cls: "setting-item",
});
completedEl.createEl("label", {
text: "Show completed projects",
});
const completedToggle = completedEl.createEl("input", {
type: "checkbox",
});
return settingsEl;
}
private showEmptyState(): void {
this.hideEmptyState();
const emptyEl = this.createEmptyContainer("No project tasks found");
emptyEl.addClass("projects-empty-state");
const helpEl = emptyEl.createDiv({
cls: "projects-empty-help",
});
helpEl.createEl("p", {
text: "Tasks with project assignments will appear here.",
});
const helpText = helpEl.createEl("div", {
cls: "projects-help-text",
});
helpText.createEl("p", {
text: "To assign a task to a project:",
});
const helpList = helpText.createEl("ul");
helpList.createEl("li", {
text: "Add #project/projectname tag to the task",
});
helpList.createEl("li", {
text: "Use project:: property in frontmatter",
});
const createBtn = helpEl.createEl("button", {
cls: "projects-create-btn",
text: "Create Project",
});
createBtn.addEventListener("click", () => {
this.createNewProject();
});
}
private hideEmptyState(): void {
const emptyEl = this.containerEl.querySelector(".projects-empty-state");
if (emptyEl) {
emptyEl.remove();
}
}
private showErrorState(message: string): void {
this.containerEl.empty();
this.createErrorContainer(message);
}
}

View file

@ -1,239 +0,0 @@
/**
* Tags Bases View
* Specialized view for tag-based task organization
*/
import { App, Menu } from "obsidian";
import { BaseTaskBasesView } from "./BaseTaskBasesView";
import { TagsComponent } from "@/components/features/task/view/tags";
import TaskProgressBarPlugin from "@/index";
import { filterTasks } from "@/utils/task/task-filter-utils";
import { t } from "@/translations/helper";
import { Task } from "@/types/task";
export class TagsBasesView extends BaseTaskBasesView {
type = "tags-bases-view";
private tagsComponent: TagsComponent;
private isLoaded = false;
constructor(
containerEl: HTMLElement,
app: App,
plugin: TaskProgressBarPlugin
) {
super(containerEl, app, plugin, "tags");
this.initializeComponents();
}
private initializeComponents(): void {
// Create tags component
this.tagsComponent = new TagsComponent(
this.containerEl,
this.app,
this.plugin,
{
onTaskSelected: (task) => {
// Handle task selection using base class method
console.log("[TagsBasesView] Task selected:", task);
this.handleTaskSelection(task);
},
onTaskCompleted: (task) => {
// Handle task completion using base class method
console.log("[TagsBasesView] Task completed:", task);
this.handleTaskCompletionLocal(task);
},
onTaskContextMenu: (event, task) => {
// Handle context menu
console.log("[TagsBasesView] Task context menu:", task);
this.handleTaskContextMenu(event, task);
},
}
);
this.addChild(this.tagsComponent);
}
// Abstract method implementations
protected onConfigUpdated(): void {
console.log(
"[TagsBasesView] onConfigUpdated called, isLoaded:",
this.isLoaded
);
if (this.isLoaded) {
// Convert data again in case configuration affects data processing
this.convertEntriesToTasks();
this.updateTagTasks();
}
}
protected onDataUpdated(): void {
// Handle data updates - convert data and update tasks
console.log(
"[TagsBasesView] onDataUpdated called, isLoaded:",
this.isLoaded
);
// Force convert entries to tasks to get latest data
this.convertEntriesToTasks();
// Then update the view
this.updateTagTasks();
}
protected onDisplay(): void {
this.containerEl.removeClass("is-loading");
if (this.tasks.length === 0) {
this.showEmptyState();
} else {
this.hideEmptyState();
}
}
protected onViewLoad(): void {
this.tagsComponent.load();
this.isLoaded = true;
this.updateTagTasks();
}
protected onViewUnload(): void {
this.isLoaded = false;
}
protected onViewResize(): void {
// Tags component handles its own resize
}
protected getCustomActions(): Array<{
name: string;
callback: () => void;
icon: string;
}> {
return [
{
name: t("Manage Tags"),
icon: "tags",
callback: () => {
this.openTagManager();
},
},
];
}
protected getEditMenuItems(): Array<{
displayName: string;
component: (container: HTMLElement) => any;
}> {
return [];
}
private updateTagTasks(): void {
console.log(
"[TagsBasesView] updateTagTasks called, isLoaded:",
this.isLoaded
);
if (!this.isLoaded) {
console.log("[TagsBasesView] View not loaded yet, skipping update");
return;
}
console.log(
"[TagsBasesView] Processing",
this.tasks.length,
"total tasks"
);
try {
// Filter tasks for tags view (tasks with tags)
const tagTasks = filterTasks(this.tasks, "tags", this.plugin);
console.log(
`[TagsBasesView] Filtered ${tagTasks.length} tagged tasks from ${this.tasks.length} total tasks`
);
// Update tags component with filtered tasks
this.tagsComponent.setTasks(tagTasks);
console.log(
`[TagsBasesView] Successfully updated TagsComponent with ${tagTasks.length} tagged tasks`
);
} catch (error) {
console.error("[TagsBasesView] Error updating tag tasks:", error);
this.showErrorState("Failed to update tag tasks");
}
}
private async handleTaskCompletionLocal(task: Task): Promise<void> {
// Use base class method for task completion
try {
await super.handleTaskCompletion(task);
// Trigger refresh after completion
setTimeout(() => {
this.refreshTasks();
}, 100);
} catch (error) {
console.error(
"[TagsBasesView] Error handling task completion:",
error
);
}
}
/**
* Handle task context menu
*/
private handleTaskContextMenu(event: MouseEvent, task: Task): void {
const menu = new Menu();
menu.addItem((item: any) => {
item.setTitle(t("Complete"));
item.setIcon("check-square");
item.onClick(() => {
this.handleTaskCompletionLocal(task);
});
})
.addSeparator()
.addItem((item: any) => {
item.setTitle(t("Edit"));
item.setIcon("pencil");
item.onClick(() => {
this.handleTaskSelection(task); // Open details view for editing
});
})
.addItem((item: any) => {
item.setTitle(t("Edit in File"));
item.setIcon("file-edit");
item.onClick(() => {
this.handleTaskEdit(task);
});
});
menu.showAtMouseEvent(event);
}
private openTagManager(): void {
console.log("[TagsBasesView] Opening tag manager");
// This would open a tag management modal
}
private showEmptyState(): void {
this.hideEmptyState();
const emptyEl = this.createEmptyContainer("No tagged tasks found");
emptyEl.addClass("tags-empty-state");
}
private hideEmptyState(): void {
const emptyEl = this.containerEl.querySelector(".tags-empty-state");
if (emptyEl) {
emptyEl.remove();
}
}
private showErrorState(message: string): void {
this.containerEl.empty();
this.createErrorContainer(message);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,186 @@
---
bases:
properties:
- name: text
- description: text
- status: checkbox
- priority: select
options: ["1", "2", "3", "4", "5"]
- project: text
- tags: list
- due_date: date
- start_date: date
- completed_date: date
- context: text
- assignee: text
- estimated_hours: number
---
# Sample Tasks Base for Task Genius Integration
This file demonstrates how to use Task Genius views with Bases plugin queries.
## Example Queries
### 1. Kanban Board View
```base
view: task-genius-kanban
from: "Tasks"
where: status != "x"
config:
taskContent: name
taskStatus: status
taskPriority: priority
taskProject: project
taskTags: tags
taskDueDate: due_date
```
### 2. Calendar View
```base
view: task-genius-calendar
from: "Tasks"
where: due_date != null
config:
taskContent: name
taskDueDate: due_date
taskStatus: status
```
### 3. Gantt Chart View
```base
view: task-genius-gantt
from: "Tasks"
where: start_date != null AND due_date != null
config:
taskContent: name
taskStartDate: start_date
taskDueDate: due_date
taskStatus: status
taskProject: project
```
### 4. Project View with Tree Structure
```base
view: task-genius-projects
from: "Tasks"
where: project != null
sort: project asc, priority desc
config:
taskContent: name
taskProject: project
taskStatus: status
taskPriority: priority
```
### 5. Inbox View for Unprocessed Tasks
```base
view: task-genius-inbox
from: "Tasks"
where: status = " " AND project = null
sort: created desc
config:
taskContent: name
taskStatus: status
taskPriority: priority
taskTags: tags
```
### 6. Review View for Daily Processing
```base
view: task-genius-review
from: "Tasks"
where: status != "x" AND (due_date <= date("today") OR priority >= 3)
config:
taskContent: name
taskStatus: status
taskPriority: priority
taskDueDate: due_date
taskProject: project
```
### 7. Unified View with Custom Configuration
```base
view: task-genius-unified
from: "Tasks"
config:
viewMode: "forecast"
taskContent: name
taskStatus: status
taskPriority: priority
taskProject: project
taskTags: tags
taskDueDate: due_date
taskStartDate: start_date
taskContext: context
```
## Sample Task Entries
| name | description | status | priority | project | tags | due_date | start_date | context | assignee |
|------|-------------|--------|----------|---------|------|----------|------------|---------|----------|
| Write documentation | Create comprehensive docs for new feature | [ ] | 3 | Development/Docs | documentation, high-priority | 2024-02-15 | 2024-02-10 | @work | John |
| Review PR #123 | Code review for authentication changes | [ ] | 4 | Development/Backend | review, urgent | 2024-02-12 | | @work | Sarah |
| Plan Q2 roadmap | Define goals and milestones for Q2 | [ ] | 5 | Planning | planning, strategic | 2024-03-01 | 2024-02-20 | @office | Team Lead |
| Fix login bug | Users can't login with special characters | [ ] | 5 | Development/Frontend | bug, critical | 2024-02-11 | | @urgent | Alex |
| Update dependencies | Upgrade all npm packages to latest versions | [x] | 2 | Development/Maintenance | maintenance | 2024-02-10 | 2024-02-09 | @work | DevOps |
| Design new logo | Create brand refresh proposal | [ ] | 3 | Design/Branding | design, creative | 2024-02-28 | 2024-02-15 | @creative | Designer |
| Customer interview | Interview key customer about new features | [ ] | 4 | Research | research, customer | 2024-02-13 | | @calls | PM |
| Write blog post | Technical blog about new architecture | [ ] | 2 | Marketing/Content | content, blog | 2024-02-25 | 2024-02-18 | @writing | Tech Writer |
| Setup monitoring | Configure APM for production | [ ] | 4 | DevOps/Infrastructure | monitoring, setup | 2024-02-14 | 2024-02-12 | @technical | SRE |
| Team retrospective | Monthly team retrospective meeting | [ ] | 3 | Team/Meetings | meeting, team | 2024-02-16 | | @meetings | Scrum Master |
## Configuration Notes
### Property Mappings
- **taskContent**: Maps to the main task text (usually 'name' or 'title')
- **taskStatus**: Maps to completion status (checkbox or status text)
- **taskPriority**: Maps to priority level (1-5, where 5 is highest)
- **taskProject**: Maps to project assignment (supports nested projects with '/')
- **taskTags**: Maps to tags (can be list or comma-separated text)
- **taskDueDate**: Maps to due date field
- **taskStartDate**: Maps to start date field
- **taskCompletedDate**: Maps to completion date field
- **taskContext**: Maps to context (@context format)
### View Modes
Each view type has specific features:
- **Inbox**: Simple task list with filtering
- **Forecast**: Timeline-based view
- **Projects**: Hierarchical project organization
- **Tags**: Tag-based grouping
- **Calendar**: Monthly/weekly calendar
- **Kanban**: Drag-and-drop board by status
- **Gantt**: Project timeline visualization
- **Review**: Task processing workflow
- **Habits**: Recurring task tracking
- **Flagged**: High-priority task focus
### Advanced Features
1. **Live Filtering**: Use the filter button in any view
2. **Quick Capture**: Add tasks quickly with the capture button
3. **Details Panel**: Click tasks to see/edit details
4. **Status Switching**: Right-click for status options
5. **Drag & Drop**: Supported in Kanban and some other views
6. **Keyboard Shortcuts**: Various shortcuts for task operations
## Tips for Best Results
1. **Consistent Property Names**: Use consistent property names across your bases
2. **Status Values**: Use standard status marks (space for not started, 'x' for completed)
3. **Project Hierarchy**: Use '/' to create nested projects (e.g., "Development/Frontend/Components")
4. **Date Formats**: Use ISO format (YYYY-MM-DD) for dates
5. **Priority Scale**: Use 1-5 scale where 5 is highest priority
6. **Tags Format**: Use comma-separated values or list format for tags
7. **Context Format**: Use @context format for contexts (e.g., "@work", "@home")
## Troubleshooting
If views don't appear:
1. Ensure Bases plugin is installed and enabled
2. Restart Obsidian after installing Task Genius
3. Check console for any error messages
4. Verify property mappings match your base schema
5. Ensure query syntax is correct
For more information, see the Task Genius documentation.

118
src/pages/bases/index.ts Normal file
View file

@ -0,0 +1,118 @@
/**
* Bases Plugin Integration for Task Genius
*
* This module provides complete integration between Task Genius and the Bases plugin,
* allowing all Task Genius views to be used as Bases views with full data conversion
* and view management capabilities.
*/
export { TaskBasesView, TaskBasesViewType } from './TaskBasesView';
export {
registerTaskGeniusBasesViews
} from './registerBasesViews';
/**
* Example usage in Bases:
*
* 1. Create a base file with task-related properties:
* ```yaml
* ---
* bases:
* properties:
* - task_content: text
* - status: checkbox
* - priority: number
* - project: text
* - tags: list
* - due_date: date
* - context: text
* ---
* ```
*
* 2. Query your tasks:
* ```base
* view: task-genius-kanban
* from: "Tasks"
* where: status != "x"
* sort: priority desc, due_date asc
* ```
*
* 3. Available view types:
* - task-genius-inbox: Inbox view for managing tasks
* - task-genius-forecast: Timeline forecast view
* - task-genius-projects: Project-based organization
* - task-genius-tags: Tag-based browsing
* - task-genius-calendar: Calendar layout
* - task-genius-kanban: Kanban board
* - task-genius-gantt: Gantt chart
* - task-genius-review: Task review interface
* - task-genius-habits: Habit tracking
* - task-genius-flagged: High-priority flagged tasks
* - task-genius-unified: Configurable unified view
* - task-genius-custom: Fully customizable view
*
* 4. Property mappings:
* Map your base properties to Task Genius fields:
* - taskContent: The main task text
* - taskStatus: Completion status (checkbox or text)
* - taskPriority: Priority level (1-5)
* - taskProject: Project assignment
* - taskTags: Tags (list or comma-separated)
* - taskDueDate: Due date
* - taskStartDate: Start date
* - taskCompletedDate: Completion date
* - taskContext: Task context (@context)
*
* 5. View-specific configurations (saved to view config):
*
* Kanban View:
* - kanban.groupBy: Group tasks by (status, priority, tags, project, context, dueDate, startDate)
* - kanban.hideEmptyColumns: Hide columns with no tasks
* - kanban.defaultSortField: Default sort field (priority, dueDate, scheduledDate, startDate, createdDate)
* - kanban.defaultSortOrder: Sort order (asc or desc)
*
* Calendar View:
* - calendar.firstDayOfWeek: First day of week (0=Sunday, 1=Monday, etc.)
* - calendar.hideWeekends: Hide weekend columns
*
* Gantt View:
* - gantt.showTaskLabels: Show task labels on bars
* - gantt.useMarkdownRenderer: Use markdown rendering for task names
*
* Forecast View:
* - forecast.firstDayOfWeek: First day of week (0=Sunday, 1=Monday, etc.)
* - forecast.hideWeekends: Hide weekend columns
*
* These configurations are saved directly to the .base file's frontmatter and are
* specific to each view instance. They override plugin-level settings.
*/
/**
* Integration Features:
*
* 1. Full View Support:
* - All Task Genius views available as Bases views
* - Seamless switching between view modes
* - Persistent configuration per view
*
* 2. Data Conversion:
* - Automatic conversion between Bases entries and Task format
* - Support for all task properties and metadata
* - Bi-directional sync when using WriteAPI
*
* 3. Filtering & Sorting:
* - Advanced task filtering system
* - Multiple sort criteria support
* - Live filter updates
*
* 4. Task Operations:
* - Complete/uncomplete tasks
* - Edit task properties
* - Status switching
* - Quick capture integration
*
* 5. Customization:
* - Configurable property mappings
* - View-specific settings
* - Theme-aware styling
*/

View file

@ -0,0 +1,157 @@
import { ViewOption } from 'obsidian';
import { TaskBasesView } from './TaskBasesView';
import TaskProgressBarPlugin from '../../index';
import { ViewMode } from '@/common/setting-definition';
/**
* View type definitions for Task Genius Bases views
*/
interface TaskGeniusBasesViewType {
id: string;
name: string;
icon: string;
defaultViewMode: ViewMode;
description?: string;
}
/**
* All available Task Genius view types for Bases
*/
const TASK_GENIUS_BASES_VIEWS: TaskGeniusBasesViewType[] = [
{
id: 'task-genius-inbox',
name: 'Inbox (Task Genius)',
icon: 'lucide-inbox',
defaultViewMode: 'inbox',
description: 'View and manage tasks in inbox mode'
},
{
id: 'task-genius-forecast',
name: 'Forecast (Task Genius)',
icon: 'lucide-calendar-days',
defaultViewMode: 'forecast',
description: 'View tasks in forecast timeline'
},
{
id: 'task-genius-projects',
name: 'Projects (Task Genius)',
icon: 'lucide-folder-tree',
defaultViewMode: 'projects',
description: 'Organize tasks by projects'
},
{
id: 'task-genius-tags',
name: 'Tags (Task Genius)',
icon: 'lucide-tags',
defaultViewMode: 'tags',
description: 'Browse tasks by tags'
},
{
id: 'task-genius-calendar',
name: 'Calendar (Task Genius)',
icon: 'lucide-calendar',
defaultViewMode: 'calendar',
description: 'View tasks in calendar layout'
},
{
id: 'task-genius-kanban',
name: 'Kanban (Task Genius)',
icon: 'lucide-columns-3',
defaultViewMode: 'kanban',
description: 'Manage tasks in kanban board'
},
{
id: 'task-genius-gantt',
name: 'Gantt (Task Genius)',
icon: 'lucide-gantt-chart-square',
defaultViewMode: 'gantt',
description: 'View tasks in Gantt chart'
},
{
id: 'task-genius-review',
name: 'Review (Task Genius)',
icon: 'lucide-list-checks',
defaultViewMode: 'review',
description: 'Review and process tasks'
},
{
id: 'task-genius-habits',
name: 'Habits (Task Genius)',
icon: 'lucide-target',
defaultViewMode: 'habit',
description: 'Track habits and recurring tasks'
},
{
id: 'task-genius-flagged',
name: 'Flagged (Task Genius)',
icon: 'lucide-flag',
defaultViewMode: 'flagged',
description: 'View high-priority flagged tasks'
},
];
/**
* Create view options for a specific view type
*/
function createViewOptions(viewType: TaskGeniusBasesViewType): () => ViewOption[] {
return () => {
// For specialized views, pass the viewMode to filter options
const viewMode = viewType.defaultViewMode;
const baseOptions = TaskBasesView.getViewOptions(viewMode);
const viewSettingsGroup = baseOptions.find(
opt => opt.displayName === 'View Settings' && opt.type === 'group'
);
if (viewSettingsGroup && 'items' in viewSettingsGroup) {
// Remove the view mode selector for specialized views
viewSettingsGroup.items = viewSettingsGroup.items?.filter(
item => item.key !== 'viewMode'
);
// Add a note about the view type
viewSettingsGroup.items?.unshift({
displayName: 'View Type',
type: 'text',
key: '__viewType',
placeholder: `This is a ${viewType.name} view`,
default: viewType.name,
readonly: true,
} as any);
}
return baseOptions;
};
}
/**
* Register all Task Genius views with the Bases plugin
* @param plugin - The main Task Genius plugin instance
*/
export function registerTaskGeniusBasesViews(plugin: TaskProgressBarPlugin) {
// Register each view type
TASK_GENIUS_BASES_VIEWS.forEach(viewType => {
try {
plugin.registerBasesView(viewType.id, {
name: viewType.name,
icon: viewType.icon,
factory: (controller, containerEl) => {
const view = new TaskBasesView(
controller,
containerEl,
plugin,
viewType.defaultViewMode
);
view.setForcedViewMode(viewType.defaultViewMode as ViewMode);
return view;
},
options: createViewOptions(viewType),
});
console.log(`Registered Bases view: ${viewType.name} (${viewType.id})`);
} catch (error) {
console.error(`Failed to register Bases view ${viewType.id}:`, error);
}
});
}

View file

@ -205,6 +205,10 @@
padding: 0;
}
.bases-view .task-genius-view {
height: 100%;
}
/* Main container layout */
.task-genius-container {
display: flex;
@ -405,12 +409,12 @@
}
.mod-left-split
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
display: none;
}
.mod-right-split
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
display: none;
}

View file

@ -12,8 +12,7 @@ import {
} from "obsidian";
import { Component } from "obsidian";
import { HabitProps } from "./habit-card";
import { RootFilterState } from "../components/features/task/filter/ViewTaskFilter";
import { BasesViewRegistration } from "./bases";
import { RootFilterState } from "@/components/features/task/filter/ViewTaskFilter";
interface Token extends EditorRange {
/** @todo Documentation incomplete. */
@ -338,17 +337,21 @@ declare module "obsidian" {
interface MenuItem {
setSubmenu(): Menu;
titleEl: HTMLElement;
setWarning(warning: boolean): this;
}
interface Setting {
open(): void;
openTabById(tabId: string): void;
}
interface Commands {
executeCommandById(commandId: string): void;
executeCommandById(commandId: string, ...args: any[]): void;
}
@ -356,31 +359,17 @@ declare module "obsidian" {
_loaded: boolean;
}
/**
* Plugin interface extension for Bases API support
*/
interface Plugin {
/**
* Register a bases view (Obsidian 1.9.3+)
* @param viewId - Unique identifier for the view
* @param factory - Factory function to create the view
* @returns true if registration was successful, false otherwise
*/
registerBasesView(
viewId: string,
config: BasesViewRegistration
): boolean;
}
interface Workspace {
on(
event: "task-genius:task-added",
callback: (task: Task) => void
): EventRef;
on(
event: "task-genius:task-updated",
callback: (task: Task) => void
): EventRef;
on(
event: "task-genius:task-deleted",
callback: (taskId: string) => void
@ -400,42 +389,55 @@ declare module "obsidian" {
event: "task-genius:ics-cache-updated",
callback: () => void
): EventRef;
on(
event: "task-genius:task-completed",
callback: (task: Task) => void
): EventRef;
on(
event: "task-genius:habit-index-updated",
callback: (habits: HabitProps[]) => void
): EventRef;
on(
event: "task-genius:filter-changed",
callback: (filterState: RootFilterState, leafId?: string) => void
): EventRef;
on(
event: "task-genius:view-config-changed",
callback: (payload: { reason: string; viewId?: string }) => void
): EventRef;
trigger(event: "task-genius:task-completed", task: Task): void;
trigger(event: "task-genius:task-added", task: Task): void;
trigger(event: "task-genius:task-updated", task: Task): void;
trigger(event: "task-genius:task-deleted", taskId: string): void;
trigger(
event: "task-genius:task-cache-updated",
cache: TaskCache
): void;
trigger(event: "task-genius:ics-config-changed"): void;
trigger(event: "task-genius:ics-cache-updated"): void;
trigger(
event: "task-genius:habit-index-updated",
habits: HabitProps[]
): void;
trigger(
event: "task-genius:filter-changed",
filterState: RootFilterState,
leafId?: string
): void;
trigger(
event: "task-genius:view-config-changed",
payload: { reason: string; viewId?: string }
@ -577,9 +579,13 @@ declare module "obsidian" {
interface WidgetEditorView {
editable: boolean;
showEditor(): void;
editMode: MarkdownScrollableEditView;
unload(): void;
/**
* Data after reference.
*/

File diff suppressed because one or more lines are too long