Context
Surfaced during CR pass-1 review of PR LeanerCloud/cloud-commitments-cli#924 (Purchaser group carve-out). CodeRabbit noted that mountBottomActionBox() in frontend/src/recommendations.ts (around line 2861) returns early when the recommendations-action-box DOM element already exists, which means the purchaseBtn.hidden snapshot and the no-Purchaser banner are evaluated once and never refreshed on subsequent renders.
Symptom
If the current user's permission set changes in-session (for example an admin adds themselves to the Purchaser group via Settings then Users without reloading the page) the Purchase CTA visibility and the no-Purchaser banner stay stuck at their initial values until a full page reload. The new copy on the banner explicitly tells the user "add yourself in Settings then Users" so the worst case is a user follows that instruction, comes back, and still sees the read-only banner with no spending button.
The carve-out work in PR LeanerCloud/cloud-commitments-cli#924 only changed the predicates that drive these elements (canAccess('execute', 'purchases') and the matching carved-out gates). The cache-bypass fix touches the entire mountBottomActionBox and updateBottomActionBox lifecycle and is the same shape of issue on the Create Plan button, the disabled-hint, and the Capacity input. Scoping that into PR LeanerCloud/cloud-commitments-cli#924 would have expanded the diff considerably and would have needed its own regression suite for the lifecycle changes.
Proposed fix
Options to consider:
- Remove the early return at the top of
mountBottomActionBox and let updateBottomActionBox be the single source of truth for visibility, or
- Add an explicit
updateBottomActionBox() call on every render so the hidden / banner snapshot is re-evaluated, or
- Push the Purchase CTA and banner state into
updateBottomActionBox so mountBottomActionBox only creates the DOM scaffolding and the per-render state lives in the update function.
The same pattern applies to the #create-plan-btn (create:plans) and the disabled-hint, so the fix should be lifecycle-wide rather than verb-specific.
Repro
- Log in as admin who is NOT in the Purchaser group (no execute:purchases via custom group either).
- Open the Opportunities tab. The Purchase button is hidden, the no-Purchaser banner is visible.
- Open Settings then Users and add yourself to the Purchaser group (no page reload).
- Navigate back to Opportunities. Purchase button is still hidden, banner still visible. A full reload fixes it.
Acceptance
- A test that flips the current user's
effectivePermissions between renders and asserts purchaseBtn.hidden plus banner presence both update without a reload.
- The same fix applies to
#create-plan-btn and the disabled-hint.
References
Context
Surfaced during CR pass-1 review of PR LeanerCloud/cloud-commitments-cli#924 (Purchaser group carve-out). CodeRabbit noted that
mountBottomActionBox()infrontend/src/recommendations.ts(around line 2861) returns early when therecommendations-action-boxDOM element already exists, which means thepurchaseBtn.hiddensnapshot and the no-Purchaser banner are evaluated once and never refreshed on subsequent renders.Symptom
If the current user's permission set changes in-session (for example an admin adds themselves to the Purchaser group via Settings then Users without reloading the page) the Purchase CTA visibility and the no-Purchaser banner stay stuck at their initial values until a full page reload. The new copy on the banner explicitly tells the user "add yourself in Settings then Users" so the worst case is a user follows that instruction, comes back, and still sees the read-only banner with no spending button.
Why this was not fixed in PR LeanerCloud/cloud-commitments-cli#924
The carve-out work in PR LeanerCloud/cloud-commitments-cli#924 only changed the predicates that drive these elements (
canAccess('execute', 'purchases')and the matching carved-out gates). The cache-bypass fix touches the entiremountBottomActionBoxandupdateBottomActionBoxlifecycle and is the same shape of issue on the Create Plan button, the disabled-hint, and the Capacity input. Scoping that into PR LeanerCloud/cloud-commitments-cli#924 would have expanded the diff considerably and would have needed its own regression suite for the lifecycle changes.Proposed fix
Options to consider:
mountBottomActionBoxand letupdateBottomActionBoxbe the single source of truth for visibility, orupdateBottomActionBox()call on every render so the hidden / banner snapshot is re-evaluated, orupdateBottomActionBoxsomountBottomActionBoxonly creates the DOM scaffolding and the per-render state lives in the update function.The same pattern applies to the
#create-plan-btn(create:plans) and the disabled-hint, so the fix should be lifecycle-wide rather than verb-specific.Repro
Acceptance
effectivePermissionsbetween renders and assertspurchaseBtn.hiddenplus banner presence both update without a reload.#create-plan-btnand the disabled-hint.References
frontend/src/recommendations.tslines around 2861-2863, 2904-2919 (post-rebase line numbers), and 3018-3132.