[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
104
components/editor/UnsavedChangesDialog.tsx
Normal file
104
components/editor/UnsavedChangesDialog.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useI18n } from "../../application/i18n/I18nProvider";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
|
||||
export type UnsavedChoice = "save" | "discard" | "cancel";
|
||||
|
||||
interface Pending {
|
||||
fileName: string;
|
||||
resolve: (choice: UnsavedChoice) => void;
|
||||
}
|
||||
|
||||
interface UnsavedChangesAPI {
|
||||
prompt: (fileName: string) => Promise<UnsavedChoice>;
|
||||
}
|
||||
|
||||
export const UnsavedChangesProvider: React.FC<{
|
||||
children: (api: UnsavedChangesAPI) => React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
const { t } = useI18n();
|
||||
const [pending, setPending] = useState<Pending | null>(null);
|
||||
const pendingRef = useRef<Pending | null>(null);
|
||||
pendingRef.current = pending;
|
||||
|
||||
const prompt = useCallback(
|
||||
(fileName: string) =>
|
||||
new Promise<UnsavedChoice>((resolve) => {
|
||||
// Re-entrance: if a prior prompt is still pending, cancel it so its caller
|
||||
// doesn't hang forever waiting for a resolve that now belongs to a new prompt.
|
||||
const prior = pendingRef.current;
|
||||
if (prior) prior.resolve("cancel");
|
||||
setPending({ fileName, resolve });
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Keep the singleton current during render so AppView close handlers and
|
||||
// hotkeys never race the post-commit useEffect registration window.
|
||||
promptSingleton = prompt;
|
||||
useEffect(() => () => {
|
||||
promptSingleton = null;
|
||||
}, []);
|
||||
|
||||
// On unmount, resolve any in-flight prompt as "cancel" so awaiting callers don't leak.
|
||||
useEffect(() => () => {
|
||||
const prior = pendingRef.current;
|
||||
if (prior) {
|
||||
prior.resolve("cancel");
|
||||
pendingRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resolveWith = useCallback((choice: UnsavedChoice) => {
|
||||
if (!pending) return;
|
||||
pending.resolve(choice);
|
||||
setPending(null);
|
||||
}, [pending]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children({ prompt })}
|
||||
<Dialog open={!!pending} onOpenChange={(o) => { if (!o) resolveWith("cancel"); }}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("sftp.editor.unsavedTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("sftp.editor.unsavedMessage", { fileName: pending?.fileName ?? "" })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="ghost" onClick={() => resolveWith("cancel")}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => resolveWith("discard")}>
|
||||
{t("sftp.editor.discardChanges")}
|
||||
</Button>
|
||||
<Button variant="default" onClick={() => resolveWith("save")}>
|
||||
{t("sftp.editor.saveAndClose")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module-level singleton — lets non-React code call the dialog without
|
||||
// prop-drilling. Registered/unregistered by UnsavedChangesProvider above.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let promptSingleton: ((fileName: string) => Promise<UnsavedChoice>) | null = null;
|
||||
|
||||
export const promptUnsavedChanges = (fileName: string): Promise<UnsavedChoice> => {
|
||||
if (!promptSingleton) return Promise.resolve("cancel");
|
||||
return promptSingleton(fileName);
|
||||
};
|
||||
Reference in New Issue
Block a user