refactor: remove void keywords and standardize code formatting

- Remove unnecessary void keywords from async function calls
- Standardize code formatting with consistent trailing commas
- Add date operation utilities for task date management
- Add TextPromptModal component for user input
- Enhance FluentActionHandlers with improved task update handling
This commit is contained in:
Quorafind 2025-10-20 16:46:55 +08:00
parent 9aaabe716f
commit 7d41359c6e
17 changed files with 790 additions and 188 deletions

View file

@ -1207,7 +1207,7 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
enableIndexer: true, // Enable indexer by default
enableView: true, // Enable view by default
enableInlineEditor: true, // Enable inline editing by default
enableDynamicMetadataPositioning: true, // Enable intelligent metadata positioning by default
enableDynamicMetadataPositioning: false, // Enable intelligent metadata positioning by default
defaultViewMode: "list", // Global default view mode for all views
// Global Filter Defaults

View file

@ -74,7 +74,7 @@ export class ChangelogView extends ItemView {
markdown: "",
sourceUrl: "",
};
void this.render();
this.render();
}
async setContent(content: ChangelogContent) {
@ -87,7 +87,7 @@ export class ChangelogView extends ItemView {
showError(message: string) {
this.isLoading = false;
this.error = message;
void this.render();
this.render();
}
private async render(): Promise<void> {

View file

@ -215,8 +215,8 @@ export class FluentIntegration {
v1Leaves.forEach((leaf) => leaf.detach());
// Close all Fluent views
const v2Leaves = workspace.getLeavesOfType(FLUENT_TASK_VIEW);
v2Leaves.forEach((leaf) => leaf.detach());
const fluentLeaves = workspace.getLeavesOfType(FLUENT_TASK_VIEW);
fluentLeaves.forEach((leaf) => leaf.detach());
// Toggle the setting
if (!this.plugin.settings.fluentView) {

View file

@ -98,7 +98,10 @@ export class TopNavigation extends Component {
this.updateNotificationBadge();
} catch (error) {
console.warn("[FluentTopNavigation] Failed to update notification count:", error);
console.warn(
"[FluentTopNavigation] Failed to update notification count:",
error,
);
}
}
@ -169,7 +172,9 @@ export class TopNavigation extends Component {
cls: "fluent-nav-icon-button",
});
setIcon(settingsBtn, "settings");
this.registerDomEvent(settingsBtn, "click", () => this.onSettingsClick());
this.registerDomEvent(settingsBtn, "click", () =>
this.onSettingsClick(),
);
}
private createViewTab(

View file

@ -5,6 +5,15 @@ import { QuickCaptureModal } from "@/components/features/quick-capture/modals/Qu
import { ConfirmModal } from "@/components/ui/modals/ConfirmModal";
import { createTaskCheckbox } from "@/components/features/task/view/details";
import { emitTaskSelected } from "@/components/features/fluent/events/ui-event";
import { DatePickerModal } from "@/components/ui/date-picker/DatePickerModal";
import { TextPromptModal } from "@/components/ui/modals/TextPromptModal";
import type { CreateTaskArgs } from "@/dataflow/api/WriteAPI";
import {
getExistingDateTypes as getExistingDateTypesFromTask,
postponeDate as postponeDateUtil,
smartPostponeRelatedDates,
TaskDateType,
} from "@/utils/dateOperations";
import { t } from "@/translations/helper";
import { ViewMode } from "../components/FluentTopNavigation";
@ -151,6 +160,7 @@ export class FluentActionHandlers extends Component {
async handleTaskUpdate(
originalTask: Task,
updatedTask: Task,
successMessage?: string,
): Promise<void> {
if (!this.plugin.writeAPI) {
console.error("WriteAPI not available");
@ -176,10 +186,10 @@ export class FluentActionHandlers extends Component {
// Notify about task update
this.onTaskUpdated?.(originalTask.id, updated);
new Notice(t("Task updated"));
new Notice(successMessage ?? t("Task updated"));
} catch (error) {
console.error("Failed to update task:", error);
new Notice("Failed to update task");
new Notice(t("Failed to update task"));
}
}
@ -218,85 +228,464 @@ export class FluentActionHandlers extends Component {
item.onClick(() => {
this.toggleTaskCompletion(task);
});
})
.addItem((item) => {
item.setIcon("square-pen");
item.setTitle(t("Switch status"));
const submenu = item.setSubmenu();
});
// Get unique statuses from taskStatusMarks
const statusMarks = this.plugin.settings.taskStatusMarks;
const uniqueStatuses = new Map<string, string>();
menu.addItem((item) => {
item.setIcon("square-pen");
item.setTitle(t("Switch status"));
const submenu = item.setSubmenu();
// Build a map of unique mark -> status name to avoid duplicates
for (const status of Object.keys(statusMarks)) {
const mark =
statusMarks[status as keyof typeof statusMarks];
if (!Array.from(uniqueStatuses.values()).includes(mark)) {
uniqueStatuses.set(status, mark);
}
// Get unique statuses from taskStatusMarks
const statusMarks = this.plugin.settings.taskStatusMarks;
const uniqueStatuses = new Map<string, string>();
// Build a map of unique mark -> status name to avoid duplicates
for (const status of Object.keys(statusMarks)) {
const mark = statusMarks[status as keyof typeof statusMarks];
if (!Array.from(uniqueStatuses.values()).includes(mark)) {
uniqueStatuses.set(status, mark);
}
}
// Create menu items from unique statuses
for (const [status, mark] of uniqueStatuses) {
submenu.addItem((item) => {
item.titleEl.createEl(
"span",
{
cls: "status-option-checkbox",
},
(el) => {
createTaskCheckbox(mark, task, el);
},
);
item.titleEl.createEl("span", {
cls: "status-option",
text: status,
});
item.onClick(async () => {
const willComplete = this.isCompletedMark(mark);
const updatedTask = {
...task,
status: mark,
completed: willComplete,
};
if (!task.completed && willComplete) {
updatedTask.metadata.completedDate = Date.now();
} else if (task.completed && !willComplete) {
updatedTask.metadata.completedDate = undefined;
}
await this.handleTaskUpdate(task, updatedTask);
});
// Create menu items from unique statuses
for (const [status, mark] of uniqueStatuses) {
submenu.addItem((subItem) => {
subItem.titleEl.createEl(
"span",
{
cls: "status-option-checkbox",
},
(el) => {
createTaskCheckbox(mark, task, el);
},
);
subItem.titleEl.createEl("span", {
cls: "status-option",
text: status,
});
subItem.onClick(async () => {
const willComplete = this.isCompletedMark(mark);
const updatedTask = {
...task,
status: mark,
completed: willComplete,
};
if (!task.completed && willComplete) {
updatedTask.metadata.completedDate = Date.now();
} else if (task.completed && !willComplete) {
updatedTask.metadata.completedDate = undefined;
}
await this.handleTaskUpdate(task, updatedTask);
});
}
})
.addSeparator()
.addItem((item) => {
item.setTitle(t("Edit"));
item.setIcon("pencil");
item.onClick(() => {
this.handleTaskSelection(task);
});
})
.addItem((item) => {
item.setTitle(t("Edit in File"));
item.setIcon("pencil");
item.onClick(() => {
this.editTask(task);
}
});
this.addPriorityMenuItems(menu, task);
this.addDateMenuItems(menu, task);
menu.addSeparator();
menu.addItem((item) => {
item.setTitle(t("Set Project"));
item.setIcon("folder");
item.onClick(() => {
this.setProject(task);
});
});
menu.addItem((item) => {
item.setTitle(t("Add Tags"));
item.setIcon("tag");
item.onClick(() => {
this.addTags(task);
});
});
menu.addSeparator();
menu.addItem((item) => {
item.setTitle(t("Duplicate Task"));
item.setIcon("copy");
item.onClick(() => {
this.duplicateTask(task);
});
});
menu.addSeparator();
menu.addItem((item) => {
item.setTitle(t("Edit"));
item.setIcon("pencil");
item.onClick(() => {
this.handleTaskSelection(task);
});
});
menu.addItem((item) => {
item.setTitle(t("Edit in File"));
item.setIcon("pencil");
item.onClick(() => {
this.editTask(task);
});
});
menu.addSeparator();
menu.addItem((item) => {
item.setTitle(t("Delete Task"));
item.setIcon("trash");
item.onClick(() => {
this.confirmAndDeleteTask(event, task);
});
});
menu.showAtMouseEvent(event);
}
private addPriorityMenuItems(menu: Menu, task: Task): void {
menu.addSeparator();
menu.addItem((item) => {
const subMenu = item
.setTitle(t("Set Priority"))
.setIcon("flag")
.setSubmenu();
const priorityLevels = [
{ level: 5, label: "Highest", icon: "alert-triangle" },
{ level: 4, label: "High", icon: "arrow-up" },
{ level: 3, label: "Medium", icon: "minus" },
{ level: 2, label: "Low", icon: "arrow-down" },
{ level: 1, label: "Lowest", icon: "chevron-down" },
] as const;
priorityLevels.forEach(({ level, label, icon }) => {
subMenu.addItem((subItem) => {
subItem
.setTitle(t(label))
.setIcon(icon)
.setChecked(task.metadata.priority === level)
.onClick(() => {
this.setPriority(task, level);
});
});
})
.addSeparator()
.addItem((item) => {
item.setTitle(t("Delete Task"));
item.setIcon("trash");
});
});
}
private async setPriority(task: Task, priority: number): Promise<void> {
const updatedTask: Task = {
...task,
metadata: {
...task.metadata,
priority,
},
};
await this.handleTaskUpdate(task, updatedTask, t("Priority updated"));
}
private addDateMenuItems(menu: Menu, task: Task): void {
menu.addSeparator();
const existingDates = this.getExistingDateTypes(task);
const dateTypes: TaskDateType[] = [
"dueDate",
"startDate",
"scheduledDate",
];
dateTypes.forEach((dateType) => {
menu.addItem((item) => {
item.setTitle(t(`Set ${this.getDateLabel(dateType)}`));
item.setIcon("calendar");
item.onClick(() => {
this.confirmAndDeleteTask(event, task);
this.openDatePicker(task, dateType);
});
});
menu.showAtMouseEvent(event);
if (existingDates.includes(dateType)) {
this.addPostponeDateMenu(menu, task, dateType);
}
});
}
private addPostponeDateMenu(
menu: Menu,
task: Task,
dateType: TaskDateType,
): void {
menu.addItem((item) => {
const subMenu = item
.setTitle(t(`Postpone ${this.getDateLabel(dateType)}`))
.setIcon("calendar-plus")
.setSubmenu();
const quickOptions = [
{ days: 1, label: "1 day" },
{ days: 2, label: "2 days" },
{ days: 3, label: "3 days" },
{ days: 7, label: "1 week" },
] as const;
quickOptions.forEach(({ days, label }) => {
subMenu.addItem((subItem) => {
subItem.setTitle(t(label)).onClick(() => {
this.postponeDate(task, dateType, days);
});
});
});
subMenu.addSeparator();
subMenu.addItem((subItem) => {
subItem
.setTitle(t("Custom..."))
.setIcon("calendar")
.onClick(() => {
this.openDatePicker(task, dateType, true);
});
});
});
}
private async postponeDate(
task: Task,
dateType: TaskDateType,
offsetDays: number,
): Promise<void> {
try {
const currentTimestamp = task.metadata?.[dateType];
if (typeof currentTimestamp !== "number") {
new Notice(t("No date to postpone"));
return;
}
const newTimestamp = postponeDateUtil(currentTimestamp, offsetDays);
const updatedMetadata = smartPostponeRelatedDates(
task,
dateType,
newTimestamp,
offsetDays,
);
const updatedTask: Task = {
...task,
metadata: updatedMetadata,
};
const action = offsetDays > 0 ? "postponed" : "advanced";
const message = `${this.getDateLabel(dateType)} ${action} by ${Math.abs(offsetDays)} day(s)`;
await this.handleTaskUpdate(task, updatedTask, t(message));
} catch (error) {
console.error(
"[FluentActionHandlers] Failed to postpone date:",
error,
);
new Notice(t("Failed to postpone date"));
}
}
private openDatePicker(
task: Task,
dateType: TaskDateType,
isPostpone: boolean = false,
): void {
const currentDate = task.metadata?.[dateType];
const initialDate =
typeof currentDate === "number"
? new Date(currentDate).toISOString().split("T")[0]
: undefined;
const modal = new DatePickerModal(
this.app,
this.plugin,
initialDate,
this.getDateMark(dateType),
);
modal.onDateSelected = async (dateStr: string | null) => {
if (!dateStr) return;
const newTimestamp = new Date(dateStr).getTime();
if (Number.isNaN(newTimestamp)) {
new Notice(t("Invalid date selected"));
return;
}
if (isPostpone && typeof currentDate === "number") {
const offsetDays = Math.round(
(newTimestamp - currentDate) / (24 * 60 * 60 * 1000),
);
const updatedMetadata = smartPostponeRelatedDates(
task,
dateType,
newTimestamp,
offsetDays,
);
const updatedTask: Task = {
...task,
metadata: updatedMetadata,
};
await this.handleTaskUpdate(
task,
updatedTask,
t(`${this.getDateLabel(dateType)} updated`),
);
} else {
const updatedTask: Task = {
...task,
metadata: {
...task.metadata,
[dateType]: newTimestamp,
},
};
await this.handleTaskUpdate(
task,
updatedTask,
t(`${this.getDateLabel(dateType)} updated`),
);
}
};
modal.open();
}
private async setProject(task: Task): Promise<void> {
const currentProject = task.metadata?.project || "";
const newProject = await new TextPromptModal(this.app, this.plugin, {
title: t("Set Project"),
placeholder: t("Enter project name"),
initialValue: currentProject,
okLabel: t("Save"),
cancelLabel: t("Cancel"),
allowEmpty: true,
suggestion: "project",
}).openAndWait();
if (newProject === null) {
return;
}
const normalized = newProject.trim();
const updatedTask: Task = {
...task,
metadata: {
...task.metadata,
project: normalized || undefined,
},
};
await this.handleTaskUpdate(task, updatedTask, t("Project updated"));
}
private async addTags(task: Task): Promise<void> {
const newTagsInput = await new TextPromptModal(this.app, this.plugin, {
title: t("Add Tags"),
placeholder: t("tag-one, tag-two"),
okLabel: t("Add"),
cancelLabel: t("Cancel"),
suggestion: "tag",
}).openAndWait();
if (newTagsInput === null) {
return;
}
const tags = newTagsInput
.split(",")
.map((tag) => tag.trim())
.filter(Boolean);
if (tags.length === 0) {
return;
}
const existingTags = task.metadata.tags ?? [];
const mergedTags = Array.from(new Set([...existingTags, ...tags]));
const updatedTask: Task = {
...task,
metadata: {
...task.metadata,
tags: mergedTags,
},
};
await this.handleTaskUpdate(task, updatedTask, t("Tags added"));
}
private async duplicateTask(task: Task): Promise<void> {
try {
if (!this.plugin.writeAPI) {
new Notice(t("WriteAPI not available"));
return;
}
const formatDate = (timestamp?: number): string | undefined => {
if (typeof timestamp !== "number") {
return undefined;
}
return new Date(timestamp).toISOString().split("T")[0];
};
const tags = task.metadata.tags ?? [];
const duplicateArgs: CreateTaskArgs = {
content: `${task.content} (Copy)`,
filePath: task.filePath,
completed: false,
tags: tags.length > 0 ? [...tags] : undefined,
project: task.metadata.project,
context: task.metadata.context,
priority: task.metadata.priority,
startDate: formatDate(task.metadata.startDate),
dueDate: formatDate(task.metadata.dueDate),
};
const result = await this.plugin.writeAPI.createTask(duplicateArgs);
if (result.success) {
new Notice(t("Task duplicated"));
this.onTaskUpdated?.(task.id, task);
} else {
throw new Error(result.error || "Failed to duplicate task");
}
} catch (error) {
console.error(
"[FluentActionHandlers] Failed to duplicate task:",
error,
);
new Notice(t("Failed to duplicate task"));
}
}
private getExistingDateTypes(task: Task): TaskDateType[] {
return getExistingDateTypesFromTask(task);
}
private getDateLabel(dateType: TaskDateType): string {
switch (dateType) {
case "startDate":
return "Start Date";
case "scheduledDate":
return "Scheduled Date";
case "dueDate":
default:
return "Due Date";
}
}
private getDateMark(dateType: TaskDateType): string {
switch (dateType) {
case "scheduledDate":
return "⏳";
case "startDate":
return "🚀";
case "dueDate":
default:
return "📅";
}
}
/**

View file

@ -128,7 +128,7 @@ export class HabitList {
.onClick(() => {
this.deleteHabit(habit);
});
}
},
);
});
}
@ -148,7 +148,7 @@ export class HabitList {
// 更新已有习惯
const habits = this.plugin.settings.habit.habits;
const index = habits.findIndex(
(h) => h.id === habitData.id
(h) => h.id === habitData.id,
);
if (index > -1) {
habits[index] = updatedHabit;
@ -161,10 +161,10 @@ export class HabitList {
// 保存设置并刷新显示
this.plugin.saveSettings();
// 重新初始化习惯索引,通知视图刷新
void this.plugin.habitManager?.initializeHabits();
this.plugin.habitManager?.initializeHabits();
this.render();
new Notice(habitData ? t("Habit updated") : t("Habit added"));
}
},
);
dialog.open();
@ -180,7 +180,7 @@ export class HabitList {
content.setText(
t(`Are you sure you want to delete the habit `) +
`"${habitName}"?` +
t("This action cannot be undone.")
t("This action cannot be undone."),
);
modal.contentEl.createDiv(
@ -209,19 +209,19 @@ export class HabitList {
const habits = this.plugin.settings.habit.habits;
const index = habits.findIndex(
(h) => h.id === habit.id
(h) => h.id === habit.id,
);
if (index > -1) {
habits.splice(index, 1);
this.plugin.saveSettings();
// 重新初始化习惯索引,通知视图刷新
void this.plugin.habitManager?.initializeHabits();
this.plugin.habitManager?.initializeHabits();
this.render();
new Notice(t("Habit deleted"));
}
modal.close();
});
}
},
);
modal.open();

View file

@ -23,7 +23,7 @@ export class FileFilterRuleEditorModal extends Modal {
constructor(
app: App,
plugin: TaskProgressBarPlugin,
options: FileFilterRuleEditorModalOptions = {}
options: FileFilterRuleEditorModalOptions = {},
) {
super(app);
this.plugin = plugin;
@ -38,7 +38,7 @@ export class FileFilterRuleEditorModal extends Modal {
contentEl.createEl("h2", { text: t("Edit File Filter Rules") });
contentEl.createEl("p", {
text: t(
"Configure which files, folders, or patterns are included during task indexing."
"Configure which files, folders, or patterns are included during task indexing.",
),
cls: "setting-item-description",
});
@ -47,14 +47,14 @@ export class FileFilterRuleEditorModal extends Modal {
.setName(t("Filter Mode"))
.setDesc(
t(
"Whitelist: include only specified paths. Blacklist: exclude specified paths."
)
"Whitelist: include only specified paths. Blacklist: exclude specified paths.",
),
)
.addDropdown((dropdown) =>
dropdown
.addOption(
FilterMode.WHITELIST,
t("Whitelist (Include only)")
t("Whitelist (Include only)"),
)
.addOption(FilterMode.BLACKLIST, t("Blacklist (Exclude)"))
.setValue(this.plugin.settings.fileFilter.mode)
@ -62,7 +62,7 @@ export class FileFilterRuleEditorModal extends Modal {
this.plugin.settings.fileFilter.mode = value;
await this.plugin.saveSettings();
this.updateStats();
})
}),
);
const actionSetting = new Setting(contentEl);
@ -74,18 +74,18 @@ export class FileFilterRuleEditorModal extends Modal {
.setButtonText(t("Add File Rule"))
.setCta()
.onClick(() => {
void this.addRule("file");
})
this.addRule("file");
}),
)
.addButton((button) =>
button.setButtonText(t("Add Folder Rule")).onClick(() => {
void this.addRule("folder");
})
this.addRule("folder");
}),
)
.addButton((button) =>
button.setButtonText(t("Add Pattern Rule")).onClick(() => {
void this.addRule("pattern");
})
this.addRule("pattern");
}),
);
this.rulesContainer = contentEl.createDiv({
@ -100,14 +100,14 @@ export class FileFilterRuleEditorModal extends Modal {
button
.setButtonText(t("Done"))
.setCta()
.onClick(() => this.close())
.onClick(() => this.close()),
);
this.renderRules();
this.updateStats();
if (this.options.autoAddRuleType) {
void this.addRule(this.options.autoAddRuleType).then((rule) => {
this.addRule(this.options.autoAddRuleType).then((rule) => {
this.pendingRule = rule;
});
}
@ -116,11 +116,11 @@ export class FileFilterRuleEditorModal extends Modal {
onClose() {
if (this.pendingRule && !this.pendingRule.path.trim()) {
const index = this.plugin.settings.fileFilter.rules.indexOf(
this.pendingRule
this.pendingRule,
);
if (index !== -1) {
this.plugin.settings.fileFilter.rules.splice(index, 1);
void this.plugin.saveSettings();
this.plugin.saveSettings();
}
}
@ -130,7 +130,7 @@ export class FileFilterRuleEditorModal extends Modal {
}
private async addRule(
type: FileFilterRule["type"]
type: FileFilterRule["type"],
): Promise<FileFilterRule> {
const newRule: FileFilterRule = {
type,
@ -207,8 +207,8 @@ export class FileFilterRuleEditorModal extends Modal {
rule.type === "pattern"
? "*.tmp, temp/*"
: rule.type === "folder"
? "path/to/folder"
: "path/to/file.md",
? "path/to/folder"
: "path/to/file.md",
});
if (rule.type === "folder") {
@ -216,7 +216,7 @@ export class FileFilterRuleEditorModal extends Modal {
this.plugin.app,
pathInput,
this.plugin,
"single"
"single",
);
} else if (rule.type === "file") {
new FileSuggest(pathInput, this.plugin, (file) => {
@ -269,7 +269,7 @@ export class FileFilterRuleEditorModal extends Modal {
container.empty();
const activeRules = this.plugin.settings.fileFilter.rules.filter(
(rule) => rule.enabled
(rule) => rule.enabled,
).length;
const stats = [

View file

@ -199,7 +199,7 @@ export class FileNameInput {
if (updated) {
this.plugin.settings.quickCapture.fileNameTemplates = templates;
if (typeof this.plugin.saveSettings === "function") {
void this.plugin.saveSettings();
this.plugin.saveSettings();
}
}

View file

@ -601,7 +601,7 @@ export class QuickCaptureModal extends BaseQuickCaptureModal {
} else {
if (this.previewPlainEl) {
const snapshot = this.capturedContent;
void this.computeFileModePreviewContent(snapshot).then(
this.computeFileModePreviewContent(snapshot).then(
(finalContent) => {
if (
this.previewPlainEl &&

View file

@ -0,0 +1,122 @@
import { App, Modal, Notice, Setting } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { ProjectSuggest, TagSuggest } from "@/components/ui/inputs/AutoComplete";
import { t } from "@/translations/helper";
export type TextPromptSuggestion = "project" | "tag";
export interface TextPromptOptions {
title: string;
placeholder?: string;
initialValue?: string;
okLabel?: string;
cancelLabel?: string;
allowEmpty?: boolean;
suggestion?: TextPromptSuggestion;
}
export class TextPromptModal extends Modal {
private readonly allowEmpty: boolean;
private currentValue: string;
private resolvePromise: ((value: string | null) => void) | null = null;
private resolved = false;
private inputEl: HTMLInputElement | null = null;
private inputKeydownHandler?: (event: KeyboardEvent) => void;
constructor(
app: App,
private readonly plugin: TaskProgressBarPlugin,
private readonly options: TextPromptOptions,
) {
super(app);
this.currentValue = options.initialValue ?? "";
this.allowEmpty = options.allowEmpty ?? false;
}
openAndWait(): Promise<string | null> {
return new Promise((resolve) => {
this.resolvePromise = resolve;
this.open();
});
}
onOpen(): void {
this.titleEl.setText(this.options.title);
const { contentEl } = this;
contentEl.empty();
const inputSetting = new Setting(contentEl);
inputSetting.setName(t("Set value")).addText((text) => {
text.setPlaceholder(this.options.placeholder ?? "");
text.setValue(this.currentValue);
text.onChange((value) => {
this.currentValue = value;
});
this.inputEl = text.inputEl;
});
if (this.inputEl) {
if (this.options.suggestion === "project") {
new ProjectSuggest(this.app, this.inputEl, this.plugin);
} else if (this.options.suggestion === "tag") {
new TagSuggest(this.app, this.inputEl, this.plugin);
}
this.inputKeydownHandler = (event: KeyboardEvent) => {
if (event.key === "Enter" && !event.isComposing) {
event.preventDefault();
this.handleSubmit();
}
};
this.inputEl.addEventListener("keydown", this.inputKeydownHandler);
}
const buttonSetting = new Setting(contentEl);
buttonSetting.addButton((btn) => {
btn.setButtonText(this.options.cancelLabel ?? t("Cancel"));
btn.onClick(() => {
this.finish(null);
});
});
buttonSetting.addButton((btn) => {
btn.setButtonText(this.options.okLabel ?? t("Save")).setCta();
btn.onClick(() => {
this.handleSubmit();
});
});
window.setTimeout(() => {
if (this.inputEl) {
this.inputEl.focus();
this.inputEl.select();
}
}, 50);
}
onClose(): void {
if (this.inputEl && this.inputKeydownHandler) {
this.inputEl.removeEventListener("keydown", this.inputKeydownHandler);
}
this.contentEl.empty();
if (!this.resolved) {
this.resolved = true;
this.resolvePromise?.(null);
}
}
private handleSubmit(): void {
const trimmedValue = this.currentValue.trim();
if (!this.allowEmpty && trimmedValue.length === 0) {
new Notice(t("Value cannot be empty"));
return;
}
this.finish(trimmedValue);
}
private finish(value: string | null): void {
if (this.resolved) return;
this.resolved = true;
this.resolvePromise?.(value);
this.close();
}
}

View file

@ -60,7 +60,7 @@ export class FileSource {
constructor(
private app: App,
initialConfig?: Partial<FileSourceConfiguration>,
private fileFilterManager?: FileFilterManager
private fileFilterManager?: FileFilterManager,
) {
this.config = new FileSourceConfig(initialConfig);
}
@ -94,7 +94,7 @@ export class FileSource {
console.log(
`[FileSource] Initialized with strategies: ${this.config
.getEnabledStrategies()
.join(", ")}`
.join(", ")}`,
);
}
@ -108,7 +108,7 @@ export class FileSource {
if (payload?.path) {
this.handleFileUpdate(payload.path, payload.reason);
}
})
}),
);
// Subscribe to more granular events if they exist
@ -121,8 +121,8 @@ export class FileSource {
if (payload?.path) {
this.handleFileMetadataChange(payload.path);
}
}
)
},
),
);
this.eventRefs.push(
@ -133,8 +133,8 @@ export class FileSource {
if (payload?.path) {
this.handleFileContentChange(payload.path);
}
}
)
},
),
);
}
@ -162,7 +162,7 @@ export class FileSource {
} catch (error) {
console.error(
`[FileSource] Error processing file update for ${filePath}:`,
error
error,
);
}
}, this.DEBOUNCE_DELAY);
@ -191,7 +191,7 @@ export class FileSource {
*/
private async processFileUpdate(
filePath: string,
reason: string
reason: string,
): Promise<void> {
if (reason === "delete") {
await this.removeFileTask(filePath);
@ -232,12 +232,12 @@ export class FileSource {
return this.evaluateRecognitionStrategies(
filePath,
fileContent,
fileCache
fileCache,
);
} catch (error) {
console.error(
`[FileSource] Error reading file ${filePath}:`,
error
error,
);
return false;
}
@ -249,10 +249,10 @@ export class FileSource {
private evaluateRecognitionStrategies(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
fileCache: CachedMetadata | null,
): boolean {
const config = this.config.getConfig();
const {recognitionStrategies} = config;
const { recognitionStrategies } = config;
// Check metadata strategy
if (recognitionStrategies.metadata.enabled) {
@ -261,7 +261,7 @@ export class FileSource {
filePath,
fileContent,
fileCache,
recognitionStrategies.metadata
recognitionStrategies.metadata,
)
) {
return true;
@ -275,7 +275,7 @@ export class FileSource {
filePath,
fileContent,
fileCache,
recognitionStrategies.tags
recognitionStrategies.tags,
)
) {
return true;
@ -289,7 +289,7 @@ export class FileSource {
filePath,
fileContent,
fileCache,
recognitionStrategies.templates
recognitionStrategies.templates,
)
) {
return true;
@ -303,7 +303,7 @@ export class FileSource {
filePath,
fileContent,
fileCache,
recognitionStrategies.paths
recognitionStrategies.paths,
)
) {
return true;
@ -320,17 +320,17 @@ export class FileSource {
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: any
config: any,
): boolean {
if (!fileCache?.frontmatter) return false;
const {taskFields, requireAllFields} = config;
const { taskFields, requireAllFields } = config;
const frontmatter = fileCache.frontmatter;
const matchingFields = taskFields.filter(
(field: string) =>
frontmatter.hasOwnProperty(field) &&
frontmatter[field] !== undefined
frontmatter[field] !== undefined,
);
if (requireAllFields) {
@ -347,11 +347,11 @@ export class FileSource {
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: any
config: any,
): boolean {
if (!fileCache?.tags) return false;
const {taskTags, matchMode} = config;
const { taskTags, matchMode } = config;
const fileTags = fileCache.tags.map((tag) => tag.tag);
return taskTags.some((taskTag: string) => {
@ -377,7 +377,7 @@ export class FileSource {
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: TemplateRecognitionConfig
config: TemplateRecognitionConfig,
): boolean {
if (
!config.enabled ||
@ -415,7 +415,7 @@ export class FileSource {
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: PathRecognitionConfig
config: PathRecognitionConfig,
): boolean {
if (
!config.enabled ||
@ -436,7 +436,7 @@ export class FileSource {
case "prefix":
if (normalizedPath.startsWith(normalizedPattern)) {
console.log(
`[FileSource] Path matches prefix pattern: ${pattern} for ${filePath}`
`[FileSource] Path matches prefix pattern: ${pattern} for ${filePath}`,
);
return true;
}
@ -447,14 +447,14 @@ export class FileSource {
const regex = new RegExp(normalizedPattern);
if (regex.test(normalizedPath)) {
console.log(
`[FileSource] Path matches regex pattern: ${pattern} for ${filePath}`
`[FileSource] Path matches regex pattern: ${pattern} for ${filePath}`,
);
return true;
}
} catch (e) {
console.warn(
`[FileSource] Invalid regex pattern: ${pattern}`,
e
e,
);
}
break;
@ -464,7 +464,7 @@ export class FileSource {
this.matchGlobPattern(normalizedPath, normalizedPattern)
) {
console.log(
`[FileSource] Path matches glob pattern: ${pattern} for ${filePath}`
`[FileSource] Path matches glob pattern: ${pattern} for ${filePath}`,
);
return true;
}
@ -501,7 +501,7 @@ export class FileSource {
} catch (e) {
console.warn(
`[FileSource] Failed to compile glob pattern: ${pattern}`,
e
e,
);
return false;
}
@ -511,7 +511,7 @@ export class FileSource {
* Create a new file task
*/
async createFileTask(
filePath: string
filePath: string,
): Promise<Task<FileSourceTaskMetadata> | null> {
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
if (!file) return null;
@ -523,7 +523,7 @@ export class FileSource {
const fileTask = await this.buildFileTask(
filePath,
fileContent,
fileCache
fileCache,
);
if (!fileTask) return null;
@ -540,7 +540,7 @@ export class FileSource {
} catch (error) {
console.error(
`[FileSource] Error creating file task for ${filePath}:`,
error
error,
);
return null;
}
@ -550,7 +550,7 @@ export class FileSource {
* Update an existing file task
*/
async updateFileTask(
filePath: string
filePath: string,
): Promise<Task<FileSourceTaskMetadata> | null> {
// For Phase 1, just recreate the task
// Phase 2 will add smart update detection
@ -570,7 +570,7 @@ export class FileSource {
// Update statistics
this.stats.trackedFileCount = Math.max(
0,
this.stats.trackedFileCount - 1
this.stats.trackedFileCount - 1,
);
// Emit removal event
@ -592,7 +592,7 @@ export class FileSource {
private async buildFileTask(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
fileCache: CachedMetadata | null,
): Promise<Task<FileSourceTaskMetadata> | null> {
const config = this.config.getConfig();
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
@ -602,7 +602,7 @@ export class FileSource {
const strategy = this.getMatchingStrategy(
filePath,
fileContent,
fileCache
fileCache,
);
if (!strategy) return null;
@ -610,7 +610,7 @@ export class FileSource {
const content = this.generateTaskContent(
filePath,
fileContent,
fileCache
fileCache,
);
const safeContent =
typeof content === "string"
@ -622,7 +622,7 @@ export class FileSource {
filePath,
fileContent,
fileCache,
strategy
strategy,
);
// Create the file task
@ -658,7 +658,7 @@ export class FileSource {
private getMatchingStrategy(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
fileCache: CachedMetadata | null,
): { name: RecognitionStrategy; criteria: string } | null {
const config = this.config.getConfig();
@ -668,10 +668,10 @@ export class FileSource {
filePath,
fileContent,
fileCache,
config.recognitionStrategies.metadata
config.recognitionStrategies.metadata,
)
) {
return {name: "metadata", criteria: "frontmatter"};
return { name: "metadata", criteria: "frontmatter" };
}
if (
@ -680,10 +680,10 @@ export class FileSource {
filePath,
fileContent,
fileCache,
config.recognitionStrategies.tags
config.recognitionStrategies.tags,
)
) {
return {name: "tag", criteria: "file-tags"};
return { name: "tag", criteria: "file-tags" };
}
// Check path strategy
@ -693,7 +693,7 @@ export class FileSource {
filePath,
fileContent,
fileCache,
config.recognitionStrategies.paths
config.recognitionStrategies.paths,
)
) {
return {
@ -715,7 +715,7 @@ export class FileSource {
fileCache?.frontmatter?.template === templatePath ||
fileCache?.frontmatter?.templateFile === templatePath
);
}
},
);
if (matchesTemplate) {
@ -735,7 +735,7 @@ export class FileSource {
private generateTaskContent(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
fileCache: CachedMetadata | null,
): string {
const config = this.config.getConfig().fileTaskProperties;
const fileName = filePath.split("/").pop() || filePath;
@ -805,7 +805,7 @@ export class FileSource {
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
strategy: { name: RecognitionStrategy; criteria: string }
strategy: { name: RecognitionStrategy; criteria: string },
): Partial<FileSourceTaskMetadata> {
const config = this.config.getConfig();
const frontmatter = fileCache?.frontmatter || {};
@ -847,21 +847,20 @@ export class FileSource {
: config.fileTaskProperties.defaultStatus;
if (rawStatus && status !== rawStatus) {
console.log(
`[FileSource] Mapped status '${rawStatus}' to '${status}' for ${filePath}`
`[FileSource] Mapped status '${rawStatus}' to '${status}' for ${filePath}`,
);
}
// TODO: Future enhancement - read frontmatter.repeat and map into FileSourceTaskMetadata (e.g., as recurrence)
// TODO: Future enhancement - read frontmatter.repeat and map into FileSourceTaskMetadata (e.g., as recurrence)
// Extract standard task metadata
const metadata: Partial<FileSourceTaskMetadata> = {
dueDate: this.parseDate(frontmatter.dueDate || frontmatter.due),
startDate: this.parseDate(
frontmatter.startDate || frontmatter.start
frontmatter.startDate || frontmatter.start,
),
scheduledDate: this.parseDate(
frontmatter.scheduledDate || frontmatter.scheduled
frontmatter.scheduledDate || frontmatter.scheduled,
),
priority:
frontmatter.priority ||
@ -919,7 +918,7 @@ export class FileSource {
*/
private updateFileTaskCache(
filePath: string,
task: Task<FileSourceTaskMetadata>
task: Task<FileSourceTaskMetadata>,
): void {
const frontmatterHash = this.generateFrontmatterHash(filePath);
@ -944,7 +943,7 @@ export class FileSource {
// Simple hash of frontmatter JSON
const frontmatterStr = JSON.stringify(
fileCache.frontmatter,
Object.keys(fileCache.frontmatter).sort()
Object.keys(fileCache.frontmatter).sort(),
);
return this.simpleHash(frontmatterStr);
}
@ -967,7 +966,7 @@ export class FileSource {
*/
private shouldUpdateFileTask(
filePath: string,
changeType: "metadata" | "content"
changeType: "metadata" | "content",
): boolean {
// Simple check for Phase 1 - always update if file is tracked
return this.fileTaskCache.has(filePath);
@ -978,7 +977,7 @@ export class FileSource {
*/
private updateStatistics(
strategy: RecognitionStrategy,
delta: number
delta: number,
): void {
this.stats.recognitionBreakdown[strategy] += delta;
this.stats.trackedFileCount += delta;
@ -991,7 +990,7 @@ export class FileSource {
*/
private emitFileTaskUpdate(
action: "created" | "updated" | "removed",
task: Task<FileSourceTaskMetadata>
task: Task<FileSourceTaskMetadata>,
): void {
const seq = Seq.next();
this.lastUpdateSeq = seq;
@ -1025,7 +1024,7 @@ export class FileSource {
if (this.fileFilterManager) {
const include = this.fileFilterManager.shouldIncludePath(
filePath,
"file"
"file",
);
if (!include) return false;
}
@ -1041,7 +1040,7 @@ export class FileSource {
const mdFiles = this.app.vault.getMarkdownFiles();
console.log(
`[FileSource] Found ${mdFiles.length} markdown files to check`
`[FileSource] Found ${mdFiles.length} markdown files to check`,
);
let scannedCount = 0;
@ -1053,7 +1052,7 @@ export class FileSource {
relevantCount++;
try {
const shouldBeTask = await this.shouldCreateFileTask(
file.path
file.path,
);
if (shouldBeTask) {
const task = await this.createFileTask(file.path);
@ -1065,40 +1064,40 @@ export class FileSource {
} catch (error) {
console.error(
`[FileSource] Error scanning ${file.path}:`,
error
error,
);
}
}
}
console.log(
`[FileSource] Initial scan complete: ${mdFiles.length} total files, ${relevantCount} relevant, ${scannedCount} scanned, ${taskCount} file tasks created`
`[FileSource] Initial scan complete: ${mdFiles.length} total files, ${relevantCount} relevant, ${scannedCount} scanned, ${taskCount} file tasks created`,
);
if (taskCount === 0 && relevantCount > 0) {
console.log(
`[FileSource] No file tasks created. Check if your files match the configured recognition strategies:`
`[FileSource] No file tasks created. Check if your files match the configured recognition strategies:`,
);
const config = this.config.getConfig();
if (config.recognitionStrategies.metadata.enabled) {
console.log(
`[FileSource] - Metadata strategy: requires frontmatter with fields: ${config.recognitionStrategies.metadata.taskFields.join(
", "
)}`
", ",
)}`,
);
}
if (config.recognitionStrategies.tags.enabled) {
console.log(
`[FileSource] - Tag strategy: requires tags: ${config.recognitionStrategies.tags.taskTags.join(
", "
)}`
", ",
)}`,
);
}
if (config.recognitionStrategies.paths.enabled) {
console.log(
`[FileSource] - Path strategy: requires files in paths: ${config.recognitionStrategies.paths.taskPaths.join(
", "
)}`
", ",
)}`,
);
}
}
@ -1116,7 +1115,7 @@ export class FileSource {
if (newConfig.enabled && !this.isInitialized) {
// FileSource is being enabled
void this.initialize();
this.initialize();
return;
}
@ -1129,7 +1128,7 @@ export class FileSource {
* Get current statistics
*/
getStats(): FileSourceStats {
return {...this.stats};
return { ...this.stats };
}
/**
@ -1151,14 +1150,14 @@ export class FileSource {
* Sync FileSource status mapping from plugin TaskStatus settings
*/
public syncStatusMappingFromSettings(
taskStatuses: Record<string, string>
taskStatuses: Record<string, string>,
): void {
try {
this.config.syncWithTaskStatuses(taskStatuses);
} catch (e) {
console.warn(
"[FileSource] Failed to sync status mapping from settings",
e
e,
);
}
}
@ -1216,7 +1215,7 @@ export class FileSource {
this.stats = {
initialized: false,
trackedFileCount: 0,
recognitionBreakdown: {metadata: 0, tag: 0, template: 0, path: 0},
recognitionBreakdown: { metadata: 0, tag: 0, template: 0, path: 0 },
lastUpdate: 0,
lastUpdateSeq: 0,
};

View file

@ -61,7 +61,7 @@ export class IcsSource {
// Listen for ICS cache updates
this.app.workspace.on("ics-cache-updated" as any, () => {
console.log("[IcsSource] ICS cache updated, reloading events...");
void this.loadAndEmitIcsEvents();
this.loadAndEmitIcsEvents();
});
// Listen for ICS configuration changes

View file

@ -597,7 +597,7 @@ export default class TaskProgressBarPlugin extends Plugin {
}
const isBeta = targetVersion.toLowerCase().includes("beta");
void this.changelogManager.openChangelog(targetVersion, isBeta);
this.changelogManager.openChangelog(targetVersion, isBeta);
},
});
}
@ -1781,7 +1781,7 @@ export default class TaskProgressBarPlugin extends Plugin {
}
const isBeta = manifestVersion.toLowerCase().includes("beta");
void this.changelogManager.openChangelog(manifestVersion, isBeta);
this.changelogManager.openChangelog(manifestVersion, isBeta);
} catch (error) {
console.error("[TG] Failed to show changelog:", error);
}

View file

@ -1328,7 +1328,7 @@
}
/* Section shared styles */
.section-title {
.onboarding-section-title {
font-size: 1.25em;
font-weight: 600;
color: var(--text-normal);

View file

@ -103,6 +103,7 @@
/* Main container adjustments */
.tg-settings-main-container {
width: 100%;
position: relative;
}
/* Ensure search results appear below the header bar */

View file

@ -0,0 +1,66 @@
import { addDays } from "date-fns";
import { StandardTaskMetadata, Task } from "@/types/task";
export type TaskDateType = "dueDate" | "startDate" | "scheduledDate";
/**
* Calculate a new timestamp by shifting days relative to the current value.
*/
export function postponeDate(currentTimestamp: number, offsetDays: number): number {
if (!offsetDays) {
return currentTimestamp;
}
if (typeof currentTimestamp !== "number" || Number.isNaN(currentTimestamp)) {
return currentTimestamp;
}
return addDays(new Date(currentTimestamp), offsetDays).getTime();
}
/**
* Update task metadata while keeping related date fields consistent.
*/
export function smartPostponeRelatedDates(
task: Task,
dateType: TaskDateType,
newTimestamp: number,
offsetDays: number,
): StandardTaskMetadata {
const metadata: StandardTaskMetadata = {
...(task.metadata as StandardTaskMetadata),
[dateType]: newTimestamp,
};
if (!offsetDays) {
return metadata;
}
if (
dateType === "dueDate" &&
typeof task.metadata?.startDate === "number" &&
task.metadata.startDate > newTimestamp
) {
metadata.startDate = postponeDate(task.metadata.startDate, offsetDays);
}
if (
dateType === "dueDate" &&
typeof task.metadata?.scheduledDate === "number" &&
task.metadata.scheduledDate > newTimestamp
) {
metadata.scheduledDate = postponeDate(task.metadata.scheduledDate, offsetDays);
}
return metadata;
}
/**
* Return the list of date fields that are currently set on the task.
*/
export function getExistingDateTypes(task: Task): TaskDateType[] {
const metadata = task.metadata ?? {};
const dateTypes: TaskDateType[] = ["dueDate", "startDate", "scheduledDate"];
return dateTypes.filter((type) => typeof (metadata as StandardTaskMetadata)[type] === "number");
}

File diff suppressed because one or more lines are too long