diff --git a/src/App.tsx b/src/App.tsx index b73376c..a611734 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,8 @@ import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QApp import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData"; import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import Avatar from "@mui/material/Avatar"; import CssBaseline from "@mui/material/CssBaseline"; import Icon from "@mui/material/Icon"; @@ -35,7 +37,7 @@ import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} f import {useCookies} from "react-cookie"; import {Navigate, Route, Routes, useLocation,} from "react-router-dom"; import {Md5} from "ts-md5/dist/md5"; -import CommandMenu from "Command"; +import CommandMenu from "CommandMenu"; import QContext from "QContext"; import Sidenav from "qqq/components/horseshoe/sidenav/SideNav"; import theme from "qqq/components/legacy/Theme"; @@ -60,7 +62,7 @@ export const SESSION_ID_COOKIE_NAME = "sessionId"; export default function App() { const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]); - const {user, getAccessTokenSilently, getIdTokenClaims, logout} = useAuth0(); + const {user, getAccessTokenSilently, logout} = useAuth0(); const [loadingToken, setLoadingToken] = useState(false); const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false); const [profileRoutes, setProfileRoutes] = useState({}); @@ -309,14 +311,14 @@ export default function App() name: `${app.label}`, key: `${app.name}.edit`, route: `${path}/:id/edit`, - component: , + component: , }); routeList.push({ name: `${app.label}`, - key: `${app.name}.duplicate`, - route: `${path}/:id/duplicate`, - component: , + key: `${app.name}.copy`, + route: `${path}/:id/copy`, + component: , }); routeList.push({ @@ -560,17 +562,23 @@ export default function App() const [pageHeader, setPageHeader] = useState("" as string | JSX.Element); const [accentColor, setAccentColor] = useState("#0062FF"); - const [allowShortcuts, setAllowShortcuts] = useState(true); + const [tableMetaData, setTableMetaData] = useState(null); + const [tableProcesses, setTableProcesses] = useState(null); + const [dotMenuOpen, setDotMenuOpen] = useState(false); return ( appRoutes && ( setPageHeader(header), setAccentColor: (accentColor: string) => setAccentColor(accentColor), - setAllowShortcuts: (allowShortcuts: boolean) => setAllowShortcuts(allowShortcuts) + setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData), + setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses), + setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent) }}> diff --git a/src/Command.tsx b/src/Command.tsx deleted file mode 100644 index 3da324d..0000000 --- a/src/Command.tsx +++ /dev/null @@ -1,192 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2022. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData"; -import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; -import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Icon from "@mui/material/Icon"; -import {Command} from "cmdk"; -import React, {useContext, useEffect, useReducer, useRef, useState} from "react"; -import {useNavigate} from "react-router-dom"; -import QContext from "QContext"; -import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils"; - -interface Props -{ - metaData?: QInstance; -} - - -const CommandMenu = ({metaData}: Props) => -{ - const [open, setOpen] = useState(false) - const navigate = useNavigate(); - const {setAllowShortcuts, allowShortcuts, accentColor} = useContext(QContext); - const [, forceUpdate] = useReducer((x) => x + 1, 0); - - function evalueKeyPress(e: KeyboardEvent) - { - forceUpdate(); - if (e.key === "." && allowShortcuts) - { - e.preventDefault(); - setOpen((open) => !open) - } - } - - //////////////////////////////////////// - // Toggle the menu when ⌘K is pressed // - //////////////////////////////////////// - useEffect(() => - { - const down = (e: KeyboardEvent) => - { - evalueKeyPress(e); - } - - document.addEventListener("keydown", down) - return () => - { - document.removeEventListener("keydown", down) - } - }, [allowShortcuts]) - - ///////////////////////////////////////////////////// - // change allowing shortcuts based on open's value // - ///////////////////////////////////////////////////// - useEffect(() => - { - setAllowShortcuts(!open); - }, [open]) - - function goToItem(path: string) - { - navigate(path, {replace: true}); - setOpen(false); - } - - function TablesSection() - { - let tableNames : string[]= []; - metaData.tables.forEach((value: QTableMetaData, key: string) => - { - tableNames.push(value.name); - }) - tableNames = tableNames.sort((a: string, b:string) => - { - return (metaData.tables.get(a).label.localeCompare(metaData.tables.get(b).label)); - }) - return( - - { - tableNames.map((tableName: string, index: number) => - !metaData.tables.get(tableName).isHidden && metaData.getTablePath(metaData.tables.get(tableName)) && - ( - goToItem(`${metaData.getTablePath(metaData.tables.get(tableName))}`)} key={`${tableName}-${index}`}>{metaData.tables.get(tableName).iconName ? metaData.tables.get(tableName).iconName : "table_rows"}{metaData.tables.get(tableName).label} - ) - ) - } - - ); - } - - function AppsSection() - { - let appNames: string[] = []; - metaData.apps.forEach((value: QAppMetaData, key: string) => - { - appNames.push(value.name); - }) - - appNames = appNames.sort((a: string, b:string) => - { - return (metaData.apps.get(a).label.localeCompare(metaData.apps.get(b).label)); - }) - - return( - - { - appNames.map((appName: string, index: number) => - metaData.getAppPath(metaData.apps.get(appName)) && - ( - goToItem(`${metaData.getAppPath(metaData.apps.get(appName))}`)} key={`${appName}-${index}`}>{metaData.apps.get(appName).iconName ? metaData.apps.get(appName).iconName : "apps"}{metaData.apps.get(appName).label} - ) - ) - } - - ); - } - - 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) => - { - return (metaData.apps.get(a).label.localeCompare(metaData.apps.get(b).label)); - }) - - return( - - { - history.entries.reverse().map((entry: QHistoryEntry, index: number) => - goToItem(`${entry.path}`)} key={`${entry.label}-${index}`}>{entry.iconName}{entry.label} - ) - } - - ); - } - - const containerElement = useRef(null) - return ( - - - - - - - - - - No results found. - - - - - - - - - ) -} -export default CommandMenu; diff --git a/src/CommandMenu.tsx b/src/CommandMenu.tsx new file mode 100644 index 0000000..8d28f84 --- /dev/null +++ b/src/CommandMenu.tsx @@ -0,0 +1,295 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability"; +import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData"; +import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType"; +import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode"; +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Icon from "@mui/material/Icon"; +import {Command} from "cmdk"; +import React, {useContext, useEffect, useRef} from "react"; +import {useNavigate} from "react-router-dom"; +import QContext from "QContext"; +import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils"; + +interface Props +{ + metaData?: QInstance; +} + + +const CommandMenu = ({metaData}: Props) => +{ + const navigate = useNavigate(); + const pathParts = location.pathname.replace(/\/+$/, "").split("/"); + + const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, setTableMetaData, tableProcesses} = useContext(QContext); + + function evalueKeyPress(e: KeyboardEvent) + { + /////////////////////////////////////////////////////////////////////////// + // if a dot pressed, not from a "text" element, then toggle command menu // + /////////////////////////////////////////////////////////////////////////// + const type = (e.target as any).type; + if (e.key === "." && type !== "text") + { + e.preventDefault(); + setDotMenuOpen(!dotMenuOpen); + } + } + + //////////////////////////////////////////// + // Toggle the menu when period is pressed // + //////////////////////////////////////////// + useEffect(() => + { + ///////////////////////////////////////////////////////////////// + // if we are not in the right table, clear the table meta data // + ///////////////////////////////////////////////////////////////// + if (metaData && tableMetaData && !location.pathname.startsWith(`${metaData.getTablePath(tableMetaData)}/`)) + { + setTableMetaData(null); + } + + const down = (e: KeyboardEvent) => + { + evalueKeyPress(e); + } + + document.addEventListener("keydown", down) + return () => + { + document.removeEventListener("keydown", down) + } + }, [tableMetaData, dotMenuOpen]) + + useEffect(() => + { + setDotMenuOpen(false); + }, [location.pathname]) + + function goToItem(path: string) + { + navigate(path, {replace: true}); + setDotMenuOpen(false); + } + + function getIconName(iconName: string, defaultIconName: string) + { + return iconName ?? defaultIconName; + } + + + /******************************************************************************* + ** + *******************************************************************************/ + function getFullAppLabel(nodes: QAppTreeNode[] | undefined, name: string, depth: number, path: string): string | null + { + if (nodes === undefined) + { + return (null); + } + + for (let i = 0; i < nodes.length; i++) + { + if (nodes[i].type === QAppNodeType.APP && nodes[i].name === name) + { + return (`${path} > ${nodes[i].label}`); + } + else if (nodes[i].type === QAppNodeType.APP) + { + const result = getFullAppLabel(nodes[i].children, name, depth + 1, `${path} ${nodes[i].label}`); + if (result !== null) + { + return (result); + } + } + } + return (null); + } + + /******************************************************************************* + ** + *******************************************************************************/ + function ActionsSection() + { + let tableNames : string[]= []; + metaData.tables.forEach((value: QTableMetaData, key: string) => + { + tableNames.push(value.name); + }) + tableNames = tableNames.sort((a: string, b:string) => + { + return (metaData.tables.get(a).label.localeCompare(metaData.tables.get(b).label)); + }) + + const path = location.pathname; + 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 + } + { + tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && + 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 + } + { + metaData && metaData.tables.has("audit") && + 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} + )) + ) + } + + ); + } + + /******************************************************************************* + ** + *******************************************************************************/ + function TablesSection() + { + let tableNames : string[]= []; + metaData.tables.forEach((value: QTableMetaData, key: string) => + { + tableNames.push(value.name); + }) + tableNames = tableNames.sort((a: string, b:string) => + { + return (metaData.tables.get(a).label.localeCompare(metaData.tables.get(b).label)); + }) + return( + + { + tableNames.map((tableName: string, index: number) => + !metaData.tables.get(tableName).isHidden && metaData.getTablePath(metaData.tables.get(tableName)) && + ( + goToItem(`${metaData.getTablePath(metaData.tables.get(tableName))}`)} key={`${tableName}-${index}`} value={metaData.tables.get(tableName).label}>{getIconName(metaData.tables.get(tableName).iconName, "table_rows")}{metaData.tables.get(tableName).label} + ) + ) + } + + ); + } + + /******************************************************************************* + ** + *******************************************************************************/ + function AppsSection() + { + let appNames: string[] = []; + metaData.apps.forEach((value: QAppMetaData, key: string) => + { + appNames.push(value.name); + }) + + appNames = appNames.sort((a: string, b:string) => + { + return (getFullAppLabel(metaData.appTree, a, 1, "").localeCompare(getFullAppLabel(metaData.appTree, b, 1, ""))); + }) + + return( + + { + appNames.map((appName: string, index: number) => + metaData.getAppPath(metaData.apps.get(appName)) && + ( + goToItem(`${metaData.getAppPath(metaData.apps.get(appName))}`)} key={`${appName}-${index}`} value={getFullAppLabel(metaData.appTree, appName, 1, "")}>{getIconName(metaData.apps.get(appName).iconName, "apps")}{getFullAppLabel(metaData.appTree, appName, 1, "")} + ) + ) + } + + ); + } + + 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) => + { + return (metaData.apps.get(a).label.localeCompare(metaData.apps.get(b).label)); + }) + + const entryMap = new Map(); + return( + + { + history.entries.reverse().map((entry: QHistoryEntry, index: number) => + ! entryMap.has(entry.label) && entryMap.set(entry.label, true) && ( + goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}>{entry.iconName}{entry.label} + ) + ) + } + + ); + } + + const containerElement = useRef(null) + return ( + + + + + + + + + + No results found. + + + + + + + + + + + ) +} +export default CommandMenu; diff --git a/src/QContext.tsx b/src/QContext.tsx index c5c9e26..ed205b4 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -21,6 +21,7 @@ import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {createContext} from "react"; @@ -31,17 +32,20 @@ interface QContext setPageHeader?: (header: string | JSX.Element) => void; accentColor: string; setAccentColor?: (header: string) => void; + dotMenuOpen: boolean; qInstance?: QInstance; appMetaData?: QAppMetaData; tableMetaData?: QTableMetaData; - allowShortcuts?: boolean; - setAllowShortcuts?: (allowShortcuts: boolean) => void; + setTableMetaData?: (tableMetaData: QTableMetaData) => void; + tableProcesses?: QProcessMetaData[]; + setTableProcesses?: (tableProcesses: QProcessMetaData[]) => void; + setDotMenuOpen?: (dotMenuOpen: boolean) => void; } const defaultState = { pageHeader: "", accentColor: "#0062FF", - allowShortcuts: true + dotMenuOpen: false }; const QContext = createContext(defaultState); diff --git a/src/qqq/components/forms/DynamicFormField.tsx b/src/qqq/components/forms/DynamicFormField.tsx index 57a71f5..e2fb6f8 100644 --- a/src/qqq/components/forms/DynamicFormField.tsx +++ b/src/qqq/components/forms/DynamicFormField.tsx @@ -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 = ( <> - { - setAllowShortcuts(true); - }} - onFocus={(e: any) => - { - setAllowShortcuts(false); - }} onKeyPress={(e: any) => { if (e.key === "Enter") diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index ba22d4b..ac558c8 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -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"; diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 4719711..1d3c2ef 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -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(); 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}}); } diff --git a/src/qqq/components/horseshoe/NavBar.tsx b/src/qqq/components/horseshoe/NavBar.tsx index 51963ba..bdfc7bd 100644 --- a/src/qqq/components/horseshoe/NavBar.tsx +++ b/src/qqq/components/horseshoe/NavBar.tsx @@ -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(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} diff --git a/src/qqq/pages/records/edit/RecordEdit.tsx b/src/qqq/pages/records/edit/RecordEdit.tsx index 66dd34d..9d84298 100644 --- a/src/qqq/pages/records/edit/RecordEdit.tsx +++ b/src/qqq/pages/records/edit/RecordEdit.tsx @@ -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 - + diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index 780aecd..2903e5f 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -88,21 +88,18 @@ function RecordView({table, launchProcess}: Props): JSX.Element const [sectionFieldElements, setSectionFieldElements] = useState(null as Map); const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map); 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 && gotoCreate()}> add - Create New + New } { table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission && - navigate("duplicate")}> + navigate("copy")}> copy - Create Duplicate + Copy } { @@ -597,14 +618,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element Delete } - {tableProcesses.length > 0 && hasEditOrDelete && } - {tableProcesses.map((process) => ( + {tableProcesses?.length > 0 && hasEditOrDelete && } + {tableProcesses?.map((process) => ( processClicked(process)}> {process.iconName ?? "arrow_forward"} {process.label} ))} - {(tableProcesses.length > 0 || hasEditOrDelete) && } + {(tableProcesses?.length > 0 || hasEditOrDelete) && } navigate("dev")}> data_object Developer Mode diff --git a/src/qqq/styles/raycast.scss b/src/qqq/styles/raycast.scss index d4e5227..5a74cb5 100644 --- a/src/qqq/styles/raycast.scss +++ b/src/qqq/styles/raycast.scss @@ -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] {