Skip to content

feat: move selection plans grid to mui#917

Open
priscila-moneo wants to merge 2 commits into
masterfrom
feature/move-selection-plans-grid-mui
Open

feat: move selection plans grid to mui#917
priscila-moneo wants to merge 2 commits into
masterfrom
feature/move-selection-plans-grid-mui

Conversation

@priscila-moneo
Copy link
Copy Markdown

@priscila-moneo priscila-moneo commented May 5, 2026

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

image

Summary by CodeRabbit

  • New Features

    • Modal-based editor with onSaved callback for post-save handling.
    • Per-page pagination control exposed to list and API requests.
  • Improvements

    • Reworked list and form to modern hook-based UI with updated search and table components.
    • Enhanced marketing/CFP settings editing and import flow.
    • Simplified routing and redirects for selection-plan pages; streamlined delete behavior.
  • Bug Fixes

    • Debounced queries for extra questions to reduce redundant requests.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Walkthrough

Refactors selection-plans to a modal-driven list/editor flow, modernizes components to React hooks, adds per-page pagination support across actions/reducer/components, updates routing to redirect to the list, and adjusts several imports and ordering APIs.

Changes

Selection Plans Modal-Driven UI Refactor

Layer / File(s) Summary
Action signature & params
src/actions/selection-plan-actions.js
getSelectionPlans now accepts perPage and uses DEFAULT_CURRENT_PAGE, DEFAULT_PER_PAGE, DEFAULT_ORDER_DIR; payload includes { order, orderDir, page, perPage, term }; added lodash/debounce import.
Reducer shape & mapping
src/reducers/selection_plans/selection-plan-list-reducer.js
REQUEST_SELECTION_PLANS reads order, orderDir, page, perPage, term; RECEIVE_SELECTION_PLANS reads total, last_page, data and maps items adding is_enabled/is_hidden strings and updates pagination state.
List page (UI + wiring)
src/pages/selection-plans/selection-plan-list-page.js
Converted to functional component with hooks; uses SearchInput/MuiTable, modal Dialog editor (EditSelectionPlanPage), handlers for open/edit/new/delete, sort/page/per-page handlers, and fetch wiring including getMarketingSettingsBySelectionPlan.
Edit page → Form integration
src/pages/selection-plans/edit-selection-plan-page.js, src/components/forms/selection-plan-form.js
SelectionPlanForm converted to functional component with hooks; new public onSaved prop wired through EditSelectionPlanPage; form uses onSubmit(entity) then saveSelectionPlanSettings(...) and calls onSaved(response) if provided; SortableTable reorder API updated to onReorder/updateOrderKey.
Routing/layout changes
src/layouts/selection-plan-layout.js, src/layouts/selection-plan-id-layout.js
Removed EditSelectionPlanPage lazy base route; /new now redirects to list; exact /:selection_plan_id(\d+) mounts SelectionPlanListPage; base redirect now points to selection-plans list.
Import path / small tweaks
src/components/inputs/import-modal/index.jsx, src/components/forms/selection-plan-form.js
UploadInput import changed to default from narrowed path; various explicit per-module imports added; removed stray console.log; replaced older SortableTable API usages.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant List as SelectionPlanListPage
    participant API as API
    participant Dialog as Dialog
    participant EditPage as EditSelectionPlanPage
    participant Form as SelectionPlanForm

    User->>List: View list / Click edit or route contains id
    List->>API: getSelectionPlan / getMarketingSettingsBySelectionPlan
    API-->>List: Return entity & marketing settings
    List->>Dialog: openEditModal(entity)
    Dialog->>EditPage: render with entity & onSaved
    EditPage->>Form: render form with onSaved callback
    User->>Form: Submit changes
    Form->>API: POST/PUT selection plan & save marketing settings
    API-->>Form: Save response
    Form->>EditPage: invoke onSaved(response)
    EditPage->>List: handleSelectionPlanSaved(response)
    List->>API: refresh selection plans (getSelectionPlans with page/perPage/order)
    API-->>List: updated list
    List->>User: render refreshed list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • fntechgit/summit-admin#760: Overlaps with selection-plan component and routing refactors (selection-plan-id-layout.js, selection-plan-layout.js, edit-selection-plan-page.js).
  • fntechgit/summit-admin#749: Changes to src/actions/selection-plan-actions.js related to debounce and pagination constants.

