fix: tag input event issue

This commit is contained in:
Quorafind 2025-10-30 14:08:02 +08:00
parent bc3a1d31d4
commit 4d04384b6e
15 changed files with 1007 additions and 434 deletions

5
.cursor/worktrees.json Normal file
View file

@ -0,0 +1,5 @@
{
"setup-worktree": [
"pnpm install"
]
}

View file

@ -37,9 +37,9 @@ export class MinimalQuickCaptureModal extends BaseQuickCaptureModal {
private targetFileEl: HTMLDivElement | null = null;
private editorContainer: HTMLElement | null = null;
constructor(app: App, plugin: TaskProgressBarPlugin) {
constructor(app: App, plugin: TaskProgressBarPlugin, metadata?: TaskMetadata) {
// Default to checkbox mode for task creation
super(app, plugin, "checkbox");
super(app, plugin, "checkbox", metadata);
this.minimalSuggest = plugin.minimalQuickCaptureSuggest;
@ -48,6 +48,11 @@ export class MinimalQuickCaptureModal extends BaseQuickCaptureModal {
this.taskMetadata.location =
(targetType === "custom-file" ? "file" : targetType) || "fixed";
this.taskMetadata.targetFile = this.getTargetFile();
// Merge passed metadata with existing taskMetadata
if (metadata) {
this.taskMetadata = { ...this.taskMetadata, ...metadata };
}
}
onOpen() {

View file

@ -252,7 +252,6 @@ export class SettingsSearchComponent extends Component {
nameEl.addClass("tg-settings-search-result-name");
nameEl.textContent = result.item.name;
// 所属分类和标签页
const metaEl = resultEl.createDiv();
metaEl.addClass("tg-settings-search-result-meta");
@ -272,12 +271,10 @@ export class SettingsSearchComponent extends Component {
);
}
// 使用 registerDomEvent 注册点击事件
this.registerDomEvent(resultEl, "click", () => {
this.selectResult(result);
});
// 使用 registerDomEvent 注册鼠标悬停事件
this.registerDomEvent(resultEl, "mouseenter", () => {
this.setSelectedIndex(index);
});

View file

@ -1,4 +1,4 @@
import { App, Component, setIcon } from "obsidian";
import { App, Component, setIcon, debounce } from "obsidian";
import { Task } from "@/types/task";
import { TaskListItemComponent } from "./listItem"; // Re-import needed components
import { ViewMode, getViewSettingOrDefault } from "@/common/setting-definition"; // 导入 SortCriterion
@ -181,7 +181,7 @@ export class ContentComponent extends Component {
console.log("toggle view mode");
// Save the new view mode state
saveViewMode(this.app, this.currentViewId, this.isTreeView);
this.refreshTaskList(); // Refresh list completely on view mode change
this.debounceRefreshTaskList(); // Refresh list completely on view mode change
}
public setIsTreeView(isTree: boolean) {
@ -193,7 +193,7 @@ export class ContentComponent extends Component {
if (viewToggleBtn) {
setIcon(viewToggleBtn, this.isTreeView ? "git-branch" : "list");
}
this.refreshTaskList();
this.debounceRefreshTaskList();
}
}
@ -218,7 +218,7 @@ export class ContentComponent extends Component {
this.notFilteredTasks = notFilteredTasks;
updateSignatures();
this.applyFilters();
this.refreshTaskList();
this.debounceRefreshTaskList();
return;
}
@ -248,7 +248,7 @@ export class ContentComponent extends Component {
this.lastAllTasksSignature = nextAllSignature;
this.lastNotFilteredTasksSignature = nextNotFilteredSignature;
this.applyFilters();
this.refreshTaskList();
this.debounceRefreshTaskList();
}
// Updated method signature
@ -266,7 +266,7 @@ export class ContentComponent extends Component {
this.initializeViewMode();
this.applyFilters();
this.refreshTaskList();
this.debounceRefreshTaskList();
}
private applyFilters() {
@ -329,7 +329,7 @@ export class ContentComponent extends Component {
private filterTasks(query: string) {
this.applyFilters(); // Re-apply all filters including the new text query
this.refreshTaskList();
this.debounceRefreshTaskList();
}
private computeTaskSignature(tasks: Task[]): string {
@ -370,6 +370,8 @@ export class ContentComponent extends Component {
return rect.width > 0 && rect.height > 0;
}
private debounceRefreshTaskList = debounce(this.refreshTaskList, 700);
private refreshTaskList() {
console.log("refreshing");
// Defer rendering if container is not visible yet (e.g., view hidden during init)

View file

@ -1,13 +1,9 @@
import {
Component,
ExtraButtonComponent,
TFile,
ButtonComponent,
DropdownComponent,
TextComponent,
moment,
App,
Menu,
debounce,
Platform,
} from "obsidian";
@ -22,6 +18,7 @@ import {
ContextSuggest,
ProjectSuggest,
TagSuggest,
TAG_COMMIT_EVENT,
} from "@/components/ui/inputs/AutoComplete";
import { FileTask } from "@/types/file-task";
import {
@ -108,8 +105,8 @@ export class TaskDetailsComponent extends Component {
public containerEl: HTMLElement;
private contentEl: HTMLElement;
public currentTask: Task | null = null;
private isVisible: boolean = true;
private isEditing: boolean = false;
private isVisible = true;
private isEditing = false;
private editFormEl: HTMLElement | null = null;
// Events
@ -328,6 +325,8 @@ export class TaskDetailsComponent extends Component {
cls: "details-edit-form",
});
let requestSave = () => {};
// Task content/title
const contentField = this.createFormField(
this.editFormEl,
@ -421,26 +420,124 @@ export class TaskDetailsComponent extends Component {
new ProjectSuggest(this.app, projectInput.inputEl, this.plugin);
// Tags field
let currentTags = (task.metadata.tags ?? [])
.map((tag) => (tag.startsWith("#") ? tag.slice(1) : tag))
.filter((tag) => tag);
const tagsField = this.createFormField(this.editFormEl, t("Tags"));
const tagsInput = new TextComponent(tagsField);
console.log("tagsInput", tagsInput, task.metadata.tags);
// Remove # prefix from tags when displaying them
tagsInput.setValue(
task.metadata.tags
? task.metadata.tags
.map((tag) =>
tag.startsWith("#") ? tag.slice(1) : tag
)
.join(", ")
: ""
const tagsContainer = tagsField.createDiv({ cls: "tags-editor" });
const tagsList = tagsContainer.createDiv({ cls: "tags-editor__list" });
const addTagButton = new ExtraButtonComponent(tagsContainer).setIcon(
"plus"
);
const tagInputWrapper = tagsContainer.createDiv({
cls: "tags-editor__input tags-editor__input--hidden",
});
const tagInput = new TextComponent(tagInputWrapper);
tagInput.setPlaceholder(t("Add tag"));
new TagSuggest(this.app, tagInput.inputEl, this.plugin, true);
const hideTagInput = () => {
tagInput.setValue("");
tagInputWrapper.addClass("tags-editor__input--hidden");
};
const renderTags = () => {
tagsList.empty();
if (currentTags.length === 0) {
tagsList.createSpan({
cls: "tags-editor__empty",
text: t("No tags"),
});
return;
}
currentTags.forEach((tag, index) => {
const tagChip = tagsList.createDiv({
cls: "tags-editor__tag",
});
tagChip.createSpan({
cls: "tags-editor__tag-label",
text: `#${tag}`,
});
new ExtraButtonComponent(tagChip)
.setIcon("x")
.setTooltip(t("Remove tag"))
.onClick(() => {
currentTags.splice(index, 1);
renderTags();
requestSave();
});
});
};
const addTag = (rawValue: string) => {
const trimmedValue = rawValue.trim();
if (!trimmedValue) {
hideTagInput();
return;
}
const normalized = trimmedValue
.replace(/^#+/, "")
.replace(/[,\s]+$/g, "")
.trim();
if (!normalized) {
hideTagInput();
return;
}
const exists = currentTags.some(
(existing) =>
existing.toLowerCase() === normalized.toLowerCase()
);
if (exists) {
hideTagInput();
return;
}
currentTags.push(normalized);
renderTags();
hideTagInput();
requestSave();
};
const addTagFromInput = () => addTag(tagInput.getValue());
this.registerDomEvent(
tagInput.inputEl,
TAG_COMMIT_EVENT as keyof HTMLElementEventMap,
(event) => {
event.stopPropagation();
const customEvent = event as CustomEvent<{ tag: string }>;
addTag(customEvent.detail?.tag ?? "");
}
);
addTagButton.onClick(() => {
tagInputWrapper.removeClass("tags-editor__input--hidden");
tagInput.inputEl.focus();
});
this.registerDomEvent(tagInput.inputEl, "keydown", (event) => {
if (event.key === "Escape") {
event.preventDefault();
hideTagInput();
}
});
this.registerDomEvent(tagInput.inputEl, "blur", () => {
addTagFromInput();
});
tagsField
.createSpan({ cls: "field-description" })
.setText(
t("Comma separated") + " " + t("e.g. #tag1, #tag2, #tag3")
);
.setText(t("Click + to add tags. Click × on a tag to remove it."));
new TagSuggest(this.app, tagsInput.inputEl, this.plugin);
renderTags();
// Context field
const contextField = this.createFormField(
@ -559,17 +656,10 @@ export class TaskDetailsComponent extends Component {
metadata.project = task.metadata.project;
}
// Parse and update tags (remove # prefix if present)
const tagsValue = tagsInput.getValue();
metadata.tags = tagsValue
? tagsValue
.split(",")
.map((tag) => tag.trim())
.map((tag) =>
tag.startsWith("#") ? tag.slice(1) : tag
) // Remove # prefix if present
.filter((tag) => tag)
: [];
// Update tags from current list
metadata.tags = currentTags
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
// Update context
const contextValue = contextInput.getValue();
@ -700,6 +790,8 @@ export class TaskDetailsComponent extends Component {
}
}, 800); // 1500ms debounce time - allow time for multi-field editing
requestSave = () => saveTask();
// Use OnCompletionConfigurator directly
const onCompletionConfigurator = new OnCompletionConfigurator(
onCompletionField,
@ -803,7 +895,7 @@ export class TaskDetailsComponent extends Component {
// Register all input elements
registerBlurEvent(contentInput.inputEl);
registerBlurEvent(projectInput.inputEl);
registerBlurEvent(tagsInput.inputEl);
registerBlurEvent(tagInput.inputEl);
registerBlurEvent(contextInput.inputEl);
registerBlurEvent(priorityDropdown.selectEl);
// Remove blur events for date inputs to prevent duplicate saves

View file

@ -4,6 +4,7 @@ import {
setIcon,
ExtraButtonComponent,
Platform,
Notice,
} from "obsidian";
import { Task, TgProject } from "@/types/task";
import { t } from "@/translations/helper";
@ -23,6 +24,7 @@ import {
formatProgressText,
ProgressData,
} from "@/editor-extensions/ui-widgets/progress-bar-widget";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
interface SelectedProjects {
projects: string[];
@ -271,6 +273,32 @@ export class ProjectsComponent extends Component {
});
taskCountEl.setText(`0 ${t("tasks")}`);
// Add quick capture button to add task to current project
const addTaskButton = headerTopRightRow.createDiv({
cls: "projects-add-task-btn clickable-icon",
attr: { "aria-label": t("Add task to project") },
});
setIcon(addTaskButton, "plus");
this.registerDomEvent(addTaskButton, "click", () => {
// Get the currently selected single project
const selectedProject =
this.selectedProjects.projects.length === 1
? this.selectedProjects.projects[0]
: undefined;
if (selectedProject) {
new QuickCaptureModal(
this.app,
this.plugin,
{ project: selectedProject },
true
).open();
} else {
new Notice(t("Please select a single project first"));
}
});
// Add view toggle button
const viewToggleBtn = headerTopRightRow.createDiv({
cls: "view-toggle-btn",

View file

@ -22,7 +22,7 @@ const CACHE_DURATION = 30000; // 30 seconds
// Helper function to get cached data
export async function getCachedData(
plugin: TaskProgressBarPlugin,
forceRefresh: boolean = false
forceRefresh: boolean = false,
): Promise<GlobalAutoCompleteCache> {
const now = Date.now();
@ -33,7 +33,7 @@ export async function getCachedData(
if (!globalCache || now - globalCache.lastUpdate > CACHE_DURATION) {
// Fetch fresh data
const tags = Object.keys(plugin.app.metadataCache.getTags() || {}).map(
(tag) => tag.substring(1) // Remove # prefix
(tag) => tag.substring(1), // Remove # prefix
);
// Get projects and contexts from dataflow using the new convenience method
@ -49,7 +49,7 @@ export async function getCachedData(
} catch (error) {
console.warn(
"Failed to get projects/contexts from dataflow:",
error
error,
);
}
}
@ -92,7 +92,10 @@ export async function getCachedData(
}
abstract class BaseSuggest<T> extends AbstractInputSuggest<T> {
constructor(app: App, public inputEl: HTMLInputElement) {
constructor(
app: App,
public inputEl: HTMLInputElement,
) {
super(app, inputEl);
}
@ -124,7 +127,7 @@ class CustomSuggest extends BaseSuggest<string> {
constructor(
app: App,
inputEl: HTMLInputElement,
availableChoices: string[]
availableChoices: string[],
) {
super(app, inputEl);
this.availableChoices = availableChoices;
@ -138,8 +141,8 @@ class CustomSuggest extends BaseSuggest<string> {
return this.availableChoices
.filter(
(
cmd: string // Add type to cmd
) => fuzzySearch(cmd.toLowerCase()) // Call the returned function
cmd: string, // Add type to cmd
) => fuzzySearch(cmd.toLowerCase()), // Call the returned function
)
.slice(0, 100);
}
@ -160,7 +163,7 @@ export class ProjectSuggest extends CustomSuggest {
constructor(
app: App,
inputEl: HTMLInputElement,
plugin: TaskProgressBarPlugin
plugin: TaskProgressBarPlugin,
) {
// Initialize with empty list, will be populated asynchronously
super(app, inputEl, []);
@ -178,7 +181,7 @@ export class ProjectSuggest extends CustomSuggest {
} catch (e) {
console.warn(
"ProjectSuggest: failed to refresh projects on focus",
e
e,
);
}
});
@ -192,7 +195,7 @@ export class ContextSuggest extends CustomSuggest {
constructor(
app: App,
inputEl: HTMLInputElement,
plugin: TaskProgressBarPlugin
plugin: TaskProgressBarPlugin,
) {
// Initialize with empty list, will be populated asynchronously
super(app, inputEl, []);
@ -207,11 +210,22 @@ export class ContextSuggest extends CustomSuggest {
/**
* TagSuggest - Provides autocomplete for tag names
*/
export const TAG_COMMIT_EVENT = "task-progress-bar:tag-commit";
export class TagSuggest extends CustomSuggest {
private readonly detailedKeydownHandler = (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
event.stopPropagation();
this.commitDetailedTag();
}
};
constructor(
app: App,
inputEl: HTMLInputElement,
plugin: TaskProgressBarPlugin
plugin: TaskProgressBarPlugin,
readonly isDetailed: boolean = false,
) {
// Initialize with empty list, will be populated asynchronously
super(app, inputEl, []);
@ -220,10 +234,30 @@ export class TagSuggest extends CustomSuggest {
getCachedData(plugin).then((cachedData) => {
this.availableChoices = cachedData.tags;
});
if (this.isDetailed) {
inputEl.addEventListener("keydown", this.detailedKeydownHandler);
}
}
// Override getSuggestions to handle comma-separated tags
getSuggestions(query: string): string[] {
if (this.isDetailed) {
const currentTagInput = query
.trim()
.replace(/^#+/, "")
.toLowerCase();
if (!currentTagInput) {
return this.availableChoices.slice(0, 100);
}
const fuzzySearch = prepareFuzzySearch(currentTagInput);
return this.availableChoices
.filter((tag) => fuzzySearch(tag.toLowerCase()))
.slice(0, 100);
}
const parts = query.split(",");
const currentTagInput = parts[parts.length - 1].trim();
@ -239,6 +273,10 @@ export class TagSuggest extends CustomSuggest {
// Override to add # prefix and keep previous tags
getSuggestionValue(item: string): string {
if (this.isDetailed) {
return `#${item}`;
}
const currentValue = this.inputEl.value;
const parts = currentValue.split(",");
@ -253,13 +291,58 @@ export class TagSuggest extends CustomSuggest {
getSuggestionText(item: string): string {
return `#${item}`;
}
// Override to intercept selection in detailed mode
selectSuggestion(item: string, evt: MouseEvent | KeyboardEvent): void {
super.selectSuggestion(item, evt);
if (this.isDetailed) {
this.commitDetailedTag();
}
}
private commitDetailedTag(): void {
if (!this.isDetailed) {
return;
}
const rawValue = this.inputEl.value.trim();
if (!rawValue) {
this.resetDetailedInput();
return;
}
const normalized = rawValue
.replace(/^#+/, "")
.replace(/[,\s]+$/g, "")
.trim();
if (!normalized) {
this.resetDetailedInput();
return;
}
this.inputEl.dispatchEvent(
new CustomEvent(TAG_COMMIT_EVENT, {
detail: { tag: normalized },
bubbles: true,
}),
);
this.resetDetailedInput();
}
private resetDetailedInput(): void {
this.inputEl.value = "";
this.inputEl.trigger("input");
this.close();
}
}
export class SingleFolderSuggest extends CustomSuggest {
constructor(
app: App,
inputEl: HTMLInputElement,
plugin: TaskProgressBarPlugin
plugin: TaskProgressBarPlugin,
) {
const folders = app.vault.getAllFolders();
const paths = folders.map((file) => file.path);
@ -278,7 +361,7 @@ export class FolderSuggest extends CustomSuggest {
app: App,
inputEl: HTMLInputElement,
plugin: TaskProgressBarPlugin,
outputType: "single" | "multiple" = "multiple"
outputType: "single" | "multiple" = "multiple",
) {
// Get all markdown files in the vault
const folders = app.vault.getAllFolders();
@ -299,7 +382,7 @@ export class FolderSuggest extends CustomSuggest {
}
const fuzzySearch = prepareFuzzySearch(
currentPathInput.toLowerCase()
currentPathInput.toLowerCase(),
);
return this.availableChoices
.filter((path) => fuzzySearch(path.toLowerCase()))
@ -357,7 +440,7 @@ export class ImageSuggest extends CustomSuggest {
constructor(
app: App,
inputEl: HTMLInputElement,
plugin: TaskProgressBarPlugin
plugin: TaskProgressBarPlugin,
) {
// Get all images in the vault
const images = app.vault
@ -369,7 +452,7 @@ export class ImageSuggest extends CustomSuggest {
file.extension === "jpeg" ||
file.extension === "gif" ||
file.extension === "svg" ||
file.extension === "webp"
file.extension === "webp",
);
const paths = images.map((file) => file.path);
super(app, inputEl, paths);
@ -388,7 +471,7 @@ export class FileSuggest extends AbstractInputSuggest<TFile> {
app: App,
inputEl: HTMLInputElement | HTMLDivElement,
options: QuickCaptureOptions,
onFileSelected?: (file: TFile) => void
onFileSelected?: (file: TFile) => void,
) {
super(app, inputEl);
this.suggestEl.addClass("quick-capture-file-suggest");
@ -429,7 +512,7 @@ export class FileSuggest extends AbstractInputSuggest<TFile> {
})
.filter(
(match): match is { file: TFile; score: number } =>
match !== null
match !== null,
)
.sort((a, b) => {
// Sort by score (higher is better)
@ -459,7 +542,7 @@ export class SimpleFileSuggest extends AbstractInputSuggest<TFile> {
constructor(
inputEl: HTMLInputElement,
plugin: TaskProgressBarPlugin,
onFileSelected?: (file: TFile) => void
onFileSelected?: (file: TFile) => void,
) {
super(plugin.app, inputEl);
this.onFileSelected = onFileSelected || (() => {});
@ -480,7 +563,7 @@ export class SimpleFileSuggest extends AbstractInputSuggest<TFile> {
})
.filter(
(match): match is { file: TFile; score: number } =>
match !== null
match !== null,
)
.sort((a, b) => {
// Sort by score (higher is better)

View file

@ -1333,7 +1333,7 @@ export class DataflowOrchestrator {
// Debug: log effective specialTagPrefixes for verification
console.debug(
"[TG] Parser specialTagPrefixes:",
"[Task Genius] Parser specialTagPrefixes:",
parserConfig.specialTagPrefixes,
);

View file

@ -35,14 +35,14 @@ function parseTasksWithConfigurableParser(
// Debug: show incoming prefixes and effective specialTagPrefixes keys
try {
console.debug("[TG][Worker] incoming prefixes", {
console.debug("[Task Genius][Worker] incoming prefixes", {
preferMetadataFormat: settings.preferMetadataFormat,
projectTagPrefix: settings.projectTagPrefix,
contextTagPrefix: settings.contextTagPrefix,
areaTagPrefix: settings.areaTagPrefix,
});
console.debug(
"[TG][Worker] specialTagPrefixes keys (before)",
"[Task Genius][Worker] specialTagPrefixes keys (before)",
Object.keys(config.specialTagPrefixes || {}),
);
} catch {}
@ -63,7 +63,7 @@ function parseTasksWithConfigurableParser(
};
try {
console.debug(
"[TG][Worker] specialTagPrefixes keys (after)",
"[Task Genius][Worker] specialTagPrefixes keys (after)",
Object.keys(config.specialTagPrefixes),
);
} catch {}

View file

@ -215,7 +215,7 @@ export default class TaskProgressBarPlugin extends Plugin {
private iconsDeferred = false;
async onload() {
console.time("[TG] onload");
console.time("[Task Genius] onload");
await this.loadSettings();
// Initialize version manager first
@ -323,7 +323,7 @@ export default class TaskProgressBarPlugin extends Plugin {
);
this.app.workspace.onLayoutReady(async () => {
console.time("[TG] onLayoutReady");
console.time("[Task Genius] onLayoutReady");
await this.initializeDeferredStartup();
@ -443,14 +443,14 @@ export default class TaskProgressBarPlugin extends Plugin {
this.maybeShowChangelog();
console.timeEnd("[TG] onLayoutReady");
console.timeEnd("[Task Genius] onLayoutReady");
});
await this.migratePresetTaskFiltersIfNeeded();
this.registerCoreCommands();
console.timeEnd("[TG] onload");
console.timeEnd("[Task Genius] onload");
}
private async initializeDeferredStartup(): Promise<void> {
@ -461,7 +461,7 @@ export default class TaskProgressBarPlugin extends Plugin {
return;
}
console.time("[TG] initializeIndexer");
console.time("[Task Genius] initializeIndexer");
await this.ensureFluentIntegration();
@ -475,7 +475,7 @@ export default class TaskProgressBarPlugin extends Plugin {
const dataflowInitialized = await this.initializeDataflowOrchestrator();
if (!dataflowInitialized) {
this.scheduleExtendedCommands();
console.timeEnd("[TG] initializeIndexer");
console.timeEnd("[Task Genius] initializeIndexer");
return;
}
@ -497,7 +497,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.scheduleExtendedCommands();
console.timeEnd("[TG] initializeIndexer");
console.timeEnd("[Task Genius] initializeIndexer");
}
private async ensureFluentIntegration(): Promise<void> {
@ -829,7 +829,7 @@ export default class TaskProgressBarPlugin extends Plugin {
return;
}
console.time("[TG] migratePresetTaskFilters");
console.time("[Task Genius] migratePresetTaskFilters");
this.settings.taskFilter.presetTaskFilters = presets.map(
(preset: any) => {
if (preset.options) {
@ -839,7 +839,7 @@ export default class TaskProgressBarPlugin extends Plugin {
},
);
await this.saveSettings();
console.timeEnd("[TG] migratePresetTaskFilters");
console.timeEnd("[Task Genius] migratePresetTaskFilters");
}
registerCommands() {
@ -1814,7 +1814,7 @@ export default class TaskProgressBarPlugin extends Plugin {
lastVersion !== "9.9.0"
) {
console.log(
`[TG] Migration detected: ${previousVersion} -> ${currentVersion}, opening onboarding`,
`[Task Genius] Migration detected: ${previousVersion} -> ${currentVersion}, opening onboarding`,
);
// Directly open onboarding view (same pattern as maybeShowChangelog)
@ -1825,7 +1825,10 @@ export default class TaskProgressBarPlugin extends Plugin {
await this.saveSettings();
}
} catch (error) {
console.error("[TG] Failed to check migration onboarding:", error);
console.error(
"[Task Genius] Failed to check migration onboarding:",
error,
);
}
}
@ -1853,7 +1856,7 @@ export default class TaskProgressBarPlugin extends Plugin {
const isBeta = manifestVersion.toLowerCase().includes("beta");
this.changelogManager.openChangelog(manifestVersion, isBeta);
} catch (error) {
console.error("[TG] Failed to show changelog:", error);
console.error("[Task Genius] Failed to show changelog:", error);
}
}

View file

@ -1,4 +1,4 @@
import { debounce, ItemView, Scope, WorkspaceLeaf } from "obsidian";
import { debounce, ItemView, Scope, WorkspaceLeaf, setIcon } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { Task } from "@/types/task";
import "@/styles/fluent/fluent-main.css";
@ -29,6 +29,7 @@ import { FluentGestureManager } from "@/components/features/fluent/managers/Flue
import { FluentWorkspaceStateManager } from "@/components/features/fluent/managers/FluentWorkspaceStateManager";
import { FluentActionHandlers } from "@/components/features/fluent/managers/FluentActionHandlers";
import { TaskSelectionManager } from "@/components/features/task/selection/TaskSelectionManager";
import { getViewSettingOrDefault } from "@/common/setting-definition";
export const FLUENT_TASK_VIEW = "fluent-task-genius-view";
@ -156,12 +157,12 @@ export class FluentTaskView extends ItemView {
!leafId.startsWith("view-config-") &&
leafId !== "global-filter"
) {
console.log("[TG] Filter changed from live component");
console.log("[Task Genius] Filter changed from live component");
this.liveFilterState = filterState;
this.currentFilterState = filterState;
} else if (!leafId) {
// No leafId means it's also a live filter change
console.log("[TG] Filter changed (no leafId)");
console.log("[Task Genius] Filter changed (no leafId)");
this.liveFilterState = filterState;
this.currentFilterState = filterState;
}
@ -189,7 +190,7 @@ export class FluentTaskView extends ItemView {
}
} catch (e) {
console.warn(
"[TG] Failed to sync selectedProject from filter state",
"[Task Genius] Failed to sync selectedProject from filter state",
e,
);
}
@ -280,7 +281,7 @@ export class FluentTaskView extends ItemView {
* Main initialization method
*/
async onOpen() {
console.log("[TG] onOpen started");
console.log("[Task Genius] onOpen started");
this.isInitializing = true;
try {
@ -289,7 +290,7 @@ export class FluentTaskView extends ItemView {
// ====================
if (this.DEBUG_MODE) {
console.log(
"[TG] Initializing UI, managers, structure, and events...",
"[Task Genius] Initializing UI, managers, structure, and events...",
);
}
@ -320,7 +321,7 @@ export class FluentTaskView extends ItemView {
if (this.DEBUG_MODE) {
console.log(
"[TG] ✅ UI, managers, structure, and events initialized",
"[Task Genius] ✅ UI, managers, structure, and events initialized",
);
}
@ -328,7 +329,7 @@ export class FluentTaskView extends ItemView {
// PHASE 5: Restore Workspace State
// ====================
if (this.DEBUG_MODE) {
console.log("[TG] Restoring workspace state...");
console.log("[Task Genius] Restoring workspace state...");
}
const savedWorkspaceId =
@ -338,7 +339,7 @@ export class FluentTaskView extends ItemView {
this.viewState.currentWorkspace = savedWorkspaceId;
if (this.DEBUG_MODE) {
console.log(
`[TG] Restored workspace ID: ${savedWorkspaceId}`,
`[Task Genius] Restored workspace ID: ${savedWorkspaceId}`,
);
}
}
@ -404,11 +405,11 @@ export class FluentTaskView extends ItemView {
// ====================
// PHASE 6: Load Data (KEY PHASE)
// ====================
console.log("[TG] Loading tasks...");
console.log("[Task Genius] Loading tasks...");
await this.dataManager.loadTasks(false); // Will trigger onTasksLoaded callback
await this.dataManager.registerDataflowListeners();
console.log(
`[TG] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`,
`[Task Genius] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`,
);
// ====================
@ -419,12 +420,12 @@ export class FluentTaskView extends ItemView {
// Check window size and auto-collapse sidebar if needed
if (this.DEBUG_MODE) {
console.log("[TG] Checking sidebar collapse...");
console.log("[Task Genius] Checking sidebar collapse...");
}
this.layoutManager.checkAndCollapseSidebar();
} catch (error) {
console.error("[TG] ❌ Initialization error:", error);
console.error("[TG] Error stack:", (error as Error).stack);
console.error("[Task Genius] ❌ Initialization error:", error);
console.error("[Task Genius] Error stack:", (error as Error).stack);
this.loadError =
(error as Error).message || "Failed to initialize view";
} finally {
@ -433,17 +434,17 @@ export class FluentTaskView extends ItemView {
// ====================
if (this.DEBUG_MODE) {
console.log(
`[TG] Finalizing (isInitializing was ${this.isInitializing})`,
`[Task Genius] Finalizing (isInitializing was ${this.isInitializing})`,
);
}
this.isInitializing = false;
if (this.DEBUG_MODE) {
console.log("[TG] Calling final updateView()...");
console.log("[Task Genius] Calling final updateView()...");
}
this.updateView();
console.log("[TG] ✅ Initialization complete");
console.log("[Task Genius] ✅ Initialization complete");
}
}
@ -491,7 +492,9 @@ export class FluentTaskView extends ItemView {
}
},
onUpdateNeeded: (source) => {
console.log(`[TG] Update needed from source: ${source}`);
console.log(
`[Task Genius] Update needed from source: ${source}`,
);
// Re-apply filters and update view
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
this.updateView();
@ -559,7 +562,7 @@ export class FluentTaskView extends ItemView {
// This enables the full project overview mode
if (viewId === "projects") {
console.log(
"[TG] Navigating to projects overview - clearing project selection",
"[Task Genius] Navigating to projects overview - clearing project selection",
);
this.viewState.selectedProject = undefined;
@ -595,7 +598,10 @@ export class FluentTaskView extends ItemView {
);
}
} catch (e) {
console.warn("[TG] Failed to clear project filter", e);
console.warn(
"[Task Genius] Failed to clear project filter",
e,
);
}
}
@ -617,7 +623,7 @@ export class FluentTaskView extends ItemView {
this.updateView();
},
onProjectSelected: (projectId) => {
console.log(`[TG] Project selected: ${projectId}`);
console.log(`[Task Genius] Project selected: ${projectId}`);
this.viewState.selectedProject = projectId;
// Switch to projects view
@ -687,7 +693,7 @@ export class FluentTaskView extends ItemView {
);
} catch (e) {
console.warn(
"[TG] Failed to project-sync filter UI state",
"[Task Genius] Failed to project-sync filter UI state",
e,
);
// If filter sync fails, still update the view
@ -751,14 +757,14 @@ export class FluentTaskView extends ItemView {
// Trigger initial workspace leaf width update
this.updateWorkspaceLeafWidth();
console.log("[TG] Managers initialized");
console.log("[Task Genius] Managers initialized");
}
/**
* Build UI structure - MUST match original DOM structure for CSS
*/
private async buildUIStructure() {
console.log("[TG] Building UI structure");
console.log("[Task Genius] Building UI structure");
// Create layout structure (exact same as original)
const layoutContainer = this.rootContainerEl.createDiv({
@ -845,7 +851,7 @@ export class FluentTaskView extends ItemView {
} else {
sidebarEl.hide();
console.log(
"[TG] Using workspace side leaves: skip in-view sidebar",
"[Task Genius] Using workspace side leaves: skip in-view sidebar",
);
}
@ -868,7 +874,7 @@ export class FluentTaskView extends ItemView {
this.addChild(this.topNavigation);
// Initialize view components
console.log("[TG] Initializing view components");
console.log("[Task Genius] Initializing view components");
this.componentManager = new FluentComponentManager(
this.app,
this.plugin,
@ -891,20 +897,33 @@ export class FluentTaskView extends ItemView {
this.actionHandlers.handleTaskContextMenu(event, task);
},
onKanbanTaskStatusUpdate: (taskId, newStatusMark) => {
console.log(`[FluentTaskView] Kanban status update callback received: taskId=${taskId}, mark=${newStatusMark}`);
console.log(`[FluentTaskView] Current task list size: ${this.tasks.length}`);
console.log(
`[FluentTaskView] Kanban status update callback received: taskId=${taskId}, mark=${newStatusMark}`,
);
console.log(
`[FluentTaskView] Current task list size: ${this.tasks.length}`,
);
const task = this.tasks.find((t) => t.id === taskId);
if (task) {
console.log('[FluentTaskView] Task found, delegating to action handler');
console.log(
"[FluentTaskView] Task found, delegating to action handler",
);
this.actionHandlers.handleKanbanTaskStatusUpdate(
task,
newStatusMark,
);
} else {
console.error(`[FluentTaskView] CRITICAL: Task ${taskId} not found in local task list`);
console.error('[FluentTaskView] Available task IDs:', this.tasks.slice(0, 5).map(t => t.id));
console.error('[FluentTaskView] This indicates a synchronization issue between kanban and main view');
console.error(
`[FluentTaskView] CRITICAL: Task ${taskId} not found in local task list`,
);
console.error(
"[FluentTaskView] Available task IDs:",
this.tasks.slice(0, 5).map((t) => t.id),
);
console.error(
"[FluentTaskView] This indicates a synchronization issue between kanban and main view",
);
}
},
},
@ -914,7 +933,7 @@ export class FluentTaskView extends ItemView {
this.componentManager.initializeViewComponents();
// Sidebar toggle in header and responsive collapse
console.log("[TG] Creating sidebar toggle");
console.log("[Task Genius] Creating sidebar toggle");
this.layoutManager.createSidebarToggle();
// Create task count mark
@ -937,10 +956,10 @@ export class FluentTaskView extends ItemView {
});
// Create action buttons in Obsidian view header
console.log("[TG] Creating action buttons");
console.log("[Task Genius] Creating action buttons");
this.layoutManager.createActionButtons();
console.log("[TG] UI structure built");
console.log("[Task Genius] UI structure built");
}
/**
@ -1110,12 +1129,12 @@ export class FluentTaskView extends ItemView {
*/
private updateView() {
if (this.isInitializing) {
console.log("[TG] Skip update during initialization");
console.log("[Task Genius] Skip update during initialization");
return;
}
console.log(
`[TG] Proceeding with view update for ${this.currentViewId}`,
`[Task Genius] Proceeding with view update for ${this.currentViewId}`,
);
// Update top navigation available modes based on current view
@ -1186,7 +1205,7 @@ export class FluentTaskView extends ItemView {
* Reset all active filters
*/
private resetCurrentFilter(): void {
console.log("[TG] Resetting filter");
console.log("[Task Genius] Resetting filter");
// Clear filter states
this.liveFilterState = null;
@ -1237,7 +1256,7 @@ export class FluentTaskView extends ItemView {
* Clean up on close
*/
async onClose() {
console.log("[TG] onClose started");
console.log("[Task Genius] onClose started");
// Save workspace layout before closing
this.workspaceStateManager.saveWorkspaceLayout();
@ -1250,6 +1269,6 @@ export class FluentTaskView extends ItemView {
// Clear selection
this.actionHandlers.clearSelection();
console.log("[TG] onClose completed");
console.log("[Task Genius] onClose completed");
}
}

View file

@ -40,55 +40,83 @@ const KEY_GET_DROP = "task-genius/workspace-dnd:getDropLocation";
* Install a runtime monkey-patch for Obsidian's internal drag handling,
* using monkey-around for co-operative, removable patches.
*/
export function installWorkspaceDragMonitor(plugin: TaskProgressBarPlugin): void {
export function installWorkspaceDragMonitor(
plugin: TaskProgressBarPlugin,
): void {
const unpatch = around(Workspace.prototype as any, {
onDragLeaf(old: Function | undefined) {
return dedupe(KEY_ON_DRAG, old as Function, function (this: any, e: DragEvent, leaf: any) {
const restricted = isRestrictedLeaf(leaf);
// Mark workspace as currently dragging a restricted leaf until drop/dragend
if (restricted) setRestrictState(this, true);
if (restricted) {
const vt = leaf?.view?.getViewType?.();
console.debug("[TG][MonkeyPatch] onDragLeaf(restricted)", vt);
} else {
console.debug("[TG][MonkeyPatch] onDragLeaf");
}
// Install one-shot cleanup on drop/dragend
const ws = this;
if (restricted) {
const cleanup = () => {
setRestrictState(ws, false);
window.removeEventListener("dragend", cleanup, true);
window.removeEventListener("drop", cleanup, true);
};
window.addEventListener("dragend", cleanup, true);
window.addEventListener("drop", cleanup, true);
}
return old && old.apply(this, [e, leaf]);
});
return dedupe(
KEY_ON_DRAG,
old as Function,
function (this: any, e: DragEvent, leaf: any) {
const restricted = isRestrictedLeaf(leaf);
// Mark workspace as currently dragging a restricted leaf until drop/dragend
if (restricted) setRestrictState(this, true);
if (restricted) {
const vt = leaf?.view?.getViewType?.();
console.debug(
"[Task Genius] onDragLeaf(restricted)",
vt,
);
} else {
console.debug("[Task Genius] onDragLeaf");
}
// Install one-shot cleanup on drop/dragend
const ws = this;
if (restricted) {
const cleanup = () => {
setRestrictState(ws, false);
window.removeEventListener(
"dragend",
cleanup,
true,
);
window.removeEventListener("drop", cleanup, true);
};
window.addEventListener("dragend", cleanup, true);
window.addEventListener("drop", cleanup, true);
}
return old && old.apply(this, [e, leaf]);
},
);
},
getDropLocation(old: Function | undefined) {
return dedupe(KEY_GET_DROP, old as Function, function (this: any, ...args: any[]) {
const target = old && old.apply(this, args);
return dedupe(
KEY_GET_DROP,
old as Function,
function (this: any, ...args: any[]) {
const target = old && old.apply(this, args);
try {
if (getRestrictState(this) && target) {
const root = typeof target?.getRoot === "function" ? target.getRoot() : undefined;
const isCenterRegion = root && root === this.rootSplit && target !== this.leftSplit && target !== this.rightSplit;
if (isCenterRegion) {
console.debug("[TG][MonkeyPatch] Blocked center drop location for restricted leaf");
return null;
try {
if (getRestrictState(this) && target) {
const root =
typeof target?.getRoot === "function"
? target.getRoot()
: undefined;
const isCenterRegion =
root &&
root === this.rootSplit &&
target !== this.leftSplit &&
target !== this.rightSplit;
if (isCenterRegion) {
console.debug(
"[Task Genius] Blocked center drop location for restricted leaf",
);
return null;
}
}
} catch (err) {
console.warn(
"[Task Genius] getDropLocation patch error",
err,
);
}
} catch (err) {
console.warn("[TG][MonkeyPatch] getDropLocation patch error", err);
}
return target;
});
return target;
},
);
},
});
plugin.register(unpatch);
}

View file

@ -315,6 +315,91 @@
margin-top: 2px;
}
.tags-editor {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.tags-editor__list {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
}
.tags-editor__tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
background-color: var(--background-modifier-border);
color: var(--text-muted);
font-size: 0.8em;
line-height: 1.4;
}
.tags-editor__tag-label {
color: var(--text-normal);
}
.tags-editor__remove {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 0.8em;
line-height: 1;
padding: 0 2px;
}
.tags-editor__remove:hover {
color: var(--text-error);
}
.tags-editor__add {
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1em;
}
.tags-editor__add:hover {
background: var(--interactive-accent-hover);
}
.tags-editor__input {
min-width: 140px;
}
.tags-editor__input input {
width: 100%;
padding: 4px 6px;
}
.tags-editor__input--hidden {
display: none;
}
.tags-editor__empty {
font-size: 0.8em;
color: var(--text-muted);
}
.tags-editor__tag div.clickable-icon {
padding: 0;
--icon-size: 12px;
}
.details-form-buttons {
display: flex;
justify-content: space-between;

File diff suppressed because one or more lines are too long