Merge branch 'master' into fix/cancel-date-goes-before-task-content

This commit is contained in:
Boninall 2025-09-27 09:21:40 +08:00 committed by GitHub
commit 8b098bad38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 860 additions and 182 deletions

View file

@ -4,6 +4,39 @@ All notable changes to beta releases will be documented in this file.
## [9.9.0-beta.6](https://github.com/Quorafind/Obsidian-Task-Genius/compare/9.9.0-beta.5...9.9.0-beta.6) (2025-09-27)
### Features
* improve date positioning logic for task metadata ([41f1d95](https://github.com/Quorafind/Obsidian-Task-Genius/commit/41f1d95576940fab61d1ded3dfce5dcbaa559420)), closes [#463](https://github.com/Quorafind/Obsidian-Task-Genius/issues/463)
* **settings:** add workspace selector component ([27c426c](https://github.com/Quorafind/Obsidian-Task-Genius/commit/27c426c4748afd0470a15913b8c663bcd3cb2cda))
### Bug Fixes
* cancel date goes before task content ([8b48e6a](https://github.com/Quorafind/Obsidian-Task-Genius/commit/8b48e6a25ba04622f359043df7d88e52b38da24f))
* **editor:** improve date positioning in task content ([dab3ecb](https://github.com/Quorafind/Obsidian-Task-Genius/commit/dab3ecb5d70cc40c8507bed1c63928e7e8d3eb3b))
## [9.9.0-beta.5](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/compare/9.9.0-beta.4...9.9.0-beta.5) (2025-09-26)
### Bug Fixes
* **setting:** manage workspace setting should jump to workspace setting tab ([f282bff](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/f282bffbf84d2b8fdde21521620342e1f47f6a58))
* **task-mover:** prevent archive markers on non-task lines and preserve folded content ([f20c5eb](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/f20c5ebee3b999fe69a902afee1b0d12e28fcfbe))
* **v2:** hide top navigation for two-column views ([2c24068](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/2c24068c69f314d7ccc63212c243e11abaaa6262))
### Chores
* **release:** bump version to 9.9.0-beta.5 [beta] ([ef0834c](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/ef0834c65a859eba1b8fbe68cf44d8328d2a7a9e))
* **version:** fix version issue before ([8abd736](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/8abd736e8400b97657fd63f0857d927cbc71e3e8))
## [9.9.0-beta.5](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/compare/9.9.0-beta.4...9.9.0-beta.5) (2025-09-26)
### Bug Fixes
* **setting:** manage workspace setting should jump to workspace setting tab ([f282bff](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/f282bffbf84d2b8fdde21521620342e1f47f6a58))
* **task-mover:** prevent archive markers on non-task lines and preserve folded content ([f20c5eb](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/f20c5ebee3b999fe69a902afee1b0d12e28fcfbe))
* **v2:** hide top navigation for two-column views ([2c24068](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/2c24068c69f314d7ccc63212c243e11abaaa6262))
## [9.9.0-beta.4](https://github.com/Quorafind/Obsidian-Task-Genius/compare/9.9.0-beta.3...9.9.0-beta.4) (2025-09-24)
### Features

View file

@ -1,7 +1,7 @@
{
"id": "obsidian-task-progress-bar",
"name": "Task Genius",
"version": "9.9.0-beta.4",
"version": "9.9.0-beta.6",
"minAppVersion": "0.15.2",
"description": "Comprehensive task management that includes progress bars, task status cycling, and advanced task tracking features.",
"author": "Boninall",

View file

@ -1,6 +1,6 @@
{
"name": "task-genius",
"version": "9.9.0-beta.4",
"version": "9.9.0-beta.6",
"description": "Comprehensive task management plugin for Obsidian with progress bars, task status cycling, and advanced task tracking features.",
"main": "main.js",
"scripts": {

View file

@ -0,0 +1,215 @@
// @ts-ignore
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
import {
findMetadataInsertPosition,
} from "../editor-extensions/date-time/date-manager";
import TaskProgressBarPlugin from "../index";
import { App } from "obsidian";
// Mock the plugin
const mockPlugin: Partial<TaskProgressBarPlugin> = {
settings: {
autoDateManager: {
enabled: true,
manageStartDate: true,
manageCompletedDate: true,
manageCancelledDate: true,
startDateFormat: "YYYY-MM-DD",
completedDateFormat: "YYYY-MM-DD",
cancelledDateFormat: "YYYY-MM-DD",
startDateMarker: "🚀",
completedDateMarker: "✅",
cancelledDateMarker: "❌",
},
preferMetadataFormat: "emoji",
taskStatuses: {
completed: "x|X",
inProgress: "/|-",
abandoned: "_",
planned: "!",
notStarted: " ",
},
},
} as unknown as TaskProgressBarPlugin;
describe("Improved Date Insertion Logic", () => {
describe("Content with Special Characters", () => {
it("should handle task with wiki links correctly", () => {
const lineText = "- [ ] Check [[Project Notes]] for details";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Check [[Project Notes]] for details");
});
it("should handle nested wiki links", () => {
const lineText = "- [ ] Read [[Books/[[Nested]] Guide]]";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Read [[Books/[[Nested]] Guide]]");
});
it("should handle hashtag in URL content", () => {
const lineText = "- [ ] Visit https://example.com/#section";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Visit https://example.com/#section");
});
it("should handle emoji in task content", () => {
const lineText = "- [ ] Fix the 🚀 rocket launch code";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Fix the 🚀 rocket launch code");
});
it("should handle multiple hashtags in URL", () => {
const lineText = "- [ ] Check site.com/#anchor#section#part";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Check site.com/#anchor#section#part");
});
});
describe("Metadata Positioning", () => {
it("should insert cancelled date before tags", () => {
const lineText = "- [ ] Important task #urgent #priority";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Important task");
expect(lineText.substring(position)).toBe(" #urgent #priority");
});
it("should insert cancelled date before dataview fields", () => {
const lineText = "- [ ] Task content [due:: 2025-10-01]";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Task content");
expect(lineText.substring(position)).toBe(" [due:: 2025-10-01]");
});
it("should insert cancelled date after start date", () => {
const lineText = "- [ ] Task 🚀 2025-09-01";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
// For cancelled date, it should go after the start date
expect(position).toBeGreaterThan(24); // After "🚀 2025-09-01"
});
it("should handle multiple metadata items correctly", () => {
const lineText = "- [ ] Task content #tag1 [due:: 2025-10-01] #tag2";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Task content");
});
});
describe("Block References", () => {
it("should insert date before block reference", () => {
const lineText = "- [ ] Task with reference ^task-123";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"completed"
);
// For completed date, should be before block reference
expect(lineText.substring(position)).toContain("^task-123");
});
it("should handle block reference with trailing spaces", () => {
const lineText = "- [ ] Task with reference ^task-123 ";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"completed"
);
expect(lineText.substring(position)).toContain("^task-123");
});
});
describe("Edge Cases", () => {
it("should handle empty task", () => {
const lineText = "- [ ] ";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(position).toBe(6); // Right after "- [ ] "
});
it("should handle task with only spaces", () => {
const lineText = "- [ ] ";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(position).toBe(6); // After trimming trailing spaces
});
it("should handle task with brackets in content", () => {
const lineText = "- [ ] Use array[0] or dict[key] in code";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Use array[0] or dict[key] in code");
});
it("should not confuse markdown links with wiki links", () => {
const lineText = "- [ ] Check [this link](https://example.com)";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
expect(lineText.substring(0, position)).toBe("- [ ] Check [this link](https://example.com)");
});
});
describe("Original PR Issue", () => {
it("should place cancelled date after task content, not before", () => {
const lineText = "- [ ] test entry";
const position = findMetadataInsertPosition(
lineText,
mockPlugin as TaskProgressBarPlugin,
"cancelled"
);
// Position should be after "test entry", not after "- [ ] "
expect(position).toBe(16); // After "test entry"
// Simulate adding the cancelled date
const result = lineText.slice(0, position) + " ❌2025-09-25" + lineText.slice(position);
expect(result).toBe("- [ ] test entry ❌2025-09-25");
expect(result).not.toBe("- [ ] ❌2025-09-25test entry"); // This was the bug
});
});
});

View file

@ -502,6 +502,18 @@ export class TaskUtils {
// Get the current task line
const currentLine = lines[taskLine];
// Check if the current line is actually a task
// Tasks must match pattern: optional whitespace + list marker (-, number., or *) + space + checkbox
const taskPattern = /^\s*(-|\d+\.|\*) \[(.)\]/;
if (!taskPattern.test(currentLine)) {
// Not a task line, return empty result
return {
content: "",
linesToRemove: [],
};
}
const currentIndent = this.getIndentation(currentLine, app);
// Extract the parent task's mark
@ -539,9 +551,42 @@ export class TaskUtils {
// Include the current line and completed child tasks
resultLines.push(parentTaskWithMarker);
// First, collect all indented content that belongs to this task (folded content)
// This includes notes, tags, and other content that is indented under the task
const taskContent: { line: string; index: number; indent: number }[] = [];
for (let i = taskLine + 1; i < lines.length; i++) {
const line = lines[i];
const lineIndent = this.getIndentation(line, app);
// Stop if we've reached content at the same or lower indentation level
if (lineIndent <= currentIndent) {
break;
}
// Check if this is a task at the direct child level
const isTask = /^\s*(-|\d+\.|\*) \[(.)\]/.test(line);
if (isTask) {
// For non-"all" modes, we need to handle child tasks specially
// So we stop collecting the immediate folded content here
if (moveMode !== "all") {
break;
}
}
// This is indented content that belongs to the parent task
taskContent.push({ line, index: i, indent: lineIndent });
}
// If we're moving all subtasks, we'll collect them all
if (moveMode === "all") {
for (let i = taskLine + 1; i < lines.length; i++) {
// Add all the folded content and subtasks
for (const item of taskContent) {
resultLines.push(this.completeTaskIfNeeded(item.line, settings));
linesToRemove.push(item.index);
}
// Continue collecting all nested subtasks beyond the immediate folded content
for (let i = taskLine + taskContent.length + 1; i < lines.length; i++) {
const line = lines[i];
const lineIndent = this.getIndentation(line, app);
@ -559,6 +604,11 @@ export class TaskUtils {
}
// If we're moving only completed tasks or direct children
else {
// Always include the immediate folded content (notes, tags, etc.)
for (const item of taskContent) {
resultLines.push(item.line); // Don't complete non-task content
linesToRemove.push(item.index);
}
// First pass: collect all child tasks to analyze
const childTasks: {
line: string;
@ -568,7 +618,9 @@ export class TaskUtils {
isIncompleted: boolean;
}[] = [];
for (let i = taskLine + 1; i < lines.length; i++) {
// Start after the folded content we already collected
const startIndex = taskLine + taskContent.length + 1;
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
const lineIndent = this.getIndentation(line, app);

View file

@ -3,6 +3,7 @@ import { SettingsIndexer } from "@/components/features/settings/core/SettingsInd
import { SearchResult } from "@/types/SettingsSearch";
import { t } from "@/translations/helper";
import { TaskProgressBarSettingTab } from "@/setting";
import { WorkspaceSettingsSelector } from "./WorkspaceSettingsSelector";
/**
*
@ -21,6 +22,7 @@ export class SettingsSearchComponent extends Component {
private debouncedSearch: (query: string) => void;
private isVisible = false;
private blurTimeoutId = 0;
private workspaceSelector: WorkspaceSettingsSelector | null = null;
constructor(
settingTab: TaskProgressBarSettingTab,
@ -46,8 +48,29 @@ export class SettingsSearchComponent extends Component {
*
*/
private createSearchUI(): void {
// 创建主容器
const mainContainer = this.containerEl.createDiv();
mainContainer.addClass("tg-settings-main-container");
// 创建头部栏包含workspace选择器和搜索框
const headerBar = mainContainer.createDiv();
headerBar.addClass("tg-settings-header-bar");
// 创建workspace选择器容器
const workspaceSelectorContainer = headerBar.createDiv();
workspaceSelectorContainer.addClass("tg-workspace-selector-container");
// 初始化workspace选择器
if (this.settingTab.plugin.workspaceManager) {
this.workspaceSelector = new WorkspaceSettingsSelector(
workspaceSelectorContainer,
this.settingTab.plugin,
this.settingTab
);
}
// 创建搜索容器
const searchContainer = this.containerEl.createDiv();
const searchContainer = headerBar.createDiv();
searchContainer.addClass("tg-settings-search-container");
// 创建搜索输入框容器
@ -75,8 +98,8 @@ export class SettingsSearchComponent extends Component {
setIcon(this.clearButton, "x");
this.clearButton.style.display = "none";
// 创建搜索结果容器
this.resultsContainerEl = searchContainer.createDiv();
// 创建搜索结果容器在header bar外面
this.resultsContainerEl = mainContainer.createDiv();
this.resultsContainerEl.addClass("tg-settings-search-results");
this.resultsContainerEl.style.display = "none";
this.resultsContainerEl.setAttribute("role", "listbox");

View file

@ -0,0 +1,148 @@
import { Menu, setIcon } from "obsidian";
import type TaskProgressBarPlugin from "@/index";
import { WorkspaceData } from "@/experimental/v2/types/workspace";
import { t } from "@/translations/helper";
import { TaskProgressBarSettingTab } from "@/setting";
export class WorkspaceSettingsSelector {
private containerEl: HTMLElement;
private plugin: TaskProgressBarPlugin;
private settingTab: TaskProgressBarSettingTab;
private currentWorkspaceId: string;
private buttonEl: HTMLElement;
constructor(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
settingTab: TaskProgressBarSettingTab
) {
this.containerEl = containerEl;
this.plugin = plugin;
this.settingTab = settingTab;
this.currentWorkspaceId =
plugin.workspaceManager?.getActiveWorkspace().id || "";
this.render();
}
private render() {
// Create workspace selector container
const selectorContainer = this.containerEl.createDiv({
cls: "workspace-settings-selector",
});
if (!this.plugin.workspaceManager) return;
const currentWorkspace =
this.plugin.workspaceManager.getActiveWorkspace();
this.buttonEl = selectorContainer.createDiv({
cls: "workspace-settings-selector-button",
});
// Workspace icon
const workspaceIcon = this.buttonEl.createDiv({
cls: "workspace-icon",
});
setIcon(workspaceIcon, currentWorkspace.icon || "layers");
// Workspace name
const workspaceName = this.buttonEl.createSpan({
cls: "workspace-name",
text: currentWorkspace.name,
});
// Dropdown arrow
const dropdownIcon = this.buttonEl.createDiv({
cls: "workspace-dropdown-icon",
});
setIcon(dropdownIcon, "chevron-down");
// Click handler
this.buttonEl.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this.showWorkspaceMenu(e);
});
}
private showWorkspaceMenu(event: MouseEvent) {
if (!this.plugin.workspaceManager) return;
const menu = new Menu();
const workspaces = this.plugin.workspaceManager.getAllWorkspaces();
const currentWorkspace =
this.plugin.workspaceManager.getActiveWorkspace();
// Add workspace items for switching
workspaces.forEach((workspace) => {
menu.addItem((item) => {
item.setTitle(workspace.name)
.setIcon(workspace.icon || "layers")
.onClick(async () => {
await this.switchWorkspace(workspace.id);
});
// Mark current workspace with checkmark
if (workspace.id === currentWorkspace.id) {
item.setChecked(true);
}
});
});
// Add separator
menu.addSeparator();
// Add "Manage Workspaces..." option to navigate to settings
menu.addItem((item) => {
item.setTitle(t("Manage Workspaces..."))
.setIcon("settings")
.onClick(() => {
// Navigate to workspace settings tab
this.settingTab.switchToTab("workspaces");
});
});
menu.showAtMouseEvent(event);
}
private async switchWorkspace(workspaceId: string) {
if (!this.plugin.workspaceManager) return;
// Switch workspace
await this.plugin.workspaceManager.setActiveWorkspace(workspaceId);
this.currentWorkspaceId = workspaceId;
// Update button display
this.updateDisplay();
// Trigger settings reload to reflect workspace change
await this.plugin.saveSettings();
this.settingTab.applySettingsUpdate();
}
private updateDisplay() {
if (!this.plugin.workspaceManager || !this.buttonEl) return;
const currentWorkspace =
this.plugin.workspaceManager.getActiveWorkspace();
// Update icon
const iconEl = this.buttonEl.querySelector(".workspace-icon");
if (iconEl) {
iconEl.empty();
setIcon(iconEl as HTMLElement, currentWorkspace.icon || "layers");
}
// Update name
const nameEl = this.buttonEl.querySelector(".workspace-name");
if (nameEl) {
nameEl.textContent = currentWorkspace.name;
}
}
public setWorkspace(workspaceId: string) {
this.currentWorkspaceId = workspaceId;
this.updateDisplay();
}
}

View file

@ -670,25 +670,94 @@ function findMetadataInsertPosition(
): number {
// Work with the full line text, don't extract block reference yet
const blockRef = detectBlockReference(lineText);
// Find the end of the task content, right after the task description
const taskMatch = lineText.match(
/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]\s*([^#\[📅🚀✅❌🛫▶️⏰🏁]*)/
);
// Find the task marker and status
const taskMatch = lineText.match(/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]\s*/);
if (!taskMatch) return blockRef ? blockRef.index : lineText.length;
// Start position is right after the task checkbox
let position = taskMatch[0].length;
// For cancelled date, we need special handling to insert after all metadata and start dates
// Find the actual end of task content by scanning through the text
// This handles content with special characters, links, etc.
let contentEnd = position;
let inLink = 0; // Track nested [[links]]
let inDataview = false; // Track [field:: value] metadata
const remainingText = lineText.slice(position);
for (let i = 0; i < remainingText.length; i++) {
const char = remainingText[i];
const nextChar = remainingText[i + 1];
const twoChars = char + (nextChar || '');
// Handle [[wiki links]] - they are part of content
if (twoChars === '[[') {
inLink++;
contentEnd = position + i + 2;
i++; // Skip next char
continue;
}
if (twoChars === ']]' && inLink > 0) {
inLink--;
contentEnd = position + i + 2;
i++; // Skip next char
continue;
}
// If we're inside a link, everything is content
if (inLink > 0) {
contentEnd = position + i + 1;
continue;
}
// Check for dataview metadata [field:: value]
if (char === '[' && !inDataview) {
const afterBracket = remainingText.slice(i + 1);
if (afterBracket.match(/^[a-zA-Z]+::/)) {
// This is dataview metadata, stop here
break;
}
}
// Check for tags (only if preceded by whitespace or at start)
if (char === '#') {
if (i === 0 || remainingText[i - 1] === ' ' || remainingText[i - 1] === '\t') {
// Check if this is actually a tag (followed by word characters)
const afterHash = remainingText.slice(i + 1);
if (afterHash.match(/^[\w-]+/)) {
// This is a tag, stop here
break;
}
}
}
// Check for date emojis (these are metadata markers)
const dateEmojis = ['📅', '🚀', '✅', '❌', '🛫', '▶️', '⏰', '🏁'];
if (dateEmojis.includes(char)) {
// Check if this is followed by a date pattern
const afterEmoji = remainingText.slice(i + 1);
if (afterEmoji.match(/^\s*\d{4}-\d{2}-\d{2}/)) {
// This is a date marker, stop here
break;
}
}
// Regular content character
contentEnd = position + i + 1;
}
position = contentEnd;
// Trim trailing whitespace
while (position > taskMatch[0].length && lineText[position - 1] === ' ') {
position--;
}
// For cancelled date, we need special handling to insert after start dates if present
if (dateType === "cancelled") {
const useDataviewFormat =
plugin.settings.preferMetadataFormat === "dataview";
// Find the last occurrence of either:
// 1. Start date marker (🛫 or [start::)
// 2. If no start date, find the end of all metadata
// Look for start date first
// Look for existing start date
let startDateFound = false;
if (useDataviewFormat) {
const startDateMatch = lineText.match(/\[start::[^\]]*\]/);
@ -717,9 +786,6 @@ function findMetadataInsertPosition(
);
startDateMatch = lineText.match(pattern);
if (startDateMatch) {
console.log(
`[AutoDateManager] Found start date with emoji ${emoji}`
);
break;
}
}
@ -728,47 +794,11 @@ function findMetadataInsertPosition(
if (startDateMatch && startDateMatch.index !== undefined) {
position = startDateMatch.index + startDateMatch[0].length;
startDateFound = true;
console.log(
`[AutoDateManager] Found start date at index ${startDateMatch.index}, length ${startDateMatch[0].length}, new position: ${position}`
);
}
}
console.log(
`[AutoDateManager] Start date found: ${startDateFound}, position: ${position}`
);
if (!startDateFound) {
// No start date found, find the end of all metadata
// This includes tags (#), dataview fields ([field::]), and other date markers
let lastMetadataEnd = position;
// Find all metadata occurrences
const metadataRegexes = [
/#[\w-]+/g, // Tags
/\[[a-zA-Z]+::[^\]]*\]/g, // Dataview fields
/[📅🚀✅❌🛫▶️⏰🏁]\s*\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}(?::\d{2})?)?/g, // Date markers (including all common start emojis)
];
for (const regex of metadataRegexes) {
let match;
while ((match = regex.exec(lineText)) !== null) {
if (match.index >= position) {
const matchEnd = match.index + match[0].length;
if (matchEnd > lastMetadataEnd) {
lastMetadataEnd = matchEnd;
}
}
}
}
position = lastMetadataEnd;
}
// Ensure we have a space before the cancelled date
if (position > 0 && lineText[position - 1] !== " ") {
// No need to add space here, it will be added with the date
}
// If no start date found, position is already correct from initial parsing
// It points to the end of content before metadata
} else if (dateType === "completed") {
// For completed date, we want to go to the end of the line (before block reference)
// This is different from cancelled/start dates which go after content/metadata
@ -783,56 +813,9 @@ function findMetadataInsertPosition(
}
}
} else {
// For start date, find the end of main content before metadata
let contentEnd = position;
let inBrackets = false;
const chars = lineText.slice(position).split("");
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
// Track if we're inside dataview brackets
if (char === "[" && !inBrackets) {
// Check if this is a dataview field like [due::...]
const remainingText = lineText.slice(position + i);
if (remainingText.match(/^\[[a-zA-Z]+::/)) {
// This is metadata, stop here
break;
}
inBrackets = true;
contentEnd = position + i + 1;
} else if (char === "]" && inBrackets) {
inBrackets = false;
contentEnd = position + i + 1;
} else if (!inBrackets) {
// Check for metadata markers when not inside brackets
if (
char === "#" ||
char === "📅" ||
char === "🚀" ||
char === "✅" ||
char === "❌" ||
char === "🛫" ||
char === "▶️" ||
char === "⏰" ||
char === "🏁"
) {
// This is metadata, stop here
break;
}
contentEnd = position + i + 1;
} else {
// Inside brackets, keep going
contentEnd = position + i + 1;
}
}
position = contentEnd;
// Trim any trailing whitespace from position
while (position > 0 && lineText[position - 1] === " ") {
position--;
}
// For start date, the position has already been calculated correctly
// in the initial content parsing above
// No additional processing needed
}
// Ensure position doesn't exceed the block reference position

View file

@ -1,7 +1,6 @@
import {
ItemView,
WorkspaceLeaf,
Plugin,
Notice,
debounce,
setIcon,
@ -9,8 +8,8 @@ import {
TFile,
ButtonComponent,
} from "obsidian";
import TaskProgressBarPlugin from "../../index";
import { Task, BaseTask } from "../../types/task";
import TaskProgressBarPlugin from "@/index";
import { Task } from "@/types/task";
import { V2Sidebar } from "./components/V2Sidebar";
import "./styles/v2.css";
import "./styles/v2-enhanced.css";
@ -22,43 +21,42 @@ import {
onWorkspaceSwitched,
onWorkspaceOverridesSaved,
} from "./events/ui-event";
import { Events, on } from "../../dataflow/events/Events";
import { TaskListItemComponent } from "../../components/features/task/view/listItem";
import { TaskTreeItemComponent } from "../../components/features/task/view/treeItem";
import { Events, on } from "@/dataflow/events/Events";
import { TaskListItemComponent } from "@/components/features/task/view/listItem";
import { TaskTreeItemComponent } from "@/components/features/task/view/treeItem";
import {
CalendarComponent,
CalendarEvent,
} from "../../components/features/calendar";
import { KanbanComponent } from "../../components/features/kanban/kanban";
import { GanttComponent } from "../../components/features/gantt/gantt";
import { filterTasks } from "../../utils/task/task-filter-utils";
} from "@/components/features/calendar";
import { KanbanComponent } from "@/components/features/kanban/kanban";
import { GanttComponent } from "@/components/features/gantt/gantt";
import { filterTasks } from "@/utils/task/task-filter-utils";
import {
getViewSettingOrDefault,
TwoColumnSpecificConfig,
} from "../../common/setting-definition";
import { ContentComponent } from "../../components/features/task/view/content";
import { ForecastComponent } from "../../components/features/task/view/forecast";
import { TagsComponent } from "../../components/features/task/view/tags";
import { ProjectsComponent } from "../../components/features/task/view/projects";
import { ReviewComponent } from "../../components/features/task/view/review";
import { Habit } from "../../components/features/habit/habit";
import { ViewComponentManager } from "../../components/ui/behavior/ViewComponentManager";
import { TaskPropertyTwoColumnView } from "../../components/features/task/view/TaskPropertyTwoColumnView";
} from "@/common/setting-definition";
import { ContentComponent } from "@/components/features/task/view/content";
import { ForecastComponent } from "@/components/features/task/view/forecast";
import { TagsComponent } from "@/components/features/task/view/tags";
import { ProjectsComponent } from "@/components/features/task/view/projects";
import { ReviewComponent } from "@/components/features/task/view/review";
import { Habit } from "@/components/features/habit/habit";
import { ViewComponentManager } from "@/components/ui/behavior/ViewComponentManager";
import { TaskPropertyTwoColumnView } from "@/components/features/task/view/TaskPropertyTwoColumnView";
import {
TaskDetailsComponent,
createTaskCheckbox,
} from "../../components/features/task/view/details";
import { ConfirmModal } from "../../components/ui/modals/ConfirmModal";
import { QuickCaptureModal } from "../../components/features/quick-capture/modals/QuickCaptureModal";
} from "@/components/features/task/view/details";
import { ConfirmModal } from "@/components/ui/modals/ConfirmModal";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModal";
import {
ViewTaskFilterPopover,
ViewTaskFilterModal,
} from "../../components/features/task/filter";
import { RootFilterState } from "../../components/features/task/filter/ViewTaskFilter";
} from "@/components/features/task/filter";
import { RootFilterState } from "@/components/features/task/filter/ViewTaskFilter";
import { Platform } from "obsidian";
import { TaskProgressBarSettingTab } from "../../setting";
import { isDataflowEnabled } from "../../dataflow/createDataflow";
import { t } from "../../translations/helper";
import { isDataflowEnabled } from "@/dataflow/createDataflow";
import { t } from "@/translations/helper";
export const TASK_VIEW_V2_TYPE = "task-genius-view-v2";
@ -121,27 +119,27 @@ export class TaskViewV2 extends ItemView {
private taskCountEl: HTMLElement;
private filterInputEl: HTMLInputElement;
private viewToggleBtn: HTMLElement;
private isTreeView: boolean = false;
private isTreeView = false;
// View action buttons
private detailsToggleBtn: HTMLElement;
private currentSelectedTaskId: string | null = null;
private lastToggleTimestamp: number = 0;
private isDetailsVisible: boolean = false;
private lastToggleTimestamp = 0;
private isDetailsVisible = false;
private currentFilterState: RootFilterState | null = null;
// Sidebar collapse state
private isSidebarCollapsed: boolean = false;
private isSidebarCollapsed = false;
private sidebarToggleBtn: HTMLElement | null = null;
private isMobileDrawerOpen: boolean = false;
private isMobileDrawerOpen = false;
private drawerOverlay: HTMLElement | null = null;
// Touch gesture tracking
private touchStartX: number = 0;
private touchStartY: number = 0;
private touchCurrentX: number = 0;
private isSwiping: boolean = false;
private swipeThreshold: number = 50;
private touchStartX = 0;
private touchStartY = 0;
private touchCurrentX = 0;
private isSwiping = false;
private swipeThreshold = 50;
// V2 Details panel
private detailsPanelEl: HTMLElement | null = null;
@ -831,7 +829,7 @@ export class TaskViewV2 extends ItemView {
}
}
private hideAllComponents(forceHideAll: boolean = false) {
private hideAllComponents(forceHideAll = false) {
// During initialization, we need to hide all components initially
// But during view switches, we can be smarter about it
const isInitialHide = this.isInitializing && forceHideAll;
@ -993,7 +991,7 @@ export class TaskViewV2 extends ItemView {
);
}
private async loadTasks(showLoading: boolean = true) {
private async loadTasks(showLoading = true) {
try {
console.log("[TG-V2] loadTasks started, showLoading:", showLoading);
// Only show loading state if requested, not initializing, and we don't have tasks
@ -3056,8 +3054,8 @@ export class TaskViewV2 extends ItemView {
// Check for special two-column views
const viewConfig = getViewSettingOrDefault(this.plugin, viewId as any);
if (viewConfig?.specificConfig?.viewType === "twocolumn") {
// Two-column views typically support list and tree modes
return ["list", "tree"];
// Two-column views have their own specialized UI and don't need view mode switching
return [];
}
// Check for special views managed by ViewComponentManager

View file

@ -184,6 +184,12 @@ export class WorkspaceSelector {
this.plugin.app.setting.openTabById(
"obsidian-task-progress-bar"
);
setTimeout(() => {
if (this.plugin.settingTab) {
this.plugin.settingTab.openTab("workspaces");
}
}, 100);
});
});

View file

@ -19,6 +19,7 @@ import "./styles/setting-v2.css";
import "./styles/beta-warning.css";
import "./styles/settings-search.css";
import "./styles/settings-migration.css";
import "./styles/workspace-settings-selector.css";
import {
renderAboutSettingsTab,
renderBetaTestSettingsTab,
@ -45,7 +46,6 @@ import { renderMcpIntegrationSettingsTab } from "./components/features/settings/
import { IframeModal } from "@/components/ui/modals/IframeModal";
import { renderTaskTimerSettingTab } from "./components/features/settings/tabs/TaskTimerSettingsTab";
import { renderBasesSettingsTab } from "./components/features/settings/tabs/BasesSettingsTab";
import { WorkspaceData } from "./experimental/v2/types/workspace";
import { renderWorkspaceSettingsTab } from "@/components/features/settings/tabs/WorkspaceSettingTab";
export class TaskProgressBarSettingTab extends PluginSettingTab {
@ -321,6 +321,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
if (tab.id === "mcp-integration" && !Platform.isDesktopApp) {
return;
}
// Skip workspaces tab from main navigation (accessed via dropdown)
if (tab.id === "workspaces") {
return;
}
const category = tab.category || "core";
if (categories[category as keyof typeof categories]) {
categories[category as keyof typeof categories].tabs.push(tab);
@ -440,6 +444,18 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
(settingsHeader as unknown as HTMLElement).style.display =
"none";
}
// Special handling for workspaces tab - ensure it's still accessible via dropdown
if (tabId === "workspaces") {
// Make sure the workspace section is visible even though the tab is hidden
const workspaceSection = this.containerEl.querySelector(
'[data-tab-id="workspaces"]'
);
if (workspaceSection) {
(workspaceSection as unknown as HTMLElement).style.display = "block";
workspaceSection.addClass("settings-tab-section-active");
}
}
}
public openTab(tabId: string) {

View file

@ -1,11 +1,5 @@
/* Settings Search Component Styles */
/* 搜索容器 */
.tg-settings-search-container {
margin-bottom: var(--size-4-4);
position: relative;
}
/* 搜索输入框容器 - 使用 Obsidian 标准输入框样式 */
.tg-settings-search-input-container {
position: relative;
@ -209,10 +203,6 @@ input.tg-settings-search-input {
/* 响应式设计 */
@media (max-width: 768px) {
.tg-settings-search-container {
margin-bottom: var(--size-4-3);
}
.tg-settings-search-results {
max-height: 280px;
border-radius: var(--radius-s);
@ -258,13 +248,6 @@ input.tg-settings-search-input {
background-clip: content-box;
}
/* 确保搜索框在设置页面中的正确定位 */
.task-genius-settings .tg-settings-search-container {
order: -1; /* 确保搜索框显示在最前面 */
margin-top: 0;
margin-bottom: var(--size-4-4);
}
/* 与设置标签页容器的间距调整 */
.task-genius-settings .settings-tabs-categorized-container {
margin-top: var(--size-4-3);

View file

@ -0,0 +1,130 @@
/* Workspace Settings Selector Styles */
/* Main container for the header bar */
.tg-settings-header-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
width: 100%;
}
/* Container for workspace selector */
.tg-workspace-selector-container {
flex-shrink: 0;
}
/* Workspace selector button */
.workspace-settings-selector {
display: inline-block;
}
.workspace-settings-selector-button {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background-color: var(--background-modifier-form-field);
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
min-width: 150px;
height: 34px; /* Match search bar height */
}
.workspace-settings-selector-button:hover {
background-color: var(--background-modifier-hover);
border-color: var(--background-modifier-border-hover);
}
.workspace-settings-selector-button:active {
background-color: var(--background-modifier-active-hover);
}
/* Workspace icon */
.workspace-settings-selector-button .workspace-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.workspace-settings-selector-button .workspace-icon svg {
width: 16px;
height: 16px;
fill: var(--text-muted);
}
/* Workspace name */
.workspace-settings-selector-button .workspace-name {
flex: 1;
font-size: 14px;
color: var(--text-normal);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Dropdown arrow icon */
.workspace-settings-selector-button .workspace-dropdown-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex-shrink: 0;
margin-left: auto;
}
.workspace-settings-selector-button .workspace-dropdown-icon svg {
width: 12px;
height: 12px;
fill: var(--text-muted);
transition: transform 0.15s ease;
}
.workspace-settings-selector-button:hover .workspace-dropdown-icon svg {
fill: var(--text-normal);
}
/* Adjust search container to take remaining space */
.tg-settings-header-bar .tg-settings-search-container {
flex: 1;
max-width: none;
}
/* Ensure search input container fills width */
.tg-settings-header-bar .tg-settings-search-input-container {
width: 100%;
}
/* Main container adjustments */
.tg-settings-main-container {
width: 100%;
}
/* Ensure search results appear below the header bar */
.tg-settings-main-container .tg-settings-search-results {
margin-top: 8px;
}
/* Mobile responsive adjustments */
@media (max-width: 768px) {
.tg-settings-header-bar {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.workspace-settings-selector-button {
width: 100%;
min-width: unset;
}
.tg-settings-header-bar .tg-settings-search-container {
width: 100%;
}
}

File diff suppressed because one or more lines are too long

View file

@ -153,5 +153,7 @@
"9.9.0-beta.1": "0.15.2",
"9.9.0-beta.2": "0.15.2",
"9.9.0-beta.3": "0.15.2",
"9.9.0-beta.4": "0.15.2"
"9.9.0-beta.4": "0.15.2",
"9.9.0-beta.5": "0.15.2",
"9.9.0-beta.6": "0.15.2"
}