Suggested reviewers

  • smarcet

Poem

🐰
I hopped through lists and modal light,
With hooks I stitched each field just right.
Pages count, orders fall in line,
A nimble form, a save, a sign.
Now off I bound—refactor time!

🚥 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 'feat: move selection plans grid to mui' directly describes the primary change—migrating the selection plans grid component to Material-UI, which is the core focus of this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 feature/move-selection-plans-grid-mui

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

🧹 Nitpick comments (3)
src/pages/selection-plans/selection-plan-list-page.js (2)

88-95: 💤 Low value

Edit flow goes through URL push — works but worth a brief inline comment.

Clicking edit pushes /selection-plans/:id and relies on the routeSelectionPlanId effect to open the dialog. This is a nice property for deep-linking, but it's non-obvious and easy to break in future refactors. A one-line comment explaining the URL ↔ modal relationship would help.

🤖 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 `@src/pages/selection-plans/selection-plan-list-page.js` around lines 88 - 95,
Add a one-line inline comment in the handleEdit function explaining that calling
history.push(`/app/summits/${currentSummit.id}/selection-plans/${selectionPlanId}`)
intentionally updates the URL to trigger the routeSelectionPlanId effect which
opens the edit modal/dialog (enables deep-linking), so future refactors know the
URL change is relied upon to show the modal rather than directly invoking a
modal open method.

60-73: ⚡ Quick win

Add a rejection handler to openEditModal.

If getSelectionPlan or getMarketingSettingsBySelectionPlan rejects (e.g., 404 on a deep-linked stale ID, network failure), the popup never opens and the user is left on the list with no signal. A .catch ensures the route-driven open flow degrades gracefully.

🛡️ Proposed fix
   const openEditModal = (selectionPlanId) => {
     if (!selectionPlanId) return;

     getSelectionPlan(selectionPlanId)
       .then(() =>
         getMarketingSettingsBySelectionPlan(
           selectionPlanId,
           null,
           DEFAULT_CURRENT_PAGE,
           MAX_PER_PAGE
         )
       )
-      .then(() => setOpenSelectionPlanPopup(true));
+      .then(() => setOpenSelectionPlanPopup(true))
+      .catch(() => {
+        if (routeSelectionPlanId) {
+          history.replace(`/app/summits/${currentSummit.id}/selection-plans`);
+        }
+      });
   };
🤖 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 `@src/pages/selection-plans/selection-plan-list-page.js` around lines 60 - 73,
openEditModal's promise chain currently has no rejection handler so failures
from getSelectionPlan or getMarketingSettingsBySelectionPlan leave the UI
unchanged; add a .catch to the chain that handles errors (log/show a
notification and/or update error state) and still calls
setOpenSelectionPlanPopup(true) so the modal opens in a degraded state; keep
references to openEditModal, getSelectionPlan,
getMarketingSettingsBySelectionPlan and setOpenSelectionPlanPopup when
implementing the catch behavior.
src/layouts/selection-plan-layout.js (1)

41-52: 💤 Low value

LGTM, with a small readability note.

Routing is correct: the exact strict route on :selection_plan_id(\\d+) renders the list page (which auto-opens the edit dialog from the URL), while the non-exact one continues to delegate sub-paths (/extra-questions, /rating-types) to SelectionPlanIdLayout. Two <Route> entries with the same path is unusual at first glance — a brief inline comment ("exact → list with auto-open dialog; non-exact → nested sub-pages") would save future readers a double-take.

