Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
106 changes: 55 additions & 51 deletions src/file-writer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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";
import escapeRegex from "regex-escape";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

regex-escape npm package export type default

💡 Result:

Short answer: regex-escape is a CommonJS module that exports a single function (module.exports = RegexEscape). The DefinitelyTyped types use export = RegexEscape (not an ES named export). Import as CommonJS (require) or with TypeScript import-equals; default-style ESM imports rely on CJS/ESM interop.

Examples:

  • CommonJS: const RegexEscape = require('regex-escape');. [1]
  • TypeScript (types): import RegexEscape = require('regex-escape');. [2]
  • ESM (interop, may work depending on bundler/node): import RegexEscape from 'regex-escape'; (uses default interop). [1][2]

Sources:
[1] npm: regex-escape package page.
[2] @types/regex-escape (DefinitelyTyped) — index.d.ts (export = RegexEscape).


Use TypeScript import-equals syntax for the regex-escape import.

The regex-escape package is a CommonJS module that exports via module.exports = RegexEscape. While the current default import syntax (import escapeRegex from "regex-escape") works via CommonJS/ESM interop, the proper TypeScript approach is to use import-equals: import escapeRegex = require("regex-escape"); for better type safety and clarity.

🤖 Prompt for AI Agents
In src/file-writer.ts around line 7, the file currently uses an ESModule-style
default import for the CommonJS package "regex-escape"; replace that import with
TypeScript's import-equals syntax so the module is required via CommonJS (import
escapeRegex = require("regex-escape");) to align with the package's
module.exports shape and improve type-safety and clarity.


export class FileWriter {
constructor(
Expand Down Expand Up @@ -244,7 +245,7 @@ export class FileWriter {
const isDone = markAsDone[i] || false;
content = this.addActionToSection(
content,
"## Next actions",
`## ${nextActionsHeaderText(this.settings)}`,
action,
isWaiting,
isDone,
Expand Down Expand Up @@ -413,62 +414,64 @@ export class FileWriter {
}

// Add next actions to the template
let content = templateContent;
let actionsText = "";
const dueDateSuffix = dueDate ? ` 📅 ${dueDate}` : "";

// Find the "## Next actions" section and add the actions
const nextActionsRegex = /(## Next actions\s*\n)(\s*)/;
const match = content.match(nextActionsRegex);
if (result.nextActions && result.nextActions.length > 0) {
actionsText =
result.nextActions
.map((action, i) => {
const isDone = markAsDone[i] || false;
const isWaiting = waitingFor[i] || false;

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 = "- [ ]";
}
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;

actionsText = `${checkbox} ${actionText}${dueDateSuffix}\n`;
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) {
// Replace "## Next actions\n<any whitespace>" with "## Next actions\n<actions>\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;
Expand All @@ -490,6 +493,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 =
Expand Down Expand Up @@ -524,7 +528,7 @@ status: ${this.settings.defaultStatus}`;

${originalItemDescription}

## Next actions
## ${nextActionsHeader}
`;

// Handle multiple next actions or single next action
Expand Down
11 changes: 7 additions & 4 deletions src/flow-scanner.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { App, TFile, CachedMetadata } from "obsidian";
import { FlowProject } from "./types";
import { FlowProject, milestonesHeaderText, nextActionsHeaderText, PluginSettings } from "./types";
import { ProjectNode, buildProjectHierarchy } from "./project-hierarchy";

export class FlowProjectScanner {
private cache: Map<string, { mtime: number; project: FlowProject }> = 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/')
Expand Down Expand Up @@ -60,9 +63,9 @@ 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"),
milestones: this.extractSectionText(content, `## ${milestonesHeaderText(this.settings)}`),
coverImage: frontmatter["cover-image"],
current: frontmatter.current === true,
};
Expand Down
2 changes: 1 addition & 1 deletion src/focus-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/inbox-processing-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class InboxProcessingController {
saveSettings?: () => Promise<void>
) {
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 = (
Expand Down
2 changes: 1 addition & 1 deletion src/new-project-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
Expand Down
32 changes: 32 additions & 0 deletions src/settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,38 @@ 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();
})
);

// 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")
Expand Down
2 changes: 1 addition & 1 deletion src/someday-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/sphere-data-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SphereViewData> {
Expand Down
28 changes: 28 additions & 0 deletions src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface PluginSettings {
inboxFilesFolderPath: string;
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;
Expand Down Expand Up @@ -51,6 +53,8 @@ export const DEFAULT_SETTINGS: PluginSettings = {
inboxFolderPath: "Flow Inbox Folder",
processedInboxFolderPath: "Processed Inbox Folder Notes",
nextActionsFilePath: "Next actions.md",
nextActionsHeaderText: "Next actions",
milestonesHeaderText: "Milestones",
somedayFilePath: "Someday.md",
projectsFolderPath: "Projects",
projectTemplateFilePath: "Templates/Project.md",
Expand All @@ -68,3 +72,27 @@ 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";
}

/**
* 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";
}
Loading