Repository navigation
fix(clusters): a saved edit draft reopens its cluster's edit form, not a new-cluster form - #1870
Draft
dawsontoth wants to merge 1 commit into
Draft
dawsontoth wants to merge 1 commit into
dawsontoth wants to merge 1 commit into
Conversation
…t a new-cluster form A draft saved while editing carries clusterId, but the clusters list sent every draft to new-cluster, where the form took it as a create and the billing step offered Create New Cluster for an edit. The list now sends an edit draft to that cluster's edit form, as the billing return page already did, and leaves an edit draft for a cluster this organization doesn't list alone. All three read the draft's cluster through one helper. The form now reads a draft only when its clusterId matches the route's, and clears only such a draft, so an edit draft never fills a create form and is never discarded by a visit to another form, including the create form an empty organization's list shows. The clear checks the stored draft at the moment it runs, because the billing step saves a draft and clears it again across an await. Closes #1852 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to ensure that saved cluster drafts are correctly associated with and reopened in the specific form they were saved from (either a new cluster form or a specific cluster's edit form). It introduces a helper function draftClusterIdOf to extract the cluster ID from the saved state, which is then used in ClustersList, UpsertCluster, and the billing confirmation flow (ProcessSetupIntent) to handle routing and draft clearance correctly. Extensive unit and integration tests have been added to verify these behaviors. There are no review comments to address, and I have no additional feedback to provide.
dawsontoth
added this pull request to stack #1907
October 11, 2026 21:26
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
⊙ Problem
A cluster draft saved in local storage makes the clusters list redirect to the cluster form, and the list sent every draft to
/<org>/new-cluster. A draft saved while editing a cluster carriesclusterId, so landing on the list with one reopened the edit as a create form, and the billing step offered Create New Cluster for what was an edit of an existing cluster (#1852). The billing return page (ProcessSetupIntent) already sent an edit draft back to../../<clusterId>/edit. Tracing it further, the form itself applied whatever draft was saved, on any cluster form: the new-cluster page, another cluster's edit page, or the create form an empty organization's list renders inline.Stack:
stage← #1842 ← #1849 ← #1851 ← this PR. It is based on #1851's branch (claude/1326-new-cluster-back-button), which made this redirectreplaceits history entry and deliberately left the destination alone. Retarget it once the stack lands.💡 Solution
A draft now belongs to the form it was saved from. An edit draft (one with
clusterId) sends the list to that cluster's edit form; a new-cluster draft still goes to new-cluster. The form reads a draft only when itsclusterIdmatches the route's, and lets its own clears through only for such a draft, so visiting another form neither fills it from the draft nor discards the draft. The list, the billing return page and the form all decide this through one helper.🔧 Changes
src/features/clusters/upsert/draftClusterIdOf.ts(new): returns the cluster a saved draft was editing, orundefined. OnlyClusterForm's billing save writesclusterId; a failed cluster being tried again is saved as a wholeCluster(keyedid), and a?createCluster=link is saved as a string, so both read as new-cluster drafts, as they did before.src/features/clusters/ClustersList.tsx: an edit draft redirects, withreplace, to/<org>/<clusterId>/editwhen this organization lists that cluster; a new-cluster draft still redirects to new-cluster.src/features/clusters/upsert/index.tsx:UpsertClusterreads the stored draft only when itsclusterIdmatches the route's, and wraps the setter it handsClusterFormso a clear goes through only for such a draft.ClusterFormclears the draft when it mounts and when it saves, so without that wrapper, reading a draft as absent would still have deleted it.src/features/organization/billing/confirm/ProcessSetupIntent.tsx: the Stripe return page readsclusterIdthrough the same helper, and its storage type drops theclusterIdshape it no longer reads directly. Its destinations are unchanged.src/features/clusters/DESIGN.mdandDESIGN.md: record which form a draft belongs to, where each of the three readers enforces it, and why the clear reads the stored draft. The root index line names the new topic.✅ Verification
End-to-end route: component tests through the real
ClustersListandUpsertCluster, with routing, queries, storage and the form mocked at their boundaries. Not browser-verified: reproducing it needs a Stripe payment method whose confirmation redirects to an external page, and port 5173 is shared by parallel sessions here.src/features/clusters/ClustersList.test.tsx: an edit draft for a listed cluster rendersReplace /org-a/Staging/edit; an edit draft for an unlisted cluster renders the list and no redirect. The existing new-cluster-draft test is unchanged.src/features/clusters/upsert/index.test.tsx(new): the form mock clears on mount likeClusterFormand replays the billing save-then-clear from its first setter. Matching edit draft: applied, opens on billing, cleared. Edit draft on the new-cluster route or another cluster's edit route: not applied and still stored. New-cluster draft and a failed cluster tried again: applied and cleared. Billing save-then-clear from a stale render: cleared.src/features/clusters/upsert/draftClusterIdOf.test.ts(new): an edit draft names its cluster; no draft, a new-cluster draft, a tried-againCluster, acreateClusterstring, an empty id and a non-string id name none.npx vitest runon those three files plusProcessSetupIntent.test.tsx: 23 passed, exit 0.ClustersListfails both list tests; dropping only the organization check fails the unlisted-cluster test; reading every draft inUpsertClusterfails both not-applied tests; letting every clear through fails both still-stored tests; deciding the clear from the render's copy fails the billing test.npx tsc -b,npx oxlint --format stylish .,npx dprint checkall exit 0.git fetch. The Harper domain adjudicator failed both rounds on an expired Claude OAuth session, so findings were triaged by hand. Round 1 found a real defect, adopted above: a draft the form ignored was still deleted byClusterForm's mount clear, including another organization's draft through an empty organization's inline create form. Round 2 confirmed that fixed. Rejected on evidence: round 2's "the clear can erase a newer draft another tab wrote" is howuseLocalStoragealready works. Each hook instance keeps its own copy and writes it back, by design (src/hooks/useLocalStorage.ts:8, "does NOT pub-sub"). This change only ever skips a write, so it can't widen that race. Kept the two inline comments both legs called narration: one says why an unlisted cluster's draft falls through on purpose, the other why the clear must read the stored draft, which a test pins.🤖 Generated by Anthropic Claude Code (Claude Opus 5.5); posted via @dawsontoth.
🤖 Generated with Claude Code
Related PRs: #1851 overlaps (this PR's base; it made the same redirect replace its history entry)
Complexity: medium
Review-Coverage: authored=claude; ran=gemini,codex; blocked=cursor-composer(no-receipt),domain(auth),cursor-muse(no-receipt); declined=cursor-grok,cursor-kimi; rounds=2; full=1 @ 7f8e4c2
Review-Attention: study ~9m (raised: degraded review, open major) @ 7f8e4c2