CTLE-214: initial checkin of 'dot menu'

This commit is contained in:
Tim Chamberlain
2023-06-26 09:17:50 -05:00
parent 87e82c26ba
commit 4243c5dbd7
11 changed files with 400 additions and 272 deletions

View File

@ -23,9 +23,8 @@ import {InputAdornment, InputLabel} from "@mui/material";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch";
import {ErrorMessage, Field, useFormikContext} from "formik";
import React, {useContext, useState} from "react";
import React, {useState} from "react";
import AceEditor from "react-ace";
import QContext from "QContext";
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
import MDInput from "qqq/components/legacy/MDInput";
import MDTypography from "qqq/components/legacy/MDTypography";
@ -53,7 +52,6 @@ function QDynamicFormField({
{
const [switchChecked, setSwitchChecked] = useState(false);
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const {setAllowShortcuts} = useContext(QContext);
const {setFieldValue} = useFormikContext();
@ -127,14 +125,6 @@ function QDynamicFormField({
field = (
<>
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
onBlur={(e: any) =>
{
setAllowShortcuts(true);
}}
onFocus={(e: any) =>
{
setAllowShortcuts(false);
}}
onKeyPress={(e: any) =>
{
if (e.key === "Enter")

View File

@ -22,8 +22,6 @@
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import CheckBoxIcon from "@mui/icons-material/CheckBox";
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
import {Checkbox, Chip, CircularProgress, FilterOptionsState, Icon} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";

View File

@ -26,8 +26,9 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Box} from "@mui/material";
import {Alert} from "@mui/material";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
@ -54,7 +55,7 @@ interface Props
closeModalHandler?: (event: object, reason: string) => void;
defaultValues: { [key: string]: string };
disabledFields: { [key: string]: boolean } | string[];
isDuplicate?: boolean;
isCopy?: boolean;
}
EntityForm.defaultProps = {
@ -64,7 +65,7 @@ EntityForm.defaultProps = {
closeModalHandler: null,
defaultValues: {},
disabledFields: {},
isDuplicate: false
isCopy: false
};
function EntityForm(props: Props): JSX.Element
@ -175,9 +176,9 @@ function EntityForm(props: Props): JSX.Element
fieldArray.push(fieldMetaData);
});
//////////////////////////////////////////////////////////////////////////////////////////////
// if doing an edit or duplicate, fetch the record and pre-populate the form values from it //
//////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// if doing an edit or copy, fetch the record and pre-populate the form values from it //
/////////////////////////////////////////////////////////////////////////////////////////
let record: QRecord = null;
let defaultDisplayValues = new Map<string, string>();
if (props.id !== null)
@ -185,7 +186,7 @@ function EntityForm(props: Props): JSX.Element
record = await qController.get(tableName, props.id);
setRecord(record);
const titleVerb = props.isDuplicate ? "Duplicate" : "Edit";
const titleVerb = props.isCopy ? "Copy" : "Edit";
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
if (!props.isModal)
@ -195,20 +196,26 @@ function EntityForm(props: Props): JSX.Element
tableMetaData.fields.forEach((fieldMetaData, key) =>
{
if (props.isDuplicate && fieldMetaData.name == tableMetaData.primaryKeyField)
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
{
return;
}
initialValues[key] = record.values.get(key);
});
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(! props.isCopy)
{
setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{
setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
}
}
}
else
@ -256,7 +263,7 @@ function EntityForm(props: Props): JSX.Element
//////////////////////////////////////
// check capabilities & permissions //
//////////////////////////////////////
if (props.isDuplicate || !props.id)
if (props.isCopy || !props.id)
{
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{
@ -341,11 +348,11 @@ function EntityForm(props: Props): JSX.Element
const fieldName = section.fieldNames[j];
const field = tableMetaData.fields.get(fieldName);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null (and we're not duplicating) - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if ((props.id !== null && !props.isDuplicate) || field.isEditable)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if ((props.id !== null && !props.isCopy) || field.isEditable)
{
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
}
@ -393,9 +400,9 @@ function EntityForm(props: Props): JSX.Element
// but if the user used the anchors on the page, this doesn't effectively cancel... //
// what we have here pushed a new history entry (I think?), so could be better //
///////////////////////////////////////////////////////////////////////////////////////
if (props.id !== null && props.isDuplicate)
if (props.id !== null && props.isCopy)
{
const path = `${location.pathname.replace(/\/duplicate$/, "")}`;
const path = `${location.pathname.replace(/\/copy$/, "")}`;
navigate(path, {replace: true});
}
else if (props.id !== null)
@ -458,7 +465,7 @@ function EntityForm(props: Props): JSX.Element
}
}
if (props.id !== null && !props.isDuplicate)
if (props.id !== null && !props.isCopy)
{
// todo - audit that it's a dupe
await qController
@ -504,8 +511,8 @@ function EntityForm(props: Props): JSX.Element
}
else
{
const path = props.isDuplicate ?
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
const path = props.isCopy ?
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
navigate(path, {state: {createSuccess: true}});
}
@ -514,8 +521,8 @@ function EntityForm(props: Props): JSX.Element
{
if(error.message.toLowerCase().startsWith("warning"))
{
const path = props.isDuplicate ?
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
const path = props.isCopy ?
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
navigate(path, {state: {createSuccess: true, warning: error.message}});
}

View File

@ -30,9 +30,8 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu";
import TextField from "@mui/material/TextField";
import Toolbar from "@mui/material/Toolbar";
import React, {useContext, useEffect, useState} from "react";
import React, {useEffect, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
import QContext from "QContext";
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
import {navbar, navbarContainer, navbarIconButton, navbarRow,} from "qqq/components/horseshoe/Styles";
import {setTransparentNavbar, useMaterialUIController,} from "qqq/context";
@ -63,7 +62,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
const route = useLocation().pathname.split("/").slice(1);
const navigate = useNavigate();
const {setAllowShortcuts} = useContext(QContext);
useEffect(() =>
{
@ -122,15 +120,9 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
function handleHistoryOnOpen()
{
setAllowShortcuts(false);
buildHistoryEntries();
}
function handleHistoryOnClose()
{
setAllowShortcuts(true);
}
const handleOpenMenu = (event: any) => setOpenMenu(event.currentTarget);
const handleCloseMenu = () => setOpenMenu(false);
@ -165,7 +157,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
blurOnSelect
style={{width: "200px"}}
onOpen={handleHistoryOnOpen}
onClose={handleHistoryOnClose}
onChange={handleAutocompleteOnChange}
PopperComponent={CustomPopper}
isOptionEqualToValue={(option, value) => option.id === value.id}

View File

@ -29,15 +29,15 @@ import BaseLayout from "qqq/layouts/BaseLayout";
interface Props
{
table?: QTableMetaData;
isDuplicate?: boolean
isCopy?: boolean
}
EntityEdit.defaultProps = {
table: null,
isDuplicate: false
isCopy: false
};
function EntityEdit({table, isDuplicate}: Props): JSX.Element
function EntityEdit({table, isCopy}: Props): JSX.Element
{
const {id} = useParams();
@ -49,7 +49,7 @@ function EntityEdit({table, isDuplicate}: Props): JSX.Element
<Box mb={3}>
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityForm table={table} id={id} isDuplicate={isDuplicate} />
<EntityForm table={table} id={id} isCopy={isCopy} />
</Grid>
</Grid>
</Box>

View File

@ -88,21 +88,18 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>);
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
const [tableMetaData, setTableMetaData] = useState(null);
const [metaData, setMetaData] = useState(null as QInstance);
const [record, setRecord] = useState(null as QRecord);
const [tableSections, setTableSections] = useState([] as QTableSection[]);
const [t1SectionName, setT1SectionName] = useState(null as string);
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
const [actionsMenu, setActionsMenu] = useState(null);
const [notFoundMessage, setNotFoundMessage] = useState(null as string);
const [errorMessage, setErrorMessage] = useState(null as string)
const [successMessage, setSuccessMessage] = useState(null as string);
const [warningMessage, setWarningMessage] = useState(null as string);
const {accentColor, setPageHeader, allowShortcuts} = useContext(QContext);
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
const [reloadCounter, setReloadCounter] = useState(0);
@ -113,6 +110,8 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null);
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen} = useContext(QContext);
const reload = () =>
@ -133,11 +132,25 @@ function RecordView({table, launchProcess}: Props): JSX.Element
// Toggle the menu when ⌘K is pressed
useEffect(() =>
{
if(tableMetaData == null)
{
(async() =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
})();
}
const down = (e: { key: string; metaKey: any; ctrlKey: any; preventDefault: () => void; }) =>
{
if(allowShortcuts)
if(!dotMenuOpen)
{
if (e.key === "e" && table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)
if (e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{
e.preventDefault()
gotoCreate();
}
else if (e.key === "e" && table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)
{
e.preventDefault()
navigate("edit");
@ -145,19 +158,27 @@ function RecordView({table, launchProcess}: Props): JSX.Element
else if (e.key === "c" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{
e.preventDefault()
gotoCreate();
navigate("copy");
}
else if (e.key === "d" && table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission)
{
e.preventDefault()
handleClickDeleteButton();
}
else if (e.key === "a" && metaData && metaData.tables.has("audit"))
{
e.preventDefault()
navigate("#audit");
}
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [allowShortcuts])
return () =>
{
document.removeEventListener("keydown", down)
}
}, [dotMenuOpen])
const gotoCreate = () =>
{
@ -568,14 +589,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
<MenuItem onClick={() => gotoCreate()}>
<ListItemIcon><Icon>add</Icon></ListItemIcon>
Create New
New
</MenuItem>
}
{
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
<MenuItem onClick={() => navigate("duplicate")}>
<MenuItem onClick={() => navigate("copy")}>
<ListItemIcon><Icon>copy</Icon></ListItemIcon>
Create Duplicate
Copy
</MenuItem>
}
{
@ -597,14 +618,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element
Delete
</MenuItem>
}
{tableProcesses.length > 0 && hasEditOrDelete && <Divider />}
{tableProcesses.map((process) => (
{tableProcesses?.length > 0 && hasEditOrDelete && <Divider />}
{tableProcesses?.map((process) => (
<MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{process.label}
</MenuItem>
))}
{(tableProcesses.length > 0 || hasEditOrDelete) && <Divider />}
{(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />}
<MenuItem onClick={() => navigate("dev")}>
<ListItemIcon><Icon>data_object</Icon></ListItemIcon>
Developer Mode

View File

@ -282,10 +282,16 @@
[cmdk-group-heading] {
user-select: none;
font-size: 12px;
font-weight: bold;
color: var(--gray11);
padding: 0 8px;
display: flex;
align-items: center;
position:sticky;
top: -1;
padding-bottom: 4px;
background: white;
z-index: 1;
}
[cmdk-raycast-footer] {