Skip to content

feat: display and edit sponsorship tiers in forms list view table#827

Open
priscila-moneo wants to merge 3 commits intomasterfrom
feature/show-level-form-see-tiers
Open

feat: display and edit sponsorship tiers in forms list view table#827
priscila-moneo wants to merge 3 commits intomasterfrom
feature/show-level-form-see-tiers

Conversation

@priscila-moneo
Copy link
Copy Markdown

@priscila-moneo priscila-moneo commented Mar 17, 2026

ref: https://app.clickup.com/t/86b8t9qw1

image image image

Summary by CodeRabbit

  • New Features

    • Inline tier editing for sponsor forms with an editable Tiers column and dropdown; sponsorship types are loaded for selection.
    • Dropdown exposes onBlur/onOpen/onClose/onCloseMenu callbacks.
    • Localized labels added: "Tiers" and "Manage Items".
  • Bug Fixes

    • Snackbar no longer opens for empty/whitespace HTML.
    • Date and meta-field normalization tightened to avoid invalid values.
    • Promise rejections no longer silently swallowed; saves/updates surface errors appropriately.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds inline sponsorship-tier editing to the sponsor forms list, expands sponsorship_types/apply_to_all_types throughout fetch/save/reducer flows, tightens date/meta_fields normalization and snackbar HTML checks, exposes dropdown lifecycle callbacks, removes silent promise swallowing, and adds two i18n keys.

Changes

Cohort / File(s) Summary
Sponsor Forms Actions
src/actions/sponsor-forms-actions.js
Fetch now requests sponsorship_types and apply_to_all_types; normalizeFormTemplate conditionally deletes or converts opens_at/expires_at, defaults/guards sponsorship_types, changes meta_fields handling, and no longer swallows some promise rejections; copyright year updated.
Sponsor Forms List UI
src/pages/sponsors/sponsor-forms-list-page/index.js
Adds editable "Tiers" column using DropdownCheckbox, local edit state and outside-click exit handling, normalization/comparison helpers for tier arrays (including "all" semantics), fetches getSponsorships on mount, and calls updateFormTemplate on save.
Reducer: Sponsor Forms List
src/reducers/sponsors/sponsor-forms-list-reducer.js
RECEIVE_SPONSOR_FORMS mapping now includes sponsorship_types derived from apply_to_all_types or mapped ids from returned objects.
Dropdown Checkbox Component
src/components/mui/dropdown-checkbox.js
Added public props onBlur, onOpen, onClose, onCloseMenu; normalizes incoming selection to arrays; enforces exclusive "all" semantics; forwards/wraps MUI Select lifecycle events.
Snackbar Notification
src/components/mui/SnackbarNotification/index.js
Open/sync logic now requires non-empty trimmed msgData.html to open or update local state.
Internationalization
src/i18n/en.json
Added sponsor_forms.forms.tiers_column_label ("Tiers") and sponsor_forms.forms.manage_items_button ("Manage Items").

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as SponsorFormsList
    participant Dropdown as DropdownCheckbox
    participant Action as SponsorFormsActions
    participant API as Backend
    participant Reducer as SponsorFormsReducer
    participant Store as ReduxState

    User->>UI: Click tier cell (enter edit)
    UI->>Dropdown: open with current sponsorship_types
    User->>Dropdown: select/deselect tiers ("all" logic)
    Dropdown->>UI: onClose / onCloseMenu with new value
    UI->>Action: dispatch updateFormTemplate(id, payload)
    Action->>API: PATCH / sponsor form
    API-->>Action: returns updated form
    Action->>Reducer: dispatch RECEIVE_SPONSOR_FORMS (updated item)
    Reducer->>Store: update state.sponsorForms
    Store-->>UI: re-render list with updated tiers
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Reviewers

  • smarcet
  • tomrndom
  • romanetar

Poem

🐇 I hopped through lists and tiers so bright,

Dropdowns clicked beneath the moonlight,
Dates trimmed neat and meta springs aligned,
Snackbars hush until the HTML's defined,
Now forms and carrots dance in line. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 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 changes: adding the ability to display and edit sponsorship tiers in the forms list view table, which is the primary objective demonstrated in the PR images and implemented across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 feature/show-level-form-see-tiers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Caution

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

⚠️ Outside diff range comments (3)
src/actions/sponsor-forms-actions.js (1)

471-484: ⚠️ Potential issue | 🔴 Critical

Do not synthesize meta_fields: [] for partial form updates.

The new inline tier editor in src/pages/sponsors/sponsor-forms-list-page/index.js (Lines 194-198) updates a form with only tier fields. Defaulting missing meta_fields to [] turns that call into a destructive write and can wipe existing additional fields from the template.

🛡️ Proposed fix
-  const sponsorship_types = entity.sponsorship_types || [];
-  const meta_fields = Array.isArray(entity.meta_fields)
-    ? entity.meta_fields
-    : [];
+  const sponsorship_types = entity.sponsorship_types || [];
+  const hasMetaFields = Array.isArray(entity.meta_fields);

   normalizedEntity.apply_to_all_types = false;
   normalizedEntity.sponsorship_types = sponsorship_types;
@@
-  normalizedEntity.meta_fields = meta_fields.filter((mf) => !!mf.name);
+  if (hasMetaFields) {
+    normalizedEntity.meta_fields = entity.meta_fields.filter((mf) => !!mf.name);
+  } else {
+    delete normalizedEntity.meta_fields;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/sponsor-forms-actions.js` around lines 471 - 484, The code
currently synthesizes meta_fields = [] when entity.meta_fields is missing,
causing partial updates to wipe existing fields; change the logic to only assign
normalizedEntity.meta_fields when entity actually has a meta_fields property.
Specifically, remove the unconditional defaulting "const meta_fields =
Array.isArray(entity.meta_fields) ? entity.meta_fields : []" and instead check
"if (Object.prototype.hasOwnProperty.call(entity, 'meta_fields')) {
normalizedEntity.meta_fields = Array.isArray(entity.meta_fields) ?
entity.meta_fields.filter(mf => !!mf.name) : undefined }" so that
normalizedEntity.meta_fields is not written at all for partial updates that omit
meta_fields (referencing normalizedEntity, entity, and meta_fields/filter in the
diff).
src/pages/sponsors/sponsor-forms-list-page/index.js (1)

53-68: ⚠️ Potential issue | 🟠 Major

Fetch sponsorship options before using the new tiers column.

This component now relies on sponsorships.items for both label rendering and inline editing, but it never populates that slice. Since sponsorships.items starts empty in src/reducers/sponsors/sponsor-forms-list-reducer.js (Lines 48-53), the new tiers UI comes up with raw ids at best and an empty dropdown at worst.

🔧 Proposed fix
 import {
   archiveSponsorForm,
   getSponsorForm,
   getSponsorForms,
+  getSponsorships,
   unarchiveSponsorForm,
   deleteSponsorForm,
   updateFormTemplate
 } from "../../../actions/sponsor-forms-actions";
@@
   totalCount,
   getSponsorForms,
+  getSponsorships,
   getSponsorForm,
   archiveSponsorForm,
   unarchiveSponsorForm,
   deleteSponsorForm,
   updateFormTemplate,
   sponsorships
 }) => {
@@
   useEffect(() => {
     getSponsorForms();
-  }, []);
+    getSponsorships();
+  }, [getSponsorForms, getSponsorships]);
@@
 export default connect(mapStateToProps, {
   getSponsorForms,
+  getSponsorships,
   getSponsorForm,
   archiveSponsorForm,
   unarchiveSponsorForm,
   deleteSponsorForm,
   updateFormTemplate
 })(SponsorFormsListPage);

If getSponsorships is paginated, make sure this load fetches the full tier set; otherwise some ids still won’t resolve.

Also applies to: 216-289

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 53 - 68,
The SponsorFormsListPage now renders and edits tiers using sponsorships.items
but never loads them; call the action that populates that slice (e.g.,
getSponsorships) when the component mounts or before rendering the tiers UI so
sponsorships.items contains the full set, and guard the tiers renderer/editors
to wait for sponsorships.items to be populated (or show a loading state). If
getSponsorships is paginated, ensure you request the full set (e.g., fetch all
pages or use a non-paginated endpoint) so IDs resolve correctly; update the
component’s useEffect (or lifecycle) where getSponsorForms/getSponsorForm is
called to also dispatch getSponsorships and rely on sponsorships.items for label
lookup and dropdown options.
src/components/mui/dropdown-checkbox.js (1)

25-42: ⚠️ Potential issue | 🟡 Minor

Normalize event.target.value before using array methods.

MUI Select with multiple can emit a string value via browser autofill. The current code will fail on line 35 when .filter() is called on a string, and .includes() on line 30 will incorrectly match substrings rather than array elements.

♻️ Proposed fix
   const handleChange = (ev) => {
-    const selected = ev.target.value;
+    const rawValue = ev.target.value;
+    const selected = Array.isArray(rawValue)
+      ? rawValue
+      : typeof rawValue === "string"
+      ? rawValue.split(",")
+      : [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/dropdown-checkbox.js` around lines 25 - 42, In
handleChange normalize ev.target.value (and the existing value) to an array
before calling .includes() or .filter(): inside handleChange (where selected is
defined) coerce selected to an array (e.g. if typeof ev.target.value ===
'string' convert to ev.target.value.split(',') or wrap in [ev.target.value];
likewise normalize the current value used in comparisons) so subsequent checks
like selected.includes("all") and selected.filter(...) operate on arrays, then
call onChange with the normalized arrays as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/sponsor-forms-actions.js`:
- Around line 455-470: The parsing of opens_at and expires_at uses moment(value,
summitTZ) which treats summitTZ as a format string; update both occurrences so
non-number timestamps are parsed with moment.tz(entity.opens_at,
summitTZ).unix() and moment.tz(entity.expires_at, summitTZ).unix() respectively,
keeping the existing typeof number branch and the delete behavior; modify the
lines that assign normalizedEntity.opens_at and normalizedEntity.expires_at to
call moment.tz(...) instead of moment(...), referencing normalizedEntity,
entity.opens_at, entity.expires_at and summitTZ.

In `@src/reducers/sponsors/sponsor-forms-list-reducer.js`:
- Around line 94-110: The reducer currently assigns sponsorship_types as the
expanded objects instead of scalar ids; update the mapping inside the
sponsorForms creation (the payload.response.data.map callback) so that
sponsorship_types is normalized to an array of scalar ids (e.g.,
a.sponsorship_types.map(t => t.id) when a.sponsorship_types is an array) and
keep the "all" behavior for a.apply_to_all_types; ensure the returned object
uses this normalized sponsorship_types so the tiers lookup in the UI (which
expects ids) receives scalar ids.

---

Outside diff comments:
In `@src/actions/sponsor-forms-actions.js`:
- Around line 471-484: The code currently synthesizes meta_fields = [] when
entity.meta_fields is missing, causing partial updates to wipe existing fields;
change the logic to only assign normalizedEntity.meta_fields when entity
actually has a meta_fields property. Specifically, remove the unconditional
defaulting "const meta_fields = Array.isArray(entity.meta_fields) ?
entity.meta_fields : []" and instead check "if
(Object.prototype.hasOwnProperty.call(entity, 'meta_fields')) {
normalizedEntity.meta_fields = Array.isArray(entity.meta_fields) ?
entity.meta_fields.filter(mf => !!mf.name) : undefined }" so that
normalizedEntity.meta_fields is not written at all for partial updates that omit
meta_fields (referencing normalizedEntity, entity, and meta_fields/filter in the
diff).

In `@src/components/mui/dropdown-checkbox.js`:
- Around line 25-42: In handleChange normalize ev.target.value (and the existing
value) to an array before calling .includes() or .filter(): inside handleChange
(where selected is defined) coerce selected to an array (e.g. if typeof
ev.target.value === 'string' convert to ev.target.value.split(',') or wrap in
[ev.target.value]; likewise normalize the current value used in comparisons) so
subsequent checks like selected.includes("all") and selected.filter(...) operate
on arrays, then call onChange with the normalized arrays as before.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 53-68: The SponsorFormsListPage now renders and edits tiers using
sponsorships.items but never loads them; call the action that populates that
slice (e.g., getSponsorships) when the component mounts or before rendering the
tiers UI so sponsorships.items contains the full set, and guard the tiers
renderer/editors to wait for sponsorships.items to be populated (or show a
loading state). If getSponsorships is paginated, ensure you request the full set
(e.g., fetch all pages or use a non-paginated endpoint) so IDs resolve
correctly; update the component’s useEffect (or lifecycle) where
getSponsorForms/getSponsorForm is called to also dispatch getSponsorships and
rely on sponsorships.items for label lookup and dropdown options.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a6a045d-c905-42c1-aece-9d9586ad55cd

📥 Commits

Reviewing files that changed from the base of the PR and between 6152c8e and bb758a2.

📒 Files selected for processing (6)
  • src/actions/sponsor-forms-actions.js
  • src/components/mui/SnackbarNotification/index.js
  • src/components/mui/dropdown-checkbox.js
  • src/i18n/en.json
  • src/pages/sponsors/sponsor-forms-list-page/index.js
  • src/reducers/sponsors/sponsor-forms-list-reducer.js

@priscila-moneo priscila-moneo changed the title feat: show level form see tiers feat: display and edit sponsorship tiers in forms list view table Mar 17, 2026
@priscila-moneo priscila-moneo force-pushed the feature/show-level-form-see-tiers branch 2 times, most recently from f36c334 to 7089c0f Compare March 17, 2026 20:27
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

🧹 Nitpick comments (2)
src/pages/sponsors/sponsor-forms-list-page/index.js (1)

181-183: Use order-insensitive equality for tier sets.

Current comparison treats [1,2] and [2,1] as different and can trigger unnecessary updates.

Proposed adjustment
-  const arraysEqual = (a, b) =>
-    a.length === b.length && a.every((v, i) => v === b[i]);
+  const arraysEqual = (a, b) => {
+    if (a.length !== b.length) return false;
+    const as = [...a].map(String).sort();
+    const bs = [...b].map(String).sort();
+    return as.every((v, i) => v === bs[i]);
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 181 - 183,
The current arraysEqual helper used by handleTiersSave is order-sensitive and
will treat [1,2] and [2,1] as different; change arraysEqual to perform
order-insensitive comparison (e.g., convert both inputs to Sets and compare
sizes and that every value in one Set exists in the other, or sort both arrays
of primitive ids before comparing) and ensure handleTiersSave uses the updated
arraysEqual so tier sets are detected correctly without triggering unnecessary
updates.
src/actions/sponsor-forms-actions.js (1)

447-447: Redundant reject wrapper in catch.

.catch((e) => Promise.reject(e)) is equivalent to rethrowing; it adds noise without behavior gain.

Proposed simplification
-    .catch((e) => Promise.reject(e))
+    .catch((e) => {
+      throw e;
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/sponsor-forms-actions.js` at line 447, The .catch((e) =>
Promise.reject(e)) wrapper is redundant—remove this catch (or replace it with a
simple rethrow if you need to run side-effects) so the original Promise
rejection propagates naturally; locate the .catch((e) => Promise.reject(e))
occurrence in src/actions/sponsor-forms-actions.js and delete that call (or
change to .catch(e => { /* optional logging */ throw e }) if you want to log
before rethrowing).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/sponsor-forms-actions.js`:
- Around line 455-470: The normalize logic for opens_at and expires_at treats
empty strings as present and converts them; update the guards in the block
handling entity.opens_at and entity.expires_at so you only normalize when the
value is not undefined/null/empty string (e.g., check entity.opens_at !==
undefined && entity.opens_at !== null && entity.opens_at !== "" and same for
entity.expires_at), otherwise delete normalizedEntity.opens_at /
normalizedEntity.expires_at; keep using typeof entity.* === "number" ? ... :
moment.tz(entity.*, summitTZ).unix() for conversion.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 44-51: normalizeTiers currently coerces an empty tiers array into
["all"], which causes untouched/legacy empty rows to be treated as
apply_to_all_types=true when saved; change normalizeTiers so that when arr is an
empty array it returns [] (do not return ["all"]), and update any save-path
logic that checks for ["all"] (the code that maps tiers into apply_to_all_types)
to treat an empty array as the absence of selection rather than "all". Ensure
callers of normalizeTiers still handle both arrays of ids and arrays of objects
(preserve the arr.map((t) => t.id) branch) but remove the empty-array -> ["all"]
coercion.

---

Nitpick comments:
In `@src/actions/sponsor-forms-actions.js`:
- Line 447: The .catch((e) => Promise.reject(e)) wrapper is redundant—remove
this catch (or replace it with a simple rethrow if you need to run side-effects)
so the original Promise rejection propagates naturally; locate the .catch((e) =>
Promise.reject(e)) occurrence in src/actions/sponsor-forms-actions.js and delete
that call (or change to .catch(e => { /* optional logging */ throw e }) if you
want to log before rethrowing).

In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 181-183: The current arraysEqual helper used by handleTiersSave is
order-sensitive and will treat [1,2] and [2,1] as different; change arraysEqual
to perform order-insensitive comparison (e.g., convert both inputs to Sets and
compare sizes and that every value in one Set exists in the other, or sort both
arrays of primitive ids before comparing) and ensure handleTiersSave uses the
updated arraysEqual so tier sets are detected correctly without triggering
unnecessary updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 304a11dd-ae82-443a-a090-142f3a068270

📥 Commits

Reviewing files that changed from the base of the PR and between bb758a2 and 7089c0f.

📒 Files selected for processing (6)
  • src/actions/sponsor-forms-actions.js
  • src/components/mui/SnackbarNotification/index.js
  • src/components/mui/dropdown-checkbox.js
  • src/i18n/en.json
  • src/pages/sponsors/sponsor-forms-list-page/index.js
  • src/reducers/sponsors/sponsor-forms-list-reducer.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/mui/SnackbarNotification/index.js
  • src/reducers/sponsors/sponsor-forms-list-reducer.js

Copy link
Copy Markdown

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

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

@priscila-moneo please review comment

);
})
.catch(() => {}) // need to catch promise reject
.catch((e) => Promise.reject(e))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is redundant, since it's an implicit rejection. A catch block returns a new Promise. If the handler function itself throws an error, the returned promise is automatically rejected with that error as the reason.

@priscila-moneo priscila-moneo force-pushed the feature/show-level-form-see-tiers branch from 7089c0f to 0da7610 Compare March 19, 2026 20:43
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

🧹 Nitpick comments (1)
src/pages/sponsors/sponsor-forms-list-page/index.js (1)

145-168: Fragile array comparison may prevent edit cancellation for empty tiers.

The toString() comparison at lines 156-157 doesn't account for the normalizeTiers coercion. When row.sponsorship_types is [], it compares "" === "all" (since tiersValue is ["all"]), which is always false. This means click-outside will never cancel editing for rows that originally had no tiers.

Consider using a consistent normalized comparison:

Proposed fix
           if (
-            tiersValue.length === 0 ||
-            (
-              sponsorForms.find((f) => f.id === editingTiersId)
-                ?.sponsorship_types || []
-            ).toString() === tiersValue.toString()
+            arraysEqual(
+              normalizeTiers(
+                sponsorForms.find((f) => f.id === editingTiersId)
+                  ?.sponsorship_types
+              ),
+              tiersValue
+            )
           ) {

Note: This requires moving arraysEqual and normalizeTiers before this effect or extracting them outside the component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 145 - 168,
The click-outside handler uses toString() to compare sponsorship arrays which is
brittle and fails when normalizeTiers coerces empty arrays (e.g., [] ->
["all"]); update handleClickOutside in the useEffect that watches
editingTiersId/dropdownOpen to compare normalized arrays using the existing
normalizeTiers and arraysEqual helpers (call normalizeTiers on both
sponsorForms.find(...).sponsorship_types and tiersValue, then use arraysEqual)
instead of toString(), and ensure normalizeTiers and arraysEqual are defined
before this effect or moved outside the component so they can be used reliably;
keep the existing logic to call setEditingTiersId(null) when the normalized
arrays match or tiersValue.length === 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/mui/SnackbarNotification/index.js`:
- Around line 40-50: Both useEffect blocks call .trim() on msgData.html and
snackbarMessage.html without ensuring html is a string, which can throw if
non-strings are passed; update the effects that setOpen (watching msgData) and
setMsgData (watching snackbarMessage) to first guard html with a string check
(e.g., typeof html === 'string' && html.trim().length > 0) before calling
.trim(); factor the guard into a small helper like isNonEmptyString used by both
effects and by any local successMessage/errorMessage call sites to defensively
validate inputs.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 194-203: The updateFormTemplate(...) promise chain redundantly
calls getSponsorForms() after updateFormTemplate resolves even though
updateFormTemplate (in updateFormTemplate action) already dispatches
getSponsorForms() in its .then() handler; remove the extra getSponsorForms()
call from the .then() block where updateFormTemplate is invoked (leave the
.catch() to handle errors or surface them) so the list is only refreshed once by
the action itself.

---

Nitpick comments:
In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 145-168: The click-outside handler uses toString() to compare
sponsorship arrays which is brittle and fails when normalizeTiers coerces empty
arrays (e.g., [] -> ["all"]); update handleClickOutside in the useEffect that
watches editingTiersId/dropdownOpen to compare normalized arrays using the
existing normalizeTiers and arraysEqual helpers (call normalizeTiers on both
sponsorForms.find(...).sponsorship_types and tiersValue, then use arraysEqual)
instead of toString(), and ensure normalizeTiers and arraysEqual are defined
before this effect or moved outside the component so they can be used reliably;
keep the existing logic to call setEditingTiersId(null) when the normalized
arrays match or tiersValue.length === 0.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17de2d18-d0d0-4c2b-8e43-78e5ec110966

📥 Commits

Reviewing files that changed from the base of the PR and between 7089c0f and 0da7610.

📒 Files selected for processing (6)
  • src/actions/sponsor-forms-actions.js
  • src/components/mui/SnackbarNotification/index.js
  • src/components/mui/dropdown-checkbox.js
  • src/i18n/en.json
  • src/pages/sponsors/sponsor-forms-list-page/index.js
  • src/reducers/sponsors/sponsor-forms-list-reducer.js
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/en.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/reducers/sponsors/sponsor-forms-list-reducer.js
  • src/components/mui/dropdown-checkbox.js
  • src/actions/sponsor-forms-actions.js

@priscila-moneo priscila-moneo force-pushed the feature/show-level-form-see-tiers branch from 0da7610 to 5c6b0e2 Compare March 27, 2026 21:03
Copy link
Copy Markdown

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

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

  1. normalizeTiers([]) converts empty tiers to "all tiers"
    src/pages/sponsors/sponsor-forms-list-page/index.js:44-51 If a form has no tiers assigned, clicking into the row auto-selects "All tiers" and closing it saves that change. This silently overwrites the form's tier scope without the user intending to.

  2. Tier dropdown has no options until a popup is opened
    src/pages/sponsors/sponsor-forms-list-page/index.js The inline tiers column reads from sponsorships.items, but sponsorships are only fetched when the user opens the GlobalTemplate or FormTemplate popups. On initial page load, the dropdown will be empty and tier names will render as raw IDs. Needs a useEffect to call getSponsorships() on mount.

  3. Editor closes before save completes
    src/pages/sponsors/sponsor-forms-list-page/index.js:192 setEditingTiersId(null) runs synchronously before the PUT request. If the save fails, the user's selection is already gone and they'd have to redo it.

  4. Double list refresh on save
    src/pages/sponsors/sponsor-forms-list-page/index.js:199-201 getSponsorForms() is called here after updateFormTemplate resolves, but updateFormTemplate already dispatches that same refresh internally. This causes two full reloads per save.

  5. Empty string dates pass through to moment
    src/actions/sponsor-forms-actions.js:455-470 The guards check for undefined and null but not empty string. If a date field is cleared, moment.tz("", summitTZ).unix() will produce an invalid value.

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: 1

♻️ Duplicate comments (2)
src/pages/sponsors/sponsor-forms-list-page/index.js (2)

187-191: ⚠️ Potential issue | 🟠 Major

Don’t drop empty/no-op close paths here.

[] is now a meaningful “no tiers selected” state, but Line 191 returns before persisting it. Line 188 also exits without clearing editingTiersId, so closing the dropdown unchanged can leave the row stuck in edit mode until the user clicks again.

Suggested fix
     const changed = prevAll !== nextAll || !arraysEqual(prevIds, nextIds);
-    if (!changed) return;
+    if (!changed) {
+      setEditingTiersId(null);
+      return;
+    }
     const sponsorship_types = nextIds;
     const apply_to_all_types = nextAll;
-    if (!apply_to_all_types && sponsorship_types.length === 0) return;
     updateFormTemplate({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 187 - 191,
The current early-returns drop the meaningful empty selection and leave rows in
edit mode; update the block that computes changed/next values (using prevAll,
nextAll, prevIds, nextIds, arraysEqual) so that an explicit empty
sponsorship_types (sponsorship_types = nextIds) when apply_to_all_types is false
is allowed to persist instead of returning early, and ensure editingTiersId is
cleared/unset whenever the dropdown is closed (i.e., even when there is no
change) so the row doesn’t remain stuck in edit mode.

192-200: ⚠️ Potential issue | 🟠 Major

updateFormTemplate is the wrong action for this inline flow.

That action still refreshes the list with getSponsorForms() defaults and resolves after its internal catch. From this screen, a tier edit can bounce users back to the default table state, and this .then() path will also close the editor after a failed save. Please use an update path that preserves the current query and returns a real rejection on error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 192 - 200,
The code is calling updateFormTemplate(...) which refreshes using
getSponsorForms() defaults and swallows errors; replace this call with the
update action that preserves the current query and propagates real rejections
(e.g., an action named updateSponsorFormPreserveQuery or similar in your store)
so saving a tier doesn't bounce the table to defaults and errors are not
swallowed; ensure the setEditingTiersId(null) remains inside the successful
.then() handler only, remove the empty .catch() and let the action return a
rejected promise on failure so callers can handle/display the error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 272-283: The clickable <span> used to open tier editing should be
keyboard-accessible; change the element rendering that currently uses the span
(the block with title={label} and onClick={() => handleTiersEdit(row)}) to a
semantic <button> or, if retaining a non-button, add role="button", tabIndex={0}
and keyDown handler that calls handleTiersEdit(row) on Enter/Space and ensure
appropriate styling and aria-label/title remain; update the element where
handleTiersEdit(row) is referenced so keyboard and screen-reader users can
activate the editor.

---

Duplicate comments:
In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 187-191: The current early-returns drop the meaningful empty
selection and leave rows in edit mode; update the block that computes
changed/next values (using prevAll, nextAll, prevIds, nextIds, arraysEqual) so
that an explicit empty sponsorship_types (sponsorship_types = nextIds) when
apply_to_all_types is false is allowed to persist instead of returning early,
and ensure editingTiersId is cleared/unset whenever the dropdown is closed
(i.e., even when there is no change) so the row doesn’t remain stuck in edit
mode.
- Around line 192-200: The code is calling updateFormTemplate(...) which
refreshes using getSponsorForms() defaults and swallows errors; replace this
call with the update action that preserves the current query and propagates real
rejections (e.g., an action named updateSponsorFormPreserveQuery or similar in
your store) so saving a tier doesn't bounce the table to defaults and errors are
not swallowed; ensure the setEditingTiersId(null) remains inside the successful
.then() handler only, remove the empty .catch() and let the action return a
rejected promise on failure so callers can handle/display the error.
🪄 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: dae62388-958b-41c8-850d-215f0dc653c6

📥 Commits

Reviewing files that changed from the base of the PR and between 0da7610 and eb437ac.

📒 Files selected for processing (6)
  • src/actions/sponsor-forms-actions.js
  • src/components/mui/SnackbarNotification/index.js
  • src/components/mui/dropdown-checkbox.js
  • src/i18n/en.json
  • src/pages/sponsors/sponsor-forms-list-page/index.js
  • src/reducers/sponsors/sponsor-forms-list-reducer.js
✅ Files skipped from review due to trivial changes (1)
  • src/i18n/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/mui/SnackbarNotification/index.js
  • src/components/mui/dropdown-checkbox.js

Comment on lines +272 to +283
<span
style={{
cursor: "pointer",
textDecoration: "underline dotted",
display: "block",
whiteSpace: "normal",
wordBreak: "break-word",
verticalAlign: "middle"
}}
title={label}
onClick={() => handleTiersEdit(row)}
>
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

Use a keyboard-accessible control for tier editing.

This is a clickable <span>, so keyboard users cannot focus it or open the editor. Please render a button here, or at least add button semantics plus Enter/Space handling.

Suggested fix
-            <span
+            <button
+              type="button"
               style={{
                 cursor: "pointer",
                 textDecoration: "underline dotted",
                 display: "block",
                 whiteSpace: "normal",
                 wordBreak: "break-word",
-                verticalAlign: "middle"
+                verticalAlign: "middle",
+                background: "none",
+                border: 0,
+                padding: 0,
+                textAlign: "left",
+                width: "100%"
               }}
               title={label}
               onClick={() => handleTiersEdit(row)}
             >
               {label}
-            </span>
+            </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
style={{
cursor: "pointer",
textDecoration: "underline dotted",
display: "block",
whiteSpace: "normal",
wordBreak: "break-word",
verticalAlign: "middle"
}}
title={label}
onClick={() => handleTiersEdit(row)}
>
<button
type="button"
style={{
cursor: "pointer",
textDecoration: "underline dotted",
display: "block",
whiteSpace: "normal",
wordBreak: "break-word",
verticalAlign: "middle",
background: "none",
border: 0,
padding: 0,
textAlign: "left",
width: "100%"
}}
title={label}
onClick={() => handleTiersEdit(row)}
>
{label}
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 272 - 283,
The clickable <span> used to open tier editing should be keyboard-accessible;
change the element rendering that currently uses the span (the block with
title={label} and onClick={() => handleTiersEdit(row)}) to a semantic <button>
or, if retaining a non-button, add role="button", tabIndex={0} and keyDown
handler that calls handleTiersEdit(row) on Enter/Space and ensure appropriate
styling and aria-label/title remain; update the element where
handleTiersEdit(row) is referenced so keyboard and screen-reader users can
activate the editor.

getSponsorForms();
}, []);
getSponsorships();
}, [getSponsorForms, getSponsorships]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getSponsorships() is called with no args, defaulting to page=1, perPage=10. Summits with >10 sponsorship types will have missing options in the dropdown and unresolvable tier names in the display. The popup code elsewhere uses MAX_PER_PAGE = 100 — this call should too.

