-
Notifications
You must be signed in to change notification settings - Fork 23
[UX] Confirmation Dialog System #256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Replaces `window.confirm` with a custom, accessible `ConfirmDialog` component. - Implements `ConfirmContext` for asynchronous confirmation handling using Promises. - Adds `ConfirmProvider` to `App.tsx` to enable global usage. - Updates `GroupDetails.tsx` to use `useConfirm` for destructive actions (Delete Group, Delete Expense, Leave Group, Remove Member). - Enhances `Modal.tsx` accessibility by adding `role="dialog"`, `aria-modal="true"`, and `aria-labelledby`. - Supports both Neobrutalism and Glassmorphism themes.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
✅ Deploy Preview for split-but-wiser ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
WalkthroughAdds a promise-based Confirmation Dialog System (ConfirmProvider, useConfirm, ConfirmDialog), integrates it into the app, replaces native window.confirm usages in GroupDetails, and improves Modal accessibility with ARIA attributes; updates related docs. Changes
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this 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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/pages/GroupDetails.tsx (1)
366-393: Unsettled balance check happens after confirmation.The check for unsettled balances (lines 379-385) occurs after the user confirms removal. Consider validating this before showing the confirmation dialog to avoid a confusing experience where the user confirms but the action still fails.
Move validation before confirmation
const handleKickMember = async (memberId: string, memberName: string) => { if (!id || !isAdmin) return; if (memberId === user?._id) return; + const hasUnsettled = settlements.some( + s => (s.fromUserId === memberId || s.toUserId === memberId) && (s.amount || 0) > 0 + ); + if (hasUnsettled) { + addToast('Cannot remove: This member has unsettled balances in the group.', 'error'); + return; + } + const confirmed = await confirm({ title: 'Remove Member', description: `Are you sure you want to remove ${memberName} from the group?`, confirmText: 'Remove', variant: 'danger' }); if (confirmed) { try { - const hasUnsettled = settlements.some( - s => (s.fromUserId === memberId || s.toUserId === memberId) && (s.amount || 0) > 0 - ); - if (hasUnsettled) { - alert('Cannot remove: This member has unsettled balances in the group.'); - return; - } await removeMember(id, memberId); fetchData(); addToast(`Removed ${memberName} from group`, 'success'); } catch (err: any) { addToast(err.response?.data?.detail || "Failed to remove member", 'error'); } } };
🤖 Fix all issues with AI agents
In `@web/components/ui/ConfirmDialog.tsx`:
- Around line 31-32: The destructured but unused variable `mode` from useTheme()
should be removed to eliminate the unused variable warning: update the
destructuring in the ConfirmDialog component (where useTheme() is called) to
only pull `style` (e.g., change `const { style, mode } = useTheme()` to `const {
style } = useTheme()`), leaving the rest of the logic that uses `style`/`isNeo`
(`THEMES.NEOBRUTALISM`, `isNeo`) untouched.
- Around line 71-79: The ConfirmDialog currently sets autoFocus on the Confirm
button which can cause accidental destructive confirmations; update the footer
so that autoFocus is applied conditionally: if getButtonVariant() returns a
destructive variant (e.g., "danger" or "warning") move the autoFocus prop from
the confirm Button (onConfirm) to the cancel Button (onCancel), otherwise keep
autoFocus on the confirm Button; reference ConfirmDialog, getButtonVariant,
onConfirm, onCancel, cancelText and confirmText when making this change.
In `@web/components/ui/Modal.tsx`:
- Around line 68-70: The close button in Modal.tsx currently can act as a submit
button when placed inside forms; update the <button> element that calls onClose
(the button rendering the X icon) to include an explicit type="button" attribute
so it never triggers form submission, keeping the onClose handler and existing
classes/aria-label unchanged.
- Line 46: The Modal component currently hardcodes id="modal-title", which can
collide when multiple modals render; update the Modal (in
web/components/ui/Modal.tsx) to generate a unique id (e.g., call React's useId()
at top of the Modal component), assign that generated id to the dialog's
aria-labelledby attribute and to the modal title element (the element that
currently has id="modal-title"); add the necessary import for useId and replace
the hardcoded string with the generated id so each Modal instance gets a unique,
matching aria-labelledby/title id pair.
In `@web/contexts/ConfirmContext.tsx`:
- Around line 26-32: The current confirm function overwrites the previous
pending promise's resolver, leaving it unresolved; update confirm to first check
for an existing resolver (e.g., resolveRef.current or the existing value held by
setResolveRef) and settle it (call the previous resolve with false or call a
stored reject) before storing the new resolve and returning the new Promise;
specifically, in confirm, before setResolveRef(() => resolve) call the existing
resolver if present to resolve(false) (or reject) so the previous promise is not
left hanging, then proceed to setOptions, setIsOpen, and set the new resolver.
|
@jules In 366-393: Unsettled balance check happens after confirmation. The check for unsettled balances (lines 379-385) occurs after the user confirms removal. Consider validating this before showing the confirmation dialog to avoid a confusing experience where the user confirms but the action still fails. |
|
@jules In 366-393: Unsettled balance check happens after confirmation. The check for unsettled balances (lines 379-385) occurs after the user confirms removal. Consider validating this before showing the confirmation dialog to avoid a confusing experience where the user confirms but the action still fails. |
Thank you for the detailed feedback. I have implemented all suggested changes:
|
I have moved the unsettled balance check to before the confirmation dialog in |
- **ConfirmDialog.tsx**: Removed unused `mode` variable and updated `autoFocus` logic to prioritize Cancel button for destructive actions. - **Modal.tsx**: Added `type="button"` to close button and implemented `React.useId()` for unique accessibility IDs. - **ConfirmContext.tsx**: Updated to safely resolve pending promises using `useRef` before starting new confirmations. - **GroupDetails.tsx**: Moved unsettled balance check before the removal confirmation dialog for better UX. Co-authored-by: Devasy <110348311+Devasy@users.noreply.github.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this 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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/pages/GroupDetails.tsx (1)
360-361: Consider typing error objects instead of usingany.The
err: anypattern bypasses TypeScript's type checking. Consider using a more specific type or a type guard for safer error handling.♻️ Optional: Type-safe error handling
- } catch (err: any) { - addToast(err.response?.data?.detail || "Cannot leave - please settle balances first", 'error'); + } catch (err) { + const message = err instanceof Error ? err.message : "Cannot leave - please settle balances first"; + const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail; + addToast(detail || message, 'error'); }Alternatively, create a utility function for extracting API error messages.
Also applies to: 397-398
🤖 Fix all issues with AI agents
In `@web/components/ui/ConfirmDialog.tsx`:
- Around line 35-44: The switch statements in getIcon and getIconBg should
include explicit default branches to return a safe fallback icon/background when
variant (ConfirmVariant) is unexpected; update getIcon to add a default case
that returns a neutral icon (e.g., Info or AlertTriangle with neutral styling)
and update getIconBg to add a default background case so the UI still renders if
a new/unknown variant is passed, keeping existing cases intact.
| const getIcon = () => { | ||
| switch (variant) { | ||
| case 'danger': | ||
| return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-red-500'} />; | ||
| case 'warning': | ||
| return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-yellow-500'} />; | ||
| case 'info': | ||
| return <Info size={32} className={isNeo ? 'text-black' : 'text-blue-500'} />; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding default cases to switch statements for defensive coding.
The getIcon() and getIconBg() functions lack default cases. While currently safe due to the typed ConfirmVariant union and default prop value, adding explicit defaults would future-proof against accidental omissions if new variants are added.
♻️ Optional: Add default cases
const getIcon = () => {
switch (variant) {
case 'danger':
return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-red-500'} />;
case 'warning':
return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-yellow-500'} />;
case 'info':
return <Info size={32} className={isNeo ? 'text-black' : 'text-blue-500'} />;
+ default:
+ return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-red-500'} />;
}
};📝 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.
| const getIcon = () => { | |
| switch (variant) { | |
| case 'danger': | |
| return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-red-500'} />; | |
| case 'warning': | |
| return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-yellow-500'} />; | |
| case 'info': | |
| return <Info size={32} className={isNeo ? 'text-black' : 'text-blue-500'} />; | |
| } | |
| }; | |
| const getIcon = () => { | |
| switch (variant) { | |
| case 'danger': | |
| return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-red-500'} />; | |
| case 'warning': | |
| return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-yellow-500'} />; | |
| case 'info': | |
| return <Info size={32} className={isNeo ? 'text-black' : 'text-blue-500'} />; | |
| default: | |
| return <AlertTriangle size={32} className={isNeo ? 'text-black' : 'text-red-500'} />; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In `@web/components/ui/ConfirmDialog.tsx` around lines 35 - 44, The switch
statements in getIcon and getIconBg should include explicit default branches to
return a safe fallback icon/background when variant (ConfirmVariant) is
unexpected; update getIcon to add a default case that returns a neutral icon
(e.g., Info or AlertTriangle with neutral styling) and update getIconBg to add a
default background case so the UI still renders if a new/unknown variant is
passed, keeping existing cases intact.
This PR introduces a complete Confirmation Dialog system to the Web application, replacing the native and intrusive
window.confirmdialogs with a custom, accessible, and themable UI.Changes:
web/components/ui/ConfirmDialog.tsx- A specialized modal for confirmations with support for 'danger', 'warning', and 'info' variants.web/contexts/ConfirmContext.tsx- Provides auseConfirmhook that returns a Promise, allowing for cleaner async/await usage in components.ConfirmProviderwithinweb/App.tsx.web/pages/GroupDetails.tsxto use the new system for all destructive actions.Modalcomponent to include proper ARIA attributes, ensuring it is correctly identified by screen readers.Verification:
Modalstyles works for both themes.PR created automatically by Jules for task 12669270586709539879 started by @Devasy23
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.