fix(projects): let an item leave a project from its project chips - #1944
Draft
h4yfans wants to merge 5 commits into
Draft
fix(projects): let an item leave a project from its project chips#1944h4yfans wants to merge 5 commits into
h4yfans wants to merge 5 commits into
Conversation
Reproduces #1941 on the surface the report names. A binary file's project membership is only ever shown by ItemProjectChips, and those chips have no remove control, so the renderer cases and the E2E file case fail against current source. The E2E spec drives both item kinds through the real app, and the main cases pin the file branch of unlinkProjectItem that the missing UI never reached.
A file assigned to a project could only be unassigned by deleting the project or the file. Three surfaces add a file to a project (a sidebar drag, the file page's Add to project, the project hub's paperclip import) and none could undo it: the file page shows the membership as chips that only navigate, and a file has no frontmatter, so unlike a markdown note it has no project property row to clear instead. Every chip now carries a remove control that calls the unlinkProjectItem IPC that already existed and had no caller outside the calendar. For a file that deletes the project_links row, which is the whole of its membership and cannot be resurrected by a reindex; for a markdown note the same call rewrites the name out of frontmatter, so the file stays the payload sync carries. The project name is now plain text where the row is mounted without onProjectClick, as the file page does, rather than a second tab stop that does nothing beside the real control. Closes #1941
Also drops the claim that a project chip jumps to the project hub. The only place those chips are mounted is the file page, which has never passed a click handler to them.
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
The calendar widget refresh suite faked the clock onto 2026-08-31, the day it was written. use-today snapshots the local date into module scope at import and re-reads the wall clock for its first subscriber, so on every later day that fake clock arrives as a midnight rollover. todayCalendarRange moves, the useCalendarRange query key moves with it, and the widget fetches a second day during mount, which is the second getRange call the first test counted. Derive the fixture clock and its event hours from the real local date instead, so the mount no longer straddles a day boundary. Local date fields rather than a UTC instant, because far enough from UTC the two name different days.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The file sat in the tsconfig.test.web.json exclude backlog, so the four new cases compiled nowhere and the typecheck:test gate said nothing about them. Taking it off the list surfaced one real error: the hoisted onProjectUpdated mock declared no parameters, which the subscribe-capture case then could not implement. That list only ever shrinks. Also documents how a calendar event leaves a project, which its own form has always supported through No project and the per-chip remove.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A file assigned to a project could not be unassigned. The only exits were deleting the project or deleting the file, which is what the reporter hit on both the built-in Inbox project and one they had made themselves.
The gap is one-sided UI, not a broken write. Three surfaces add a file to a project: a drag onto a sidebar project row (
sortable-project-item.tsx:139), the file page's Add to project dialog (add-file-to-project-dialog.tsx:44), and the project hub's paperclip import (project-capture-input.tsx:84). The file page then shows that membership as chips rendered byItemProjectChips(pages/file.tsx:217and:238), and those chips were plain buttons with no remove control. Thetasks:project-unlink-itemIPC has existed and worked the whole time; outside the calendar'sevent-project-field.tsxnothing in the renderer ever called it. A markdown note has an escape hatch a file does not, itsprojectfrontmatter property and the chip remove control inProjectEditor, which is why the report is about files.Every chip in
ItemProjectChipsnow carries a remove control that callstasksService.unlinkProjectItem. Behind it the two item kinds already diverge correctly and neither needed changing. For a binary fileunlinkProjectItemtakes the table-native branch and deletes theproject_linksrow, which is the whole of that file's membership, andnote-project-links-projectoronly reconcileskind === 'markdown', so no reindex can put the row back. For a markdown note the same call rewrites the name out of frontmatter throughsetEntityProperties, so the file stays the payload and sync carries the removal to a second device without a protocol change. No schema change, no migration, nothing an older client cannot read.One adjacent change rides along.
pages/file.tsxmounts the row withoutonProjectClick, so the chip's "Open project X" button has always done nothing there. Putting a real control next to a dead one would leave the file page with two adjacent tab stops where only one acts, so the project name now renders as plain text wherever no navigation handler is supplied and stays a button where one is.Blast radius is the file page plus any future caller of
ItemProjectChips.pages/file.tsxis its only production caller today,pages/file.test.tsxstubs the component, and no E2E spec asserted on its markup. The calendar's project field is a separate component and is untouched.Two things the reviewer should know, because both contradict the issue's "Where to look". It asked whether a chip in
ProjectEditorhas a remove affordance at all: it does, atProjectEditor.tsx:94, andProjectEditor.test.tsxalready asserted that removing the last chip emits[]. It also suggested copying a clear path fromproject-picker.tsxortask-detail-drawer.tsx; there is none to copy, becausetasks.project_idisNOT NULLandTaskUpdateSchema.projectIdis.optional()rather than.nullish(), so a task can only move between projects, never leave one. The shape copied instead is the calendar's removable chip atevent-project-field.tsx:265.item-project-chips.test.tsxsat in thetsconfig.test.web.jsonexclude backlog, sotypecheck:testcompiled none of it. That line is gone, which surfaced one real error: the hoistedonProjectUpdatedmock declared no parameters, so the pre-existing subscribe-capture case could not implement it. The backlog only ever shrinks.Carries the test-only main fix from the calendar-widget PR until it lands; rebasing after that drops it.
Closes #1941
Release note
Files can now be removed from a project from the chips under the file's title, without deleting the file or the project.
Test plan
vitest --project renderer item-project-chips.test.tsxvitest --project renderer item-project-chips.test.tsxon origin/main sourcevitest --project main project-item-links / project-links-domain / note-project-links-projectorrun-e2e.sh tests/e2e/project-unassign.e2e.tsrun-e2e.sh tests/e2e/project-unassign.e2e.tson origin/main sourcepnpm --filter @memry/desktop typecheck:webpnpm --filter @memry/desktop typecheck:nodepnpm --filter @memry/desktop typecheck:test--listFilesnow showsitem-project-chips.test.tsxin the programpnpm --filter @memry/desktop test:renderercalendar-widget-refresh.test.tsxthat reproduces on origin/main and is fixed by the carried commit; that file alone is 6 passed on this headpnpm --filter @memry/desktop test:mainpnpm lintpnpm --filter @memry/desktop i18n:checkpnpm check:architecture && pnpm check:contractsgit diff --check origin/main..HEADpnpm docs:impact --base origin/main --strictpnpm docs:buildpnpm ipc:checkwas not run: no contract, preload, or main IPC handler changed.No second-device E2E. The diff is a button; it calls
tasks:project-unlink-item, whose sync publication already existed and is already covered.project-links-domain.test.tsasserts that unlink re-enqueuespublisher.projectUpdatedwithchangedFields: ['links'], and a project's payload is what carries its links, so a file's removal reaches a second device the same way its addition always has. For a markdown note the frontmatter write is the sync payload, and the E2E asserts the name is gone from the bytes on disk. A sync spec here would exercise none of the changed lines while holding the shared machine lock.The issue's "a note that was never assigned is unaffected" holds by construction:
ItemProjectChipsrenders nothing when the item has no links (covered by its existingrenders nothing when there are no linked projectscase), so there is no control to press and no write to make.