From 3bf1cea9ddeb895328ea622a8a9658acee687056 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 17 May 2024 12:55:24 -0500 Subject: [PATCH 1/2] Do custom sort & filter --- src/CommandMenu.tsx | 207 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 159 insertions(+), 48 deletions(-) diff --git a/src/CommandMenu.tsx b/src/CommandMenu.tsx index 5f74d66..67fde61 100644 --- a/src/CommandMenu.tsx +++ b/src/CommandMenu.tsx @@ -36,7 +36,7 @@ import Icon from "@mui/material/Icon"; import Typography from "@mui/material/Typography"; import {makeStyles} from "@mui/styles"; import {Command} from "cmdk"; -import React, {useContext, useEffect, useRef} from "react"; +import React, {useContext, useEffect, useRef, useState} from "react"; import {useNavigate} from "react-router-dom"; import QContext from "QContext"; import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils"; @@ -62,8 +62,13 @@ const useStyles = makeStyles((theme: any) => ({ } })); +const A_FIRST = -1; +const B_FIRST = 1; + const CommandMenu = ({metaData}: Props) => { + const [searchString, setSearchString] = useState(""); + const navigate = useNavigate(); const pathParts = location.pathname.replace(/\/+$/, "").split("/"); @@ -71,7 +76,7 @@ const CommandMenu = ({metaData}: Props) => const classes = useStyles(); - function evalueKeyPress(e: KeyboardEvent) + function evaluateKeyPress(e: KeyboardEvent) { /////////////////////////////////////////////////////////////////////////// // if a dot pressed, not from a "text" element, then toggle command menu // @@ -107,20 +112,20 @@ const CommandMenu = ({metaData}: Props) => const down = (e: KeyboardEvent) => { - evalueKeyPress(e); - } + evaluateKeyPress(e); + }; - document.addEventListener("keydown", down) + document.addEventListener("keydown", down); return () => { - document.removeEventListener("keydown", down) - } - }, [tableMetaData, dotMenuOpen, keyboardHelpOpen]) + document.removeEventListener("keydown", down); + }; + }, [tableMetaData, dotMenuOpen, keyboardHelpOpen]); useEffect(() => { setDotMenuOpen(false); - }, [location.pathname]) + }, [location.pathname]); function goToItem(path: string) { @@ -162,73 +167,113 @@ const CommandMenu = ({metaData}: Props) => return (null); } + + /******************************************************************************* + ** sort a section (e.g, tables, apps). + ** + ** put labels that start-with the search word first. + *******************************************************************************/ + function comparator(labelA: string, labelB: string) + { + if (searchString != "") + { + let aStartsWith = labelA.toLowerCase().startsWith(searchString.toLowerCase()); + let bStartsWith = labelB.toLowerCase().startsWith(searchString.toLowerCase()); + + if (aStartsWith && !bStartsWith) + { + return A_FIRST; + } + else if (bStartsWith && !aStartsWith) + { + return B_FIRST; + } + + aStartsWith = labelA.toLowerCase().startsWith(searchString.toLowerCase()); + bStartsWith = labelB.toLowerCase().startsWith(searchString.toLowerCase()); + + if (aStartsWith && !bStartsWith) + { + return A_FIRST; + } + else if (bStartsWith && !aStartsWith) + { + return B_FIRST; + } + } + + return (labelA.localeCompare(labelB)); + } + + /******************************************************************************* ** *******************************************************************************/ function ActionsSection() { - let tableNames : string[]= []; + let tableNames: string[] = []; metaData.tables.forEach((value: QTableMetaData, key: string) => { tableNames.push(value.name); - }) - tableNames = tableNames.sort((a: string, b:string) => + }); + tableNames = tableNames.sort((a: string, b: string) => { const labelA = metaData.tables.get(a).label ?? ""; const labelB = metaData.tables.get(b).label ?? ""; - return (labelA.localeCompare(labelB)); - }) + return comparator(labelA, labelB); + }); const path = location.pathname; - return tableMetaData && !path.endsWith("/edit") && !path.endsWith("/create") && !path.endsWith("#audit") && ! path.endsWith("copy") && + return tableMetaData && !path.endsWith("/edit") && !path.endsWith("/create") && !path.endsWith("#audit") && !path.endsWith("copy") && ( { tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && - goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New">addNew + goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New">addNew } { tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && - goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy">copyCopy + goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy">copyCopy } { tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission && - goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit">editEdit + goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit">editEdit } { metaData && metaData.tables.has("audit") && - goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit">checklistAudit + goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit">checklistAudit } { tableProcesses && tableProcesses.length > 0 && - ( - tableProcesses.map((process) => ( - goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}>{getIconName(process.iconName, "play_arrow")}{process.label} - )) - ) + ( + tableProcesses.map((process) => ( + goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}>{getIconName(process.iconName, "play_arrow")}{process.label} + )) + ) } ); } + /******************************************************************************* ** *******************************************************************************/ function TablesSection() { - let tableNames : string[]= []; + let tableNames: string[] = []; metaData.tables.forEach((value: QTableMetaData, key: string) => { tableNames.push(value.name); - }) - tableNames = tableNames.sort((a: string, b:string) => + }); + tableNames = tableNames.sort((a: string, b: string) => { const labelA = metaData.tables.get(a).label ?? ""; const labelB = metaData.tables.get(b).label ?? ""; - return (labelA.localeCompare(labelB)); - }) - return( + return comparator(labelA, labelB); + }); + return ( { tableNames.map((tableName: string, index: number) => @@ -243,6 +288,7 @@ const CommandMenu = ({metaData}: Props) => ); } + /******************************************************************************* ** *******************************************************************************/ @@ -252,16 +298,16 @@ const CommandMenu = ({metaData}: Props) => metaData.apps.forEach((value: QAppMetaData, key: string) => { appNames.push(value.name); - }) + }); - appNames = appNames.sort((a: string, b:string) => + appNames = appNames.sort((a: string, b: string) => { const labelA = getFullAppLabel(metaData.appTree, a, 1, "") ?? ""; const labelB = getFullAppLabel(metaData.appTree, b, 1, "") ?? ""; - return (labelA.localeCompare(labelB)); - }) + return comparator(labelA, labelB); + }); - return( + return ( { appNames.map((appName: string, index: number) => @@ -276,33 +322,37 @@ const CommandMenu = ({metaData}: Props) => ); } + + /******************************************************************************* + ** + *******************************************************************************/ function RecentlyViewedSection() { const history = HistoryUtils.get(); const options = [] as any; history.entries.reverse().forEach((entry, index) => options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName}) - ) + ); let appNames: string[] = []; metaData.apps.forEach((value: QAppMetaData, key: string) => { appNames.push(value.name); - }) + }); - appNames = appNames.sort((a: string, b:string) => + appNames = appNames.sort((a: string, b: string) => { const labelA = metaData.apps.get(a).label ?? ""; const labelB = metaData.apps.get(b).label ?? ""; - return (labelA.localeCompare(labelB)); - }) + return comparator(labelA, labelB); + }); const entryMap = new Map(); - return( + return ( { history.entries.reverse().map((entry: QHistoryEntry, index: number) => - ! entryMap.has(entry.label) && entryMap.set(entry.label, true) && ( + !entryMap.has(entry.label) && entryMap.set(entry.label, true) && ( goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}>{entry.iconName}{entry.label} ) ) @@ -311,29 +361,90 @@ const CommandMenu = ({metaData}: Props) => ); } - const containerElement = useRef(null) + const containerElement = useRef(null); + + /******************************************************************************* + ** + *******************************************************************************/ function closeKeyboardHelp() { setKeyboardHelpOpen(false); } + + /******************************************************************************* + ** + *******************************************************************************/ function closeDotMenu() { setDotMenuOpen(false); } + + /******************************************************************************* + ** filter function for cmd-k library + ** + *******************************************************************************/ + function doFilter(value: string, search: string) + { + setSearchString(search); + + ///////////////////// + // split on spaces // + ///////////////////// + const searchParts = search.toLowerCase().split(" "); + if (searchParts.length == 1) + { + ////////////////////////////////////////////// + // if only 1 word, just do an includes test // + ////////////////////////////////////////////// + return (value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0); + } + else + { + //////////////////////////////////////// + // else split the value on spaces too // + //////////////////////////////////////// + const valueParts = value.toLowerCase().split(" "); + if (searchParts.length > valueParts.length) + { + ////////////////////////////////////////////////////////////////////////////////// + // if there are more words in the search than in the value, then it can't match // + // e.g. "order c" can't ever match, say "order" // + ////////////////////////////////////////////////////////////////////////////////// + return (0); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // iterate over the search parts - if any don't match the corresponding value parts, then it's a non-match // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + for (let i = 0; i < searchParts.length; i++) + { + if (!valueParts[i].includes(searchParts[i])) + { + return (0); + } + } + + ///////////////////////////////// + // if no failure, return a hit // + ///////////////////////////////// + return (1); + } + } + return ( { - + doFilter(value, search)}> - + - + No results found. @@ -381,6 +492,6 @@ const CommandMenu = ({metaData}: Props) => } - ) -} + ); +}; export default CommandMenu; From 7b562aea508e8b388e3ef4e65d73557a88ba998a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 17 May 2024 17:11:35 -0500 Subject: [PATCH 2/2] Slightly better sort for multi-word search terms --- src/CommandMenu.tsx | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/CommandMenu.tsx b/src/CommandMenu.tsx index 67fde61..b59e95e 100644 --- a/src/CommandMenu.tsx +++ b/src/CommandMenu.tsx @@ -189,16 +189,20 @@ const CommandMenu = ({metaData}: Props) => return B_FIRST; } - aStartsWith = labelA.toLowerCase().startsWith(searchString.toLowerCase()); - bStartsWith = labelB.toLowerCase().startsWith(searchString.toLowerCase()); + const indexOfSpace = searchString.indexOf(" "); + if (indexOfSpace > 0) + { + aStartsWith = labelA.toLowerCase().startsWith(searchString.substring(0, indexOfSpace).toLowerCase()); + bStartsWith = labelB.toLowerCase().startsWith(searchString.substring(0, indexOfSpace).toLowerCase()); - if (aStartsWith && !bStartsWith) - { - return A_FIRST; - } - else if (bStartsWith && !aStartsWith) - { - return B_FIRST; + if (aStartsWith && !bStartsWith) + { + return A_FIRST; + } + else if (bStartsWith && !aStartsWith) + { + return B_FIRST; + } } } @@ -439,7 +443,7 @@ const CommandMenu = ({metaData}: Props) => { - doFilter(value, search)}> + doFilter(value, search)}>