Skip to content

Unify Delete UX: Add Consistent Confirmation Dialogs Across Pages and Introduce Per-Entry Delete in History#68

Merged
macnev2013 merged 12 commits into
macnev2013:mainfrom
adiudiuu:main
Jun 10, 2026
Merged

Unify Delete UX: Add Consistent Confirmation Dialogs Across Pages and Introduce Per-Entry Delete in History#68
macnev2013 merged 12 commits into
macnev2013:mainfrom
adiudiuu:main

Conversation

@adiudiuu

@adiudiuu adiudiuu commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Background

Several pages in the project support delete actions, but confirmation behavior is inconsistent:

  • Some pages delete immediately
  • Some pages use system-level confirm
  • Some pages use custom confirmation dialogs

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

  • Added src/components/shared/ConfirmDangerDialog.tsx
  • Unified dangerous action dialog style and button behavior (Cancel / Confirm)

2) History page

  • Added per-entry delete (Delete from context menu)
  • Added in-app confirmation before deletion
  • No “clear all history” entry point

3) Unified delete confirmations on Dashboard

  • Added confirmation before host deletion
    • src/components/dashboard/HostCard.tsx
  • Added confirmation before S3 connection deletion
    • src/components/dashboard/S3Card.tsx

4) Port Forwarding page

  • Added confirmation before tunnel rule deletion
    • src/components/port-forwarding/PortForwardingPage.tsx

5) S3 page

  • Added confirmation before deleting saved connections
    • src/components/s3/S3Page.tsx

6) Snippets page

  • Added confirmation before snippet deletion
    • src/components/snippets/SnippetCard.tsx
  • Added confirmation before snippet folder deletion
    • src/components/snippets/SnippetFolderCard.tsx

7) Backend delete API status

  • Added the command chain for per-entry History deletion:
    • src-tauri/src/db/mod.rs
    • src-tauri/src/db/commands.rs
    • src-tauri/src/lib.rs
  • Did not expose a “clear all history” command

Scope

  • Frontend: delete interaction flows
  • Backend: History per-entry delete command registration/retention
  • No database schema changes

Risk and Compatibility

  • Low risk: changes are mostly in UI interaction flow
  • Behavior change: delete actions now require explicit user confirmation

Validation Checklist

Please verify the following flows:

  1. History context menu Delete -> dialog -> Confirm deletes only the selected entry.
  2. Host context menu Delete -> dialog -> Confirm deletes the host.
  3. S3 card/saved connection Delete -> dialog -> Confirm deletes the connection.
  4. Port Forward rule Delete -> dialog -> Confirm deletes the rule.
  5. Snippet / Snippet Folder Delete -> dialog -> Confirm deletes the target.
  6. Cancel or clicking the backdrop does not perform deletion.

Changed Files

  • src/components/shared/ConfirmDangerDialog.tsx
  • src/components/history/HistoryPage.tsx
  • src/components/dashboard/HostCard.tsx
  • src/components/dashboard/S3Card.tsx
  • src/components/port-forwarding/PortForwardingPage.tsx
  • src/components/s3/S3Page.tsx
  • src/components/snippets/SnippetCard.tsx
  • src/components/snippets/SnippetFolderCard.tsx
  • src-tauri/src/db/mod.rs
  • src-tauri/src/db/commands.rs
  • src-tauri/src/lib.rs

Copilot AI review requested due to automatic review settings June 8, 2026 12:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 ConfirmDangerDialog component 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 in HistoryPage.

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">
@macnev2013 macnev2013 added this to the v0.10.4 milestone Jun 10, 2026
@macnev2013 macnev2013 self-requested a review June 10, 2026 09:07
@macnev2013 macnev2013 added the enhancement New feature or request label Jun 10, 2026
adiudiuu and others added 2 commits June 10, 2026 16:35
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>
macnev2013 and others added 10 commits June 10, 2026 16:39
…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>
@macnev2013 macnev2013 merged commit 95028f8 into macnev2013:main Jun 10, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants