refactor(frontend): migrate remaining confirm() to ConfirmDialog (6 files)

Replaced 7 native confirm() calls with styled <ConfirmDialog>:
- settings/git (2: delete config + delete repo)
- settings/automation (delete policy)
- TaskHistory (clear tasks)
- ApiKeysTab (revoke key)
- ConversationList (delete conversation)
- Settings page (delete environment)

Updated 2 test files to verify ConfirmDialog interactions.
This commit is contained in:
2026-06-17 15:13:17 +03:00
parent 1803f3fa1e
commit bd9a8cba79
7 changed files with 147 additions and 38 deletions

View File

@@ -14,6 +14,7 @@
import Input from "$lib/ui/Input.svelte";
import Button from "$lib/ui/Button.svelte";
import Icon from "$lib/ui/Icon.svelte";
import { ConfirmDialog } from "$lib/ui";
let {
conversations = [],
@@ -37,6 +38,8 @@
let searchQuery = $state("");
let searchTimer: ReturnType<typeof setTimeout> | null = null;
let showDeleteConfirm = $state(false);
let pendingDeleteId: string | null = $state(null);
// Group conversations by date on client
let groupedConversations = $derived.by(() => {
@@ -76,9 +79,8 @@
function handleDelete(e: Event, id: string) {
e.stopPropagation();
if (confirm($t.assistant?.delete_confirm || "Delete this conversation?")) {
ondelete?.(id);
}
pendingDeleteId = id;
showDeleteConfirm = true;
}
</script>
@@ -148,4 +150,16 @@
{/if}
</div>
</div>
<ConfirmDialog
bind:show={showDeleteConfirm}
title={$t.assistant?.delete_title || "Delete conversation?"}
message={$t.assistant?.delete_confirm || "This conversation will be permanently deleted."}
variant="destructive"
confirmLabel={$t.assistant?.delete || "Delete"}
cancelLabel={$t.common?.cancel || "Cancel"}
onConfirm={() => { if (pendingDeleteId) ondelete?.(pendingDeleteId); }}
onCancel={() => { pendingDeleteId = null; }}
/>
<!-- #endregion AgentChat.ConversationList -->

View File

@@ -163,12 +163,9 @@ describe("ConversationList — search", () => {
// #endregion TestAgentChat.ConversationList.Search
// #region TestAgentChat.ConversationList.Delete [C:2] [TYPE Test]
// @UX_STATE loaded — delete button calls ondelete after window.confirm.
// @UX_STATE loaded — delete button opens ConfirmDialog, confirm triggers ondelete.
describe("ConversationList — delete action", () => {
it("calls ondelete after window.confirm returns true", () => {
// Set up window.confirm to return true
const originalConfirm = window.confirm;
window.confirm = vi.fn(() => true);
it("calls ondelete after confirming in the dialog", () => {
const ondelete = vi.fn();
render(ConversationList, {
@@ -186,14 +183,15 @@ describe("ConversationList — delete action", () => {
expect(deleteBtns.length).toBeGreaterThan(0);
fireEvent.click(deleteBtns[0]);
expect(window.confirm).toHaveBeenCalled();
expect(ondelete).toHaveBeenCalledWith("c1");
// ConfirmDialog appears — click the "Delete" button inside it
const confirmBtn = screen.getByText("Delete");
expect(confirmBtn).toBeTruthy();
fireEvent.click(confirmBtn);
window.confirm = originalConfirm;
expect(ondelete).toHaveBeenCalledWith("c1");
});
it("does not call ondelete when window.confirm returns false", () => {
window.confirm = vi.fn(() => false);
it("does not call ondelete when dialog is cancelled", () => {
const ondelete = vi.fn();
render(ConversationList, {
@@ -209,7 +207,11 @@ describe("ConversationList — delete action", () => {
const deleteBtns = screen.getAllByTitle(/delete/i);
fireEvent.click(deleteBtns[0]);
expect(window.confirm).toHaveBeenCalled();
// ConfirmDialog appears — click "Cancel" button
const cancelBtn = screen.getByText("Cancel");
expect(cancelBtn).toBeTruthy();
fireEvent.click(cancelBtn);
expect(ondelete).not.toHaveBeenCalled();
});
});

View File

@@ -18,6 +18,7 @@
import { t } from '$lib/i18n/index.svelte.js';
import { api } from '$lib/api.js';
import { addToast } from '$lib/toasts.svelte.js';
import { ConfirmDialog } from '$lib/ui';
let keys = $state([]);
let environments = $state([]);
@@ -27,6 +28,8 @@
let isGenerating = $state(false);
let revokingId = $state(null);
let revealedKey = $state(null);
let showRevokeConfirm = $state(false);
let pendingRevokeKey = $state(null);
// Generate form state
let formName = $state('');
@@ -137,9 +140,8 @@
}
function confirmRevoke(key) {
if (confirm('All tools using this key will get 401. Are you sure?')) {
handleRevoke(key.id);
}
pendingRevokeKey = key.id;
showRevokeConfirm = true;
}
async function copyToClipboard(text) {
@@ -394,4 +396,16 @@
</div>
{/if}
</div>
<ConfirmDialog
bind:show={showRevokeConfirm}
title={$t.api_keys?.revoke?.title || 'Revoke API key?'}
message="All tools using this key will get 401. Are you sure?"
variant="destructive"
confirmLabel={$t.api_keys?.revoke?.revoke || 'Revoke'}
cancelLabel={$t.common?.cancel || 'Cancel'}
onConfirm={() => { if (pendingRevokeKey) handleRevoke(pendingRevokeKey); }}
onCancel={() => { pendingRevokeKey = null; }}
/>
<!-- #endregion ApiKeysTab -->

View File

@@ -23,6 +23,7 @@
import { getSettings, updateGlobalSettings, addEnvironment, updateEnvironment, deleteEnvironment, testEnvironmentConnection } from '../lib/api';
import { addToast } from '$lib/toasts.svelte.js';
import { t } from '$lib/i18n/index.svelte.js';
import { ConfirmDialog } from '$lib/ui';
// [/SECTION]
let settings = {
@@ -53,6 +54,8 @@
};
let editingEnvId = null;
let showDeleteEnv = $state(false);
let pendingDeleteEnvId = $state(null);
// #region loadSettings:Function [TYPE Function]
// @ingroup Module
@@ -129,18 +132,23 @@
* @post Environment is removed from backend and list is reloaded.
* @param {string} id - The ID of the environment to delete.
*/
async function handleDeleteEnv(id) {
if (confirm($t.settings?.env_delete_confirm)) {
try {
console.log(`[Settings.handleDeleteEnv][Action] Deleting environment: ${id}`);
await deleteEnvironment(id);
addToast($t.settings?.env_deleted, 'success');
await loadSettings();
console.log("[Settings.handleDeleteEnv][Coherence:OK] Environment deleted.");
} catch (error) {
console.error("[Settings.handleDeleteEnv][Coherence:Failed] Failed to delete environment:", error);
addToast($t.settings?.env_delete_failed, 'error');
}
function handleDeleteEnv(id) {
pendingDeleteEnvId = id;
showDeleteEnv = true;
}
async function confirmDeleteEnv() {
const id = pendingDeleteEnvId;
pendingDeleteEnvId = null;
try {
console.log(`[Settings.handleDeleteEnv][Action] Deleting environment: ${id}`);
await deleteEnvironment(id);
addToast($t.settings?.env_deleted, 'success');
await loadSettings();
console.log("[Settings.handleDeleteEnv][Coherence:OK] Environment deleted.");
} catch (error) {
console.error("[Settings.handleDeleteEnv][Coherence:Failed] Failed to delete environment:", error);
addToast($t.settings?.env_delete_failed, 'error');
}
}
// #endregion handleDeleteEnv:Function
@@ -352,4 +360,15 @@
</div>
<!-- [/SECTION] -->
<ConfirmDialog
bind:show={showDeleteEnv}
title={$t.settings?.env_delete_title || 'Delete environment?'}
message={$t.settings?.env_delete_confirm}
variant="destructive"
confirmLabel={$t.common?.delete || 'Delete'}
cancelLabel={$t.common?.cancel || 'Cancel'}
onConfirm={confirmDeleteEnv}
onCancel={() => { pendingDeleteEnvId = null; }}
/>
<!-- #endregion SettingsPage -->

View File

@@ -12,7 +12,7 @@
<!-- @UX_RECOVERY: Retry load via refresh. -->
<script lang="ts">
import { onMount } from 'svelte';
import { Button, PageHeader, Card, EmptyState } from '$lib/ui';
import { Button, PageHeader, Card, EmptyState, ConfirmDialog } from '$lib/ui';
import PolicyForm from '$lib/components/health/PolicyForm.svelte';
import {
getValidationPolicies,
@@ -31,6 +31,8 @@
let isLoading = $state(true);
let showForm = $state(false);
let selectedPolicy = $state(null);
let showDeletePolicy = $state(false);
let pendingDeletePolicyId = $state(null);
onMount(async () => {
await loadData();
@@ -84,7 +86,13 @@
}
async function handleDelete(id) {
if (!confirm(t.settings?.delete_confirm || 'Are you sure you want to delete this policy?')) return;
pendingDeletePolicyId = id;
showDeletePolicy = true;
}
async function confirmDeletePolicy() {
const id = pendingDeletePolicyId;
pendingDeletePolicyId = null;
try {
await deleteValidationPolicy(id);
addToast(t.settings?.policy_deleted || 'Policy deleted successfully', 'success');
@@ -254,4 +262,16 @@
{/if}
{/if}
</div>
<ConfirmDialog
bind:show={showDeletePolicy}
title={$t.settings?.delete_confirm_title || 'Delete policy?'}
message={$t.settings?.delete_confirm || 'Are you sure you want to delete this policy?'}
variant="destructive"
confirmLabel={$t.common?.delete || 'Delete'}
cancelLabel={$t.common?.cancel || 'Cancel'}
onConfirm={confirmDeletePolicy}
onCancel={() => { pendingDeletePolicyId = null; }}
/>
<!-- #endregion AutomationPage -->

View File

@@ -12,7 +12,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { t } from '$lib/i18n/index.svelte.js';
import { Button, Input, Card, PageHeader, Select } from '$lib/ui';
import { Button, Input, Card, PageHeader, Select, ConfirmDialog } from '$lib/ui';
import HelpTooltip from '$lib/ui/HelpTooltip.svelte';
import { GitConfigModel } from '$lib/models/GitConfigModel.svelte.ts';
@@ -23,6 +23,22 @@
$effect(() => {
if (model.selectedGiteaConfigId) void model.loadGiteaRepos();
});
let showDeleteConfig = $state(false);
let pendingDeleteConfigId: string | null = $state(null);
let showDeleteRepo = $state(false);
let pendingDeleteRepo: object | null = $state(null);
function handleDeleteConfig(id: string) {
pendingDeleteConfigId = id;
showDeleteConfig = true;
}
function handleDeleteRepo(repo: object) {
pendingDeleteRepo = repo;
showDeleteRepo = true;
}
</script>
<div class="p-6 max-w-6xl mx-auto">
@@ -48,7 +64,7 @@
<Button variant="ghost" class="text-text-subtle hover:text-primary transition-colors" onclick={() => { model.startEdit(config.id); window.scrollTo({ top: 0, behavior: 'smooth' }); }} title={$t.common?.edit || "Edit"}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
</Button>
<Button variant="ghost" class="text-text-subtle hover:text-destructive transition-colors" onclick={() => { if (confirm($t.settings?.git_delete_confirm)) model.deleteConfig(config.id); }} title={$t.common?.delete}>
<Button variant="ghost" class="text-text-subtle hover:text-destructive transition-colors" onclick={() => handleDeleteConfig(config.id)} title={$t.common?.delete}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
</Button>
</div>
@@ -137,7 +153,7 @@
<div class="font-medium text-text truncate">{repo.full_name || repo.name}</div>
<div class="text-xs text-text-muted truncate">{repo.clone_url || repo.html_url}</div>
</div>
<Button variant="destructive" size="sm" onclick={() => { if (confirm(`Delete repository ${repo.full_name || repo.name}?`)) model.deleteRemoteRepo(repo); }} disabled={model.giteaDeleting}>Delete</Button>
<Button variant="destructive" size="sm" onclick={() => handleDeleteRepo(repo)} disabled={model.giteaDeleting}>Delete</Button>
</div>
{/each}
</div>
@@ -147,4 +163,27 @@
</Card>
</div>
</div>
<ConfirmDialog
bind:show={showDeleteConfig}
title="Delete Git server?"
message={$t.settings?.git_delete_confirm}
variant="destructive"
confirmLabel="Delete"
cancelLabel="Cancel"
onConfirm={() => { if (pendingDeleteConfigId) model.deleteConfig(pendingDeleteConfigId); }}
onCancel={() => { pendingDeleteConfigId = null; }}
/>
<ConfirmDialog
bind:show={showDeleteRepo}
title="Delete repository?"
message={pendingDeleteRepo ? `Delete repository ${pendingDeleteRepo.full_name || pendingDeleteRepo.name}?` : ''}
variant="destructive"
confirmLabel="Delete"
cancelLabel="Cancel"
onConfirm={() => { if (pendingDeleteRepo) model.deleteRemoteRepo(pendingDeleteRepo); }}
onCancel={() => { pendingDeleteRepo = null; }}
/>
<!-- #endregion GitDashboardPage -->

View File

@@ -79,8 +79,6 @@ describe('GitSettingsPage UX Contracts', () => {
beforeEach(() => {
vi.clearAllMocks();
// Default confirm to always accept for tests
global.confirm = vi.fn(() => true);
});
// @UX_STATE: Initial Load
@@ -164,12 +162,15 @@ describe('GitSettingsPage UX Contracts', () => {
render(GitSettingsPage);
await waitFor(() => expect(screen.getByText('Dev GitLab')).toBeTruthy());
// Click delete button
// Click delete button to open ConfirmDialog
const deleteButton = screen.getByTitle('Delete');
await fireEvent.click(deleteButton);
// Click the "Delete" button in the ConfirmDialog
await waitFor(() => expect(screen.getByText('Delete')).toBeTruthy());
await fireEvent.click(screen.getByText('Delete'));
await waitFor(() => {
expect(global.confirm).toHaveBeenCalledWith('Are you sure?');
expect(gitService.deleteConfig).toHaveBeenCalledWith('conf-1');
expect(addToast).toHaveBeenCalledWith('Git configuration deleted', 'success');
expect(screen.queryByText('Dev GitLab')).toBeNull(); // Should be removed from DOM