mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 21:30:45 +00:00
Merge branch 'integration/sprint-28' into feature/CTLE-503-optimization-weather-api-data
This commit is contained in:
19114
package-lock.json
generated
19114
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -18,12 +18,13 @@
|
|||||||
"@react-jvectormap/unitedstates": "1.0.1",
|
"@react-jvectormap/unitedstates": "1.0.1",
|
||||||
"@react-oauth/google": "0.2.8",
|
"@react-oauth/google": "0.2.8",
|
||||||
"@types/prop-types": "^15.7.5",
|
"@types/prop-types": "^15.7.5",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "18.0.0",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "18.0.0",
|
||||||
"@types/react-router-hash-link": "2.4.5",
|
"@types/react-router-hash-link": "2.4.5",
|
||||||
"ace-builds": "1.12.3",
|
"ace-builds": "1.12.3",
|
||||||
"chart.js": "3.4.1",
|
"chart.js": "3.4.1",
|
||||||
"chroma-js": "2.4.2",
|
"chroma-js": "2.4.2",
|
||||||
|
"cmdk": "0.2.0",
|
||||||
"datejs": "1.0.0-rc3",
|
"datejs": "1.0.0-rc3",
|
||||||
"downshift": "3.2.10",
|
"downshift": "3.2.10",
|
||||||
"faker": "5.5.3",
|
"faker": "5.5.3",
|
||||||
@ -33,16 +34,17 @@
|
|||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
"rapidoc": "9.3.4",
|
"rapidoc": "9.3.4",
|
||||||
"react": "17.0.2",
|
"react": "18.0.0",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
"react-chartjs-2": "3.0.4",
|
"react-chartjs-2": "3.0.4",
|
||||||
"react-cookie": "4.1.1",
|
"react-cookie": "4.1.1",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "18.0.0",
|
||||||
"react-github-btn": "1.2.1",
|
"react-github-btn": "1.2.1",
|
||||||
"react-google-drive-picker": "^1.2.0",
|
"react-google-drive-picker": "^1.2.0",
|
||||||
"react-router-dom": "6.2.1",
|
"react-router-dom": "6.2.1",
|
||||||
"react-router-hash-link": "2.4.3",
|
"react-router-hash-link": "2.4.3",
|
||||||
"react-table": "7.7.0",
|
"react-table": "7.7.0",
|
||||||
|
"sass": "1.63.4",
|
||||||
"ts-md5": "1.2.11",
|
"ts-md5": "1.2.11",
|
||||||
"yup": "0.32.11"
|
"yup": "0.32.11"
|
||||||
},
|
},
|
||||||
@ -50,8 +52,8 @@
|
|||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"clean": "rm -rf node_modules package-lock.json lib",
|
"clean": "rm -rf node_modules package-lock.json lib",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install",
|
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps",
|
||||||
"npm-install": "npm install",
|
"npm-install": "npm install --legacy-peer-deps",
|
||||||
"prepublishOnly": "tsc -p ./ --outDir lib/",
|
"prepublishOnly": "tsc -p ./ --outDir lib/",
|
||||||
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
|
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
|
38
src/App.tsx
38
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 {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
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 Avatar from "@mui/material/Avatar";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
@ -35,6 +37,7 @@ import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} f
|
|||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
||||||
import {Md5} from "ts-md5/dist/md5";
|
import {Md5} from "ts-md5/dist/md5";
|
||||||
|
import CommandMenu from "CommandMenu";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
||||||
import theme from "qqq/components/legacy/Theme";
|
import theme from "qqq/components/legacy/Theme";
|
||||||
@ -59,13 +62,13 @@ export const SESSION_ID_COOKIE_NAME = "sessionId";
|
|||||||
export default function App()
|
export default function App()
|
||||||
{
|
{
|
||||||
const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
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 [loadingToken, setLoadingToken] = useState(false);
|
||||||
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
||||||
const [profileRoutes, setProfileRoutes] = useState({});
|
const [profileRoutes, setProfileRoutes] = useState({});
|
||||||
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
||||||
|
const [metaData, setMetaData] = useState({} as QInstance);
|
||||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||||
|
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -263,14 +266,14 @@ export default function App()
|
|||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: app.name,
|
key: app.name,
|
||||||
route: path,
|
route: path,
|
||||||
component: <RecordQuery table={table} />,
|
component: <RecordQuery table={table} key={table.name}/>,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: app.name,
|
key: app.name,
|
||||||
route: `${path}/savedFilter/:id`,
|
route: `${path}/savedFilter/:id`,
|
||||||
component: <RecordQuery table={table} />,
|
component: <RecordQuery table={table} key={table.name}/>,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
@ -308,14 +311,14 @@ export default function App()
|
|||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: `${app.name}.edit`,
|
key: `${app.name}.edit`,
|
||||||
route: `${path}/:id/edit`,
|
route: `${path}/:id/edit`,
|
||||||
component: <EntityEdit table={table} isDuplicate={false} />,
|
component: <EntityEdit table={table} isCopy={false} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: `${app.name}.duplicate`,
|
key: `${app.name}.copy`,
|
||||||
route: `${path}/:id/duplicate`,
|
route: `${path}/:id/copy`,
|
||||||
component: <EntityEdit table={table} isDuplicate={true} />,
|
component: <EntityEdit table={table} isCopy={true} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
@ -336,14 +339,14 @@ export default function App()
|
|||||||
name: process.label,
|
name: process.label,
|
||||||
key: process.name,
|
key: process.name,
|
||||||
route: `${path}/${process.name}`,
|
route: `${path}/${process.name}`,
|
||||||
component: <RecordQuery table={table} launchProcess={process} />,
|
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
name: process.label,
|
name: process.label,
|
||||||
key: `${app.name}/${process.name}`,
|
key: `${app.name}/${process.name}`,
|
||||||
route: `${path}/:id/${process.name}`,
|
route: `${path}/:id/${process.name}`,
|
||||||
component: <RecordView table={table} launchProcess={process} />,
|
component: <RecordView table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -355,7 +358,7 @@ export default function App()
|
|||||||
name: process.label,
|
name: process.label,
|
||||||
key: process.name,
|
key: process.name,
|
||||||
route: `${path}/${process.name}`,
|
route: `${path}/${process.name}`,
|
||||||
component: <RecordQuery table={table} launchProcess={process} />,
|
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
@ -403,6 +406,7 @@ export default function App()
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
const metaData = await Client.getInstance().loadMetaData();
|
const metaData = await Client.getInstance().loadMetaData();
|
||||||
|
setMetaData(metaData);
|
||||||
if (metaData.branding)
|
if (metaData.branding)
|
||||||
{
|
{
|
||||||
setBranding(metaData.branding);
|
setBranding(metaData.branding);
|
||||||
@ -558,17 +562,27 @@ export default function App()
|
|||||||
|
|
||||||
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
|
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
|
||||||
const [accentColor, setAccentColor] = useState("#0062FF");
|
const [accentColor, setAccentColor] = useState("#0062FF");
|
||||||
|
const [tableMetaData, setTableMetaData] = useState(null);
|
||||||
|
const [tableProcesses, setTableProcesses] = useState(null);
|
||||||
|
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
|
|
||||||
appRoutes && (
|
appRoutes && (
|
||||||
<QContext.Provider value={{
|
<QContext.Provider value={{
|
||||||
pageHeader: pageHeader,
|
pageHeader: pageHeader,
|
||||||
accentColor: accentColor,
|
accentColor: accentColor,
|
||||||
|
tableMetaData: tableMetaData,
|
||||||
|
tableProcesses: tableProcesses,
|
||||||
|
dotMenuOpen: dotMenuOpen,
|
||||||
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
|
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
|
||||||
setAccentColor: (accentColor: string) => setAccentColor(accentColor)
|
setAccentColor: (accentColor: string) => setAccentColor(accentColor),
|
||||||
|
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
|
||||||
|
setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses),
|
||||||
|
setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent)
|
||||||
}}>
|
}}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<CommandMenu metaData={metaData}/>
|
||||||
<Sidenav
|
<Sidenav
|
||||||
color={sidenavColor}
|
color={sidenavColor}
|
||||||
icon={branding.icon}
|
icon={branding.icon}
|
||||||
|
295
src/CommandMenu.tsx
Normal file
295
src/CommandMenu.tsx
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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") &&
|
||||||
|
(
|
||||||
|
<Command.Group heading={`${tableMetaData.label} Actions`}>
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
tableProcesses && tableProcesses.length > 0 &&
|
||||||
|
(
|
||||||
|
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.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
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(
|
||||||
|
<Command.Group heading="Tables">
|
||||||
|
{
|
||||||
|
tableNames.map((tableName: string, index: number) =>
|
||||||
|
!metaData.tables.get(tableName).isHidden && metaData.getTablePath(metaData.tables.get(tableName)) &&
|
||||||
|
(
|
||||||
|
<Command.Item onSelect={() => goToItem(`${metaData.getTablePath(metaData.tables.get(tableName))}`)} key={`${tableName}-${index}`} value={metaData.tables.get(tableName).label}><Icon sx={{color: accentColor}}>{getIconName(metaData.tables.get(tableName).iconName, "table_rows")}</Icon>{metaData.tables.get(tableName).label}</Command.Item>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Command.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
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(
|
||||||
|
<Command.Group heading="Apps">
|
||||||
|
{
|
||||||
|
appNames.map((appName: string, index: number) =>
|
||||||
|
metaData.getAppPath(metaData.apps.get(appName)) &&
|
||||||
|
(
|
||||||
|
<Command.Item onSelect={() => goToItem(`${metaData.getAppPath(metaData.apps.get(appName))}`)} key={`${appName}-${index}`} value={getFullAppLabel(metaData.appTree, appName, 1, "")}><Icon sx={{color: accentColor}}>{getIconName(metaData.apps.get(appName).iconName, "apps")}</Icon>{getFullAppLabel(metaData.appTree, appName, 1, "")}</Command.Item>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Command.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, boolean>();
|
||||||
|
return(
|
||||||
|
<Command.Group heading="Recently Viewed Records">
|
||||||
|
{
|
||||||
|
history.entries.reverse().map((entry: QHistoryEntry, index: number) =>
|
||||||
|
! 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.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerElement = useRef(null)
|
||||||
|
return (
|
||||||
|
<Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}>
|
||||||
|
<Command.Dialog open={dotMenuOpen} onOpenChange={setDotMenuOpen} container={containerElement.current} label="Test Global Command Menu">
|
||||||
|
<Box sx={{display: "flex"}}>
|
||||||
|
<Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..."/>
|
||||||
|
<Button onClick={() => setDotMenuOpen(false)}><Icon>close</Icon></Button>
|
||||||
|
</Box>
|
||||||
|
<Command.Loading />
|
||||||
|
<Command.Separator />
|
||||||
|
<Command.List>
|
||||||
|
<Command.Empty>No results found.</Command.Empty>
|
||||||
|
<ActionsSection />
|
||||||
|
<Command.Separator />
|
||||||
|
<TablesSection />
|
||||||
|
<Command.Separator />
|
||||||
|
<AppsSection />
|
||||||
|
<Command.Separator />
|
||||||
|
<RecentlyViewedSection />
|
||||||
|
</Command.List>
|
||||||
|
</Command.Dialog>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default CommandMenu;
|
@ -19,6 +19,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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";
|
import {createContext} from "react";
|
||||||
|
|
||||||
|
|
||||||
@ -28,11 +32,20 @@ interface QContext
|
|||||||
setPageHeader?: (header: string | JSX.Element) => void;
|
setPageHeader?: (header: string | JSX.Element) => void;
|
||||||
accentColor: string;
|
accentColor: string;
|
||||||
setAccentColor?: (header: string) => void;
|
setAccentColor?: (header: string) => void;
|
||||||
|
dotMenuOpen: boolean;
|
||||||
|
qInstance?: QInstance;
|
||||||
|
appMetaData?: QAppMetaData;
|
||||||
|
tableMetaData?: QTableMetaData;
|
||||||
|
setTableMetaData?: (tableMetaData: QTableMetaData) => void;
|
||||||
|
tableProcesses?: QProcessMetaData[];
|
||||||
|
setTableProcesses?: (tableProcesses: QProcessMetaData[]) => void;
|
||||||
|
setDotMenuOpen?: (dotMenuOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
pageHeader: "",
|
pageHeader: "",
|
||||||
accentColor: "#0062FF"
|
accentColor: "#0062FF",
|
||||||
|
dotMenuOpen: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const QContext = createContext<QContext>(defaultState);
|
const QContext = createContext<QContext>(defaultState);
|
||||||
|
@ -22,10 +22,12 @@
|
|||||||
import {Auth0Provider} from "@auth0/auth0-react";
|
import {Auth0Provider} from "@auth0/auth0-react";
|
||||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {render} from "react-dom";
|
import {createRoot} from "react-dom/client";
|
||||||
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import App from "App";
|
import App from "App";
|
||||||
import "qqq/styles/qqq-override-styles.css";
|
import "qqq/styles/qqq-override-styles.css";
|
||||||
|
import "qqq/styles/globals.scss";
|
||||||
|
import "qqq/styles/raycast.scss";
|
||||||
import HandleAuthorizationError from "HandleAuthorizationError";
|
import HandleAuthorizationError from "HandleAuthorizationError";
|
||||||
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
||||||
import {MaterialUIControllerProvider} from "qqq/context";
|
import {MaterialUIControllerProvider} from "qqq/context";
|
||||||
@ -73,6 +75,9 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const container = document.getElementById("root");
|
||||||
|
const root = createRoot(container);
|
||||||
|
|
||||||
if (authenticationMetaData.type === "AUTH_0")
|
if (authenticationMetaData.type === "AUTH_0")
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -86,9 +91,8 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
|
|||||||
|
|
||||||
if(!domain || !clientId)
|
if(!domain || !clientId)
|
||||||
{
|
{
|
||||||
render(
|
root.render(
|
||||||
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>,
|
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>
|
||||||
document.getElementById("root"),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -101,7 +105,7 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
|
|||||||
domain = domain.replace(/\/$/, "");
|
domain = domain.replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
root.render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Auth0ProviderWithRedirectCallback
|
<Auth0ProviderWithRedirectCallback
|
||||||
domain={domain}
|
domain={domain}
|
||||||
@ -113,19 +117,18 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
|
|||||||
<ProtectedRoute component={App} />
|
<ProtectedRoute component={App} />
|
||||||
</MaterialUIControllerProvider>
|
</MaterialUIControllerProvider>
|
||||||
</Auth0ProviderWithRedirectCallback>
|
</Auth0ProviderWithRedirectCallback>
|
||||||
</BrowserRouter>,
|
</BrowserRouter>
|
||||||
document.getElementById("root"),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
render(
|
root.render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<MaterialUIControllerProvider>
|
<MaterialUIControllerProvider>
|
||||||
<App />
|
<App />
|
||||||
</MaterialUIControllerProvider>
|
</MaterialUIControllerProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
, document.getElementById("root"));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -22,8 +22,6 @@
|
|||||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
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 {Checkbox, Chip, CircularProgress, FilterOptionsState, Icon} from "@mui/material";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
@ -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 {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
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 Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
@ -54,7 +55,7 @@ interface Props
|
|||||||
closeModalHandler?: (event: object, reason: string) => void;
|
closeModalHandler?: (event: object, reason: string) => void;
|
||||||
defaultValues: { [key: string]: string };
|
defaultValues: { [key: string]: string };
|
||||||
disabledFields: { [key: string]: boolean } | string[];
|
disabledFields: { [key: string]: boolean } | string[];
|
||||||
isDuplicate?: boolean;
|
isCopy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityForm.defaultProps = {
|
EntityForm.defaultProps = {
|
||||||
@ -64,7 +65,7 @@ EntityForm.defaultProps = {
|
|||||||
closeModalHandler: null,
|
closeModalHandler: null,
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
disabledFields: {},
|
disabledFields: {},
|
||||||
isDuplicate: false
|
isCopy: false
|
||||||
};
|
};
|
||||||
|
|
||||||
function EntityForm(props: Props): JSX.Element
|
function EntityForm(props: Props): JSX.Element
|
||||||
@ -175,9 +176,9 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
fieldArray.push(fieldMetaData);
|
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 record: QRecord = null;
|
||||||
let defaultDisplayValues = new Map<string, string>();
|
let defaultDisplayValues = new Map<string, string>();
|
||||||
if (props.id !== null)
|
if (props.id !== null)
|
||||||
@ -185,7 +186,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
record = await qController.get(tableName, props.id);
|
record = await qController.get(tableName, props.id);
|
||||||
setRecord(record);
|
setRecord(record);
|
||||||
|
|
||||||
const titleVerb = props.isDuplicate ? "Duplicate" : "Edit";
|
const titleVerb = props.isCopy ? "Copy" : "Edit";
|
||||||
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||||
|
|
||||||
if (!props.isModal)
|
if (!props.isModal)
|
||||||
@ -195,13 +196,18 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
|
|
||||||
tableMetaData.fields.forEach((fieldMetaData, key) =>
|
tableMetaData.fields.forEach((fieldMetaData, key) =>
|
||||||
{
|
{
|
||||||
if (props.isDuplicate && fieldMetaData.name == tableMetaData.primaryKeyField)
|
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initialValues[key] = record.values.get(key);
|
initialValues[key] = record.values.get(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(! props.isCopy)
|
||||||
|
{
|
||||||
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
||||||
{
|
{
|
||||||
setNotAllowedError("Records may not be edited in this table");
|
setNotAllowedError("Records may not be edited in this table");
|
||||||
@ -211,6 +217,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////
|
///////////////////////////////////////////
|
||||||
@ -256,7 +263,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
// check capabilities & permissions //
|
// check capabilities & permissions //
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
if (props.isDuplicate || !props.id)
|
if (props.isCopy || !props.id)
|
||||||
{
|
{
|
||||||
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
|
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
|
||||||
{
|
{
|
||||||
@ -341,11 +348,11 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
const fieldName = section.fieldNames[j];
|
const fieldName = section.fieldNames[j];
|
||||||
const field = tableMetaData.fields.get(fieldName);
|
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. //
|
// 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. //
|
// || (or) we're on the insert screen in which case, only show editable fields. //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if ((props.id !== null && !props.isDuplicate) || field.isEditable)
|
if ((props.id !== null && !props.isCopy) || field.isEditable)
|
||||||
{
|
{
|
||||||
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
|
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... //
|
// 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 //
|
// 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});
|
navigate(path, {replace: true});
|
||||||
}
|
}
|
||||||
else if (props.id !== null)
|
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
|
// todo - audit that it's a dupe
|
||||||
await qController
|
await qController
|
||||||
@ -504,8 +511,8 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const path = props.isDuplicate ?
|
const path = props.isCopy ?
|
||||||
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
||||||
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
|
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
|
||||||
navigate(path, {state: {createSuccess: true}});
|
navigate(path, {state: {createSuccess: true}});
|
||||||
}
|
}
|
||||||
@ -514,8 +521,8 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
if(error.message.toLowerCase().startsWith("warning"))
|
if(error.message.toLowerCase().startsWith("warning"))
|
||||||
{
|
{
|
||||||
const path = props.isDuplicate ?
|
const path = props.isCopy ?
|
||||||
location.pathname.replace(new RegExp(`/${props.id}/duplicate$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
location.pathname.replace(new RegExp(`/${props.id}/copy$`), "/" + record.values.get(tableMetaData.primaryKeyField))
|
||||||
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
|
: location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField));
|
||||||
navigate(path, {state: {createSuccess: true, warning: error.message}});
|
navigate(path, {state: {createSuccess: true, warning: error.message}});
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
buildHistoryEntries();
|
buildHistoryEntries();
|
||||||
|
|
||||||
const history = HistoryUtils.get();
|
const history = HistoryUtils.get();
|
||||||
setHistory([ {label: "The Godfather", id: 1}, {label: "Pulp Fiction", id: 2}]);
|
|
||||||
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})
|
||||||
@ -119,6 +118,11 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
setHistory(options);
|
setHistory(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleHistoryOnOpen()
|
||||||
|
{
|
||||||
|
buildHistoryEntries();
|
||||||
|
}
|
||||||
|
|
||||||
const handleOpenMenu = (event: any) => setOpenMenu(event.currentTarget);
|
const handleOpenMenu = (event: any) => setOpenMenu(event.currentTarget);
|
||||||
const handleCloseMenu = () => setOpenMenu(false);
|
const handleCloseMenu = () => setOpenMenu(false);
|
||||||
|
|
||||||
@ -152,7 +156,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
autoHighlight
|
autoHighlight
|
||||||
blurOnSelect
|
blurOnSelect
|
||||||
style={{width: "200px"}}
|
style={{width: "200px"}}
|
||||||
onOpen={buildHistoryEntries}
|
onOpen={handleHistoryOnOpen}
|
||||||
onChange={handleAutocompleteOnChange}
|
onChange={handleAutocompleteOnChange}
|
||||||
PopperComponent={CustomPopper}
|
PopperComponent={CustomPopper}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
@ -62,6 +62,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El
|
|||||||
width="100%"
|
width="100%"
|
||||||
showPrintMargin={false}
|
showPrintMargin={false}
|
||||||
height="100%"
|
height="100%"
|
||||||
|
style={{borderBottomRightRadius: "1rem", borderBottomLeftRadius: "1rem"}}
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -23,7 +23,7 @@ import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QControl
|
|||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
|
import {IconButton, SelectChangeEvent, ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
@ -31,9 +31,14 @@ import Dialog from "@mui/material/Dialog";
|
|||||||
import DialogActions from "@mui/material/DialogActions";
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import FormControl from "@mui/material/FormControl/FormControl";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Select from "@mui/material/Select/Select";
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
import React, {useEffect, useReducer, useRef, useState} from "react";
|
||||||
import AceEditor from "react-ace";
|
import AceEditor from "react-ace";
|
||||||
@ -56,22 +61,82 @@ export interface ScriptEditorProps
|
|||||||
tableName: string;
|
tableName: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
recordId: any;
|
recordId: any;
|
||||||
scriptDefinition: any;
|
|
||||||
scriptTypeRecord: QRecord;
|
scriptTypeRecord: QRecord;
|
||||||
|
scriptTypeFileSchemaList: QRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tableName, fieldName, recordId, scriptDefinition, scriptTypeRecord}: ScriptEditorProps): JSX.Element
|
function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
||||||
|
{
|
||||||
|
const rs: {[name: string]: string} = {};
|
||||||
|
|
||||||
|
if(!scriptTypeFileSchemaList)
|
||||||
|
{
|
||||||
|
console.log("Missing scriptTypeFileSchemaList");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let files = scriptRevisionRecord?.associatedRecords?.get("files")
|
||||||
|
|
||||||
|
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
||||||
|
{
|
||||||
|
let scriptTypeFileSchema = scriptTypeFileSchemaList[i];
|
||||||
|
let name = scriptTypeFileSchema.values.get("name");
|
||||||
|
let contents = "";
|
||||||
|
|
||||||
|
for (let j = 0; j < files?.length; j++)
|
||||||
|
{
|
||||||
|
let file = files[j];
|
||||||
|
if(file.values.get("fileName") == name)
|
||||||
|
{
|
||||||
|
contents = file.values.get("contents");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rs[name] = contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFileTypeMap(scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
||||||
|
{
|
||||||
|
const rs: {[name: string]: string} = {};
|
||||||
|
|
||||||
|
if(!scriptTypeFileSchemaList)
|
||||||
|
{
|
||||||
|
console.log("Missing scriptTypeFileSchemaList");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
||||||
|
{
|
||||||
|
let name = scriptTypeFileSchemaList[i].values.get("name");
|
||||||
|
rs[name] = scriptTypeFileSchemaList[i].values.get("fileType");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tableName, fieldName, recordId, scriptTypeRecord, scriptTypeFileSchemaList}: ScriptEditorProps): JSX.Element
|
||||||
{
|
{
|
||||||
const [closing, setClosing] = useState(false);
|
const [closing, setClosing] = useState(false);
|
||||||
|
|
||||||
const [updatedCode, setUpdatedCode] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("contents") : "");
|
|
||||||
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
|
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
|
||||||
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
|
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
|
||||||
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
|
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
|
||||||
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null)
|
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null)
|
||||||
|
|
||||||
|
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
|
||||||
|
const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema);
|
||||||
|
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]])
|
||||||
|
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList))
|
||||||
|
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList))
|
||||||
|
console.log(`file types: ${JSON.stringify(fileTypes)}`);
|
||||||
|
|
||||||
const [commitMessage, setCommitMessage] = useState("")
|
const [commitMessage, setCommitMessage] = useState("")
|
||||||
const [openTool, setOpenTool] = useState(null);
|
const [openTool, setOpenTool] = useState(null);
|
||||||
const [errorAlert, setErrorAlert] = useState("")
|
const [errorAlert, setErrorAlert] = useState("")
|
||||||
@ -200,7 +265,6 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
{
|
{
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("scriptId", scriptId);
|
formData.append("scriptId", scriptId);
|
||||||
formData.append("contents", updatedCode);
|
|
||||||
formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
|
formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
|
||||||
|
|
||||||
if(apiName)
|
if(apiName)
|
||||||
@ -213,6 +277,15 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
formData.append("apiVersion", apiVersion);
|
formData.append("apiVersion", apiVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
|
||||||
|
formData.append("fileNames", fileNamesFromSchema.join(","));
|
||||||
|
|
||||||
|
for (let fileName in fileContents)
|
||||||
|
{
|
||||||
|
formData.append("fileContents:" + fileName, fileContents[fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
// we don't want this job to go async, so, pass a large timeout //
|
// we don't want this job to go async, so, pass a large timeout //
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
@ -249,10 +322,9 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
closeCallback(null, "cancelled");
|
closeCallback(null, "cancelled");
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCode = (value: string, event: any) =>
|
const updateCode = (value: string, event: any, index: number) =>
|
||||||
{
|
{
|
||||||
console.log("Updating code")
|
fileContents[openEditorFileNames[index]] = value;
|
||||||
setUpdatedCode(value);
|
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,8 +372,34 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
|
||||||
|
{
|
||||||
|
openEditorFileNames[index] = event.target.value
|
||||||
|
setOpenEditorFileNames(openEditorFileNames);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitEditorClicked = () =>
|
||||||
|
{
|
||||||
|
openEditorFileNames.push(availableFileNames[0])
|
||||||
|
setOpenEditorFileNames(openEditorFileNames);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeEditorClicked = (index: number) =>
|
||||||
|
{
|
||||||
|
openEditorFileNames.splice(index, 1)
|
||||||
|
setOpenEditorFileNames(openEditorFileNames);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeEditorWidth = (): string =>
|
||||||
|
{
|
||||||
|
return (100 / openEditorFileNames.length) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
<Box className="scriptEditor" sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
||||||
<Card sx={{height: "100%", p: 3}}>
|
<Card sx={{height: "100%", p: 3}}>
|
||||||
|
|
||||||
<Snackbar open={errorAlert !== null && errorAlert !== ""} onClose={(event?: React.SyntheticEvent | Event, reason?: string) =>
|
<Snackbar open={errorAlert !== null && errorAlert !== ""} onClose={(event?: React.SyntheticEvent | Event, reason?: string) =>
|
||||||
@ -348,8 +446,42 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
|
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Box display="flex" sx={{height: "100%"}}>
|
||||||
|
{openEditorFileNames.map((fileName, index) =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Box key={`${fileName}-${index}`} sx={{height: "100%", width: computeEditorWidth()}}>
|
||||||
|
<Box sx={{borderBottom: 1, borderColor: "divider"}} display="flex" justifyContent="space-between" alignItems="flex-end">
|
||||||
|
<FormControl className="selectedFileTab" variant="standard" sx={{verticalAlign: "bottom"}}>
|
||||||
|
<Select value={openEditorFileNames[index]} onChange={(event) => handleSelectingFile(event, index)}>
|
||||||
|
{
|
||||||
|
availableFileNames.map((name) => (
|
||||||
|
<MenuItem key={name} value={name}>{name}</MenuItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Box>
|
||||||
|
{
|
||||||
|
openEditorFileNames.length > 1 &&
|
||||||
|
<Tooltip title="Close this editor split" enterDelay={500}>
|
||||||
|
<IconButton size="small" onClick={() => closeEditorClicked(index)}>
|
||||||
|
<Icon>close</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
index == openEditorFileNames.length - 1 &&
|
||||||
|
<Tooltip title="Open a new editor split" enterDelay={500}>
|
||||||
|
<IconButton size="small" onClick={splitEditorClicked}>
|
||||||
|
<Icon>vertical_split</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<AceEditor
|
<AceEditor
|
||||||
mode="javascript"
|
mode={fileTypes[openEditorFileNames[index]] ?? "javascript"}
|
||||||
theme="github"
|
theme="github"
|
||||||
name="editor"
|
name="editor"
|
||||||
editorProps={{$blockScrolling: true}}
|
editorProps={{$blockScrolling: true}}
|
||||||
@ -358,19 +490,23 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
enableBasicAutocompletion: true,
|
enableBasicAutocompletion: true,
|
||||||
enableLiveAutocompletion: true,
|
enableLiveAutocompletion: true,
|
||||||
}}
|
}}
|
||||||
onChange={updateCode}
|
onChange={(value, event) => updateCode(value, event, index)}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="calc(100% - 58px)"
|
height="calc(100% - 88px)"
|
||||||
value={updatedCode}
|
value={fileContents[openEditorFileNames[index]]}
|
||||||
style={{border: "1px solid gray"}}
|
style={{border: "1px solid gray"}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{
|
{
|
||||||
openTool &&
|
openTool &&
|
||||||
<Box sx={{height: "45%"}} pt={2}>
|
<Box sx={{height: "45%"}} pt={2}>
|
||||||
{
|
{
|
||||||
openTool == "test" && <ScriptTestForm scriptId={scriptId} scriptDefinition={scriptDefinition} tableName={tableName} fieldName={fieldName} recordId={recordId} code={updatedCode} apiName={apiName} apiVersion={apiVersion} />
|
openTool == "test" && <ScriptTestForm scriptId={scriptId} scriptType={scriptTypeRecord} tableName={tableName} fieldName={fieldName} recordId={recordId} fileContents={fileContents} apiName={apiName} apiVersion={apiVersion} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
openTool == "docs" && <ScriptDocsForm helpText={scriptTypeRecord?.values.get("helpText")} exampleCode={scriptTypeRecord?.values.get("sampleCode")} aceEditorHeight="100%" />
|
openTool == "docs" && <ScriptDocsForm helpText={scriptTypeRecord?.values.get("helpText")} exampleCode={scriptTypeRecord?.values.get("sampleCode")} aceEditorHeight="100%" />
|
||||||
|
@ -24,6 +24,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
|
|||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {Typography} from "@mui/material";
|
import {Typography} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
@ -51,19 +52,21 @@ interface AssociatedScriptDefinition
|
|||||||
export interface ScriptTestFormProps
|
export interface ScriptTestFormProps
|
||||||
{
|
{
|
||||||
scriptId: number;
|
scriptId: number;
|
||||||
scriptDefinition: AssociatedScriptDefinition;
|
scriptType: QRecord;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
recordId: any;
|
recordId: any;
|
||||||
code: string;
|
fileContents: {[name: string]: string};
|
||||||
apiName: string;
|
apiName: string;
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recordId, code, apiName, apiVersion}: ScriptTestFormProps): JSX.Element
|
function ScriptTestForm({scriptId, scriptType, tableName, fieldName, recordId, fileContents, apiName, apiVersion}: ScriptTestFormProps): JSX.Element
|
||||||
{
|
{
|
||||||
|
const [testInputFields, setTestInputFields] = useState(null as QFieldMetaData[])
|
||||||
|
const [testOutputFields, setTestOutputFields] = useState(null as QFieldMetaData[])
|
||||||
const [testInputValues, setTestInputValues] = useState({} as any);
|
const [testInputValues, setTestInputValues] = useState({} as any);
|
||||||
const [testOutputValues, setTestOutputValues] = useState({} as any);
|
const [testOutputValues, setTestOutputValues] = useState({} as any);
|
||||||
const [logLines, setLogLines] = useState([] as any[])
|
const [logLines, setLogLines] = useState([] as any[])
|
||||||
@ -77,10 +80,46 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
|||||||
|
|
||||||
if(firstRender)
|
if(firstRender)
|
||||||
{
|
{
|
||||||
scriptDefinition.testInputFields.forEach((field: QFieldMetaData) =>
|
(async () =>
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// call backend to load details about how to test this script type //
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("scriptTypeId", scriptType.values.get("id"));
|
||||||
|
const processResult = await qController.processRun("loadScriptTestDetails", formData, null, true);
|
||||||
|
|
||||||
|
if (processResult instanceof QJobError)
|
||||||
|
{
|
||||||
|
const jobError = processResult as QJobError
|
||||||
|
setTestException(jobError.userFacingError ?? jobError.error)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobComplete = processResult as QJobComplete
|
||||||
|
|
||||||
|
const testInputFields = [] as QFieldMetaData[];
|
||||||
|
for(let i = 0; i <jobComplete?.values?.testInputFields?.length; i++)
|
||||||
|
{
|
||||||
|
testInputFields.push(new QFieldMetaData(jobComplete.values.testInputFields[i]));
|
||||||
|
}
|
||||||
|
setTestInputFields(testInputFields);
|
||||||
|
|
||||||
|
const testOutputFields = [] as QFieldMetaData[];
|
||||||
|
for(let i = 0; i <jobComplete?.values?.testOutputFields?.length; i++)
|
||||||
|
{
|
||||||
|
testOutputFields.push(new QFieldMetaData(jobComplete.values.testOutputFields[i]));
|
||||||
|
}
|
||||||
|
setTestOutputFields(testOutputFields);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// set a default value in each input field //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
testInputFields.forEach((field: QFieldMetaData) =>
|
||||||
{
|
{
|
||||||
testInputValues[field.name] = field.defaultValue ?? "";
|
testInputValues[field.name] = field.defaultValue ?? "";
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildFullExceptionMessage = (exception: any): string =>
|
const buildFullExceptionMessage = (exception: any): string =>
|
||||||
@ -91,9 +130,9 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
|||||||
const testScript = () =>
|
const testScript = () =>
|
||||||
{
|
{
|
||||||
const inputValues = new Map<string, any>();
|
const inputValues = new Map<string, any>();
|
||||||
if (scriptDefinition.testInputFields)
|
if (testInputFields)
|
||||||
{
|
{
|
||||||
scriptDefinition.testInputFields.forEach((field: QFieldMetaData) =>
|
testInputFields.forEach((field: QFieldMetaData) =>
|
||||||
{
|
{
|
||||||
inputValues.set(field.name, testInputValues[field.name]);
|
inputValues.set(field.name, testInputValues[field.name]);
|
||||||
});
|
});
|
||||||
@ -108,6 +147,7 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
let output;
|
let output;
|
||||||
|
/*
|
||||||
if(tableName && recordId && fieldName)
|
if(tableName && recordId && fieldName)
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////
|
||||||
@ -115,15 +155,21 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
|||||||
/////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////
|
||||||
inputValues.set("apiName", apiName);
|
inputValues.set("apiName", apiName);
|
||||||
inputValues.set("apiVersion", apiVersion);
|
inputValues.set("apiVersion", apiVersion);
|
||||||
output = await qController.testScript(tableName, recordId, fieldName, code, inputValues);
|
output = await qController.testScript(tableName, recordId, fieldName, "todo!", inputValues);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
*/
|
||||||
{
|
{
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("scriptId", scriptId);
|
formData.append("scriptId", scriptId);
|
||||||
formData.append("apiName", apiName);
|
formData.append("apiName", apiName);
|
||||||
formData.append("apiVersion", apiVersion);
|
formData.append("apiVersion", apiVersion);
|
||||||
formData.append("code", code);
|
|
||||||
|
formData.append("fileNames", Object.keys(fileContents).join(","))
|
||||||
|
for (let fileName in fileContents)
|
||||||
|
{
|
||||||
|
formData.append("fileContents:" + fileName, fileContents[fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
for(let fieldName of inputValues.keys())
|
for(let fieldName of inputValues.keys())
|
||||||
{
|
{
|
||||||
@ -195,7 +241,7 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
|||||||
<Typography variant="h6" p={2} pb={1}>Test Input</Typography>
|
<Typography variant="h6" p={2} pb={1}>Test Input</Typography>
|
||||||
<Box px={2} pb={2}>
|
<Box px={2} pb={2}>
|
||||||
{
|
{
|
||||||
scriptDefinition.testInputFields && testInputValues && scriptDefinition.testInputFields.map((field: QFieldMetaData) =>
|
testInputFields && testInputValues && testInputFields.map((field: QFieldMetaData) =>
|
||||||
{
|
{
|
||||||
return (<TextField
|
return (<TextField
|
||||||
key={field.name}
|
key={field.name}
|
||||||
@ -234,16 +280,20 @@ function ScriptTestForm({scriptId, scriptDefinition, tableName, fieldName, recor
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
scriptDefinition.testOutputFields && scriptDefinition.testOutputFields.map((f: any) =>
|
testOutputFields && testOutputFields.map((f: any) =>
|
||||||
{
|
{
|
||||||
const field = new QFieldMetaData(f);
|
const field = new QFieldMetaData(f);
|
||||||
return (
|
return (
|
||||||
<Box key={field.name} flexDirection="row" pr={2}>
|
<Box key={field.name} flexDirection="row" pr={2}>
|
||||||
<Typography variant="button" fontWeight="bold" pr={1}>
|
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1}>
|
||||||
{field.label}:
|
{field.label}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
<MDTypography variant="button" fontWeight="regular" color="text">
|
||||||
{ValueUtils.getValueForDisplay(field, testOutputValues[field.name], testOutputValues[field.name], "view")}
|
{
|
||||||
|
testOutputValues.values ?
|
||||||
|
ValueUtils.getValueForDisplay(field, testOutputValues.values[field.name], testOutputValues.displayValues[field.name], "view") :
|
||||||
|
ValueUtils.getValueForDisplay(field, testOutputValues[field.name], testOutputValues[field.name], "view")
|
||||||
|
}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -27,18 +27,21 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
|
|||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
|
import {Chip, SelectChangeEvent} from "@mui/material";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Avatar from "@mui/material/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Chip from "@mui/material/Chip";
|
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
|
import FormControl from "@mui/material/FormControl/FormControl";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import ListItem from "@mui/material/ListItem";
|
import ListItem from "@mui/material/ListItem";
|
||||||
import ListItemAvatar from "@mui/material/ListItemAvatar";
|
import ListItemAvatar from "@mui/material/ListItemAvatar";
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Modal from "@mui/material/Modal";
|
import Modal from "@mui/material/Modal";
|
||||||
|
import Select from "@mui/material/Select/Select";
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
import Tab from "@mui/material/Tab";
|
import Tab from "@mui/material/Tab";
|
||||||
import Tabs from "@mui/material/Tabs";
|
import Tabs from "@mui/material/Tabs";
|
||||||
@ -59,6 +62,7 @@ import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
|||||||
|
|
||||||
import "ace-builds/src-noconflict/mode-java";
|
import "ace-builds/src-noconflict/mode-java";
|
||||||
import "ace-builds/src-noconflict/mode-javascript";
|
import "ace-builds/src-noconflict/mode-javascript";
|
||||||
|
import "ace-builds/src-noconflict/mode-velocity";
|
||||||
import "ace-builds/src-noconflict/mode-json";
|
import "ace-builds/src-noconflict/mode-json";
|
||||||
import "ace-builds/src-noconflict/theme-github";
|
import "ace-builds/src-noconflict/theme-github";
|
||||||
import "ace-builds/src-noconflict/ext-language_tools";
|
import "ace-builds/src-noconflict/ext-language_tools";
|
||||||
@ -94,7 +98,9 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord);
|
const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord);
|
||||||
const [scriptLogs, setScriptLogs] = useState({} as any);
|
const [scriptLogs, setScriptLogs] = useState({} as any);
|
||||||
const [scriptTypeRecord, setScriptTypeRecord] = useState(null as QRecord)
|
const [scriptTypeRecord, setScriptTypeRecord] = useState(null as QRecord)
|
||||||
const [testScriptDefinitionObject, setTestScriptDefinitionObject] = useState({} as any)
|
const [scriptTypeFileSchemaList, setScriptTypeFileSchemaList] = useState(null as QRecord[])
|
||||||
|
const [availableFileNames, setAvailableFileNames] = useState([] as string[]);
|
||||||
|
const [selectedFileName, setSelectedFileName] = useState("");
|
||||||
const [currentVersionId , setCurrentVersionId] = useState(null as number);
|
const [currentVersionId , setCurrentVersionId] = useState(null as number);
|
||||||
const [notFoundMessage, setNotFoundMessage] = useState(null);
|
const [notFoundMessage, setNotFoundMessage] = useState(null);
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
const [selectedTab, setSelectedTab] = useState(0);
|
||||||
@ -118,17 +124,32 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
const scriptRecord = await qController.get("script", scriptId);
|
const scriptRecord = await qController.get("script", scriptId);
|
||||||
setScriptRecord(scriptRecord);
|
setScriptRecord(scriptRecord);
|
||||||
|
|
||||||
setScriptTypeRecord(await qController.get("scriptType", scriptRecord.values.get("scriptTypeId")));
|
const scriptTypeRecord = await qController.get("scriptType", scriptRecord.values.get("scriptTypeId"));
|
||||||
|
setScriptTypeRecord(scriptTypeRecord);
|
||||||
|
|
||||||
if(testInputFields !== null || testOutputFields !== null)
|
let fileMode = scriptTypeRecord.values.get("fileMode");
|
||||||
|
let scriptTypeFileSchemaList: QRecord[] = null;
|
||||||
|
if(fileMode == 1) // SINGLE
|
||||||
{
|
{
|
||||||
setTestScriptDefinitionObject({testInputFields: testInputFields, testOutputFields: testOutputFields});
|
scriptTypeFileSchemaList = [new QRecord({values: {name: "Script.js", fileType: "javascript"}})];
|
||||||
}
|
}
|
||||||
else
|
else if(fileMode == 2) // MULTI_PRE_DEFINED
|
||||||
{
|
{
|
||||||
setTestScriptDefinitionObject({testInputFields: [
|
const filter = new QQueryFilter([new QFilterCriteria("scriptTypeId", QCriteriaOperator.EQUALS, [scriptRecord.values.get("scriptTypeId")])], [new QFilterOrderBy("id")])
|
||||||
new QFieldMetaData({name: "recordPrimaryKeyList", label: "Record Primary Key List"})
|
scriptTypeFileSchemaList = await qController.query("scriptTypeFileSchema", filter);
|
||||||
], testOutputFields: []})
|
}
|
||||||
|
else // MULTI AD_HOC
|
||||||
|
{
|
||||||
|
// todo - not yet supported
|
||||||
|
console.log(`Script Type File Mode of ${fileMode} is not yet supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setScriptTypeFileSchemaList(scriptTypeFileSchemaList);
|
||||||
|
if(scriptTypeFileSchemaList)
|
||||||
|
{
|
||||||
|
const availableFileNames = scriptTypeFileSchemaList.map((fileSchemaRecord) => fileSchemaRecord.values.get("name"))
|
||||||
|
setAvailableFileNames(availableFileNames);
|
||||||
|
setSelectedFileName(availableFileNames[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
const criteria = [new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, [scriptId])];
|
const criteria = [new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, [scriptId])];
|
||||||
@ -141,13 +162,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
|
|
||||||
if(versions && versions.length > 0)
|
if(versions && versions.length > 0)
|
||||||
{
|
{
|
||||||
setCurrentVersionId(versions[0].values.get("id"));
|
selectVersion(versions[0]);
|
||||||
const latestVersion = await qController.get("scriptRevision", versions[0].values.get("id"));
|
|
||||||
console.log("Fetched latestVersion:");
|
|
||||||
console.log(latestVersion);
|
|
||||||
setSelectedVersionRecord(latestVersion);
|
|
||||||
loadingSelectedVersion.setNotLoading();
|
|
||||||
forceUpdate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
@ -174,8 +189,8 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
editorProps.tableName = associatedScriptTableName;
|
editorProps.tableName = associatedScriptTableName;
|
||||||
editorProps.fieldName = associatedScriptFieldName;
|
editorProps.fieldName = associatedScriptFieldName;
|
||||||
editorProps.recordId = associatedScriptRecordId;
|
editorProps.recordId = associatedScriptRecordId;
|
||||||
editorProps.scriptDefinition = testScriptDefinitionObject;
|
|
||||||
editorProps.scriptTypeRecord = scriptTypeRecord;
|
editorProps.scriptTypeRecord = scriptTypeRecord;
|
||||||
|
editorProps.scriptTypeFileSchemaList = scriptTypeFileSchemaList;
|
||||||
setEditorProps(editorProps);
|
setEditorProps(editorProps);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -223,8 +238,10 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
setCurrentVersionId(version.values.get("id"));
|
setCurrentVersionId(version.values.get("id"));
|
||||||
loadingSelectedVersion.setLoading();
|
loadingSelectedVersion.setLoading();
|
||||||
|
|
||||||
// fetch the full version
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
const selectedVersion = await qController.get("scriptRevision", version.values.get("id"));
|
// fetch the full version - including its associated scriptRevisionFile sub-records //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const selectedVersion = await qController.get("scriptRevision", version.values.get("id"), null, true);
|
||||||
console.log("Fetched selectedVersion:");
|
console.log("Fetched selectedVersion:");
|
||||||
console.log(selectedVersion);
|
console.log(selectedVersion);
|
||||||
setSelectedVersionRecord(selectedVersion);
|
setSelectedVersionRecord(selectedVersion);
|
||||||
@ -233,6 +250,44 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectFile = (event: SelectChangeEvent) =>
|
||||||
|
{
|
||||||
|
setSelectedFileName(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelectedFileCode = (): string =>
|
||||||
|
{
|
||||||
|
return (getSelectedVersionCode()[selectedFileName] ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelectedFileType = (): string =>
|
||||||
|
{
|
||||||
|
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
||||||
|
{
|
||||||
|
let name = scriptTypeFileSchemaList[i].values.get("name");
|
||||||
|
if(name == selectedFileName)
|
||||||
|
{
|
||||||
|
return (scriptTypeFileSchemaList[i].values.get("fileType"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ("javascript"); // have some default...
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelectedVersionCode = (): {[name: string]: string} =>
|
||||||
|
{
|
||||||
|
let rs: {[name: string]: string} = {}
|
||||||
|
let files = selectedVersionRecord?.associatedRecords?.get("files")
|
||||||
|
|
||||||
|
for (let j = 0; j < files?.length; j++)
|
||||||
|
{
|
||||||
|
let file = files[j];
|
||||||
|
rs[file.values.get("fileName")] = file.values.get("contents");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (rs);
|
||||||
|
}
|
||||||
|
|
||||||
function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
|
function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
|
||||||
{
|
{
|
||||||
return <List sx={{pl: 3, height: "400px", overflow: "auto"}}>
|
return <List sx={{pl: 3, height: "400px", overflow: "auto"}}>
|
||||||
@ -344,7 +399,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container>
|
<Grid container className="scriptViewer">
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
@ -420,10 +475,22 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
</CustomWidthTooltip>
|
</CustomWidthTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
loadingSelectedVersion.isNotLoading() && selectedVersionRecord && selectedVersionRecord.values.get("contents") ? (
|
loadingSelectedVersion.isNotLoading() && selectedVersionRecord ? (
|
||||||
<>
|
<>
|
||||||
|
{
|
||||||
|
availableFileNames && availableFileNames.length > 1 &&
|
||||||
|
<FormControl className="selectedFileTab" variant="standard" sx={{verticalAlign: "bottom", pl: "4px"}}>
|
||||||
|
<Select value={selectedFileName} onChange={(event) => handleSelectFile(event)}>
|
||||||
|
{
|
||||||
|
availableFileNames.map((name) => (
|
||||||
|
<MenuItem key={name} value={name}>{name}</MenuItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
}
|
||||||
<AceEditor
|
<AceEditor
|
||||||
mode="javascript"
|
mode={getSelectedFileType()}
|
||||||
theme="github"
|
theme="github"
|
||||||
name={"viewData"}
|
name={"viewData"}
|
||||||
readOnly
|
readOnly
|
||||||
@ -431,8 +498,9 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
editorProps={{$blockScrolling: true}}
|
editorProps={{$blockScrolling: true}}
|
||||||
setOptions={{useWorker: false}}
|
setOptions={{useWorker: false}}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="400px"
|
height="368px"
|
||||||
value={selectedVersionRecord?.values?.get("contents")}
|
value={getSelectedFileCode()}
|
||||||
|
style={{borderTop: "1px solid lightgray", borderBottomRightRadius: "1rem"}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null
|
) : null
|
||||||
@ -473,11 +541,11 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
<TabPanel index={2} value={selectedTab}>
|
<TabPanel index={2} value={selectedTab}>
|
||||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
<Box sx={{height: "455px"}} px={2} pb={1}>
|
||||||
<ScriptTestForm scriptId={scriptId}
|
<ScriptTestForm scriptId={scriptId}
|
||||||
scriptDefinition={testScriptDefinitionObject}
|
scriptType={scriptTypeRecord}
|
||||||
tableName={associatedScriptTableName}
|
tableName={associatedScriptTableName}
|
||||||
fieldName={associatedScriptFieldName}
|
fieldName={associatedScriptFieldName}
|
||||||
recordId={associatedScriptRecordId}
|
recordId={associatedScriptRecordId}
|
||||||
code={selectedVersionRecord?.values.get("contents")}
|
fileContents={getSelectedVersionCode()}
|
||||||
apiName={selectedVersionRecord?.values.get("apiName")}
|
apiName={selectedVersionRecord?.values.get("apiName")}
|
||||||
apiVersion={selectedVersionRecord?.values.get("apiVersion")} />
|
apiVersion={selectedVersionRecord?.values.get("apiVersion")} />
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -29,15 +29,15 @@ import BaseLayout from "qqq/layouts/BaseLayout";
|
|||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
table?: QTableMetaData;
|
table?: QTableMetaData;
|
||||||
isDuplicate?: boolean
|
isCopy?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityEdit.defaultProps = {
|
EntityEdit.defaultProps = {
|
||||||
table: null,
|
table: null,
|
||||||
isDuplicate: false
|
isCopy: false
|
||||||
};
|
};
|
||||||
|
|
||||||
function EntityEdit({table, isDuplicate}: Props): JSX.Element
|
function EntityEdit({table, isCopy}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const {id} = useParams();
|
const {id} = useParams();
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ function EntityEdit({table, isDuplicate}: Props): JSX.Element
|
|||||||
<Box mb={3}>
|
<Box mb={3}>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<EntityForm table={table} id={id} isDuplicate={isDuplicate} />
|
<EntityForm table={table} id={id} isCopy={isCopy} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -25,12 +25,14 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
|
|||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
|
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
|
||||||
import {Alert, Collapse, TablePagination} from "@mui/material";
|
import {Alert, Collapse, TablePagination, Typography} from "@mui/material";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
@ -79,6 +81,7 @@ const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
|||||||
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
|
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
|
||||||
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
||||||
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||||
|
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -135,6 +138,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const seenJoinTablesLocalStorageKey = `${SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const seenJoinTablesLocalStorageKey = `${SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
|
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
let defaultSort = [] as GridSortItem[];
|
let defaultSort = [] as GridSortItem[];
|
||||||
let defaultVisibility = {} as { [index: string]: boolean };
|
let defaultVisibility = {} as { [index: string]: boolean };
|
||||||
let didDefaultVisibilityComeFromLocalStorage = false;
|
let didDefaultVisibilityComeFromLocalStorage = false;
|
||||||
@ -142,6 +146,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
let defaultDensity = "standard" as GridDensity;
|
let defaultDensity = "standard" as GridDensity;
|
||||||
let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns;
|
let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns;
|
||||||
let seenJoinTables: {[tableName: string]: boolean} = {};
|
let seenJoinTables: {[tableName: string]: boolean} = {};
|
||||||
|
let defaultTableVariant: QTableVariant = null;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
// set the to be not per table (do as above if we want per table) at a later port //
|
// set the to be not per table (do as above if we want per table) at a later port //
|
||||||
@ -173,11 +178,16 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
seenJoinTables = JSON.parse(localStorage.getItem(seenJoinTablesLocalStorageKey));
|
seenJoinTables = JSON.parse(localStorage.getItem(seenJoinTablesLocalStorageKey));
|
||||||
}
|
}
|
||||||
|
if (localStorage.getItem(tableVariantLocalStorageKey))
|
||||||
|
{
|
||||||
|
defaultTableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||||
|
}
|
||||||
|
|
||||||
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
||||||
const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState("");
|
const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState("");
|
||||||
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
||||||
const [queryFilter, setQueryFilter] = useState(new QQueryFilter());
|
const [queryFilter, setQueryFilter] = useState(new QQueryFilter());
|
||||||
|
const [tableVariant, setTableVariant] = useState(defaultTableVariant);
|
||||||
|
|
||||||
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
||||||
const [shouldSetAllNewJoinFieldsToHidden, setShouldSetAllNewJoinFieldsToHidden] = useState(!didDefaultVisibilityComeFromLocalStorage)
|
const [shouldSetAllNewJoinFieldsToHidden, setShouldSetAllNewJoinFieldsToHidden] = useState(!didDefaultVisibilityComeFromLocalStorage)
|
||||||
@ -205,6 +215,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const [distinctRecordsOnPageCount, setDistinctRecordsOnPageCount] = useState(null as number);
|
const [distinctRecordsOnPageCount, setDistinctRecordsOnPageCount] = useState(null as number);
|
||||||
const [selectionSubsetSize, setSelectionSubsetSize] = useState(null as number);
|
const [selectionSubsetSize, setSelectionSubsetSize] = useState(null as number);
|
||||||
const [selectionSubsetSizePromptOpen, setSelectionSubsetSizePromptOpen] = useState(false);
|
const [selectionSubsetSizePromptOpen, setSelectionSubsetSizePromptOpen] = useState(false);
|
||||||
|
const [tableVariantPromptOpen, setTableVariantPromptOpen] = useState(false);
|
||||||
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter" | "filterSubset");
|
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter" | "filterSubset");
|
||||||
const [rowSelectionModel, setRowSelectionModel] = useState<GridSelectionModel>([]);
|
const [rowSelectionModel, setRowSelectionModel] = useState<GridSelectionModel>([]);
|
||||||
const [columnsModel, setColumnsModel] = useState([] as GridColDef[]);
|
const [columnsModel, setColumnsModel] = useState([] as GridColDef[]);
|
||||||
@ -338,6 +349,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
}, [location, tableMetaData]);
|
}, [location, tableMetaData]);
|
||||||
|
|
||||||
|
function promptForTableVariantSelection()
|
||||||
|
{
|
||||||
|
setTableVariantPromptOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
const updateColumnVisibilityModel = () =>
|
const updateColumnVisibilityModel = () =>
|
||||||
{
|
{
|
||||||
if (localStorage.getItem(columnVisibilityLocalStorageKey))
|
if (localStorage.getItem(columnVisibilityLocalStorageKey))
|
||||||
@ -424,8 +440,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return (false);
|
return (false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPageHeader = (tableMetaData: QTableMetaData, visibleJoinTables: Set<string>): string | JSX.Element =>
|
const getPageHeader = (tableMetaData: QTableMetaData, visibleJoinTables: Set<string>, tableVariant: QTableVariant): string | JSX.Element =>
|
||||||
{
|
{
|
||||||
|
let label: string = tableMetaData?.label ?? "";
|
||||||
|
|
||||||
if (visibleJoinTables.size > 0)
|
if (visibleJoinTables.size > 0)
|
||||||
{
|
{
|
||||||
let joinLabels = [];
|
let joinLabels = [];
|
||||||
@ -462,15 +480,36 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
return(
|
return(
|
||||||
<div>
|
<div>
|
||||||
{tableMetaData?.label}
|
{label}
|
||||||
<CustomWidthTooltip title={tooltipHTML}>
|
<CustomWidthTooltip title={tooltipHTML}>
|
||||||
<IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton>
|
<IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton>
|
||||||
</CustomWidthTooltip>
|
</CustomWidthTooltip>
|
||||||
|
{
|
||||||
|
tableVariant &&
|
||||||
|
<Typography variant="h6" color="text" fontWeight="light">
|
||||||
|
{tableMetaData.variantTableLabel}: {tableVariant.name}
|
||||||
|
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
|
||||||
|
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return (tableMetaData?.label);
|
return (
|
||||||
|
<div>
|
||||||
|
{label}
|
||||||
|
{
|
||||||
|
tableVariant &&
|
||||||
|
<Typography variant="h6" color="text" fontWeight="light">
|
||||||
|
{tableMetaData.variantTableLabel}: {tableVariant.name}
|
||||||
|
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
|
||||||
|
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
</div>);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -481,9 +520,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||||
|
|
||||||
const visibleJoinTables = getVisibleJoinTables();
|
const visibleJoinTables = getVisibleJoinTables();
|
||||||
setPageHeader(getPageHeader(tableMetaData, visibleJoinTables));
|
setPageHeader(getPageHeader(tableMetaData, visibleJoinTables, tableVariant));
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if there's an exposedJoin that we haven't seen before, we want to make sure that all of its fields //
|
// if there's an exposedJoin that we haven't seen before, we want to make sure that all of its fields //
|
||||||
@ -544,6 +582,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setTableMetaData(tableMetaData);
|
setTableMetaData(tableMetaData);
|
||||||
setTableLabel(tableMetaData.label);
|
setTableLabel(tableMetaData.label);
|
||||||
|
|
||||||
|
if(tableMetaData?.usesVariants && ! tableVariant)
|
||||||
|
{
|
||||||
|
promptForTableVariantSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (columnsModel.length == 0)
|
if (columnsModel.length == 0)
|
||||||
{
|
{
|
||||||
let linkBase = metaData.getTablePath(table);
|
let linkBase = metaData.getTablePath(table);
|
||||||
@ -634,7 +678,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
if (tableMetaData.capabilities.has(Capability.TABLE_COUNT))
|
if (tableMetaData.capabilities.has(Capability.TABLE_COUNT))
|
||||||
{
|
{
|
||||||
let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables());
|
let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables());
|
||||||
qController.count(tableName, qFilter, queryJoins, includeDistinct).then(([count, distinctCount]) =>
|
qController.count(tableName, qFilter, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
|
||||||
{
|
{
|
||||||
console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`);
|
console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`);
|
||||||
countResults[thisQueryId] = [];
|
countResults[thisQueryId] = [];
|
||||||
@ -652,7 +696,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
|
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
|
||||||
qController.query(tableName, qFilter, queryJoins).then((results) =>
|
qController.query(tableName, qFilter, queryJoins, tableVariant).then((results) =>
|
||||||
{
|
{
|
||||||
console.log(`Received results for query ${thisQueryId}`);
|
console.log(`Received results for query ${thisQueryId}`);
|
||||||
queryResults[thisQueryId] = results;
|
queryResults[thisQueryId] = results;
|
||||||
@ -1708,7 +1752,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
menuItems.push(<MenuItem key={process.name} onClick={() => processClicked(process)}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
menuItems.push(<MenuItem key={process.name} onClick={() => processClicked(process)}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||||
}
|
}
|
||||||
|
|
||||||
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate("dev")}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
|
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate(`${metaData.getTablePathByName(tableName)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
|
||||||
|
|
||||||
if (tableProcesses && tableProcesses.length)
|
if (tableProcesses && tableProcesses.length)
|
||||||
{
|
{
|
||||||
@ -1769,7 +1813,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setTotalRecords(null);
|
setTotalRecords(null);
|
||||||
setDistinctRecords(null);
|
setDistinctRecords(null);
|
||||||
updateTable();
|
updateTable();
|
||||||
}, [columnsModel, tableState]);
|
}, [columnsModel, tableState, tableVariant]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
@ -1970,6 +2014,15 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
tableMetaData &&
|
||||||
|
<TableVariantDialog table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
|
||||||
|
{
|
||||||
|
setTableVariantPromptOpen(false);
|
||||||
|
setTableVariant(value);
|
||||||
|
}} />
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
columnStatsFieldName &&
|
columnStatsFieldName &&
|
||||||
<Modal open={columnStatsFieldName !== null} onClose={(event, reason) => closeColumnStats(event, reason)}>
|
<Modal open={columnStatsFieldName !== null} onClose={(event, reason) => closeColumnStats(event, reason)}>
|
||||||
@ -1992,6 +2045,93 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
||||||
|
{
|
||||||
|
const [value, setValue] = useState(null)
|
||||||
|
const [dropDownOpen, setDropDownOpen] = useState(false)
|
||||||
|
const [variants, setVariants] = useState(null);
|
||||||
|
|
||||||
|
const handleVariantChange = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||||
|
{
|
||||||
|
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${props.table.name}`;
|
||||||
|
if(value != null)
|
||||||
|
{
|
||||||
|
localStorage.setItem(tableVariantLocalStorageKey, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
localStorage.removeItem(tableVariantLocalStorageKey);
|
||||||
|
}
|
||||||
|
props.closeHandler(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||||
|
{
|
||||||
|
if(e.key == "Enter" && value)
|
||||||
|
{
|
||||||
|
props.closeHandler(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
console.log("queryVariants")
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
const variants = await qController.tableVariants(props.table.name);
|
||||||
|
console.log(JSON.stringify(variants));
|
||||||
|
setVariants(variants);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return variants && (
|
||||||
|
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
||||||
|
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
||||||
|
<Autocomplete
|
||||||
|
id="tableVariantId"
|
||||||
|
sx={{width: "400px", marginTop: "10px"}}
|
||||||
|
open={dropDownOpen}
|
||||||
|
size="small"
|
||||||
|
onOpen={() =>
|
||||||
|
{
|
||||||
|
setDropDownOpen(true);
|
||||||
|
}}
|
||||||
|
onClose={() =>
|
||||||
|
{
|
||||||
|
setDropDownOpen(false);
|
||||||
|
}}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={handleVariantChange}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
options={variants}
|
||||||
|
renderInput={(params) => <TextField {...params} label={props.table.variantTableLabel} />}
|
||||||
|
getOptionLabel={(option) =>
|
||||||
|
{
|
||||||
|
if(typeof option == "object")
|
||||||
|
{
|
||||||
|
return (option as QTableVariant).name;
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
// mini-component that is the dialog for the user to enter the selection-subset //
|
// mini-component that is the dialog for the user to enter the selection-subset //
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -25,9 +25,11 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
|
|||||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
|
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {Alert, Box, Typography} from "@mui/material";
|
import {Alert, Typography} from "@mui/material";
|
||||||
import Avatar from "@mui/material/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Dialog from "@mui/material/Dialog";
|
import Dialog from "@mui/material/Dialog";
|
||||||
@ -74,36 +76,36 @@ RecordView.defaultProps =
|
|||||||
launchProcess: null,
|
launchProcess: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||||
|
|
||||||
function RecordView({table, launchProcess}: Props): JSX.Element
|
function RecordView({table, launchProcess}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const {id} = useParams();
|
const {id} = useParams();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {accentColor} = useContext(QContext);
|
|
||||||
|
|
||||||
const pathParts = location.pathname.replace(/\/+$/, "").split("/");
|
const pathParts = location.pathname.replace(/\/+$/, "").split("/");
|
||||||
const tableName = table.name;
|
const tableName = table.name;
|
||||||
|
let tableVariant: QTableVariant = null;
|
||||||
|
|
||||||
|
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
|
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
|
||||||
const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>);
|
const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>);
|
||||||
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
|
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
|
||||||
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
|
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
|
||||||
const [tableMetaData, setTableMetaData] = useState(null);
|
|
||||||
const [metaData, setMetaData] = useState(null as QInstance);
|
const [metaData, setMetaData] = useState(null as QInstance);
|
||||||
const [record, setRecord] = useState(null as QRecord);
|
const [record, setRecord] = useState(null as QRecord);
|
||||||
const [tableSections, setTableSections] = useState([] as QTableSection[]);
|
const [tableSections, setTableSections] = useState([] as QTableSection[]);
|
||||||
const [t1SectionName, setT1SectionName] = useState(null as string);
|
const [t1SectionName, setT1SectionName] = useState(null as string);
|
||||||
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
|
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
|
||||||
const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]);
|
const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]);
|
||||||
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
|
|
||||||
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
||||||
const [actionsMenu, setActionsMenu] = useState(null);
|
const [actionsMenu, setActionsMenu] = useState(null);
|
||||||
const [notFoundMessage, setNotFoundMessage] = useState(null as string);
|
const [notFoundMessage, setNotFoundMessage] = useState(null as string);
|
||||||
const [errorMessage, setErrorMessage] = useState(null as string)
|
const [errorMessage, setErrorMessage] = useState(null as string)
|
||||||
const [successMessage, setSuccessMessage] = useState(null as string);
|
const [successMessage, setSuccessMessage] = useState(null as string);
|
||||||
const [warningMessage, setWarningMessage] = useState(null as string);
|
const [warningMessage, setWarningMessage] = useState(null as string);
|
||||||
const {setPageHeader} = useContext(QContext);
|
|
||||||
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
||||||
const [reloadCounter, setReloadCounter] = useState(0);
|
const [reloadCounter, setReloadCounter] = useState(0);
|
||||||
|
|
||||||
@ -114,6 +116,13 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
||||||
const closeActionsMenu = () => setActionsMenu(null);
|
const closeActionsMenu = () => setActionsMenu(null);
|
||||||
|
|
||||||
|
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen} = useContext(QContext);
|
||||||
|
|
||||||
|
if (localStorage.getItem(tableVariantLocalStorageKey))
|
||||||
|
{
|
||||||
|
tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||||
|
}
|
||||||
|
|
||||||
const reload = () =>
|
const reload = () =>
|
||||||
{
|
{
|
||||||
setSuccessMessage(null);
|
setSuccessMessage(null);
|
||||||
@ -129,6 +138,69 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
setShowAudit(false);
|
setShowAudit(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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(!dotMenuOpen)
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
else if (e.key === "c" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
|
||||||
|
{
|
||||||
|
e.preventDefault()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}, [dotMenuOpen])
|
||||||
|
|
||||||
|
const gotoCreate = () =>
|
||||||
|
{
|
||||||
|
const path = `${pathParts.slice(0, -1).join("/")}/create`;
|
||||||
|
navigate(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gotoEdit = () =>
|
||||||
|
{
|
||||||
|
const path = `${pathParts.slice(0, -1).join("/")}/${record.values.get(table.primaryKeyField)}/edit`;
|
||||||
|
navigate(path);
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// monitor location changes - if we've clicked a link from viewing one record to viewing another, //
|
// monitor location changes - if we've clicked a link from viewing one record to viewing another, //
|
||||||
// we'll stay in this component, but we'll need to reload all data for the new record. //
|
// we'll stay in this component, but we'll need to reload all data for the new record. //
|
||||||
@ -266,10 +338,24 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
const metaData = await qController.loadMetaData();
|
const metaData = await qController.loadMetaData();
|
||||||
setMetaData(metaData);
|
setMetaData(metaData);
|
||||||
ValueUtils.qInstance = metaData;
|
ValueUtils.qInstance = metaData;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
// load the processes to show in the action menu //
|
||||||
|
///////////////////////////////////////////////////
|
||||||
const processesForTable = ProcessUtils.getProcessesForTable(metaData, tableName);
|
const processesForTable = ProcessUtils.getProcessesForTable(metaData, tableName);
|
||||||
processesForTable.sort((a, b) => a.label.localeCompare(b.label));
|
processesForTable.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
setTableProcesses(processesForTable);
|
setTableProcesses(processesForTable);
|
||||||
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// load processes that the routing needs to respect //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
const allTableProcesses = ProcessUtils.getProcessesForTable(metaData, tableName, true) // these include hidden ones (e.g., to find the bulks)
|
||||||
|
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
|
||||||
|
if (runRecordScriptProcess)
|
||||||
|
{
|
||||||
|
allTableProcesses.unshift(runRecordScriptProcess)
|
||||||
|
}
|
||||||
|
setAllTableProcesses(allTableProcesses);
|
||||||
|
|
||||||
if (launchingProcess)
|
if (launchingProcess)
|
||||||
{
|
{
|
||||||
@ -283,7 +369,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
let record: QRecord;
|
let record: QRecord;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
record = await qController.get(tableName, id);
|
record = await qController.get(tableName, id, tableVariant);
|
||||||
setRecord(record);
|
setRecord(record);
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
@ -507,11 +593,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission);
|
let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission);
|
||||||
|
|
||||||
function gotoCreate()
|
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
|
||||||
{
|
|
||||||
const path = `${pathParts.slice(0, -1).join("/")}/create`;
|
|
||||||
navigate(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderActionsMenu = (
|
const renderActionsMenu = (
|
||||||
<Menu
|
<Menu
|
||||||
@ -532,14 +614,14 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
||||||
<MenuItem onClick={() => gotoCreate()}>
|
<MenuItem onClick={() => gotoCreate()}>
|
||||||
<ListItemIcon><Icon>add</Icon></ListItemIcon>
|
<ListItemIcon><Icon>add</Icon></ListItemIcon>
|
||||||
Create New
|
New
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
||||||
<MenuItem onClick={() => navigate("duplicate")}>
|
<MenuItem onClick={() => navigate("copy")}>
|
||||||
<ListItemIcon><Icon>copy</Icon></ListItemIcon>
|
<ListItemIcon><Icon>copy</Icon></ListItemIcon>
|
||||||
Create Duplicate
|
Copy
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@ -561,16 +643,23 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
}
|
}
|
||||||
{tableProcesses.length > 0 && hasEditOrDelete && <Divider />}
|
{tableProcesses?.length > 0 && hasEditOrDelete && <Divider />}
|
||||||
{tableProcesses.map((process) => (
|
{tableProcesses?.map((process) => (
|
||||||
<MenuItem key={process.name} onClick={() => processClicked(process)}>
|
<MenuItem key={process.name} onClick={() => processClicked(process)}>
|
||||||
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
|
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
|
||||||
{process.label}
|
{process.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
{(tableProcesses.length > 0 || hasEditOrDelete) && <Divider />}
|
{(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />}
|
||||||
|
{
|
||||||
|
runRecordScriptProcess &&
|
||||||
|
<MenuItem key={runRecordScriptProcess.name} onClick={() => processClicked(runRecordScriptProcess)}>
|
||||||
|
<ListItemIcon><Icon>{runRecordScriptProcess.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
|
||||||
|
{runRecordScriptProcess.label}
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
<MenuItem onClick={() => navigate("dev")}>
|
<MenuItem onClick={() => navigate("dev")}>
|
||||||
<ListItemIcon><Icon>data_object</Icon></ListItemIcon>
|
<ListItemIcon><Icon>code</Icon></ListItemIcon>
|
||||||
Developer Mode
|
Developer Mode
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{
|
{
|
||||||
@ -799,7 +888,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
activeModalProcess &&
|
activeModalProcess &&
|
||||||
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
|
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
|
||||||
<div className="modalProcess">
|
<div className="modalProcess">
|
||||||
<ProcessRun process={activeModalProcess} isModal={true} recordIds={id} closeModalHandler={closeModalProcess} />
|
<ProcessRun process={activeModalProcess} isModal={true} table={tableMetaData} recordIds={id} closeModalHandler={closeModalProcess} />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
136
src/qqq/styles/globals.scss
Normal file
136
src/qqq/styles/globals.scss
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--app-bg);
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::after,
|
||||||
|
*::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-sans: 'Inter', --apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans,
|
||||||
|
Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
--app-bg: var(--gray1);
|
||||||
|
--cmdk-shadow: 0 16px 700px rgb(0 0 0 / 40%);
|
||||||
|
|
||||||
|
--lowContrast: #ffffff;
|
||||||
|
--highContrast: #000000;
|
||||||
|
|
||||||
|
--gray1: hsl(0, 0%, 99%);
|
||||||
|
--gray2: hsl(0, 0%, 97.3%);
|
||||||
|
--gray3: hsl(0, 0%, 95.1%);
|
||||||
|
--gray4: hsl(0, 0%, 93%);
|
||||||
|
--gray5: hsl(0, 0%, 90.9%);
|
||||||
|
--gray6: hsl(0, 0%, 88.7%);
|
||||||
|
--gray7: hsl(0, 0%, 85.8%);
|
||||||
|
--gray8: hsl(0, 0%, 78%);
|
||||||
|
--gray9: hsl(0, 0%, 56.1%);
|
||||||
|
--gray10: hsl(0, 0%, 52.3%);
|
||||||
|
--gray11: hsl(0, 0%, 43.5%);
|
||||||
|
--gray12: hsl(0, 0%, 9%);
|
||||||
|
|
||||||
|
--grayA1: hsla(0, 0%, 0%, 0.012);
|
||||||
|
--grayA2: hsla(0, 0%, 0%, 0.027);
|
||||||
|
--grayA3: hsla(0, 0%, 0%, 0.047);
|
||||||
|
--grayA4: hsla(0, 0%, 0%, 0.071);
|
||||||
|
--grayA5: hsla(0, 0%, 0%, 0.09);
|
||||||
|
--grayA6: hsla(0, 0%, 0%, 0.114);
|
||||||
|
--grayA7: hsla(0, 0%, 0%, 0.141);
|
||||||
|
--grayA8: hsla(0, 0%, 0%, 0.22);
|
||||||
|
--grayA9: hsla(0, 0%, 0%, 0.439);
|
||||||
|
--grayA10: hsla(0, 0%, 0%, 0.478);
|
||||||
|
--grayA11: hsla(0, 0%, 0%, 0.565);
|
||||||
|
--grayA12: hsla(0, 0%, 0%, 0.91);
|
||||||
|
|
||||||
|
--blue1: hsl(206, 100%, 99.2%);
|
||||||
|
--blue2: hsl(210, 100%, 98%);
|
||||||
|
--blue3: hsl(209, 100%, 96.5%);
|
||||||
|
--blue4: hsl(210, 98.8%, 94%);
|
||||||
|
--blue5: hsl(209, 95%, 90.1%);
|
||||||
|
--blue6: hsl(209, 81.2%, 84.5%);
|
||||||
|
--blue7: hsl(208, 77.5%, 76.9%);
|
||||||
|
--blue8: hsl(206, 81.9%, 65.3%);
|
||||||
|
--blue9: hsl(206, 100%, 50%);
|
||||||
|
--blue10: hsl(208, 100%, 47.3%);
|
||||||
|
--blue11: hsl(211, 100%, 43.2%);
|
||||||
|
--blue12: hsl(211, 100%, 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--app-bg: var(--gray1);
|
||||||
|
|
||||||
|
--lowContrast: #000000;
|
||||||
|
--highContrast: #ffffff;
|
||||||
|
|
||||||
|
--gray1: hsl(0, 0%, 8.5%);
|
||||||
|
--gray2: hsl(0, 0%, 11%);
|
||||||
|
--gray3: hsl(0, 0%, 13.6%);
|
||||||
|
--gray4: hsl(0, 0%, 15.8%);
|
||||||
|
--gray5: hsl(0, 0%, 17.9%);
|
||||||
|
--gray6: hsl(0, 0%, 20.5%);
|
||||||
|
--gray7: hsl(0, 0%, 24.3%);
|
||||||
|
--gray8: hsl(0, 0%, 31.2%);
|
||||||
|
--gray9: hsl(0, 0%, 43.9%);
|
||||||
|
--gray10: hsl(0, 0%, 49.4%);
|
||||||
|
--gray11: hsl(0, 0%, 62.8%);
|
||||||
|
--gray12: hsl(0, 0%, 93%);
|
||||||
|
|
||||||
|
--grayA1: hsla(0, 0%, 100%, 0);
|
||||||
|
--grayA2: hsla(0, 0%, 100%, 0.026);
|
||||||
|
--grayA3: hsla(0, 0%, 100%, 0.056);
|
||||||
|
--grayA4: hsla(0, 0%, 100%, 0.077);
|
||||||
|
--grayA5: hsla(0, 0%, 100%, 0.103);
|
||||||
|
--grayA6: hsla(0, 0%, 100%, 0.129);
|
||||||
|
--grayA7: hsla(0, 0%, 100%, 0.172);
|
||||||
|
--grayA8: hsla(0, 0%, 100%, 0.249);
|
||||||
|
--grayA9: hsla(0, 0%, 100%, 0.386);
|
||||||
|
--grayA10: hsla(0, 0%, 100%, 0.446);
|
||||||
|
--grayA11: hsla(0, 0%, 100%, 0.592);
|
||||||
|
--grayA12: hsla(0, 0%, 100%, 0.923);
|
||||||
|
|
||||||
|
--blue1: hsl(212, 35%, 9.2%);
|
||||||
|
--blue2: hsl(216, 50%, 11.8%);
|
||||||
|
--blue3: hsl(214, 59.4%, 15.3%);
|
||||||
|
--blue4: hsl(214, 65.8%, 17.9%);
|
||||||
|
--blue5: hsl(213, 71.2%, 20.2%);
|
||||||
|
--blue6: hsl(212, 77.4%, 23.1%);
|
||||||
|
--blue7: hsl(211, 85.1%, 27.4%);
|
||||||
|
--blue8: hsl(211, 89.7%, 34.1%);
|
||||||
|
--blue9: hsl(206, 100%, 50%);
|
||||||
|
--blue10: hsl(209, 100%, 60.6%);
|
||||||
|
--blue11: hsl(210, 100%, 66.1%);
|
||||||
|
--blue12: hsl(206, 98%, 95.8%);
|
||||||
|
}
|
@ -523,3 +523,25 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* file-select box in script editor & viewer - make it look tabby */
|
||||||
|
.scriptEditor .selectedFileTab div.MuiSelect-select,
|
||||||
|
.scriptViewer .selectedFileTab div.MuiSelect-select
|
||||||
|
{
|
||||||
|
padding: 0.25rem 1.5rem 0.25rem 1rem !important;
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
border-radius: 0.375rem 0.375rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scriptEditor .selectedFileTab,
|
||||||
|
.scriptViewer .selectedFileTab
|
||||||
|
{
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* show the down-arrow in the file-select box in script editor & viewer */
|
||||||
|
.scriptEditor .selectedFileTab .MuiSelect-iconStandard,
|
||||||
|
.scriptViewer .selectedFileTab .MuiSelect-iconStandard
|
||||||
|
{
|
||||||
|
display: inline;
|
||||||
|
right: .5rem
|
||||||
|
}
|
||||||
|
550
src/qqq/styles/raycast.scss
Normal file
550
src/qqq/styles/raycast.scss
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
/*!
|
||||||
|
* Copyright © 2022-2023. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.raycast {
|
||||||
|
[cmdk-root] {
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 650px;
|
||||||
|
background: var(--gray1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
box-shadow: var(--cmdk-shadow);
|
||||||
|
border: 1px solid var(--gray6);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: var(--gray2);
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--gray6) 20%,
|
||||||
|
var(--gray6) 40%,
|
||||||
|
var(--gray10) 50%,
|
||||||
|
var(--gray10) 55%,
|
||||||
|
var(--gray6) 70%,
|
||||||
|
var(--gray6) 100%
|
||||||
|
);
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 12px;
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
width: calc(100% + 2px);
|
||||||
|
height: calc(100% + 2px);
|
||||||
|
animation: shine 3s ease forwards 0.1s;
|
||||||
|
background-size: 200% auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 12px;
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
width: calc(100% + 2px);
|
||||||
|
height: calc(100% + 2px);
|
||||||
|
box-shadow: 0 0 0 1px transparent;
|
||||||
|
animation: border 1s linear forwards 0.5s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--gray3);
|
||||||
|
color: var(--gray11);
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-input] {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
outline: none;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--gray12);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-top-shine] {
|
||||||
|
.dark & {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(56, 189, 248, 0),
|
||||||
|
var(--gray5) 20%,
|
||||||
|
var(--gray9) 67.19%,
|
||||||
|
rgba(236, 72, 153, 0)
|
||||||
|
);
|
||||||
|
height: 1px;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
animation: showTopShine 0.1s ease forwards 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-loader] {
|
||||||
|
--loader-color: var(--gray9);
|
||||||
|
border: 0;
|
||||||
|
width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--gray6);
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
display: block;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
width: 50%;
|
||||||
|
height: 1px;
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(90deg, transparent 0%, var(--loader-color) 50%, transparent 100%);
|
||||||
|
top: -1px;
|
||||||
|
opacity: 0;
|
||||||
|
animation-duration: 1.5s;
|
||||||
|
animation-delay: 1s;
|
||||||
|
animation-timing-function: ease;
|
||||||
|
animation-name: loading;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-item] {
|
||||||
|
content-visibility: auto;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--gray12);
|
||||||
|
user-select: none;
|
||||||
|
will-change: background, color;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
transition-property: none;
|
||||||
|
|
||||||
|
&[data-selected='true'] {
|
||||||
|
background: var(--gray4);
|
||||||
|
color: var(--gray12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-disabled='true'] {
|
||||||
|
color: var(--gray8);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transition-property: background;
|
||||||
|
background: var(--gray4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + [cmdk-item] {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-meta] {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--gray11);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-list] {
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 393px;
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
scroll-padding-block-end: 40px;
|
||||||
|
transition: 100ms ease;
|
||||||
|
transition-property: height;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-open-trigger],
|
||||||
|
[cmdk-raycast-subcommand-trigger] {
|
||||||
|
color: var(--gray11);
|
||||||
|
padding: 0px 4px 0px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 28px;
|
||||||
|
letter-spacing: -0.25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-clipboard-icon],
|
||||||
|
[cmdk-raycast-hammer-icon] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-clipboard-icon] {
|
||||||
|
background: linear-gradient(to bottom, #f55354, #eb4646);
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-hammer-icon] {
|
||||||
|
background: linear-gradient(to bottom, #6cb9a3, #2c6459);
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-open-trigger] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--gray12);
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-subcommand-trigger] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--gray6);
|
||||||
|
border: 0;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-expanded='true'],
|
||||||
|
&:hover {
|
||||||
|
background: var(--gray4);
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
background: var(--gray7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-separator] {
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--gray5);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:not([hidden]) + [cmdk-group] {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[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] {
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background: var(--gray1);
|
||||||
|
bottom: 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-top: 1px solid var(--gray6);
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
filter: grayscale(1);
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 12px;
|
||||||
|
width: 1px;
|
||||||
|
border: 0;
|
||||||
|
background: var(--gray6);
|
||||||
|
margin: 0 4px 0px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: var(--gray2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-dialog] {
|
||||||
|
z-index: var(--layer-portal);
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 20%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
[cmdk] {
|
||||||
|
width: 640px;
|
||||||
|
transform-origin: center center;
|
||||||
|
animation: dialogIn var(--transition-fast) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-state='closed'] [cmdk] {
|
||||||
|
animation: dialogOut var(--transition-fast) forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-empty] {
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 64px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: var(--gray11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
to {
|
||||||
|
background-position: 200% center;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes border {
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 0 1px var(--gray6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes showTopShine {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.raycast-submenu {
|
||||||
|
[cmdk-root] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 320px;
|
||||||
|
border: 1px solid var(--gray6);
|
||||||
|
background: var(--gray2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-list] {
|
||||||
|
padding: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
transition: 100ms ease;
|
||||||
|
transition-property: height;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-item] {
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--gray12);
|
||||||
|
user-select: none;
|
||||||
|
will-change: background, color;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
transition-property: none;
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
background: var(--gray5);
|
||||||
|
color: var(--gray12);
|
||||||
|
|
||||||
|
[cmdk-raycast-submenu-shortcuts] kbd {
|
||||||
|
background: var(--gray7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-disabled='true'] {
|
||||||
|
color: var(--gray8);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-raycast-submenu-shortcuts] {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--gray5);
|
||||||
|
color: var(--gray11);
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-group-heading] {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray11);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-input] {
|
||||||
|
padding: 12px;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--gray6);
|
||||||
|
font-size: 13px;
|
||||||
|
background: transparent;
|
||||||
|
margin-top: auto;
|
||||||
|
width: 100%;
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
animation-timing-function: ease;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
transform-origin: var(--radix-popover-content-transform-origin);
|
||||||
|
|
||||||
|
&[data-state='open'] {
|
||||||
|
animation-name: slideIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-state='closed'] {
|
||||||
|
animation-name: slideOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
[cmdk-empty] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 64px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--gray11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.raycast {
|
||||||
|
[cmdk-input] {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -62,10 +62,21 @@ export default class HtmlUtils
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Download a server-side generated file.
|
** Download a server-side generated file (or the contents of a data: url)
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
static downloadUrlViaIFrame = (url: string) =>
|
static downloadUrlViaIFrame = (url: string, filename: string) =>
|
||||||
{
|
{
|
||||||
|
if(url.startsWith("data:"))
|
||||||
|
{
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = filename;
|
||||||
|
link.href = url;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (document.getElementById("downloadIframe"))
|
if (document.getElementById("downloadIframe"))
|
||||||
{
|
{
|
||||||
document.body.removeChild(document.getElementById("downloadIframe"));
|
document.body.removeChild(document.getElementById("downloadIframe"));
|
||||||
@ -101,10 +112,21 @@ export default class HtmlUtils
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Open a server-side generated file from a url in a new window.
|
** Open a server-side generated file from a url in a new window (or a data: url)
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
static openInNewWindow = (url: string, filename: string) =>
|
static openInNewWindow = (url: string, filename: string) =>
|
||||||
{
|
{
|
||||||
|
if(url.startsWith("data:"))
|
||||||
|
{
|
||||||
|
const openInWindow = window.open("", "_blank");
|
||||||
|
openInWindow.document.write(`<html lang="en">
|
||||||
|
<body style="margin: 0">
|
||||||
|
<iframe src="${url}" width="100%" height="100%" style="border: 0">
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const openInWindow = window.open("", "_blank");
|
const openInWindow = window.open("", "_blank");
|
||||||
openInWindow.document.write(`<html lang="en">
|
openInWindow.document.write(`<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -39,6 +39,7 @@ import HtmlUtils from "qqq/utils/HtmlUtils";
|
|||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
|
||||||
import "ace-builds/src-noconflict/mode-sql";
|
import "ace-builds/src-noconflict/mode-sql";
|
||||||
|
import "ace-builds/src-noconflict/mode-velocity";
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Utility class for working with QQQ Values
|
** Utility class for working with QQQ Values
|
||||||
@ -637,7 +638,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
|||||||
const download = (event: React.MouseEvent<HTMLSpanElement>) =>
|
const download = (event: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
{
|
{
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
HtmlUtils.downloadUrlViaIFrame(url);
|
HtmlUtils.downloadUrlViaIFrame(url, filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = (event: React.MouseEvent<HTMLSpanElement>) =>
|
const open = (event: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
|
@ -58,6 +58,7 @@ public class BulkEditTest extends QBaseSeleniumTest
|
|||||||
super.addJavalinRoutes(qSeleniumJavalin);
|
super.addJavalinRoutes(qSeleniumJavalin);
|
||||||
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json");
|
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json");
|
||||||
qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json");
|
qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json");
|
||||||
|
qSeleniumJavalin.withRouteToFile("/data/person/variants", "data/person/variants.json");
|
||||||
qSeleniumJavalin.withRouteToString("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/records", "[]");
|
qSeleniumJavalin.withRouteToString("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/records", "[]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
qSeleniumJavalin
|
qSeleniumJavalin
|
||||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||||
.withRouteToFile("/data/person/query", "data/person/index.json")
|
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||||
|
.withRouteToFile("/data/person/variants", "data/person/variants.json")
|
||||||
.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ public class ScriptTableTest extends QBaseSeleniumTest
|
|||||||
.withRouteToFile("/data/script/1", "data/script/1.json")
|
.withRouteToFile("/data/script/1", "data/script/1.json")
|
||||||
.withRouteToFile("/data/scriptType/1", "data/scriptType/1.json")
|
.withRouteToFile("/data/scriptType/1", "data/scriptType/1.json")
|
||||||
.withRouteToFile("/data/scriptRevision/query", "data/scriptRevision/query.json")
|
.withRouteToFile("/data/scriptRevision/query", "data/scriptRevision/query.json")
|
||||||
|
.withRouteToFile("/data/scriptLog/query", "data/scriptLog/query.json")
|
||||||
.withRouteToFile("/data/scriptRevision/100", "data/scriptRevision/100.json")
|
.withRouteToFile("/data/scriptRevision/100", "data/scriptRevision/100.json")
|
||||||
.withRouteToFile("/metaData/table/script", "metaData/table/script.json")
|
.withRouteToFile("/metaData/table/script", "metaData/table/script.json")
|
||||||
.withRouteToFile("/widget/scriptViewer", "widget/scriptViewer.json")
|
.withRouteToFile("/widget/scriptViewer", "widget/scriptViewer.json")
|
||||||
|
1
src/test/resources/fixtures/data/person/variants.json
Normal file
1
src/test/resources/fixtures/data/person/variants.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
3
src/test/resources/fixtures/data/scriptLog/query.json
Normal file
3
src/test/resources/fixtures/data/scriptLog/query.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"records": []
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"tableName": "scriptRevision",
|
"tableName": "scriptRevision",
|
||||||
"values": {
|
"values": {
|
||||||
"contents": "var hello;",
|
|
||||||
"id": 100,
|
"id": 100,
|
||||||
"commitMessage": "Initial checkin",
|
"commitMessage": "Initial checkin",
|
||||||
"author": "Jon Programmer",
|
"author": "Jon Programmer",
|
||||||
@ -10,5 +9,20 @@
|
|||||||
},
|
},
|
||||||
"displayValues": {
|
"displayValues": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"associatedRecords": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"tableName": "scriptRevisionFile",
|
||||||
|
"values": {
|
||||||
|
"id": 101,
|
||||||
|
"fileName": "Script.js",
|
||||||
|
"contents": "var hello;",
|
||||||
|
"scriptRevisionId": 100,
|
||||||
|
"createDate": "2023-06-23T21:59:57Z",
|
||||||
|
"modifyDate": "2023-06-23T21:59:57Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,7 +5,8 @@
|
|||||||
"name": "Record Script",
|
"name": "Record Script",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"createDate": "2023-02-18T00:47:51Z",
|
"createDate": "2023-02-18T00:47:51Z",
|
||||||
"modifyDate": "2023-02-18T00:47:51Z"
|
"modifyDate": "2023-02-18T00:47:51Z",
|
||||||
|
"fileMode": 1
|
||||||
},
|
},
|
||||||
"displayValues": {
|
"displayValues": {
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user