Redesign the frontend with a dark-tech theme: add Tailwind + PostCSS, design tokens, and shadcn-style primitives (Button/Card/Input/Dialog/ DropdownMenu/Tabs/ScrollArea/etc.); restyle the app shell, navigation, sidebar tree, content view, markdown rendering, editors, modals and settings panels. Behavior/props unchanged; Font Awesome replaced with lucide-react. Add the patch cards feature UI: patch-queries hooks and a PatchCards component rendered below the markdown body, with an Add Patch button and create/edit dialog. Fix tree expandability: folders with an index page now expand on name click (and navigate), and the chevron+folder icon is one larger toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
7.0 KiB
JavaScript
181 lines
7.0 KiB
JavaScript
import React, {useEffect, useState, useRef, useContext} from "react";
|
|
import {useCreatePath, usePaths} from "../utils/queries/path-queries";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { Plus } from "lucide-react";
|
|
import "./PathManager.css";
|
|
import {fetch_} from "../utils/request-utils";
|
|
import {ConfigContext} from "../ConfigProvider";
|
|
import { Input } from "./ui/input";
|
|
import { Button } from "./ui/button";
|
|
import { Spinner } from "./ui/misc";
|
|
|
|
|
|
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|
const [currentFullPath, setCurrentFullPath] = useState([{ name: "Root", id: 1 }]);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [dropdownActive, setDropdownActive] = useState(false);
|
|
|
|
const inputRef = useRef();
|
|
|
|
const queryClient = useQueryClient();
|
|
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
|
|
const createPath = useCreatePath();
|
|
const config = useContext(ConfigContext).config;
|
|
|
|
|
|
const buildFullPath = async (pathId) => {
|
|
const path = [];
|
|
let current_id = pathId;
|
|
while (current_id) {
|
|
try {
|
|
const pathData = await queryClient.fetchQuery({
|
|
queryKey: ["path", current_id],
|
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/path/${current_id}`)
|
|
});
|
|
if (!pathData) break;
|
|
path.unshift({ name: pathData.name, id: pathData.id });
|
|
current_id = pathData.parent_id;
|
|
} catch (error) {
|
|
console.error(`Failed to fetch path with id ${current_id}:`, error);
|
|
break;
|
|
}
|
|
}
|
|
return path;
|
|
};
|
|
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
const path = await buildFullPath(currentPathId);
|
|
setCurrentFullPath(path);
|
|
};
|
|
init();
|
|
}, [currentPathId, queryClient]);
|
|
|
|
const handlePathClick = (pathId, pathIndex) => {
|
|
const newPath = currentFullPath.slice(0, pathIndex + 1);
|
|
setCurrentFullPath(newPath);
|
|
onPathChange(pathId);
|
|
};
|
|
|
|
const handleSubPathSelect = (subPath) => {
|
|
const updatedPath = [...currentFullPath, { name: subPath.name, id: subPath.id }];
|
|
setCurrentFullPath(updatedPath);
|
|
onPathChange(subPath.id);
|
|
setSearchTerm("");
|
|
setDropdownActive(false);
|
|
};
|
|
|
|
const handleAddDirectory = () => {
|
|
if (!searchTerm.trim()) {
|
|
alert("Directory name cannot be empty.");
|
|
return;
|
|
}
|
|
|
|
createPath.mutate(
|
|
{ name: searchTerm.trim(), parent_id: currentPathId },
|
|
{
|
|
onSuccess: (newDir) => {
|
|
queryClient.setQueryData({queryKey: ["path", newDir.id]}, newDir);
|
|
queryClient.invalidateQueries({queryKey: ["paths", currentPathId]});
|
|
setSearchTerm("");
|
|
alert("Directory created successfully.");
|
|
},
|
|
onError: (error) => {
|
|
console.error("Failed to create directory:", error);
|
|
alert("Failed to create directory.");
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
const handleInputFocus = () => setDropdownActive(true);
|
|
|
|
const handleInputBlur = () => {
|
|
setTimeout(() => setDropdownActive(false), 150);
|
|
};
|
|
|
|
const filteredSubPaths = subPaths
|
|
? subPaths.filter((path) =>
|
|
path.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
: [];
|
|
|
|
const handleKeyDown = (e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
handleAddDirectory();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="path-manager space-y-3">
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
{currentFullPath.map((path, index) => (
|
|
<button
|
|
type="button"
|
|
key={path.id}
|
|
className="rounded-md border border-border bg-surface/60 px-2 py-1 font-mono text-xs text-primary transition-colors hover:border-primary/60"
|
|
onClick={() => handlePathClick(path.id, index)}
|
|
>
|
|
{path.name + "/"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="path-manager-body">
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative flex-1">
|
|
<Input
|
|
ref={inputRef}
|
|
className="h-8 text-xs"
|
|
type="text"
|
|
placeholder="Search or create directory"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
onFocus={handleInputFocus}
|
|
onBlur={handleInputBlur}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
{dropdownActive && (
|
|
<div className="path-manager-dropdown absolute left-0 top-full z-20 mt-1 w-full overflow-y-auto rounded-md border border-border bg-card shadow-glow">
|
|
{filteredSubPaths.length > 0 ? (
|
|
filteredSubPaths.map((subPath) => (
|
|
<button
|
|
type="button"
|
|
key={subPath.id}
|
|
className="block w-full px-3 py-2 text-left text-xs text-foreground transition-colors hover:bg-accent"
|
|
onClick={() => handleSubPathSelect(subPath)}
|
|
>
|
|
{subPath.name}
|
|
</button>
|
|
))
|
|
) : (
|
|
<div className="px-3 py-2 text-xs text-muted-foreground">
|
|
No matches found
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
onClick={handleAddDirectory}
|
|
disabled={isSubPathsLoading || !searchTerm.trim()}
|
|
>
|
|
<Plus className="h-4 w-4" /> Create "{searchTerm}"
|
|
</Button>
|
|
</div>
|
|
{isSubPathsLoading && <Spinner label="Loading paths" />}
|
|
{subPathsError && (
|
|
<p className="font-mono text-xs text-destructive">
|
|
Error loading subdirectories.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PathManager;
|