From 5f121ce58ea11ad9c90c3a29b5080755d53e2d91 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Sun, 28 Dec 2025 14:39:27 +0000 Subject: [PATCH 1/6] refactor: Expose PluginSettings in FlowProjectScanner --- src/flow-scanner.ts | 7 +++++-- src/focus-view.ts | 2 +- src/inbox-processing-controller.ts | 2 +- src/new-project-modal.ts | 2 +- src/someday-scanner.ts | 2 +- src/sphere-data-loader.ts | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/flow-scanner.ts b/src/flow-scanner.ts index 8fad5128..8acda3ce 100644 --- a/src/flow-scanner.ts +++ b/src/flow-scanner.ts @@ -1,11 +1,14 @@ import type { App, TFile, CachedMetadata } from "obsidian"; -import { FlowProject } from "./types"; +import { FlowProject, PluginSettings } from "./types"; import { ProjectNode, buildProjectHierarchy } from "./project-hierarchy"; export class FlowProjectScanner { private cache: Map = new Map(); - constructor(private app: App) {} + constructor( + private app: App, + private settings: PluginSettings + ) {} /** * Scans the vault for all Flow projects (files with tags starting with 'project/') diff --git a/src/focus-view.ts b/src/focus-view.ts index db37aba2..48149b2f 100644 --- a/src/focus-view.ts +++ b/src/focus-view.ts @@ -33,7 +33,7 @@ export class FocusView extends RefreshingView { super(leaf); this.settings = settings; this.validator = new FocusValidator(this.app); - this.scanner = new FlowProjectScanner(this.app); + this.scanner = new FlowProjectScanner(this.app, settings); this.saveSettings = saveSettings; // Check if Dataview is available for fast refreshes diff --git a/src/inbox-processing-controller.ts b/src/inbox-processing-controller.ts index 77498729..fab06d56 100644 --- a/src/inbox-processing-controller.ts +++ b/src/inbox-processing-controller.ts @@ -34,7 +34,7 @@ export class InboxProcessingController { saveSettings?: () => Promise ) { this.settings = settings; - this.scanner = dependencies.scanner ?? new FlowProjectScanner(app); + this.scanner = dependencies.scanner ?? new FlowProjectScanner(app, settings); this.personScanner = dependencies.personScanner ?? new PersonScanner(app); this.writer = dependencies.writer ?? new FileWriter(app, settings); this.inboxScanner = ( diff --git a/src/new-project-modal.ts b/src/new-project-modal.ts index 627ade91..2b351ea9 100644 --- a/src/new-project-modal.ts +++ b/src/new-project-modal.ts @@ -32,7 +32,7 @@ export class NewProjectModal extends Modal { super(app); this.settings = settings; this.saveSettings = saveSettings; - this.scanner = new FlowProjectScanner(app); + this.scanner = new FlowProjectScanner(app, settings); this.fileWriter = new FileWriter(app, settings); this.data = { title: "", diff --git a/src/someday-scanner.ts b/src/someday-scanner.ts index 3b4b2cb3..efa202ce 100644 --- a/src/someday-scanner.ts +++ b/src/someday-scanner.ts @@ -32,7 +32,7 @@ export class SomedayScanner { constructor(app: App, settings: PluginSettings) { this.app = app; this.settings = settings; - this.projectScanner = new FlowProjectScanner(app); + this.projectScanner = new FlowProjectScanner(app, settings); } private extractSphere(lineContent: string): string | undefined { diff --git a/src/sphere-data-loader.ts b/src/sphere-data-loader.ts index c3ac27fa..62a5b086 100644 --- a/src/sphere-data-loader.ts +++ b/src/sphere-data-loader.ts @@ -35,7 +35,7 @@ export class SphereDataLoader { this.app = app; this.sphere = sphere; this.settings = settings; - this.scanner = new FlowProjectScanner(app); + this.scanner = new FlowProjectScanner(app, settings); } async loadSphereData(): Promise { From d0d1f802a3879138a3fe717e3b1ae527cea28545 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Sun, 28 Dec 2025 13:24:31 +0000 Subject: [PATCH 2/6] feat: Allow localization of the "Next actions" header --- src/file-writer.ts | 10 ++++++---- src/flow-scanner.ts | 4 ++-- src/settings-tab.ts | 16 ++++++++++++++++ src/types/settings.ts | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/file-writer.ts b/src/file-writer.ts index ba904cfe..ace6ed80 100644 --- a/src/file-writer.ts +++ b/src/file-writer.ts @@ -1,6 +1,6 @@ import type { App } from "obsidian"; import { TFile, normalizePath } from "obsidian"; -import { FlowProject, GTDProcessingResult, PluginSettings, PersonNote } from "./types"; +import { FlowProject, GTDProcessingResult, PluginSettings, PersonNote, nextActionsHeaderText } from "./types"; import { GTDResponseValidationError, FileNotFoundError, ValidationError } from "./errors"; import { EditableItem } from "./inbox-types"; import { sanitizeFileName } from "./validation"; @@ -244,7 +244,7 @@ export class FileWriter { const isDone = markAsDone[i] || false; content = this.addActionToSection( content, - "## Next actions", + `## ${nextActionsHeaderText(this.settings)}`, action, isWaiting, isDone, @@ -416,7 +416,8 @@ export class FileWriter { let content = templateContent; // Find the "## Next actions" section and add the actions - const nextActionsRegex = /(## Next actions\s*\n)(\s*)/; + // TODO: Properly escape the regex (using RegExp.escape if available) + const nextActionsRegex = new RegExp(`(##\\s*${nextActionsHeaderText(this.settings)}\\s*\\n)(\\s*)`); const match = content.match(nextActionsRegex); if (match) { @@ -490,6 +491,7 @@ export class FileWriter { const date = this.formatDate(new Date()); const title = result.projectOutcome || originalItem; const originalItemDescription = this.formatOriginalInboxItem(originalItem, sourceNoteLink); + const nextActionsHeader = nextActionsHeaderText(this.settings); // Format sphere tags for YAML list format const sphereTagsList = @@ -524,7 +526,7 @@ status: ${this.settings.defaultStatus}`; ${originalItemDescription} -## Next actions +## ${nextActionsHeader} `; // Handle multiple next actions or single next action diff --git a/src/flow-scanner.ts b/src/flow-scanner.ts index 8acda3ce..f82aafc8 100644 --- a/src/flow-scanner.ts +++ b/src/flow-scanner.ts @@ -1,5 +1,5 @@ import type { App, TFile, CachedMetadata } from "obsidian"; -import { FlowProject, PluginSettings } from "./types"; +import { FlowProject, nextActionsHeaderText, PluginSettings } from "./types"; import { ProjectNode, buildProjectHierarchy } from "./project-hierarchy"; export class FlowProjectScanner { @@ -63,7 +63,7 @@ export class FlowProjectScanner { status: frontmatter.status, creationDate: frontmatter["creation-date"], mtime: file.stat.mtime, - nextActions: this.extractSection(content, "## Next actions"), + nextActions: this.extractSection(content, `## ${nextActionsHeaderText(this.settings)}`), parentProject: frontmatter["parent-project"], milestones: this.extractSectionText(content, "## Milestones"), coverImage: frontmatter["cover-image"], diff --git a/src/settings-tab.ts b/src/settings-tab.ts index 314ff48f..155d2683 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -363,6 +363,22 @@ export class FlowGTDSettingTab extends PluginSettingTab { }) ); + // Next Actions Header Text + new Setting(containerEl) + .setName("Next Actions Header Text") + .setDesc( + "Alternative text for the \"Next actions\" header in project files." + ) + .addText((text) => + text + .setPlaceholder("Next actions") + .setValue(this.plugin.settings.nextActionsHeaderText) + .onChange(async (value) => { + this.plugin.settings.nextActionsHeaderText = value; + await this.plugin.saveSettings(); + }) + ); + // Default Inbox File new Setting(containerEl) .setName("Default Inbox File") diff --git a/src/types/settings.ts b/src/types/settings.ts index 663701dc..c8b3f42d 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -17,6 +17,7 @@ export interface PluginSettings { inboxFilesFolderPath: string; inboxFolderPath: string; processedInboxFolderPath: string; + nextActionsHeaderText: string; // Header text to use instead of "Next actions" nextActionsFilePath: string; somedayFilePath: string; projectsFolderPath: string; @@ -51,6 +52,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { inboxFolderPath: "Flow Inbox Folder", processedInboxFolderPath: "Processed Inbox Folder Notes", nextActionsFilePath: "Next actions.md", + nextActionsHeaderText: "Next actions", somedayFilePath: "Someday.md", projectsFolderPath: "Projects", projectTemplateFilePath: "Templates/Project.md", @@ -68,3 +70,15 @@ export const DEFAULT_SETTINGS: PluginSettings = { legacyFocusMigrationDismissed: false, legacyFocusTagRemovalDismissed: false, }; + +/** + * Returns "Next actions" header text from settings if present; + * returns a fallback text otherwise. + */ +export function nextActionsHeaderText(settings: PluginSettings): string { + const headerText = settings.nextActionsHeaderText; + if (headerText && headerText.trim().length > 0) { + return headerText; + } + return "Next actions"; +} From e299600c19075890196c78e7408e22fb7f7c2247 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Sun, 28 Dec 2025 14:40:31 +0000 Subject: [PATCH 3/6] feat: Allow localization of the "Milestones" header --- src/flow-scanner.ts | 4 ++-- src/settings-tab.ts | 16 ++++++++++++++++ src/types/settings.ts | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/flow-scanner.ts b/src/flow-scanner.ts index f82aafc8..e3982bf4 100644 --- a/src/flow-scanner.ts +++ b/src/flow-scanner.ts @@ -1,5 +1,5 @@ import type { App, TFile, CachedMetadata } from "obsidian"; -import { FlowProject, nextActionsHeaderText, PluginSettings } from "./types"; +import { FlowProject, milestonesHeaderText, nextActionsHeaderText, PluginSettings } from "./types"; import { ProjectNode, buildProjectHierarchy } from "./project-hierarchy"; export class FlowProjectScanner { @@ -65,7 +65,7 @@ export class FlowProjectScanner { mtime: file.stat.mtime, nextActions: this.extractSection(content, `## ${nextActionsHeaderText(this.settings)}`), parentProject: frontmatter["parent-project"], - milestones: this.extractSectionText(content, "## Milestones"), + milestones: this.extractSectionText(content, `## ${milestonesHeaderText(this.settings)}`), coverImage: frontmatter["cover-image"], current: frontmatter.current === true, }; diff --git a/src/settings-tab.ts b/src/settings-tab.ts index 155d2683..3c72b9c7 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -379,6 +379,22 @@ export class FlowGTDSettingTab extends PluginSettingTab { }) ); + // Milestones Header Text + new Setting(containerEl) + .setName("Milestones Header Text") + .setDesc( + "Alternative text for the \"Milestones\" header in project files." + ) + .addText((text) => + text + .setPlaceholder("Milestones") + .setValue(this.plugin.settings.milestonesHeaderText) + .onChange(async (value) => { + this.plugin.settings.milestonesHeaderText = value; + await this.plugin.saveSettings(); + }) + ); + // Default Inbox File new Setting(containerEl) .setName("Default Inbox File") diff --git a/src/types/settings.ts b/src/types/settings.ts index c8b3f42d..4e80bb1b 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -18,6 +18,7 @@ export interface PluginSettings { inboxFolderPath: string; processedInboxFolderPath: string; nextActionsHeaderText: string; // Header text to use instead of "Next actions" + milestonesHeaderText: string; // Header text to use instead of "Milestones" nextActionsFilePath: string; somedayFilePath: string; projectsFolderPath: string; @@ -53,6 +54,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { processedInboxFolderPath: "Processed Inbox Folder Notes", nextActionsFilePath: "Next actions.md", nextActionsHeaderText: "Next actions", + milestonesHeaderText: "Milestones", somedayFilePath: "Someday.md", projectsFolderPath: "Projects", projectTemplateFilePath: "Templates/Project.md", @@ -82,3 +84,15 @@ export function nextActionsHeaderText(settings: PluginSettings): string { } return "Next actions"; } + +/** + * Returns "Milestones" header text from settings if present; + * returns a fallback text otherwise. + */ +export function milestonesHeaderText(settings: PluginSettings): string { + const headerText = settings.milestonesHeaderText; + if (headerText && headerText.trim().length > 0) { + return headerText; + } + return "Milestones"; +} From 7167ad9056333391578576c042b162d777e5c03c Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Sun, 28 Dec 2025 15:15:16 +0000 Subject: [PATCH 4/6] fix: Escape user string before passing it to the regex --- package-lock.json | 15 +++++++++++++++ package.json | 2 ++ src/file-writer.ts | 4 ++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0b1f151..651ace4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "gray-matter": "^4.0.3", "marked": "^15.0.12", "marked-terminal": "^7.3.0", + "regex-escape": "^3.4.11", "uuid": "^13.0.0" }, "devDependencies": { @@ -21,6 +22,7 @@ "@types/jsdom": "^27.0.0", "@types/marked-terminal": "^6.1.1", "@types/node": "^20.11.19", + "@types/regex-escape": "^3.4.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "builtin-modules": "^3.3.0", @@ -2247,6 +2249,13 @@ "form-data": "^4.0.4" } }, + "node_modules/@types/regex-escape": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@types/regex-escape/-/regex-escape-3.4.1.tgz", + "integrity": "sha512-aQihdLwAzyST0/LTidv76ZKtltWbea61FprvpMHbqUToUpdZrSYUqPb3ToojMW2a29JfoJXBxtO6K5py4o8uwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -6736,6 +6745,12 @@ "dev": true, "license": "MIT" }, + "node_modules/regex-escape": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/regex-escape/-/regex-escape-3.4.11.tgz", + "integrity": "sha512-051l4Hl/0HoJwTvNztrWVjoxLiseSfCrDgWqwR1cnGM/nyQSeIjmvti5zZ7HzOmsXDPaJ2k0iFxQ6/WNpJD5wQ==", + "license": "MIT" + }, "node_modules/remove-markdown": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.5.5.tgz", diff --git a/package.json b/package.json index df05f1cd..9a01f917 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/jsdom": "^27.0.0", "@types/marked-terminal": "^6.1.1", "@types/node": "^20.11.19", + "@types/regex-escape": "^3.4.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "builtin-modules": "^3.3.0", @@ -59,6 +60,7 @@ "gray-matter": "^4.0.3", "marked": "^15.0.12", "marked-terminal": "^7.3.0", + "regex-escape": "^3.4.11", "uuid": "^13.0.0" } } diff --git a/src/file-writer.ts b/src/file-writer.ts index ace6ed80..1fd20900 100644 --- a/src/file-writer.ts +++ b/src/file-writer.ts @@ -4,6 +4,7 @@ import { FlowProject, GTDProcessingResult, PluginSettings, PersonNote, nextActio import { GTDResponseValidationError, FileNotFoundError, ValidationError } from "./errors"; import { EditableItem } from "./inbox-types"; import { sanitizeFileName } from "./validation"; +import escapeRegex from "regex-escape"; export class FileWriter { constructor( @@ -416,8 +417,7 @@ export class FileWriter { let content = templateContent; // Find the "## Next actions" section and add the actions - // TODO: Properly escape the regex (using RegExp.escape if available) - const nextActionsRegex = new RegExp(`(##\\s*${nextActionsHeaderText(this.settings)}\\s*\\n)(\\s*)`); + const nextActionsRegex = new RegExp(`(##\\s*${escapeRegex(nextActionsHeaderText(this.settings))}\\s*\\n)(\\s*)`); const match = content.match(nextActionsRegex); if (match) { From 74423c420252c2b42ba885561d5328e830474737 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Sun, 28 Dec 2025 15:37:46 +0000 Subject: [PATCH 5/6] fix: Handle the case where the template does not end in a newline --- src/file-writer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file-writer.ts b/src/file-writer.ts index 1fd20900..6c051ca7 100644 --- a/src/file-writer.ts +++ b/src/file-writer.ts @@ -417,7 +417,7 @@ export class FileWriter { let content = templateContent; // Find the "## Next actions" section and add the actions - const nextActionsRegex = new RegExp(`(##\\s*${escapeRegex(nextActionsHeaderText(this.settings))}\\s*\\n)(\\s*)`); + const nextActionsRegex = new RegExp(`(##\\s*${escapeRegex(nextActionsHeaderText(this.settings))}\\s*(?:\\n|$))(\\s*)`); const match = content.match(nextActionsRegex); if (match) { From 05d195058321a09bf434eee20066a41a3b75b5b5 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Sun, 28 Dec 2025 16:17:08 +0000 Subject: [PATCH 6/6] fix: Provide fallback on project creation if "Next actions" header is not found --- src/file-writer.ts | 96 +++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/src/file-writer.ts b/src/file-writer.ts index 6c051ca7..e4cbd5aa 100644 --- a/src/file-writer.ts +++ b/src/file-writer.ts @@ -414,62 +414,64 @@ export class FileWriter { } // Add next actions to the template - let content = templateContent; + let actionsText = ""; + const dueDateSuffix = dueDate ? ` 📅 ${dueDate}` : ""; + + if (result.nextActions && result.nextActions.length > 0) { + actionsText = + result.nextActions + .map((action, i) => { + const isDone = markAsDone[i] || false; + const isWaiting = waitingFor[i] || false; + + let checkbox: string; + let actionText = action; + + if (isDone) { + checkbox = "- [x]"; + const completionDate = new Date().toISOString().split("T")[0]; + actionText = `${action} ✅ ${completionDate}`; + } else if (isWaiting) { + checkbox = "- [w]"; + } else { + checkbox = "- [ ]"; + } + + return `${checkbox} ${actionText}${dueDateSuffix}`; + }) + .join("\n") + "\n"; + } else if (result.nextAction) { + const isDone = markAsDone[0] || false; + const isWaiting = waitingFor[0] || false; + + let checkbox: string; + let actionText = result.nextAction; + + if (isDone) { + checkbox = "- [x]"; + const completionDate = new Date().toISOString().split("T")[0]; + actionText = `${result.nextAction} ✅ ${completionDate}`; + } else if (isWaiting) { + checkbox = "- [w]"; + } else { + checkbox = "- [ ]"; + } + + actionsText = `${checkbox} ${actionText}${dueDateSuffix}\n`; + } // Find the "## Next actions" section and add the actions + let content = templateContent; const nextActionsRegex = new RegExp(`(##\\s*${escapeRegex(nextActionsHeaderText(this.settings))}\\s*(?:\\n|$))(\\s*)`); const match = content.match(nextActionsRegex); if (match) { - let actionsText = ""; - const dueDateSuffix = dueDate ? ` 📅 ${dueDate}` : ""; - - if (result.nextActions && result.nextActions.length > 0) { - actionsText = - result.nextActions - .map((action, i) => { - const isDone = markAsDone[i] || false; - const isWaiting = waitingFor[i] || false; - - let checkbox: string; - let actionText = action; - - if (isDone) { - checkbox = "- [x]"; - const completionDate = new Date().toISOString().split("T")[0]; - actionText = `${action} ✅ ${completionDate}`; - } else if (isWaiting) { - checkbox = "- [w]"; - } else { - checkbox = "- [ ]"; - } - - return `${checkbox} ${actionText}${dueDateSuffix}`; - }) - .join("\n") + "\n"; - } else if (result.nextAction) { - const isDone = markAsDone[0] || false; - const isWaiting = waitingFor[0] || false; - - let checkbox: string; - let actionText = result.nextAction; - - if (isDone) { - checkbox = "- [x]"; - const completionDate = new Date().toISOString().split("T")[0]; - actionText = `${result.nextAction} ✅ ${completionDate}`; - } else if (isWaiting) { - checkbox = "- [w]"; - } else { - checkbox = "- [ ]"; - } - - actionsText = `${checkbox} ${actionText}${dueDateSuffix}\n`; - } - // Replace "## Next actions\n" with "## Next actions\n\n" // This ensures proper spacing regardless of template whitespace content = content.replace(nextActionsRegex, `$1${actionsText}\n`); + } else { + // Fallback: Append at the end if the section is not found + content += `\n## ${nextActionsHeaderText(this.settings)}\n${actionsText}\n`; } return content;