| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- "use client";
- import { useQuery } from "@tanstack/react-query";
- import { useEffect, useRef, useState } from "react";
- import { get } from "../../lib/api";
- interface Directory {
- name: string;
- path: string;
- }
- interface PathAutocompleteProps {
- value: string;
- onChange: (value: string) => void;
- placeholder?: string;
- className?: string;
- defaultPath?: string;
- }
- export default function PathAutocomplete({
- value,
- onChange,
- placeholder = "/path/to/destination",
- className = "",
- defaultPath = "",
- }: PathAutocompleteProps) {
- const [showSuggestions, setShowSuggestions] = useState(false);
- const [selectedIndex, setSelectedIndex] = useState(-1);
- const inputRef = useRef<HTMLInputElement>(null);
- const suggestionsRef = useRef<HTMLDivElement>(null);
- // Get the parent directory to search for subdirectories
- const searchPath = value || defaultPath;
- const parentPath = searchPath.endsWith("/")
- ? searchPath.slice(0, -1)
- : searchPath;
- // Fetch directories for autocomplete
- const { data: dirData } = useQuery<{ directories: Directory[] }>({
- queryKey: ["directories", parentPath],
- queryFn: async () => {
- if (!parentPath) return { directories: [] };
- return get("/directories", { path: parentPath });
- },
- enabled: !!parentPath && showSuggestions,
- staleTime: 30000, // Cache for 30 seconds
- });
- const directories = dirData?.directories || [];
- // Close suggestions when clicking outside
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- suggestionsRef.current &&
- !suggestionsRef.current.contains(event.target as Node) &&
- !inputRef.current?.contains(event.target as Node)
- ) {
- setShowSuggestions(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const newValue = e.target.value;
- onChange(newValue);
- setShowSuggestions(true);
- setSelectedIndex(-1);
- };
- const handleSuggestionClick = (dirPath: string) => {
- onChange(dirPath);
- setShowSuggestions(false);
- setSelectedIndex(-1);
- };
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (!showSuggestions || directories.length === 0) {
- if (e.key === "ArrowDown") {
- e.preventDefault();
- setShowSuggestions(true);
- }
- return;
- }
- switch (e.key) {
- case "ArrowDown":
- e.preventDefault();
- setSelectedIndex((prev) =>
- prev < directories.length - 1 ? prev + 1 : prev
- );
- break;
- case "ArrowUp":
- e.preventDefault();
- setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
- break;
- case "Enter":
- e.preventDefault();
- if (selectedIndex >= 0 && selectedIndex < directories.length) {
- handleSuggestionClick(directories[selectedIndex].path);
- }
- break;
- case "Escape":
- setShowSuggestions(false);
- setSelectedIndex(-1);
- break;
- }
- };
- const handleFocus = () => {
- if (directories.length > 0) {
- setShowSuggestions(true);
- }
- };
- return (
- <div className="relative">
- <input
- ref={inputRef}
- type="text"
- value={value}
- onChange={handleInputChange}
- onKeyDown={handleKeyDown}
- onFocus={handleFocus}
- placeholder={placeholder}
- className={className}
- />
- {showSuggestions && directories.length > 0 && (
- <div
- ref={suggestionsRef}
- className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-60 overflow-y-auto"
- >
- {directories.map((dir, index) => (
- <button
- key={dir.path}
- type="button"
- onClick={() => handleSuggestionClick(dir.path)}
- className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700 ${
- index === selectedIndex ? "bg-blue-50 dark:bg-blue-900/20" : ""
- }`}
- >
- <div className="flex items-center">
- <svg
- className="h-4 w-4 mr-2 text-gray-400"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
- />
- </svg>
- <span className="text-gray-900 dark:text-gray-100">
- {dir.name}
- </span>
- </div>
- <div className="text-xs text-gray-500 dark:text-gray-400 ml-6 truncate">
- {dir.path}
- </div>
- </button>
- ))}
- </div>
- )}
- {defaultPath && value !== defaultPath && (
- <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
- Default: {defaultPath}
- </p>
- )}
- </div>
- );
- }
|