Skip to content

fix(frontend/recs): require explicit selection for Purchase/Plan actions (closes #273)#275

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/recs-action-buttons-273
May 5, 2026
Merged

fix(frontend/recs): require explicit selection for Purchase/Plan actions (closes #273)#275
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/recs-action-buttons-273

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 4, 2026

Summary

Closes #273. Drops the unsafe "Purchase / Plan from N visible" fallback
on the Recommendations page bottom action box.

Before: with no checkboxes ticked, both buttons defaulted to "all
visible cells" and remained enabled. A single misclick triggered an
irreversible bulk Purchase / Plan across every visible row, and the
visible set silently changes when Refresh runs or filters mutate.

After: with 0 selected → buttons disabled, label is the static form
("Purchase" / "Create Plan"), tooltip prompts "Select at least one cell
to enable", and resolvePurchaseTarget() returns an empty slice as
defence-in-depth (so a programmatic caller bypassing the disabled UI
also can't fire). With ≥1 selected → buttons enabled, label shows the
selection count.

Changes

  • frontend/src/recommendations.ts:1842::updateBottomActionBox
    drops pickBestVariantPerCell(visible) fallback; gates labels +
    tooltips + disabled state on selectedVisibleCount > 0.
  • frontend/src/recommendations.ts:1826::resolvePurchaseTarget
    returns selected ∩ visible only.
  • Summary line replaces (All N cells visible — no selection) with
    (Select cells to act on) so the prompt is unambiguous.

Test plan

  • New button labels reflect the selection (#273) pins both
    branches (disabled + tooltip when no selection; enabled + count
    when selected).
  • Existing bottom action box shows "All N visible" test renamed
    to prompts for selection when no row is selected (#273) with the
    new copy.
  • 5 fan-out / SP-flow tests that previously relied on the
    no-selection default-target fallback now seed the selection mock
    explicitly so they assert the post-click flow, not the gating UI.
  • npm test -- --testPathPattern=recommendations — 140 / 140
    pass.
  • npm run typecheck — clean.

🤖 Generated with claude-flow

Summary by CodeRabbit

  • New Features

    • Recommendations now require explicit selection before taking actions (purchase, create plans). UI messaging updated to "Select cells to act on" to guide users.
  • Tests

    • Test suite updated to reflect explicit selection requirements and verify button states with appropriate tooltips when no items are selected.

…ons (closes #273)

Before this commit, the bottom action box let the user click "Purchase
N visible" / "Plan from N visible" with no checkbox ticked — and a
single misclick on either button triggered an irreversible action
across every visible cell. The visible set silently changes when
Refresh runs or filters mutate, so the no-selection fallback was
structurally unsafe (a Refresh between intent and click could turn a
"buy these 3 recs" into a "buy these 12 recs").

Changes in updateBottomActionBox (recommendations.ts:1842):

- Drop the no-selection-fallback path entirely. When 0 rows are
  selected, both buttons are `disabled`, the label is the static form
  ("Purchase" / "Create Plan"), and the tooltip prompts the user
  ("Select at least one cell to enable").
- When ≥1 rows are selected, label shows the selection count
  ("Purchase N selected" / "Plan from N selected") — same as before.
- Summary text replaces the chatty "(All N cells visible — no
  selection)" with the prompt "(Select cells to act on)" so the
  intended state is unambiguous.

Defence-in-depth in resolvePurchaseTarget (recommendations.ts:1826):
return only the explicit selection ∩ visible. Removes the prior
fallback to pickBestVariantPerCell(visible) so a programmatic caller
that bypasses the disabled UI cannot trigger the unsafe path either.

Test updates:
- New "button labels reflect the selection (#273)" pins both branches
  (disabled state + tooltip when no selection; enabled with count when
  selected).
- Existing "All N visible" test renamed to "prompts for selection when
  no row is selected" with the new summary copy and tests that the
  new prompt fires.
- 5 fan-out / SP-flow tests that previously relied on the no-selection
  default-target fallback now seed the selection mock explicitly so
  they assert the post-click flow, not the gating UI.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b10aec2b-7d55-44a4-a059-378b9fa7b660

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR removes the auto-selection fallback from Purchase/Plan buttons. When no cells are explicitly selected, the buttons are now disabled with an explanatory tooltip; selection summary text prompts users to "Select cells to act on" instead of advertising a visible-cells fallback.

Changes

Selection-Gated Purchase & Action Box

Layer / File(s) Summary
Core Logic
frontend/src/recommendations.ts (lines 1816–1832, 1849–1895)
resolvePurchaseTarget() now returns only selected ∩ visible (no auto-fallback to best variants per visible cell). updateBottomActionBox() disables Purchase/Create Plan buttons and adds tooltips when selectedVisibleCount === 0, and summary text changes to prompt "Select cells to act on".
Test Coverage
frontend/src/__tests__/recommendations.test.ts (lines 428–440, 533–551, 1479–1504, 1555–1568, 1633–1641, 2011–2104)
Purchase modal test now pre-seeds getSelectedRecommendationIDs before clicking. Selection summary expectations updated to "N selected" / "Select cells to act on". Button state/label tests rewritten to verify disabled + tooltip when no selection exists, and updated labels for single/multi selections. Fan-out and matrix helper tests pre-seed all rec ids to keep Purchase enabled for downstream assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • LeanerCloud/CUDly#231: Introduced pickBestVariantPerCell and auto-selection logic that this PR now removes.
  • LeanerCloud/CUDly#243: Modified config-seeded per-cell auto-selection behavior; this PR removes auto-selection entirely in favor of explicit selection.

Suggested labels

ux, frontend

Poem

🐰 A hop, a click, a choice made clear—
No hidden defaults lurking here!
Selections speak, or buttons sleep,
Commitments safe, no accidental leap.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: requiring explicit selection for Purchase/Plan actions instead of falling back to visible rows.
Linked Issues check ✅ Passed The PR meets all coding requirements from issue #273: buttons are disabled with tooltips when nothing selected, labels show only selected form, summary text updated, and tests verify both disabled and enabled states.
Out of Scope Changes check ✅ Passed All changes directly address the requirements in issue #273; no out-of-scope modifications detected in test assertions or behavior implementations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/recs-action-buttons-273

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/xs Trivial / one-liner type/bug Defect triaged Item has been triaged labels May 4, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 4, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/recommendations.ts`:
- Around line 1816-1831: resolvePurchaseTarget() produces the correct snapshot
but the plan flow ignores it and later re-derives the selection in savePlan(),
allowing mutations between click and save; fix by threading the resolved target
into the plan modal: change openCreatePlanFromBottomBox(...) to accept the
resolved LocalRecommendation[] (or ids) returned by resolvePurchaseTarget(),
update the call site that currently does the emptiness check to pass that
snapshot through, and modify savePlan(...) (and its modal/context plumbing) to
use the passed-in snapshot instead of calling
state.getVisibleRecommendations()/getSelectedRecommendationIDs() so the plan
uses the exact resolved target the user clicked on.
- Around line 1869-1892: The title/tooltip on disabled buttons (purchaseBtn,
planBtn) is unreliable and inaccessible; instead wrap each button in a focusable
container (or add a sibling hint element) and move the explanatory text from
button.title/disabledTooltip to that wrapper/sibling, then link it with
aria-describedby when the button is disabled and ensure the wrapper is
keyboard-focusable (e.g., tabindex="0") so the message is available to mouse and
keyboard users; update the logic around hasSelection to set/remove the
aria-describedby or toggle the hint text visibility rather than setting title on
the disabled control.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f57a5f6-ffa7-4547-856e-f5fbdaff60e2

📥 Commits

Reviewing files that changed from the base of the PR and between e010987 and 8c081d2.

📒 Files selected for processing (2)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts

Comment on lines +1816 to +1831
// Resolve the action target: selected ∩ visible. Returns an empty slice when
// no rows are selected — the action buttons are disabled in that state by
// updateBottomActionBox (closes #273), so callers should never reach this
// helper without a selection. The empty-return is defence-in-depth: if a
// caller bypasses the disabled UI (programmatic invocation, future code path,
// regression on the gating), no purchase happens.
//
// CR #253: when no rows are manually selected, fall back to one variant per
// cell (pickBestVariantPerCell) rather than all visible variants. With the
// grouped table, a collapsed 2-variant cell counts as "1 visible cell" to
// the user but "2 visible recs" to the flat visible list — submitting both
// would create 2 conflicting reservations for the same resource, violating
// the one-variant-per-cell contract from #224.
// Historical context: prior to #273 this fell back to
// pickBestVariantPerCell(visible) when no rows were selected, so misclicking
// a "Purchase visible" button could trigger an irreversible bulk purchase.
// The fallback was removed because Refresh and filter changes silently
// mutate the visible set, making the no-selection path structurally unsafe.
function resolvePurchaseTarget(): LocalRecommendation[] {
const visible = state.getVisibleRecommendations() as unknown as LocalRecommendation[];
const selected = state.getSelectedRecommendationIDs();
const intersection = visible.filter((r) => selected.has(r.id));
// Selection path: use the explicit user selection (radio enforcement from
// #224 already caps at one per cell).
if (intersection.length > 0) return intersection;
// Default (no selection): pick the best variant per cell so the purchase
// target is exactly one rec per physical resource, matching what the user
// sees in the grouped collapsed table.
return pickBestVariantPerCell(visible);
return visible.filter((r) => selected.has(r.id));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i 'plans.ts' frontend
rg -n -C3 'openCreatePlanModal|savePlan|getVisibleRecommendations|getSelectedRecommendationIDs|all visible|selected' \
  frontend/src/recommendations.ts frontend/src/plans.ts

Repository: LeanerCloud/CUDly

Length of output: 33860


Plan creation still does not snapshot the resolved target.

resolvePurchaseTarget() correctly computes the safe selected ∩ visible set, but the Plan path only uses that result as an emptiness check (line 1805) and then calls openCreatePlanFromBottomBox() without passing the snapshot through. The plan flow re-derives the target from global state in savePlan() (plans.ts:559–565), including the fallback to all visible recommendations when no selection exists (line 563–564). This allows state mutations between button-click and save (Refresh, filter changes, deselections) to silently change which recommendations are planned. Thread the resolved target into the plan modal the same way the purchase flow already does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/recommendations.ts` around lines 1816 - 1831,
resolvePurchaseTarget() produces the correct snapshot but the plan flow ignores
it and later re-derives the selection in savePlan(), allowing mutations between
click and save; fix by threading the resolved target into the plan modal: change
openCreatePlanFromBottomBox(...) to accept the resolved LocalRecommendation[]
(or ids) returned by resolvePurchaseTarget(), update the call site that
currently does the emptiness check to pass that snapshot through, and modify
savePlan(...) (and its modal/context plumbing) to use the passed-in snapshot
instead of calling
state.getVisibleRecommendations()/getSelectedRecommendationIDs() so the plan
uses the exact resolved target the user clicked on.

Comment thread frontend/src/recommendations.ts Outdated
Comment on lines +1869 to +1892
const hasSelection = selectedVisibleCount > 0;
const disabledTooltip = loadedCount === 0
? 'No recommendations loaded'
: visibleCount === 0
? 'No rows visible — adjust filters'
: 'Select at least one cell to enable';

if (purchaseBtn) {
purchaseBtn.disabled = empty;
purchaseBtn.textContent = empty
? 'Purchase'
: targetIsSelection
? `Purchase ${targetCount} selected`
: `Purchase ${targetCount} visible`;
purchaseBtn.title = empty
? (loadedCount === 0
? 'No recommendations loaded'
: 'No rows visible — adjust filters')
: 'Buy these reservations now (one-off, processed immediately)';
purchaseBtn.disabled = !hasSelection;
purchaseBtn.textContent = hasSelection
? `Purchase ${selectedVisibleCount} selected`
: 'Purchase';
purchaseBtn.title = hasSelection
? 'Buy these reservations now (one-off, processed immediately)'
: disabledTooltip;
}
if (planBtn) {
planBtn.disabled = empty;
planBtn.textContent = empty
? 'Create Plan'
: targetIsSelection
? `Plan from ${targetCount} selected`
: `Plan from ${targetCount} visible`;
planBtn.title = empty
? (loadedCount === 0
? 'No recommendations loaded'
: 'No rows visible — adjust filters')
: 'Schedule a recurring plan that will purchase these recommendations on a defined cadence';
planBtn.disabled = !hasSelection;
planBtn.textContent = hasSelection
? `Plan from ${selectedVisibleCount} selected`
: 'Create Plan';
planBtn.title = hasSelection
? 'Schedule a recurring plan that will purchase these recommendations on a defined cadence'
: disabledTooltip;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Do disabled HTML <button> elements reliably show native title tooltips or receive keyboard focus across major browsers?

💡 Result:

No. Disabled native elements are not reliable for native title tooltips, and they are also not reliably focusable via keyboard because disabled form controls are specified to be non-focusable/non-interactive. 1) Native title tooltips on disabled - Disabled elements are effectively non-interactive, and browsers have historically differed on whether the element’s title attribute is shown as a native tooltip. Mozilla tracked this directly: “Disabled form controls…don’t show title tooltip attribute,” noting that “disabled HTML items and tooltips” worked in IE/Chrome/Safari/Opera but did not in older Firefox, and that the issue was fixed so that Firefox matched other browsers for mousemove-driven tooltip behavior [1]. - However, because this has been a browser bug/regression class (at least in Firefox), you should not treat native title tooltips as reliably consistent across all versions and edge cases (e.g., differing event suppression and focus/hover triggers). In practice, teams often use wrappers (e.g., focusable /

) or custom tooltip implementations to ensure consistent behavior for disabled controls rather than relying on the disabled element itself [2][3]. 2) Keyboard focus on disabled
- The HTML disabled attribute “makes the element not mutable, focusable” (i.e., a disabled control is not in the focus/interaction model) [4]. - The HTML Standard similarly treats disabled elements as excluded from focusability: the focusing steps require that “the element is not actually disabled” [5]. - Therefore, across major browsers, a disabled should not receive keyboard focus (Tab focus) and should not be treated as a focusable interactive control. Bottom line - Native title tooltips: not something to rely on as “reliable across major browsers and versions,” because disabled controls are non-interactive and browser implementations have differed/bugged in the past, including Firefox [1]. - Native keyboard focus: disabled is specified/implemented to be non-focusable [4][5]. If you need tooltip + keyboard discoverability, common robust patterns are: - Keep the disabled for semantics, but render the tooltip trigger on a separate wrapper that remains focusable (tabindex="0"), and override event/pointer handling so the wrapper—not the disabled button—drives the tooltip [3][6].

Citations:


🏁 Script executed:

# Check if the file exists and read the specific lines
if [ -f "frontend/src/recommendations.ts" ]; then
  echo "File found. Reading lines 1869-1892:"
  sed -n '1869,1892p' "frontend/src/recommendations.ts"
else
  echo "File not found at frontend/src/recommendations.ts"
  # Try to locate it
  find . -name "recommendations.ts" -type f 2>/dev/null
fi

Repository: LeanerCloud/CUDly

Length of output: 980


Disabled buttons don't reliably show title tooltips across browsers.

Setting the title attribute on a disabled <button> is unreliable for users:

  • Mouse users: Disabled buttons don't consistently display native title tooltips across browsers (Firefox historically excluded them; behavior is not guaranteed even now)
  • Keyboard users: Disabled buttons are non-focusable per HTML spec, so they can't be accessed via keyboard at all, making the tooltip completely unavailable

Move the help text to a focusable wrapper element, use aria-describedby, or expose it as adjacent hint text instead of relying on the disabled control itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/recommendations.ts` around lines 1869 - 1892, The title/tooltip
on disabled buttons (purchaseBtn, planBtn) is unreliable and inaccessible;
instead wrap each button in a focusable container (or add a sibling hint
element) and move the explanatory text from button.title/disabledTooltip to that
wrapper/sibling, then link it with aria-describedby when the button is disabled
and ensure the wrapper is keyboard-focusable (e.g., tabindex="0") so the message
is available to mouse and keyboard users; update the logic around hasSelection
to set/remove the aria-describedby or toggle the hint text visibility rather
than setting title on the disabled control.

Two CodeRabbit findings on PR #275 (closes #273):

1. **Plan flow re-derives target from global state** —
   `resolvePurchaseTarget()` correctly returns `selected ∩ visible` after
   #273 dropped the unsafe visible-fallback for the Purchase path, but
   the Plan path threads only the *emptiness* of the snapshot through
   `openCreatePlanFromBottomBox()` and `savePlan()` re-derives the target
   from `state.getVisibleRecommendations()` + `getSelectedRecommendationIDs()`
   at modal-save time. That left the same race window the Purchase path
   had before #273: Refresh / filter changes / deselections between
   button-click and Save can silently change which recs are planned.

   Fix: drop the `else if (visibleRecs.length > 0)` fallback in
   `plans.ts::savePlan` that mirrored the old `pickBestVariantPerCell(visible)`
   path. Plans now require an explicit selection; with no selection the
   `recommendations` field is omitted entirely, so an empty plan fails
   visibly at Save rather than silently planning everything visible.
   The bottom action box already disables the button in that state; this
   is defence-in-depth for any caller that bypasses the disabled UI
   (programmatic invocation, future code paths, regressions on the
   gating).

2. **Disabled `<button>` `title` tooltips are unreliable** — the HTML
   spec excludes disabled form controls from the focus model, and
   browsers historically differ on whether `title` tooltips render on
   them. The previous fix put the disabled-state explanation directly
   on `purchaseBtn.title` / `planBtn.title`, which is invisible to
   keyboard users (can't focus disabled buttons) and inconsistent for
   mouse users.

   Fix: add a sibling `<span#recommendations-action-disabled-hint>` with
   `role="status"` + `aria-live="polite"` and link the buttons via
   `aria-describedby` when they're disabled. Hidden when a selection is
   active. Both keyboard and mouse paths get the message.
   `updateBottomActionBox` toggles the hint visibility / aria-describedby
   linkage on every render.

Tests:
- `plans.test.ts::does NOT include any recs when no selection (#273
  CR follow-up)` replaces the prior `falls back to all visible` test —
  same setup, asserts `recommendations` is empty / undefined.
- `recommendations.test.ts::button labels reflect the selection (#273)`
  expanded to assert the new a11y wiring: hint span visibility +
  textContent, `aria-describedby` set on disabled buttons and cleared
  when enabled, `title` attribute removed when disabled (so the
  unreliable channel isn't competing with the reliable one) and
  restored when enabled.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

Addressed CR feedback. See follow-up commit. @coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim: Thanks for the update! Triggering a review of the follow-up commit now.

[review]

…273 CR pass 2)

CR's first finding on PR #275 surfaced a residual race that the initial
follow-up only partly closed: dropping `savePlan`'s visible-fallback
prevented the "no selection → plan everything visible" misclick path,
but the Plan flow still re-derived its target from
`state.getVisibleRecommendations()` + `getSelectedRecommendationIDs()`
at Save time. State mutations between modal-open and modal-Save (Refresh,
filter changes, deselections) could still silently shrink, widen, or
replace the planned set — the same race the Purchase flow already
avoided by capturing `resolvePurchaseTarget()` at button-click time.

Mirror the Purchase pattern in the Plan flow:

1. `recommendations.ts::openCreatePlanFromBottomBox(snapshot)` accepts
   the resolved target.
2. The Plan-button click handler captures `resolvePurchaseTarget()` and
   passes it through.
3. `plans.ts::openCreatePlanModal(snapshot?)` stamps the snapshot on a
   new module-level cache `pendingPlanRecommendations`. The "Create
   Purchase Plan" vs "New Purchase Plan" title now keys off the
   snapshot, not the live selection state, so it's race-free too.
4. `openNewPlanModal()` clears the cache (the New-Plan-from-scratch
   path explicitly has no pre-resolved target).
5. `closePlanModal()` invalidates the cache on every close path (Save,
   Cancel, Esc, anything else). Once the modal closes, the snapshot's
   tied-to-button-click moment is over.
6. `savePlan` reads from `pendingPlanRecommendations` instead of
   re-deriving from state.

The `state` import in plans.ts becomes unused after this and was
removed.

Tests updated:
- Replaced `includes selected ∩ visible recommendations (Bundle B)`
  with `includes the snapshot stamped by openCreatePlanModal (#273 CR)`
  — same setup intent, but exercises the new snapshot path.
- New `snapshot is immune to state mutations between modal-open and
  Save (#273 CR)` is the load-bearing race-closure assertion: stamps
  the snapshot, then mutates getSelectedRecommendationIDs +
  getVisibleRecommendations, then calls savePlan, asserts the plan
  carries the snapshot's recs (not the post-mutation state).
- `does NOT include any recs when no selection (#273 CR follow-up)`
  now starts with `openNewPlanModal()` to clear any cache stamped by
  the previous test (module-level state persists across jest tests
  in the same file; jest.clearAllMocks doesn't touch our cache).
- New `empty snapshot from openCreatePlanModal also produces no recs`
  pins the defensive empty-array path.
- `sets "Create Purchase Plan" title when a selection exists` →
  `… when a snapshot is passed` — title now keys off the snapshot, not
  state.

CR labeled this finding "🏗️ Heavy lift" — agreed in retrospect: the
right fix needed plumbing through three layers (button click →
openCreatePlanFromBottomBox → openCreatePlanModal → savePlan). The
result is structurally simpler than the prior "re-derive from global
state at Save time" model and matches the Purchase flow.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

Threaded the resolved Plan target through to savePlan via a module-level snapshot, mirroring the Purchase flow. Closes the residual race window between modal-open and modal-Save. New snapshot is immune to state mutations test pins the contract by mutating selection + visible state mid-flow and asserting the saved plan carries the snapshot's recs, not the post-mutation state. @coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim: Thanks for the update! The snapshot-based approach for the Plan target looks like a solid fix for the race window — mirroring the Purchase flow is a clean pattern. Triggering a review of the latest commit now.

[review]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant