mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
Merge pull request #61 from Kingsrook/feature/dot-menu-sort-filter-change
Feature/dot menu sort filter change
This commit is contained in:
@ -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;
|
||||||
|
Reference in New Issue
Block a user