fix: stabilize workflow settings editing
This commit is contained in:
parent
28a2531082
commit
b438977a68
@ -113,6 +113,14 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -160,6 +168,7 @@ const WORKFLOW_STEP_DRAG_DATA_TYPE = "application/x-agent-workflow-step";
|
||||
const WORKFLOW_SETTINGS_DRAFT_STORAGE_KEY = "agent-workflow-settings-draft:v1";
|
||||
const WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY = "agent-workflow-settings-stale-draft:v1";
|
||||
const WORKFLOW_SETTINGS_DRAFT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const DELETE_WORKFLOW_STEP_PASSWORD = "123456";
|
||||
|
||||
type WorkflowStepDropPosition = "before" | "after" | "inside";
|
||||
|
||||
@ -258,15 +267,26 @@ function scheduleWorkflowIdleTask(callback: () => void, timeoutMs = 2000) {
|
||||
function BufferedInput({
|
||||
commitDelayMs = 450,
|
||||
onBlur,
|
||||
onCompositionEnd,
|
||||
onCompositionStart,
|
||||
onFocus,
|
||||
onValueCommit,
|
||||
value,
|
||||
...props
|
||||
}: BufferedInputProps) {
|
||||
const [draftValue, setDraftValue] = useState(value);
|
||||
const committedValueRef = useRef(value);
|
||||
const draftValueRef = useRef(value);
|
||||
const isFocusedRef = useRef(false);
|
||||
const isComposingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusedRef.current && draftValueRef.current !== committedValueRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDraftValue(value);
|
||||
draftValueRef.current = value;
|
||||
committedValueRef.current = value;
|
||||
}, [value]);
|
||||
|
||||
@ -287,7 +307,7 @@ function BufferedInput({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (draftValue === committedValueRef.current) {
|
||||
if (draftValue === committedValueRef.current || isComposingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -302,9 +322,30 @@ function BufferedInput({
|
||||
<Input
|
||||
{...props}
|
||||
value={draftValue}
|
||||
onChange={(event) => setDraftValue(event.target.value)}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
draftValueRef.current = nextValue;
|
||||
setDraftValue(nextValue);
|
||||
}}
|
||||
onFocus={(event) => {
|
||||
isFocusedRef.current = true;
|
||||
onFocus?.(event);
|
||||
}}
|
||||
onCompositionStart={(event) => {
|
||||
isComposingRef.current = true;
|
||||
onCompositionStart?.(event);
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
isComposingRef.current = false;
|
||||
const nextValue = event.currentTarget.value;
|
||||
draftValueRef.current = nextValue;
|
||||
setDraftValue(nextValue);
|
||||
commitValue(nextValue);
|
||||
onCompositionEnd?.(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
commitValue(draftValue, { immediate: true });
|
||||
isFocusedRef.current = false;
|
||||
commitValue(draftValueRef.current, { immediate: true });
|
||||
onBlur?.(event);
|
||||
}}
|
||||
/>
|
||||
@ -314,15 +355,26 @@ function BufferedInput({
|
||||
function BufferedTextarea({
|
||||
commitDelayMs = 450,
|
||||
onBlur,
|
||||
onCompositionEnd,
|
||||
onCompositionStart,
|
||||
onFocus,
|
||||
onValueCommit,
|
||||
value,
|
||||
...props
|
||||
}: BufferedTextareaProps) {
|
||||
const [draftValue, setDraftValue] = useState(value);
|
||||
const committedValueRef = useRef(value);
|
||||
const draftValueRef = useRef(value);
|
||||
const isFocusedRef = useRef(false);
|
||||
const isComposingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocusedRef.current && draftValueRef.current !== committedValueRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDraftValue(value);
|
||||
draftValueRef.current = value;
|
||||
committedValueRef.current = value;
|
||||
}, [value]);
|
||||
|
||||
@ -343,7 +395,7 @@ function BufferedTextarea({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (draftValue === committedValueRef.current) {
|
||||
if (draftValue === committedValueRef.current || isComposingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -358,9 +410,30 @@ function BufferedTextarea({
|
||||
<Textarea
|
||||
{...props}
|
||||
value={draftValue}
|
||||
onChange={(event) => setDraftValue(event.target.value)}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
draftValueRef.current = nextValue;
|
||||
setDraftValue(nextValue);
|
||||
}}
|
||||
onFocus={(event) => {
|
||||
isFocusedRef.current = true;
|
||||
onFocus?.(event);
|
||||
}}
|
||||
onCompositionStart={(event) => {
|
||||
isComposingRef.current = true;
|
||||
onCompositionStart?.(event);
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
isComposingRef.current = false;
|
||||
const nextValue = event.currentTarget.value;
|
||||
draftValueRef.current = nextValue;
|
||||
setDraftValue(nextValue);
|
||||
commitValue(nextValue);
|
||||
onCompositionEnd?.(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
commitValue(draftValue, { immediate: true });
|
||||
isFocusedRef.current = false;
|
||||
commitValue(draftValueRef.current, { immediate: true });
|
||||
onBlur?.(event);
|
||||
}}
|
||||
/>
|
||||
@ -1820,6 +1893,9 @@ export function AgentWorkflowSettingsClient({
|
||||
const [copiedWorkflowStep, setCopiedWorkflowStep] = useState<AgentWorkflowStepUiConfig | null>(
|
||||
null
|
||||
);
|
||||
const [pendingDeleteStepKey, setPendingDeleteStepKey] = useState<string | null>(null);
|
||||
const [deleteStepPassword, setDeleteStepPassword] = useState("");
|
||||
const [deleteStepDialogOpen, setDeleteStepDialogOpen] = useState(false);
|
||||
const [autoDraftUpdatedAt, setAutoDraftUpdatedAt] = useState<number | null>(null);
|
||||
const [autoDraftStatus, setAutoDraftStatus] = useState<"idle" | "restored" | "saved">("idle");
|
||||
const [recoverableDraftUpdatedAt, setRecoverableDraftUpdatedAt] = useState<number | null>(null);
|
||||
@ -2115,6 +2191,13 @@ export function AgentWorkflowSettingsClient({
|
||||
() => new Map(workflowStepAnalyses.map((item) => [item.key, item] as const)),
|
||||
[workflowStepAnalyses]
|
||||
);
|
||||
const pendingDeleteStep = useMemo(
|
||||
() =>
|
||||
pendingDeleteStepKey
|
||||
? findAgentWorkflowStepByKey(config.steps, pendingDeleteStepKey)
|
||||
: null,
|
||||
[config.steps, pendingDeleteStepKey]
|
||||
);
|
||||
const workflowSpecialModuleOptions = useMemo(
|
||||
() =>
|
||||
getAgentWorkflowSpecialModuleRootOptions(deferredWorkflowSteps || []).map((option) => ({
|
||||
@ -3043,6 +3126,12 @@ export function AgentWorkflowSettingsClient({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestRemoveStep = (stepKey: string) => {
|
||||
setPendingDeleteStepKey(stepKey);
|
||||
setDeleteStepPassword("");
|
||||
setDeleteStepDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleRemoveStep = (stepKey: string) => {
|
||||
const targetStep = findAgentWorkflowStepByKey(config.steps, stepKey);
|
||||
setConfig((prev) =>
|
||||
@ -3060,6 +3149,25 @@ export function AgentWorkflowSettingsClient({
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirmRemoveStep = () => {
|
||||
if (deleteStepPassword.trim() !== DELETE_WORKFLOW_STEP_PASSWORD) {
|
||||
toast.error("删除密码错误,请输入正确密码");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingDeleteStepKey) {
|
||||
toast.error("没有找到要删除的模块");
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteStepTitle = pendingDeleteStep?.title || pendingDeleteStepKey;
|
||||
handleRemoveStep(pendingDeleteStepKey);
|
||||
setDeleteStepDialogOpen(false);
|
||||
setPendingDeleteStepKey(null);
|
||||
setDeleteStepPassword("");
|
||||
toast.success(`已删除模块「${deleteStepTitle}」`);
|
||||
};
|
||||
|
||||
const canMoveWorkflowStep = (
|
||||
sourceStepKey: string | null,
|
||||
targetStepKey: string | null,
|
||||
@ -5070,7 +5178,7 @@ export function AgentWorkflowSettingsClient({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive"
|
||||
onClick={() => handleRemoveStep(step.key)}
|
||||
onClick={() => handleRequestRemoveStep(step.key)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@ -6947,6 +7055,70 @@ export function AgentWorkflowSettingsClient({
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
|
||||
<Dialog
|
||||
open={deleteStepDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDeleteStepDialogOpen(open);
|
||||
if (!open) {
|
||||
setPendingDeleteStepKey(null);
|
||||
setDeleteStepPassword("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[460px]">
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
handleConfirmRemoveStep();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认删除模块</DialogTitle>
|
||||
<DialogDescription>
|
||||
删除模块会同时移除该模块下的卡片和子模块。请输入删除密码后继续。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="workflow-delete-step-password">删除模块</Label>
|
||||
<div className="rounded-md border bg-muted/30 px-3 py-2 text-sm">
|
||||
{pendingDeleteStep?.title || pendingDeleteStepKey || "未选择模块"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="workflow-delete-step-password">删除密码</Label>
|
||||
<Input
|
||||
id="workflow-delete-step-password"
|
||||
type="password"
|
||||
value={deleteStepPassword}
|
||||
onChange={(event) => setDeleteStepPassword(event.target.value)}
|
||||
placeholder="请输入删除密码"
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当前固定删除密码为 123456,输错不会删除任何数据。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setDeleteStepDialogOpen(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" variant="destructive">
|
||||
确认删除
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user