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