feat(confirm): centralize confirmation dialogs and add i18n
Introduce a generic ConfirmDialog and a promise-based requestConfirmation API in App.svelte so callers can await user responses instead of using window.confirm. Provide helper builders (branchDeleteConfirmRequest, discardConfirmRequest) to create dialog content for common cases. Many call sites were switched to use requestConfirmation and now render the in-app ConfirmDialog; the previous specialized confirm components (BranchDeleteConfirmDialog, DiscardConfirmDialog) were removed. Add lightweight i18n support (setLanguage, t()) and new messages/i18n modules, and replace hardcoded English strings in several components (e.g. AiSettingsPage, BlameDialog and many confirmation prompts) with translated keys. Summary of effects: - Replaces native window.confirm with awaitable in-app ConfirmDialog dialogs. - Centralizes confirmation UI and content construction in App.svelte. - Adds i18n plumbing and updates UI text to use t(). - Removes two specialized confirm dialog components and adds src/lib/components/ConfirmDialog.svelte.
This commit is contained in:
+193
-33
@@ -7,6 +7,8 @@
|
||||
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||
import { AlertCircle, Cherry, CloudOff, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
||||
import { beginFrontendShutdown, resumeFrontend } from "./lib/telemetry";
|
||||
import { setLanguage, t } from "./lib/i18n.svelte";
|
||||
import type { ConfirmRequest } from "./lib/components/ConfirmDialog.svelte";
|
||||
|
||||
import TitleBar from "./lib/TitleBar.svelte";
|
||||
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
||||
@@ -22,17 +24,16 @@
|
||||
import AiCommitSplitDialog from "./lib/components/AiCommitSplitDialog.svelte";
|
||||
import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte";
|
||||
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
||||
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
||||
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
||||
import TagsPanel from "./lib/components/TagsPanel.svelte";
|
||||
import WorktreePanel from "./lib/components/WorktreePanel.svelte";
|
||||
import CloneRepositoryDialog from "./lib/components/CloneRepositoryDialog.svelte";
|
||||
import ConfirmDialog from "./lib/components/ConfirmDialog.svelte";
|
||||
import CommandPalette from "./lib/components/CommandPalette.svelte";
|
||||
import CommitNoteDialog from "./lib/components/CommitNoteDialog.svelte";
|
||||
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
||||
import CompareSelectDialog from "./lib/components/CompareSelectDialog.svelte";
|
||||
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
|
||||
import DiscardConfirmDialog from "./lib/components/DiscardConfirmDialog.svelte";
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||
import InitRepositoryDialog from "./lib/components/InitRepositoryDialog.svelte";
|
||||
@@ -427,6 +428,8 @@
|
||||
let externalToolsDetectionUnavailable = false;
|
||||
let errorMessage = "";
|
||||
let operation = "";
|
||||
let confirmDialogRequest: ConfirmRequest | null = null;
|
||||
let confirmDialogResolve: ((confirmed: boolean) => void) | null = null;
|
||||
let compareFrom = "";
|
||||
let compareTo = "";
|
||||
let comparison: GitCommitComparison | null = null;
|
||||
@@ -1283,6 +1286,81 @@
|
||||
function applyLanguagePreference(next: AppLanguage) {
|
||||
document.documentElement.lang = next;
|
||||
document.documentElement.dataset.language = next;
|
||||
setLanguage(next);
|
||||
}
|
||||
|
||||
/** Show the confirmation dialog and resolve once the user answers. */
|
||||
function requestConfirmation(request: ConfirmRequest): Promise<boolean> {
|
||||
confirmDialogResolve?.(false);
|
||||
confirmDialogRequest = request;
|
||||
return new Promise<boolean>((resolve) => {
|
||||
confirmDialogResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/** Confirmation shown before deleting a local or remote branch. */
|
||||
function branchDeleteConfirmRequest(branch: GitBranchInfo, force: boolean): ConfirmRequest {
|
||||
const remoteName = branch.remote ? branch.name.split("/")[0] : "";
|
||||
return {
|
||||
eyebrow: branch.remote
|
||||
? t("confirm.branchDelete.eyebrowRemote")
|
||||
: force ? t("confirm.branchDelete.eyebrowForce") : t("confirm.branchDelete.eyebrow"),
|
||||
title: branch.remote
|
||||
? t("confirm.branchDelete.titleRemote")
|
||||
: force ? t("confirm.branchDelete.titleForce") : t("confirm.branchDelete.title"),
|
||||
message: branch.remote
|
||||
? t("confirm.branchDelete.messageRemote")
|
||||
: force ? t("confirm.branchDelete.messageForce") : t("confirm.branchDelete.message"),
|
||||
items: [branch.name],
|
||||
note: branch.remote
|
||||
? t("confirm.branchDelete.noteRemote", { remote: remoteName || t("common.remote") })
|
||||
: force ? t("confirm.branchDelete.noteForce") : t("confirm.branchDelete.note"),
|
||||
confirmLabel: branch.remote
|
||||
? t("confirm.branchDelete.actionRemote")
|
||||
: force ? t("confirm.branchDelete.actionForce") : t("confirm.branchDelete.action"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Confirmation shown before discarding working-tree changes. */
|
||||
function discardConfirmRequest(discard: PendingDiscard): ConfirmRequest {
|
||||
const files = discard.kind === "patch" ? [discard.file] : discard.files;
|
||||
const staged = discard.kind === "all-changes" ? null : discard.staged;
|
||||
const source = staged === null
|
||||
? t("confirm.discard.sourceBoth")
|
||||
: staged ? t("confirm.discard.sourceStaged") : t("confirm.discard.sourceUnstaged");
|
||||
const scope = discard.kind === "patch" ? discard.scope : "file";
|
||||
|
||||
const title = scope === "hunk"
|
||||
? t("confirm.discard.titleHunk")
|
||||
: scope === "lines"
|
||||
? t("confirm.discard.titleLines")
|
||||
: files.length > 1
|
||||
? t("confirm.discard.titleFiles", { count: files.length })
|
||||
: t("confirm.discard.titleFile");
|
||||
|
||||
const message = scope === "hunk"
|
||||
? t("confirm.discard.messageHunk", { source })
|
||||
: scope === "lines"
|
||||
? t("confirm.discard.messageLines", { source })
|
||||
: files.length > 1
|
||||
? t("confirm.discard.messageFiles", { source, count: files.length })
|
||||
: t("confirm.discard.messageFile", { source });
|
||||
|
||||
return {
|
||||
eyebrow: t("confirm.discard.eyebrow"),
|
||||
title,
|
||||
message,
|
||||
items: files.map((file) => (file.old_path ? `${file.old_path} -> ${file.path}` : file.path)),
|
||||
note: t("confirm.discard.note"),
|
||||
confirmLabel: t("confirm.discard.action"),
|
||||
};
|
||||
}
|
||||
|
||||
function answerConfirmation(confirmed: boolean) {
|
||||
const resolve = confirmDialogResolve;
|
||||
confirmDialogRequest = null;
|
||||
confirmDialogResolve = null;
|
||||
resolve?.(confirmed);
|
||||
}
|
||||
|
||||
function applyThemePreference(next: AppTheme) {
|
||||
@@ -3281,9 +3359,21 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = remoteFolder ? "remote" : "local";
|
||||
const currentNote = currentBranchKept ? "\n\nThe current branch will be kept." : "";
|
||||
if (!window.confirm(`Delete ${deletableBranches.length} ${scope} branches in “${folderName}”?${currentNote}`)) return;
|
||||
const notes = [
|
||||
currentBranchKept ? t("confirm.branchFolder.noteCurrent") : "",
|
||||
remoteFolder ? t("confirm.branchFolder.noteRemote") : "",
|
||||
].filter(Boolean);
|
||||
const folderConfirmed = await requestConfirmation({
|
||||
title: deletableBranches.length === 1
|
||||
? t("confirm.branchFolder.titleOne")
|
||||
: t("confirm.branchFolder.title", { count: deletableBranches.length }),
|
||||
message: deletableBranches.length === 1
|
||||
? t(remoteFolder ? "confirm.branchFolder.messageRemoteOne" : "confirm.branchFolder.messageLocalOne", { folder: folderName })
|
||||
: t(remoteFolder ? "confirm.branchFolder.messageRemote" : "confirm.branchFolder.messageLocal", { folder: folderName }),
|
||||
items: deletableBranches.map((branch) => branch.name),
|
||||
note: notes.join(" ") || undefined,
|
||||
});
|
||||
if (!folderConfirmed) return;
|
||||
|
||||
const repoPath = activeRepoPath;
|
||||
if (remoteFolder) {
|
||||
@@ -3828,7 +3918,11 @@
|
||||
|
||||
async function abortRebase() {
|
||||
if (!activeRepoPath || !rebaseInProgress) return;
|
||||
const confirmed = window.confirm("Abort the current rebase and return to the previous state?");
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.rebaseAbort.title"),
|
||||
message: t("confirm.rebaseAbort.message"),
|
||||
confirmLabel: t("confirm.rebaseAbort.action"),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
await runOperation("Aborting rebase", async () => {
|
||||
@@ -3995,7 +4089,11 @@
|
||||
|
||||
async function deleteLocalTag(tag: GitTag) {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
const confirmed = window.confirm(`Delete tag '${tag.name}'?\n\nThis only removes the local tag, not any copy already pushed to a remote.`);
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.tagDelete.title", { name: tag.name }),
|
||||
message: t("confirm.tagDelete.message"),
|
||||
note: t("confirm.tagDelete.note"),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
await runOperation(`Deleting tag ${tag.name}`, async () => {
|
||||
@@ -4188,7 +4286,11 @@
|
||||
|
||||
async function abortCherryPick() {
|
||||
if (!activeRepoPath || !cherryPickInProgress) return;
|
||||
const confirmed = window.confirm("Abort the current cherry-pick and return to the previous state?");
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.cherryPickAbort.title"),
|
||||
message: t("confirm.cherryPickAbort.message"),
|
||||
confirmLabel: t("confirm.cherryPickAbort.action"),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
await runOperation("Aborting cherry-pick", async () => {
|
||||
@@ -4366,9 +4468,13 @@
|
||||
}
|
||||
|
||||
errorMessage = "";
|
||||
const confirmed = window.confirm(appLanguage === "de"
|
||||
? "Das lokale und das entfernte Repository besitzen getrennte Commit-Historien.\n\nTrotzdem zusammenführen? Dabei können Merge-Konflikte entstehen."
|
||||
: "The local and remote repositories have separate commit histories.\n\nMerge them anyway? This may produce merge conflicts.");
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.unrelatedHistories.title"),
|
||||
message: t("confirm.unrelatedHistories.message"),
|
||||
note: t("confirm.unrelatedHistories.note"),
|
||||
confirmLabel: t("confirm.unrelatedHistories.action"),
|
||||
danger: false,
|
||||
});
|
||||
if (!confirmed) {
|
||||
errorMessage = appLanguage === "de"
|
||||
? "Pull abgebrochen: Die getrennten Historien wurden nicht verändert."
|
||||
@@ -4432,9 +4538,13 @@
|
||||
|
||||
if (errorMessage && !isAuthError(errorMessage) && isNonFastForwardPushError(errorMessage)) {
|
||||
errorMessage = "";
|
||||
const shouldSync = window.confirm(
|
||||
"The remote has newer commits, so the push was rejected.\n\nRun Pull/Merge now and try pushing again afterwards?",
|
||||
);
|
||||
const shouldSync = await requestConfirmation({
|
||||
title: t("confirm.pushRejected.title"),
|
||||
message: t("confirm.pushRejected.message"),
|
||||
note: t("confirm.pushRejected.note"),
|
||||
confirmLabel: t("confirm.pushRejected.action"),
|
||||
danger: false,
|
||||
});
|
||||
|
||||
if (!shouldSync) {
|
||||
const message = "Push rejected: the remote has newer commits. Pull first, then push again.";
|
||||
@@ -4620,7 +4730,15 @@
|
||||
}
|
||||
|
||||
async function revertHistoryCommit(commit: GitCommit) {
|
||||
if (!activeRepoPath || !window.confirm(`Revert commit ${commit.short_hash} (${commit.summary}) with a new commit?`)) return;
|
||||
if (!activeRepoPath) return;
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.revert.title", { hash: commit.short_hash }),
|
||||
message: t("confirm.revert.message"),
|
||||
items: [commit.summary],
|
||||
confirmLabel: t("confirm.revert.action"),
|
||||
danger: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
await runOperation(`Reverting ${commit.short_hash}`, async () => {
|
||||
applyStatus(await revertCommit(activeRepoPath, commit.hash));
|
||||
await refreshRepositoryViews(activeRepoPath, { branches: false });
|
||||
@@ -4636,7 +4754,13 @@
|
||||
}
|
||||
|
||||
async function abortMerge() {
|
||||
if (!activeRepoPath || !window.confirm("Abort the current merge and restore the pre-merge state?")) return;
|
||||
if (!activeRepoPath) return;
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.mergeAbort.title"),
|
||||
message: t("confirm.mergeAbort.message"),
|
||||
confirmLabel: t("confirm.mergeAbort.action"),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
await runOperation("Aborting merge", async () => {
|
||||
applyStatus(await mergeAbort(activeRepoPath));
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
@@ -4649,7 +4773,13 @@
|
||||
}
|
||||
|
||||
async function forcePushRepo() {
|
||||
if (!window.confirm("Push the current branch with --force-with-lease? This is intended for a branch whose history you rebased.")) return;
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.forcePush.title"),
|
||||
message: t("confirm.forcePush.message"),
|
||||
note: t("confirm.forcePush.note"),
|
||||
confirmLabel: t("confirm.forcePush.action"),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
remoteActionForceWithLease = true;
|
||||
await startRemoteAction("push");
|
||||
}
|
||||
@@ -4747,7 +4877,12 @@
|
||||
|
||||
async function dropStashEntry(stash: GitStash) {
|
||||
if (!activeRepoPath) return;
|
||||
const confirmed = window.confirm(`Delete ${stash.selector}?\n\n"${stash.message || stash.selector}"`);
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.stashDrop.title", { name: stash.selector }),
|
||||
message: t("confirm.stashDrop.message"),
|
||||
items: [stash.message || stash.selector],
|
||||
note: t("confirm.stashDrop.note"),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
await runOperation(`Dropping ${stash.selector}`, async () => {
|
||||
@@ -5095,9 +5230,13 @@
|
||||
|
||||
async function undoLastCommitChange() {
|
||||
if (!activeRepoPath || !canAmend || isBusy) return;
|
||||
const confirmed = window.confirm(
|
||||
"Undo the last commit?\n\nIts changes remain staged, ready to commit again. Your working tree files are preserved.",
|
||||
);
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.undoCommit.title"),
|
||||
message: t("confirm.undoCommit.message"),
|
||||
note: t("confirm.undoCommit.note"),
|
||||
confirmLabel: t("confirm.undoCommit.action"),
|
||||
danger: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
await runOperation("Undoing last commit", async () => {
|
||||
@@ -5116,7 +5255,13 @@
|
||||
|
||||
async function restoreCommit(target: GitCommit) {
|
||||
if (!activeRepoPath) return;
|
||||
const confirmed = window.confirm(`Restore working tree to ${target.short_hash}?\n\nThis brings back the files from that commit as unstaged changes so you can review and commit them. No commit is removed and the branch stays where it is.`);
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.restoreTree.title", { hash: target.short_hash }),
|
||||
message: t("confirm.restoreTree.message"),
|
||||
note: t("confirm.restoreTree.note"),
|
||||
confirmLabel: t("confirm.restoreTree.action"),
|
||||
danger: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
await runOperation(`Restoring ${target.short_hash}`, async () => {
|
||||
applyStatus(await restoreToCommit(activeRepoPath, target.hash));
|
||||
@@ -5127,7 +5272,12 @@
|
||||
|
||||
async function restoreCommitFile(target: GitCommit, file: GitCommitFile): Promise<boolean> {
|
||||
if (!activeRepoPath) return false;
|
||||
const confirmed = window.confirm(`Restore ${file.path} from ${target.short_hash}?\n\nThis changes the file in your working tree so you can review and commit it.`);
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t("confirm.restoreFile.title", { path: file.path, hash: target.short_hash }),
|
||||
message: t("confirm.restoreFile.message"),
|
||||
confirmLabel: t("confirm.restoreTree.action"),
|
||||
danger: false,
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
await runOperation(`Restoring ${file.path}`, async () => {
|
||||
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, file.path));
|
||||
@@ -5406,7 +5556,12 @@
|
||||
async function restoreSelectedFileFromCommit(target: GitCommit) {
|
||||
if (!activeRepoPath || !selectedExplorerPath) return;
|
||||
const kind = selectedExplorerKind === "folder" ? "folder" : "file";
|
||||
const confirmed = window.confirm(`Restore ${kind} ${selectedExplorerPath} from ${target.short_hash}?\n\nThis changes the selected ${kind} in your working tree so you can review and commit it.`);
|
||||
const confirmed = await requestConfirmation({
|
||||
title: t(kind === "folder" ? "confirm.restoreFile.titleFolder" : "confirm.restoreFile.title", { path: selectedExplorerPath, hash: target.short_hash }),
|
||||
message: t("confirm.restoreFile.message"),
|
||||
confirmLabel: t("confirm.restoreTree.action"),
|
||||
danger: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
await runOperation(`Restoring ${selectedExplorerPath}`, async () => {
|
||||
applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, selectedExplorerPath));
|
||||
@@ -6481,14 +6636,20 @@
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if confirmDialogRequest}
|
||||
<ConfirmDialog
|
||||
request={confirmDialogRequest}
|
||||
onConfirm={() => answerConfirmation(true)}
|
||||
onCancel={() => answerConfirmation(false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if pendingDiscard}
|
||||
<DiscardConfirmDialog
|
||||
files={pendingDiscard.kind === "patch" ? [pendingDiscard.file] : pendingDiscard.files}
|
||||
staged={pendingDiscard.kind === "all-changes" ? null : pendingDiscard.staged}
|
||||
scope={pendingDiscard.kind === "patch" ? pendingDiscard.scope : "file"}
|
||||
<ConfirmDialog
|
||||
request={discardConfirmRequest(pendingDiscard)}
|
||||
{isBusy}
|
||||
onConfirm={confirmDiscard}
|
||||
onClose={closeDiscardConfirm}
|
||||
onCancel={closeDiscardConfirm}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -6623,12 +6784,11 @@
|
||||
|
||||
<!-- Confirm deletion of a local or remote branch from the shared branch context menu -->
|
||||
{#if deleteBranchTarget}
|
||||
<BranchDeleteConfirmDialog
|
||||
branch={deleteBranchTarget}
|
||||
force={deleteBranchForce}
|
||||
<ConfirmDialog
|
||||
request={branchDeleteConfirmRequest(deleteBranchTarget, deleteBranchForce)}
|
||||
{isBusy}
|
||||
onConfirm={confirmDeleteBranch}
|
||||
onClose={closeDeleteBranchDialog}
|
||||
onCancel={closeDeleteBranchDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user