feat(bin): explicit cascade deletion — user-confirmed cleanup of orphans (JS-9809)#2288
Merged
Conversation
Toast buttons are underline-only, but the dark theme's generic `.button.black:not(.disabled):hover` rule carries an extra element (html) and so outranks toast.scss's `background-color: transparent` at equal class count. The base `.button` radius of 20px, applied to the toast's text-height, zero-padding box, then rendered that background as a circle around the label. Pre-existing: the lone Undo button had it too; it only became obvious once a second button sat beside it. Restore the intended hover (underline only) with a dark-theme scoped override. Claude-Session: https://claude.ai/code/session_01VcHUsR2mAzi8i2Yn3aJbjx
The backend no longer archives, restores or deletes anything implicitly when an object is archived, deleted, or unlinked. Every orphan it would previously have swept up — nested objects at any depth, and files at any depth — is now reported via the new `Object.CleanupSuggestion` event, and the client decides what to do. Protocol - Handle `CleanupSuggestion` (mapper + dispatcher). `ObjectAutoArchive` / `ObjectAutoRestore` are retired: they are still defined in the proto but never emitted, so their handlers, toasts, `I.ToastAction` entries, `I.Toast` fields and strings are removed. - Thread `skipCascade` through `ObjectListSetIsArchived`. The confirmation call passes `true` so archiving the user's picks cannot re-trigger detection and re-open the popup forever. - Wire the on-demand half: `ObjectCleanupSuggestions` (forest of `details`/`isRoot`/`reason`) and `ObjectCleanupSuggestionIgnore`. UX — non-blocking, per the brief's "client decides" - The event extends the existing Archive toast rather than racing it: events are drained before the command callback, so a standalone toast was always clobbered by "N moved to Bin". The ids ride inside the RPC response, so `request()` lifts them onto `message.cleanupIds` and the toast grows a line plus a Review button. A standalone toast still covers delete (broadcast) and link removal, which have no toast of their own. - Review opens a popup: a checkbox tree of candidates nested by `createdInContext`, cascade-selecting subtrees, confirming with skipCascade. - Missed the toast? The Bin grows a "Cleanup suggestions" section, grouped by the source object each orphan was created in, labelled with what happened to it (In Bin / ghost + Deleted object / "Link removed from"). Source rows are not checkable, which is why the Bin's table component could not simply be reused — the section reuses its DOM and classes instead. Offers Delete permanently (mirroring the Bin's shared-space creator rule) and Don't suggest again. - The Bin widget appears when the space has archived objects *or* cleanup suggestions, seeded by one delayed probe on space open and kept in sync by the event and the section's own reloads. Also fixes `Action.delete`, whose callback fired before the RPC landed despite its doc promising "after deletion" — callers refresh their lists from it. Claude-Session: https://claude.ai/code/session_01VcHUsR2mAzi8i2Yn3aJbjx
Replace the section that hung under the Bin list with a second tab beside it: "Bin | Suggestions N". Bin is default; the tab shows only when there are suggestions and carries the count in a badge; it auto-returns to Bin once the last item is cleared. - Count is lifted to the page via onCountChange so the badge/tab visibility work without mounting the tab's body; the component stays mounted on either tab to keep it live, and only renders its body when active. - Actions move to the Bin's header toolbar: reuse the trash icon for Delete permanently (with the same shared-space creator gate) and a new menu/action/minus for Don't suggest again. No search or view toggle here. ArchiveSuggested exposes onDelete/onIgnore via ref and reports selection up. - Explanation shows as a hover hint on the tab rather than a static line. - Keep the Suggestions section immune to the Bin's detailed view mode and match the "Select all" row to the "Name" head row so switching tabs doesn't jump. Claude-Session: https://claude.ai/code/session_01VcHUsR2mAzi8i2Yn3aJbjx
Large spaces produce long suggestion lists, so make each source group foldable and add a Collapse all / Expand all toggle beside Select all. - Group headers toggle collapse (small arrow/button caret); collapsed groups skip their rows. Select all also expands every group so the user can see what got selected rather than ticking rows hidden in collapsed sections. - Remember the active tab on the history entry (location.state), not globally: back/forward restores it (state-only replace, no remount), while opening Bin fresh from the sidebar is a new entry with no tab and shows Bin. Count is null until loaded so a restored Suggestions tab isn't bounced to Bin on mount. - Header polish: match the Select all row to the Bin "Name" head (no jump on tab change) and keep the Suggestions section out of the Bin's detailed view mode; group description moved to a hover hint on the tab; no hover-fill on header rows. Claude-Session: https://claude.ai/code/session_01VcHUsR2mAzi8i2Yn3aJbjx
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Client implementation of GO-7323. Backend lives on
anytype-heart@go-7323-cascade-deletion-orphan-events; brief is atdocs/GO-7323-cascade-deletion-client-impl.mdon that branch.What changed
The backend no longer archives, restores or deletes anything implicitly when you archive, delete, or unlink an object. Every orphan it used to sweep up — nested objects at any depth, and files at any depth, including direct children — is now reported via
Object.CleanupSuggestion, and the client decides what to do with them.Protocol
CleanupSuggestion(mapper + dispatcher).ObjectAutoArchive/ObjectAutoRestore. They remain in the proto but are never emitted, so their handlers, toasts,I.ToastActionentries,I.Toastfields and strings are gone. Files now appear in the candidate list like any other orphan.skipCascadethroughObjectListSetIsArchived. The confirmation call passestrue, so archiving the user's picks can't re-trigger detection and re-open the popup forever.ObjectCleanupSuggestions(forest ofdetails/isRoot/reason) andObjectCleanupSuggestionIgnore.UX — non-blocking, per the brief's "client decides"
The brief explicitly leaves blocking-vs-informative to us. A blocking popup on every archive is intrusive; a passive notice alone lets orphans pile up invisibly. So: both.
1. The Archive toast grows a line and a Review button.
Events are drained before the command callback, so a standalone toast was always clobbered by "N moved to Bin". The ids ride inside the RPC response, so
request()lifts them ontomessage.cleanupIdsand the existing toast extends itself — no race to fight. A standalone toast still covers delete (broadcast) and link removal, which have no toast of their own.2. Review opens a popup — a checkbox tree of candidates nested by
createdInContext, cascade-selecting subtrees, confirming withskipCascade=true.3. Missed the toast? The Bin grows a Cleanup suggestions section, grouped by the source object each orphan was created in and labelled with what happened to it:
Source rows are deliberately not checkable — which is exactly why the Bin's table component couldn't just be reused. The section reuses its DOM and classes (
.listObject > .table > .row > .cell, the −36px checkbox gutter, hover highlight) so it inherits the styling without a fork. Offers Delete permanently — mirroring the Bin's shared-space rule that a non-moderator may only delete objects they created — and Don't suggest again.4. The Bin widget now appears when the space has archived objects or cleanup suggestions, seeded by one delayed probe on space open and kept in sync by the event and the section's own reloads.
Drive-by fixes
Action.deleteinvoked its callback before the RPC landed, despite its doc promising "after deletion" — callers refresh their lists from it. Audited all 7 callers; every one is more correct running after the delete confirms.Design notes
In Binis true of the source; "link removed" isn't (that object is alive and unchanged), so link-removal is carried by the prefix instead.isRootis derived, not read. The forest is built with the sharedU.Data.treeFromRecords, which yields the same roots the backend marks;reasonis read from the server. Avoids a second tree implementation.keys: ['id']— passing any key replaces the RPC's default set, so[]would have pulled name/type/creator/snippet/icons just to read.length.Open questions from the brief
CleanupSuggestionIgnoreis reversible (ignored=false); a settings screen is a sensible follow-up.Verification
bun run typecheck✓ ·eslint✓ ·lint-staged✓ · SCSS compiles ✓ ·text.jsonvalid ✓ · dark-mode audit ✓ (theme vars only, no hardcoded colours)https://claude.ai/code/session_01VcHUsR2mAzi8i2Yn3aJbjx