Skip to content

Commit d7721f3

Browse files
authored
Merge pull request #3528 from justise/edit-modal-api-v2
feat: Add the ability to launch the task modal to edit an existing line
2 parents add6f23 + 2660924 commit d7721f3

File tree

10 files changed

+228
-19
lines changed

10 files changed

+228
-19
lines changed

docs/Advanced/Tasks Api.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ export interface TasksApiV1 {
3434
*/
3535
createTaskLineModal(): Promise<string>;
3636

37+
/**
38+
* Opens the Tasks UI pre-filled with the provided task line for editing.
39+
* Does not edit the task line in the file, but returns the edited task line as a Markdown string.
40+
*
41+
* @param taskLine The markdown string of the task line to edit
42+
* @returns {Promise<string>} A promise that contains the Markdown string for the edited task or
43+
* an empty string in the case where the data entry was cancelled.
44+
*/
45+
editTaskLineModal(taskLine: string): Promise<string>;
46+
3747
/**
3848
* Executes the 'Tasks: Toggle task done' command on the supplied line string
3949
*
@@ -55,8 +65,6 @@ This method was introduced in Tasks 2.0.0.
5565
This method opens the Tasks [[Create or edit Task|Create or edit task UI]] and returns the Markdown for the task entered.
5666
If data entry is cancelled, an empty string is returned.
5767

58-
### Basic usage
59-
6068
```javascript
6169
const tasksApi = this.app.plugins.plugins['obsidian-tasks-plugin'].apiV1;
6270
let taskLine = await tasksApi.createTaskLineModal();
@@ -82,6 +90,26 @@ to automatically add tasks to a specific file.
8290

8391
See [[QuickAdd#Launching the Edit task modal via QuickAdd|Launching the Edit task modal via QuickAdd]] for full details of how to do this.
8492

93+
## `editTaskLineModal(taskLine: string): Promise<string>;`
94+
95+
> [!released]
96+
This method was introduced in Tasks X.X.X.
97+
98+
This method opens the Tasks [[Create or edit Task|Create or edit task UI]] with the provided task line pre-filled for editing.
99+
If data entry is cancelled, an empty string is returned.
100+
101+
```javascript
102+
const tasksApi = this.app.plugins.plugins['obsidian-tasks-plugin'].apiV1;
103+
let editedTaskLine = await tasksApi.editTaskLineModal('- [ ] My existing task');
104+
105+
// Do whatever you want with the returned value.
106+
// It's just a string containing the Markdown for the edited task.
107+
console.log(editedTaskLine);
108+
```
109+
110+
> [!warning]
111+
> This function returns a `Promise` - always `await` the result!
112+
85113
## `executeToggleTaskDoneCommand: (line: string, path: string) => string;`
86114

87115
> [!released]
@@ -149,8 +177,6 @@ This can be used, for example, to display the Auto-Suggest on non-task lines. [S
149177
150178
## Limitations of the Tasks API
151179

152-
- Editing tasks:
153-
- It is not yet possible to use the API to edit an *existing task line* with Tasks [[Create or edit Task|Create or edit task UI]]. We are tracking this in [issue #1945](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1945).
154180
- Auto Suggest:
155181
- It is not yet possible for [[auto-suggest]] to add [[Task Dependencies|dependencies]] when Auto-Suggest is used in [[Kanban plugin]] cards - or any other plugins that use the [[Tasks Api#Auto-Suggest Integration|Auto-Suggest Integration]]. We are tracking this in [issue #3274](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/3274).
156182
- Searching tasks:

docs/Other Plugins/QuickAdd.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
publish: true
33
aliases:
4-
- Advanced/Quickadd
4+
- Advanced/Quickadd
55
---
66

77
# QuickAdd
88

99
<span class="related-pages">#plugin/quickadd</span>
1010

11-
## Launching the Edit task modal via QuickAdd
11+
## Launching the Create task modal via QuickAdd
1212

1313
This section shows how to use QuickAdd with the [[Create or edit Task]] modal to automatically add tasks to a specific file.
1414

@@ -24,7 +24,7 @@ Or if you would like a newline character to be added after your new task line, u
2424

2525
````markdown
2626
```js quickadd
27-
return await this.app.plugins.plugins['obsidian-tasks-plugin'].apiV1.createTaskLineModal() + '\n';
27+
return (await this.app.plugins.plugins['obsidian-tasks-plugin'].apiV1.createTaskLineModal()) + '\n';
2828
```
2929
````
3030

resources/sample_vaults/Tasks-Demo/Manual Testing/QuickAdd Tasks API Demo.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,43 @@ It's modified to:
1111
## New Tasks
1212

1313
- [ ] #task Test task added via the API!
14+
15+
# Smoke Testing the Edit Task API
16+
17+
You can quickly verify that the `editTaskLineModal()` API works as expected using the Obsidian developer console.
18+
19+
You can test the API directly in the Obsidian console
20+
21+
1. Open the developer console in Obsidian (Cmd+Opt+I on Mac).
22+
2. Get access to the Tasks API by typing `this.app.plugins.plugins['obsidian-tasks-plugin'].apiV1` in the console.
23+
3. Use the `editTaskLineModal()` method to open the edit task modal with a sample task text.
24+
25+
## Edit Tasks
26+
27+
Edit Task Command:
28+
29+
```js
30+
const tasksApi = this.app.plugins.plugins['obsidian-tasks-plugin'].apiV1;
31+
let editedTaskLine = await tasksApi.editTaskLineModal('- [ ] #task Do every day 🔼 🔁 every day ➕ 2025-07-06 ⏳ 2025-07-06');
32+
console.log(editedTaskLine);
33+
```
34+
35+
- [ ] #task Test Populated Values in Modal
36+
37+
1. Open modal with the sample task text above.
38+
2. Verify that the task properties are populated in the modal.
39+
3. Mark Task Done
40+
4. Expected
41+
42+
> - [ ] #task Do every day 🔼 🔁 every day ⏳ 2025-07-07
43+
> - [x] #task Do every day 🔼 🔁 every day ➕ 2025-07-06 ⏳ 2025-07-06 ✅ 2025-07-06
44+
45+
- [ ] #task Reverse the order of Recurring tasks in the Tasks settings
46+
47+
1. Reverse the order of Recurring tasks in the Tasks settings
48+
2. Open modal with the sample task text above.
49+
3. Change the Status to Done
50+
4. Expected
51+
52+
> - [x] #task Do every day 🔼 🔁 every day ➕ 2025-07-06 ⏳ 2025-07-06 ✅ 2025-07-06
53+
> - [ ] #task Do every day 🔼 🔁 every day ⏳ 2025-07-07

src/Api/TasksApiV1.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ export interface TasksApiV1 {
1010
*/
1111
createTaskLineModal(): Promise<string>;
1212

13+
/**
14+
* Opens the Tasks UI and returns the Markdown string for the edited
15+
* task line. Does not edit the task line in the file.
16+
*
17+
* @returns {Promise<string>} A promise that contains the Markdown string for the task edited or
18+
* an empty string, if data entry was cancelled.
19+
*/
20+
editTaskLineModal(taskLine: string): Promise<string>;
21+
1322
/**
1423
* Executes the 'Tasks: Toggle task done' command on the supplied line string
1524
*

src/Api/editTaskLineModal.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { App } from 'obsidian';
2+
import type { Task } from '../Task/Task';
3+
import { taskFromLine } from '../Commands/CreateOrEditTaskParser';
4+
import { TaskModal } from '../Obsidian/TaskModal';
5+
6+
/**
7+
* Opens the Tasks UI and returns the Markdown string for the task entered.
8+
* If the optional Markdown string for a task is passed, the form will be
9+
* populated with that task's properties.
10+
*
11+
* @param app - The Obsidian App
12+
* @param taskLine - Markdown string of the task to edit.
13+
* @param allTasks - An array of all tasks, used to populate the modal dependencies fields
14+
*
15+
* @returns {Promise<string>} A promise that contains the Markdown string for the task entered or
16+
* an empty string, if data entry was cancelled.
17+
*/
18+
export function editTaskLineModal(app: App, taskLine: string, allTasks: Task[]): Promise<string> {
19+
let resolvePromise: (input: string) => void;
20+
const waitForClose = new Promise<string>((resolve, _) => {
21+
resolvePromise = resolve;
22+
});
23+
24+
const onSubmit = (updatedTasks: Task[]): void => {
25+
const line = updatedTasks.map((task: Task) => task.toFileLineString()).join('\n');
26+
resolvePromise(line);
27+
};
28+
29+
const task = taskFromLine({ line: taskLine ?? '', path: '' });
30+
const taskModal = new TaskModal({ app, task, onSubmit, allTasks });
31+
32+
taskModal.open();
33+
return waitForClose;
34+
}

src/Api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type TasksPlugin from '../main';
22
import { toggleLine } from '../Commands/ToggleDone';
33
import { createTaskLineModal } from './createTaskLineModal';
44
import type { TasksApiV1 } from './TasksApiV1';
5+
import { editTaskLineModal } from './editTaskLineModal';
56

67
/**
78
* Factory method for API v1
@@ -15,6 +16,9 @@ export const tasksApiV1 = (plugin: TasksPlugin): TasksApiV1 => {
1516
createTaskLineModal: (): Promise<string> => {
1617
return createTaskLineModal(app, plugin.getTasks());
1718
},
19+
editTaskLineModal: (taskLine: string): Promise<string> => {
20+
return editTaskLineModal(app, taskLine, plugin.getTasks());
21+
},
1822
executeToggleTaskDoneCommand: (line: string, path: string) => toggleLine(line, path).text,
1923
};
2024
};

src/Obsidian/TaskModal.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
1-
import { App, Modal } from 'obsidian';
1+
import type { App } from 'obsidian';
2+
import { Modal } from 'obsidian';
23

34
import EditTask from '../ui/EditTask.svelte';
45
import type { Task } from '../Task/Task';
56
import { StatusRegistry } from '../Statuses/StatusRegistry';
67
import { Status } from '../Statuses/Status';
78

9+
export interface TaskModalParams {
10+
app: App;
11+
task: Task;
12+
onSubmit: (updatedTasks: Task[]) => void;
13+
allTasks: Task[];
14+
}
15+
816
export class TaskModal extends Modal {
917
public readonly task: Task;
1018
public readonly onSubmit: (updatedTasks: Task[]) => void;
1119
public readonly allTasks: Task[];
1220

13-
constructor({
14-
app,
15-
task,
16-
onSubmit,
17-
allTasks,
18-
}: {
19-
app: App;
20-
task: Task;
21-
onSubmit: (updatedTasks: Task[]) => void;
22-
allTasks: Task[];
23-
}) {
21+
constructor({ app, task, onSubmit, allTasks }: TaskModalParams) {
2422
super(app);
2523

2624
this.task = task;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { App } from 'obsidian';
2+
import type { Task } from '../../src/Task/Task';
3+
import { editTaskLineModal } from '../../src/Api/editTaskLineModal';
4+
import { taskFromLine } from '../../src/Commands/CreateOrEditTaskParser';
5+
import { TaskModal } from '../__mocks__/TaskModal';
6+
import type { TaskModalParams } from '../../src/Obsidian/TaskModal';
7+
8+
const app = {} as App;
9+
10+
const createNewTask = (line = ''): Task => {
11+
return taskFromLine({ line, path: '' });
12+
};
13+
14+
jest.mock('../../src/Obsidian/TaskModal', () => {
15+
return {
16+
TaskModal: jest.fn(({ app, task, onSubmit, allTasks }: TaskModalParams) => {
17+
return new TaskModal({ app, task, onSubmit, allTasks });
18+
}),
19+
};
20+
});
21+
22+
describe('APIv1 - editTaskLineModal', () => {
23+
beforeEach(() => {
24+
jest.clearAllMocks();
25+
});
26+
27+
it('TaskModal.open() should be called', () => {
28+
const taskLine = '- [ ] ';
29+
editTaskLineModal(app, taskLine, []);
30+
31+
expect(TaskModal.instance.open).toHaveBeenCalled();
32+
});
33+
34+
it('should return the edited Markdown', async () => {
35+
const taskLine = '- [ ] Updated Task';
36+
const taskLinePromise = editTaskLineModal(app, '- [ ] Task Name', []);
37+
38+
TaskModal.instance.onSubmit([createNewTask(taskLine)]);
39+
40+
const result = await taskLinePromise;
41+
expect(result).toEqual('- [ ] Updated Task');
42+
});
43+
44+
it('should return empty string on cancel', async () => {
45+
const taskLine = '- [ ] ';
46+
const taskLinePromise = editTaskLineModal(app, taskLine, []);
47+
48+
TaskModal.instance.cancel();
49+
50+
const result = await taskLinePromise;
51+
expect(result).toEqual('');
52+
});
53+
54+
it('should pass allTasks to TaskModal', () => {
55+
const taskLine = '- [ ] Task Name';
56+
const allTasks = [createNewTask('- [ ] Task 1')];
57+
58+
editTaskLineModal(app, taskLine, allTasks);
59+
60+
expect(TaskModal.instance.allTasks).toEqual(allTasks);
61+
});
62+
});

tests/Api/index.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import type { App } from 'obsidian';
22
import type TasksPlugin from '../../src/main';
33
import type { Task } from '../../src/Task/Task';
44
import { createTaskLineModal } from '../../src/Api/createTaskLineModal';
5+
import { editTaskLineModal } from '../../src/Api/editTaskLineModal';
56
import { tasksApiV1 } from '../../src/Api/index';
67

78
jest.mock('../../src/Api/createTaskLineModal', () => ({
89
createTaskLineModal: jest.fn(),
910
}));
1011

12+
jest.mock('../../src/Api/editTaskLineModal', () => ({
13+
editTaskLineModal: jest.fn(),
14+
}));
15+
1116
describe('definition of public Api', () => {
1217
it('should call createTaskLineModal with the app and allTasks', async () => {
1318
const task = jest.fn();
@@ -23,4 +28,20 @@ describe('definition of public Api', () => {
2328
await publicApi.createTaskLineModal();
2429
expect(createTaskLineModal).toHaveBeenCalledWith(app, tasks);
2530
});
31+
32+
it('should call editTaskLineModal with the app, taskLine and allTasks', async () => {
33+
const task = jest.fn();
34+
const app = {} as App; // Mock the app object
35+
const tasks = [task as Partial<Task>];
36+
const mockPlugin = {
37+
getTasks: () => tasks,
38+
app,
39+
} as Partial<TasksPlugin> as TasksPlugin;
40+
const taskLine = '- [ ] Task Name';
41+
42+
const publicApi = tasksApiV1(mockPlugin);
43+
44+
await publicApi.editTaskLineModal(taskLine);
45+
expect(editTaskLineModal).toHaveBeenCalledWith(app, taskLine, tasks);
46+
});
2647
});

tests/__mocks__/obsidian.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,18 @@ export function prepareSimpleSearch(query: string): (text: string) => SearchResu
221221

222222
type IconName = string;
223223
export function setIcon(_parent: HTMLElement, _iconId: IconName): void {}
224+
225+
/**
226+
* A mock implementation of the Obsidian Modal class.
227+
* Without this testing the TaskModal throws an error attempting to extend Modal
228+
*/
229+
export class Modal {
230+
public open(): void {
231+
// Mocked interface, no-op
232+
}
233+
public close(): void {
234+
// Mocked interface, no-op
235+
}
236+
public onOpen(): void {}
237+
public onClose(): void {}
238+
}

0 commit comments

Comments
 (0)