From 99d82e96244b7bbb9b9dc5070e4af60a5ed02e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Normen=20Mu=CC=88ller?= Date: Wed, 7 Jan 2026 21:36:53 +0100 Subject: [PATCH] fix: persist project removals --- PR.md | 20 ++++++++++++++++++++ src/modals/TaskEditModal.ts | 21 ++++++++++++++++++--- src/services/TaskService.ts | 9 +++++++++ tests/unit/services/TaskService.test.ts | 20 +++++++++++++++++++- 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 PR.md diff --git a/PR.md b/PR.md new file mode 100644 index 00000000..a034ecf7 --- /dev/null +++ b/PR.md @@ -0,0 +1,20 @@ +# fix/projects-removal + +## Ensure project removals persist from the edit dialog + +When removing project assignments in the task edit modal, the frontmatter now updates correctly (including fully removing the property when the list is emptied). We also normalize link comparisons so different link syntaxes (e.g., angle‑bracket markdown links) do not cause false change detection. + +Examples (illustrative): + +- Removing the last project now deletes the `projects` property from frontmatter. +- `[Project]()` and `[[path/to/Project]]` compare as the same target for change detection. + +## Changelog + +- Normalize project link comparisons in the edit modal to handle angle‑bracket markdown links. +- Persist empty project lists by explicitly removing the `projects` field from frontmatter. +- Add a unit test to ensure empty project updates remove the property. + +## Tests + +- `./node_modules/.bin/jest tests/unit/services/TaskService.test.ts --runInBand` diff --git a/src/modals/TaskEditModal.ts b/src/modals/TaskEditModal.ts index a0252dd5..3d811a0d 100644 --- a/src/modals/TaskEditModal.ts +++ b/src/modals/TaskEditModal.ts @@ -25,7 +25,7 @@ import { } from "../utils/helpers"; import { splitListPreservingLinksAndQuotes } from "../utils/stringSplit"; import { ReminderContextMenu } from "../components/ReminderContextMenu"; -import { generateLinkWithDisplay } from "../utils/linkUtils"; +import { generateLinkWithDisplay, parseLinkToPath } from "../utils/linkUtils"; import { EmbeddableMarkdownEditor } from "../editor/EmbeddableMarkdownEditor"; import { ConfirmationModal } from "./ConfirmationModal"; @@ -761,8 +761,23 @@ export class TaskEditModal extends TaskModal { const newProjects = splitListPreservingLinksAndQuotes(this.projects); const oldProjects = this.task.projects || []; - if (JSON.stringify(newProjects.sort()) !== JSON.stringify(oldProjects.sort())) { - changes.projects = newProjects.length > 0 ? newProjects : undefined; + const normalizeProjectList = (projects: string[]): string[] => + projects + .map((project) => { + if (!project || typeof project !== "string") return ""; + const trimmed = project.trim(); + if (!trimmed) return ""; + return parseLinkToPath(trimmed).trim(); + }) + .filter((project) => project.length > 0); + + const normalizedNewProjects = normalizeProjectList(newProjects).sort(); + const normalizedOldProjects = normalizeProjectList(oldProjects).sort(); + + if ( + JSON.stringify(normalizedNewProjects) !== JSON.stringify(normalizedOldProjects) + ) { + changes.projects = newProjects.length > 0 ? newProjects : []; } // Parse and compare tags diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts index c93a3866..56c00c63 100644 --- a/src/services/TaskService.ts +++ b/src/services/TaskService.ts @@ -1414,6 +1414,15 @@ export class TaskService { delete frontmatter[this.plugin.fieldMapper.toUserField("scheduled")]; if (updates.hasOwnProperty("contexts") && updates.contexts === undefined) delete frontmatter[this.plugin.fieldMapper.toUserField("contexts")]; + if (updates.hasOwnProperty("projects")) { + const projectsField = this.plugin.fieldMapper.toUserField("projects"); + const projectsToSet = Array.isArray(updates.projects) ? updates.projects : []; + if (projectsToSet.length > 0) { + frontmatter[projectsField] = projectsToSet; + } else { + delete frontmatter[projectsField]; + } + } if (updates.hasOwnProperty("timeEstimate") && updates.timeEstimate === undefined) delete frontmatter[this.plugin.fieldMapper.toUserField("timeEstimate")]; if (updates.hasOwnProperty("completedDate") && updates.completedDate === undefined) diff --git a/tests/unit/services/TaskService.test.ts b/tests/unit/services/TaskService.test.ts index ec0a9bfc..b5b47611 100644 --- a/tests/unit/services/TaskService.test.ts +++ b/tests/unit/services/TaskService.test.ts @@ -987,6 +987,24 @@ describe('TaskService', () => { expect(mockPlugin.app.fileManager.processFrontMatter).toHaveBeenCalled(); }); + it('should remove projects when set to an empty array', async () => { + const taskWithProjects = TaskFactory.createTask({ + projects: ['[[Project Alpha]]'] + }); + mockFile = new TFile(taskWithProjects.path); + mockPlugin.app.vault.getAbstractFileByPath.mockReturnValue(mockFile); + + let capturedFrontmatter: any = {}; + mockPlugin.app.fileManager.processFrontMatter.mockImplementation(async (_file, fn) => { + capturedFrontmatter = { projects: ['[[Project Alpha]]'] }; + fn(capturedFrontmatter); + }); + + await taskService.updateTask(taskWithProjects, { projects: [] }); + + expect(capturedFrontmatter.projects).toBeUndefined(); + }); + it('should preserve tags when not being updated', async () => { const taskWithTags = TaskFactory.createTask({ tags: ['task', 'important'] }); const updates = { priority: 'high' }; @@ -1145,4 +1163,4 @@ describe('TaskService', () => { expect(completed.priority).toBe('high'); }); }); -}); \ No newline at end of file +});