split parent note project defaults

This commit is contained in:
callumalpass 2026-06-08 19:37:50 +10:00
parent 88a0c350ca
commit 47f99ea266
11 changed files with 190 additions and 20 deletions

View file

@ -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

View file

@ -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",

View file

@ -8,6 +8,9 @@ import {
TFile,
getLanguage,
} from "obsidian";
type Nullable<T> = 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<TaskInfo>) {
new TaskCreationModal(this.app, this, {
prePopulatedValues: this.applyParentNoteProjectDefault(prePopulatedValues),
prePopulatedValues: this.applyParentNoteProjectDefault(
prePopulatedValues,
"task-creation"
),
}).open();
}
private applyParentNoteProjectDefault(
prePopulatedValues?: Partial<TaskInfo>
prePopulatedValues: Partial<TaskInfo> | undefined,
context: ParentNoteProjectDefaultContext
): Partial<TaskInfo> | 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<TFile>
): Promise<void> {
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)

View file

@ -108,6 +108,7 @@ export const DEFAULT_TASK_CREATION_DEFAULTS: TaskCreationDefaults = {
defaultContexts: "",
defaultTags: "",
defaultProjects: "",
useParentNoteForTaskCreation: false,
useParentNoteAsProject: false,
useParentHeaderAsProject: false,
inheritParentTaskProperties: false,

View file

@ -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<T extends object>(settings: T, key: PropertyKey): boolean {
return Object.prototype.hasOwnProperty.call(settings, key);
}
function delay(ms: number): Promise<void> {
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,
};
}

View file

@ -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,
},
{

View file

@ -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

View file

@ -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<TaskInfo>,

View file

@ -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");
});
});

View file

@ -9,6 +9,7 @@ function defaults(overrides: Partial<TaskCreationDefaults> = {}): TaskCreationDe
defaultContexts: "",
defaultTags: "",
defaultProjects: "",
useParentNoteForTaskCreation: false,
useParentNoteAsProject: false,
useParentHeaderAsProject: false,
inheritParentTaskProperties: false,
@ -20,6 +21,8 @@ function defaults(overrides: Partial<TaskCreationDefaults> = {}): TaskCreationDe
defaultScheduledTime: "none",
bodyTemplate: "",
useBodyTemplate: false,
occurrenceBodyTemplate: "",
useOccurrenceBodyTemplate: false,
defaultReminders: [],
...overrides,
};

View file

@ -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<TaskNotesSettings["taskCreationDefaults"]> = {
...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,