🤖 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 `@src/layouts/selection-plan-layout.js` around lines 41 - 52, Add a brief
inline comment above the two Route entries explaining their intent: the
exact/strict Route with path `${match.url}/:selection_plan_id(\\d+)` renders
SelectionPlanListPage (which auto-opens the edit dialog from the URL), while the
non-exact Route with the same path delegates nested sub-pages to
SelectionPlanIdLayout (e.g., `/extra-questions`, `/rating-types`); place the
comment near the two Route elements so future readers immediately understand why
both routes share the same path.
🤖 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 `@src/components/forms/selection-plan-form.js`:
- Around line 191-205: handleSubmit currently chains onSubmit →
saveSelectionPlanSettings → onSaved with no error handling; update handleSubmit
to return the promise chain, add a .catch at the end, and guard the intermediate
result before using e.id. Concretely: in handleSubmit (method name) make the
call return this.props.onSubmit(this.state.entity).then((e) => { if (!e ||
!e.id) throw new Error("Missing id from onSubmit result"); return
this.props.saveSelectionPlanSettings(this.state.entity.marketing_settings,
e.id).then(() => { if (onSaved) onSaved(e); }); }).catch(err => { /* handle
error: set local state (e.g. this.setState({saveError: err, saving:false}))
and/or call this.props.onError(err) if provided, and rethrow or swallow as
appropriate */ }); ensure you reference onSubmit, saveSelectionPlanSettings,
onSaved and this.state.entity.marketing_settings in your changes.

In `@src/pages/selection-plans/selection-plan-list-page.js`:
- Around line 174-242: Remove the redundant actions.edit entry from
tableOptions: locate the tableOptions constant and delete the actions: { edit: {
onClick: handleEdit } } block so that edit/delete behavior is provided only via
the top-level onEdit={handleEdit} and onDelete={handleDelete} props passed to
MuiTable; keep tableOptions containing sortCol and sortDir unchanged and ensure
no other references to actions.edit remain.
- Around line 246-254: Add an inline comment above the Dialog usage explaining
that focus management props (disableEnforceFocus, disableAutoFocus,
disableRestoreFocus) are intentionally disabled because EditSelectionPlanPage
triggers nested SweetAlert2 modals via Swal.fire() which conflict with MUI
Dialog's focus trap; mention that this is a deliberate tradeoff to allow those
modals to function and warn future maintainers not to re-enable those props
without addressing the SweetAlert2/MUI focus conflict.
- Around line 97-102: handleDelete currently performs a silent delete via
deleteSelectionPlan(selectionPlanId).then(() => refreshSelectionPlans()) without
user confirmation or error handling; update this flow so the table shows a
confirmation dialog (add the deleteDialogBody prop to the MuiTable instance for
this page, matching the pattern used in sponsor users table and keep
confirmButtonColor="error"), and modify handleDelete to attach a .catch(...) to
deleteSelectionPlan to surface server errors to the user (e.g., via the existing
notification/snackbar mechanism) and only call refreshSelectionPlans() on
success; ensure you reference the handleDelete function and
deleteSelectionPlan/refreshSelectionPlans identifiers when making these changes.
- Around line 75-83: The first useEffect calling getSelectionPlans should
include all referenced variables (term, perPage, order, orderDir, and
getSelectionPlans) in its dependency array (or, if it truly must only run on
mount, replace the array with [] and add a comment explaining why it must not
re-run); the second effect that checks routeSelectionPlanId must include a
stable openEditModal in its deps—wrap openEditModal in useCallback to stabilize
its identity and then add openEditModal (and routeSelectionPlanId) to that
effect's dependency array so react-hooks/exhaustive-deps is satisfied.

---

Nitpick comments:
In `@src/layouts/selection-plan-layout.js`:
- Around line 41-52: Add a brief inline comment above the two Route entries
explaining their intent: the exact/strict Route with path
`${match.url}/:selection_plan_id(\\d+)` renders SelectionPlanListPage (which
auto-opens the edit dialog from the URL), while the non-exact Route with the
same path delegates nested sub-pages to SelectionPlanIdLayout (e.g.,
`/extra-questions`, `/rating-types`); place the comment near the two Route
elements so future readers immediately understand why both routes share the same
path.

In `@src/pages/selection-plans/selection-plan-list-page.js`:
- Around line 88-95: Add a one-line inline comment in the handleEdit function
explaining that calling
history.push(`/app/summits/${currentSummit.id}/selection-plans/${selectionPlanId}`)
intentionally updates the URL to trigger the routeSelectionPlanId effect which
opens the edit modal/dialog (enables deep-linking), so future refactors know the
URL change is relied upon to show the modal rather than directly invoking a
modal open method.
- Around line 60-73: openEditModal's promise chain currently has no rejection
handler so failures from getSelectionPlan or getMarketingSettingsBySelectionPlan
leave the UI unchanged; add a .catch to the chain that handles errors (log/show
a notification and/or update error state) and still calls
setOpenSelectionPlanPopup(true) so the modal opens in a degraded state; keep
references to openEditModal, getSelectionPlan,
getMarketingSettingsBySelectionPlan and setOpenSelectionPlanPopup when
implementing the catch behavior.
🪄 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: e66a2e0b-b7c8-462d-8a25-e51770a30ccf

📥 Commits

Reviewing files that changed from the base of the PR and between 8607ac5 and 5aa12fb.

📒 Files selected for processing (8)
  • src/actions/selection-plan-actions.js
  • src/components/forms/selection-plan-form.js
  • src/components/inputs/import-modal/index.jsx
  • src/layouts/selection-plan-id-layout.js
  • src/layouts/selection-plan-layout.js
  • src/pages/selection-plans/edit-selection-plan-page.js
  • src/pages/selection-plans/selection-plan-list-page.js
  • src/reducers/selection_plans/selection-plan-list-reducer.js

Comment thread src/components/forms/selection-plan-form.js Outdated
Comment on lines +75 to +83
useEffect(() => {
getSelectionPlans();
}, []);
getSelectionPlans(term, DEFAULT_CURRENT_PAGE, perPage, order, orderDir);
}, [getSelectionPlans]);

useEffect(() => {
if (routeSelectionPlanId) {
openEditModal(routeSelectionPlanId);
}
}, [routeSelectionPlanId]);
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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

React useEffect exhaustive-deps mount-only effect best practice eslint-plugin-react-hooks

💡 Result:

The best practice for a mount-only useEffect in React, while respecting the eslint-plugin-react-hooks 'exhaustive-deps' rule, is to include all reactive dependencies (props, state) referenced in the effect rather than using an empty dependency array []. This ensures the effect synchronizes correctly with changing values and avoids stale closures [1][2][3]. Official React documentation explicitly discourages suppressing the linter with // eslint-disable-next-line react-hooks/exhaustive-deps for mount-only effects, calling it "lying" to React and risking bugs [1][2][3]. Instead: 1. Preferred: Add the dependencies - Let the effect re-run when relevant values change (e.g., userId): useEffect(() => { sendAnalytics(userId); }, [userId]); This is correct if re-running on userId change makes sense [2]. 2. True mount-only (ignores deps changes): Use a ref guard - For cases where you must run exactly once despite deps: const sent = useRef(false); useEffect(() => { if (sent.current) return; sent.current = true; sendAnalytics(userId); // Uses latest userId }, [userId]); [2] An empty [] array runs only on mount/unmount but causes eslint warnings if deps are missing, leading to potential bugs like stale values [1][4][5]. Suppression is a last resort and anti-pattern per React team [1][3][6]. To minimize deps: - Move logic outside component or inside effect [3]. - Use useCallback for stable functions. - Extract primitives from objects. Empty [] is common for no-reactive-value effects (e.g., document.title setup with no props/state used) [7][5], but eslint won't complain then. Sources confirm this as current practice (docs unchanged as of 2026) [1][2].

Citations:


🏁 Script executed:

cat -n src/pages/selection-plans/selection-plan-list-page.js | head -100

Repository: fntechgit/summit-admin

Length of output: 4047


🏁 Script executed:

tail -50 src/pages/selection-plans/selection-plan-list-page.js

Repository: fntechgit/summit-admin

Length of output: 1562


🏁 Script executed:

fd -i "eslint" -type f | head -20

Repository: fntechgit/summit-admin

Length of output: 237


🏁 Script executed:

find . -maxdepth 2 -name ".eslintrc*" -o -name "eslint.config.*" -o -name "package.json" | head -5

Repository: fntechgit/summit-admin

Length of output: 98


🏁 Script executed:

cat .eslintrc.json

Repository: fntechgit/summit-admin

Length of output: 2996


🏁 Script executed:

cat package.json | grep -A 20 '"dependencies"' | head -40

Repository: fntechgit/summit-admin

Length of output: 910


🏁 Script executed:

cat package.json | grep -i "eslint-plugin-react-hooks\|react-hooks"

Repository: fntechgit/summit-admin

Length of output: 109


Fix missing dependencies in useEffect hooks to comply with react-hooks/exhaustive-deps.

Line 76 reads term, perPage, order, orderDir but the dependency array is [getSelectionPlans]. Include the values in the dependency array, or explicitly document if this should be a mount-only effect by using an empty array with a comment explaining why it won't re-run when those props change.

Line 79-83 reads openEditModal (a function recreated on every render) but it's not in the dependency array. Wrap openEditModal in useCallback to stabilize its identity across renders.

🤖 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 `@src/pages/selection-plans/selection-plan-list-page.js` around lines 75 - 83,
The first useEffect calling getSelectionPlans should include all referenced
variables (term, perPage, order, orderDir, and getSelectionPlans) in its
dependency array (or, if it truly must only run on mount, replace the array with
[] and add a comment explaining why it must not re-run); the second effect that
checks routeSelectionPlanId must include a stable openEditModal in its deps—wrap
openEditModal in useCallback to stabilize its identity and then add
openEditModal (and routeSelectionPlanId) to that effect's dependency array so
react-hooks/exhaustive-deps is satisfied.

Comment on lines 97 to 102
const handleDelete = (row) => {
const selectionPlanId = row?.id || row;
if (!selectionPlanId) return;

deleteSelectionPlan(selectionPlanId).then(() => refreshSelectionPlans());
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether MuiTable wraps onDelete in a confirmation dialog and
# what `confirmButtonColor` actually controls.
fd -i 'mui' -t d
fd -i 'table' -t f --extension js --extension jsx --extension ts --extension tsx | xargs rg -nP -C3 'onDelete|confirmButtonColor' 2>/dev/null | head -200
rg -nP -C3 '"openstack-uicore-foundation/lib/components/mui/table"' --type=js --type=jsx

Repository: fntechgit/summit-admin

Length of output: 10887


🏁 Script executed:

cat -n src/pages/selection-plans/selection-plan-list-page.js | sed -n '170,250p'

Repository: fntechgit/summit-admin

Length of output: 2799


🏁 Script executed:

# Look for MuiTable component definition locally
find src/components -name "*table*" -o -name "*Table*" | head -20

Repository: fntechgit/summit-admin

Length of output: 1142


🏁 Script executed:

# Search for MuiTable imports and their usage pattern
rg "from.*openstack-uicore-foundation.*table" -A 5 -B 2

Repository: fntechgit/summit-admin

Length of output: 50378


🏁 Script executed:

sed -n '100,200p' src/pages/sponsors/sponsor-users-list-page/components/users-table.js

Repository: fntechgit/summit-admin

Length of output: 1332


🏁 Script executed:

sed -n '97,102p' src/pages/selection-plans/selection-plan-list-page.js

Repository: fntechgit/summit-admin

Length of output: 262


Add delete confirmation dialog and error handling to handleDelete.

handleDelete silently deletes selection plans without user confirmation. Unlike other MuiTable instances in the codebase (e.g., sponsor users table), this one lacks the deleteDialogBody prop required to display a confirmation dialog. confirmButtonColor="error" alone does not provide confirmation UX. Additionally, no .catch() is attached, so server-side failures leave the list unrefreshed and unsignaled to the user.

Suggested fix
  const handleDelete = (row) => {
    const selectionPlanId = row?.id || row;
    if (!selectionPlanId) return;

-    deleteSelectionPlan(selectionPlanId).then(() => refreshSelectionPlans());
+    deleteSelectionPlan(selectionPlanId)
+      .then(() => refreshSelectionPlans())
+      .catch(() => {
+        // surfaced via authErrorHandler; no-op
+      });
  };

Also add deleteDialogBody prop to MuiTable to match the pattern used elsewhere:

  <MuiTable
    // ...
    onDelete={handleDelete}
+   deleteDialogBody={(plan) =>
+     T.translate("selection_plan_list.delete_confirmation_body", { plan })
+   }
    confirmButtonColor="error"
  />
📝 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
const handleDelete = (row) => {
const selectionPlanId = row?.id || row;
if (!selectionPlanId) return;
deleteSelectionPlan(selectionPlanId).then(() => refreshSelectionPlans());
};
const handleDelete = (row) => {
const selectionPlanId = row?.id || row;
if (!selectionPlanId) return;
deleteSelectionPlan(selectionPlanId)
.then(() => refreshSelectionPlans())
.catch(() => {
// surfaced via authErrorHandler; no-op
});
};
🤖 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 `@src/pages/selection-plans/selection-plan-list-page.js` around lines 97 - 102,
handleDelete currently performs a silent delete via
deleteSelectionPlan(selectionPlanId).then(() => refreshSelectionPlans()) without
user confirmation or error handling; update this flow so the table shows a
confirmation dialog (add the deleteDialogBody prop to the MuiTable instance for
this page, matching the pattern used in sponsor users table and keep
confirmButtonColor="error"), and modify handleDelete to attach a .catch(...) to
deleteSelectionPlan to surface server errors to the user (e.g., via the existing
notification/snackbar mechanism) and only call refreshSelectionPlans() on
success; ensure you reference the handleDelete function and
deleteSelectionPlan/refreshSelectionPlans identifiers when making these changes.

Comment thread src/pages/selection-plans/selection-plan-list-page.js
Comment on lines +246 to +254
<Dialog
open={openSelectionPlanPopup}
onClose={handleClosePopup}
maxWidth="xl"
fullWidth
disableEnforceFocus
disableAutoFocus
disableRestoreFocus
>
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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

MUI Dialog disableEnforceFocus disableAutoFocus disableRestoreFocus accessibility implications

💡 Result:

In MUI, Dialog is built on Modal/FocusTrap focus management. The props disableEnforceFocus, disableAutoFocus, and disableRestoreFocus (and disableRestoreFocus) all change focus handling and therefore have accessibility implications. 1) disableEnforceFocus (focus trap disabled) - What it does: “If true, the modal will not prevent focus from leaving the modal while open.” [1] - Accessibility implication: This makes the modal less accessible to assistive technologies because focus may move outside the dialog while it’s open. MUI’s Modal docs explicitly say this should generally never be set to true for accessibility reasons (screen readers). [1] - Practical effect: With enforce-focus off, keyboard users/screen-reader focus can escape the dialog content, which can break the expected modal “user must interact with the dialog” pattern. (MUI documentation emphasizes the focus trap behavior for modals.) [2] 2) disableAutoFocus (initial focus shift disabled) - What it does: “If true, the modal will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes.” [1] - Accessibility implication: MUI says this should generally never be set to true because it makes the modal less accessible to assistive technologies like screen readers. [1] - Practical effect: When the dialog opens, focus may not land on the dialog content (or the intended first control). This can cause screen readers to announce the wrong element/order, or delay users reaching dialog content. - Related community symptoms: For example, autofocus behavior in dialogs can be affected by focus restoration/management; one workaround reported was using disableRestoreFocus. [3] 3) disableRestoreFocus (restoring previous focus disabled) - What it does: “If true, the modal will not restore focus to previously focused element once modal is hidden.” [1] - Accessibility implication: Because focus won’t be returned to the element that opened the dialog, dismissing the dialog can leave keyboard/screen-reader users disoriented about where they are in the app. - Practical effect: In at least some nested-dialog scenarios, disabling focus restoration is used as a workaround to make nested focus behavior work. [4][3] 4) What to watch for when you combine these props - Disabling enforce focus + disabling autofocus + disabling restore focus removes (a) trapping focus inside the dialog, (b) initial focus placement into the dialog, and (c) restoring focus on close. Together, that can significantly degrade keyboard navigation and screen-reader experience, which is why MUI’s docs caution against setting these to true. [1] 5) Recommended approach (accessibility-safe alternatives) - Prefer leaving these defaults (all false) so MUI can manage modal focus appropriately. MUI’s modal docs describe moving focus to the modal content and keeping it there until closed, plus appropriate ARIA roles. [2] - If your goal is to allow interaction in the background (e.g., a chatbot), consider whether you truly need a modal dialog. If you must, disableEnforceFocus is the knob that allows focus to leave, but you should test keyboard and screen-reader behavior carefully because MUI warns it reduces accessibility. [1] Grounding: the accessibility caveats and exact prop behaviors come from MUI’s Modal API docs (the underlying implementation used by Dialog). [1][2]

Citations:


🏁 Script executed:

cat -n src/pages/selection-plans/selection-plan-list-page.js | sed -n '240,260p'

Repository: fntechgit/summit-admin

Length of output: 872


🏁 Script executed:

# Search for child components of this Dialog that might require focus props to be disabled
rg -A 30 'disableEnforceFocus' src/pages/selection-plans/selection-plan-list-page.js

Repository: fntechgit/summit-admin

Length of output: 1007


🏁 Script executed:

# Check for any comments explaining these focus props
rg -B 5 -A 5 'disableRestoreFocus' src/pages/selection-plans/selection-plan-list-page.js

Repository: fntechgit/summit-admin

Length of output: 435


🏁 Script executed:

# Look for TextEditorV3 or SweetAlert mentions in the file
rg -n 'TextEditorV3|SweetAlert' src/pages/selection-plans/selection-plan-list-page.js

Repository: fntechgit/summit-admin

Length of output: 48


🏁 Script executed:

fd -t f 'EditSelectionPlanPage' --or -path '*selection*plan*' -type f | head -20

Repository: fntechgit/summit-admin

Length of output: 311


🏁 Script executed:

# Search for EditSelectionPlanPage import
rg -n 'EditSelectionPlanPage' src/pages/selection-plans/selection-plan-list-page.js

Repository: fntechgit/summit-admin

Length of output: 170


🏁 Script executed:

# Look for TextEditorV3 or SweetAlert in the entire selection-plans directory
rg -r 'TextEditorV3|SweetAlert|swal|sweetalert' src/pages/selection-plans/

Repository: fntechgit/summit-admin

Length of output: 48


🏁 Script executed:

fd 'edit-selection-plan' src/pages/selection-plans/

Repository: fntechgit/summit-admin

Length of output: 189


🏁 Script executed:

# Also search for any comments in the entire list-page file about focus management
rg -B 2 -A 2 'handleClosePopup|openSelectionPlanPopup' src/pages/selection-plans/selection-plan-list-page.js | head -40

Repository: fntechgit/summit-admin

Length of output: 726


🏁 Script executed:

# Read the edit-selection-plan-page.js file to see what components it uses
head -100 src/pages/selection-plans/edit-selection-plan-page.js

Repository: fntechgit/summit-admin

Length of output: 3168


🏁 Script executed:

# Search specifically for TextEditorV3, SweetAlert, or other nested modals in edit-selection-plan-page
rg -n 'TextEditorV3|SweetAlert|swal|Dialog|Modal' src/pages/selection-plans/edit-selection-plan-page.js | head -30

Repository: fntechgit/summit-admin

Length of output: 48


🏁 Script executed:

# Look for any comments about focus management in the entire codebase related to dialogs
rg -n 'disable.*Focus|focus.*trap|accessibility' src/pages/selection-plans/

Repository: fntechgit/summit-admin

Length of output: 321


🏁 Script executed:

rg -n 'Swal\.' src/pages/selection-plans/edit-selection-plan-page.js

Repository: fntechgit/summit-admin

Length of output: 125


🏁 Script executed:

# Also check SelectionPlanForm to see if it uses any modals
rg -n 'Swal\.|Dialog|Modal' src/components/forms/selection-plan-form.js | head -20

Repository: fntechgit/summit-admin

Length of output: 550


Document why focus management is disabled — this Dialog contains nested SweetAlert2 dialogs.

The EditSelectionPlanPage component uses Swal.fire() for three different confirmation modals, which conflict with MUI Dialog's focus trap. Disabling disableEnforceFocus, disableAutoFocus, and disableRestoreFocus is a necessary workaround for this interaction, but add an inline comment explaining this to prevent accidental re-enablement and to clarify the tradeoff for future maintainers.

🤖 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 `@src/pages/selection-plans/selection-plan-list-page.js` around lines 246 -
254, Add an inline comment above the Dialog usage explaining that focus
management props (disableEnforceFocus, disableAutoFocus, disableRestoreFocus)
are intentionally disabled because EditSelectionPlanPage triggers nested
SweetAlert2 modals via Swal.fire() which conflict with MUI Dialog's focus trap;
mention that this is a deliberate tradeoff to allow those modals to function and
warn future maintainers not to re-enable those props without addressing the
SweetAlert2/MUI focus conflict.

} from "../../reducers/selection_plans/selection-plan-reducer";
import history from "../../history";

class SelectionPlanForm extends React.Component {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

please refactor to functional component

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I thought this would be out of scope so I didn't do It initially but I'm adding this now.

entity,
allowedMembers,
errors,
hideHeader = false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

do we really need a prop ? I don't see where this is used WITH the header

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Just did a refactor so this is not needed anymore! Thanks

totalSelectionPlans: total,
currentPage: current_page,
lastPage: last_page
lastPage: last_page,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

no need to do this if you are already doing it in the request action

getSelectionPlans(term, currentPage, perPage, order, orderDir);

const handleEdit = (selectionPlan) => {
const selectionPlanId = selectionPlan?.id || selectionPlan;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

why selectionPlan can be both an object or an id ? we should consolidate this on either one or the other

if (!selectionPlanId) return;

const handleEdit = (selectionPlanId) => {
history.push(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@smarcet linking the user to /id kind of defeats the purpose of having the edit screen in a modal, shouldn't we just open the modal in this case ? or do we want the url to reflect the edit action ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The edit page for a selection plan uses sub-routes (e.g. /extra-questions, /rating-types) that depend on the selection_plan_id URL parameter being present. Removing the ID from the URL on edit would break navigation to those nested routes inside SelectionPlanIdLayout. Keeping the ID ensures the layout can resolve the correct selection plan context for all child routes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not really, you can just open the popup on edit but keep the /id a valid route for the rest of the routes

const handleNew = () => {
history.push(`/app/summits/${currentSummit.id}/selection-plans/new`);
resetSelectionPlanForm();
setOpenSelectionPlanPopup(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

on edit you do change the url, but on new not .. this is not consistent

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The /new path existed in the layout but was never functional — it just redirected back to the list. Creation is handled directly from the list page by opening the form with id=0. Once the plan is saved, the API returns the new ID and the user is redirected to the proper edit URL. There was no reason to keep a dead redirect route.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Also, in task description it was requested to delete this route legacy route https://showadmin.dev.fnopen.com/app/summits/73/selection-plans/new should be removed

Copy link
Copy Markdown

@santipalenque santipalenque left a comment

Choose a reason for hiding this comment

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

Assuming the form is out of scope. Good job @priscila-moneo

};

return (
<form className="selection-plan-form">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

shouldn't we use MUI components ? or this file is out of scope ? cc @smarcet

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.

2 participants