Add confirmation dialog for discarding changes.

This commit is contained in:
System233 2025-02-19 02:15:14 +08:00
parent 2036e84d9d
commit 1fcdd33339
4 changed files with 112 additions and 19 deletions

View file

@ -299,9 +299,6 @@ const ApiConfigManager = ({
aria-labelledby="new-profile-title">
<DialogContent className="p-4 max-w-sm">
<DialogTitle>New Configuration Profile</DialogTitle>
<button className="absolute right-4 top-4" aria-label="Close dialog" onClick={resetCreateState}>
<span className="codicon codicon-close" />
</button>
<VSCodeTextField
ref={newProfileInputRef}
value={newProfileName}

View file

@ -5,11 +5,12 @@ import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import ApiOptions from "./ApiOptions"
import ExperimentalFeature from "./ExperimentalFeature"
import { EXPERIMENT_IDS, experimentConfigsMap } from "../../../../src/shared/experiments"
import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
import ApiConfigManager from "./ApiConfigManager"
import { Dropdown } from "vscrui"
import type { DropdownOption } from "vscrui"
import { ApiConfiguration } from "../../../../src/shared/api"
import ConfirmDialog from "../ui/comfirm-dialog"
type SettingsViewProps = {
onDone: () => void
@ -21,6 +22,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const [commandInput, setCommandInput] = useState("")
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
// TODO: Reduce WebviewMessage/ExtensionState complexity
const [cachedState, setCachedState] = useState(extensionState)
@ -67,7 +69,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
}, [currentApiConfigName, extensionState, isChangeDetected])
const setCachedStateField = useCallback(
<K extends keyof ExtensionStateContextType>(field: K, value: ExtensionStateContextType[K]) =>
<K extends keyof ExtensionStateContextType>(field: K, value: ExtensionStateContextType[K]) => {
setCachedState((prevState) => {
if (prevState[field] === value) {
return prevState
@ -77,7 +79,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
...prevState,
[field]: value,
}
}),
})
},
[],
)
@ -91,20 +94,27 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
return {
...prevState,
apiConfiguration: {
...apiConfiguration,
...prevState.apiConfiguration,
[field]: value,
},
}
})
},
[apiConfiguration],
[],
)
const setExperimentEnabled = useCallback(
(id: string, enabled: boolean) =>
setCachedStateField("experiments", { ...cachedState.experiments, [id]: enabled }),
[cachedState.experiments, setCachedStateField],
)
const setExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => {
setCachedState((prevState) => {
if (prevState.experiments?.[id] === enabled) {
return prevState
}
setChangeDetected(true)
return {
...prevState,
experiments: { ...prevState.experiments, [id]: enabled },
}
})
}, [])
const handleSubmit = () => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
@ -171,6 +181,24 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setModelIdErrorMessage(modelIdValidationResult)
}, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
const confirmDialogHandler = useRef<() => void>()
const onConfirmDialogResult = useCallback((confirm: boolean) => {
if (confirm) {
confirmDialogHandler.current?.()
}
}, [])
const checkUnsaveChanges = useCallback(
(then: () => void) => {
if (isChangeDetected) {
confirmDialogHandler.current = then
setDiscardDialogShow(true)
} else {
then()
}
},
[isChangeDetected],
)
const handleResetState = () => {
vscode.postMessage({ type: "resetState" })
}
@ -215,6 +243,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
flexDirection: "column",
overflow: "hidden",
}}>
<ConfirmDialog
icon="codicon-warning"
title="Unsaved changes"
message="Do you want to discard changes and continue?"
show={isDiscardDialogShow}
onResult={onConfirmDialogResult}
onClose={() => setDiscardDialogShow(false)}
aria-labelledby="unsave-warning-dialog"></ConfirmDialog>
<div
style={{
display: "flex",
@ -240,7 +276,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<VSCodeButton
appearance="secondary"
title="Discard unsaved changes and close settings panel"
onClick={onDone}>
onClick={() => checkUnsaveChanges(onDone)}>
Done
</VSCodeButton>
</div>
@ -254,9 +290,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
currentApiConfigName={currentApiConfigName}
listApiConfigMeta={extensionState.listApiConfigMeta}
onSelectConfig={(configName: string) => {
vscode.postMessage({
type: "loadApiConfiguration",
text: configName,
checkUnsaveChanges(() => {
vscode.postMessage({
type: "loadApiConfiguration",
text: configName,
})
})
}}
onDeleteConfig={(configName: string) => {

View file

@ -0,0 +1,58 @@
import { Dialog, DialogContent, DialogTitle } from "./dialog"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCallback } from "react"
export interface ConfirmDialogProps {
show: boolean
icon: string
title?: string
message: string
onResult: (confirm: boolean) => void
onClose: () => void
}
export const ConfirmDialog = ({ onResult, onClose, icon, show, title, message }: ConfirmDialogProps) => {
const onCloseConfirmDialog = useCallback(
(confirm: boolean) => {
onResult(confirm)
onClose()
},
[onClose, onResult],
)
return (
<Dialog
open={show}
onOpenChange={(open) => {
!open && onCloseConfirmDialog(false)
}}
aria-labelledby="unsave-warning-dialog">
<DialogContent className="p-4 max-w-sm">
<DialogTitle>{title}</DialogTitle>
<p className="text-lg mt-2" data-testid="error-message">
<span
style={{ fontSize: "2em" }}
className={`codicon align-middle mr-1 ${icon || "codicon-warning"}`}
/>
<span>{message}</span>
</p>
<div className="flex justify-end gap-2 mt-4">
<VSCodeButton
appearance="primary"
onClick={() => {
onCloseConfirmDialog(true)
}}>
Yes
</VSCodeButton>
<VSCodeButton
appearance="secondary"
onClick={() => {
onCloseConfirmDialog(false)
}}>
No
</VSCodeButton>
</div>
</DialogContent>
</Dialog>
)
}
export default ConfirmDialog

View file

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
)}
{...props}>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="cursor-pointer absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>