mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
add quick actions project assignment
This commit is contained in:
parent
5a8eb76fb1
commit
cac4f5e170
3 changed files with 181 additions and 14 deletions
|
|
@ -41,6 +41,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
|
|||
- (#2060) Added date-type custom fields to the task right-click menu under **Custom dates**. Thanks to @chmac for requesting this.
|
||||
- (#2067) Added an **Edit task** action to the task card context menu, including inline task cards. Thanks to @DarkCellar for requesting this.
|
||||
- (#2068) Added `reminders` input support to the MCP `tasknotes_create_task` and `tasknotes_update_task` tools. Thanks to @Spirit597 for requesting this.
|
||||
- (#2069) Added an **Add project** action to the Quick Actions palette. Thanks to @chmac for requesting this.
|
||||
|
||||
## Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
FuzzyMatch,
|
||||
setIcon,
|
||||
Notice,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
moment as obsidianMoment,
|
||||
} from "obsidian";
|
||||
|
|
@ -15,6 +16,8 @@ import {
|
|||
openOrCreateOccurrenceNote,
|
||||
} from "../ui/occurrenceNoteActions";
|
||||
import { createTaskNotesLogger } from "../utils/tasknotesLogger";
|
||||
import { ProjectSelectModal } from "./ProjectSelectModal";
|
||||
import { addTaskToProject } from "../services/taskRelationshipActions";
|
||||
|
||||
const tasknotesLogger = createTaskNotesLogger({ tag: "Modals/TaskActionPaletteModal" });
|
||||
|
||||
|
|
@ -225,21 +228,35 @@ export class TaskActionPaletteModal extends FuzzySuggestModal<TaskAction> {
|
|||
}
|
||||
|
||||
// Organization actions
|
||||
actions.push({
|
||||
id: "toggle-archive",
|
||||
title: this.task.archived ? "Unarchive task" : "Archive task",
|
||||
description: this.task.archived
|
||||
? "Move task back to active tasks"
|
||||
: "Archive this task",
|
||||
icon: this.task.archived ? "archive-restore" : "archive",
|
||||
category: "organization",
|
||||
keywords: ["archive", this.task.archived ? "unarchive" : "archive", "organize"],
|
||||
isApplicable: () => true,
|
||||
execute: async (task) => {
|
||||
await this.plugin.toggleTaskArchive(task);
|
||||
new Notice(task.archived ? "Task unarchived" : "Task archived");
|
||||
actions.push(
|
||||
{
|
||||
id: "add-project",
|
||||
title: "Add project",
|
||||
description: "Select a project note and add it to this task",
|
||||
icon: "folder-plus",
|
||||
category: "organization",
|
||||
keywords: ["project", "organization", "add", "link"],
|
||||
isApplicable: () => true,
|
||||
execute: async (task) => {
|
||||
this.openProjectSelector(task);
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
id: "toggle-archive",
|
||||
title: this.task.archived ? "Unarchive task" : "Archive task",
|
||||
description: this.task.archived
|
||||
? "Move task back to active tasks"
|
||||
: "Archive this task",
|
||||
icon: this.task.archived ? "archive-restore" : "archive",
|
||||
category: "organization",
|
||||
keywords: ["archive", this.task.archived ? "unarchive" : "archive", "organize"],
|
||||
isApplicable: () => true,
|
||||
execute: async (task) => {
|
||||
await this.plugin.toggleTaskArchive(task);
|
||||
new Notice(task.archived ? "Task unarchived" : "Task archived");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Recurring task actions (only for recurring tasks)
|
||||
if (this.task.recurrence) {
|
||||
|
|
@ -465,6 +482,42 @@ export class TaskActionPaletteModal extends FuzzySuggestModal<TaskAction> {
|
|||
});
|
||||
}
|
||||
|
||||
private openProjectSelector(task: TaskInfo): void {
|
||||
const selector = new ProjectSelectModal(this.plugin.app, this.plugin, (projectFile) => {
|
||||
void this.addSelectedProjectToTask(task, projectFile);
|
||||
});
|
||||
this.close();
|
||||
selector.open();
|
||||
}
|
||||
|
||||
private async addSelectedProjectToTask(
|
||||
task: TaskInfo,
|
||||
projectFile: TAbstractFile
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!(projectFile instanceof TFile)) {
|
||||
new Notice(
|
||||
this.plugin.i18n.translate(
|
||||
"contextMenus.task.organization.notices.projectSelectFailed"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await addTaskToProject(this.plugin, task, projectFile);
|
||||
} catch (error) {
|
||||
tasknotesLogger.error("Failed to add task to project:", {
|
||||
category: "persistence",
|
||||
operation: "add-task-project",
|
||||
details: { taskPath: task.path },
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
new Notice(
|
||||
this.plugin.i18n.translate("contextMenus.task.organization.notices.addToProjectFailed")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getItems(): TaskAction[] {
|
||||
// Filter to only applicable actions and sort by category and title
|
||||
return this.actions
|
||||
|
|
|
|||
113
tests/unit/issues/issue-2069-quick-actions-add-project.test.ts
Normal file
113
tests/unit/issues/issue-2069-quick-actions-add-project.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { App, TFile } from "obsidian";
|
||||
import { TaskActionPaletteModal } from "../../../src/modals/TaskActionPaletteModal";
|
||||
import { ProjectSelectModal } from "../../../src/modals/ProjectSelectModal";
|
||||
import { addTaskToProject } from "../../../src/services/taskRelationshipActions";
|
||||
import type TaskNotesPlugin from "../../../src/main";
|
||||
import type { TaskInfo } from "../../../src/types";
|
||||
|
||||
const mockProjectSelectModalOpen = jest.fn();
|
||||
const mockProjectSelectModalCallbacks: Array<(file: TFile) => void> = [];
|
||||
|
||||
jest.mock("../../../src/modals/ProjectSelectModal", () => ({
|
||||
ProjectSelectModal: jest.fn().mockImplementation((_app, _plugin, onChoose) => {
|
||||
mockProjectSelectModalCallbacks.push(onChoose);
|
||||
return {
|
||||
open: mockProjectSelectModalOpen,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/services/taskRelationshipActions", () => ({
|
||||
addTaskToProject: jest.fn(),
|
||||
}));
|
||||
|
||||
function createTask(overrides: Partial<TaskInfo> = {}): TaskInfo {
|
||||
return {
|
||||
id: "Tasks/quick-actions-project.md",
|
||||
path: "Tasks/quick-actions-project.md",
|
||||
title: "Quick Actions project task",
|
||||
status: "open",
|
||||
priority: "normal",
|
||||
projects: [],
|
||||
...overrides,
|
||||
} as TaskInfo;
|
||||
}
|
||||
|
||||
function createPlugin(): TaskNotesPlugin {
|
||||
const app = new App();
|
||||
return {
|
||||
app,
|
||||
i18n: {
|
||||
translate: jest.fn((key: string) => key),
|
||||
},
|
||||
statusManager: {
|
||||
getAllStatuses: jest.fn(() => [{ value: "open", label: "Open" }]),
|
||||
getNonCompletionStatuses: jest.fn(() => [{ value: "open", label: "Open" }]),
|
||||
},
|
||||
priorityManager: {
|
||||
getAllPriorities: jest.fn(() => [{ value: "normal", label: "Normal" }]),
|
||||
},
|
||||
cacheManager: {
|
||||
getTaskInfo: jest.fn(),
|
||||
},
|
||||
updateTaskProperty: jest.fn(),
|
||||
getActiveTimeSession: jest.fn(() => null),
|
||||
stopTimeTracking: jest.fn(),
|
||||
startTimeTracking: jest.fn(),
|
||||
openDueDateModal: jest.fn(),
|
||||
openScheduledDateModal: jest.fn(),
|
||||
openTimeEntryEditor: jest.fn(),
|
||||
toggleTaskArchive: jest.fn(),
|
||||
openTaskEditModal: jest.fn(),
|
||||
} as unknown as TaskNotesPlugin;
|
||||
}
|
||||
|
||||
function createModal(task: TaskInfo, plugin: TaskNotesPlugin): TaskActionPaletteModal {
|
||||
return new TaskActionPaletteModal(
|
||||
new App() as never,
|
||||
task,
|
||||
plugin,
|
||||
new Date("2026-06-23T10:00:00Z")
|
||||
);
|
||||
}
|
||||
|
||||
describe("Issue #2069: Quick Actions add project action", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockProjectSelectModalCallbacks.length = 0;
|
||||
});
|
||||
|
||||
it("exposes Add project as an organization action", () => {
|
||||
const modal = createModal(createTask(), createPlugin());
|
||||
|
||||
const action = modal.getItems().find((item) => item.id === "add-project");
|
||||
|
||||
expect(action).toEqual(
|
||||
expect.objectContaining({
|
||||
title: "Add project",
|
||||
category: "organization",
|
||||
icon: "folder-plus",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("opens the project selector and adds the chosen project through the shared helper", async () => {
|
||||
const task = createTask();
|
||||
const plugin = createPlugin();
|
||||
const projectFile = new TFile("Projects/Alpha.md");
|
||||
const modal = createModal(task, plugin);
|
||||
const closeSpy = jest.spyOn(modal, "close");
|
||||
const action = modal.getItems().find((item) => item.id === "add-project");
|
||||
|
||||
await action?.execute(task, plugin, new Date("2026-06-23T10:00:00Z"));
|
||||
mockProjectSelectModalCallbacks[0](projectFile);
|
||||
|
||||
expect(ProjectSelectModal).toHaveBeenCalledWith(plugin.app, plugin, expect.any(Function));
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockProjectSelectModalOpen).toHaveBeenCalledTimes(1);
|
||||
expect(closeSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockProjectSelectModalOpen.mock.invocationCallOrder[0]
|
||||
);
|
||||
expect(addTaskToProject).toHaveBeenCalledWith(plugin, task, projectFile);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue