Add handle on enter button;
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface ConfirmationOptions {
|
interface ConfirmationOptions {
|
||||||
@@ -9,10 +9,47 @@ interface ConfirmationOptions {
|
|||||||
variant?: "default" | "destructive";
|
variant?: "default" | "destructive";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ToastConfirmOptions {
|
||||||
|
confirmOnEnter?: boolean;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function useConfirmation() {
|
export function useConfirmation() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [options, setOptions] = useState<ConfirmationOptions | null>(null);
|
const [options, setOptions] = useState<ConfirmationOptions | null>(null);
|
||||||
const [onConfirm, setOnConfirm] = useState<(() => void) | null>(null);
|
const [onConfirm, setOnConfirm] = useState<(() => void) | null>(null);
|
||||||
|
const [activeToastId, setActiveToastId] = useState<string | number | null>(null);
|
||||||
|
const [pendingConfirmCallback, setPendingConfirmCallback] = useState<(() => void) | null>(null);
|
||||||
|
const [pendingResolve, setPendingResolve] = useState<((value: boolean) => void) | null>(null);
|
||||||
|
|
||||||
|
const handleEnterKey = useCallback((event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Enter" && activeToastId !== null) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (pendingConfirmCallback) {
|
||||||
|
pendingConfirmCallback();
|
||||||
|
}
|
||||||
|
if (pendingResolve) {
|
||||||
|
pendingResolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.dismiss(activeToastId);
|
||||||
|
setActiveToastId(null);
|
||||||
|
setPendingConfirmCallback(null);
|
||||||
|
setPendingResolve(null);
|
||||||
|
}
|
||||||
|
}, [activeToastId, pendingConfirmCallback, pendingResolve]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeToastId !== null) {
|
||||||
|
// Use capture phase to intercept Enter before terminal receives it
|
||||||
|
window.addEventListener("keydown", handleEnterKey, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleEnterKey, true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [activeToastId, handleEnterKey]);
|
||||||
|
|
||||||
const confirm = (opts: ConfirmationOptions, callback: () => void) => {
|
const confirm = (opts: ConfirmationOptions, callback: () => void) => {
|
||||||
setOptions(opts);
|
setOptions(opts);
|
||||||
@@ -40,6 +77,7 @@ export function useConfirmation() {
|
|||||||
callback?: () => void,
|
callback?: () => void,
|
||||||
variantOrConfirmLabel: "default" | "destructive" | string = "Confirm",
|
variantOrConfirmLabel: "default" | "destructive" | string = "Confirm",
|
||||||
cancelLabel: string = "Cancel",
|
cancelLabel: string = "Cancel",
|
||||||
|
toastOptions: ToastConfirmOptions = { confirmOnEnter: false },
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const isVariant =
|
const isVariant =
|
||||||
@@ -47,43 +85,56 @@ export function useConfirmation() {
|
|||||||
variantOrConfirmLabel === "destructive";
|
variantOrConfirmLabel === "destructive";
|
||||||
const confirmLabel = isVariant ? "Confirm" : variantOrConfirmLabel;
|
const confirmLabel = isVariant ? "Confirm" : variantOrConfirmLabel;
|
||||||
|
|
||||||
if (typeof opts === "string") {
|
const { confirmOnEnter = false, duration = 8000 } = toastOptions;
|
||||||
toast(opts, {
|
|
||||||
action: {
|
|
||||||
label: confirmLabel,
|
|
||||||
onClick: () => {
|
|
||||||
if (callback) callback();
|
|
||||||
resolve(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
label: cancelLabel,
|
|
||||||
onClick: () => {
|
|
||||||
resolve(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
} else if (typeof opts === "object") {
|
|
||||||
const actualConfirmLabel = opts.confirmText || confirmLabel;
|
|
||||||
const actualCancelLabel = opts.cancelText || cancelLabel;
|
|
||||||
|
|
||||||
toast(opts.description, {
|
const handleToastConfirm = () => {
|
||||||
action: {
|
|
||||||
label: actualConfirmLabel,
|
|
||||||
onClick: () => {
|
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
resolve(true);
|
resolve(true);
|
||||||
},
|
setActiveToastId(null);
|
||||||
|
setPendingConfirmCallback(null);
|
||||||
|
setPendingResolve(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToastCancel = () => {
|
||||||
|
resolve(false);
|
||||||
|
setActiveToastId(null);
|
||||||
|
setPendingConfirmCallback(null);
|
||||||
|
setPendingResolve(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const message = typeof opts === "string" ? opts : opts.description;
|
||||||
|
const actualConfirmLabel = typeof opts === "object" && opts.confirmText ? opts.confirmText : confirmLabel;
|
||||||
|
const actualCancelLabel = typeof opts === "object" && opts.cancelText ? opts.cancelText : cancelLabel;
|
||||||
|
|
||||||
|
const toastId = toast(message, {
|
||||||
|
duration,
|
||||||
|
action: {
|
||||||
|
label: confirmOnEnter ? `${actualConfirmLabel} ↵` : actualConfirmLabel,
|
||||||
|
onClick: handleToastConfirm,
|
||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
label: actualCancelLabel,
|
label: actualCancelLabel,
|
||||||
onClick: () => {
|
onClick: handleToastCancel,
|
||||||
resolve(false);
|
|
||||||
},
|
},
|
||||||
|
onDismiss: () => {
|
||||||
|
setActiveToastId(null);
|
||||||
|
setPendingConfirmCallback(null);
|
||||||
|
setPendingResolve(null);
|
||||||
|
},
|
||||||
|
onAutoClose: () => {
|
||||||
|
resolve(false);
|
||||||
|
setActiveToastId(null);
|
||||||
|
setPendingConfirmCallback(null);
|
||||||
|
setPendingResolve(null);
|
||||||
},
|
},
|
||||||
} as any);
|
} as any);
|
||||||
} else {
|
|
||||||
resolve(false);
|
if (confirmOnEnter) {
|
||||||
|
setActiveToastId(toastId);
|
||||||
|
setPendingConfirmCallback(() => () => {
|
||||||
|
if (callback) callback();
|
||||||
|
});
|
||||||
|
setPendingResolve(() => resolve);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -715,8 +715,11 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
: msg.data;
|
: msg.data;
|
||||||
|
|
||||||
terminal.write(outputData);
|
terminal.write(outputData);
|
||||||
|
// Universal regex pattern for sudo password prompt
|
||||||
|
// Matches "[sudo]" prefix + any text + ":" suffix, works for all languages
|
||||||
|
// Also matches "sudo: a password is required" for some systems
|
||||||
const sudoPasswordPattern =
|
const sudoPasswordPattern =
|
||||||
/(?:\[sudo\] password for \S+:|sudo: a password is required)/;
|
/(?:\[sudo\][^\n]*:\s*$|sudo:[^\n]*password[^\n]*required)/i;
|
||||||
const passwordToFill =
|
const passwordToFill =
|
||||||
hostConfig.terminalConfig?.sudoPassword || hostConfig.password;
|
hostConfig.terminalConfig?.sudoPassword || hostConfig.password;
|
||||||
if (
|
if (
|
||||||
@@ -746,6 +749,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
},
|
},
|
||||||
t("common.confirm"),
|
t("common.confirm"),
|
||||||
t("common.cancel"),
|
t("common.cancel"),
|
||||||
|
{ confirmOnEnter: true },
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sudoPromptShownRef.current = false;
|
sudoPromptShownRef.current = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user