// src/pages/sponsors/sponsor-forms-list-page/index.js
import { MAX_PER_PAGE } from "../../../utils/constants";

// in the useEffect:
getSponsorships(1, MAX_PER_PAGE);

snackbarErrorHandler
)(params)(dispatch)
.then(() => {
dispatch(getSponsorForms());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

updateFormTemplate on success dispatches getSponsorForms() with no arguments. That resets term, page, sort, and archive filter all to defaults. If the user had searched/filtered/paginated, they lose that state after every inline save.

Keep the action's dispatch(getSponsorForms()) but have it read the current query state from the reducer:

// in updateFormTemplate's .then():
const { term, currentPage, perPage, order, orderDir, hideArchived } =
  getState().sponsorFormsListState;
dispatch(getSponsorForms(term, currentPage, perPage, order, orderDir, hideArchived));

Copy link
Copy Markdown

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

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

@priscila-moneo please see comments

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.

🧹 Nitpick comments (2)
src/pages/sponsors/sponsor-forms-list-page/index.js (2)

180-181: Move arraysEqual outside the component.

This utility function is recreated on every render. Moving it outside the component (next to normalizeTiers) improves performance and follows the same pattern used for the other helper.

Suggested refactor
 const normalizeTiers = (arr) => {
   if (!Array.isArray(arr)) return [];
   return typeof arr[0] === "object" ? arr.map((t) => t.id) : arr;
 };

+const arraysEqual = (a, b) =>
+  a.length === b.length && a.every((v, i) => v === b[i]);
+
 const SponsorFormsListPage = ({

Then remove lines 180-181 from inside the component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 180 - 181,
Move the arraysEqual helper out of the component and co-locate it with the
existing normalizeTiers function: extract the const arraysEqual = (a, b) =>
a.length === b.length && a.every((v, i) => v === b[i]); declaration from inside
the component and place it at module scope next to normalizeTiers so it isn't
recreated on every render, then remove the in-component declaration and ensure
any references to arraysEqual remain unchanged.

144-167: Click-outside behavior may be confusing to users.

When the dropdown is closed but tiers have been changed, clicking outside does not exit edit mode. Users must re-open and close the dropdown to trigger the save. Consider either:

  1. Auto-saving on outside click when changes exist, or
  2. Providing a clear visual indicator that unsaved changes exist

This is minor and can be addressed later if user feedback warrants it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sponsors/sponsor-forms-list-page/index.js` around lines 144 - 167,
The current click-outside handler (handleClickOutside) only exits edit mode when
tiers are unchanged, leaving users stuck if they modified tiers while the
dropdown is closed; change it to detect when tiersValue differs from the
original sponsorship_types for the form (compare sponsorForms.find(f => f.id ===
editingTiersId)?.sponsorship_types || [] to tiersValue) and on outside click
either call the existing save/update routine (implement a save function like
saveTiersForForm(editingTiersId, tiersValue) if none exists) to persist changes
and then call setEditingTiersId(null), or if you prefer not to auto-save, set a
flag or UI indicator for unsaved changes and still allow setEditingTiersId(null)
so edit mode exits; update handleClickOutside and its registration/removal
accordingly and keep dropdownRef, dropdownOpen, editingTiersId, tiersValue,
sponsorForms, and setEditingTiersId as the key references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/pages/sponsors/sponsor-forms-list-page/index.js`:
- Around line 180-181: Move the arraysEqual helper out of the component and
co-locate it with the existing normalizeTiers function: extract the const
arraysEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
declaration from inside the component and place it at module scope next to
normalizeTiers so it isn't recreated on every render, then remove the
in-component declaration and ensure any references to arraysEqual remain
unchanged.
- Around line 144-167: The current click-outside handler (handleClickOutside)
only exits edit mode when tiers are unchanged, leaving users stuck if they
modified tiers while the dropdown is closed; change it to detect when tiersValue
differs from the original sponsorship_types for the form (compare
sponsorForms.find(f => f.id === editingTiersId)?.sponsorship_types || [] to
tiersValue) and on outside click either call the existing save/update routine
(implement a save function like saveTiersForForm(editingTiersId, tiersValue) if
none exists) to persist changes and then call setEditingTiersId(null), or if you
prefer not to auto-save, set a flag or UI indicator for unsaved changes and
still allow setEditingTiersId(null) so edit mode exits; update
handleClickOutside and its registration/removal accordingly and keep
dropdownRef, dropdownOpen, editingTiersId, tiersValue, sponsorForms, and
setEditingTiersId as the key references.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af2466b0-fc72-4b29-9424-eb8a38dc87ce

📥 Commits

Reviewing files that changed from the base of the PR and between eb437ac and af5bc78.

📒 Files selected for processing (2)
  • src/actions/sponsor-forms-actions.js
  • src/pages/sponsors/sponsor-forms-list-page/index.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/actions/sponsor-forms-actions.js

Copy link
Copy Markdown

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

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

LGTM

// when snackbarMessage changes in base-reducer, we trigger the snackbar
useEffect(() => {
if (snackbarMessage?.html) {
if (snackbarMessage?.html && snackbarMessage.html.trim()) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

options,
onChange
onChange,
onBlur,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@priscila-moneo
DropdownCheckbox pollutes client contract instead of using ...rest

  • Why it matters: Explicitly adding onBlur, onClose, onOpen, onCloseMenu hardcodes a growing Select prop surface. onCloseMenu is redundant with MUI's own onClose, which is now forcibly wrapped and fires even when callers pass neither handler — a subtle behavior change for the 3 existing consumers (form-template-form, page-template-popup, ItemTableField).
  • Where: src/components/mui/dropdown-checkbox.js (DropdownCheckbox props + onClose wrapper)
  • Suggested fix: destructure only what the component owns (name,label,allLabel,value,options,onChange) and forward ...rest to ; drop onCloseMenu and let callers combine logic inside their own onClose.

Copy link
Copy Markdown

@smarcet smarcet left a comment

Choose a reason for hiding this comment

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

@priscila-moneo please review

)(params)(dispatch)
.then(() => {
dispatch(getSponsorForms());
const { term, currentPage, perPage, order, orderDir, hideArchived } =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@priscila-moneo please break down this on 2 actions. u are modifying the updateFormTEmplate action that is meant to update the whole entity and you are not providing any regresion test

code: a.code,
name: a.name,
items_qty: `${a.items.length} ${
a.items.length === 1 ? "Item" : "Items"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@priscila-moneo this should be resourced at i18n file

const sponsorship_types = nextIds;
const apply_to_all_types = nextAll;
if (!apply_to_all_types && sponsorship_types.length === 0) return;
updateFormTemplate({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@priscila-moneo this should be handled by an specialized action not the same action meant to save the entire entity

setTiersValue(newValue);
};

const arraysEqual = (a, b) =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@priscila-moneo arraysEqual compares positionally (a[i] === b[i]),
but prevIds and nextIds have no shared canonical order:

- `prevIds` comes from the reducer which preserves the backend's order
  (usually sorted by id).
- `nextIds` comes from MUI `Select multiple`, which appends newly-clicked
  items to the END of the value array.

So the two arrays diverge in order as soon as the user unchecks + rechecks
any item, or selects items in a different order than the backend returns
them. Repro:

1. Row has sponsorship_types = [1, 2, 3] (from backend).
2. User opens the inline editor, unchecks 2, rechecks 2.
3. tiersValue is now [1, 3, 2] — same set, different order.
4. arraysEqual([1,2,3], [1,3,2]) → false → `changed = true`.
5. A spurious PUT fires with the exact set the backend already has.

This is especially bad combined with the partial-PUT concern on
updateFormTemplate — a routine check/uncheck cycle (the most common
interaction on a multi-select) will silently re-save the row. Every
spurious call also triggers startLoading + a success snackbar + a full
getSponsorForms refetch, so the UI churns on no-ops.

The same order-sensitive bug exists in the click-outside useEffect above,
which compares via .toString():

  (row.sponsorship_types || []).toString() === tiersValue.toString()

Both should use set-equality. Suggested helper (no new imports, O(n),
intent is explicit):

  const sameTierSet = (a, b) => {
    if (a.length !== b.length) return false;
    const setB = new Set(b);
    return a.every((v) => setB.has(v));
  };

Then:

  const changed = prevAll !== nextAll || !sameTierSet(prevIds, nextIds);

…and replace the .toString() comparison in the click-outside effect
with sameTierSet(...) as well, otherwise only half the bug is fixed.

Please add a unit test covering the "uncheck + recheck" case so this
doesn't regress.

@priscila-moneo priscila-moneo requested a review from smarcet April 7, 2026 21:59
@priscila-moneo priscila-moneo force-pushed the feature/show-level-form-see-tiers branch 3 times, most recently from 8e07bc0 to b3db8ce Compare April 10, 2026 13:28
@priscila-moneo priscila-moneo force-pushed the feature/show-level-form-see-tiers branch from b3db8ce to d7760b0 Compare April 10, 2026 13:46
});
};

export const normalizeFormTemplate = (entity, summitTZ) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@priscila-moneo please rollback this chante to the original normalizeFormTemplate function if u need a new one for updateFormTemplateTiers then define a new pipeline
to avoid any regression

Copy link
Copy Markdown

@smarcet smarcet left a comment

Choose a reason for hiding this comment

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

@priscila-moneo please review

"moment-duration-format": "^2.3.2",
"moment-timezone": "^0.5.33",
"openstack-uicore-foundation": "5.0.6-beta.1",
"openstack-uicore-foundation": "5.0.8-beta.1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@priscila-moneo please update it to 5.0.8

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants