Merge pull request #61 from Kingsrook/feature/dot-menu-sort-filter-change

Feature/dot menu sort filter change
This commit is contained in:
2024-06-04 19:56:48 -05:00
committed by GitHub

View File

@ -36,7 +36,7 @@ import Icon from "@mui/material/Icon";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {makeStyles} from "@mui/styles"; import {makeStyles} from "@mui/styles";
import {Command} from "cmdk"; 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 {useNavigate} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils"; 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 CommandMenu = ({metaData}: Props) =>
{ {
const [searchString, setSearchString] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const pathParts = location.pathname.replace(/\/+$/, "").split("/"); const pathParts = location.pathname.replace(/\/+$/, "").split("/");
@ -71,7 +76,7 @@ const CommandMenu = ({metaData}: Props) =>
const classes = useStyles(); const classes = useStyles();
function evalueKeyPress(e: KeyboardEvent) function evaluateKeyPress(e: KeyboardEvent)
{ {
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// if a dot pressed, not from a "text" element, then toggle command menu // // 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) => const down = (e: KeyboardEvent) =>
{ {
evalueKeyPress(e); evaluateKeyPress(e);
} };
document.addEventListener("keydown", down) document.addEventListener("keydown", down);
return () => return () =>
{ {
document.removeEventListener("keydown", down) document.removeEventListener("keydown", down);
} };
}, [tableMetaData, dotMenuOpen, keyboardHelpOpen]) }, [tableMetaData, dotMenuOpen, keyboardHelpOpen]);
useEffect(() => useEffect(() =>
{ {
setDotMenuOpen(false); setDotMenuOpen(false);
}, [location.pathname]) }, [location.pathname]);
function goToItem(path: string) function goToItem(path: string)
{ {
@ -162,73 +167,117 @@ const CommandMenu = ({metaData}: Props) =>
return (null); 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;
}
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;
}
}
}
return (labelA.localeCompare(labelB));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function ActionsSection() function ActionsSection()
{ {
let tableNames : string[]= []; let tableNames: string[] = [];
metaData.tables.forEach((value: QTableMetaData, key: string) => metaData.tables.forEach((value: QTableMetaData, key: string) =>
{ {
tableNames.push(value.name); 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 labelA = metaData.tables.get(a).label ?? "";
const labelB = metaData.tables.get(b).label ?? ""; const labelB = metaData.tables.get(b).label ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
const path = location.pathname; 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") &&
( (
<Command.Group heading={`${tableMetaData.label} Actions`}> <Command.Group heading={`${tableMetaData.label} Actions`}>
{ {
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
<Command.Item onSelect={() => goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New"><Icon sx={{color: accentColor}}>add</Icon>New</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New"><Icon sx={{color: accentColor}}>add</Icon>New</Command.Item>
} }
{ {
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy"><Icon sx={{color: accentColor}}>copy</Icon>Copy</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy"><Icon sx={{color: accentColor}}>copy</Icon>Copy</Command.Item>
} }
{ {
tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission && tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission &&
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit"><Icon sx={{color: accentColor}}>edit</Icon>Edit</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit"><Icon sx={{color: accentColor}}>edit</Icon>Edit</Command.Item>
} }
{ {
metaData && metaData.tables.has("audit") && metaData && metaData.tables.has("audit") &&
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit"><Icon sx={{color: accentColor}}>checklist</Icon>Audit</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit"><Icon sx={{color: accentColor}}>checklist</Icon>Audit</Command.Item>
} }
{ {
tableProcesses && tableProcesses.length > 0 && tableProcesses && tableProcesses.length > 0 &&
( (
tableProcesses.map((process) => ( tableProcesses.map((process) => (
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}><Icon sx={{color: accentColor}}>{getIconName(process.iconName, "play_arrow")}</Icon>{process.label}</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}><Icon sx={{color: accentColor}}>{getIconName(process.iconName, "play_arrow")}</Icon>{process.label}</Command.Item>
)) ))
) )
} }
<Command.Separator /> <Command.Separator />
</Command.Group> </Command.Group>
); );
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function TablesSection() function TablesSection()
{ {
let tableNames : string[]= []; let tableNames: string[] = [];
metaData.tables.forEach((value: QTableMetaData, key: string) => metaData.tables.forEach((value: QTableMetaData, key: string) =>
{ {
tableNames.push(value.name); 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 labelA = metaData.tables.get(a).label ?? "";
const labelB = metaData.tables.get(b).label ?? ""; const labelB = metaData.tables.get(b).label ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
return( return (
<Command.Group heading="Tables"> <Command.Group heading="Tables">
{ {
tableNames.map((tableName: string, index: number) => tableNames.map((tableName: string, index: number) =>
@ -243,6 +292,7 @@ const CommandMenu = ({metaData}: Props) =>
); );
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -252,16 +302,16 @@ const CommandMenu = ({metaData}: Props) =>
metaData.apps.forEach((value: QAppMetaData, key: string) => metaData.apps.forEach((value: QAppMetaData, key: string) =>
{ {
appNames.push(value.name); 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 labelA = getFullAppLabel(metaData.appTree, a, 1, "") ?? "";
const labelB = getFullAppLabel(metaData.appTree, b, 1, "") ?? ""; const labelB = getFullAppLabel(metaData.appTree, b, 1, "") ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
return( return (
<Command.Group heading="Apps"> <Command.Group heading="Apps">
{ {
appNames.map((appName: string, index: number) => appNames.map((appName: string, index: number) =>
@ -276,33 +326,37 @@ const CommandMenu = ({metaData}: Props) =>
); );
} }
/*******************************************************************************
**
*******************************************************************************/
function RecentlyViewedSection() function RecentlyViewedSection()
{ {
const history = HistoryUtils.get(); const history = HistoryUtils.get();
const options = [] as any; const options = [] as any;
history.entries.reverse().forEach((entry, index) => history.entries.reverse().forEach((entry, index) =>
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName}) options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
) );
let appNames: string[] = []; let appNames: string[] = [];
metaData.apps.forEach((value: QAppMetaData, key: string) => metaData.apps.forEach((value: QAppMetaData, key: string) =>
{ {
appNames.push(value.name); 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 labelA = metaData.apps.get(a).label ?? "";
const labelB = metaData.apps.get(b).label ?? ""; const labelB = metaData.apps.get(b).label ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
const entryMap = new Map<string, boolean>(); const entryMap = new Map<string, boolean>();
return( return (
<Command.Group heading="Recently Viewed Records"> <Command.Group heading="Recently Viewed Records">
{ {
history.entries.reverse().map((entry: QHistoryEntry, index: number) => 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) && (
<Command.Item onSelect={() => goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}><Icon sx={{color: accentColor}}>{entry.iconName}</Icon>{entry.label}</Command.Item> <Command.Item onSelect={() => goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}><Icon sx={{color: accentColor}}>{entry.iconName}</Icon>{entry.label}</Command.Item>
) )
) )
@ -311,29 +365,90 @@ const CommandMenu = ({metaData}: Props) =>
); );
} }
const containerElement = useRef(null) const containerElement = useRef(null);
/*******************************************************************************
**
*******************************************************************************/
function closeKeyboardHelp() function closeKeyboardHelp()
{ {
setKeyboardHelpOpen(false); setKeyboardHelpOpen(false);
} }
/*******************************************************************************
**
*******************************************************************************/
function closeDotMenu() function closeDotMenu()
{ {
setDotMenuOpen(false); 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 ( return (
<React.Fragment> <React.Fragment>
<Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}> <Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}>
{ {
<Dialog open={dotMenuOpen} onClose={closeDotMenu}> <Dialog open={dotMenuOpen} onClose={closeDotMenu}>
<Command.Dialog open={dotMenuOpen} onOpenChange={setDotMenuOpen} container={containerElement.current} label="Test Global Command Menu"> <Command.Dialog open={dotMenuOpen} onOpenChange={setDotMenuOpen} container={containerElement.current} filter={(value, search) => doFilter(value, search)}>
<Box sx={{display: "flex"}}> <Box sx={{display: "flex"}}>
<Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..."/> <Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..." />
<Button onClick={closeDotMenu}><Icon>close</Icon></Button> <Button onClick={closeDotMenu}><Icon>close</Icon></Button>
</Box> </Box>
<Command.Loading /> <Command.Loading />
<Command.Separator /> <Command.Separator />
<Command.List> <Command.List>
<Command.Empty>No results found.</Command.Empty> <Command.Empty>No results found.</Command.Empty>
@ -381,6 +496,6 @@ const CommandMenu = ({metaData}: Props) =>
</Dialog> </Dialog>
} }
</React.Fragment> </React.Fragment>
) );
} };
export default CommandMenu; export default CommandMenu;