From a24169e9b889d71aaab1915519e747692e4b7f4d Mon Sep 17 00:00:00 2001 From: Bram Suurd <78373894+BramSuurdje@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:45:24 +0200 Subject: [PATCH] Fix navigation (#7376) * Removed double entries from the search to improve navigation * change input on search field to improve searchability * added type to search to make sure that LXC and VM's dont get mixed up * run linting over changes --------- Co-authored-by: Bram Suurd --- frontend/src/components/command-menu.tsx | 102 +++++++++++++++++++---- 1 file changed, 86 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/command-menu.tsx b/frontend/src/components/command-menu.tsx index bd3945848f..5753d0a387 100644 --- a/frontend/src/components/command-menu.tsx +++ b/frontend/src/components/command-menu.tsx @@ -36,19 +36,24 @@ export function formattedBadge(type: string) { return null; } -// random Script -function getRandomScript(categories: Category[]): Script | null { +function getRandomScript(categories: Category[], previouslySelected: Set = new Set()): Script | null { const allScripts = categories.flatMap(cat => cat.scripts || []); if (allScripts.length === 0) return null; - const idx = Math.floor(Math.random() * allScripts.length); - return allScripts[idx]; + + const availableScripts = allScripts.filter(script => !previouslySelected.has(script.slug)); + if (availableScripts.length === 0) { + return allScripts[Math.floor(Math.random() * allScripts.length)]; + } + const idx = Math.floor(Math.random() * availableScripts.length); + return availableScripts[idx]; } -export default function CommandMenu() { +function CommandMenu() { const [open, setOpen] = React.useState(false); const [links, setLinks] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); + const [selectedScripts, setSelectedScripts] = React.useState>(new Set()); const router = useRouter(); const fetchSortedCategories = () => { @@ -65,25 +70,26 @@ export default function CommandMenu() { }; React.useEffect(() => { - const down = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); fetchSortedCategories(); setOpen(open => !open); } }; - document.addEventListener("keydown", down); - return () => document.removeEventListener("keydown", down); + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); }, []); - const openRandomScript = async () => { + const handleOpenRandomScript = async () => { if (links.length === 0) { setIsLoading(true); try { const categories = await fetchCategories(); setLinks(categories); - const randomScript = getRandomScript(categories); + const randomScript = getRandomScript(categories, selectedScripts); if (randomScript) { + setSelectedScripts(prev => new Set([...prev, randomScript.slug])); router.push(`/scripts?id=${randomScript.slug}`); } } @@ -92,13 +98,54 @@ export default function CommandMenu() { } } else { - const randomScript = getRandomScript(links); + const randomScript = getRandomScript(links, selectedScripts); if (randomScript) { + setSelectedScripts(prev => new Set([...prev, randomScript.slug])); router.push(`/scripts?id=${randomScript.slug}`); } } }; + const getUniqueScriptsMap = React.useCallback(() => { + const scriptMap = new Map(); + for (const category of links) { + for (const script of category.scripts) { + if (!scriptMap.has(script.slug)) { + scriptMap.set(script.slug, { script, categoryName: category.name }); + } + } + } + return scriptMap; + }, [links]); + + const getUniqueScriptsByCategory = React.useCallback(() => { + const scriptMap = getUniqueScriptsMap(); + const categoryOrder = links.map(cat => cat.name); + const grouped: Record = {}; + + for (const name of categoryOrder) { + grouped[name] = []; + } + + for (const { script, categoryName } of scriptMap.values()) { + if (grouped[categoryName]) { + grouped[categoryName].push(script); + } + else { + grouped[categoryName] = [script]; + } + } + + Object.keys(grouped).forEach((cat) => { + if (grouped[cat].length === 0) + delete grouped[cat]; + }); + + return grouped; + }, [getUniqueScriptsMap, links]); + + const uniqueScriptsByCategory = getUniqueScriptsByCategory(); + return ( <>
@@ -122,7 +169,20 @@ export default function CommandMenu() { - @@ -139,16 +199,24 @@ export default function CommandMenu() { {isLoading ? "Loading..." : "No scripts found."} - {links.map(category => ( - - {category.scripts.map(script => ( + {Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => ( + + {scripts.map(script => ( { setOpen(false); router.push(`/scripts?id=${script.slug}`); }} + tabIndex={0} + aria-label={`Open script ${script.name}`} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setOpen(false); + router.push(`/scripts?id=${script.slug}`); + } + }} >
setOpen(false)}> ); } + +export default CommandMenu;