diff --git a/src/modals/TaskCreationModal.ts b/src/modals/TaskCreationModal.ts index dc52567f..613fb288 100644 --- a/src/modals/TaskCreationModal.ts +++ b/src/modals/TaskCreationModal.ts @@ -1349,11 +1349,14 @@ export class TaskCreationModal extends TaskModal { private generateFilename(taskData: TaskCreationData): string { const context: FilenameContext = { - title: taskData.title || "", - status: taskData.status || "open", - priority: taskData.priority || "normal", - dueDate: taskData.due, - scheduledDate: taskData.scheduled, + taskData: { + title: taskData.title || "", + status: taskData.status || "open", + priority: taskData.priority || "normal", + due: taskData.due, + scheduled: taskData.scheduled, + }, + date: new Date(), }; return generateTaskFilename(context, this.plugin.settings); diff --git a/src/services/ICSNoteService.ts b/src/services/ICSNoteService.ts index 92775e98..f24b620e 100644 --- a/src/services/ICSNoteService.ts +++ b/src/services/ICSNoteService.ts @@ -148,12 +148,15 @@ export class ICSNoteService { // Generate filename context for ICS events // Use clean event title for filename template variables, not the formatted noteTitle const filenameContext: ICSFilenameContext = { - title: icsEvent.title, // Use clean event title for {title} variable - priority: "", - status: "", + taskData: { + title: icsEvent.title, // Use clean event title for {title} variable + priority: "", + status: "", + due: icsEvent.end, + scheduled: icsEvent.start, + }, date: eventStartDate, - dueDate: icsEvent.end, - scheduledDate: icsEvent.start, + icsEventTitle: icsEvent.title, icsEventLocation: icsEvent.location, icsEventDescription: icsEvent.description, diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts index a57caeb2..056e18da 100644 --- a/src/services/TaskService.ts +++ b/src/services/TaskService.ts @@ -202,12 +202,15 @@ export class TaskService { // Generate filename const filenameContext: FilenameContext = { - title: title, - priority: priority, - status: status, + taskData: { + title: title, + priority: priority, + status: status, + projects: projectsArray, + due: taskData.due, + scheduled: taskData.scheduled, + }, date: new Date(), - dueDate: taskData.due, - scheduledDate: taskData.scheduled, }; const baseFilename = generateTaskFilename(filenameContext, this.plugin.settings); diff --git a/src/settings/tabs/appearanceTab.ts b/src/settings/tabs/appearanceTab.ts index 9a45fc22..d0da8d41 100644 --- a/src/settings/tabs/appearanceTab.ts +++ b/src/settings/tabs/appearanceTab.ts @@ -59,85 +59,6 @@ export function renderAppearanceTab( const currentLabels = getPropertyLabels(plugin, currentProperties); createHelpText(container, `Currently showing: ${currentLabels.join(", ")}`); - - // Task Filenames Section - createSectionHeader(container, translate("settings.appearance.taskFilenames.header")); - createHelpText(container, translate("settings.appearance.taskFilenames.description")); - - createToggleSetting(container, { - name: translate("settings.appearance.taskFilenames.storeTitleInFilename.name"), - desc: translate("settings.appearance.taskFilenames.storeTitleInFilename.description"), - getValue: () => plugin.settings.storeTitleInFilename, - setValue: async (value: boolean) => { - plugin.settings.storeTitleInFilename = value; - save(); - // Re-render to show/hide other options - renderAppearanceTab(container, plugin, save); - }, - }); - - if (!plugin.settings.storeTitleInFilename) { - createDropdownSetting(container, { - name: translate("settings.appearance.taskFilenames.filenameFormat.name"), - desc: translate("settings.appearance.taskFilenames.filenameFormat.description"), - options: [ - { - value: "title", - label: translate( - "settings.appearance.taskFilenames.filenameFormat.options.title" - ), - }, - { - value: "zettel", - label: translate( - "settings.appearance.taskFilenames.filenameFormat.options.zettel" - ), - }, - { - value: "timestamp", - label: translate( - "settings.appearance.taskFilenames.filenameFormat.options.timestamp" - ), - }, - { - value: "custom", - label: translate( - "settings.appearance.taskFilenames.filenameFormat.options.custom" - ), - }, - ], - getValue: () => plugin.settings.taskFilenameFormat, - setValue: async (value: string) => { - plugin.settings.taskFilenameFormat = value as any; - save(); - // Re-render to update visibility - renderAppearanceTab(container, plugin, save); - }, - ariaLabel: "Task filename generation format", - }); - - if (plugin.settings.taskFilenameFormat === "custom") { - createTextSetting(container, { - name: translate("settings.appearance.taskFilenames.customTemplate.name"), - desc: translate("settings.appearance.taskFilenames.customTemplate.description"), - placeholder: translate( - "settings.appearance.taskFilenames.customTemplate.placeholder" - ), - getValue: () => plugin.settings.customFilenameTemplate, - setValue: async (value: string) => { - plugin.settings.customFilenameTemplate = value; - save(); - }, - ariaLabel: "Custom filename template with variables", - }); - - createHelpText( - container, - translate("settings.appearance.taskFilenames.customTemplate.helpText") - ); - } - } - // Display Formatting Section createSectionHeader(container, translate("settings.appearance.displayFormatting.header")); createHelpText(container, translate("settings.appearance.displayFormatting.description")); diff --git a/src/settings/tabs/generalTab.ts b/src/settings/tabs/generalTab.ts index 32e5bf6f..39873bb7 100644 --- a/src/settings/tabs/generalTab.ts +++ b/src/settings/tabs/generalTab.ts @@ -153,6 +153,90 @@ export function renderGeneralTab( ariaLabel: "Excluded folder paths", }); + // Task Filenames Section + createSectionHeader(container, translate("settings.appearance.taskFilenames.header")); + createHelpText(container, translate("settings.appearance.taskFilenames.description")); + + createToggleSetting(container, { + name: translate("settings.appearance.taskFilenames.storeTitleInFilename.name"), + desc: translate("settings.appearance.taskFilenames.storeTitleInFilename.description"), + getValue: () => plugin.settings.storeTitleInFilename, + setValue: async (value: boolean) => { + plugin.settings.storeTitleInFilename = value; + save(); + // Re-render to show/hide other options + renderGeneralTab(container, plugin, save); + }, + }); + + if (!plugin.settings.storeTitleInFilename) { + createDropdownSetting(container, { + name: translate("settings.general.taskFilenames.filenameFormat.name"), + desc: translate("settings.general.taskFilenames.filenameFormat.description"), + options: [ + { + value: "title", + label: translate( + "settings.general.taskFilenames.filenameFormat.options.title" + ), + }, + { + value: "zettel", + label: translate( + "settings.general.taskFilenames.filenameFormat.options.zettel" + ), + }, + { + value: "timestamp", + label: translate( + "settings.general.taskFilenames.filenameFormat.options.timestamp" + ), + }, + { + value: "project", + label: translate( + "settings.general.taskFilenames.filenameFormat.options.project" + ) + }, + { + value: "custom", + label: translate( + "settings.appearance.taskFilenames.filenameFormat.options.custom" + ), + }, + ], + getValue: () => plugin.settings.taskFilenameFormat, + setValue: async (value: string) => { + plugin.settings.taskFilenameFormat = value as any; + save(); + // Re-render to update visibility + renderGeneralTab(container, plugin, save); + }, + ariaLabel: "Task filename generation format", + }); + + if (plugin.settings.taskFilenameFormat === "custom") { + createTextSetting(container, { + name: translate("settings.appearance.taskFilenames.customTemplate.name"), + desc: translate("settings.appearance.taskFilenames.customTemplate.description"), + placeholder: translate( + "settings.appearance.taskFilenames.customTemplate.placeholder" + ), + getValue: () => plugin.settings.customFilenameTemplate, + setValue: async (value: string) => { + plugin.settings.customFilenameTemplate = value; + save(); + }, + ariaLabel: "Custom filename template with variables", + }); + + createHelpText( + container, + translate("settings.appearance.taskFilenames.customTemplate.helpText") + ); + } + } + // UI Language Section createSectionHeader(container, translate("settings.features.uiLanguage.header")); createHelpText(container, translate("settings.features.uiLanguage.description")); diff --git a/src/types/settings.ts b/src/types/settings.ts index cd918702..8b7e8639 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -102,7 +102,7 @@ export interface TaskNotesSettings { defaultTaskStatus: string; // Changed to string to support custom statuses taskOrgFiltersCollapsed: boolean; // Save collapse state of task organization filters // Task filename settings - taskFilenameFormat: "title" | "zettel" | "timestamp" | "custom"; + taskFilenameFormat: "title" | "zettel" | "timestamp" | "project" | "custom"; storeTitleInFilename: boolean; customFilenameTemplate: string; // Template for custom format // Task creation defaults @@ -269,7 +269,7 @@ export interface ICSIntegrationSettings { // Default folders defaultNoteFolder: string; // Folder for notes created from ICS events // Filename settings for ICS event notes - icsNoteFilenameFormat: "title" | "zettel" | "timestamp" | "custom"; + icsNoteFilenameFormat: "title" | "zettel" | "timestamp" | "project" | "custom"; customICSNoteFilenameTemplate: string; // Template for custom format // Automatic export settings enableAutoExport: boolean; // Whether to automatically export tasks to ICS file diff --git a/src/utils/filenameGenerator.ts b/src/utils/filenameGenerator.ts index fd9e50ac..ae320d50 100644 --- a/src/utils/filenameGenerator.ts +++ b/src/utils/filenameGenerator.ts @@ -1,14 +1,11 @@ import { format } from "date-fns"; import { normalizePath } from "obsidian"; import { TaskNotesSettings } from "../types/settings"; +import {TaskTemplateData} from "./folderTemplateProcessor"; export interface FilenameContext { - title: string; - priority: string; - status: string; + taskData?: TaskTemplateData; date?: Date; - dueDate?: string; // YYYY-MM-DD format - scheduledDate?: string; // YYYY-MM-DD format } export interface ICSFilenameContext extends FilenameContext { @@ -25,16 +22,16 @@ export function generateICSNoteFilename( settings: TaskNotesSettings ): string { // Validate inputs - if (!context || !settings) { + if (!context || !settings || !context.taskData) { throw new Error("Invalid context or settings provided"); } - if (!context.title || typeof context.title !== "string") { + if (!context.taskData.title) { throw new Error("Context must have a valid title"); } // Validate title content - if (context.title.trim().length === 0) { + if (context.taskData.title.trim().length === 0) { throw new Error("Title cannot be empty"); } @@ -51,7 +48,7 @@ export function generateICSNoteFilename( if (icsSettings) { switch (icsSettings.icsNoteFilenameFormat) { case "title": - return sanitizeForFilename(context.title); + return sanitizeForFilename(context.taskData.title); case "zettel": return generateZettelId(now); @@ -59,12 +56,15 @@ export function generateICSNoteFilename( case "timestamp": return generateTimestampFilename(now); + case "project": + return generateProjectIdFilename(context.taskData); + case "custom": { // Create ICS-specific additional variables const icsVariables: Record = { icsEventTitle: context.icsEventTitle ? sanitizeForFilename(context.icsEventTitle) - : sanitizeForFilename(context.title), + : sanitizeForFilename(context.taskData.title), icsEventLocation: context.icsEventLocation ? sanitizeForFilename(context.icsEventLocation) : "", @@ -73,7 +73,7 @@ export function generateICSNoteFilename( : "", // Add formatted title with date for users who want the full context icsEventTitleWithDate: sanitizeForFilename( - `${context.icsEventTitle || context.title} - ${format(now, "PPP")}` + `${context.icsEventTitle || context.taskData.title} - ${format(now, "PPP")}` ), }; return generateCustomFilename( @@ -86,16 +86,16 @@ export function generateICSNoteFilename( default: // Fallback to title format for ICS notes - return sanitizeForFilename(context.title); + return sanitizeForFilename(context.taskData.title); } } // Fallback to title format if no ICS settings - return sanitizeForFilename(context.title); + return sanitizeForFilename(context.taskData.title); } catch (error) { console.error("Error generating ICS note filename:", error); // Fallback to safe title format - return sanitizeForFilename(context.title); + return sanitizeForFilename(context.taskData.title); } } @@ -107,16 +107,16 @@ export function generateTaskFilename( settings: TaskNotesSettings ): string { // Validate inputs - if (!context || !settings) { + if (!context || !settings || !context.taskData) { throw new Error("Invalid context or settings provided"); } - if (!context.title || typeof context.title !== "string") { + if (!context.taskData.title) { throw new Error("Context must have a valid title"); } // Validate title content - if (context.title.trim().length === 0) { + if (context.taskData.title.trim().length === 0) { throw new Error("Title cannot be empty"); } @@ -128,13 +128,13 @@ export function generateTaskFilename( } if (settings.storeTitleInFilename) { - return sanitizeForFilename(context.title); + return sanitizeForFilename(context.taskData.title); } try { switch (settings.taskFilenameFormat) { case "title": - return sanitizeForFilename(context.title); + return sanitizeForFilename(context.taskData.title); case "zettel": return generateZettelId(now); @@ -142,6 +142,9 @@ export function generateTaskFilename( case "timestamp": return generateTimestampFilename(now); + case "project": + return generateProjectIdFilename(context.taskData); + case "custom": return generateCustomFilename(context, settings.customFilenameTemplate, now); @@ -180,6 +183,22 @@ function generateTimestampFilename(date: Date): string { return format(date, "yyyy-MM-dd-HHmmss"); } +/** + * Generates a filename based on a project and incrementing number + * format: {ProjectId}-{incValue} + * @example PROJ-1234 + */ +function generateProjectIdFilename(taskData:TaskTemplateData | undefined) { + if (!taskData) { + throw new Error("Invalid Task or Context Data."); + } + const projectId = + (taskData.projects) + ? taskData.projects[0].trim().substring(2,6).toUpperCase() + : "TASK"; + return `${projectId}`; +} + /** * Generates a filename based on a custom template */ @@ -190,7 +209,7 @@ function generateCustomFilename( additionalVariables?: Record ): string { // Validate inputs - if (!context || !template || !date) { + if (!context || !template || !date || !context.taskData) { throw new Error("Invalid inputs for custom filename generation"); } @@ -204,15 +223,22 @@ function generateCustomFilename( try { // Validate and sanitize context values - const sanitizedTitle = sanitizeForFilename(context.title); + const sanitizedTitle = + context.taskData.title + ? sanitizeForFilename(context.taskData.title) + : "untitled"; const sanitizedPriority = - context.priority && ["low", "normal", "medium", "high"].includes(context.priority) - ? context.priority + context.taskData.priority && ["low", "normal", "medium", "high"].includes(context.taskData.priority) + ? context.taskData.priority : "normal"; const sanitizedStatus = - context.status && ["open", "in-progress", "done", "scheduled"].includes(context.status) - ? context.status + context.taskData.status && ["open", "in-progress", "done", "scheduled"].includes(context.taskData.status) + ? context.taskData.status : "open"; + const sanitizedProject = + context.taskData.projects + ? context.taskData.projects[0].trim().substring(2,6).toUpperCase() + : "TASK"; const variables: Record = { title: sanitizedTitle, @@ -220,16 +246,18 @@ function generateCustomFilename( time: format(date, "HHmmss"), priority: sanitizedPriority, status: sanitizedStatus, + project: sanitizedProject, timestamp: format(date, "yyyy-MM-dd-HHmmss"), dateTime: format(date, "yyyy-MM-dd-HHmm"), year: format(date, "yyyy"), + shortYear: format(date, "yy"), month: format(date, "MM"), day: format(date, "dd"), hour: format(date, "HH"), minute: format(date, "mm"), second: format(date, "ss"), - dueDate: context.dueDate || "", - scheduledDate: context.scheduledDate || "", + dueDate: context.taskData.due || "", + scheduledDate: context.taskData.scheduled || "", // New date format variations shortDate: format(date, "yyMMdd"), monthName: format(date, "MMMM"), @@ -274,6 +302,7 @@ function generateCustomFilename( // Date-based identifiers zettel: generateZettelId(date), nano: Date.now().toString() + Math.random().toString(36).substring(2, 7), + uuid: crypto.randomUUID(), // Merge any additional variables ...(additionalVariables || {}), @@ -308,7 +337,11 @@ function generateCustomFilename( } catch (error) { console.error("Error generating custom filename:", error); // Fallback to safe title-based filename - return sanitizeForFilename(context.title) || generateZettelId(date); + return ( + context.taskData.title + ? sanitizeForFilename(context.taskData.title) + : generateZettelId(date) + ) } } @@ -461,8 +494,8 @@ export async function generateUniqueFilename( } // If not, try appending numbers - for (let i = 2; i <= 999; i++) { - const candidateFilename = `${sanitizedFilename}-${i}`; + for (let i = 1; i <= 999; i++) { + const candidateFilename = `${sanitizedFilename}-${i.toString().padStart(4,"0")}`; const candidatePath = normalizePath(`${sanitizedFolderPath}/${candidateFilename}.md`); // Check path length for each candidate