Skip to content

ux(settings): bulk-create service override modal applies to multiple services (closes #119) - #844

Merged
cristim merged 5 commits into
feat/multicloud-web-frontendfrom
fix/119-wave13
Jun 6, 2026
Merged

ux(settings): bulk-create service override modal applies to multiple services (closes #119)#844
cristim merged 5 commits into
feat/multicloud-web-frontendfrom
fix/119-wave13

Conversation

@cristim

@cristim cristim commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Failure handling

Outcome UX
All succeed Success toast "Created N overrides", modal closes, panel reloads
Partial (k of N fail) Warning toast "Created k overrides; failed for: svc1, svc2", modal closes
All fail Inline error in form, submit re-enabled, modal stays open for retry

Batch strategy

Frontend fan-out via Promise.allSettled -- all N PUT /api/accounts/:id/service-overrides/:provider/:service calls fire in parallel. No new batch endpoint needed; the backend already handles idempotent individual PUTs.

Test plan

  • npx tsc --noEmit clean
  • npx jest -- 2150 pass / 0 fail
  • 8 new tests in settings-accounts.test.ts:
    • bulk toggle hides single-select and shows checkbox group
    • submit disabled until at least one box is checked
    • 3/3 success: aggregated success toast, modal closes
    • 2/3 partial failure: warning toast names the failed service
    • 0/3 total failure: inline error, modal stays open
    • submit without any box checked: inline error, API not called
    • already-overridden services excluded from checkbox list
    • closing the modal resets toggle to off

Summary by CodeRabbit

  • New Features

    • Bulk-override mode in the account override modal: toggle between single-service and multi-service selection to apply overrides to multiple services.
  • Bug Fixes / UX

    • Submit disabled until at least one service selected; closing resets to single-service view; aggregated notifications and modal behavior reflect full/partial/failure outcomes; incompatible service/payment/term combos are gated (disabled) to prevent invalid submissions.
  • Accessibility

    • Improved labeling for toggle controls to enhance screen-reader support.
  • Tests

    • Added tests covering bulk flow, validation, notifications, conflict gating, and reset behavior.

@cristim cristim added triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline impact/few Limited audience effort/m Days type/feat New capability labels May 28, 2026
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Too many files changed? Review this PR in Change Stack to see how the pieces fit before you dive in.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 84f88eec-c39b-4a8c-b8c6-7c8ce6eb5005

📥 Commits

Reviewing files that changed from the base of the PR and between c5cc243 and 338a83e.

📒 Files selected for processing (3)
  • frontend/src/__tests__/settings-accounts.test.ts
  • frontend/src/index.html
  • frontend/src/settings.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/tests/settings-accounts.test.ts
  • frontend/src/index.html
  • frontend/src/settings.ts

📝 Walkthrough

Walkthrough

Adds bulk-mode to the "Add service override" modal: compute availableServices excluding already-overridden services, toggle between single-service select and bulk checkbox list, re-evaluate checkbox compatibility on term/payment changes, route submission to a parallel fan-out with aggregated toasts, and add tests for UI/state/outcome permutations.

Changes

Bulk Override Modal

Layer / File(s) Summary
Modal context and available services computation
frontend/src/settings.ts
Modal context enriched with availableServices; computed by filtering existingOverrides to exclude already-overridden services for the provider.
Bulk override modal markup
frontend/src/index.html
Adds single-service row container, bulk-apply toggle, hidden bulk services checkbox list, explicit override-submit-btn id on the modal submit button, and accessibility label spans for toggles.
Modal lifecycle and bulk toggle behavior
frontend/src/settings.ts
On open: reset bulk toggle, force single-service mode, initialize single-service options from availableServices. applyOverrideBulkMode toggles single-select vs checkbox list, rebuilds checkbox options, and syncBulkServiceConflicts disables/unchecks incompatible services and updates submit enabled state; close resets bulk toggle and forces single mode.
Submission routing and bulk save handler
frontend/src/settings.ts
submitOverrideForm detects bulk toggle and delegates to submitBulkOverrideForm for bulk saves. submitBulkOverrideForm validates at least one checked service and term/payment compatibility, disables submit, saves all checked services with Promise.allSettled, shows aggregated toasts (all-success/partial/all-failure), reloads overrides/recommendations on success/partial, and re-enables/retains modal on full failure.
Test DOM builder and bulk override test suite
frontend/src/__tests__/settings-accounts.test.ts
Test DOM updated to include bulk UI elements. New tests validate UI swap, submit enable/disable rules, empty-selection guard, full/partial/failure save outcomes and messaging, exclusion of already-overridden services, bulk toggle reset on close, and term/payment-based conflict gating.

Sequence Diagram

sequenceDiagram
  participant User
  participant OverrideModal
  participant submitOverrideForm
  participant submitBulkOverrideForm
  participant OverrideAPI
  participant UI
  User->>OverrideModal: open modal (bulk toggle on)
  User->>OverrideModal: click submit
  OverrideModal->>submitOverrideForm: submit event
  submitOverrideForm->>submitBulkOverrideForm: delegate (bulk on)
  submitBulkOverrideForm->>OverrideAPI: parallel save for each checked service
  OverrideAPI-->>submitBulkOverrideForm: per-service results
  submitBulkOverrideForm->>UI: show aggregated toast or inline error
  submitBulkOverrideForm->>OverrideModal: close modal on success or partial success
  submitBulkOverrideForm->>OverrideModal: keep modal open on full failure
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • LeanerCloud/CUDly#176: Modifies override modal/validation logic; touches the same term/payment compatibility checks extended into the bulk flow.
  • LeanerCloud/CUDly#588: Updates override modal and tests; related at the modal repopulation and exclusion of already-overridden services.
  • LeanerCloud/CUDly#30: Introduced payment/term clamping and per-service validity rules that this PR reuses to gate bulk checkbox selections.

Poem

🐰 I hop through checkboxes in a row,
Toggling bulk so many saves can go,
Promises run in parallel light,
Toasts report the wins and plight,
The modal resets — now that's delight!

🚥 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 clearly and specifically describes the main change: adding a bulk-create capability to the service override modal that allows applying overrides to multiple services, and references the closed issue #119.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/119-wave13

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

@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/settings.ts (1)

1675-1778: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the bulk selection before fan-out.

Lines 1679-1681 jump into bulk mode before the isValidCombination(...) guard on Lines 1693-1701 runs. AWS validity is service-specific, so a term/payment pair that is valid for the hidden #override-service select can still be invalid for some checked services, producing deterministic partial failures that the frontend already knows how to prevent.

Suggested fix
   const bulkToggle = document.getElementById('override-bulk-toggle') as HTMLInputElement | null;
   const isBulk = bulkToggle?.checked ?? false;
 
   if (isBulk) {
+    const checkedServices = Array.from(
+      document.querySelectorAll<HTMLInputElement>('`#override-bulk-services-list` input[type="checkbox"]:checked'),
+    ).map(cb => cb.value);
+
+    if (req.term != null && req.payment) {
+      const invalid = checkedServices.filter(service =>
+        !isValidCombination(ctx.provider, service, req.term!, req.payment!),
+      );
+      if (invalid.length > 0) {
+        const errEl = document.getElementById('override-form-error');
+        if (errEl) {
+          errEl.textContent = `Unsupported term/payment selection for: ${invalid.join(', ')}.`;
+        }
+        return;
+      }
+    }
+
     await submitBulkOverrideForm(ctx, req);
     return;
   }
🤖 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/settings.ts` around lines 1675 - 1778, submitBulkOverrideForm
currently skips the per-service validity check and can attempt saves for
services that don't support the requested term/payment; before creating the
Promise.allSettled fan-out, iterate the checked services (variable checked) and
call isValidCombination(ctx.provider, service, req.term, req.payment) for each,
collect any invalid services, and if any exist set override-form-error to a
clear message listing the offending services and return early; otherwise proceed
with the existing save logic. Ensure you reference submitBulkOverrideForm,
checked, ctx.provider, req.term, req.payment, isValidCombination and errEl when
implementing the validation.
🤖 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/__tests__/settings-accounts.test.ts`:
- Around line 1921-1935: The test "already-overridden services are excluded from
the checkbox list" calls loadAccountsForProvider('aws') without explicitly
mocking api.listAccounts, making it flaky; add an explicit mockResolvedValue for
api.listAccounts at the start of this test (similar to other tests) so
loadAccountsForProvider receives a deterministic accounts list, while keeping
the existing api.listAccountServiceOverrides mock; update the test to mock
api.listAccounts (and api.listAccounts should return the account(s) expected by
loadAccountsForProvider) before calling loadAccountsForProvider('aws').

In `@frontend/src/index.html`:
- Around line 1102-1108: The markup nests a <label> inside another for the bulk
override toggle which is invalid and causes odd click/focus behavior; update the
template so each toggle (for the element using id="override-bulk-toggle" and
likewise for the other toggles account-enabled and account-aws-is-org-root) uses
a single wrapping label (or a single input outside with a separate text element)
and remove the inner <label>; follow the existing pattern used elsewhere by
giving the descriptive text an id and adding aria-labelledby="<that-id>" on the
<input> (keep the <span class="slider"> for styling) so the input remains
accessible and the nested label is eliminated.

In `@frontend/src/settings.ts`:
- Around line 1534-1580: In applyOverrideBulkMode: when bulk is false the
function currently returns before restoring the single-service submit button
state, leaving `#override-submit-btn` disabled after toggling back from bulk; fix
by, before returning in the (!bulk) branch, recomputing submitBtn.disabled using
the single-service control inside singleRow (e.g., find the single select or
input via singleRow.querySelector('select') or appropriate input) and set
submitBtn.disabled = <true if nothing selected/empty, false otherwise>, handling
nulls for singleRow and submitBtn.

---

Outside diff comments:
In `@frontend/src/settings.ts`:
- Around line 1675-1778: submitBulkOverrideForm currently skips the per-service
validity check and can attempt saves for services that don't support the
requested term/payment; before creating the Promise.allSettled fan-out, iterate
the checked services (variable checked) and call
isValidCombination(ctx.provider, service, req.term, req.payment) for each,
collect any invalid services, and if any exist set override-form-error to a
clear message listing the offending services and return early; otherwise proceed
with the existing save logic. Ensure you reference submitBulkOverrideForm,
checked, ctx.provider, req.term, req.payment, isValidCombination and errEl when
implementing the validation.
🪄 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: c313cd42-78a0-4b8e-89ce-4beb34feb41b

📥 Commits

Reviewing files that changed from the base of the PR and between 4956d66 and 47b09fe.

📒 Files selected for processing (3)
  • frontend/src/__tests__/settings-accounts.test.ts
  • frontend/src/index.html
  • frontend/src/settings.ts

Comment thread frontend/src/__tests__/settings-accounts.test.ts
Comment thread frontend/src/index.html Outdated
Comment thread frontend/src/settings.ts
@cristim

cristim commented May 30, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
✅ 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.

cristim added a commit that referenced this pull request May 30, 2026
… rows

Eliminate nested <label> inside <label> (invalid HTML) for the
bulk-override toggle and the two account-form toggles
(account-enabled, account-aws-is-org-root). Replace the outer
<label class="toggle-row"> with <div class="toggle-row">, add a
<span id="..."> for the descriptive text, and wire aria-labelledby
on each <input> -- the same pattern already used for the global
settings toggles (issue #464). Addresses CR finding on PR #844.
@cristim

cristim commented May 30, 2026

Copy link
Copy Markdown
Member Author

Addressed remaining CR finding (nested label elements) in commit c5cc243. @coderabbitai review

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for addressing the nested label elements finding! Triggering a review of the latest commit.

✅ 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.

@cristim

cristim commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

All CR findings addressed: listAccounts mock in 7cda593, nested labels in c5cc243, submit state restore in a2259f6. @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for addressing all the findings — the listAccounts mock, nested label elements, and submit state restore all sound solid. Triggering a review of the latest commits now.

✅ 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.

@cristim

cristim commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

All CR findings from the initial review (2026-05-28) are addressed in HEAD (c5cc243):

  • Test flakiness (settings-accounts.test.ts): explicit api.listAccounts mock added in 7cda593. CR confirmed.
  • Nested label elements (index.html): outer <label class="toggle-row"> replaced with <div class="toggle-row"> + aria-labelledby for all three toggles in c5cc243. CR confirmed.
  • Submit button state on bulk toggle-off (settings.ts applyOverrideBulkMode): recomputes submitBtn.disabled from the single-service <select> value before returning in the !bulk branch, in a2259f6. CR confirmed.
  • Outside-diff: bulk validation before fan-out (settings.ts submitBulkOverrideForm): iterates checked services and calls isValidCombination before Promise.allSettled, shows per-service error and returns early if any are invalid, also in a2259f6. Verified in current HEAD at lines 1750-1762.

No new commits needed; all four findings are satisfied by the three commits already on the branch.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough summary — all four findings are clearly accounted for:

  1. Test flakiness — explicit api.listAccounts mock (7cda593)
  2. Nested label elements<div class="toggle-row"> + aria-labelledby for all three toggles (c5cc243)
  3. Submit button state on bulk toggle-offsubmitBtn.disabled recomputed from the single-service <select> value (a2259f6)
  4. Bulk validation before fan-outisValidCombination check per service before Promise.allSettled (a2259f6, lines 1750–1762)

Triggering a review of the latest commits now.

✅ 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.

cristim added 4 commits June 3, 2026 23:19
Add "Apply to multiple services" toggle to the existing per-account
override modal. When enabled, the single-service <select> is replaced
by a checkbox group listing unconfigured services. On save, all checked
services receive the same term/payment/coverage via Promise.allSettled
(parallel PUTs, no new backend endpoint). A single aggregated toast
reports success count on full success, names the failed service(s) on
partial failure, and surfaces an inline error on total failure while
keeping the modal open for retry. Existing overrides are excluded from
the checkbox list. The toggle resets to off on every close.

Tests: 8 new cases in settings-accounts.test.ts covering toggle UI,
submit-disabled-until-checked, 3/3 success, 2/3 partial failure,
0/3 total failure, no-selection guard, exclusion of existing overrides,
and modal-reset-on-close.
… state on bulk toggle-off

Per-service isValidCombination check in submitBulkOverrideForm blocks invalid
(provider, service, term, payment) combos before Promise.allSettled fan-out,
mirroring the existing single-service path guard.

applyOverrideBulkMode now recomputes submitBtn.disabled from the single-service
<select> value when toggling back to single mode, so the button is not left
stuck disabled.
…n test

Removes order-dependent flakiness by providing an explicit deterministic
accounts list, so the test no longer relies on state from a previous test
that happened to call loadAccountsForProvider.
… rows

Eliminate nested <label> inside <label> (invalid HTML) for the
bulk-override toggle and the two account-form toggles
(account-enabled, account-aws-is-org-root). Replace the outer
<label class="toggle-row"> with <div class="toggle-row">, add a
<span id="..."> for the descriptive text, and wire aria-labelledby
on each <input> -- the same pattern already used for the global
settings toggles (issue #464). Addresses CR finding on PR #844.
@cristim

cristim commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

Rebased on feat/multicloud-web-frontend to clear DIRTY state.

Conflicts resolved (additive merges on both sides):

  • frontend/src/settings.ts: kept HEAD's issue ux(settings): show current global default in override modal's 'Inherit' labels #112 Inherit-label updates (term placeholder + coverage placeholder) and added this branch's bulk-toggle reset; dropped HEAD's duplicate used/available recomputation since this branch already moved that block earlier in openOverrideModal.
  • frontend/src/__tests__/settings-accounts.test.ts: kept HEAD's full describe('Override modal Inherit labels (issue #112)') block intact and appended this branch's describe('Bulk override modal (issue #119)') block as a sibling top-level describe.
  • frontend/src/index.html: clean auto-merge.

Post-rebase verification: npx tsc --noEmit clean, npx jest settings-accounts.test.ts green (76 tests pass).

All prior CR pass-1 findings remain addressed (confirmed by CR in commits a2259f6 and 7cda593). No new code changes in this rebase beyond conflict resolution.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the detailed rebase summary! Clean additive merges on both sides, TypeScript clean, and 76 Jest tests passing — that all looks solid. Triggering a review of the rebased commits now.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.
✅ Action performed

Review finished.

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.

@cristim

cristim commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Rate Limit Exceeded

@cristim have exceeded the limit for the number of chat messages per hour. Please wait 39 minutes and 42 seconds before sending another message.

…fore save (refs #119)

In bulk mode the override modal now proactively disables (and greys out via
title annotation) service checkboxes whose service does not support the
currently chosen term/payment combination, mirroring the existing single-select
logic (isValidCombination from commitmentOptions.ts).

Previously the UI blocked the entire bulk save at submit time with an inline
error if any checked service conflicted, forcing the user to manually find and
deselect it. The submit-time guard is kept as a belt-and-suspenders defence, but
is now unreachable in normal use.

Changes:
- Add syncBulkServiceConflicts(provider) that iterates the bulk checkbox list,
  calls isValidCombination per service, and disables/re-enables each
- Call it immediately after the checkbox list is built in applyOverrideBulkMode
- Extend the onChange listener in openOverrideModal to call it whenever term
  or payment changes (also wires the payment select to onChange, previously
  untouched by that handler)
- Add two tests in settings-accounts.test.ts: one asserts rds is disabled when
  3yr/no-upfront is chosen and the API is never called; the other asserts
  rds is re-enabled when the combo changes back to a valid one
@cristim

cristim commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@cristim
cristim merged commit 5785a72 into feat/multicloud-web-frontend Jun 6, 2026
5 checks passed
@cristim
cristim deleted the fix/119-wave13 branch July 27, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/feat New capability urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant