Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/HTTP_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ Update task with partial payload.

Configured TaskNotes user fields can be updated either by their frontmatter property key or via `customProperties`.

Sending an empty array for `contexts` or `blockedBy` clears the corresponding frontmatter field.

```bash
curl -X PUT "http://localhost:8080/api/tasks/TaskNotes%2FTasks%2FReview%20docs.md" \
-H "Content-Type: application/json" \
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
```

-->

## Fixed

- Fixed `PUT /api/tasks/:id` ignoring empty arrays for `contexts` and `blockedBy`: sending `{"contexts": []}` or `{"blockedBy": []}` now clears the corresponding frontmatter field instead of silently leaving the previous value in place. The deletion pass previously fired only on a literal `undefined`, which JSON cannot express, so HTTP clients had no way to clear these fields. Thanks to @tgrosinger for the contribution.
4 changes: 2 additions & 2 deletions src/services/task-service/taskUpdatePlanning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ function removeUnsetMappedFields(
}
if (
Object.prototype.hasOwnProperty.call(updates, "contexts") &&
updates.contexts === undefined
(!Array.isArray(updates.contexts) || updates.contexts.length === 0)
) {
delete frontmatter[fieldMapper.toUserField("contexts")];
}
Expand Down Expand Up @@ -356,7 +356,7 @@ function removeUnsetMappedFields(
}
if (
Object.prototype.hasOwnProperty.call(updates, "blockedBy") &&
updates.blockedBy === undefined
(!Array.isArray(updates.blockedBy) || updates.blockedBy.length === 0)
) {
delete frontmatter[fieldMapper.toUserField("blockedBy")];
}
Expand Down
79 changes: 79 additions & 0 deletions tests/unit/services/taskUpdatePlanning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,85 @@ describe("taskUpdatePlanning", () => {
expect(result.finalTags).toEqual([]);
});

it("clears contexts and blockedBy when an update explicitly sets them to empty arrays", () => {
const frontmatter: Record<string, unknown> = {
title: "Old",
status: "open",
contexts: ["old"],
blockedBy: [{ uid: "[[Other]]", reltype: "FINISHTOSTART" }],
tags: ["task"],
};

applyTaskUpdateFrontmatterChange({
frontmatter,
originalTask: createTask(),
updates: { contexts: [], blockedBy: [] },
recurrenceUpdates: {},
dateModified: "2026-05-19T09:00:00.000Z",
fieldMapper: createFieldMapper(),
taskIdentification: {
method: "tag",
tag: "task",
propertyName: "",
propertyValue: "",
},
storeTitleInFilename: false,
updateCompletedDateInFrontmatter: jest.fn(),
});

expect(frontmatter).not.toHaveProperty("contexts");
expect(frontmatter).not.toHaveProperty("blockedBy");
});

it("leaves contexts and blockedBy alone when the update omits them or keeps values", () => {
const frontmatter: Record<string, unknown> = {
title: "Old",
status: "open",
contexts: ["old"],
blockedBy: [{ uid: "[[Other]]", reltype: "FINISHTOSTART" }],
tags: ["task"],
};

applyTaskUpdateFrontmatterChange({
frontmatter,
originalTask: createTask(),
updates: { title: "Renamed" },
recurrenceUpdates: {},
dateModified: "2026-05-19T09:00:00.000Z",
fieldMapper: createFieldMapper(),
taskIdentification: {
method: "tag",
tag: "task",
propertyName: "",
propertyValue: "",
},
storeTitleInFilename: false,
updateCompletedDateInFrontmatter: jest.fn(),
});

expect(frontmatter.contexts).toEqual(["old"]);
expect(frontmatter.blockedBy).toEqual([{ uid: "[[Other]]", reltype: "FINISHTOSTART" }]);

applyTaskUpdateFrontmatterChange({
frontmatter,
originalTask: createTask(),
updates: { contexts: ["new"] },
recurrenceUpdates: {},
dateModified: "2026-05-19T09:00:00.000Z",
fieldMapper: createFieldMapper(),
taskIdentification: {
method: "tag",
tag: "task",
propertyName: "",
propertyValue: "",
},
storeTitleInFilename: false,
updateCompletedDateInFrontmatter: jest.fn(),
});

expect(frontmatter.contexts).toEqual(["new"]);
});

it("builds the returned task state from the same planned mutation", () => {
const updated = buildUpdatedTaskFromPlan({
originalTask: createTask({ completedDate: undefined }),
Expand Down
Loading