返回 DeepSeek-Reasonix
BlankProjectDialog.tsx
根目录 / desktop / frontend / src / components / BlankProjectDialog.tsx
1 import { ErrorMessage } from "./ErrorMessage";
2 import { useEffect, useId, useLayoutEffect, useRef, useState, type FormEvent } from "react";
3 import { createPortal } from "react-dom";
4 import { useT } from "../lib/i18n";
5
6 export function blankProjectNameProblem(name: string): "required" | "invalid" | null {
7 const trimmed = name.trim();
8 if (!trimmed) return "required";
9 if (trimmed === "." || trimmed === ".." || /[\\/]/.test(trimmed) || /[\u0000-\u001f\u007f]/.test(trimmed)) {
10 return "invalid";
11 }
12 return null;
13 }
14
15 export function BlankProjectDialog({
16 parentDirectory,
17 createdPath,
18 busy,
19 error,
20 onSubmit,
21 onCancel,
22 }: {
23 parentDirectory: string;
24 createdPath?: string;
25 busy: boolean;
26 error?: string;
27 onSubmit: (name: string) => void;
28 onCancel: () => void;
29 }) {
30 const t = useT();
31 const titleId = useId();
32 const errorId = useId();
33 const inputRef = useRef<HTMLInputElement>(null);
34 const dialogRef = useRef<HTMLDivElement>(null);
35 const restoreFocusRef = useRef<HTMLElement | null>(null);
36 const [name, setName] = useState("");
37 const problem = blankProjectNameProblem(name);
38 const visibleError = error || "";
39
40 useLayoutEffect(() => {
41 restoreFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
42 inputRef.current?.focus();
43 return () => {
44 if (restoreFocusRef.current?.isConnected) restoreFocusRef.current.focus();
45 };
46 }, []);
47
48 useEffect(() => {
49 const onKeyDown = (event: KeyboardEvent) => {
50 if (event.key === "Escape" && !busy) {
51 event.preventDefault();
52 event.stopPropagation();
53 onCancel();
54 return;
55 }
56 if (event.key !== "Tab") return;
57 const focusable = Array.from(dialogRef.current?.querySelectorAll<HTMLElement>(
58 'input:not(:disabled), button:not(:disabled)',
59 ) ?? []);
60 if (focusable.length === 0) return;
61 const first = focusable[0];
62 const last = focusable[focusable.length - 1];
63 if (event.shiftKey && document.activeElement === first) {
64 event.preventDefault();
65 last.focus();
66 } else if (!event.shiftKey && document.activeElement === last) {
67 event.preventDefault();
68 first.focus();
69 }
70 };
71 document.addEventListener("keydown", onKeyDown, { capture: true });
72 return () => document.removeEventListener("keydown", onKeyDown, { capture: true });
73 }, [busy, onCancel]);
74
75 const submit = (event: FormEvent) => {
76 event.preventDefault();
77 if (busy || (!createdPath && problem)) return;
78 onSubmit(name.trim());
79 };
80
81 return createPortal(
82 <div
83 data-app-overlay=""
84 className="modal-backdrop blank-project-backdrop"
85 role="presentation"
86 onMouseDown={(event) => {
87 if (!busy && event.target === event.currentTarget) onCancel();
88 }}
89 >
90 <div
91 ref={dialogRef}
92 className="modal blank-project-dialog"
93 role="dialog"
94 aria-modal="true"
95 aria-labelledby={titleId}
96 >
97 <form onSubmit={submit}>
98 <div className="modal__title" id={titleId}>{t("projectTree.createBlankProject")}</div>
99 <div className="blank-project-dialog__field">
100 <span className="blank-project-dialog__label">{t("memory.showStorage")}</span>
101 <code className="blank-project-dialog__path">{parentDirectory}</code>
102 </div>
103 <label className="blank-project-dialog__field">
104 <span className="blank-project-dialog__label">{t("caps.name")}</span>
105 <input
106 ref={inputRef}
107 className="mem-input blank-project-dialog__input"
108 type="text"
109 value={name}
110 disabled={busy || Boolean(createdPath)}
111 aria-invalid={Boolean(visibleError && !createdPath)}
112 aria-describedby={visibleError && !createdPath ? errorId : undefined}
113 autoComplete="off"
114 onChange={(event) => setName(event.target.value)}
115 />
116 </label>
117 {createdPath ? (
118 <p className="blank-project-dialog__created">
119 {t("memory.createdSkillSuggestion")}: {createdPath}
120 </p>
121 ) : null}
122 <div className="blank-project-dialog__error" id={errorId} role={visibleError ? "alert" : undefined}>
123 {visibleError ? <ErrorMessage error={visibleError} /> : "\u00a0"}
124 </div>
125 <div className="modal__actions blank-project-dialog__actions">
126 <button className="btn btn--small" type="button" disabled={busy} onClick={onCancel}>
127 {t("common.cancel")}
128 </button>
129 <button className="btn btn--small btn--primary" type="submit" disabled={busy || (!createdPath && Boolean(problem))}>
130 {busy ? t("common.loading") : createdPath ? t("common.retry") : t("common.confirm")}
131 </button>
132 </div>
133 </form>
134 </div>
135 </div>,
136 document.body,
137 );
138 }
139
139 lines Plain Text