feat: display and edit sponsorship tiers in forms list view table#827
feat: display and edit sponsorship tiers in forms list view table#827priscila-moneo wants to merge 1 commit intomasterfrom
Conversation
📝 WalkthroughWalkthroughAdds inline sponsorship-tier editing to sponsor forms, includes sponsorship_types and apply_to_all_types in fetches and reducer, improves defensive date/meta_field normalization and propagates promise rejections, exposes dropdown lifecycle callbacks, tightens snackbar HTML checks, and adds two i18n keys. Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as SponsorFormsList
participant Dropdown as DropdownCheckbox
participant Action as updateFormTemplate
participant API as Backend
participant Reducer as SponsorFormsReducer
participant Store as ReduxState
User->>UI: Click tier label (enter edit)
UI->>Dropdown: Open with current sponsorship_types
User->>Dropdown: Select/deselect tiers (handles "all" logic)
Dropdown->>UI: onClose / onCloseMenu callback with new value
UI->>Action: Call updateFormTemplate(formId, {sponsorship_types, apply_to_all_types})
Action->>API: PATCH form
API-->>Action: Return updated form
Action->>Reducer: Dispatch RECEIVE_SPONSOR_FORMS
Reducer->>Store: Compute/attach sponsorship_types and update list
Store-->>UI: State updated -> re-render tiers column
UI-->>User: Show updated tiers
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
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 | 🔴 CriticalDo 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 missingmeta_fieldsto[]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 | 🟠 MajorFetch sponsorship options before using the new tiers column.
This component now relies on
sponsorships.itemsfor both label rendering and inline editing, but it never populates that slice. Sincesponsorships.itemsstarts empty insrc/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
getSponsorshipsis 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 | 🟡 MinorNormalize
event.target.valuebefore using array methods.MUI Select with
multiplecan 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
📒 Files selected for processing (6)
src/actions/sponsor-forms-actions.jssrc/components/mui/SnackbarNotification/index.jssrc/components/mui/dropdown-checkbox.jssrc/i18n/en.jsonsrc/pages/sponsors/sponsor-forms-list-page/index.jssrc/reducers/sponsors/sponsor-forms-list-reducer.js
6484db0 to
f36c334
Compare
f36c334 to
7089c0f
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
src/actions/sponsor-forms-actions.jssrc/components/mui/SnackbarNotification/index.jssrc/components/mui/dropdown-checkbox.jssrc/i18n/en.jsonsrc/pages/sponsors/sponsor-forms-list-page/index.jssrc/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
| if (entity.opens_at !== undefined && entity.opens_at !== null) { | ||
| normalizedEntity.opens_at = | ||
| typeof entity.opens_at === "number" | ||
| ? entity.opens_at | ||
| : moment.tz(entity.opens_at, summitTZ).unix(); | ||
| } else { | ||
| delete normalizedEntity.opens_at; | ||
| } | ||
| if (entity.expires_at !== undefined && entity.expires_at !== null) { | ||
| normalizedEntity.expires_at = | ||
| typeof entity.expires_at === "number" | ||
| ? entity.expires_at | ||
| : moment.tz(entity.expires_at, summitTZ).unix(); | ||
| } else { | ||
| delete normalizedEntity.expires_at; | ||
| } |
There was a problem hiding this comment.
Guard empty date strings before conversion.
opens_at/expires_at currently treat "" as present (Line 455 and Line 463) and still normalize it. This can push invalid date values in update payloads when users clear date fields.
Proposed fix
- if (entity.opens_at !== undefined && entity.opens_at !== null) {
+ if (
+ entity.opens_at !== undefined &&
+ entity.opens_at !== null &&
+ entity.opens_at !== ""
+ ) {
normalizedEntity.opens_at =
typeof entity.opens_at === "number"
? entity.opens_at
: moment.tz(entity.opens_at, summitTZ).unix();
} else {
delete normalizedEntity.opens_at;
}
- if (entity.expires_at !== undefined && entity.expires_at !== null) {
+ if (
+ entity.expires_at !== undefined &&
+ entity.expires_at !== null &&
+ entity.expires_at !== ""
+ ) {
normalizedEntity.expires_at =
typeof entity.expires_at === "number"
? entity.expires_at
: moment.tz(entity.expires_at, summitTZ).unix();
} else {
delete normalizedEntity.expires_at;
}🤖 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 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.
| const normalizeTiers = (arr) => | ||
| Array.isArray(arr) | ||
| ? arr.length === 0 | ||
| ? ["all"] | ||
| : typeof arr[0] === "object" | ||
| ? arr.map((t) => t.id) | ||
| : arr | ||
| : []; |
There was a problem hiding this comment.
Don’t coerce empty tiers to ["all"] in normalization.
Line 47 changes an empty tiers array into "all". In the current save path (Lines 183-193), that can convert untouched legacy/empty rows into apply_to_all_types=true just by entering and closing edit mode.
Proposed fix
const normalizeTiers = (arr) =>
Array.isArray(arr)
- ? arr.length === 0
- ? ["all"]
+ ? arr.length === 0
+ ? []
: typeof arr[0] === "object"
? arr.map((t) => t.id)
: arr
: [];Also applies to: 183-193
🤖 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 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.
ref: https://app.clickup.com/t/86b8t9qw1
Summary by CodeRabbit
New Features
Bug Fixes