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 (
{
}
- )
-}
+ );
+};
export default CommandMenu;