Unify Delete UX: Add Consistent Confirmation Dialogs Across Pages and Introduce Per-Entry Delete in History#68
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR adds a reusable danger confirmation dialog and wires it into multiple “Delete” actions across the UI, including a new backend command to delete individual connection history entries.
Changes:
- Introduced a shared
ConfirmDangerDialogcomponent for destructive action confirmation. - Updated various cards/pages to open the confirmation dialog before executing delete actions.
- Added a new Tauri backend DB command (
delete_connection_history_entry) and frontend integration inHistoryPage.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/shared/ConfirmDangerDialog.tsx | Adds the shared confirmation modal used for destructive actions. |
| src/components/snippets/SnippetCard.tsx | Routes snippet deletion through the confirmation dialog. |
| src/components/snippets/SnippetFolderCard.tsx | Routes folder deletion through the confirmation dialog. |
| src/components/dashboard/HostCard.tsx | Routes host deletion through the confirmation dialog. |
| src/components/dashboard/S3Card.tsx | Routes S3 connection deletion through the confirmation dialog. |
| src/components/s3/S3Page.tsx | Adds confirmation flow for deleting saved S3 connections from the page context menu. |
| src/components/port-forwarding/PortForwardingPage.tsx | Adds confirmation flow for deleting port-forwarding rules. |
| src/components/history/HistoryPage.tsx | Adds history-row delete UI + calls the new backend delete command. |
| src-tauri/src/lib.rs | Registers the new Tauri command. |
| src-tauri/src/db/mod.rs | Implements DB deletion for a single history entry by id. |
| src-tauri/src/db/commands.rs | Exposes the delete command to the frontend via Tauri. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+27
to
+32
| <div className="fixed inset-0 z-50 flex items-center justify-center" aria-modal="true" role="dialog" aria-labelledby="confirm-danger-title"> | ||
| <div | ||
| className="absolute inset-0 bg-bg-base/70 backdrop-blur-sm" | ||
| onClick={onCancel} | ||
| aria-hidden="true" | ||
| /> |
Comment on lines
+47
to
+62
| <button | ||
| onClick={onCancel} | ||
| className={[ | ||
| "px-4 py-1.5 rounded-lg text-[length:var(--text-sm)] font-medium", | ||
| "text-text-secondary bg-bg-overlay border border-border", | ||
| "hover:text-text-primary hover:border-border-focus hover:bg-bg-subtle", | ||
| "transition-all duration-[var(--duration-fast)]", | ||
| "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", | ||
| ].join(" ")} | ||
| > | ||
| {cancelLabel} | ||
| </button> | ||
| <button | ||
| onClick={onConfirm} | ||
| disabled={busy} | ||
| className={[ |
| if (!open) return null; | ||
|
|
||
| return ( | ||
| <div className="fixed inset-0 z-50 flex items-center justify-center" aria-modal="true" role="dialog" aria-labelledby="confirm-danger-title"> |
Comment on lines
+39
to
+42
| <h2 id="confirm-danger-title" className="text-[length:var(--text-sm)] font-semibold text-text-primary"> | ||
| {title} | ||
| </h2> | ||
| <p className="text-[length:var(--text-xs)] text-text-muted mt-1">{message}</p> |
Comment on lines
79
to
84
| { | ||
| label: "Delete", | ||
| icon: Trash2, | ||
| danger: true, | ||
| onClick: () => onDelete(snippet.id), | ||
| onClick: () => setConfirmDelete(true), | ||
| }, |
Comment on lines
+185
to
+191
| { | ||
| label: "Delete", | ||
| icon: Trash2, | ||
| danger: true, | ||
| disabled: mutating, | ||
| onClick: () => setConfirmDeleteId(entry.id), | ||
| }, |
| <AlertTriangle size={18} strokeWidth={1.8} className="text-status-error" aria-hidden="true" /> | ||
| </div> | ||
| <div> | ||
| <h2 id="confirm-danger-title" className="text-[length:var(--text-sm)] font-semibold text-text-primary"> |
…try Delete in History
ConfirmDangerDialog: - Add Escape-to-cancel (capture phase, matches GroupDeleteDialog pattern) - Focus the Cancel button on open so keyboard users land safely - Backdrop click now respects the busy flag (won't dismiss while deleting) - Use useId() for aria-labelledby so multiple instances never share a DOM id HistoryPage: - Keep the dialog open while the delete is in flight; close only on success so the busy spinner and disabled buttons are actually visible - Surface backend errors with a toast instead of swallowing them silently - Decrement offsetRef after a local delete to keep Load-more pagination in sync with the database (previously skipped one row per deleted entry) db/mod.rs: - Generalize DbError::NotFound message from "Host not found" to "Not found" now that the variant is used for history rows too - Add delete_history_entry_removes_single_row and delete_history_entry_returns_not_found_for_missing_id tests following the existing delete_removes_row / delete_missing_returns_not_found pattern Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…e keyboard handling - Structure now matches HostEditModal: entrance animation (opacity + translate-y), header with AlertTriangle icon + title + X close button + border-b, body with message, footer with Cancel/Confirm + border-t, bg-bg-overlay panel - Keyboard: wrap content in <form onSubmit> so Enter natively confirms via the submit button (type="submit") without any manual Enter-key wiring - Escape is handled by onKeyDown on the form element (React synthetic event) instead of a document.addEventListener — more reliable in Tauri's WebView and doesn't rely on capture-phase ordering with other listeners - Cancel button auto-focused after the entrance animation completes - Backdrop click guarded by busy flag; X button in header as an additional close target matching the pattern in HostEditModal Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Keyboard: - Escape: switch from onKeyDown on <form> to document.addEventListener (capture phase), exactly matching GroupDeleteDialog and HostEditModal — fires regardless of which element has focus - Enter: kept via <form onSubmit> + type="submit" on Confirm; Tab reaches the Confirm button from the Cancel default - autoFocus on the Cancel button: synchronous mount-time focus, replacing the two-level requestAnimationFrame chain which was unreliable Animation: - Drop the dual visible/open state and manual rAF transitions in favour of Tailwind animate-in (fade-in + slide-in-from-top-2), consistent with how Toaster and ContextMenu animate in the same codebase Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…pattern
Every dialog now shares:
- Position: fixed inset-0 z-50 items-start justify-center pt-[8vh]
- Backdrop: bg-black/50 backdrop-blur-sm, click-outside closes
- Panel: bg-bg-overlay, rounded-xl, border border-border, shadow-lg
- Animation: animate-in fade-in slide-in-from-top-2
- Header: title + AlertTriangle/icon + X close button, border-b separator
- Body: px-6 py-4
- Footer: Cancel (autoFocus) + primary action, border-t separator
- Keyboard: document.addEventListener("keydown", handler, true) for Escape
Components updated:
- ExplorerFileTable.DeleteConfirmDialog — was centered with no keyboard
handling; now matches the pattern, imports AlertTriangle/X
- DropOverwriteDialog — was centered, window.addEventListener, no animation;
now consistent; focus managed via ref + combined effect
- GroupDeleteDialog — was bg-bg-surface, bg-bg-base/70 backdrop, centered,
no animation, old button styling; now fully aligned; useId for aria title
- UpdateDialog — was rounded-2xl, custom keyframe animation, no Escape;
now rounded-xl, animate-in, Escape closes via document listener
Dialogs already consistent (no changes): GroupModal, SnippetFolderModal,
VariableDialog, ImportSshConfigModal, SnippetEditModal, S3ConnectDialog,
FilePropertiesDialog, ConnectionDialog, HostEditModal, DisconnectOverlay
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Footer container: px-6 pb-5 pt-3 → px-6 py-3 (symmetric, removes the heavy 20px bottom padding that made footers feel oversized on small panels) Buttons: py-2 → py-1.5 to match the tighter container Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…the standard shell All three now match HostEditModal / S3ConnectDialog / SnippetEditModal: - Panel: flex flex-col, bg-bg-overlay, rounded-xl, border, shadow - Header: px-6 pt-5 pb-4 border-b — icon preview + title on left, X close button on right - Body: px-6 py-5 (VariableDialog: overflow-y-auto flex-1 min-h-0 max-h-[84vh]) - Footer: px-6 py-3 border-t — Cancel + primary action, py-1.5 buttons - Backdrop position: pt-[12vh] → pt-[8vh] to match every other modal - Form is now the panel container so type="submit" continues to work Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…o shared pattern
Footer padding: pb-5 pt-3 → py-3; action buttons: py-2 → py-1.5 in all three,
matching the compact footer introduced on GroupModal and SnippetFolderModal.
Close button: replace inline SVG in HostEditModal and S3ConnectDialog with
<X size={14} strokeWidth={1.8}> from lucide-react, matching every other modal.
S3ConnectDialog animation: replace instant animate-[fadeIn_120ms_...] +
inline-style backdrop with the visible-state pattern used by HostEditModal
and SnippetEditModal (transition-[opacity,transform] duration-slow slide-in +
transition-[background-color,backdrop-filter] backdrop fade).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…sistency
GroupModal and SnippetFolderModal already had a colored live-preview icon in
the header. The four form modals that were missing one now each have a static
accent-tinted icon that matches the app's sidebar navigation icon for that
entity type:
- HostEditModal: Monitor (same icon as Hosts in the sidebar)
- S3ConnectDialog: Cloud (same icon as S3Card)
- SnippetEditModal: Braces (same icon as Snippets in the sidebar)
- VariableDialog: Braces (it operates on a snippet, consistent with edit)
Icon container: w-8 h-8 rounded-lg bg-accent/10, icon size={16} strokeWidth={1.8}
text-accent — mirrors the GroupModal/SnippetFolderModal preview container style.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…dialogs Creates src/components/shared/ModalShell.tsx as the single source of truth for every dialog's visual structure: - Animated backdrop (visible-state transition-[bg,backdrop-filter]) - Animated panel (opacity + translate-y, ease-expo-out, duration-slow) - Header: icon container + title + optional subtitle + X close button + border-b - Scrollable or fixed body - Footer: right-aligned actions + optional left-side footerStart + border-t - Escape key via document listener - Backdrop click close (respects busy flag) Also exports BTN_GHOST / BTN_SECONDARY / BTN_PRIMARY / BTN_DANGER constants so every footer button uses identical classes. Migrated (12 components): ConfirmDangerDialog, GroupDeleteDialog, DropOverwriteDialog, UpdateDialog GroupModal, SnippetFolderModal, VariableDialog, SnippetEditModal S3ConnectDialog, HostEditModal, ImportSshConfigModal ExplorerFileTable.DeleteConfirmDialog Not migrated (intentionally different patterns): ConnectionDialog — centered status/progress dialog, no header/body/footer FilePropertiesDialog — compact properties panel, px-4 padding, centered Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…Modal headers Both used iconNode with a dynamic user-chosen color for the icon container, making them visually different from every other modal which uses bg-accent/10. Switch to icon= so all modal headers render the same bg-accent/10 text-accent icon box — the color picker in the body still works exactly as before. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…al to ModalShell
Three modals missed in the previous pass:
PortForwardingPage.RuleDialog:
- Had inline-style backdrop, animate-[fadeIn] panel, pb-5 pt-3 footer, py-2 buttons
- Now uses ModalShell with icon={Plug} (matches sidebar Tunnels icon)
SettingsPage.BackupPasswordModal:
- Had pt-[10vh], raw icon in header (no container box), no X button, pb-5 pt-3 footer
- Export: icon={ShieldCheck} iconVariant="accent"; Import: icon={AlertCircle} iconVariant="danger"
SettingsPage.ConfirmResetModal:
- Had pt-[12vh], raw icon in header, no X button, pb-5 pt-3 footer, py-2 buttons
- Now uses icon={AlertCircle} iconVariant="danger" via ModalShell
All three now share the exact same backdrop, animation, header, body, and footer
structure as every other modal in the app.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
Several pages in the project support delete actions, but confirmation behavior is inconsistent:
This change standardizes delete confirmations with one in-app confirmation dialog pattern to reduce accidental deletions and improve UX consistency.
What Changed
1) Added a shared danger confirmation dialog
src/components/shared/ConfirmDangerDialog.tsxCancel/Confirm)2) History page
Deletefrom context menu)3) Unified delete confirmations on Dashboard
src/components/dashboard/HostCard.tsxsrc/components/dashboard/S3Card.tsx4) Port Forwarding page
src/components/port-forwarding/PortForwardingPage.tsx5) S3 page
src/components/s3/S3Page.tsx6) Snippets page
src/components/snippets/SnippetCard.tsxsrc/components/snippets/SnippetFolderCard.tsx7) Backend delete API status
src-tauri/src/db/mod.rssrc-tauri/src/db/commands.rssrc-tauri/src/lib.rsScope
Risk and Compatibility
Validation Checklist
Please verify the following flows:
Delete-> dialog ->Confirmdeletes only the selected entry.Delete-> dialog ->Confirmdeletes the host.Delete-> dialog ->Confirmdeletes the connection.Delete-> dialog ->Confirmdeletes the rule.Delete-> dialog ->Confirmdeletes the target.Cancelor clicking the backdrop does not perform deletion.Changed Files
src/components/shared/ConfirmDangerDialog.tsxsrc/components/history/HistoryPage.tsxsrc/components/dashboard/HostCard.tsxsrc/components/dashboard/S3Card.tsxsrc/components/port-forwarding/PortForwardingPage.tsxsrc/components/s3/S3Page.tsxsrc/components/snippets/SnippetCard.tsxsrc/components/snippets/SnippetFolderCard.tsxsrc-tauri/src/db/mod.rssrc-tauri/src/db/commands.rssrc-tauri/src/lib.rs