Files
coco-app/src/components/Common/DropdownList.tsx
SteveLauC 45ffe4cad8 refactor: refactoring Coco App (#112)
* feat: impl Coco server related APIs

* chore: remove unused method

* fix: invoke Rust interfaces in tauri::run()

* chore: add invoke

* feat: add add_coco_server

* fix: trim the tailing forward slash

* feat: interface get_user_profiles

* chore: add

* fix: store the servers in add interface

* chore: ass

* fix: skip non-publich servers with no token

* feat: add

* feat: get datasources and connectors

* fix: invoke interfaces in tauri::run()

* chore: add SidebarRef

* refactor: refactoring coco-app

* refactor: refactoring coco app

* refactor: refactoring project layout

* refactor: refactoring server management

* chore: cleanup code

* chore: display error when connect failed

* refactor: refactoring refresh server's info

* refactor: refactoring how to connect the coco serverg

* chore: rename to cloud

* refactor: refactoring remove coco server

* fix: refresh current selected server

* fix: reset server selection

* chore: update login status

* feat: add error message tips

* fix: fix login and logout

* refactor: refactoring http client

* fix: fix the datasources

* chore: minor fix

* refactor: refactoring code

* fix: fix search api

* chore: optimize part of icons

* chore: fix build

* refactor: search list icon

* refactor: search list icon

* chore: lib

* feat: add plugin-os

---------

Co-authored-by: rain <15911122312@163.com>
Co-authored-by: medcl <m@medcl.net>
2025-02-06 11:45:37 +08:00

162 lines
4.9 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-shell";
interface DropdownListProps {
selected: (item: any) => void;
suggests: any[];
isSearchComplete: boolean;
}
function DropdownList({ selected, suggests }: DropdownListProps) {
const [selectedItem, setSelectedItem] = useState<number | null>(null);
const [showIndex, setShowIndex] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const handleOpenURL = async (url: string) => {
if (!url) return;
try {
if (isTauri()) {
await open(url);
// console.log("URL opened in default browser");
}
} catch (error) {
console.error("Failed to open URL:", error);
}
};
const handleKeyDown = (e: KeyboardEvent) => {
// console.log(
// "handleKeyDown",
// e.key,
// showIndex,
// e.key >= "0" && e.key <= "9" && showIndex
// );
if (!suggests.length) return;
if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedItem((prev) =>
prev === null || prev === 0 ? suggests.length - 1 : prev - 1
);
} else if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedItem((prev) =>
prev === null || prev === suggests.length - 1 ? 0 : prev + 1
);
} else if (e.key === "Meta") {
e.preventDefault();
setShowIndex(true);
}
if (e.key === "Enter" && selectedItem !== null) {
// console.log("Enter key pressed", selectedItem);
const item = suggests[selectedItem];
if (item?.url) {
handleOpenURL(item?.url);
} else {
selected(item);
}
}
if (e.key >= "0" && e.key <= "9" && showIndex) {
// console.log(`number ${e.key}`);
const item = suggests[parseInt(e.key, 10)];
if (item?.url) {
handleOpenURL(item?.url);
} else {
selected(item);
}
}
};
const handleKeyUp = (e: KeyboardEvent) => {
// console.log("handleKeyUp", e.key);
if (!suggests.length) return;
if (!e.metaKey) {
setShowIndex(false);
}
};
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [showIndex, selectedItem, suggests]);
// useEffect(() => {
// if (suggests.length > 0) {
// setSelectedItem(0);
// }
// }, [JSON.stringify(suggests)]);
useEffect(() => {
if (selectedItem !== null && itemRefs.current[selectedItem]) {
itemRefs.current[selectedItem]?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}
}, [selectedItem]);
return (
<div
ref={containerRef}
data-tauri-drag-region
className="max-h-[458px] w-full p-2 flex flex-col rounded-xl overflow-y-auto overflow-hidden custom-scrollbar focus:outline-none"
tabIndex={0}
>
<div className="p-2 text-xs text-[#999] dark:text-[#666]">Results</div>
{suggests?.map((item, index) => {
const isSelected = selectedItem === index;
return (
<div
key={item._id}
ref={(el) => (itemRefs.current[index] = el)}
onMouseEnter={() => setSelectedItem(index)}
onClick={() => {
if (item?.url) {
handleOpenURL(item?.url);
} else {
selected(item);
}
}}
className={`w-full px-2 py-2.5 text-sm flex items-center justify-between rounded-lg transition-colors ${
isSelected
? "bg-[rgba(0,0,0,0.1)] dark:bg-[rgba(255,255,255,0.1)] hover:bg-[rgba(0,0,0,0.1)] dark:hover:bg-[rgba(255,255,255,0.1)]"
: ""
}`}
>
<div className="flex gap-2 items-center">
<img className="w-5 h-5" src={item?.icon} alt="icon" />
<span className="text-[#333] dark:text-[#d8d8d8] truncate w-80 text-left">
{item?.title}
</span>
</div>
<div className="flex gap-2 items-center relative">
<span className="text-sm text-[#666] dark:text-[#666] truncate w-52 text-right">
{item?.source}
</span>
{showIndex && index < 10 ? (
<div
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] shadow-[-6px_0px_6px_2px_#e6e6e6] dark:shadow-[-6px_0px_6px_2px_#000] rounded-md`}
>
{index}
</div>
) : null}
</div>
</div>
);
})}
</div>
);
}
export default DropdownList;