import { InputHTMLAttributes, useEffect, useState } from "react"; import { faFolder } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; import { twMerge } from "tailwind-merge"; import { useProject } from "@app/context"; import { useDebounce } from "@app/hooks"; import { useGetFoldersByEnv } from "@app/hooks/api"; import { Input } from "../Input"; type Props = Omit, "size" | "onChange"> & { value?: string | null; isImport?: boolean; isVisible?: boolean; isReadOnly?: boolean; isDisabled?: boolean; environment?: string; containerClassName?: string; onChange?: (arg: string) => void; }; export const SecretPathInput = ({ containerClassName, onChange, environment, value: propValue, ...props }: Props) => { const [inputValue, setInputValue] = useState(propValue ?? ""); const [secretPath, setSecretPath] = useState("/"); const [suggestions, setSuggestions] = useState([]); const [isInputFocused, setIsInputFocus] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const [debouncedInputValue] = useDebounce(inputValue, 200); const { currentProject } = useProject(); const projectId = currentProject?.id || ""; const { folderNames: folders } = useGetFoldersByEnv({ path: secretPath, environments: [environment || currentProject?.environments?.[0].slug || ""], projectId }); useEffect(() => { setInputValue(propValue ?? "/"); }, [propValue]); useEffect(() => { if ( (debouncedInputValue.length > 0 && debouncedInputValue[debouncedInputValue.length - 1] === "/") || debouncedInputValue.length === 0 ) { setSecretPath(debouncedInputValue); } }, [debouncedInputValue]); useEffect(() => { const searchFragment = debouncedInputValue.split("/").pop() || ""; const filteredSuggestions = folders .filter((suggestionEntry) => suggestionEntry.toUpperCase().startsWith(searchFragment.toUpperCase()) ) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); setSuggestions(filteredSuggestions); }, [debouncedInputValue, folders]); const handleSuggestionSelect = (selectedIndex: number) => { if (!suggestions[selectedIndex]) { return; } const validPaths = inputValue.split("/"); validPaths.pop(); const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`; onChange?.(newValue); setInputValue(newValue); setSecretPath(newValue); setHighlightedIndex(-1); setSuggestions([]); }; const handleKeyDown = (e: React.KeyboardEvent) => { const mod = (n: number, m: number) => ((n % m) + m) % m; if (e.key === "ArrowDown") { setHighlightedIndex((prevIndex) => mod(prevIndex + 1, suggestions.length)); } else if (e.key === "ArrowUp") { setHighlightedIndex((prevIndex) => mod(prevIndex - 1, suggestions.length)); } else if (e.key === "Enter" && highlightedIndex >= 0) { handleSuggestionSelect(highlightedIndex); } if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) { e.preventDefault(); } }; const handleInputChange = (e: any) => { if (onChange) { onChange(e.target.value); } setInputValue(e.target.value); }; return ( 0 && isInputFocused} onOpenChange={() => { setHighlightedIndex(-1); }} > setIsInputFocus(true)} onBlur={() => setIsInputFocus(false)} value={inputValue} onChange={handleInputChange} className={containerClassName} /> e.preventDefault()} className={twMerge( "relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md" )} style={{ width: "var(--radix-popover-trigger-width)", maxHeight: "var(--radix-select-content-available-height)" }} >
{suggestions.map((suggestion, i) => (
{ e.preventDefault(); setHighlightedIndex(i); handleSuggestionSelect(i); }} style={{ pointerEvents: "auto" }} className="flex items-center justify-between border-mineshaft-600 text-left" key={`secret-reference-secret-${i + 1}`} >
{suggestion}
))}
); };