From 47f99ea2660dd2e6bf4ffc43929e024626ca1c7f Mon Sep 17 00:00:00 2001 From: callumalpass Date: Mon, 8 Jun 2026 19:37:50 +1000 Subject: [PATCH] split parent note project defaults --- docs/settings/task-properties.md | 3 +- src/i18n/resources/en.ts | 14 ++++-- src/main.ts | 26 +++++++--- src/settings/defaults.ts | 1 + src/settings/settingsPersistence.ts | 47 ++++++++++++++++--- .../taskProperties/projectsPropertyCard.ts | 17 ++++++- src/types/settings.ts | 3 +- src/utils/taskCreationPrepopulation.ts | 17 +++++++ ...5-inline-conversion-parent-project.test.ts | 43 +++++++++++++++++ .../unit/modals/taskCreationFormState.test.ts | 3 ++ .../unit/settings/settingsPersistence.test.ts | 36 ++++++++++++++ 11 files changed, 190 insertions(+), 20 deletions(-) diff --git a/docs/settings/task-properties.md b/docs/settings/task-properties.md index 3ae4bf2b..b01d0583 100644 --- a/docs/settings/task-properties.md +++ b/docs/settings/task-properties.md @@ -130,7 +130,8 @@ Projects the task belongs to. Configuration options: - **Property key**: Frontmatter field name (default: `projects`) - **Default projects**: Select project notes to automatically link to new tasks -- **Use parent note as project**: Automatically link the parent note as a project during instant task conversion +- **Use active note for new tasks**: Automatically link the active note as a project when opening task creation from the command palette or ribbon +- **Use parent note for inline/instant conversion**: Automatically link the source note as a project when using inline task creation or instant task conversion - **NLP trigger**: Character that triggers project parsing (default: `+`) - **Autosuggest Filters**: Expandable section to filter which notes appear in project suggestions - **Customize Display**: Expandable section to configure how project suggestions appear diff --git a/src/i18n/resources/en.ts b/src/i18n/resources/en.ts index 1f2218bf..f1db23ff 100644 --- a/src/i18n/resources/en.ts +++ b/src/i18n/resources/en.ts @@ -772,10 +772,15 @@ export const en: TranslationTree = { selectTooltip: "Choose project notes to link by default", removeTooltip: "Remove {name} from default projects", }, - useParentNoteAsProject: { - name: "Use parent note as project during instant conversion", + useParentNoteForTaskCreation: { + name: "Use active note as project for new tasks", description: - "Automatically link the parent note as a project when using instant task conversion", + "Automatically link the active note as a project when opening task creation from the command palette or ribbon", + }, + useParentNoteAsProject: { + name: "Use parent note as project for inline and instant conversion", + description: + "Automatically link the source note as a project when using inline task creation or instant task conversion", }, useParentHeaderAsProject: { name: "Use parent heading as project during instant conversion", @@ -1171,7 +1176,8 @@ export const en: TranslationTree = { }, projectsCard: { defaultProjects: "Default projects:", - useParentNote: "Use parent note as project:", + useParentNoteForTaskCreation: "Use active note for new tasks:", + useParentNoteForInlineTasks: "Use parent note for inline/instant conversion:", useParentHeader: "Use parent heading as project:", inheritParentTaskProperties: "Inherit parent task properties for subtasks:", noDefaultProjects: "No default projects selected", diff --git a/src/main.ts b/src/main.ts index 79f111ac..aa81c309 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,9 @@ import { TFile, getLanguage, } from "obsidian"; + +type Nullable = T | null; + import { format } from "date-fns"; import { createDailyNote, @@ -79,7 +82,11 @@ import { TASKNOTES_COMMUNITY_PLUGIN_URL, } from "./api/releaseCheck"; import { buildCurrentNoteConversionTaskInfo } from "./services/task-service/currentNoteConversion"; -import { applyParentNoteProjectDefault } from "./utils/taskCreationPrepopulation"; +import { + applyParentNoteProjectDefault, + shouldApplyParentNoteProjectDefault, +} from "./utils/taskCreationPrepopulation"; +import type { ParentNoteProjectDefaultContext } from "./utils/taskCreationPrepopulation"; import { applySearchQueryToView } from "./utils/obsidianSearchView"; import { TaskContextMenu } from "./components/TaskContextMenu"; import { @@ -1149,14 +1156,18 @@ export default class TaskNotesPlugin extends Plugin { openTaskCreationModal(prePopulatedValues?: Partial) { new TaskCreationModal(this.app, this, { - prePopulatedValues: this.applyParentNoteProjectDefault(prePopulatedValues), + prePopulatedValues: this.applyParentNoteProjectDefault( + prePopulatedValues, + "task-creation" + ), }).open(); } private applyParentNoteProjectDefault( - prePopulatedValues?: Partial + prePopulatedValues: Partial | undefined, + context: ParentNoteProjectDefaultContext ): Partial | undefined { - if (!this.settings.taskCreationDefaults.useParentNoteAsProject) { + if (!shouldApplyParentNoteProjectDefault(this.settings.taskCreationDefaults, context)) { return prePopulatedValues; } @@ -1569,7 +1580,7 @@ export default class TaskNotesPlugin extends Plugin { async openQuickActionsForTaskUnderCursor( editor: Editor, - sourceFile?: TFile | null + sourceFile?: Nullable ): Promise { try { const activeFile = sourceFile ?? this.app.workspace.getActiveFile(); @@ -1886,7 +1897,10 @@ export default class TaskNotesPlugin extends Plugin { insertionPoint, }; - const prePopulatedValues = this.applyParentNoteProjectDefault(); + const prePopulatedValues = this.applyParentNoteProjectDefault( + undefined, + "inline-creation" + ); // Open task creation modal with callback to insert link // Use modal-inline-creation context for inline folder behavior (Issue #1424) diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 090c68ec..77b9e41f 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -108,6 +108,7 @@ export const DEFAULT_TASK_CREATION_DEFAULTS: TaskCreationDefaults = { defaultContexts: "", defaultTags: "", defaultProjects: "", + useParentNoteForTaskCreation: false, useParentNoteAsProject: false, useParentHeaderAsProject: false, inheritParentTaskProperties: false, diff --git a/src/settings/settingsPersistence.ts b/src/settings/settingsPersistence.ts index 64625cc6..a04d3c5b 100644 --- a/src/settings/settingsPersistence.ts +++ b/src/settings/settingsPersistence.ts @@ -1,7 +1,7 @@ import { normalizePath } from "obsidian"; import { DEFAULT_NLP_TRIGGERS, DEFAULT_SETTINGS } from "./defaults"; import { hasMissingMigratedSettings } from "./settingsMigration"; -import type { TaskNotesSettings } from "../types/settings"; +import type { TaskCreationDefaults, TaskNotesSettings } from "../types/settings"; import { initializeFieldConfig } from "../utils/fieldConfigDefaults"; import { createTaskNotesLogger } from "../utils/tasknotesLogger"; @@ -39,6 +39,10 @@ export type SettingsBuildResult = { shouldPersistMigratedSettings: boolean; }; +function hasOwnSetting(settings: T, key: PropertyKey): boolean { + return Object.prototype.hasOwnProperty.call(settings, key); +} + function delay(ms: number): Promise { return new Promise((resolve) => window.setTimeout(resolve, ms)); } @@ -159,12 +163,44 @@ function migrateLoadedSettingsData(data: LoadedSettingsData | null): LoadedSetti return migratedData; } +function shouldMigrateParentNoteTaskCreationDefault( + loadedData: LoadedSettingsData | null +): boolean { + const loadedDefaults = loadedData?.taskCreationDefaults; + return Boolean( + loadedDefaults && + !hasOwnSetting(loadedDefaults, "useParentNoteForTaskCreation") && + typeof loadedDefaults.useParentNoteAsProject === "boolean" + ); +} + +function buildTaskCreationDefaults( + loadedDefaults: LoadedSettingsData["taskCreationDefaults"] | undefined +): TaskCreationDefaults { + const defaults: TaskCreationDefaults = { + ...DEFAULT_SETTINGS.taskCreationDefaults, + ...(loadedDefaults || {}), + }; + + if ( + loadedDefaults && + !hasOwnSetting(loadedDefaults, "useParentNoteForTaskCreation") && + typeof loadedDefaults.useParentNoteAsProject === "boolean" + ) { + defaults.useParentNoteForTaskCreation = loadedDefaults.useParentNoteAsProject; + } + + return defaults; +} + export function buildSettingsFromLoadedData(data: LoadedSettingsData | null): SettingsBuildResult { const loadedData = migrateLoadedSettingsData(data); const migratedLegacyCustomFilenameTemplate = data?.taskFilenameFormat !== "custom" && data?.customFilenameTemplate === "{title}" && loadedData?.customFilenameTemplate === "{{title}}"; + const migratedParentNoteTaskCreationDefault = + shouldMigrateParentNoteTaskCreationDefault(loadedData); const settings: TaskNotesSettings = { ...DEFAULT_SETTINGS, @@ -173,10 +209,7 @@ export function buildSettingsFromLoadedData(data: LoadedSettingsData | null): Se ...DEFAULT_SETTINGS.fieldMapping, ...(loadedData?.fieldMapping || {}), }, - taskCreationDefaults: { - ...DEFAULT_SETTINGS.taskCreationDefaults, - ...(loadedData?.taskCreationDefaults || {}), - }, + taskCreationDefaults: buildTaskCreationDefaults(loadedData?.taskCreationDefaults), calendarViewSettings: { ...DEFAULT_SETTINGS.calendarViewSettings, ...(loadedData?.calendarViewSettings || {}), @@ -206,7 +239,9 @@ export function buildSettingsFromLoadedData(data: LoadedSettingsData | null): Se return { settings, shouldPersistMigratedSettings: - hasMissingMigratedSettings(loadedData) || migratedLegacyCustomFilenameTemplate, + hasMissingMigratedSettings(loadedData) || + migratedLegacyCustomFilenameTemplate || + migratedParentNoteTaskCreationDefault, }; } diff --git a/src/settings/tabs/taskProperties/projectsPropertyCard.ts b/src/settings/tabs/taskProperties/projectsPropertyCard.ts index 90b55c25..2adf122f 100644 --- a/src/settings/tabs/taskProperties/projectsPropertyCard.ts +++ b/src/settings/tabs/taskProperties/projectsPropertyCard.ts @@ -103,7 +103,14 @@ export function renderProjectsPropertyCard( translate ); - // Use parent note as project toggle + const useParentNoteForTaskCreationToggle = createCardToggle( + plugin.settings.taskCreationDefaults.useParentNoteForTaskCreation, + (value) => { + plugin.settings.taskCreationDefaults.useParentNoteForTaskCreation = value; + save(); + } + ); + const useParentNoteToggle = createCardToggle( plugin.settings.taskCreationDefaults.useParentNoteAsProject, (value) => { @@ -152,7 +159,13 @@ export function renderProjectsPropertyCard( fullWidth: true, }, { - label: translate("settings.taskProperties.projectsCard.useParentNote"), + label: translate( + "settings.taskProperties.projectsCard.useParentNoteForTaskCreation" + ), + input: useParentNoteForTaskCreationToggle, + }, + { + label: translate("settings.taskProperties.projectsCard.useParentNoteForInlineTasks"), input: useParentNoteToggle, }, { diff --git a/src/types/settings.ts b/src/types/settings.ts index 41b728d6..f0c80f3c 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -279,7 +279,8 @@ export interface TaskCreationDefaults { defaultContexts: string; // Comma-separated list defaultTags: string; // Comma-separated list defaultProjects: string; // Comma-separated list of project links - useParentNoteAsProject: boolean; // Use the parent note as a project during instant conversion + useParentNoteForTaskCreation: boolean; // Use the active note as a project for normal task creation + useParentNoteAsProject: boolean; // Use the parent note as a project during inline creation and instant conversion useParentHeaderAsProject: boolean; // Use the closest markdown heading as a project during instant conversion inheritParentTaskProperties: boolean; // Copy parent task projects, contexts, priority, and tags when creating subtasks defaultTimeEstimate: number; // minutes, 0 = no default diff --git a/src/utils/taskCreationPrepopulation.ts b/src/utils/taskCreationPrepopulation.ts index 21577e08..3fbd3a1e 100644 --- a/src/utils/taskCreationPrepopulation.ts +++ b/src/utils/taskCreationPrepopulation.ts @@ -1,4 +1,21 @@ import type { TaskInfo } from "../types"; +import type { TaskCreationDefaults } from "../types/settings"; + +export type ParentNoteProjectDefaultContext = "task-creation" | "inline-creation"; + +export function shouldApplyParentNoteProjectDefault( + defaults: Pick< + TaskCreationDefaults, + "useParentNoteForTaskCreation" | "useParentNoteAsProject" + >, + context: ParentNoteProjectDefaultContext +): boolean { + if (context === "task-creation") { + return defaults.useParentNoteForTaskCreation; + } + + return defaults.useParentNoteAsProject; +} export function applyParentNoteProjectDefault( prePopulatedValues?: Partial, diff --git a/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts b/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts index c2c073db..b6756f01 100644 --- a/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts +++ b/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts @@ -1,4 +1,5 @@ import { InstantTaskConvertService } from "../../../src/services/InstantTaskConvertService"; +import { DEFAULT_TASK_CREATION_DEFAULTS } from "../../../src/settings/defaults"; import { PluginFactory } from "../../helpers/mock-factories"; import { TFile } from "../../__mocks__/obsidian"; @@ -11,7 +12,9 @@ describe("Issue #1705: inline conversion parent note project", () => { ...PluginFactory.createMockPlugin().settings, useDefaultsOnInstantConvert: true, taskCreationDefaults: { + ...DEFAULT_TASK_CREATION_DEFAULTS, defaultProjects: "", + useParentNoteForTaskCreation: false, useParentNoteAsProject: true, defaultContexts: "", defaultTags: "", @@ -48,4 +51,44 @@ describe("Issue #1705: inline conversion parent note project", () => { expect(createdTaskData.parentNote).toBe("[[Projects/Home]]"); expect(createdTaskData.creationContext).toBe("inline-conversion"); }); + + it("does not use the normal task creation toggle for instant conversion", async () => { + let createdTaskData: any = null; + const currentFile = new TFile("Projects/Home.md"); + const plugin = PluginFactory.createMockPlugin({ + settings: { + ...PluginFactory.createMockPlugin().settings, + useDefaultsOnInstantConvert: true, + taskCreationDefaults: { + ...DEFAULT_TASK_CREATION_DEFAULTS, + useParentNoteForTaskCreation: true, + useParentNoteAsProject: false, + }, + }, + }); + + plugin.app.workspace.getActiveFile = jest.fn().mockReturnValue(currentFile); + plugin.app.fileManager.generateMarkdownLink = jest + .fn() + .mockReturnValue("[[Projects/Home]]"); + plugin.taskService.createTask = jest.fn().mockImplementation(async (taskData) => { + createdTaskData = taskData; + return { + file: new TFile("Tasks/Follow up.md"), + taskInfo: { title: taskData.title }, + }; + }); + + const service = new InstantTaskConvertService( + plugin, + plugin.statusManager, + plugin.priorityManager + ); + + await (service as any).createTaskFile({ title: "Follow up", isCompleted: false }); + + expect(createdTaskData.projects ?? []).toEqual([]); + expect(createdTaskData.parentNote).toBe("[[Projects/Home]]"); + expect(createdTaskData.creationContext).toBe("inline-conversion"); + }); }); diff --git a/tests/unit/modals/taskCreationFormState.test.ts b/tests/unit/modals/taskCreationFormState.test.ts index c9ad9ca2..7d90746e 100644 --- a/tests/unit/modals/taskCreationFormState.test.ts +++ b/tests/unit/modals/taskCreationFormState.test.ts @@ -9,6 +9,7 @@ function defaults(overrides: Partial = {}): TaskCreationDe defaultContexts: "", defaultTags: "", defaultProjects: "", + useParentNoteForTaskCreation: false, useParentNoteAsProject: false, useParentHeaderAsProject: false, inheritParentTaskProperties: false, @@ -20,6 +21,8 @@ function defaults(overrides: Partial = {}): TaskCreationDe defaultScheduledTime: "none", bodyTemplate: "", useBodyTemplate: false, + occurrenceBodyTemplate: "", + useOccurrenceBodyTemplate: false, defaultReminders: [], ...overrides, }; diff --git a/tests/unit/settings/settingsPersistence.test.ts b/tests/unit/settings/settingsPersistence.test.ts index ac4ef07f..0f3aab2f 100644 --- a/tests/unit/settings/settingsPersistence.test.ts +++ b/tests/unit/settings/settingsPersistence.test.ts @@ -131,6 +131,42 @@ describe("settings persistence helpers", () => { expect(shouldPersistMigratedSettings).toBe(true); }); + it("derives the normal task creation parent-note project setting from the legacy shared setting", () => { + const legacyTaskCreationDefaults: Partial = { + ...DEFAULT_SETTINGS.taskCreationDefaults, + useParentNoteAsProject: true, + }; + delete legacyTaskCreationDefaults.useParentNoteForTaskCreation; + const { settings, shouldPersistMigratedSettings } = buildSettingsFromLoadedData({ + fieldMapping: DEFAULT_SETTINGS.fieldMapping, + calendarViewSettings: DEFAULT_SETTINGS.calendarViewSettings, + commandFileMapping: DEFAULT_SETTINGS.commandFileMapping, + taskCreationDefaults: + legacyTaskCreationDefaults as TaskNotesSettings["taskCreationDefaults"], + }); + + expect(settings.taskCreationDefaults.useParentNoteAsProject).toBe(true); + expect(settings.taskCreationDefaults.useParentNoteForTaskCreation).toBe(true); + expect(shouldPersistMigratedSettings).toBe(true); + }); + + it("preserves an explicit normal task creation parent-note project setting", () => { + const { settings, shouldPersistMigratedSettings } = buildSettingsFromLoadedData({ + fieldMapping: DEFAULT_SETTINGS.fieldMapping, + calendarViewSettings: DEFAULT_SETTINGS.calendarViewSettings, + commandFileMapping: DEFAULT_SETTINGS.commandFileMapping, + taskCreationDefaults: { + ...DEFAULT_SETTINGS.taskCreationDefaults, + useParentNoteAsProject: true, + useParentNoteForTaskCreation: false, + }, + }); + + expect(settings.taskCreationDefaults.useParentNoteAsProject).toBe(true); + expect(settings.taskCreationDefaults.useParentNoteForTaskCreation).toBe(false); + expect(shouldPersistMigratedSettings).toBe(false); + }); + it("merges only known settings keys into saved data while preserving other persisted data", () => { const settings = { ...DEFAULT_SETTINGS,