({ onClose, onCreated }: { onClose: () => void; onCreated: () => void })
| 167 | } |
| 168 | |
| 169 | function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreated: () => void }) { |
| 170 | const t = useUiMessages(); |
| 171 | const [currentPath, setCurrentPath] = useState(""); |
| 172 | const [dirs, setDirs] = useState<string[]>([]); |
| 173 | const [roots, setRoots] = useState<string[]>(["/"]); |
| 174 | const [parentPath, setParentPath] = useState(""); |
| 175 | const [projectName, setProjectName] = useState(""); |
| 176 | const [filter, setFilter] = useState(""); |
| 177 | const [activeIndex, setActiveIndex] = useState(0); |
| 178 | const [loading, setLoading] = useState(false); |
| 179 | const [submitting, setSubmitting] = useState(false); |
| 180 | const [error, setError] = useState<string | null>(null); |
| 181 | const filterRef = useRef<HTMLInputElement>(null); |
| 182 | /* Path whose listing is currently shown. Lets the typed-path effect skip a |
| 183 | * redundant re-fetch after browse() sets currentPath itself. */ |
| 184 | const lastBrowsedRef = useRef<string>(""); |
| 185 | |
| 186 | const browse = useCallback(async (path?: string, opts?: { silent?: boolean }) => { |
| 187 | const silent = opts?.silent ?? false; |
| 188 | if (!silent) setLoading(true); |
| 189 | setError(null); |
| 190 | try { |
| 191 | const q = path ? `?path=${encodeURIComponent(path)}` : ""; |
| 192 | const res = await fetch(`/api/browse${q}`); |
| 193 | const data = await res.json(); |
| 194 | if (data.error) throw new Error(data.error); |
| 195 | lastBrowsedRef.current = data.path ?? ""; |
| 196 | setCurrentPath(data.path ?? ""); |
| 197 | setDirs((data.dirs ?? []).sort()); |
| 198 | setRoots(data.roots ?? ["/"]); |
| 199 | setParentPath(data.parent ?? "/"); |
| 200 | } catch (e) { |
| 201 | /* Silent (typed-path) refreshes keep the last good listing instead of |
| 202 | * flashing an error while the user is still typing a path. */ |
| 203 | if (!silent) setError(e instanceof Error ? e.message : "Browse failed"); |
| 204 | } |
| 205 | finally { if (!silent) setLoading(false); } |
| 206 | }, []); |
| 207 | |
| 208 | useEffect(() => { browse(); }, [browse]); |
| 209 | useEffect(() => { filterRef.current?.focus(); }, []); |
| 210 | |
| 211 | /* Windows only: when the user types a drive path into the Repository path |
| 212 | * field, refresh the folder listing to match (debounced). On Windows, typing |
| 213 | * is the way to switch drives, and without this the breadcrumb and path box |
| 214 | * updated but the directory list stayed stale (e.g. typing "D:/" still showed |
| 215 | * the previous drive's folders). POSIX navigation is left unchanged. */ |
| 216 | useEffect(() => { |
| 217 | if (!currentPath || currentPath === lastBrowsedRef.current) return; |
| 218 | if (!/^[A-Za-z]:/.test(currentPath.replace(/\\/g, "/"))) return; |
| 219 | const id = setTimeout(() => { void browse(currentPath, { silent: true }); }, 350); |
| 220 | return () => clearTimeout(id); |
| 221 | }, [currentPath, browse]); |
| 222 | |
| 223 | const filteredDirs = useMemo(() => { |
| 224 | const q = filter.trim().toLowerCase(); |
| 225 | if (!q) return dirs; |
| 226 | return dirs.filter((d) => d.toLowerCase().includes(q)); |
nothing calls this directly
no test coverage detected