mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
split parent note project defaults
This commit is contained in:
parent
88a0c350ca
commit
47f99ea266
11 changed files with 190 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
26
src/main.ts
26
src/main.ts
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export const DEFAULT_TASK_CREATION_DEFAULTS: TaskCreationDefaults = {
|
|||
defaultContexts: "",
|
||||
defaultTags: "",
|
||||
defaultProjects: "",
|
||||
useParentNoteForTaskCreation: false,
|
||||
useParentNoteAsProject: false,
|
||||
useParentHeaderAsProject: false,
|
||||
inheritParentTaskProperties: false,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue