mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(date): add time component support for task dates
- Extend regex patterns to support HH:mm time format in date fields - Add date-utils with isDateOnly() and formatDateSmart() helpers - Enhance time-parsing-service to normalize parsed dates with time - Update WriteAPI and executors to handle date-time patterns
This commit is contained in:
parent
42b466a826
commit
568dcbefa6
6 changed files with 358 additions and 175 deletions
|
|
@ -2,12 +2,17 @@
|
|||
const TASK_REGEX = /^(([\s>]*)?(-|\d+\.|\*|\+)\s\[([^\[\]]{1})\])\s+(.*)$/m;
|
||||
|
||||
// --- Emoji/Tasks Style Regexes ---
|
||||
const EMOJI_START_DATE_REGEX = /🛫\s*(\d{4}-\d{2}-\d{2})/;
|
||||
const EMOJI_COMPLETED_DATE_REGEX = /✅\s*(\d{4}-\d{2}-\d{2})/;
|
||||
const EMOJI_DUE_DATE_REGEX = /(?:📅|🗓️|📆)\s*(\d{4}-\d{2}-\d{2})/;
|
||||
const EMOJI_SCHEDULED_DATE_REGEX = /⏳\s*(\d{4}-\d{2}-\d{2})/;
|
||||
const EMOJI_CREATED_DATE_REGEX = /➕\s*(\d{4}-\d{2}-\d{2})/;
|
||||
const EMOJI_CANCELLED_DATE_REGEX = /❌\s*(\d{4}-\d{2}-\d{2})/;
|
||||
const EMOJI_START_DATE_REGEX = /🛫\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/;
|
||||
const EMOJI_COMPLETED_DATE_REGEX =
|
||||
/✅\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/;
|
||||
const EMOJI_DUE_DATE_REGEX =
|
||||
/(?:📅|🗓️|📆)\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/;
|
||||
const EMOJI_SCHEDULED_DATE_REGEX =
|
||||
/⏳\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/;
|
||||
const EMOJI_CREATED_DATE_REGEX =
|
||||
/➕\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/;
|
||||
const EMOJI_CANCELLED_DATE_REGEX =
|
||||
/❌\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/;
|
||||
const EMOJI_ID_REGEX = /🆔\s*([^\s]+)/;
|
||||
const EMOJI_DEPENDS_ON_REGEX = /⛔\s*([^\s]+)/;
|
||||
const EMOJI_ON_COMPLETION_REGEX = /🏁\s*([^\s]+)/;
|
||||
|
|
@ -28,7 +33,7 @@ export type MetadataFormat = "emoji" | "dataview";
|
|||
// Function to get configurable project prefix based on format
|
||||
export const getProjectPrefix = (
|
||||
prefix?: string,
|
||||
format: MetadataFormat = "emoji"
|
||||
format: MetadataFormat = "emoji",
|
||||
): string => {
|
||||
const projectPrefix = prefix || "project";
|
||||
if (format === "dataview") {
|
||||
|
|
@ -42,7 +47,7 @@ export const getProjectPrefix = (
|
|||
// Special handling: emoji format always uses @, dataview format uses configurable prefix
|
||||
export const getContextPrefix = (
|
||||
prefix?: string,
|
||||
format: MetadataFormat = "emoji"
|
||||
format: MetadataFormat = "emoji",
|
||||
): string => {
|
||||
if (format === "dataview") {
|
||||
const contextPrefix = prefix || "context";
|
||||
|
|
@ -56,7 +61,7 @@ export const getContextPrefix = (
|
|||
// Function to get configurable area prefix based on format
|
||||
export const getAreaPrefix = (
|
||||
prefix?: string,
|
||||
format: MetadataFormat = "emoji"
|
||||
format: MetadataFormat = "emoji",
|
||||
): string => {
|
||||
const areaPrefix = prefix || "area";
|
||||
if (format === "dataview") {
|
||||
|
|
@ -85,13 +90,18 @@ export const createDataviewContextRegex = (prefix?: string): RegExp => {
|
|||
};
|
||||
|
||||
// --- Dataview Style Regexes ---
|
||||
const DV_START_DATE_REGEX = /\[(?:start|🛫)::\s*(\d{4}-\d{2}-\d{2})\]/i;
|
||||
const DV_START_DATE_REGEX =
|
||||
/\[(?:start|🛫)::\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)\]/i;
|
||||
const DV_COMPLETED_DATE_REGEX =
|
||||
/\[(?:completion|✅)::\s*(\d{4}-\d{2}-\d{2})\]/i;
|
||||
const DV_DUE_DATE_REGEX = /\[(?:due|🗓️)::\s*(\d{4}-\d{2}-\d{2})\]/i;
|
||||
const DV_SCHEDULED_DATE_REGEX = /\[(?:scheduled|⏳)::\s*(\d{4}-\d{2}-\d{2})\]/i;
|
||||
const DV_CREATED_DATE_REGEX = /\[(?:created|➕)::\s*(\d{4}-\d{2}-\d{2})\]/i;
|
||||
const DV_CANCELLED_DATE_REGEX = /\[(?:cancelled|❌)::\s*(\d{4}-\d{2}-\d{2})\]/i;
|
||||
/\[(?:completion|✅)::\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)\]/i;
|
||||
const DV_DUE_DATE_REGEX =
|
||||
/\[(?:due|🗓️)::\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)\]/i;
|
||||
const DV_SCHEDULED_DATE_REGEX =
|
||||
/\[(?:scheduled|⏳)::\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)\]/i;
|
||||
const DV_CREATED_DATE_REGEX =
|
||||
/\[(?:created|➕)::\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)\]/i;
|
||||
const DV_CANCELLED_DATE_REGEX =
|
||||
/\[(?:cancelled|❌)::\s*(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)\]/i;
|
||||
const DV_ID_REGEX = /\[(?:id|🆔)::\s*([^\]]+)\]/i;
|
||||
const DV_DEPENDS_ON_REGEX = /\[(?:dependsOn|⛔)::\s*([^\]]+)\]/i;
|
||||
const DV_ON_COMPLETION_REGEX = /\[(?:onCompletion|🏁)::\s*([^\]]+)\]/i;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { CanvasTaskUpdater } from "@/parsers/canvas-task-updater";
|
|||
import { rrulestr } from "rrule";
|
||||
import { EMOJI_TAG_REGEX, TOKEN_CONTEXT_REGEX } from "@/common/regex-define";
|
||||
import { BulkOperationResult } from "@/types/selection";
|
||||
import { formatDateSmart, isDateOnly } from "@/utils/date/date-utils";
|
||||
|
||||
/**
|
||||
* Arguments for creating a task
|
||||
|
|
@ -96,9 +97,7 @@ export class WriteAPI {
|
|||
}
|
||||
|
||||
private enqueueWrite<T>(operation: () => Promise<T>): Promise<T> {
|
||||
const run = this.writeQueue
|
||||
.catch(() => undefined)
|
||||
.then(operation);
|
||||
const run = this.writeQueue.catch(() => undefined).then(operation);
|
||||
|
||||
this.writeQueue = run.then(
|
||||
() => undefined,
|
||||
|
|
@ -116,9 +115,7 @@ export class WriteAPI {
|
|||
status?: string;
|
||||
completed?: boolean;
|
||||
}): Promise<{ success: boolean; task?: Task; error?: string }> {
|
||||
return this.enqueueWrite(() =>
|
||||
this.performUpdateTaskStatus(args),
|
||||
);
|
||||
return this.enqueueWrite(() => this.performUpdateTaskStatus(args));
|
||||
}
|
||||
|
||||
private async performUpdateTaskStatus(args: {
|
||||
|
|
@ -537,7 +534,10 @@ export class WriteAPI {
|
|||
// Remove existing completion markers first
|
||||
taskLine = taskLine
|
||||
.replace(/\s*\[completion::\s*[^\]]+\]/i, "")
|
||||
.replace(/\s*✅\s*\d{4}-\d{2}-\d{2}/, "");
|
||||
.replace(
|
||||
/\s*✅\s*\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?/,
|
||||
"",
|
||||
);
|
||||
if (md.completedDate) {
|
||||
const dateStr =
|
||||
typeof md.completedDate === "number"
|
||||
|
|
@ -889,7 +889,7 @@ export class WriteAPI {
|
|||
|
||||
// Find an enabled mapping where targetKey matches the standard key
|
||||
const mapping = metadataMappings.find(
|
||||
(m) => m.enabled && m.targetKey === standardKey && m.sourceKey
|
||||
(m) => m.enabled && m.targetKey === standardKey && m.sourceKey,
|
||||
);
|
||||
|
||||
// Return the source key if mapping exists, otherwise use standard key
|
||||
|
|
@ -897,8 +897,12 @@ export class WriteAPI {
|
|||
};
|
||||
|
||||
// Handle status/completed updates by writing to frontmatter
|
||||
const hasStatusUpdate = updates.status !== undefined && updates.status !== originalTask.status;
|
||||
const hasCompletedUpdate = updates.completed !== undefined && updates.completed !== originalTask.completed;
|
||||
const hasStatusUpdate =
|
||||
updates.status !== undefined &&
|
||||
updates.status !== originalTask.status;
|
||||
const hasCompletedUpdate =
|
||||
updates.completed !== undefined &&
|
||||
updates.completed !== originalTask.completed;
|
||||
const hasMetadataUpdates = updates.metadata !== undefined;
|
||||
|
||||
if (hasStatusUpdate || hasCompletedUpdate || hasMetadataUpdates) {
|
||||
|
|
@ -910,14 +914,22 @@ export class WriteAPI {
|
|||
const statusMapping = fileSourceConfig?.statusMapping;
|
||||
let statusValue = updates.status;
|
||||
|
||||
if (statusMapping?.enabled && statusMapping.symbolToMetadata) {
|
||||
statusValue = statusMapping.symbolToMetadata[updates.status] || updates.status;
|
||||
if (
|
||||
statusMapping?.enabled &&
|
||||
statusMapping.symbolToMetadata
|
||||
) {
|
||||
statusValue =
|
||||
statusMapping.symbolToMetadata[
|
||||
updates.status
|
||||
] || updates.status;
|
||||
}
|
||||
|
||||
// Use custom field name if mapped
|
||||
const statusKey = getFrontmatterKey('status');
|
||||
const statusKey = getFrontmatterKey("status");
|
||||
(fm as any)[statusKey] = statusValue;
|
||||
console.log(`[WriteAPI][FileSource] Updated ${statusKey}: ${originalTask.status} -> ${statusValue} (symbol: ${updates.status})`);
|
||||
console.log(
|
||||
`[WriteAPI][FileSource] Updated ${statusKey}: ${originalTask.status} -> ${statusValue} (symbol: ${updates.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle metadata updates
|
||||
|
|
@ -926,8 +938,11 @@ export class WriteAPI {
|
|||
|
||||
// Priority
|
||||
if (metadata.priority !== undefined) {
|
||||
const priorityKey = getFrontmatterKey('priority');
|
||||
if (metadata.priority === null || metadata.priority === 0) {
|
||||
const priorityKey = getFrontmatterKey("priority");
|
||||
if (
|
||||
metadata.priority === null ||
|
||||
metadata.priority === 0
|
||||
) {
|
||||
delete (fm as any)[priorityKey];
|
||||
} else {
|
||||
(fm as any)[priorityKey] = metadata.priority;
|
||||
|
|
@ -936,36 +951,54 @@ export class WriteAPI {
|
|||
|
||||
// Dates
|
||||
if (metadata.dueDate !== undefined) {
|
||||
const dueDateKey = getFrontmatterKey('dueDate');
|
||||
const dueDateKey = getFrontmatterKey("dueDate");
|
||||
if (metadata.dueDate) {
|
||||
(fm as any)[dueDateKey] = new Date(metadata.dueDate).toISOString().split('T')[0];
|
||||
(fm as any)[dueDateKey] = new Date(
|
||||
metadata.dueDate,
|
||||
)
|
||||
.toISOString()
|
||||
.split("T")[0];
|
||||
} else {
|
||||
delete (fm as any)[dueDateKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.startDate !== undefined) {
|
||||
const startDateKey = getFrontmatterKey('startDate');
|
||||
const startDateKey = getFrontmatterKey("startDate");
|
||||
if (metadata.startDate) {
|
||||
(fm as any)[startDateKey] = new Date(metadata.startDate).toISOString().split('T')[0];
|
||||
(fm as any)[startDateKey] = new Date(
|
||||
metadata.startDate,
|
||||
)
|
||||
.toISOString()
|
||||
.split("T")[0];
|
||||
} else {
|
||||
delete (fm as any)[startDateKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.scheduledDate !== undefined) {
|
||||
const scheduledDateKey = getFrontmatterKey('scheduledDate');
|
||||
const scheduledDateKey =
|
||||
getFrontmatterKey("scheduledDate");
|
||||
if (metadata.scheduledDate) {
|
||||
(fm as any)[scheduledDateKey] = new Date(metadata.scheduledDate).toISOString().split('T')[0];
|
||||
(fm as any)[scheduledDateKey] = new Date(
|
||||
metadata.scheduledDate,
|
||||
)
|
||||
.toISOString()
|
||||
.split("T")[0];
|
||||
} else {
|
||||
delete (fm as any)[scheduledDateKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.completedDate !== undefined) {
|
||||
const completedDateKey = getFrontmatterKey('completedDate');
|
||||
const completedDateKey =
|
||||
getFrontmatterKey("completedDate");
|
||||
if (metadata.completedDate) {
|
||||
(fm as any)[completedDateKey] = new Date(metadata.completedDate).toISOString().split('T')[0];
|
||||
(fm as any)[completedDateKey] = new Date(
|
||||
metadata.completedDate,
|
||||
)
|
||||
.toISOString()
|
||||
.split("T")[0];
|
||||
} else {
|
||||
delete (fm as any)[completedDateKey];
|
||||
}
|
||||
|
|
@ -973,7 +1006,7 @@ export class WriteAPI {
|
|||
|
||||
// Project and context
|
||||
if (metadata.project !== undefined) {
|
||||
const projectKey = getFrontmatterKey('project');
|
||||
const projectKey = getFrontmatterKey("project");
|
||||
if (metadata.project) {
|
||||
(fm as any)[projectKey] = metadata.project;
|
||||
} else {
|
||||
|
|
@ -982,7 +1015,7 @@ export class WriteAPI {
|
|||
}
|
||||
|
||||
if (metadata.context !== undefined) {
|
||||
const contextKey = getFrontmatterKey('context');
|
||||
const contextKey = getFrontmatterKey("context");
|
||||
if (metadata.context) {
|
||||
(fm as any)[contextKey] = metadata.context;
|
||||
} else {
|
||||
|
|
@ -992,8 +1025,11 @@ export class WriteAPI {
|
|||
|
||||
// Tags
|
||||
if (metadata.tags !== undefined) {
|
||||
const tagsKey = getFrontmatterKey('tags');
|
||||
if (Array.isArray(metadata.tags) && metadata.tags.length > 0) {
|
||||
const tagsKey = getFrontmatterKey("tags");
|
||||
if (
|
||||
Array.isArray(metadata.tags) &&
|
||||
metadata.tags.length > 0
|
||||
) {
|
||||
(fm as any)[tagsKey] = metadata.tags;
|
||||
} else {
|
||||
delete (fm as any)[tagsKey];
|
||||
|
|
@ -1002,7 +1038,7 @@ export class WriteAPI {
|
|||
|
||||
// Area
|
||||
if (metadata.area !== undefined) {
|
||||
const areaKey = getFrontmatterKey('area');
|
||||
const areaKey = getFrontmatterKey("area");
|
||||
if (metadata.area) {
|
||||
(fm as any)[areaKey] = metadata.area;
|
||||
} else {
|
||||
|
|
@ -1012,9 +1048,11 @@ export class WriteAPI {
|
|||
|
||||
// Recurrence
|
||||
if (metadata.recurrence !== undefined) {
|
||||
const recurrenceKey = getFrontmatterKey('recurrence');
|
||||
const recurrenceKey =
|
||||
getFrontmatterKey("recurrence");
|
||||
if (metadata.recurrence) {
|
||||
(fm as any)[recurrenceKey] = metadata.recurrence;
|
||||
(fm as any)[recurrenceKey] =
|
||||
metadata.recurrence;
|
||||
} else {
|
||||
delete (fm as any)[recurrenceKey];
|
||||
}
|
||||
|
|
@ -1022,7 +1060,9 @@ export class WriteAPI {
|
|||
}
|
||||
});
|
||||
|
||||
console.log(`[WriteAPI][FileSource] Updated frontmatter for file-source task: ${file.path}`);
|
||||
console.log(
|
||||
`[WriteAPI][FileSource] Updated frontmatter for file-source task: ${file.path}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"WriteAPI: Error updating file-source task frontmatter:",
|
||||
|
|
@ -2008,6 +2048,12 @@ export class WriteAPI {
|
|||
|
||||
// ===== Helper Methods =====
|
||||
|
||||
private formatDateForWrite(
|
||||
timestamp: number | string | Date | undefined | null,
|
||||
): string {
|
||||
return formatDateSmart(timestamp, { includeSeconds: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate metadata string based on format preference
|
||||
*/
|
||||
|
|
@ -2145,38 +2191,48 @@ export class WriteAPI {
|
|||
|
||||
// Start Date
|
||||
if (args.startDate) {
|
||||
const dateStr = moment(args.startDate).format("YYYY-MM-DD");
|
||||
metadata.push(
|
||||
useDataviewFormat ? `[start:: ${dateStr}]` : `🛫 ${dateStr}`,
|
||||
);
|
||||
const dateStr = this.formatDateForWrite(args.startDate);
|
||||
if (dateStr) {
|
||||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[start:: ${dateStr}]`
|
||||
: `🛫 ${dateStr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduled Date
|
||||
if (args.scheduledDate) {
|
||||
const dateStr = moment(args.scheduledDate).format("YYYY-MM-DD");
|
||||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[scheduled:: ${dateStr}]`
|
||||
: `⏳ ${dateStr}`,
|
||||
);
|
||||
const dateStr = this.formatDateForWrite(args.scheduledDate);
|
||||
if (dateStr) {
|
||||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[scheduled:: ${dateStr}]`
|
||||
: `⏳ ${dateStr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Due Date
|
||||
if (args.dueDate) {
|
||||
const dateStr = moment(args.dueDate).format("YYYY-MM-DD");
|
||||
metadata.push(
|
||||
useDataviewFormat ? `[due:: ${dateStr}]` : `📅 ${dateStr}`,
|
||||
);
|
||||
const dateStr = this.formatDateForWrite(args.dueDate);
|
||||
if (dateStr) {
|
||||
metadata.push(
|
||||
useDataviewFormat ? `[due:: ${dateStr}]` : `📅 ${dateStr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Completion Date
|
||||
if (args.completed && args.completedDate) {
|
||||
const dateStr = moment(args.completedDate).format("YYYY-MM-DD");
|
||||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[completion:: ${dateStr}]`
|
||||
: `✅ ${dateStr}`,
|
||||
);
|
||||
const dateStr = this.formatDateForWrite(args.completedDate);
|
||||
if (dateStr) {
|
||||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[completion:: ${dateStr}]`
|
||||
: `✅ ${dateStr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// On Completion action
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
*/
|
||||
protected async executeForCanvas(
|
||||
context: OnCompletionExecutionContext,
|
||||
config: OnCompletionConfig
|
||||
config: OnCompletionConfig,
|
||||
): Promise<OnCompletionExecutionResult> {
|
||||
const duplicateConfig = config as OnCompletionDuplicateConfig;
|
||||
const { task, app } = context;
|
||||
|
|
@ -34,7 +34,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
targetFile,
|
||||
undefined, // targetNodeId - could be enhanced later
|
||||
duplicateConfig.targetSection,
|
||||
duplicateConfig.preserveMetadata
|
||||
duplicateConfig.preserveMetadata,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
|
|
@ -46,11 +46,11 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
? ` (section: ${duplicateConfig.targetSection})`
|
||||
: "";
|
||||
return this.createSuccessResult(
|
||||
`Task duplicated ${locationText}${sectionText}`
|
||||
`Task duplicated ${locationText}${sectionText}`,
|
||||
);
|
||||
} else {
|
||||
return this.createErrorResult(
|
||||
result.error || "Failed to duplicate Canvas task"
|
||||
result.error || "Failed to duplicate Canvas task",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -59,7 +59,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
}
|
||||
} catch (error) {
|
||||
return this.createErrorResult(
|
||||
`Error duplicating Canvas task: ${error.message}`
|
||||
`Error duplicating Canvas task: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
*/
|
||||
protected async executeForMarkdown(
|
||||
context: OnCompletionExecutionContext,
|
||||
config: OnCompletionConfig
|
||||
config: OnCompletionConfig,
|
||||
): Promise<OnCompletionExecutionResult> {
|
||||
const duplicateConfig = config as OnCompletionDuplicateConfig;
|
||||
const { task, app } = context;
|
||||
|
|
@ -79,7 +79,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
const sourceFile = app.vault.getFileByPath(task.filePath);
|
||||
if (!(sourceFile instanceof TFile)) {
|
||||
return this.createErrorResult(
|
||||
`Source file not found: ${task.filePath}`
|
||||
`Source file not found: ${task.filePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -87,18 +87,18 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
let targetFile: TFile;
|
||||
if (duplicateConfig.targetFile) {
|
||||
targetFile = app.vault.getFileByPath(
|
||||
duplicateConfig.targetFile
|
||||
duplicateConfig.targetFile,
|
||||
) as TFile;
|
||||
if (!(targetFile instanceof TFile)) {
|
||||
// Try to create the target file if it doesn't exist
|
||||
try {
|
||||
targetFile = await app.vault.create(
|
||||
duplicateConfig.targetFile,
|
||||
""
|
||||
"",
|
||||
);
|
||||
} catch (error) {
|
||||
return this.createErrorResult(
|
||||
`Failed to create target file: ${duplicateConfig.targetFile}`
|
||||
`Failed to create target file: ${duplicateConfig.targetFile}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
// Find the task line
|
||||
if (task.line === undefined || task.line >= sourceLines.length) {
|
||||
return this.createErrorResult(
|
||||
"Task line not found in source file"
|
||||
"Task line not found in source file",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
// Create duplicate task line
|
||||
let duplicateTaskLine = this.createDuplicateTaskLine(
|
||||
originalTaskLine,
|
||||
duplicateConfig
|
||||
duplicateConfig,
|
||||
);
|
||||
|
||||
// If target file is different from source, add to target file
|
||||
|
|
@ -136,7 +136,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
const sectionIndex = targetLines.findIndex(
|
||||
(line) =>
|
||||
line.trim().startsWith("#") &&
|
||||
line.includes(duplicateConfig.targetSection!)
|
||||
line.includes(duplicateConfig.targetSection!),
|
||||
);
|
||||
|
||||
if (sectionIndex !== -1) {
|
||||
|
|
@ -144,14 +144,14 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
targetLines.splice(
|
||||
sectionIndex + 1,
|
||||
0,
|
||||
duplicateTaskLine
|
||||
duplicateTaskLine,
|
||||
);
|
||||
} else {
|
||||
// Section not found, create it and add the task
|
||||
targetLines.push(
|
||||
"",
|
||||
`## ${duplicateConfig.targetSection}`,
|
||||
duplicateTaskLine
|
||||
duplicateTaskLine,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -176,11 +176,11 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
: "";
|
||||
|
||||
return this.createSuccessResult(
|
||||
`Task duplicated ${locationText}${sectionText}`
|
||||
`Task duplicated ${locationText}${sectionText}`,
|
||||
);
|
||||
} catch (error) {
|
||||
return this.createErrorResult(
|
||||
`Failed to duplicate task: ${error.message}`
|
||||
`Failed to duplicate task: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +190,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
*/
|
||||
private async duplicateCanvasToMarkdown(
|
||||
context: OnCompletionExecutionContext,
|
||||
duplicateConfig: OnCompletionDuplicateConfig
|
||||
duplicateConfig: OnCompletionDuplicateConfig,
|
||||
): Promise<OnCompletionExecutionResult> {
|
||||
const { task, app } = context;
|
||||
|
||||
|
|
@ -203,14 +203,14 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
// Reset completion status
|
||||
taskContent = taskContent.replace(
|
||||
/^(\s*[-*+]\s*\[)[xX\-](\])/,
|
||||
"$1 $2"
|
||||
"$1 $2",
|
||||
);
|
||||
|
||||
if (!duplicateConfig.preserveMetadata) {
|
||||
// Remove completion-related metadata
|
||||
taskContent = taskContent
|
||||
.replace(/✅\s*\d{4}-\d{2}-\d{2}/g, "") // Remove completion date
|
||||
.replace(/⏰\s*\d{4}-\d{2}-\d{2}/g, "") // Remove scheduled date if desired
|
||||
.replace(/✅\s*\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?/g, "") // Remove completion date
|
||||
.replace(/⏰\s*\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?/g, "") // Remove scheduled date if desired
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
targetFileObj = await app.vault.create(targetFile, "");
|
||||
} catch (error) {
|
||||
return this.createErrorResult(
|
||||
`Failed to create target file: ${targetFile}`
|
||||
`Failed to create target file: ${targetFile}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -245,7 +245,7 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
.trim()
|
||||
.toLowerCase()
|
||||
.includes(
|
||||
duplicateConfig.targetSection.toLowerCase()
|
||||
duplicateConfig.targetSection.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
insertPosition = i + 1;
|
||||
|
|
@ -269,30 +269,30 @@ export class DuplicateActionExecutor extends BaseActionExecutor {
|
|||
: "";
|
||||
|
||||
return this.createSuccessResult(
|
||||
`Task duplicated from Canvas ${locationText}${sectionText}`
|
||||
`Task duplicated from Canvas ${locationText}${sectionText}`,
|
||||
);
|
||||
} catch (error) {
|
||||
return this.createErrorResult(
|
||||
`Failed to duplicate Canvas task to Markdown: ${error.message}`
|
||||
`Failed to duplicate Canvas task to Markdown: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private createDuplicateTaskLine(
|
||||
originalLine: string,
|
||||
config: OnCompletionDuplicateConfig
|
||||
config: OnCompletionDuplicateConfig,
|
||||
): string {
|
||||
// Reset the task to incomplete state
|
||||
let duplicateLine = originalLine.replace(
|
||||
/^(\s*[-*+]\s*\[)[xX\-](\])/,
|
||||
"$1 $2"
|
||||
"$1 $2",
|
||||
);
|
||||
|
||||
if (!config.preserveMetadata) {
|
||||
// Remove completion-related metadata
|
||||
duplicateLine = duplicateLine
|
||||
.replace(/✅\s*\d{4}-\d{2}-\d{2}/g, "") // Remove completion date
|
||||
.replace(/⏰\s*\d{4}-\d{2}-\d{2}/g, "") // Remove scheduled date if desired
|
||||
.replace(/✅\s*\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?/g, "") // Remove completion date
|
||||
.replace(/⏰\s*\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?/g, "") // Remove scheduled date if desired
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { CanvasData, CanvasTextData } from "../types/canvas";
|
|||
import type TaskProgressBarPlugin from "../index";
|
||||
import { Events, emit } from "../dataflow/events/Events";
|
||||
import { CanvasParser } from "../dataflow/core/CanvasParser";
|
||||
import { formatDateSmart } from "@/utils/date/date-utils";
|
||||
|
||||
/**
|
||||
* Result of a Canvas task update operation
|
||||
|
|
@ -22,14 +23,17 @@ export interface CanvasTaskUpdateResult {
|
|||
* Utility class for updating tasks within Canvas files
|
||||
*/
|
||||
export class CanvasTaskUpdater {
|
||||
constructor(private vault: Vault, private plugin: TaskProgressBarPlugin) {}
|
||||
constructor(
|
||||
private vault: Vault,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Update a task within a Canvas file
|
||||
*/
|
||||
public async updateCanvasTask(
|
||||
task: Task<CanvasTaskMetadata>,
|
||||
updatedTask: Task<CanvasTaskMetadata>
|
||||
updatedTask: Task<CanvasTaskMetadata>,
|
||||
): Promise<CanvasTaskUpdateResult> {
|
||||
try {
|
||||
// Get the Canvas file
|
||||
|
|
@ -76,7 +80,7 @@ export class CanvasTaskUpdater {
|
|||
const updateResult = this.updateTaskInTextNode(
|
||||
textNode,
|
||||
task,
|
||||
updatedTask
|
||||
updatedTask,
|
||||
);
|
||||
|
||||
if (!updateResult.success) {
|
||||
|
|
@ -88,7 +92,7 @@ export class CanvasTaskUpdater {
|
|||
if (this.plugin.app?.workspace) {
|
||||
this.plugin.app.workspace.trigger(
|
||||
"task-genius:task-completed",
|
||||
updatedTask
|
||||
updatedTask,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +137,7 @@ export class CanvasTaskUpdater {
|
|||
private updateTaskInTextNode(
|
||||
textNode: CanvasTextData,
|
||||
originalTask: Task,
|
||||
updatedTask: Task
|
||||
updatedTask: Task,
|
||||
): CanvasTaskUpdateResult {
|
||||
try {
|
||||
const lines = textNode.text.split("\n");
|
||||
|
|
@ -153,7 +157,7 @@ export class CanvasTaskUpdater {
|
|||
const updatedLine = this.updateCompleteTaskLine(
|
||||
line,
|
||||
originalTask,
|
||||
updatedTask
|
||||
updatedTask,
|
||||
);
|
||||
updatedLines[i] = updatedLine;
|
||||
taskFound = true;
|
||||
|
|
@ -205,11 +209,11 @@ export class CanvasTaskUpdater {
|
|||
// Try matching without the checkbox status (in case status changed)
|
||||
const lineWithoutStatus = normalizedLine.replace(
|
||||
/^[-*+]\s*\[[^\]]*\]\s*/,
|
||||
"- [ ] "
|
||||
"- [ ] ",
|
||||
);
|
||||
const originalWithoutStatus = normalizedOriginal.replace(
|
||||
/^[-*+]\s*\[[^\]]*\]\s*/,
|
||||
"- [ ] "
|
||||
"- [ ] ",
|
||||
);
|
||||
|
||||
if (lineWithoutStatus === originalWithoutStatus) {
|
||||
|
|
@ -280,7 +284,7 @@ export class CanvasTaskUpdater {
|
|||
private updateCompleteTaskLine(
|
||||
taskLine: string,
|
||||
originalTask: Task,
|
||||
updatedTask: Task
|
||||
updatedTask: Task,
|
||||
): string {
|
||||
const useDataviewFormat =
|
||||
this.plugin.settings.preferMetadataFormat === "dataview";
|
||||
|
|
@ -294,7 +298,7 @@ export class CanvasTaskUpdater {
|
|||
if (updatedTask.status) {
|
||||
updatedLine = updatedLine.replace(
|
||||
/(\s*[-*+]\s*\[)[^\]]*(\]\s*)/,
|
||||
`$1${updatedTask.status}$2`
|
||||
`$1${updatedTask.status}$2`,
|
||||
);
|
||||
}
|
||||
// Otherwise, update completion status if it changed
|
||||
|
|
@ -302,7 +306,7 @@ export class CanvasTaskUpdater {
|
|||
const statusMark = updatedTask.completed ? "x" : " ";
|
||||
updatedLine = updatedLine.replace(
|
||||
/(\s*[-*+]\s*\[)[^\]]*(\]\s*)/,
|
||||
`$1${statusMark}$2`
|
||||
`$1${statusMark}$2`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +327,7 @@ export class CanvasTaskUpdater {
|
|||
const metadata = this.buildMetadataArray(
|
||||
updatedTask,
|
||||
originalTask,
|
||||
useDataviewFormat
|
||||
useDataviewFormat,
|
||||
);
|
||||
|
||||
// Append all metadata to the line
|
||||
|
|
@ -346,27 +350,24 @@ export class CanvasTaskUpdater {
|
|||
private buildMetadataArray(
|
||||
updatedTask: Task,
|
||||
originalTask: Task,
|
||||
useDataviewFormat: boolean
|
||||
useDataviewFormat: boolean,
|
||||
): string[] {
|
||||
const metadata: string[] = [];
|
||||
|
||||
// Helper function to format dates
|
||||
const formatDate = (date: number | undefined): string | undefined => {
|
||||
if (!date) return undefined;
|
||||
const d = new Date(date);
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
const formatted = formatDateSmart(date, { includeSeconds: false });
|
||||
return formatted || undefined;
|
||||
};
|
||||
|
||||
const formattedDueDate = formatDate(updatedTask.metadata.dueDate);
|
||||
const formattedStartDate = formatDate(updatedTask.metadata.startDate);
|
||||
const formattedScheduledDate = formatDate(
|
||||
updatedTask.metadata.scheduledDate
|
||||
updatedTask.metadata.scheduledDate,
|
||||
);
|
||||
const formattedCompletedDate = formatDate(
|
||||
updatedTask.metadata.completedDate
|
||||
updatedTask.metadata.completedDate,
|
||||
);
|
||||
|
||||
// Helper function to check if project is readonly
|
||||
|
|
@ -497,7 +498,7 @@ export class CanvasTaskUpdater {
|
|||
let dependsList: string[] | undefined;
|
||||
if (Array.isArray(dependsValue)) {
|
||||
dependsList = dependsValue.filter(
|
||||
(v) => typeof v === "string" && v.trim().length > 0
|
||||
(v) => typeof v === "string" && v.trim().length > 0,
|
||||
);
|
||||
} else if (typeof dependsValue === "string") {
|
||||
dependsList = dependsValue
|
||||
|
|
@ -508,7 +509,7 @@ export class CanvasTaskUpdater {
|
|||
if (dependsList && dependsList.length > 0) {
|
||||
const joined = dependsList.join(", ");
|
||||
metadata.push(
|
||||
useDataviewFormat ? `[dependsOn:: ${joined}]` : `⛔ ${joined}`
|
||||
useDataviewFormat ? `[dependsOn:: ${joined}]` : `⛔ ${joined}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +517,7 @@ export class CanvasTaskUpdater {
|
|||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[id:: ${updatedTask.metadata.id}]`
|
||||
: `🆔 ${updatedTask.metadata.id}`
|
||||
: `🆔 ${updatedTask.metadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +525,7 @@ export class CanvasTaskUpdater {
|
|||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[onCompletion:: ${updatedTask.metadata.onCompletion}]`
|
||||
: `🏁 ${updatedTask.metadata.onCompletion}`
|
||||
: `🏁 ${updatedTask.metadata.onCompletion}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -533,7 +534,7 @@ export class CanvasTaskUpdater {
|
|||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[repeat:: ${updatedTask.metadata.recurrence}]`
|
||||
: `🔁 ${updatedTask.metadata.recurrence}`
|
||||
: `🔁 ${updatedTask.metadata.recurrence}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -549,7 +550,7 @@ export class CanvasTaskUpdater {
|
|||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[start:: ${formattedStartDate}]`
|
||||
: `🛫 ${formattedStartDate}`
|
||||
: `🛫 ${formattedStartDate}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -566,7 +567,7 @@ export class CanvasTaskUpdater {
|
|||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[scheduled:: ${formattedScheduledDate}]`
|
||||
: `⏳ ${formattedScheduledDate}`
|
||||
: `⏳ ${formattedScheduledDate}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -583,7 +584,7 @@ export class CanvasTaskUpdater {
|
|||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[due:: ${formattedDueDate}]`
|
||||
: `📅 ${formattedDueDate}`
|
||||
: `📅 ${formattedDueDate}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -593,7 +594,7 @@ export class CanvasTaskUpdater {
|
|||
metadata.push(
|
||||
useDataviewFormat
|
||||
? `[completion:: ${formattedCompletedDate}]`
|
||||
: `✅ ${formattedCompletedDate}`
|
||||
: `✅ ${formattedCompletedDate}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -606,39 +607,40 @@ export class CanvasTaskUpdater {
|
|||
private removeExistingMetadata(line: string): string {
|
||||
let updatedLine = line;
|
||||
|
||||
const dateWithOptionalTime =
|
||||
"\\d{4}-\\d{2}-\\d{2}(?:\\s+\\d{1,2}:\\d{2}(?::\\d{2})?)?";
|
||||
|
||||
// Remove emoji dates
|
||||
updatedLine = updatedLine.replace(/📅\s*\d{4}-\d{2}-\d{2}/g, "");
|
||||
updatedLine = updatedLine.replace(/🛫\s*\d{4}-\d{2}-\d{2}/g, "");
|
||||
updatedLine = updatedLine.replace(/⏳\s*\d{4}-\d{2}-\d{2}/g, "");
|
||||
updatedLine = updatedLine.replace(/✅\s*\d{4}-\d{2}-\d{2}/g, "");
|
||||
updatedLine = updatedLine.replace(/➕\s*\d{4}-\d{2}-\d{2}/g, "");
|
||||
["📅", "📆", "✅", "➕", "⏳"].forEach((emoji) => {
|
||||
updatedLine = updatedLine.replace(
|
||||
new RegExp(`${emoji}\\s*${dateWithOptionalTime}`, "g"),
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
// Remove dataview dates (inline field format)
|
||||
updatedLine = updatedLine.replace(
|
||||
/\[(?:due|🗓️)::\s*\d{4}-\d{2}-\d{2}\]/gi,
|
||||
""
|
||||
);
|
||||
updatedLine = updatedLine.replace(
|
||||
/\[(?:completion|✅)::\s*\d{4}-\d{2}-\d{2}\]/gi,
|
||||
""
|
||||
);
|
||||
updatedLine = updatedLine.replace(
|
||||
/\[(?:created|➕)::\s*\d{4}-\d{2}-\d{2}\]/gi,
|
||||
""
|
||||
);
|
||||
updatedLine = updatedLine.replace(
|
||||
/\[(?:start|🛫)::\s*\d{4}-\d{2}-\d{2}\]/gi,
|
||||
""
|
||||
);
|
||||
updatedLine = updatedLine.replace(
|
||||
/\[(?:scheduled|⏳)::\s*\d{4}-\d{2}-\d{2}\]/gi,
|
||||
""
|
||||
);
|
||||
const dataviewDatePatterns = [
|
||||
"(?:due|📅)",
|
||||
"(?:completion|✅)",
|
||||
"(?:created|➕)",
|
||||
"(?:start|🛫)",
|
||||
"(?:scheduled|⏳)",
|
||||
];
|
||||
|
||||
dataviewDatePatterns.forEach((pattern) => {
|
||||
updatedLine = updatedLine.replace(
|
||||
new RegExp(
|
||||
`\\[${pattern}::\\s*${dateWithOptionalTime}\\]`,
|
||||
"gi",
|
||||
),
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
// Remove emoji priority markers
|
||||
updatedLine = updatedLine.replace(
|
||||
/\s+(🔼|🔽|⏫|⏬|🔺|\[#[A-C]\])/g,
|
||||
""
|
||||
"",
|
||||
);
|
||||
// Remove dataview priority
|
||||
updatedLine = updatedLine.replace(/\[priority::\s*\w+\]/gi, "");
|
||||
|
|
@ -648,7 +650,7 @@ export class CanvasTaskUpdater {
|
|||
// Remove dataview recurrence
|
||||
updatedLine = updatedLine.replace(
|
||||
/\[(?:repeat|recurrence)::\s*[^\]]+\]/gi,
|
||||
""
|
||||
"",
|
||||
);
|
||||
|
||||
// Remove dataview project and context (using configurable prefixes)
|
||||
|
|
@ -662,17 +664,17 @@ export class CanvasTaskUpdater {
|
|||
] || "@";
|
||||
updatedLine = updatedLine.replace(
|
||||
new RegExp(`\\[${projectPrefix}::\\s*[^\\]]+\\]`, "gi"),
|
||||
""
|
||||
"",
|
||||
);
|
||||
updatedLine = updatedLine.replace(
|
||||
new RegExp(`\\[${contextPrefix}::\\s*[^\\]]+\\]`, "gi"),
|
||||
""
|
||||
"",
|
||||
);
|
||||
|
||||
// Remove ALL existing tags to prevent duplication
|
||||
updatedLine = updatedLine.replace(
|
||||
/#[^\u2000-\u206F\u2E00-\u2E7F'!"#$%&()*+,.:;<=>?@^`{|}~\[\]\\\s]+/g,
|
||||
""
|
||||
"",
|
||||
);
|
||||
updatedLine = updatedLine.replace(/@[^\s@]+/g, "");
|
||||
|
||||
|
|
@ -684,7 +686,7 @@ export class CanvasTaskUpdater {
|
|||
*/
|
||||
public async deleteCanvasTask(
|
||||
task: Task<CanvasTaskMetadata>,
|
||||
deleteChildren: boolean = false
|
||||
deleteChildren: boolean = false,
|
||||
): Promise<CanvasTaskUpdateResult> {
|
||||
try {
|
||||
// Get the Canvas file
|
||||
|
|
@ -728,7 +730,11 @@ export class CanvasTaskUpdater {
|
|||
}
|
||||
|
||||
// Delete the task from the text node
|
||||
const deleteResult = this.deleteTaskFromTextNode(textNode, task, deleteChildren);
|
||||
const deleteResult = this.deleteTaskFromTextNode(
|
||||
textNode,
|
||||
task,
|
||||
deleteChildren,
|
||||
);
|
||||
|
||||
if (!deleteResult.success) {
|
||||
return deleteResult;
|
||||
|
|
@ -757,7 +763,7 @@ export class CanvasTaskUpdater {
|
|||
task: Task<CanvasTaskMetadata>,
|
||||
targetFilePath: string,
|
||||
targetNodeId?: string,
|
||||
targetSection?: string
|
||||
targetSection?: string,
|
||||
): Promise<CanvasTaskUpdateResult> {
|
||||
try {
|
||||
// First, get the task content before deletion
|
||||
|
|
@ -775,7 +781,7 @@ export class CanvasTaskUpdater {
|
|||
targetFilePath,
|
||||
taskContent,
|
||||
targetNodeId,
|
||||
targetSection
|
||||
targetSection,
|
||||
);
|
||||
|
||||
return addResult;
|
||||
|
|
@ -795,7 +801,7 @@ export class CanvasTaskUpdater {
|
|||
targetFilePath?: string,
|
||||
targetNodeId?: string,
|
||||
targetSection?: string,
|
||||
preserveMetadata: boolean = true
|
||||
preserveMetadata: boolean = true,
|
||||
): Promise<CanvasTaskUpdateResult> {
|
||||
try {
|
||||
// Create duplicate task content
|
||||
|
|
@ -805,7 +811,7 @@ export class CanvasTaskUpdater {
|
|||
// Reset completion status
|
||||
duplicateContent = duplicateContent.replace(
|
||||
/^(\s*[-*+]\s*\[)[xX\-](\])/,
|
||||
"$1 $2"
|
||||
"$1 $2",
|
||||
);
|
||||
|
||||
if (!preserveMetadata) {
|
||||
|
|
@ -824,7 +830,7 @@ export class CanvasTaskUpdater {
|
|||
targetFile,
|
||||
duplicateContent,
|
||||
targetNodeId,
|
||||
targetSection
|
||||
targetSection,
|
||||
);
|
||||
|
||||
return addResult;
|
||||
|
|
@ -843,7 +849,7 @@ export class CanvasTaskUpdater {
|
|||
filePath: string,
|
||||
taskContent: string,
|
||||
targetNodeId?: string,
|
||||
targetSection?: string
|
||||
targetSection?: string,
|
||||
): Promise<CanvasTaskUpdateResult> {
|
||||
try {
|
||||
// Get the Canvas file
|
||||
|
|
@ -873,7 +879,7 @@ export class CanvasTaskUpdater {
|
|||
if (targetNodeId) {
|
||||
const existingNode = canvasData.nodes.find(
|
||||
(node): node is CanvasTextData =>
|
||||
node.type === "text" && node.id === targetNodeId
|
||||
node.type === "text" && node.id === targetNodeId,
|
||||
);
|
||||
|
||||
if (!existingNode) {
|
||||
|
|
@ -894,7 +900,7 @@ export class CanvasTaskUpdater {
|
|||
const addResult = this.addTaskToTextNode(
|
||||
targetNode,
|
||||
taskContent,
|
||||
targetSection
|
||||
targetSection,
|
||||
);
|
||||
|
||||
if (!addResult.success) {
|
||||
|
|
@ -923,7 +929,7 @@ export class CanvasTaskUpdater {
|
|||
private deleteTaskFromTextNode(
|
||||
textNode: CanvasTextData,
|
||||
task: Task,
|
||||
deleteChildren: boolean = false
|
||||
deleteChildren: boolean = false,
|
||||
): CanvasTaskUpdateResult {
|
||||
try {
|
||||
const lines = textNode.text.split("\n");
|
||||
|
|
@ -963,7 +969,10 @@ export class CanvasTaskUpdater {
|
|||
const currentIndent = this.getIndentLevel(line);
|
||||
|
||||
// Stop if we reach a task at the same or higher level
|
||||
if (this.isTaskLine(line) && currentIndent <= parentIndent) {
|
||||
if (
|
||||
this.isTaskLine(line) &&
|
||||
currentIndent <= parentIndent
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1008,7 +1017,7 @@ export class CanvasTaskUpdater {
|
|||
private addTaskToTextNode(
|
||||
textNode: CanvasTextData,
|
||||
taskContent: string,
|
||||
targetSection?: string
|
||||
targetSection?: string,
|
||||
): CanvasTaskUpdateResult {
|
||||
try {
|
||||
const lines = textNode.text.split("\n");
|
||||
|
|
@ -1017,7 +1026,7 @@ export class CanvasTaskUpdater {
|
|||
// Find the target section and insert after it
|
||||
const sectionIndex = this.findSectionIndex(
|
||||
lines,
|
||||
targetSection
|
||||
targetSection,
|
||||
);
|
||||
if (sectionIndex >= 0) {
|
||||
lines.splice(sectionIndex + 1, 0, taskContent);
|
||||
|
|
@ -1063,7 +1072,7 @@ export class CanvasTaskUpdater {
|
|||
if (existingNodes.length > 0) {
|
||||
// Position new node to the right of existing nodes
|
||||
const maxX = Math.max(
|
||||
...existingNodes.map((node) => node.x + node.width)
|
||||
...existingNodes.map((node) => node.x + node.width),
|
||||
);
|
||||
x = maxX + 50;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -537,6 +537,43 @@ export class TimeParsingService {
|
|||
text: string,
|
||||
): ParsedTimeResult | EnhancedParsedTimeResult {
|
||||
const safeText = text ?? "";
|
||||
const DATE_TIME_PATTERN =
|
||||
/(\d{4}-\d{2}-\d{2})\s+(\d{1,2}):([0-5]\d)(?::([0-5]\d))?/;
|
||||
|
||||
const normalizeParsedDate = (
|
||||
dateValue: Date,
|
||||
textFragment: string,
|
||||
timeExpr?:
|
||||
| {
|
||||
timeComponent?: TimeComponent;
|
||||
rangeStart?: TimeComponent;
|
||||
rangeEnd?: TimeComponent;
|
||||
}
|
||||
| undefined,
|
||||
): Date => {
|
||||
const normalized = new Date(dateValue);
|
||||
const manualMatch = textFragment.match(DATE_TIME_PATTERN);
|
||||
const timeComponent =
|
||||
timeExpr?.timeComponent ?? timeExpr?.rangeStart;
|
||||
|
||||
const hour =
|
||||
timeComponent?.hour ??
|
||||
(manualMatch ? Number(manualMatch[2]) : undefined);
|
||||
const minute =
|
||||
timeComponent?.minute ??
|
||||
(manualMatch ? Number(manualMatch[3]) : undefined);
|
||||
const second =
|
||||
timeComponent?.second ??
|
||||
(manualMatch && manualMatch[4] ? Number(manualMatch[4]) : 0);
|
||||
|
||||
if (hour === undefined || minute === undefined) {
|
||||
normalized.setHours(0, 0, 0, 0);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
normalized.setHours(hour, minute, second ?? 0, 0);
|
||||
return normalized;
|
||||
};
|
||||
|
||||
if (!this.config.enabled) {
|
||||
return {
|
||||
|
|
@ -708,13 +745,21 @@ export class TimeParsingService {
|
|||
matchingTimeExpr.rangeEnd.hour;
|
||||
}
|
||||
|
||||
const normalizedDate = normalizeParsedDate(
|
||||
date,
|
||||
expressionText,
|
||||
matchingTimeExpr,
|
||||
);
|
||||
|
||||
const expression: EnhancedTimeExpression = {
|
||||
text: expressionText,
|
||||
date: date,
|
||||
date: normalizedDate,
|
||||
type: type,
|
||||
index: index,
|
||||
length: length,
|
||||
timeComponent: matchingTimeExpr?.timeComponent,
|
||||
timeComponent:
|
||||
matchingTimeExpr?.timeComponent ??
|
||||
matchingTimeExpr?.rangeStart,
|
||||
isTimeRange: matchingTimeExpr?.isRange || false,
|
||||
rangeStart: matchingTimeExpr?.rangeStart,
|
||||
rangeEnd: matchingTimeExpr?.rangeEnd,
|
||||
|
|
@ -726,14 +771,16 @@ export class TimeParsingService {
|
|||
// Set the appropriate date field based on type
|
||||
switch (type) {
|
||||
case "start":
|
||||
if (!result.startDate) result.startDate = date;
|
||||
if (!result.startDate)
|
||||
result.startDate = normalizedDate;
|
||||
break;
|
||||
case "due":
|
||||
if (!result.dueDate) result.dueDate = date;
|
||||
if (!result.dueDate)
|
||||
result.dueDate = normalizedDate;
|
||||
break;
|
||||
case "scheduled":
|
||||
if (!result.scheduledDate)
|
||||
result.scheduledDate = date;
|
||||
result.scheduledDate = normalizedDate;
|
||||
break;
|
||||
default:
|
||||
console.warn(
|
||||
|
|
|
|||
61
src/utils/date/date-utils.ts
Normal file
61
src/utils/date/date-utils.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { moment } from "obsidian";
|
||||
|
||||
type DateInput = number | string | Date | undefined | null;
|
||||
|
||||
const toDate = (value: DateInput): Date | null => {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (value instanceof Date) return value;
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether a timestamp represents a date-only value (00:00:00.000).
|
||||
*/
|
||||
export function isDateOnly(timestamp: DateInput): boolean {
|
||||
const date = toDate(timestamp);
|
||||
if (!date) return false;
|
||||
|
||||
return (
|
||||
date.getHours() === 0 &&
|
||||
date.getMinutes() === 0 &&
|
||||
date.getSeconds() === 0 &&
|
||||
date.getMilliseconds() === 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp using smart date/time detection.
|
||||
* - 00:00:00.000 -> "YYYY-MM-DD"
|
||||
* - Otherwise -> "YYYY-MM-DD HH:mm" (seconds optional)
|
||||
*/
|
||||
export function formatDateSmart(
|
||||
timestamp: DateInput,
|
||||
options?: {
|
||||
forceFormat?: "date-only" | "date-time";
|
||||
includeSeconds?: boolean;
|
||||
},
|
||||
): string {
|
||||
const date = toDate(timestamp);
|
||||
if (!date) return "";
|
||||
|
||||
const includeSeconds = options?.includeSeconds ?? false;
|
||||
|
||||
if (options?.forceFormat === "date-only") {
|
||||
return moment(date).format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
if (options?.forceFormat === "date-time") {
|
||||
return moment(date).format(
|
||||
includeSeconds ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm",
|
||||
);
|
||||
}
|
||||
|
||||
if (isDateOnly(date)) {
|
||||
return moment(date).format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
return moment(date).format(
|
||||
includeSeconds ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm",
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue