You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Consolidate DecisionModals into a global component #80285
Following up on #68799 with another common modal type.
Problem
When every component mounts its own DecisionModal in the component tree, it creates component bloat, memory overhead from multiple modal instances, and repetitive boilerplate code that makes the application harder to maintain.
Solution
Lift a single DecisionModal instance up in the component tree so that it can be shared across the app. Child components can use context to show and hide the shared DecisionModal with whatever props they want to pass to it. Internally, the state of the DecisionModal is a stack (allowing for multi-step confirmations, one of our UI requirements), and that state is managed through React Context.
Going to try taking the quicker/cheaper path here and do this refactoring in one go, rather than in many incremental PRs as we have done for ConfirmModal, relying on Applause for pre-merge regression testing. I understand it makes the single PR riskier, but:
we haven't really seen issues rolling this change out
The global infrastructure already exists (useDecisionModal, DecisionModalWrapper, useHoldMenuModal). There are 18 inline DecisionModals left: 16 <DecisionModal> elements plus 2 <ProcessMoneyReportHoldMenu> call sites. You can migrate 16 of them now. The 2 domain "Close account" modals need a wrapper change first.
Easy: migrate now (10)
None of these has a first option or an onModalHide, and all have static text. Each one becomes showDecisionModal({title, prompt, secondOptionText}), and you can delete the isVisible state.
BaseImportOnyxState.tsx:56. Visibility comes from props, so call showDecisionModal in the .catch of both index.tsx and index.native.tsx
Download failed / Offline
SelectionToolbar/index.tsx:309-326 (2 modals). This also fixes a bug: on wide layouts the toolbar unmounts after a CSV export starts, so a later failure modal never appears
It opens from inside a global ConfirmModal. Keep the close(() => showDecisionModal(...)) wrapper so the ConfirmModal closes first, instead of reappearing after "OK"
State lives in useSearchBulkActions (about 20 setter sites) and is asserted in 4 test files. Pass an explicit prompt at each download-failed site. Today emptyReportsCount can be stale and show the wrong message
This is an older, pay-only copy of HoldMenuModalWrapper. Replace both callers with useHoldMenuModal().showHoldMenu, as MoneyReportHeaderSelectionDropdown already does. Then delete ProcessMoneyReportHoldMenu, ReportPreviewHoldMenu, and the preview hold-menu context
Today, dismissing these modals with the backdrop, Escape, or Back does nothing. The wrapper maps dismiss to the second option (DecisionModalWrapper.tsx:47). That makes dismiss look the same as "Safe close". After a naive migration, dismissing would open the close-account confirmation. Cancelling it reopens the decision modal (DomainMemberDetailsPage.tsx:107-110), so the user would be stuck in a loop.
Fix: have the wrapper return a distinct action on dismiss, for example a new ModalActions.DISMISS. Then migrate these pages as an async flow that awaits the decision and then the confirmation.
Cross-cutting notes
Shared helper: 9 of these modals are the same "Offline" or "Download failed" alert. The same bodies are already duplicated in useExportActions.ts:114-128 and MoneyReportHeaderModals.tsx:71-85. A small hook that returns showOfflineModal and showDownloadFailedModal would remove those copies.
Behavior change:useDecisionModal defaults to shouldHandleNavigationBack: true, so browser Back will close these alerts. None of the inline modals set this today. It matches the modals that are already migrated.
Memo deps: several triggers sit inside useMemo menu-item arrays. Add showDecisionModal to their deps.
Cleanup: most files can also drop their isSmallScreenWidth / useResponsiveLayout usage, which only existed for the modal.
Not verified at runtime: the SelectionToolbar and SearchBulkActions "modal never appears after unmount" bugs come from reading the code only.
Next Steps: Reply with @MelvinBot <your feedback> to refine this, or @MelvinBot implement the Easy group to open a draft PR.
Adds a dismiss result to DecisionModalWrapper and changes the useAssignCard API
All four are assigned to ZhenjaHorbach and roryabraham with the Daily label, the same as the ConfirmModal batches. Batch 4 must add the wrapper's dismiss action before it migrates the domain pages.
Activity
melvin-bot commentedon Jan 22, 2026
Triggered auto assignment to @JmillsExpensify (
Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.roryabraham commentedon Jan 23, 2026
Going to try taking the quicker/cheaper path here and do this refactoring in one go, rather than in many incremental PRs as we have done for
ConfirmModal, relying on Applause for pre-merge regression testing. I understand it makes the single PR riskier, but:roryabraham commentedon Jan 26, 2026
QA ongoing - one issue reported but it seems like expected behavior
29 remaining items
roryabraham commentedon Sep 16, 2026
@ZhenjaHorbach is actively finishing up ConfirmModal migrations in #100076, then will start on this.
ZhenjaHorbach commentedon Oct 5, 2026
@MelvinBot
Analyze all DecisionModals and check which modals can be migrated
MelvinBot commentedon Oct 5, 2026
DecisionModal migration audit
The global infrastructure already exists (
useDecisionModal,DecisionModalWrapper,useHoldMenuModal). There are 18 inline DecisionModals left: 16<DecisionModal>elements plus 2<ProcessMoneyReportHoldMenu>call sites. You can migrate 16 of them now. The 2 domain "Close account" modals need a wrapper change first.Easy: migrate now (10)
None of these has a first option or an
onModalHide, and all have static text. Each one becomesshowDecisionModal({title, prompt, secondOptionText}), and you can delete theisVisiblestate.showDecisionModalin the.catchof bothindex.tsxandindex.native.tsxNeeds care: migrate with small extra work (6)
close(() => showDecisionModal(...))wrapper so the ConfirmModal closes first, instead of reappearing after "OK"useAssignCardtakes a state setter. Change it to anonOfflineAssigncallback, and updatetests/unit/hooks/useAssignCard.test.tsuseSearchBulkActions(about 20 setter sites) and is asserted in 4 test files. Pass an explicit prompt at each download-failed site. TodayemptyReportsCountcan be stale and show the wrong messageHoldMenuModalWrapper. Replace both callers withuseHoldMenuModal().showHoldMenu, as MoneyReportHeaderSelectionDropdown already does. Then deleteProcessMoneyReportHoldMenu,ReportPreviewHoldMenu, and the preview hold-menu contextBlocked: needs a wrapper change first (2)
Today, dismissing these modals with the backdrop, Escape, or Back does nothing. The wrapper maps dismiss to the second option (DecisionModalWrapper.tsx:47). That makes dismiss look the same as "Safe close". After a naive migration, dismissing would open the close-account confirmation. Cancelling it reopens the decision modal (DomainMemberDetailsPage.tsx:107-110), so the user would be stuck in a loop.
Fix: have the wrapper return a distinct action on dismiss, for example a new
ModalActions.DISMISS. Then migrate these pages as an async flow that awaits the decision and then the confirmation.Cross-cutting notes
showOfflineModalandshowDownloadFailedModalwould remove those copies.useDecisionModaldefaults toshouldHandleNavigationBack: true, so browser Back will close these alerts. None of the inline modals set this today. It matches the modals that are already migrated.useMemomenu-item arrays. AddshowDecisionModalto their deps.isSmallScreenWidth/useResponsiveLayoutusage, which only existed for the modal.Next Steps: Reply with
@MelvinBot <your feedback>to refine this, or@MelvinBot implement the Easy groupto open a draft PR.view run
ZhenjaHorbach commentedon Oct 5, 2026
@MelvinBot
Can you create 4 sub-issues for migrating these modals?
With approximately the same migration complexity
MelvinBot commentedon Oct 5, 2026
I created 4 sub-issues and linked them to this issue. They cover all 18 modals and are split so each is about the same amount of work:
useSearchBulkActionshook and 5 test filesuseHoldMenuModal, then deletes the old component and the preview contextDecisionModalWrapperand changes theuseAssignCardAPIAll four are assigned to
ZhenjaHorbachandroryabrahamwith theDailylabel, the same as the ConfirmModal batches. Batch 4 must add the wrapper's dismiss action before it migrates the domain pages.view run
roryabraham commentedon Oct 20, 2026
@MelvinBot what is the status of this issue and its sub-issues?
MelvinBot commentedon Oct 20, 2026
One of the 4 sub-issues is done, one is in review, and two have not started. The parent issue stays open until all 18 modals are migrated.
ZhenjaHorbach's approval, but your approval was dismissed. The PR has merge conflicts again.ZhenjaHorbachasked to close it.To unblock Batch 1, fix the conflicts and get your re-approval.
view run
ZhenjaHorbach commentedon Nov 5, 2026
@MelvinBot
Can we remove DecisionModal component now?