diff --git a/app/components/Input.tsx b/app/components/Input.tsx index c494e33c..252c06d8 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -3,27 +3,25 @@ import { getCategoryColorName, getCategoryColorValue, Graph } from "./model" import { useEffect, useRef, useState } from "react" import { PathNode } from "../page" import { cn } from "@/lib/utils" -import twcolors from 'tailwindcss/colors' - -let colors = twcolors as any +import { prepareArg } from "../utils" interface Props extends React.InputHTMLAttributes { value?: string graph: Graph onValueChange: (node: PathNode) => void - handelSubmit?: (node: any) => void + handleSubmit?: (node: any) => void icon?: React.ReactNode node?: PathNode parentClassName?: string scrollToBottom?: () => void } -export default function Input({ value, onValueChange, handelSubmit, graph, icon, node, className, parentClassName, scrollToBottom, ...props }: Props) { +export default function Input({ value, onValueChange, handleSubmit, graph, icon, node, className, parentClassName, scrollToBottom, ...props }: Props) { const [open, setOpen] = useState(false) const [options, setOptions] = useState([]) const [selectedOption, setSelectedOption] = useState(0) - const inputRef = useRef(null) + const [inputHeight, setInputHeight] = useState(0) const containerRef = useRef(null) useEffect(() => { @@ -35,6 +33,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, }, [open]) useEffect(() => { + let isLastRequest = true const timeout = setTimeout(async () => { if (!value || node?.id) { @@ -45,7 +44,7 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, return } - const result = await fetch(`/api/repo/${graph.Id}/?prefix=${value}`, { + const result = await fetch(`/api/repo/${prepareArg(graph.Id)}/?prefix=${prepareArg(value)}`, { method: 'POST' }) @@ -58,6 +57,8 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, return } + if (!isLastRequest) return + const json = await result.json() const { completions } = json.result @@ -70,17 +71,21 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, } }, 500) - return () => clearTimeout(timeout) + return () => { + clearTimeout(timeout) + isLastRequest = false + } }, [value]) - const handelKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + const container = containerRef.current switch (e.code) { case "Enter": { e.preventDefault() const option = options.find((o, i) => i === selectedOption) if (!option) return - if (handelSubmit) { - handelSubmit(option) + if (handleSubmit) { + handleSubmit(option) } else { if (!open) return onValueChange({ name: option.properties.name, id: option.id }) @@ -91,16 +96,22 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, case "ArrowUp": { e.preventDefault() setSelectedOption(prev => { - containerRef.current?.scrollTo({ behavior: 'smooth', top: (prev <= 0 ? options.length - 1 : prev - 1) * containerRef.current.children[0].clientHeight }) - return prev <= 0 ? options.length - 1 : prev - 1 + const newIndex = prev <= 0 ? options.length - 1 : prev - 1 + if (container) { + container.scrollTo({ behavior: 'smooth', top: 64 * newIndex }) + } + return newIndex }) return } case "ArrowDown": { e.preventDefault() setSelectedOption(prev => { - containerRef.current?.scrollTo({ behavior: 'smooth', top: ((prev + 1) % options.length) * containerRef.current.children[0].clientHeight }) - return (prev + 1) % options.length + const newIndex = (prev + 1) % options.length + if (container) { + container.scrollTo({ behavior: 'smooth', top: Math.min(64 * newIndex, container.scrollHeight) }) + } + return newIndex }) return } @@ -124,8 +135,12 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, data-name='search-bar' > { + if (ref) { + setInputHeight(ref.scrollHeight) + } + }} + onKeyDown={handleKeyDown} className={cn("w-full border p-2 rounded-md pointer-events-auto", className)} value={value || ""} onChange={(e) => { @@ -146,16 +161,25 @@ export default function Input({ value, onValueChange, handelSubmit, graph, icon, className="z-10 w-full bg-white absolute flex flex-col pointer-events-auto border rounded-md max-h-[50dvh] overflow-y-auto overflow-x-hidden p-2 gap-2" data-name='search-bar-list' style={{ - top: (inputRef.current?.clientHeight || 0) + 16 + top: inputHeight + 16 }} > { + options.length > 0 && options.map((option, index) => { const label = option.labels[0] const name = option.properties.name const path = option.properties.path - const colorName = getCategoryColorName(graph.CategoriesMap.get(label)!.index) - const color = getCategoryColorValue(graph.CategoriesMap.get(label)!.index) + let category = graph.CategoriesMap.get(label) + + if (!category) { + category = { name: label, index: graph.CategoriesMap.size, show: true } + graph.CategoriesMap.set(label, category) + graph.Categories.push(category) + } + + const colorName = getCategoryColorName(category.index) + const color = getCategoryColorValue(category.index) return (
diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index 225e7a8b..7034a6a7 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -15,6 +15,7 @@ import { Path, PathNode } from '../page'; import Input from './Input'; // import CommitList from './commitList'; import { Checkbox } from '@/components/ui/checkbox'; +import { prepareArg } from '../utils'; interface Props { onFetchGraph: (graphName: string) => void, @@ -156,7 +157,7 @@ export function CodeGraph({ if (!isSelectedObj && selectedObj && selectedObjects.length === 0) return if (event.key === 'Delete') { - handelRemove(selectedObjects.length > 0 ? selectedObjects.map(obj => Number(obj.id)) : [Number(isSelectedObj || selectedObj?.id)]); + handleRemove(selectedObjects.length > 0 ? selectedObjects.map(obj => Number(obj.id)) : [Number(isSelectedObj || selectedObj?.id)]); } }; @@ -168,7 +169,7 @@ export function CodeGraph({ }, [selectedObjects, selectedObj, isSelectedObj]); async function fetchCount() { - const result = await fetch(`/api/repo/${graphName}/info`, { + const result = await fetch(`/api/repo/${prepareArg(graphName)}/info`, { method: 'GET' }) @@ -190,7 +191,7 @@ export function CodeGraph({ useEffect(() => { if (!selectedValue) return - handelSelectedValue(selectedValue) + handleSelectedValue(selectedValue) }, [selectedValue]) useEffect(() => { @@ -199,7 +200,7 @@ export function CodeGraph({ const run = async () => { fetchCount() /* - const result = await fetch(`/api/repo/${graphName}/commit`, { + const result = await fetch(`/api/repo/${prepareArg(graphName)}/commit`, { method: 'GET' }) @@ -226,7 +227,7 @@ export function CodeGraph({ run() }, [graphName]) - function handelSelectedValue(value: string) { + function handleSelectedValue(value: string) { setGraphName(value) onFetchGraph(value) } @@ -252,31 +253,32 @@ export function CodeGraph({ } const deleteNeighbors = (nodes: Node[], chart: cytoscape.Core) => { + if (nodes.length === 0) return + const neighbors = chart.elements(nodes.map(node => `#${node.id}`).join(',')).outgoers() + neighbors.forEach((n) => { const id = n.id() - const index = graph.Elements.findIndex(e => e.data.id === id); - const element = graph.Elements[index] - - if (index === -1 || !element.data.collapsed) return + graph.Elements.forEach((e, i) => { + if (e.data.id === id) { + const type = "category" in e.data - const type = "category" in element.data - - if (element.data.expand) { - deleteNeighbors([element.data] as Node[], chart) - } + if (e.data.expand) { + deleteNeighbors([e.data] as Node[], chart) + } - graph.Elements.splice(index, 1); + graph.Elements.splice(i, 1); - if (type) { - graph.NodesMap.delete(Number(id)) - } else { - graph.EdgesMap.delete(Number(id.split('_')[1])) - } + if (type) { + graph.NodesMap.delete(Number(id)) + } else { + graph.EdgesMap.delete(Number(id.split('_')[1])) + } - chart.remove(`#${id}`) + chart.remove(`#${id}`) + } + }); }) - } const handleDoubleTap = async (evt?: EventObject) => { @@ -344,8 +346,6 @@ export function CodeGraph({ } else { const deleteNodes = graph.Elements.filter(n => n.data.expand === true && nodes.some(e => e.id === n.data.id)) - debugger - if (deleteNodes.length > 0) { deleteNeighbors(deleteNodes.map(n => n.data) as Node[], chart); chart.elements().layout(LAYOUT).run(); @@ -363,7 +363,7 @@ export function CodeGraph({ setSelectedObj(undefined) } - const handelSearchSubmit = (node: any) => { + const handleSearchSubmit = (node: any) => { const chart = chartRef.current if (!chart) return @@ -385,7 +385,7 @@ export function CodeGraph({ setSearchNode(n) } - const handelRemove = (ids: number[]) => { + const handleRemove = (ids: number[]) => { chartRef.current?.elements(`#${ids.join(',#')}`).style({ display: 'none' }) if (ids.some(id => Number(selectedObj?.id) === id)) { setSelectedObj(undefined) @@ -398,7 +398,7 @@ export function CodeGraph({
@@ -413,7 +413,7 @@ export function CodeGraph({ value={searchNode.name} onValueChange={({ name }) => setSearchNode({ name })} icon={} - handelSubmit={handelSearchSubmit} + handleSubmit={handleSearchSubmit} node={searchNode} /> @@ -476,10 +476,10 @@ export function CodeGraph({ setPath(path) setSelectedObj(undefined) }} - handelRemove={handelRemove} + handleRemove={handleRemove} position={position} url={url} - handelExpand={(nodes, expand) => { + handleExpand={(nodes, expand) => { handleExpand(nodes, expand) }} parentWidth={containerWidth} diff --git a/app/components/commitList.tsx b/app/components/commitList.tsx index 7af83bfc..a3a69e46 100644 --- a/app/components/commitList.tsx +++ b/app/components/commitList.tsx @@ -23,12 +23,12 @@ // const [commitChanges, setCommitChanges] = useState() -// const handelCommitChange = async (commit: any) => { +// const handleCommitChange = async (commit: any) => { // const chart = chartRef.current // if (!chart) return -// const result = await fetch(`api/repo/${graph.Id}/?type=switchCommit`, { +// const result = await fetch(`api/repo/${prepareArg(graph.Id)}/commit`, { // method: 'POST', // }) @@ -109,7 +109,7 @@ // > // @@ -90,7 +90,7 @@ export default function ElementMenu({ obj, objects, setPath, handelRemove, posit @@ -114,13 +114,13 @@ export default function ElementMenu({ obj, objects, setPath, handelRemove, posit diff --git a/app/page.tsx b/app/page.tsx index 8d73dada..4cf6fc23 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -13,6 +13,7 @@ import Image from 'next/image'; import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Progress } from '@/components/ui/progress'; +import { prepareArg } from './utils'; export type PathNode = { id?: number @@ -100,7 +101,7 @@ export default function Home() { return } - const result = await fetch(`/api/repo/?url=${createURL}`, { + const result = await fetch(`/api/repo/?url=${prepareArg(createURL)}`, { method: 'POST', }) @@ -131,7 +132,7 @@ export default function Home() { setGraph(Graph.empty()) - const result = await fetch(`/api/repo/${graphName}`, { + const result = await fetch(`/api/repo/${prepareArg(graphName)}`, { method: 'GET' }) @@ -151,7 +152,7 @@ export default function Home() { // Send the user query to the server to expand a node async function onFetchNode(nodeIds: string[]) { - const result = await fetch(`/api/repo/${graph.Id}/neighbors`, { + const result = await fetch(`/api/repo/${prepareArg(graph.Id)}/neighbors`, { method: 'POST', body: JSON.stringify({ nodeIds }), headers: { diff --git a/app/utils.ts b/app/utils.ts new file mode 100644 index 00000000..b40cb0f2 --- /dev/null +++ b/app/utils.ts @@ -0,0 +1,3 @@ +export function prepareArg(arg: string) { + return(encodeURIComponent(arg.trim())); +} \ No newline at end of file diff --git a/e2e/config/testData.ts b/e2e/config/testData.ts index 83d3f8d0..2e00ab9c 100644 --- a/e2e/config/testData.ts +++ b/e2e/config/testData.ts @@ -10,6 +10,6 @@ const categorizeCharacters = (characters: string[], expectedRes: boolean): { cha }; export const specialCharacters: { character: string; expectedRes: boolean }[] = [ - ...categorizeCharacters([ '#', '%', '&', '*', '(', ')', '-', '[', ']', '{', '}', ';', ':', '"', '|', '~'], true), - ...categorizeCharacters(['!', '@', '$', '^', '_', '=', '+', "'", ',', '.', '<', '>', '/', '?', '\\', '`'], false) + ...categorizeCharacters(['%', '*', '(', ')', '-', '[', ']', '{', '}', ';', ':', '"', '|', '~'], true), + ...categorizeCharacters(['!', '@', '$', '^', '_', '=', '+', "'", ',', '.', '<', '>', '/', '?', '\\', '`', '&', '#'], false) ];