mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
108 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
c31db7ac32 | |||
dd887037c2 | |||
92516a2eb0 | |||
13a918441c | |||
9e1c68b1fd | |||
d5c6985bc4 | |||
b8be374a01 | |||
2ef118a433 | |||
b1d685b5b1 | |||
87edebb79f | |||
c722081ae7 | |||
ca52466b79 | |||
dd45079ecd | |||
aa7f9e93f1 | |||
c899e5712b | |||
2a8bed1093 | |||
627dd3c9f5 | |||
40ac89dac3 | |||
e9223a1c23 | |||
eeb121ff12 | |||
455869c96b | |||
90861b33a4 | |||
6be18627a7 | |||
2255451745 | |||
f9b29e932a | |||
c86bfcff4d | |||
c8fe46c5bf | |||
8de2f2ce33 | |||
036c2253b1 | |||
6c6c1cfe3d | |||
754010df3d | |||
9d7315e773 | |||
12f13983ea | |||
ab0fb977fb | |||
7cb3f2284d | |||
6bdd8ed935 | |||
4f0469a04c | |||
f1c3b93049 | |||
1c1cfc6d75 | |||
87d0c7d478 | |||
fbcee2b819 | |||
c1065099e5 | |||
adcfa86f73 | |||
ad306728c2 | |||
5969f1a6ba | |||
6c3bfa776a | |||
924e657531 | |||
b52d0977cb | |||
ea728d3cf0 | |||
e5430101fa | |||
0879cb4f80 | |||
f1b0618e9d | |||
95f1fa83bb | |||
4b0e12ba47 | |||
6cfb1e04ed | |||
0d763cbfc8 | |||
279840e77a | |||
51b2f5bb5a | |||
02fe351084 | |||
25fa2e82ea | |||
24b4674208 | |||
04630fd154 | |||
1503e2a1d5 | |||
3d7502531d | |||
d0a7db28fe | |||
95244a8aba | |||
45f247785c | |||
9e6d5c10fb | |||
4e0b13ad02 | |||
a26f939859 | |||
7f57a11e00 | |||
83da3a3a0a | |||
b59ed8c8c1 | |||
7101420124 | |||
b903e6bef9 | |||
970c9f262c | |||
9313988f9b | |||
123d1742e7 | |||
47fca52437 | |||
44b92690ab | |||
64fe2305ad | |||
91d38a1d15 | |||
e3c511ef6d | |||
60a8baff35 | |||
81b46408b4 | |||
e993fcb949 | |||
e144cf3ec7 | |||
6bd6b0370b | |||
448560c427 | |||
791b50b893 | |||
b968705a01 | |||
0949ee9f78 | |||
f41b71d3c7 | |||
57fefe9671 | |||
8a018c34f6 | |||
1b4f70a547 | |||
1f343abbb5 | |||
37a18bbe0d | |||
98cc2ceb00 | |||
e351883d73 | |||
25599d0ca6 | |||
01d18902d7 | |||
580d4a90c9 | |||
eeb1b37d18 | |||
da0947b538 | |||
0c76371d59 | |||
19aebd631a | |||
5aac9ce069 |
@ -2,7 +2,7 @@ version: 2.1
|
|||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
node: circleci/node@5.1.0
|
node: circleci/node@5.1.0
|
||||||
browser-tools: circleci/browser-tools@1.4.3
|
browser-tools: circleci/browser-tools@1.4.5
|
||||||
|
|
||||||
executors:
|
executors:
|
||||||
java17:
|
java17:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.81",
|
"@kingsrook/qqq-frontend-core": "1.0.83",
|
||||||
"@mui/icons-material": "5.4.1",
|
"@mui/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.11.1",
|
"@mui/material": "5.11.1",
|
||||||
"@mui/styles": "5.11.1",
|
"@mui/styles": "5.11.1",
|
||||||
@ -33,6 +33,7 @@
|
|||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "1.4.8",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
|
"jwt-decode": "3.1.2",
|
||||||
"rapidoc": "9.3.4",
|
"rapidoc": "9.3.4",
|
||||||
"react": "18.0.0",
|
"react": "18.0.0",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
@ -56,9 +57,7 @@
|
|||||||
"npm-install": "npm install --legacy-peer-deps",
|
"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"
|
||||||
"cypress:open": "cypress open",
|
|
||||||
"cypress:run": "cypress run"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -86,8 +85,6 @@
|
|||||||
"@types/react-table": "7.7.9",
|
"@types/react-table": "7.7.9",
|
||||||
"@typescript-eslint/eslint-plugin": "5.10.2",
|
"@typescript-eslint/eslint-plugin": "5.10.2",
|
||||||
"@typescript-eslint/parser": "5.10.2",
|
"@typescript-eslint/parser": "5.10.2",
|
||||||
"cypress": "11.0.1",
|
|
||||||
"cypress-wait-for-stable-dom": "0.1.0",
|
|
||||||
"eslint": "8.8.0",
|
"eslint": "8.8.0",
|
||||||
"eslint-import-resolver-typescript": "2.5.0",
|
"eslint-import-resolver-typescript": "2.5.0",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.25.4",
|
||||||
|
18
pom.xml
18
pom.xml
@ -29,7 +29,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.19.0-SNAPSHOT</revision>
|
<revision>0.20.0-SNAPSHOT</revision>
|
||||||
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.seleniumhq.selenium</groupId>
|
<groupId>org.seleniumhq.selenium</groupId>
|
||||||
<artifactId>selenium-java</artifactId>
|
<artifactId>selenium-java</artifactId>
|
||||||
<version>4.10.0</version>
|
<version>4.15.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -161,6 +161,20 @@
|
|||||||
<skipUpdateVersion>true</skipUpdateVersion>
|
<skipUpdateVersion>true</skipUpdateVersion>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Publish this project's test code as a jar -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>test-jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
63
src/App.tsx
63
src/App.tsx
@ -33,6 +33,7 @@ import CssBaseline from "@mui/material/CssBaseline";
|
|||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {ThemeProvider} from "@mui/material/styles";
|
import {ThemeProvider} from "@mui/material/styles";
|
||||||
import {LicenseInfo} from "@mui/x-license-pro";
|
import {LicenseInfo} from "@mui/x-license-pro";
|
||||||
|
import jwt_decode from "jwt-decode";
|
||||||
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
||||||
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";
|
||||||
@ -61,7 +62,7 @@ export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
|||||||
|
|
||||||
export default function App()
|
export default function App()
|
||||||
{
|
{
|
||||||
const [, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const {user, getAccessTokenSilently, 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);
|
||||||
@ -72,29 +73,32 @@ export default function App()
|
|||||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
|
||||||
const decodeJWT = (jwt: string): any =>
|
/////////////////////////////////////////////////////////
|
||||||
|
// tell the client how to do a logout if it sees a 401 //
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
Client.setUnauthorizedCallback(() =>
|
||||||
{
|
{
|
||||||
const base64Url = jwt.split(".")[1];
|
logout();
|
||||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
})
|
||||||
const jsonPayload = decodeURIComponent(window.atob(base64).split("").map(function (c)
|
|
||||||
{
|
|
||||||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
|
||||||
}).join(""));
|
|
||||||
|
|
||||||
return JSON.parse(jsonPayload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||||
{
|
{
|
||||||
|
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||||
|
{
|
||||||
|
console.log("No session uuid cookie - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
if (!oldToken)
|
if (!oldToken)
|
||||||
{
|
{
|
||||||
|
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const oldJSON = decodeJWT(oldToken);
|
const oldJSON: any = jwt_decode(oldToken);
|
||||||
const newJSON = decodeJWT(newToken);
|
const newJSON: any = jwt_decode(newToken);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if the old (local storage) token is expired, then we need to store the new one //
|
// if the old (local storage) token is expired, then we need to store the new one //
|
||||||
@ -102,7 +106,7 @@ export default function App()
|
|||||||
const oldExp = oldJSON["exp"];
|
const oldExp = oldJSON["exp"];
|
||||||
if(oldExp * 1000 < (new Date().getTime()))
|
if(oldExp * 1000 < (new Date().getTime()))
|
||||||
{
|
{
|
||||||
console.log("Access token in local storage was expired.");
|
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +122,7 @@ export default function App()
|
|||||||
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||||
if(different)
|
if(different)
|
||||||
{
|
{
|
||||||
console.log("Latest access token from auth0 has changed vs localStorage.");
|
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||||
}
|
}
|
||||||
return (different);
|
return (different);
|
||||||
}
|
}
|
||||||
@ -157,22 +161,22 @@ export default function App()
|
|||||||
{
|
{
|
||||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||||
const newSessionUuid = await qController.manageSession(accessToken, null);
|
const newSessionUuid = await qController.manageSession(accessToken, null);
|
||||||
setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
|
||||||
localStorage.setItem("accessToken", accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
||||||
// todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. //
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// we turn off the shouldStoreNewToken block above, and turn on these 2 lines. //
|
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
localStorage.setItem("accessToken", accessToken);
|
||||||
localStorage.removeItem("accessToken");
|
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||||
*/
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("Using existing sessionUUID cookie");
|
||||||
|
}
|
||||||
|
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
qController.setGotAuthentication();
|
qController.setGotAuthentication();
|
||||||
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
|
||||||
|
|
||||||
setLoggedInUser(user);
|
setLoggedInUser(user);
|
||||||
console.log("Token load complete.");
|
console.log("Token load complete.");
|
||||||
@ -193,8 +197,8 @@ export default function App()
|
|||||||
// use a random token if anonymous or mock //
|
// use a random token if anonymous or mock //
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
console.log("Generating random token...");
|
console.log("Generating random token...");
|
||||||
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
|
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||||
console.log("Token generation complete.");
|
console.log("Token generation complete.");
|
||||||
return;
|
return;
|
||||||
@ -513,7 +517,7 @@ export default function App()
|
|||||||
name: loggedInUser?.name ?? "Anonymous",
|
name: loggedInUser?.name ?? "Anonymous",
|
||||||
key: "username",
|
key: "username",
|
||||||
noCollapse: true,
|
noCollapse: true,
|
||||||
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
icon: <Avatar src={profilePicture} alt="{loggedInUser?.name}" />,
|
||||||
};
|
};
|
||||||
setProfileRoutes(profileRoutes);
|
setProfileRoutes(profileRoutes);
|
||||||
|
|
||||||
@ -684,7 +688,6 @@ export default function App()
|
|||||||
appName={branding.appName}
|
appName={branding.appName}
|
||||||
branding={branding}
|
branding={branding}
|
||||||
routes={sideNavRoutes}
|
routes={sideNavRoutes}
|
||||||
pathToLabelMap={pathToLabelMap}
|
|
||||||
onMouseEnter={handleOnMouseEnter}
|
onMouseEnter={handleOnMouseEnter}
|
||||||
onMouseLeave={handleOnMouseLeave}
|
onMouseLeave={handleOnMouseLeave}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public interface MaterialDashboardIconRoleNames
|
||||||
|
{
|
||||||
|
String TOP_RIGHT_INSIDE_CARD = "topRightInsideCard";
|
||||||
|
}
|
@ -149,7 +149,7 @@ interface Types {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseProperties = {
|
const baseProperties = {
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
fontWeightLighter: 100,
|
fontWeightLighter: 100,
|
||||||
fontWeightLight: 300,
|
fontWeightLight: 300,
|
||||||
fontWeightRegular: 400,
|
fontWeightRegular: 400,
|
||||||
|
@ -78,6 +78,19 @@ interface Types
|
|||||||
light: string;
|
light: string;
|
||||||
main: string;
|
main: string;
|
||||||
focus: string;
|
focus: string;
|
||||||
|
}
|
||||||
|
blueGray:
|
||||||
|
| {
|
||||||
|
main: string;
|
||||||
|
}
|
||||||
|
gray:
|
||||||
|
| {
|
||||||
|
main: string;
|
||||||
|
focus: string;
|
||||||
|
}
|
||||||
|
grayLines:
|
||||||
|
| {
|
||||||
|
main: string;
|
||||||
}
|
}
|
||||||
| any;
|
| any;
|
||||||
primary: ColorsTypes | any;
|
primary: ColorsTypes | any;
|
||||||
@ -174,6 +187,19 @@ const colors: Types = {
|
|||||||
focus: "#ffffff",
|
focus: "#ffffff",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
blueGray: {
|
||||||
|
main: "#546E7A"
|
||||||
|
},
|
||||||
|
|
||||||
|
gray: {
|
||||||
|
main: "#757575",
|
||||||
|
focus: "#757575",
|
||||||
|
},
|
||||||
|
|
||||||
|
grayLines: {
|
||||||
|
main: "#D6D6D6"
|
||||||
|
},
|
||||||
|
|
||||||
black: {
|
black: {
|
||||||
light: "#000000",
|
light: "#000000",
|
||||||
main: "#000000",
|
main: "#000000",
|
||||||
@ -216,7 +242,7 @@ const colors: Types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dark: {
|
dark: {
|
||||||
main: "#344767",
|
main: "#212121",
|
||||||
focus: "#2c3c58",
|
focus: "#2c3c58",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ interface Types {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseProperties = {
|
const baseProperties = {
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
fontWeightLighter: 100,
|
fontWeightLighter: 100,
|
||||||
fontWeightLight: 300,
|
fontWeightLight: 300,
|
||||||
fontWeightRegular: 400,
|
fontWeightRegular: 400,
|
||||||
@ -199,9 +199,10 @@ const typography: Types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
h3: {
|
h3: {
|
||||||
fontSize: pxToRem(30),
|
fontSize: "1.75rem",
|
||||||
lineHeight: 1.375,
|
lineHeight: 1.375,
|
||||||
...baseHeadingProperties,
|
...baseHeadingProperties,
|
||||||
|
fontWeight: 600
|
||||||
},
|
},
|
||||||
|
|
||||||
h4: {
|
h4: {
|
||||||
@ -217,9 +218,10 @@ const typography: Types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
h6: {
|
h6: {
|
||||||
fontSize: pxToRem(16),
|
fontSize: "1.125rem",
|
||||||
lineHeight: 1.625,
|
lineHeight: 1.625,
|
||||||
...baseHeadingProperties,
|
...baseHeadingProperties,
|
||||||
|
fontWeight: 500
|
||||||
},
|
},
|
||||||
|
|
||||||
subtitle1: {
|
subtitle1: {
|
||||||
|
@ -31,7 +31,7 @@ type Types = any;
|
|||||||
|
|
||||||
const card: Types = {
|
const card: Types = {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
elevation: 3
|
elevation: 0
|
||||||
},
|
},
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
@ -42,7 +42,7 @@ const card: Types = {
|
|||||||
wordWrap: "break-word",
|
wordWrap: "break-word",
|
||||||
backgroundColor: white.main,
|
backgroundColor: white.main,
|
||||||
backgroundClip: "border-box",
|
backgroundClip: "border-box",
|
||||||
border: `${borderWidth[0]} solid ${rgba(black.main, 0.125)}`,
|
border: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||||
borderRadius: borderRadius.xl,
|
borderRadius: borderRadius.xl,
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
},
|
},
|
||||||
|
@ -1,29 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
=========================================================
|
=========================================================
|
||||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||||
=========================================================
|
=========================================================
|
||||||
|
|
||||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||||
|
|
||||||
Coded by www.creative-tim.com
|
Coded by www.creative-tim.com
|
||||||
|
|
||||||
=========================================================
|
=========================================================
|
||||||
|
|
||||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Material Dashboard 2 PRO React TS Base Styles
|
// Material Dashboard 2 PRO React TS Base Styles
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import borders from "qqq/assets/theme/base/borders";
|
|
||||||
import boxShadows from "qqq/assets/theme/base/boxShadows";
|
|
||||||
|
|
||||||
// Material Dashboard 2 PRO React TS Helper Functions
|
// Material Dashboard 2 PRO React TS Helper Functions
|
||||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||||
|
|
||||||
const { grey, white } = colors;
|
const {grey, white} = colors;
|
||||||
const { borderRadius } = borders;
|
|
||||||
const { tabsBoxShadow } = boxShadows;
|
|
||||||
|
|
||||||
// types
|
// types
|
||||||
type Types = any;
|
type Types = any;
|
||||||
@ -32,15 +28,25 @@ const tabs: Types = {
|
|||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
backgroundColor: grey[100],
|
borderRadius: 0,
|
||||||
borderRadius: borderRadius.xl,
|
borderBottom: "1px solid",
|
||||||
|
borderBottomColor: grey[400],
|
||||||
minHeight: "unset",
|
minHeight: "unset",
|
||||||
padding: pxToRem(4),
|
padding: "0",
|
||||||
|
margin: "0",
|
||||||
|
"& button": {
|
||||||
|
fontWeight: 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scroller: {
|
||||||
|
marginLeft: "0.5rem"
|
||||||
},
|
},
|
||||||
|
|
||||||
flexContainer: {
|
flexContainer: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
width: "fit-content",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -57,9 +63,10 @@ const tabs: Types = {
|
|||||||
|
|
||||||
indicator: {
|
indicator: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: 0,
|
||||||
backgroundColor: white.main,
|
backgroundColor: white.main,
|
||||||
boxShadow: tabsBoxShadow.indicator,
|
borderBottom: "2px solid",
|
||||||
|
borderBottomColor: colors.info.main,
|
||||||
transition: "all 500ms ease",
|
transition: "all 500ms ease",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
=========================================================
|
=========================================================
|
||||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||||
=========================================================
|
=========================================================
|
||||||
|
|
||||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||||
|
|
||||||
Coded by www.creative-tim.com
|
Coded by www.creative-tim.com
|
||||||
|
|
||||||
=========================================================
|
=========================================================
|
||||||
|
|
||||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Material Dashboard 2 PRO React TS Base Styles
|
// Material Dashboard 2 PRO React TS Base Styles
|
||||||
import typography from "qqq/assets/theme/base/typography";
|
import typography from "qqq/assets/theme/base/typography";
|
||||||
@ -21,9 +21,9 @@ import colors from "qqq/assets/theme/base/colors";
|
|||||||
// Material Dashboard 2 PRO React TS Helper Functions
|
// Material Dashboard 2 PRO React TS Helper Functions
|
||||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||||
|
|
||||||
const { size, fontWeightRegular } = typography;
|
const {size, fontWeightRegular} = typography;
|
||||||
const { borderRadius } = borders;
|
const {borderRadius} = borders;
|
||||||
const { dark } = colors;
|
const {dark} = colors;
|
||||||
|
|
||||||
// types
|
// types
|
||||||
type Types = any;
|
type Types = any;
|
||||||
@ -43,8 +43,10 @@ const tab: Types = {
|
|||||||
fontWeight: fontWeightRegular,
|
fontWeight: fontWeightRegular,
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
lineHeight: "inherit",
|
lineHeight: "inherit",
|
||||||
padding: pxToRem(4),
|
padding: "0.75rem 0.5rem 0.5rem",
|
||||||
borderRadius: borderRadius.lg,
|
margin: "0 0.5rem",
|
||||||
|
borderRadius: 0,
|
||||||
|
border: 0,
|
||||||
color: `${dark.main} !important`,
|
color: `${dark.main} !important`,
|
||||||
opacity: "1 !important",
|
opacity: "1 !important",
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import borders from "qqq/assets/theme/base/borders";
|
|||||||
// Material Dashboard 2 PRO React TS Helper Functions
|
// Material Dashboard 2 PRO React TS Helper Functions
|
||||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||||
|
|
||||||
const { black, light } = colors;
|
const { black, light, white, dark } = colors;
|
||||||
const { size, fontWeightRegular } = typography;
|
const { size, fontWeightRegular } = typography;
|
||||||
const { borderRadius } = borders;
|
const { borderRadius } = borders;
|
||||||
|
|
||||||
@ -39,19 +39,20 @@ const tooltip: Types = {
|
|||||||
|
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
maxWidth: pxToRem(200),
|
maxWidth: pxToRem(300),
|
||||||
backgroundColor: black.main,
|
backgroundColor: white.main,
|
||||||
color: light.main,
|
color: dark.main,
|
||||||
fontSize: size.sm,
|
fontSize: size.sm,
|
||||||
fontWeight: fontWeightRegular,
|
fontWeight: fontWeightRegular,
|
||||||
textAlign: "center",
|
textAlign: "left",
|
||||||
borderRadius: borderRadius.md,
|
borderRadius: borderRadius.md,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
padding: `${pxToRem(5)} ${pxToRem(8)} ${pxToRem(4)}`,
|
padding: "1rem",
|
||||||
|
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 3px -2px, rgba(0, 0, 0, 0.14) 0px 3px 4px 0px, rgba(0, 0, 0, 0.12) 0px 1px 8px 0px"
|
||||||
},
|
},
|
||||||
|
|
||||||
arrow: {
|
arrow: {
|
||||||
color: black.main,
|
color: white.main,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -402,7 +402,8 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={audit0.values.get("id")} className="auditGroupBlock">
|
<Box key={audit0.values.get("id")} className="auditGroupBlock">
|
||||||
<Box display="flex" flexDirection="row" justifyContent="center" fontSize={14}>
|
<Box position="sticky" top="0" zIndex={3}>
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="center" fontSize={14} position={"relative"} top={"-1px"} pb={"6px"} sx={{backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 80%, rgba(255,255,255,0))"}}>
|
||||||
<Box borderTop={1} mt={1.25} mr={1} width="100%" borderColor="#B0B0B0" />
|
<Box borderTop={1} mt={1.25} mr={1} width="100%" borderColor="#B0B0B0" />
|
||||||
<Box whiteSpace="nowrap">
|
<Box whiteSpace="nowrap">
|
||||||
{ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]}
|
{ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]}
|
||||||
@ -411,6 +412,7 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
|||||||
</Box>
|
</Box>
|
||||||
<Box borderTop={1} mt={1.25} ml={1} width="100%" borderColor="#B0B0B0" />
|
<Box borderTop={1} mt={1.25} ml={1} width="100%" borderColor="#B0B0B0" />
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{
|
{
|
||||||
audits.map((audit) =>
|
audits.map((audit) =>
|
||||||
|
@ -19,13 +19,14 @@
|
|||||||
* 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 {InputLabel} from "@mui/material";
|
import {Box, InputLabel} from "@mui/material";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import {styled} from "@mui/material/styles";
|
import {styled} from "@mui/material/styles";
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {useFormikContext} from "formik";
|
import {useFormikContext} from "formik";
|
||||||
import React, {SyntheticEvent} from "react";
|
import React, {SyntheticEvent} from "react";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
|
|
||||||
const AntSwitch = styled(Switch)(({theme}) => ({
|
const AntSwitch = styled(Switch)(({theme}) => ({
|
||||||
width: 28,
|
width: 28,
|
||||||
@ -60,6 +61,9 @@ const AntSwitch = styled(Switch)(({theme}) => ({
|
|||||||
duration: 200,
|
duration: 200,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
"&.nullSwitch .MuiSwitch-thumb": {
|
||||||
|
width: 24,
|
||||||
|
},
|
||||||
"& .MuiSwitch-track": {
|
"& .MuiSwitch-track": {
|
||||||
borderRadius: 16 / 2,
|
borderRadius: 16 / 2,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
@ -78,6 +82,7 @@ interface Props
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Element
|
function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Element
|
||||||
{
|
{
|
||||||
const {setFieldValue} = useFormikContext();
|
const {setFieldValue} = useFormikContext();
|
||||||
@ -96,8 +101,10 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
|||||||
setFieldValue(name, !value);
|
setFieldValue(name, !value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classNullSwitch = (value === null || value == undefined || `${value}` == "") ? "nullSwitch" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box bgcolor={isDisabled ? colors.grey[200] : ""}>
|
||||||
<InputLabel shrink={true}>{label}</InputLabel>
|
<InputLabel shrink={true}>{label}</InputLabel>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
<Typography
|
<Typography
|
||||||
@ -107,7 +114,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
|||||||
sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}>
|
sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}>
|
||||||
No
|
No
|
||||||
</Typography>
|
</Typography>
|
||||||
<AntSwitch name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
<AntSwitch className={classNullSwitch} name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||||
<Typography
|
<Typography
|
||||||
fontSize="0.875rem"
|
fontSize="0.875rem"
|
||||||
color={value === true ? "auto" : "#bfbfbf"}
|
color={value === true ? "auto" : "#bfbfbf"}
|
||||||
@ -116,7 +123,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
|||||||
Yes
|
Yes
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,14 @@ function QDynamicFormField({
|
|||||||
if (type === "checkbox")
|
if (type === "checkbox")
|
||||||
{
|
{
|
||||||
getsBulkEditHtmlLabel = false;
|
getsBulkEditHtmlLabel = false;
|
||||||
field = (<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />);
|
field = (<>
|
||||||
|
<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />
|
||||||
|
<Box mt={0.75}>
|
||||||
|
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||||
|
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||||
|
</MDTypography>
|
||||||
|
</Box>
|
||||||
|
</>);
|
||||||
}
|
}
|
||||||
else if (type === "ace")
|
else if (type === "ace")
|
||||||
{
|
{
|
||||||
|
@ -426,6 +426,11 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
actions.setSubmitting(true);
|
actions.setSubmitting(true);
|
||||||
await (async () =>
|
await (async () =>
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// we will be manipulating the values sent to the backend, so clone values so they remained unchanged for the form widgets //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const valuesToPost = JSON.parse(JSON.stringify(values));
|
||||||
|
|
||||||
for(let fieldName of tableMetaData.fields.keys())
|
for(let fieldName of tableMetaData.fields.keys())
|
||||||
{
|
{
|
||||||
const fieldMetaData = tableMetaData.fields.get(fieldName);
|
const fieldMetaData = tableMetaData.fields.get(fieldName);
|
||||||
@ -438,17 +443,17 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
// changing from, say, 12:15:30 to just 12:15:00... this seems to get around that, for cases when the //
|
// changing from, say, 12:15:30 to just 12:15:00... this seems to get around that, for cases when the //
|
||||||
// user didn't change the value in the field (but if the user did change the value, then we will submit it) //
|
// user didn't change the value in the field (but if the user did change the value, then we will submit it) //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(fieldMetaData.type === QFieldType.DATE_TIME && values[fieldName])
|
if(fieldMetaData.type === QFieldType.DATE_TIME && valuesToPost[fieldName])
|
||||||
{
|
{
|
||||||
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${values[fieldName]}]`)
|
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${valuesToPost[fieldName]}]`)
|
||||||
if (initialValues[fieldName] == values[fieldName])
|
if (initialValues[fieldName] == valuesToPost[fieldName])
|
||||||
{
|
{
|
||||||
console.log(" - Is the same, so, deleting from the post");
|
console.log(" - Is the same, so, deleting from the post");
|
||||||
delete (values[fieldName]);
|
delete (valuesToPost[fieldName]);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
values[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(values[fieldName]);
|
valuesToPost[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(valuesToPost[fieldName]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,10 +466,14 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(fieldMetaData.type === QFieldType.BLOB)
|
if(fieldMetaData.type === QFieldType.BLOB)
|
||||||
{
|
{
|
||||||
if(typeof values[fieldName] === "string")
|
if(typeof valuesToPost[fieldName] === "string")
|
||||||
{
|
{
|
||||||
console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`);
|
console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`);
|
||||||
delete(values[fieldName]);
|
delete(valuesToPost[fieldName]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
valuesToPost[fieldName] = values[fieldName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -473,7 +482,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
// todo - audit that it's a dupe
|
// todo - audit that it's a dupe
|
||||||
await qController
|
await qController
|
||||||
.update(tableName, props.id, values)
|
.update(tableName, props.id, valuesToPost)
|
||||||
.then((record) =>
|
.then((record) =>
|
||||||
{
|
{
|
||||||
if (props.isModal)
|
if (props.isModal)
|
||||||
@ -506,7 +515,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
await qController
|
await qController
|
||||||
.create(tableName, values)
|
.create(tableName, valuesToPost)
|
||||||
.then((record) =>
|
.then((record) =>
|
||||||
{
|
{
|
||||||
if (props.isModal)
|
if (props.isModal)
|
||||||
|
@ -25,6 +25,7 @@ import Icon from "@mui/material/Icon";
|
|||||||
import {ReactNode, useContext} from "react";
|
import {ReactNode, useContext} from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
@ -112,43 +113,32 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
|||||||
<Box mr={{xs: 0, xl: 8}}>
|
<Box mr={{xs: 0, xl: 8}}>
|
||||||
<MuiBreadcrumbs
|
<MuiBreadcrumbs
|
||||||
sx={{
|
sx={{
|
||||||
|
fontSize: "1.125rem",
|
||||||
|
fontWeight: "500",
|
||||||
|
color: colors.dark.main,
|
||||||
|
"& a": {
|
||||||
|
color: colors.gray.main
|
||||||
|
},
|
||||||
"& .MuiBreadcrumbs-separator": {
|
"& .MuiBreadcrumbs-separator": {
|
||||||
color: ({palette: {white, grey}}) => (light ? white.main : grey[600]),
|
fontSize: "1.125rem",
|
||||||
|
fontWeight: "500",
|
||||||
|
color: colors.dark.main
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<MDTypography
|
<Icon sx={{fontSize: "1.25rem!important"}}>{icon}</Icon>
|
||||||
component="span"
|
|
||||||
variant="body2"
|
|
||||||
color={light ? "white" : "dark"}
|
|
||||||
opacity={light ? 0.8 : 0.5}
|
|
||||||
sx={{lineHeight: 0}}
|
|
||||||
>
|
|
||||||
<Icon>{icon}</Icon>
|
|
||||||
</MDTypography>
|
|
||||||
</Link>
|
</Link>
|
||||||
{fullRoutes.map((fullRoute: string) => (
|
{fullRoutes.map((fullRoute: string) => (
|
||||||
<Link to={fullRoute} key={fullRoute}>
|
<Link to={fullRoute} key={fullRoute}>
|
||||||
<MDTypography
|
|
||||||
component="span"
|
|
||||||
variant="button"
|
|
||||||
fontWeight="regular"
|
|
||||||
textTransform="capitalize"
|
|
||||||
color={light ? "white" : "dark"}
|
|
||||||
opacity={light ? 0.8 : 0.5}
|
|
||||||
sx={{lineHeight: 0}}
|
|
||||||
>
|
|
||||||
{fullPathToLabel(fullRoute, fullRoute.replace(/.*\//, ""))}
|
{fullPathToLabel(fullRoute, fullRoute.replace(/.*\//, ""))}
|
||||||
</MDTypography>
|
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</MuiBreadcrumbs>
|
</MuiBreadcrumbs>
|
||||||
<MDTypography
|
<MDTypography
|
||||||
pt={1}
|
pt={1}
|
||||||
fontWeight="bold"
|
|
||||||
textTransform="capitalize"
|
textTransform="capitalize"
|
||||||
variant="h5"
|
variant="h3"
|
||||||
color={light ? "white" : "dark"}
|
color={light ? "white" : "dark"}
|
||||||
noWrap
|
noWrap
|
||||||
>
|
>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* 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 {Popper} from "@mui/material";
|
import {Popper, InputAdornment} from "@mui/material";
|
||||||
import AppBar from "@mui/material/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Badge from "@mui/material/Badge";
|
import Badge from "@mui/material/Badge";
|
||||||
@ -34,8 +34,8 @@ import React, {useContext, useEffect, useState} from "react";
|
|||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
|
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
|
||||||
import {navbar, navbarContainer, navbarIconButton, navbarRow,} from "qqq/components/horseshoe/Styles";
|
import {navbar, navbarContainer, navbarRow, navbarMobileMenu, recentlyViewedMenu,} from "qqq/components/horseshoe/Styles";
|
||||||
import {setTransparentNavbar, useMaterialUIController,} from "qqq/context";
|
import {setTransparentNavbar, useMaterialUIController, setMiniSidenav} from "qqq/context";
|
||||||
import HistoryUtils from "qqq/utils/HistoryUtils";
|
import HistoryUtils from "qqq/utils/HistoryUtils";
|
||||||
|
|
||||||
// Declaring prop types for NavBar
|
// Declaring prop types for NavBar
|
||||||
@ -57,7 +57,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
const [navbarType, setNavbarType] = useState<"fixed" | "absolute" | "relative" | "static" | "sticky">();
|
const [navbarType, setNavbarType] = useState<"fixed" | "absolute" | "relative" | "static" | "sticky">();
|
||||||
const [controller, dispatch] = useMaterialUIController();
|
const [controller, dispatch] = useMaterialUIController();
|
||||||
const {transparentNavbar, fixedNavbar, darkMode,} = controller;
|
const {miniSidenav, transparentNavbar, fixedNavbar, darkMode,} = controller;
|
||||||
const [openMenu, setOpenMenu] = useState<any>(false);
|
const [openMenu, setOpenMenu] = useState<any>(false);
|
||||||
const [history, setHistory] = useState<any>([] as HistoryEntry[]);
|
const [history, setHistory] = useState<any>([] as HistoryEntry[]);
|
||||||
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
|
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
|
||||||
@ -105,6 +105,8 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
return () => window.removeEventListener("scroll", handleTransparentNavbar);
|
return () => window.removeEventListener("scroll", handleTransparentNavbar);
|
||||||
}, [dispatch, fixedNavbar]);
|
}, [dispatch, fixedNavbar]);
|
||||||
|
|
||||||
|
const handleMiniSidenav = () => setMiniSidenav(dispatch, !miniSidenav);
|
||||||
|
|
||||||
const goToHistory = (path: string) =>
|
const goToHistory = (path: string) =>
|
||||||
{
|
{
|
||||||
navigate(path);
|
navigate(path);
|
||||||
@ -157,12 +159,20 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
options={history}
|
options={history}
|
||||||
autoHighlight
|
autoHighlight
|
||||||
blurOnSelect
|
blurOnSelect
|
||||||
style={{width: "200px"}}
|
style={{width: "16rem"}}
|
||||||
onOpen={handleHistoryOnOpen}
|
onOpen={handleHistoryOnOpen}
|
||||||
onChange={handleAutocompleteOnChange}
|
onChange={handleAutocompleteOnChange}
|
||||||
PopperComponent={CustomPopper}
|
PopperComponent={CustomPopper}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" />}
|
sx={recentlyViewedMenu}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<Icon sx={{position: "relative", right: "-1rem"}}>keyboard_arrow_down</Icon>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}} />}
|
||||||
renderOption={(props, option: HistoryEntry) => (
|
renderOption={(props, option: HistoryEntry) => (
|
||||||
<Box {...props} component="li" key={option.id} sx={{width: "auto"}}>
|
<Box {...props} component="li" key={option.id} sx={{width: "auto"}}>
|
||||||
<Box sx={{width: "auto", px: "8px", whiteSpace: "overflow"}} key={option.id}>
|
<Box sx={{width: "auto", px: "8px", whiteSpace: "overflow"}} key={option.id}>
|
||||||
@ -175,22 +185,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Render the notifications menu
|
|
||||||
const renderMenu = () => (
|
|
||||||
<Menu
|
|
||||||
anchorEl={openMenu}
|
|
||||||
anchorReference={null}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
open={Boolean(openMenu)}
|
|
||||||
onClose={handleCloseMenu}
|
|
||||||
sx={{mt: 2}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Styles for the navbar icons
|
// Styles for the navbar icons
|
||||||
const iconsStyle = ({
|
const iconsStyle = ({
|
||||||
palette: {dark, white, text},
|
palette: {dark, white, text},
|
||||||
@ -240,26 +234,22 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
>
|
>
|
||||||
<Toolbar sx={navbarContainer}>
|
<Toolbar sx={navbarContainer}>
|
||||||
<Box color="inherit" mb={{xs: 1, md: 0}} sx={(theme) => navbarRow(theme, {isMini})}>
|
<Box color="inherit" mb={{xs: 1, md: 0}} sx={(theme) => navbarRow(theme, {isMini})}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disableRipple
|
||||||
|
color="inherit"
|
||||||
|
sx={navbarMobileMenu}
|
||||||
|
onClick={handleMiniSidenav}
|
||||||
|
>
|
||||||
|
<Icon sx={iconsStyle} fontSize="large">menu</Icon>
|
||||||
|
</IconButton>
|
||||||
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
||||||
</Box>
|
</Box>
|
||||||
{isMini ? null : (
|
{isMini ? null : (
|
||||||
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
||||||
<Box pr={1}>
|
<Box pr={0} mr={-2} mt={-4}>
|
||||||
{renderHistory()}
|
{renderHistory()}
|
||||||
</Box>
|
</Box>
|
||||||
<Box color={light ? "white" : "inherit"}>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="inherit"
|
|
||||||
sx={navbarIconButton}
|
|
||||||
onClick={handleOpenMenu}
|
|
||||||
>
|
|
||||||
<Badge badgeContent={0} color="error" variant="dot">
|
|
||||||
<Icon sx={iconsStyle}>notifications</Icon>
|
|
||||||
</Badge>
|
|
||||||
</IconButton>
|
|
||||||
{renderMenu()}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Theme} from "@mui/material/styles";
|
import {Theme} from "@mui/material/styles";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
|
|
||||||
function navbar(theme: Theme | any, ownerState: any)
|
function navbar(theme: Theme | any, ownerState: any)
|
||||||
{
|
{
|
||||||
@ -110,11 +111,10 @@ const navbarContainer = ({breakpoints}: Theme): any => ({
|
|||||||
const navbarRow = ({breakpoints}: Theme, {isMini}: any) => ({
|
const navbarRow = ({breakpoints}: Theme, {isMini}: any) => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
||||||
[breakpoints.up("md")]: {
|
[breakpoints.up("md")]: {
|
||||||
justifyContent: isMini ? "space-between" : "stretch",
|
justifyContent: "stretch",
|
||||||
width: isMini ? "100%" : "max-content",
|
width: isMini ? "100%" : "max-content",
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -146,12 +146,37 @@ const navbarDesktopMenu = ({breakpoints}: Theme) => ({
|
|||||||
display: "none !important",
|
display: "none !important",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
|
||||||
[breakpoints.up("xl")]: {
|
[breakpoints.down("sm")]: {
|
||||||
display: "inline-block !important",
|
display: "inline-block !important",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const recentlyViewedMenu = ({breakpoints}: Theme) => ({
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: colors.gray.main,
|
||||||
|
fontWeight: "500",
|
||||||
|
fontSize: "1rem"
|
||||||
|
},
|
||||||
|
"& .MuiInputAdornment-root": {
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
color: colors.gray.main,
|
||||||
|
fontSize: "1rem"
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: "0",
|
||||||
|
padding: "0"
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: "0"
|
||||||
|
},
|
||||||
|
display: "block",
|
||||||
|
[breakpoints.down("md")]: {
|
||||||
|
display: "none !important",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const navbarMobileMenu = ({breakpoints}: Theme) => ({
|
const navbarMobileMenu = ({breakpoints}: Theme) => ({
|
||||||
|
left: "-0.75rem",
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
lineHeight: 0,
|
lineHeight: 0,
|
||||||
|
|
||||||
@ -167,4 +192,5 @@ export {
|
|||||||
navbarIconButton,
|
navbarIconButton,
|
||||||
navbarDesktopMenu,
|
navbarDesktopMenu,
|
||||||
navbarMobileMenu,
|
navbarMobileMenu,
|
||||||
|
recentlyViewedMenu
|
||||||
};
|
};
|
||||||
|
@ -27,7 +27,7 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
|||||||
const {palette, boxShadows, transitions, breakpoints, functions} = theme;
|
const {palette, boxShadows, transitions, breakpoints, functions} = theme;
|
||||||
const {transparentSidenav, whiteSidenav, miniSidenav, darkMode} = ownerState;
|
const {transparentSidenav, whiteSidenav, miniSidenav, darkMode} = ownerState;
|
||||||
|
|
||||||
const sidebarWidth = 250;
|
const sidebarWidth = 245;
|
||||||
const {transparent, gradients, white, background} = palette;
|
const {transparent, gradients, white, background} = palette;
|
||||||
const {xxl} = boxShadows;
|
const {xxl} = boxShadows;
|
||||||
const {pxToRem, linearGradient} = functions;
|
const {pxToRem, linearGradient} = functions;
|
||||||
@ -94,6 +94,9 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
|||||||
"& .MuiDrawer-paper": {
|
"& .MuiDrawer-paper": {
|
||||||
boxShadow: xxl,
|
boxShadow: xxl,
|
||||||
border: "none",
|
border: "none",
|
||||||
|
margin: "0",
|
||||||
|
borderRadius: "0",
|
||||||
|
height: "100%",
|
||||||
|
|
||||||
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
||||||
},
|
},
|
||||||
|
@ -65,6 +65,7 @@ function collapseItem(theme: Theme, ownerState: any)
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
boxShadow: active && !whiteSidenav && !darkMode && !transparentSidenav ? md : "none",
|
boxShadow: active && !whiteSidenav && !darkMode && !transparentSidenav ? md : "none",
|
||||||
[breakpoints.up("xl")]: {
|
[breakpoints.up("xl")]: {
|
||||||
transition: transitions.create(["box-shadow", "background-color"], {
|
transition: transitions.create(["box-shadow", "background-color"], {
|
||||||
|
@ -149,7 +149,7 @@ interface Types {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseProperties = {
|
const baseProperties = {
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
fontWeightLighter: 100,
|
fontWeightLighter: 100,
|
||||||
fontWeightLight: 300,
|
fontWeightLight: 300,
|
||||||
fontWeightRegular: 400,
|
fontWeightRegular: 400,
|
||||||
|
@ -153,7 +153,7 @@ interface Types
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseProperties = {
|
const baseProperties = {
|
||||||
fontFamily: "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif",
|
fontFamily: "\"SF Pro Display\", \"Roboto\", \"Helvetica\", \"Arial\", sans-serif",
|
||||||
fontWeightLighter: 100,
|
fontWeightLighter: 100,
|
||||||
fontWeightLight: 300,
|
fontWeightLight: 300,
|
||||||
fontWeightRegular: 400,
|
fontWeightRegular: 400,
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
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 {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
@ -45,8 +46,10 @@ interface Props
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
metaData: QInstance;
|
metaData: QInstance;
|
||||||
tableMetaData: QTableMetaData;
|
tableMetaData: QTableMetaData;
|
||||||
|
tableVariant?: QTableVariant;
|
||||||
closeHandler: () => void;
|
closeHandler: () => void;
|
||||||
mayClose: boolean;
|
mayClose: boolean;
|
||||||
|
subHeader?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
GotoRecordDialog.defaultProps = {
|
GotoRecordDialog.defaultProps = {
|
||||||
@ -155,15 +158,17 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
setError("");
|
setError("");
|
||||||
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10);
|
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10);
|
||||||
const queryResult = await qController.query(props.tableMetaData.name, filter)
|
try
|
||||||
if(queryResult.length == 0)
|
{
|
||||||
|
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant)
|
||||||
|
if (queryResult.length == 0)
|
||||||
{
|
{
|
||||||
setError("Record not found.");
|
setError("Record not found.");
|
||||||
setTimeout(() => setError(""), 3000);
|
setTimeout(() => setError(""), 3000);
|
||||||
}
|
}
|
||||||
else if(queryResult.length == 1)
|
else if (queryResult.length == 1)
|
||||||
{
|
{
|
||||||
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${queryResult[0].values.get(props.tableMetaData.primaryKeyField)}`);
|
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -172,6 +177,13 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
setTimeout(() => setError(""), 3000);
|
setTimeout(() => setError(""), 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
setError(`Error: ${(e && e.message) ? e.message : e}`);
|
||||||
|
setTimeout(() => setError(""), 6000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(props.tableMetaData)
|
if(props.tableMetaData)
|
||||||
{
|
{
|
||||||
@ -184,7 +196,9 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
||||||
<DialogTitle>Go To...</DialogTitle>
|
<DialogTitle>Go To...</DialogTitle>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
{props.subHeader}
|
||||||
{
|
{
|
||||||
fields.map((field, index) =>
|
fields.map((field, index) =>
|
||||||
(
|
(
|
||||||
@ -237,9 +251,11 @@ interface GotoRecordButtonProps
|
|||||||
{
|
{
|
||||||
metaData: QInstance;
|
metaData: QInstance;
|
||||||
tableMetaData: QTableMetaData;
|
tableMetaData: QTableMetaData;
|
||||||
|
tableVariant?: QTableVariant;
|
||||||
autoOpen?: boolean;
|
autoOpen?: boolean;
|
||||||
buttonVisible?: boolean;
|
buttonVisible?: boolean;
|
||||||
mayClose?: boolean;
|
mayClose?: boolean;
|
||||||
|
subHeader?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
GotoRecordButton.defaultProps = {
|
GotoRecordButton.defaultProps = {
|
||||||
@ -268,7 +284,7 @@ export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element
|
|||||||
{
|
{
|
||||||
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} >Go To...</Button>
|
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} >Go To...</Button>
|
||||||
}
|
}
|
||||||
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} />
|
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} tableVariant={props.tableVariant} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} subHeader={props.subHeader} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -28,11 +28,12 @@ interface TabPanelProps
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
index: number;
|
index: number;
|
||||||
value: number;
|
value: number;
|
||||||
|
style?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TabPanel(props: TabPanelProps)
|
export default function TabPanel(props: TabPanelProps)
|
||||||
{
|
{
|
||||||
const {children, value, index, ...other} = props;
|
const {children, value, index, style, ...other} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -40,6 +41,7 @@ export default function TabPanel(props: TabPanelProps)
|
|||||||
hidden={value !== index}
|
hidden={value !== index}
|
||||||
id={`simple-tabpanel-${index}`}
|
id={`simple-tabpanel-${index}`}
|
||||||
aria-labelledby={`simple-tab-${index}`}
|
aria-labelledby={`simple-tab-${index}`}
|
||||||
|
style={style}
|
||||||
{...other}
|
{...other}
|
||||||
>
|
>
|
||||||
{value === index && (
|
{value === index && (
|
||||||
|
@ -155,7 +155,7 @@ function ValidationReview({
|
|||||||
"false",
|
"false",
|
||||||
"Skip Validation. Submit the records for immediate processing", (
|
"Skip Validation. Submit the records for immediate processing", (
|
||||||
<div>
|
<div>
|
||||||
If you choose this option, the records input records will immediately be processed.
|
If you choose this option, the input records will immediately be processed.
|
||||||
You will be told how many records were successfully processed, and which ones had issues after the processing is completed.
|
You will be told how many records were successfully processed, and which ones had issues after the processing is completed.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
@ -22,11 +22,13 @@ import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Q
|
|||||||
import {Skeleton} from "@mui/material";
|
import {Skeleton} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Tab from "@mui/material/Tab";
|
||||||
|
import Tabs from "@mui/material/Tabs";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {useContext, useEffect, useReducer, useState} from "react";
|
import React, {useContext, useEffect, useReducer, useState} from "react";
|
||||||
import {useLocation} from "react-router-dom";
|
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
import TabPanel from "qqq/components/misc/TabPanel";
|
||||||
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
||||||
import HorizontalBarChart from "qqq/components/widgets/charts/barchart/HorizontalBarChart";
|
import HorizontalBarChart from "qqq/components/widgets/charts/barchart/HorizontalBarChart";
|
||||||
import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLineChart";
|
import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLineChart";
|
||||||
@ -44,7 +46,7 @@ import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
|||||||
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
||||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||||
import Widget, {WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
|
import Widget, {HeaderIcon, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, LabelComponent} from "qqq/components/widgets/Widget";
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import TableWidget from "./tables/TableWidget";
|
import TableWidget from "./tables/TableWidget";
|
||||||
@ -58,9 +60,10 @@ interface Props
|
|||||||
tableName?: string;
|
tableName?: string;
|
||||||
entityPrimaryKey?: string;
|
entityPrimaryKey?: string;
|
||||||
omitWrappingGridContainer: boolean;
|
omitWrappingGridContainer: boolean;
|
||||||
areChildren?: boolean
|
areChildren?: boolean;
|
||||||
childUrlParams?: string
|
childUrlParams?: string;
|
||||||
parentWidgetMetaData?: QWidgetMetaData
|
parentWidgetMetaData?: QWidgetMetaData;
|
||||||
|
wrapWidgetsInTabPanels: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
DashboardWidgets.defaultProps = {
|
DashboardWidgets.defaultProps = {
|
||||||
@ -70,12 +73,12 @@ DashboardWidgets.defaultProps = {
|
|||||||
omitWrappingGridContainer: false,
|
omitWrappingGridContainer: false,
|
||||||
areChildren: false,
|
areChildren: false,
|
||||||
childUrlParams: "",
|
childUrlParams: "",
|
||||||
parentWidgetMetaData: null
|
parentWidgetMetaData: null,
|
||||||
|
wrapWidgetsInTabPanels: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData}: Props): JSX.Element
|
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const location = useLocation();
|
|
||||||
const [widgetData, setWidgetData] = useState([] as any[]);
|
const [widgetData, setWidgetData] = useState([] as any[]);
|
||||||
const [widgetCounter, setWidgetCounter] = useState(0);
|
const [widgetCounter, setWidgetCounter] = useState(0);
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
@ -84,6 +87,24 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
|
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
|
||||||
const {accentColor} = useContext(QContext);
|
const {accentColor} = useContext(QContext);
|
||||||
|
|
||||||
|
let initialSelectedTab = 0;
|
||||||
|
let selectedTabKey: string = null;
|
||||||
|
if(parentWidgetMetaData && wrapWidgetsInTabPanels)
|
||||||
|
{
|
||||||
|
selectedTabKey = `qqq.widgets.selectedTabs.${parentWidgetMetaData.name}`
|
||||||
|
if (localStorage.getItem(selectedTabKey))
|
||||||
|
{
|
||||||
|
initialSelectedTab = Number(localStorage.getItem(selectedTabKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [selectedTab, setSelectedTab] = useState(initialSelectedTab);
|
||||||
|
|
||||||
|
const changeTab = (newValue: number) =>
|
||||||
|
{
|
||||||
|
setSelectedTab(newValue);
|
||||||
|
localStorage.setItem(selectedTabKey, String(newValue));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setWidgetData([]);
|
setWidgetData([]);
|
||||||
@ -102,15 +123,15 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
||||||
setWidgetData(widgetData);
|
setWidgetData(widgetData);
|
||||||
setWidgetCounter(widgetCounter + 1);
|
setWidgetCounter(widgetCounter + 1);
|
||||||
if(widgetData[i])
|
if (widgetData[i])
|
||||||
{
|
{
|
||||||
widgetData[i]["errorLoading"] = false;
|
widgetData[i]["errorLoading"] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e)
|
catch (e)
|
||||||
{
|
{
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if(widgetData[i])
|
if (widgetData[i])
|
||||||
{
|
{
|
||||||
widgetData[i]["errorLoading"] = true;
|
widgetData[i]["errorLoading"] = true;
|
||||||
}
|
}
|
||||||
@ -123,7 +144,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
|
|
||||||
const reloadWidget = async (index: number, data: string) =>
|
const reloadWidget = async (index: number, data: string) =>
|
||||||
{
|
{
|
||||||
(async() =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
||||||
setCurrentUrlParams(urlParams);
|
setCurrentUrlParams(urlParams);
|
||||||
@ -140,7 +161,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetData[index]["errorLoading"] = false;
|
widgetData[index]["errorLoading"] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e)
|
catch (e)
|
||||||
{
|
{
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (widgetData[index])
|
if (widgetData[index])
|
||||||
@ -151,7 +172,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
|
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
})();
|
})();
|
||||||
}
|
};
|
||||||
|
|
||||||
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
||||||
{
|
{
|
||||||
@ -178,36 +199,36 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(entityPrimaryKey)
|
if (entityPrimaryKey)
|
||||||
{
|
{
|
||||||
paramMap.set("id", entityPrimaryKey);
|
paramMap.set("id", entityPrimaryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(tableName)
|
if (tableName)
|
||||||
{
|
{
|
||||||
paramMap.set("tableName", tableName);
|
paramMap.set("tableName", tableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(extraParams)
|
if (extraParams)
|
||||||
{
|
{
|
||||||
let pairs = extraParams.split("&");
|
let pairs = extraParams.split("&");
|
||||||
for (let i = 0; i < pairs.length; i++)
|
for (let i = 0; i < pairs.length; i++)
|
||||||
{
|
{
|
||||||
let nameValue = pairs[i].split("=");
|
let nameValue = pairs[i].split("=");
|
||||||
if(nameValue.length == 2)
|
if (nameValue.length == 2)
|
||||||
{
|
{
|
||||||
paramMap.set(nameValue[0], nameValue[1]);
|
paramMap.set(nameValue[0], nameValue[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(childUrlParams)
|
if (childUrlParams)
|
||||||
{
|
{
|
||||||
let pairs = childUrlParams.split("&");
|
let pairs = childUrlParams.split("&");
|
||||||
for (let i = 0; i < pairs.length; i++)
|
for (let i = 0; i < pairs.length; i++)
|
||||||
{
|
{
|
||||||
let nameValue = pairs[i].split("=");
|
let nameValue = pairs[i].split("=");
|
||||||
if(nameValue.length == 2)
|
if (nameValue.length == 2)
|
||||||
{
|
{
|
||||||
paramMap.set(nameValue[0], nameValue[1]);
|
paramMap.set(nameValue[0], nameValue[1]);
|
||||||
}
|
}
|
||||||
@ -227,6 +248,16 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
|
|
||||||
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
|
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
|
||||||
{
|
{
|
||||||
|
const labelAdditionalComponentsRight: LabelComponent[] = [];
|
||||||
|
if (widgetMetaData && widgetMetaData.icons)
|
||||||
|
{
|
||||||
|
const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard");
|
||||||
|
if (topRightInsideCardIcon)
|
||||||
|
{
|
||||||
|
labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.path, topRightInsideCardIcon.color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%"}}>
|
<Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%"}}>
|
||||||
{
|
{
|
||||||
@ -238,7 +269,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetIndex={i}
|
widgetIndex={i}
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
data={widgetData[i]}
|
data={widgetData[i]}
|
||||||
reloadWidgetCallback={reloadWidget}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
storeDropdownSelections={widgetMetaData.storeDropdownSelections}
|
storeDropdownSelections={widgetMetaData.storeDropdownSelections}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -270,8 +301,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
>
|
>
|
||||||
<StackedBarChart data={widgetData[i]?.chartData}/>
|
<StackedBarChart data={widgetData[i]?.chartData} chartSubheaderData={widgetData[i]?.chartSubheaderData} />
|
||||||
</Widget>
|
</Widget>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -311,7 +343,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
>
|
>
|
||||||
<Box px={3} pt={0} pb={2}>
|
<Box>
|
||||||
<MDTypography component="div" variant="button" color="text" fontWeight="light">
|
<MDTypography component="div" variant="button" color="text" fontWeight="light">
|
||||||
{
|
{
|
||||||
widgetData && widgetData[i] && widgetData[i].html ? (
|
widgetData && widgetData[i] && widgetData[i].html ? (
|
||||||
@ -381,10 +413,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<PieChart
|
<PieChart
|
||||||
chartData={widgetData[i]?.chartData}
|
chartData={widgetData[i]?.chartData}
|
||||||
|
chartSubheaderData={widgetData[i]?.chartSubheaderData}
|
||||||
description={widgetData[i]?.description}
|
description={widgetData[i]?.description}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -461,32 +495,76 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if(wrapWidgetsInTabPanels)
|
||||||
|
{
|
||||||
|
omitWrappingGridContainer = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body: JSX.Element =
|
const body: JSX.Element =
|
||||||
(
|
(
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
widgetMetaDataList.map((widgetMetaData, i) => (
|
widgetMetaDataList.map((widgetMetaData, i) =>
|
||||||
omitWrappingGridContainer
|
{
|
||||||
? widgetMetaData && renderWidget(widgetMetaData, i)
|
let renderedWidget = widgetMetaData ? renderWidget(widgetMetaData, i) : (<></>);
|
||||||
:
|
|
||||||
widgetMetaData && <Grid id={widgetMetaData.name} key={`${widgetMetaData.name}-${i}`} item lg={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
if (!omitWrappingGridContainer)
|
||||||
{renderWidget(widgetMetaData, i)}
|
{
|
||||||
</Grid>
|
// @ts-ignore
|
||||||
))
|
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||||
|
{renderedWidget}
|
||||||
|
</Grid>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrapWidgetsInTabPanels)
|
||||||
|
{
|
||||||
|
renderedWidget = (<TabPanel index={i} value={selectedTab} style={{
|
||||||
|
padding: 0,
|
||||||
|
margin: "-1rem",
|
||||||
|
marginBottom: "-3.5rem",
|
||||||
|
width: "calc(100% + 2rem)"
|
||||||
|
}}>
|
||||||
|
{renderedWidget}
|
||||||
|
</TabPanel>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<React.Fragment key={`${widgetMetaData.name}-${i}`}>{renderedWidget}</React.Fragment>)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tabs = widgetMetaDataList && wrapWidgetsInTabPanels ?
|
||||||
|
<Tabs
|
||||||
|
sx={{m: 0, mb: 1.5, ml: -2, mr: -2, mt: -3,
|
||||||
|
"& .MuiTabs-scroller": {
|
||||||
|
ml: 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={selectedTab}
|
||||||
|
onChange={(event, newValue) => changeTab(newValue)}
|
||||||
|
variant="standard"
|
||||||
|
>
|
||||||
|
{widgetMetaDataList.map((widgetMetaData, i) => (
|
||||||
|
<Tab key={widgetMetaData.name} label={widgetMetaData.label} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
: <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
widgetCount > 0 ? (
|
widgetCount > 0 ? (
|
||||||
omitWrappingGridContainer ? body :
|
<>
|
||||||
(
|
{tabs}
|
||||||
<Grid container spacing={3} pb={4}>
|
{
|
||||||
|
omitWrappingGridContainer ? body : (
|
||||||
|
<Grid container spacing={2.5}>
|
||||||
{body}
|
{body}
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
) : null
|
) : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ export interface ParentWidgetData
|
|||||||
dropdownNeedsSelectedText?: string;
|
dropdownNeedsSelectedText?: string;
|
||||||
storeDropdownSelections?: boolean;
|
storeDropdownSelections?: boolean;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
layoutType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ interface Props
|
|||||||
widgetMetaData?: QWidgetMetaData;
|
widgetMetaData?: QWidgetMetaData;
|
||||||
widgetIndex: number;
|
widgetIndex: number;
|
||||||
data: ParentWidgetData;
|
data: ParentWidgetData;
|
||||||
reloadWidgetCallback?: (widgetIndex: number, params: string) => void;
|
reloadWidgetCallback?: (params: string) => void;
|
||||||
entityPrimaryKey?: string;
|
entityPrimaryKey?: string;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
storeDropdownSelections?: boolean;
|
storeDropdownSelections?: boolean;
|
||||||
@ -91,18 +92,29 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
|||||||
}
|
}
|
||||||
}, [qInstance, data, childUrlParams]);
|
}, [qInstance, data, childUrlParams]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
setChildUrlParams(urlParams)
|
||||||
|
}, [urlParams]);
|
||||||
|
|
||||||
const parentReloadWidgetCallback = (data: string) =>
|
const parentReloadWidgetCallback = (data: string) =>
|
||||||
{
|
{
|
||||||
setChildUrlParams(data);
|
setChildUrlParams(data);
|
||||||
reloadWidgetCallback(widgetIndex, data);
|
reloadWidgetCallback(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if this parent widget is in card form, and its children are too, then we need some px //
|
// if this parent widget is in card form, and its children are too, then we need some px //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const px = (widgetMetaData && widgetMetaData.isCard && widgets && widgets[0] && widgets[0].isCard) ? 3 : 0;
|
const parentIsCard = widgetMetaData && widgetMetaData.isCard;
|
||||||
|
const childrenAreCards = widgetMetaData && widgets && widgets[0] && widgets[0].isCard;
|
||||||
|
const px = (parentIsCard && childrenAreCards) ? 3 : 0;
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if this is a parent, which is not a card, then we need to omit the padding, i think, on the Widget that ultimately gets rendered //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const omitPadding = !parentIsCard;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return (
|
return (
|
||||||
qInstance && data ? (
|
qInstance && data ? (
|
||||||
<Widget
|
<Widget
|
||||||
@ -110,9 +122,10 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
|||||||
widgetData={data}
|
widgetData={data}
|
||||||
storeDropdownSelections={storeDropdownSelections}
|
storeDropdownSelections={storeDropdownSelections}
|
||||||
reloadWidgetCallback={parentReloadWidgetCallback}
|
reloadWidgetCallback={parentReloadWidgetCallback}
|
||||||
|
omitPadding={omitPadding}
|
||||||
>
|
>
|
||||||
<Box sx={{height: "100%", width: "100%"}} px={px}>
|
<Box sx={{height: "100%", width: "100%"}} px={px}>
|
||||||
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData}/>
|
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType == "TABS"}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Widget>
|
</Widget>
|
||||||
) : null
|
) : null
|
||||||
|
@ -25,14 +25,13 @@ 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 Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import LinearProgress from "@mui/material/LinearProgress";
|
|
||||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {Link, NavigateFunction, useNavigate} from "react-router-dom";
|
import {NavigateFunction, useNavigate} from "react-router-dom";
|
||||||
import colors from "qqq/components/legacy/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
|
||||||
|
|
||||||
export interface WidgetData
|
export interface WidgetData
|
||||||
{
|
{
|
||||||
@ -43,9 +42,11 @@ export interface WidgetData
|
|||||||
id: string,
|
id: string,
|
||||||
label: string
|
label: string
|
||||||
}[][];
|
}[][];
|
||||||
|
dropdownDefaultValueList?: string[];
|
||||||
dropdownNeedsSelectedText?: string;
|
dropdownNeedsSelectedText?: string;
|
||||||
hasPermission?: boolean;
|
hasPermission?: boolean;
|
||||||
errorLoading?: boolean;
|
errorLoading?: boolean;
|
||||||
|
|
||||||
[other: string]: any;
|
[other: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ export interface WidgetData
|
|||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
labelAdditionalComponentsLeft: LabelComponent[];
|
labelAdditionalComponentsLeft: LabelComponent[];
|
||||||
|
labelAdditionalElementsLeft: JSX.Element[];
|
||||||
labelAdditionalComponentsRight: LabelComponent[];
|
labelAdditionalComponentsRight: LabelComponent[];
|
||||||
widgetMetaData?: QWidgetMetaData;
|
widgetMetaData?: QWidgetMetaData;
|
||||||
widgetData?: WidgetData;
|
widgetData?: WidgetData;
|
||||||
@ -62,6 +64,7 @@ interface Props
|
|||||||
isChild?: boolean;
|
isChild?: boolean;
|
||||||
footerHTML?: string;
|
footerHTML?: string;
|
||||||
storeDropdownSelections?: boolean;
|
storeDropdownSelections?: boolean;
|
||||||
|
omitPadding: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget.defaultProps = {
|
Widget.defaultProps = {
|
||||||
@ -70,7 +73,9 @@ Widget.defaultProps = {
|
|||||||
widgetMetaData: {},
|
widgetMetaData: {},
|
||||||
widgetData: {},
|
widgetData: {},
|
||||||
labelAdditionalComponentsLeft: [],
|
labelAdditionalComponentsLeft: [],
|
||||||
|
labelAdditionalElementsLeft: [],
|
||||||
labelAdditionalComponentsRight: [],
|
labelAdditionalComponentsRight: [],
|
||||||
|
omitPadding: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -88,34 +93,56 @@ export class LabelComponent
|
|||||||
{
|
{
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
return (<div>Unsupported component type</div>)
|
return (<div>Unsupported component type</div>);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export class HeaderLink extends LabelComponent
|
export class HeaderIcon extends LabelComponent
|
||||||
{
|
{
|
||||||
label: string;
|
iconName: string;
|
||||||
to: string
|
iconPath: string;
|
||||||
|
color: string;
|
||||||
|
coloredBG: boolean;
|
||||||
|
|
||||||
constructor(label: string, to: string)
|
iconColor: string;
|
||||||
|
bgColor: string;
|
||||||
|
|
||||||
|
constructor(iconName: string, iconPath: string, color: string, coloredBG: boolean = true)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.label = label;
|
this.iconName = iconName;
|
||||||
this.to = to;
|
this.iconPath = iconPath;
|
||||||
|
this.color = color;
|
||||||
|
this.coloredBG = coloredBG;
|
||||||
|
|
||||||
|
this.iconColor = this.coloredBG ? "#FFFFFF" : this.color;
|
||||||
|
this.bgColor = this.coloredBG ? this.color : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
return (
|
const styles = {
|
||||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
width: "1.75rem",
|
||||||
{this.to ? <Link to={this.to}>{this.label}</Link> : null}
|
height: "1.75rem",
|
||||||
</Typography>
|
color: this.iconColor,
|
||||||
);
|
backgroundColor: this.bgColor,
|
||||||
|
borderRadius: "0.25rem"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.iconPath)
|
||||||
|
{
|
||||||
|
return (<Box sx={{textAlign: "center", ...styles}}><img src={this.iconPath} width="16" height="16" /></Box>);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (<Icon sx={{padding: "0.25rem", ...styles}} fontSize="small">{this.iconName}</Icon>);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -141,8 +168,8 @@ export class AddNewRecordButton extends LabelComponent
|
|||||||
|
|
||||||
openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
|
openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
|
||||||
{
|
{
|
||||||
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
|
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
@ -151,35 +178,7 @@ export class AddNewRecordButton extends LabelComponent
|
|||||||
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
export class ExportDataButton extends LabelComponent
|
|
||||||
{
|
|
||||||
callbackToExport: any;
|
|
||||||
tooltipTitle: string;
|
|
||||||
isDisabled: boolean;
|
|
||||||
|
|
||||||
constructor(callbackToExport: any, isDisabled = false, tooltipTitle: string = "Export")
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.callbackToExport = callbackToExport;
|
|
||||||
this.isDisabled = isDisabled;
|
|
||||||
this.tooltipTitle = tooltipTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
|
||||||
<Tooltip title={this.tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callbackToExport()} disabled={this.isDisabled}><Icon>save_alt</Icon></Button></Tooltip>
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -189,45 +188,115 @@ export class ExportDataButton extends LabelComponent
|
|||||||
export class Dropdown extends LabelComponent
|
export class Dropdown extends LabelComponent
|
||||||
{
|
{
|
||||||
label: string;
|
label: string;
|
||||||
|
dropdownMetaData: any;
|
||||||
options: DropdownOption[];
|
options: DropdownOption[];
|
||||||
|
dropdownDefaultValue?: string;
|
||||||
dropdownName: string;
|
dropdownName: string;
|
||||||
onChangeCallback: any;
|
onChangeCallback: any;
|
||||||
|
|
||||||
constructor(label: string, options: DropdownOption[], dropdownName: string, onChangeCallback: any)
|
constructor(label: string, dropdownMetaData: any, options: DropdownOption[], dropdownDefaultValue: string, dropdownName: string, onChangeCallback: any)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.label = label;
|
this.label = label;
|
||||||
|
this.dropdownMetaData = dropdownMetaData;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
this.dropdownDefaultValue = dropdownDefaultValue;
|
||||||
this.dropdownName = dropdownName;
|
this.dropdownName = dropdownName;
|
||||||
this.onChangeCallback = onChangeCallback;
|
this.onChangeCallback = onChangeCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
|
const label = `Select ${this.label}`;
|
||||||
let defaultValue = null;
|
let defaultValue = null;
|
||||||
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${this.dropdownName}`;
|
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${this.dropdownName}`;
|
||||||
if (args.widgetProps.storeDropdownSelections)
|
if (args.widgetProps.storeDropdownSelections)
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
// originally we used the full object from localStorage - but - in case the label //
|
||||||
defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
|
// changed since it was stored, we'll instead just find the option by id (or in case that //
|
||||||
|
// option isn't available anymore, then we'll select nothing instead of a missing value //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const localStorageOption = JSON.parse(localStorage.getItem(localStorageKey));
|
||||||
|
if(localStorageOption)
|
||||||
|
{
|
||||||
|
const id = localStorageOption.id;
|
||||||
|
for (let i = 0; i < this.options.length; i++)
|
||||||
|
{
|
||||||
|
if (this.options[i].id == id)
|
||||||
|
{
|
||||||
|
defaultValue = this.options[i]
|
||||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there wasn't a value selected, but there is a default from the backend, then use it. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (defaultValue == null && this.dropdownDefaultValue != null)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < this.options.length; i++)
|
||||||
|
{
|
||||||
|
if(this.options[i].id == this.dropdownDefaultValue)
|
||||||
|
{
|
||||||
|
defaultValue = this.options[i];
|
||||||
|
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||||
|
|
||||||
|
if (args.widgetProps.storeDropdownSelections)
|
||||||
|
{
|
||||||
|
localStorage.setItem(localStorageKey, JSON.stringify(defaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onChangeCallback(label, defaultValue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there's a 'label for null value' (and no default from the backend), //
|
||||||
|
// then add that as an option (and select it if nothing else was selected) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
let options = this.options;
|
||||||
|
if (this.dropdownMetaData.labelForNullValue && !this.dropdownDefaultValue)
|
||||||
|
{
|
||||||
|
const nullOption = {id: null as string, label: this.dropdownMetaData.labelForNullValue};
|
||||||
|
options = [nullOption, ...this.options];
|
||||||
|
|
||||||
|
if (!defaultValue)
|
||||||
|
{
|
||||||
|
defaultValue = nullOption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box my={2} sx={{float: "right"}}>
|
<Box mb={2} sx={{float: "right"}}>
|
||||||
<DropdownMenu
|
<WidgetDropdownMenu
|
||||||
name={this.dropdownName}
|
name={this.dropdownName}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
sx={{width: 200, marginLeft: "15px"}}
|
sx={{marginLeft: "1rem"}}
|
||||||
label={`Select ${this.label}`}
|
label={label}
|
||||||
dropdownOptions={this.options}
|
startIcon={this.dropdownMetaData.startIconName}
|
||||||
|
allowBackAndForth={this.dropdownMetaData.allowBackAndForth}
|
||||||
|
backAndForthInverted={this.dropdownMetaData.backAndForthInverted}
|
||||||
|
disableClearable={this.dropdownMetaData.disableClearable}
|
||||||
|
dropdownOptions={options}
|
||||||
onChangeCallback={this.onChangeCallback}
|
onChangeCallback={this.onChangeCallback}
|
||||||
|
width={this.dropdownMetaData.width ?? 225}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -247,11 +316,11 @@ export class ReloadControl extends LabelComponent
|
|||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.175rem">
|
||||||
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip>
|
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon sx={{color: colors.gray.main, fontSize: 1.125}}>refresh</Icon></Button></Tooltip>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -333,7 +402,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) =>
|
props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) =>
|
||||||
{
|
{
|
||||||
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
|
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
|
||||||
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], dropdownData, props.widgetData.dropdownNameList[index], handleDataChange));
|
let defaultValue = null;
|
||||||
|
if(props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
|
||||||
|
{
|
||||||
|
defaultValue = props.widgetData.dropdownDefaultValueList[index];
|
||||||
|
}
|
||||||
|
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
|
||||||
});
|
});
|
||||||
setLabelComponentsRight(updatedStateLabelComponentsRight);
|
setLabelComponentsRight(updatedStateLabelComponentsRight);
|
||||||
}
|
}
|
||||||
@ -372,7 +446,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
|
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
{
|
{
|
||||||
throw(`Could not find table name for label ${tableName}`);
|
throw (`Could not find table name for label ${tableName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
dropdownData[index] = (changedData) ? changedData.id : null;
|
dropdownData[index] = (changedData) ? changedData.id : null;
|
||||||
@ -394,7 +468,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadWidget(dropdownData)
|
reloadWidget(dropdownData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +496,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
{
|
{
|
||||||
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
|
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const toggleFullScreenWidget = () =>
|
const toggleFullScreenWidget = () =>
|
||||||
{
|
{
|
||||||
@ -434,14 +508,14 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
{
|
{
|
||||||
setFullScreenWidgetClassName("fullScreenWidget");
|
setFullScreenWidgetClassName("fullScreenWidget");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
||||||
|
|
||||||
const isSet = (v: any): boolean =>
|
const isSet = (v: any): boolean =>
|
||||||
{
|
{
|
||||||
return (v !== null && v !== undefined);
|
return (v !== null && v !== undefined);
|
||||||
}
|
};
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components //
|
// to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components //
|
||||||
@ -450,26 +524,40 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
if (hasPermission)
|
if (hasPermission)
|
||||||
{
|
{
|
||||||
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
||||||
|
needLabelBox ||= (props.labelAdditionalElementsLeft && props.labelAdditionalElementsLeft.length > 0);
|
||||||
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
||||||
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
||||||
needLabelBox ||= isSet(props.widgetData?.label);
|
needLabelBox ||= isSet(props.widgetData?.label);
|
||||||
needLabelBox ||= isSet(props.widgetMetaData?.label);
|
needLabelBox ||= isSet(props.widgetMetaData?.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// first look for a label in the widget data, which would override that in the metadata //
|
||||||
|
// note - previously this had a ?: and one was pl={2}, the other was pl={3}... //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
|
||||||
|
let labelElement = (
|
||||||
|
<Typography sx={{cursor: "default", pl: "auto", pt: props.widgetMetaData.type == "parentWidget" ? "1rem" : "auto", fontWeight: 600}} variant="h6" display="inline">
|
||||||
|
{labelToUse}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (props.widgetMetaData.tooltip)
|
||||||
|
{
|
||||||
|
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
||||||
const widgetContent =
|
const widgetContent =
|
||||||
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||||
{
|
{
|
||||||
needLabelBox &&
|
needLabelBox &&
|
||||||
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} height={"3.5rem"}>
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} minHeight={"2.5rem"}>
|
||||||
<Box pt={2} pb={1}>
|
<Box>
|
||||||
{
|
{
|
||||||
hasPermission ?
|
hasPermission ?
|
||||||
props.widgetMetaData?.icon && (
|
props.widgetMetaData?.icon && (
|
||||||
<Box
|
<Box ml={1} mr={2} mt={-4} sx={{
|
||||||
ml={3}
|
|
||||||
mt={-4}
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -487,10 +575,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
</Box>
|
</Box>
|
||||||
) :
|
) :
|
||||||
(
|
(
|
||||||
<Box
|
<Box ml={3} mt={-4} sx={{
|
||||||
ml={3}
|
|
||||||
mt={-4}
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -507,20 +592,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
hasPermission && labelToUse && (labelElement)
|
||||||
// first look for a label in the widget data, which would override that in the metadata //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
hasPermission && props.widgetData?.label ? (
|
|
||||||
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={2} display="inline-block">
|
|
||||||
{props.widgetData.label}
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
hasPermission && props.widgetMetaData?.label && (
|
|
||||||
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={3} display="inline-block">
|
|
||||||
{props.widgetMetaData.label}
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
hasPermission && (
|
hasPermission && (
|
||||||
@ -530,6 +602,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{props.labelAdditionalElementsLeft}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
@ -544,7 +617,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0"}} /> : <Box height="0.375rem" />)
|
///////////////////////////////////////////////////////////////////
|
||||||
|
// turning this off... for now. maybe make a property in future //
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
/*
|
||||||
|
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0", mx:-2}} /> : <Box height="0.375rem" />)
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
errorLoading ? (
|
errorLoading ? (
|
||||||
@ -554,7 +632,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
|
hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
|
||||||
<Box pb={3} pr={3} sx={{width: "100%", textAlign: "right"}}>
|
<Box pb={3} sx={{width: "100%", textAlign: "right"}}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{props.widgetData?.dropdownNeedsSelectedText}
|
{props.widgetData?.dropdownNeedsSelectedText}
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -575,11 +653,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|
||||||
|
const padding = props.omitPadding ? "auto" : "24px 16px";
|
||||||
return props.widgetMetaData?.isCard
|
return props.widgetMetaData?.isCard
|
||||||
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>
|
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%", p: padding}} className={fullScreenWidgetClassName}>
|
||||||
{widgetContent}
|
{widgetContent}
|
||||||
</Card>
|
</Card>
|
||||||
: widgetContent;
|
: <span style={{width: "100%", padding: padding}}>{widgetContent}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Widget;
|
export default Widget;
|
||||||
|
@ -28,6 +28,7 @@ import {Bar} from "react-chartjs-2";
|
|||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {chartColors, DefaultChartData} from "qqq/components/widgets/charts/DefaultChartData";
|
import {chartColors, DefaultChartData} from "qqq/components/widgets/charts/DefaultChartData";
|
||||||
|
import ChartSubheaderWithData, {ChartSubheaderData} from "qqq/components/widgets/components/ChartSubheaderWithData";
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@ -39,18 +40,61 @@ ChartJS.register(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const options = {
|
export const options = {
|
||||||
|
maintainAspectRatio: false,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
animation: {
|
animation: {
|
||||||
duration: 0
|
duration: 0
|
||||||
},
|
},
|
||||||
|
elements: {
|
||||||
|
bar: {
|
||||||
|
borderRadius: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
// todo - some configs around this
|
||||||
|
callbacks: {
|
||||||
|
title: function(context: any)
|
||||||
|
{
|
||||||
|
return ("");
|
||||||
|
},
|
||||||
|
label: function(context: any)
|
||||||
|
{
|
||||||
|
if(context.dataset.label.startsWith(context.label))
|
||||||
|
{
|
||||||
|
return `${context.label}: ${context.formattedValue}`;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: "bottom",
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
pointStyle: "circle",
|
||||||
|
boxHeight: 6,
|
||||||
|
boxWidth: 6,
|
||||||
|
padding: 12,
|
||||||
|
font: {
|
||||||
|
size: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
grid: {offset: false},
|
grid: {display: false},
|
||||||
ticks: {autoSkip: false, maxRotation: 90}
|
ticks: {autoSkip: false, maxRotation: 90}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
|
position: "right",
|
||||||
|
ticks: {precision: 0}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -58,10 +102,12 @@ export const options = {
|
|||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
data: DefaultChartData;
|
data: DefaultChartData;
|
||||||
|
chartSubheaderData?: ChartSubheaderData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {gradients} = colors;
|
const {gradients} = colors;
|
||||||
function StackedBarChart({data}: Props): JSX.Element
|
|
||||||
|
function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -70,24 +116,31 @@ function StackedBarChart({data}: Props): JSX.Element
|
|||||||
|
|
||||||
const handleClick = (e: Array<{}>) =>
|
const handleClick = (e: Array<{}>) =>
|
||||||
{
|
{
|
||||||
if(e && e.length > 0 && data?.urls && data?.urls.length)
|
if (e && e.length > 0 && data?.urls && data?.urls.length)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
navigate(data.urls[e[0]["index"]]);
|
navigate(data.urls[e[0]["index"]]);
|
||||||
}
|
}
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(data)
|
if (data)
|
||||||
{
|
{
|
||||||
data?.datasets.forEach((dataset: any, index: number) =>
|
data?.datasets.forEach((dataset: any, index: number) =>
|
||||||
{
|
{
|
||||||
if (!dataset.backgroundColor)
|
if (!dataset.backgroundColor)
|
||||||
|
{
|
||||||
|
if (gradients[chartColors[index]])
|
||||||
{
|
{
|
||||||
dataset.backgroundColor = gradients[chartColors[index]].state;
|
dataset.backgroundColor = gradients[chartColors[index]].state;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dataset.backgroundColor = chartColors[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setStateData(stateData);
|
setStateData(stateData);
|
||||||
}
|
}
|
||||||
@ -95,8 +148,13 @@ function StackedBarChart({data}: Props): JSX.Element
|
|||||||
|
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<Box p={3}><Bar data={data} options={options} getElementsAtEvent={handleClick} /></Box>
|
<Box>
|
||||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} /> ;
|
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||||
|
<Box width="100%" height="300px">
|
||||||
|
<Bar data={data} options={options} getElementsAtEvent={handleClick} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default StackedBarChart;
|
export default StackedBarChart;
|
||||||
|
@ -63,7 +63,7 @@ const options = {
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
@ -86,7 +86,7 @@ const options = {
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
|
@ -67,7 +67,7 @@ const options = {
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
@ -88,7 +88,7 @@ const options = {
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
|
@ -81,7 +81,7 @@ const options = {
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
@ -107,7 +107,7 @@ const options = {
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
|
@ -69,7 +69,7 @@ function configs(labels: any, datasets: any)
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
@ -90,7 +90,7 @@ function configs(labels: any, datasets: any)
|
|||||||
font: {
|
font: {
|
||||||
size: 14,
|
size: 14,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
family: "Roboto",
|
family: "SF Pro Display,Roboto",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
},
|
},
|
||||||
|
@ -30,6 +30,7 @@ import {useNavigate} from "react-router-dom";
|
|||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import {chartColors} from "qqq/components/widgets/charts/DefaultChartData";
|
import {chartColors} from "qqq/components/widgets/charts/DefaultChartData";
|
||||||
import configs from "qqq/components/widgets/charts/piechart/PieChartConfigs";
|
import configs from "qqq/components/widgets/charts/piechart/PieChartConfigs";
|
||||||
|
import ChartSubheaderWithData, {ChartSubheaderData} from "qqq/components/widgets/components/ChartSubheaderWithData";
|
||||||
|
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
// structure of expected bar chart data //
|
// structure of expected bar chart data //
|
||||||
@ -51,25 +52,29 @@ interface Props
|
|||||||
{
|
{
|
||||||
description?: string;
|
description?: string;
|
||||||
chartData: PieChartData;
|
chartData: PieChartData;
|
||||||
|
chartSubheaderData?: ChartSubheaderData;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function PieChart({description, chartData}: Props): JSX.Element
|
function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [dataLoaded, setDataLoaded] = useState(false);
|
const [dataLoaded, setDataLoaded] = useState(false);
|
||||||
|
|
||||||
if (chartData && chartData.dataset)
|
if (chartData && chartData.dataset)
|
||||||
|
{
|
||||||
|
if(!chartData.dataset.backgroundColors)
|
||||||
{
|
{
|
||||||
chartData.dataset.backgroundColors = chartColors;
|
chartData.dataset.backgroundColors = chartColors;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(chartData)
|
if (chartData)
|
||||||
{
|
{
|
||||||
setDataLoaded(true);
|
setDataLoaded(true);
|
||||||
}
|
}
|
||||||
@ -77,19 +82,20 @@ function PieChart({description, chartData}: Props): JSX.Element
|
|||||||
|
|
||||||
const handleClick = (e: Array<{}>) =>
|
const handleClick = (e: Array<{}>) =>
|
||||||
{
|
{
|
||||||
if(e && e.length > 0 && chartData?.dataset?.urls && chartData?.dataset?.urls.length)
|
if (e && e.length > 0 && chartData?.dataset?.urls && chartData?.dataset?.urls.length)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
navigate(chartData.dataset.urls[e[0]["index"]]);
|
navigate(chartData.dataset.urls[e[0]["index"]]);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{minHeight: "400px", boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1}}>
|
<Card sx={{boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1, border: 0}}>
|
||||||
<Box mt={3}>
|
<Box>
|
||||||
<Grid container alignItems="center">
|
<Box>
|
||||||
<Grid item xs={12} justifyContent="center">
|
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||||
<Box width="100%" height="80%" py={2} pr={2} pl={2}>
|
</Box>
|
||||||
|
<Box width="100%" height="300px">
|
||||||
{useMemo(
|
{useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Pie data={data} options={options} getElementsAtEvent={handleClick} />
|
<Pie data={data} options={options} getElementsAtEvent={handleClick} />
|
||||||
@ -98,32 +104,29 @@ function PieChart({description, chartData}: Props): JSX.Element
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
! chartData && (
|
!chartData && (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "40%",
|
top: "40%",
|
||||||
left: "50%",
|
left: "50%",
|
||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center"}}>
|
justifyContent: "center"
|
||||||
<Skeleton sx={{width: "150px", height: "150px"}} variant="circular"/>
|
}}>
|
||||||
|
<Skeleton sx={{width: "150px", height: "150px"}} variant="circular" />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Divider />
|
|
||||||
{
|
{
|
||||||
description && (
|
description && (
|
||||||
<Grid container>
|
<>
|
||||||
<Grid item xs={12}>
|
<Divider />
|
||||||
<Box pb={2} px={2} display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
|
<Box display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
|
||||||
<MDTypography variant="button" color="text" fontWeight="light">
|
<MDTypography variant="button" color="text" fontWeight="light">
|
||||||
{parse(description)}
|
{parse(description)}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</>
|
||||||
</Grid>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -30,10 +30,16 @@ function configs(labels: any, datasets: any)
|
|||||||
if (datasets.backgroundColors)
|
if (datasets.backgroundColors)
|
||||||
{
|
{
|
||||||
datasets.backgroundColors.forEach((color: string) =>
|
datasets.backgroundColors.forEach((color: string) =>
|
||||||
gradients[color]
|
{
|
||||||
? backgroundColors.push(gradients[color].state)
|
if (gradients[color])
|
||||||
: backgroundColors.push(dark.main)
|
{
|
||||||
);
|
backgroundColors.push(gradients[color].state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backgroundColors.push(color);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -58,12 +64,49 @@ function configs(labels: any, datasets: any)
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: false,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
aspectRatio: 2,
|
|
||||||
plugins: {
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context: any)
|
||||||
|
{
|
||||||
|
let percentSuffix = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// make percent by dividing this slice's value by the sum of all values //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
const thisSlice = context.dataset.data[context.dataIndex];
|
||||||
|
const sum = context.dataset.data.reduce((acc: number, val: number) => acc + val, 0);
|
||||||
|
percentSuffix = " (" + Number(100 * thisSlice / sum).toFixed(1) + "%)";
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
// leave percentSuffix empty
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// our labels already have the value in them - so just use the label in the //
|
||||||
|
// tooltip (lib by default puts label + value, so we were duplicating value!) //
|
||||||
|
// oh, and we add percent if we can //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
return context.label + percentSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
legend: {
|
legend: {
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
pointStyle: "circle",
|
||||||
|
padding: 12,
|
||||||
|
boxHeight: 8,
|
||||||
|
boxWidth: 8,
|
||||||
|
font: {
|
||||||
|
size: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
|
105
src/qqq/components/widgets/components/ChartSubheaderWithData.tsx
Normal file
105
src/qqq/components/widgets/components/ChartSubheaderWithData.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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 {Skeleton} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import React from "react";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
|
export interface ChartSubheaderData
|
||||||
|
{
|
||||||
|
mainNumber: number;
|
||||||
|
vsPreviousPercent: number;
|
||||||
|
vsPreviousNumber: number;
|
||||||
|
isUpVsPrevious: boolean;
|
||||||
|
isGoodVsPrevious: boolean;
|
||||||
|
vsDescription: string;
|
||||||
|
mainNumberUrl: string;
|
||||||
|
previousNumberUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
chartSubheaderData: ChartSubheaderData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GOOD_COLOR = colors.success.main;
|
||||||
|
const BAD_COLOR = colors.error.main;
|
||||||
|
const UP_ICON = "arrow_drop_up";
|
||||||
|
const DOWN_ICON = "arrow_drop_down";
|
||||||
|
|
||||||
|
function StackedBarChart({chartSubheaderData}: Props): JSX.Element
|
||||||
|
{
|
||||||
|
let color = "black";
|
||||||
|
if (chartSubheaderData && chartSubheaderData.isGoodVsPrevious != null)
|
||||||
|
{
|
||||||
|
color = chartSubheaderData.isGoodVsPrevious ? GOOD_COLOR : BAD_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
let iconName: string = null;
|
||||||
|
if (chartSubheaderData && chartSubheaderData.isUpVsPrevious != null)
|
||||||
|
{
|
||||||
|
iconName = chartSubheaderData.isUpVsPrevious ? UP_ICON : DOWN_ICON;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainNumberElement = <Typography variant="h3" display="inline">{ValueUtils.getFormattedNumber(chartSubheaderData.mainNumber)}</Typography>;
|
||||||
|
if(chartSubheaderData.mainNumberUrl)
|
||||||
|
{
|
||||||
|
mainNumberElement = <Link to={chartSubheaderData.mainNumberUrl}>{mainNumberElement}</Link>
|
||||||
|
}
|
||||||
|
mainNumberElement = <Box pr={1}>{mainNumberElement}</Box>
|
||||||
|
|
||||||
|
let previousNumberElement = (
|
||||||
|
<>
|
||||||
|
<Typography display="block" variant="body2" sx={{color: colors.gray.main, fontSize: ".875rem", fontWeight: 500}}>
|
||||||
|
{chartSubheaderData.vsDescription}
|
||||||
|
{chartSubheaderData.vsPreviousNumber && (<> ({ValueUtils.getFormattedNumber(chartSubheaderData.vsPreviousNumber)})</>)}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
if(chartSubheaderData.previousNumberUrl)
|
||||||
|
{
|
||||||
|
previousNumberElement = <Link to={chartSubheaderData.previousNumberUrl}>{previousNumberElement}</Link>
|
||||||
|
}
|
||||||
|
|
||||||
|
return chartSubheaderData ? (
|
||||||
|
<Box display="inline-flex" alignItems="flex-end" flexWrap="wrap">
|
||||||
|
{mainNumberElement}
|
||||||
|
{
|
||||||
|
chartSubheaderData.vsPreviousPercent != null && iconName != null && (
|
||||||
|
<Box display="inline-flex" alignItems="baseline" pb={0.5} ml={-0.5}>
|
||||||
|
<Icon fontSize="medium" sx={{color: color, alignSelf: "flex-end"}}>{iconName}</Icon>
|
||||||
|
<Typography display="inline" variant="body2" sx={{color: color, fontSize: ".875rem", fontWeight: 500}}>{chartSubheaderData.vsPreviousPercent}%</Typography>
|
||||||
|
{previousNumberElement}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "12px"}} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StackedBarChart;
|
@ -1,179 +0,0 @@
|
|||||||
/*
|
|
||||||
* QQQ - Low-code Application Framework for Engineers.
|
|
||||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
|
||||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
||||||
* contact@kingsrook.com
|
|
||||||
* https://github.com/Kingsrook/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Collapse, Theme} from "@mui/material";
|
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import {SxProps} from "@mui/system";
|
|
||||||
import {Field, Form, Formik} from "formik";
|
|
||||||
import React, {useState} from "react";
|
|
||||||
import MDInput from "qqq/components/legacy/MDInput";
|
|
||||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
|
||||||
|
|
||||||
|
|
||||||
export interface DropdownOption
|
|
||||||
{
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////
|
|
||||||
// inputs and defaults //
|
|
||||||
/////////////////////////
|
|
||||||
interface Props
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
defaultValue?: any;
|
|
||||||
label?: string;
|
|
||||||
dropdownOptions?: DropdownOption[];
|
|
||||||
onChangeCallback?: (dropdownLabel: string, data: any) => void;
|
|
||||||
sx?: SxProps<Theme>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StartAndEndDate
|
|
||||||
{
|
|
||||||
startDate?: string,
|
|
||||||
endDate?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
|
|
||||||
{
|
|
||||||
const customTimeValues: StartAndEndDate = {};
|
|
||||||
if(defaultValue && defaultValue.id)
|
|
||||||
{
|
|
||||||
var parts = defaultValue.id.split(",");
|
|
||||||
if(parts.length >= 2)
|
|
||||||
{
|
|
||||||
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
|
|
||||||
}
|
|
||||||
if(parts.length >= 3)
|
|
||||||
{
|
|
||||||
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (customTimeValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
|
|
||||||
{
|
|
||||||
const backendTimeValues: StartAndEndDate = {};
|
|
||||||
if(frontendDefaultValues && frontendDefaultValues.startDate)
|
|
||||||
{
|
|
||||||
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
|
|
||||||
}
|
|
||||||
if(frontendDefaultValues && frontendDefaultValues.endDate)
|
|
||||||
{
|
|
||||||
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
|
|
||||||
}
|
|
||||||
return (backendTimeValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenu({name, defaultValue, label, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
|
||||||
{
|
|
||||||
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
|
|
||||||
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
|
|
||||||
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
|
|
||||||
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
|
|
||||||
|
|
||||||
const handleOnChange = (event: any, newValue: any, reason: string) =>
|
|
||||||
{
|
|
||||||
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom"
|
|
||||||
setCustomTimesVisible(isTimeframeCustom);
|
|
||||||
|
|
||||||
if(isTimeframeCustom)
|
|
||||||
{
|
|
||||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
onChangeCallback(label, newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
|
|
||||||
{
|
|
||||||
if(customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
|
|
||||||
{
|
|
||||||
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let customTimes = <></>;
|
|
||||||
if (name == "timeframe")
|
|
||||||
{
|
|
||||||
const handleSubmit = async (values: any, actions: any) =>
|
|
||||||
{
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
|
|
||||||
{
|
|
||||||
customTimeValuesFrontend[fieldName] = event.target.value;
|
|
||||||
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
|
|
||||||
|
|
||||||
clearTimeout(debounceTimeout);
|
|
||||||
const newDebounceTimeout = setTimeout(() =>
|
|
||||||
{
|
|
||||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
|
||||||
}, 500);
|
|
||||||
setDebounceTimeout(newDebounceTimeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
|
|
||||||
<Collapse orientation="horizontal" in={customTimesVisible}>
|
|
||||||
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
|
|
||||||
{({}) => (
|
|
||||||
<Form id="timeframe-form" autoComplete="off">
|
|
||||||
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
|
|
||||||
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</Collapse>
|
|
||||||
</Box>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
dropdownOptions ? (
|
|
||||||
<span style={{whiteSpace: "nowrap", display: "flex"}} className="dashboardDropdownMenu">
|
|
||||||
<Autocomplete
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
size="small"
|
|
||||||
disablePortal
|
|
||||||
id={`${label}-combo-box`}
|
|
||||||
options={dropdownOptions}
|
|
||||||
sx={{...sx, cursor: "pointer", display: "inline-block"}}
|
|
||||||
onChange={handleOnChange}
|
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
||||||
renderInput={(params: any) => <TextField {...params} label={label} />}
|
|
||||||
renderOption={(props, option: DropdownOption) => (
|
|
||||||
<li {...props} style={{whiteSpace: "normal"}}>{option.label}</li>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{customTimes}
|
|
||||||
</span>
|
|
||||||
) : null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DropdownMenu;
|
|
335
src/qqq/components/widgets/components/WidgetDropdownMenu.tsx
Normal file
335
src/qqq/components/widgets/components/WidgetDropdownMenu.tsx
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
/*
|
||||||
|
* 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 {Collapse, Theme, InputAdornment} from "@mui/material";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {SxProps} from "@mui/system";
|
||||||
|
import {Field, Form, Formik} from "formik";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
|
import MDInput from "qqq/components/legacy/MDInput";
|
||||||
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
|
|
||||||
|
export interface DropdownOption
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// inputs and defaults //
|
||||||
|
/////////////////////////
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
defaultValue?: any;
|
||||||
|
label?: string;
|
||||||
|
startIcon?: string;
|
||||||
|
width?: number;
|
||||||
|
disableClearable?: boolean;
|
||||||
|
allowBackAndForth?: boolean;
|
||||||
|
backAndForthInverted?: boolean;
|
||||||
|
dropdownOptions?: DropdownOption[];
|
||||||
|
onChangeCallback?: (dropdownLabel: string, data: any) => void;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StartAndEndDate
|
||||||
|
{
|
||||||
|
startDate?: string,
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
|
||||||
|
{
|
||||||
|
const customTimeValues: StartAndEndDate = {};
|
||||||
|
if (defaultValue && defaultValue.id)
|
||||||
|
{
|
||||||
|
var parts = defaultValue.id.split(",");
|
||||||
|
if (parts.length >= 2)
|
||||||
|
{
|
||||||
|
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
|
||||||
|
}
|
||||||
|
if (parts.length >= 3)
|
||||||
|
{
|
||||||
|
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (customTimeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
|
||||||
|
{
|
||||||
|
const backendTimeValues: StartAndEndDate = {};
|
||||||
|
if (frontendDefaultValues && frontendDefaultValues.startDate)
|
||||||
|
{
|
||||||
|
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
|
||||||
|
}
|
||||||
|
if (frontendDefaultValues && frontendDefaultValues.endDate)
|
||||||
|
{
|
||||||
|
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
|
||||||
|
}
|
||||||
|
return (backendTimeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disableClearable, allowBackAndForth, backAndForthInverted, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
||||||
|
{
|
||||||
|
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
|
||||||
|
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
|
||||||
|
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
|
||||||
|
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
const [backDisabled, setBackDisabled] = useState(false);
|
||||||
|
const [forthDisabled, setForthDisabled] = useState(false);
|
||||||
|
|
||||||
|
const doForceOpen = (event: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
{
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSelectedIndex(value: DropdownOption)
|
||||||
|
{
|
||||||
|
let currentIndex = null;
|
||||||
|
for (let i = 0; i < dropdownOptions.length; i++)
|
||||||
|
{
|
||||||
|
if (value && dropdownOptions[i].id == value.id)
|
||||||
|
{
|
||||||
|
currentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateBackAndForth = (event: React.MouseEvent, direction: -1 | 1) =>
|
||||||
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
let currentIndex = getSelectedIndex(value);
|
||||||
|
|
||||||
|
if (currentIndex == null)
|
||||||
|
{
|
||||||
|
console.log("No current value.... TODO");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex == 0 && direction == -1)
|
||||||
|
{
|
||||||
|
console.log("Can't go -1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex == dropdownOptions.length - 1 && direction == 1)
|
||||||
|
{
|
||||||
|
console.log("Can't go +1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnChange(event, dropdownOptions[currentIndex + direction], "navigatedBackAndForth");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleOnChange = (event: any, newValue: any, reason: string) =>
|
||||||
|
{
|
||||||
|
setValue(newValue);
|
||||||
|
|
||||||
|
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom";
|
||||||
|
setCustomTimesVisible(isTimeframeCustom);
|
||||||
|
|
||||||
|
if (isTimeframeCustom)
|
||||||
|
{
|
||||||
|
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
onChangeCallback(label, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* this had bugs (seemed to not take immediate effect?), so don't use for now.
|
||||||
|
let currentIndex = getSelectedIndex(value);
|
||||||
|
if(currentIndex == 0)
|
||||||
|
{
|
||||||
|
backAndForthInverted ? setForthDisabled(true) : setBackDisabled(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backAndForthInverted ? setForthDisabled(false) : setBackDisabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex == dropdownOptions.length - 1)
|
||||||
|
{
|
||||||
|
backAndForthInverted ? setBackDisabled(true) : setForthDisabled(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backAndForthInverted ? setBackDisabled(false) : setForthDisabled(false);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnInputChange = (event: any, newValue: any, reason: string) =>
|
||||||
|
{
|
||||||
|
setInputValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
|
||||||
|
{
|
||||||
|
if (customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
|
||||||
|
{
|
||||||
|
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let customTimes = <></>;
|
||||||
|
if (name == "timeframe")
|
||||||
|
{
|
||||||
|
const handleSubmit = async (values: any, actions: any) =>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
|
||||||
|
{
|
||||||
|
customTimeValuesFrontend[fieldName] = event.target.value;
|
||||||
|
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
|
||||||
|
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
const newDebounceTimeout = setTimeout(() =>
|
||||||
|
{
|
||||||
|
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||||
|
}, 500);
|
||||||
|
setDebounceTimeout(newDebounceTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
|
||||||
|
<Collapse orientation="horizontal" in={customTimesVisible}>
|
||||||
|
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
|
||||||
|
{({}) => (
|
||||||
|
<Form id="timeframe-form" autoComplete="off">
|
||||||
|
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
|
||||||
|
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Collapse>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startAdornment = startIcon ? <Icon sx={{fontSize: "1.25rem!important", color: colors.gray.main, paddingLeft: allowBackAndForth ? "auto" : "0.25rem", width: allowBackAndForth ? "1.5rem" : "1.75rem"}}>{startIcon}</Icon> : null;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// we tried this end-adornment, for a different style of down-arrow - but by using it, we then messed something else up (i forget what), so... not used right now //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const endAdornment = <InputAdornment position="end" sx={{position: "absolute", right: allowBackAndForth ? "-0.5rem" : "0.5rem"}}><Icon sx={{fontSize: "1.75rem!important", color: colors.gray.main}}>keyboard_arrow_down</Icon></InputAdornment>;
|
||||||
|
|
||||||
|
const fontSize = "1rem";
|
||||||
|
let optionPaddingLeftRems = 0.75;
|
||||||
|
if(startIcon)
|
||||||
|
{
|
||||||
|
optionPaddingLeftRems += allowBackAndForth ? 1.5 : 1.75
|
||||||
|
}
|
||||||
|
if(allowBackAndForth)
|
||||||
|
{
|
||||||
|
optionPaddingLeftRems += 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
dropdownOptions ? (
|
||||||
|
<Box sx={{whiteSpace: "nowrap", display: "flex",
|
||||||
|
"& .MuiPopperUnstyled-root": {
|
||||||
|
border: `1px solid ${colors.grayLines.main}`,
|
||||||
|
borderTop: "none",
|
||||||
|
borderRadius: "0 0 0.75rem 0.75rem",
|
||||||
|
padding: 0,
|
||||||
|
}, "& .MuiPaper-rounded": {
|
||||||
|
borderRadius: "0 0 0.75rem 0.75rem",
|
||||||
|
}
|
||||||
|
}} className="dashboardDropdownMenu">
|
||||||
|
<Autocomplete
|
||||||
|
id={`${label}-combo-box`}
|
||||||
|
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
inputValue={inputValue}
|
||||||
|
onInputChange={handleOnInputChange}
|
||||||
|
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
|
||||||
|
open={isOpen}
|
||||||
|
onOpen={() => setIsOpen(true)}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
|
||||||
|
size="small"
|
||||||
|
disablePortal
|
||||||
|
disableClearable={disableClearable}
|
||||||
|
options={dropdownOptions}
|
||||||
|
sx={{
|
||||||
|
...sx,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "inline-block",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: "none"
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
renderInput={(params: any) =>
|
||||||
|
<>
|
||||||
|
<Box sx={{width: `${width}px`, background: "white", borderRadius: isOpen ? "0.75rem 0.75rem 0 0" : "0.75rem", border: `1px solid ${colors.grayLines.main}`, "& *": {cursor: "pointer"}}} display="flex" alignItems="center" onClick={(event) => doForceOpen(event)}>
|
||||||
|
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? 1 : -1)} disabled={backDisabled}><Icon>navigate_before</Icon></IconButton>}
|
||||||
|
<TextField {...params} placeholder={label} sx={{
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
fontSize: fontSize
|
||||||
|
}
|
||||||
|
}} InputProps={{...params.InputProps, startAdornment: startAdornment/*, endAdornment: endAdornment*/}}
|
||||||
|
/>
|
||||||
|
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? -1 : 1)} disabled={forthDisabled}><Icon>navigate_next</Icon></IconButton>}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
renderOption={(props, option: DropdownOption) => (
|
||||||
|
<li {...props} style={{whiteSpace: "normal", fontSize: fontSize, paddingLeft: `${optionPaddingLeftRems}rem`}}>{option.label}</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
noOptionsText={<Box fontSize={fontSize}>No options found</Box>}
|
||||||
|
|
||||||
|
slotProps={{
|
||||||
|
popper: {
|
||||||
|
sx: {
|
||||||
|
width: `${width}px!important`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{customTimes}
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WidgetDropdownMenu;
|
@ -286,18 +286,15 @@ export default function DataBagViewer({dataBagId}: Props): JSX.Element
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<>
|
<>
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
|
||||||
<Typography variant="h5" p={2}></Typography>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
sx={{m: 1}}
|
sx={{m: 0, mb: 1, mt: -3}}
|
||||||
value={selectedTab}
|
value={selectedTab}
|
||||||
onChange={(event, newValue) => changeTab(newValue)}
|
onChange={(event, newValue) => changeTab(newValue)}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
>
|
>
|
||||||
<Tab label="Raw Data" id="simple-tab-0" aria-controls="simple-tabpanel-0" sx={{width: "150px"}} />
|
<Tab label="Raw Data" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||||
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "150px"}} />
|
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TabPanel index={0} value={selectedTab}>
|
<TabPanel index={0} value={selectedTab}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
@ -22,10 +22,15 @@
|
|||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate, Link} from "react-router-dom";
|
||||||
import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
|
import Widget, {AddNewRecordButton, LabelComponent} from "qqq/components/widgets/Widget";
|
||||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
@ -47,6 +52,8 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
const [records, setRecords] = useState([] as QRecord[])
|
const [records, setRecords] = useState([] as QRecord[])
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
const [allColumns, setAllColumns] = useState([])
|
const [allColumns, setAllColumns] = useState([])
|
||||||
|
const [csv, setCsv] = useState(null as string);
|
||||||
|
const [fileName, setFileName] = useState(null as string);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -75,6 +82,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const allColumns = [... columns];
|
||||||
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
@ -95,11 +103,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
setRows(rows);
|
setRows(rows);
|
||||||
setRecords(records)
|
setRecords(records)
|
||||||
setColumns(columns);
|
setColumns(columns);
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const exportCallback = () =>
|
|
||||||
{
|
|
||||||
let csv = "";
|
let csv = "";
|
||||||
for (let i = 0; i < allColumns.length; i++)
|
for (let i = 0; i < allColumns.length; i++)
|
||||||
{
|
{
|
||||||
@ -118,16 +122,23 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
HtmlUtils.download(fileName, csv);
|
|
||||||
|
setCsv(csv);
|
||||||
|
setFileName(fileName);
|
||||||
}
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
// view all link //
|
// view all link //
|
||||||
///////////////////
|
///////////////////
|
||||||
const labelAdditionalComponentsLeft: LabelComponent[] = []
|
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||||
if(data && data.viewAllLink)
|
if(data && data.viewAllLink)
|
||||||
{
|
{
|
||||||
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
|
labelAdditionalElementsLeft.push(
|
||||||
|
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
||||||
|
<Link to={data.viewAllLink}>View All</Link>
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
@ -149,7 +160,26 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle))
|
const onExportClick = () =>
|
||||||
|
{
|
||||||
|
if(csv)
|
||||||
|
{
|
||||||
|
HtmlUtils.download(fileName, csv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alert("There is no data available to export.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(widgetMetaData?.showExportButton)
|
||||||
|
{
|
||||||
|
labelAdditionalElementsLeft.push(
|
||||||
|
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.25rem">
|
||||||
|
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.125}}>save_alt</Icon></Button></Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
// add new button //
|
// add new button //
|
||||||
@ -184,11 +214,17 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
<Widget
|
<Widget
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={data}
|
widgetData={data}
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
>
|
>
|
||||||
|
<Box mx={-2} mb={-3}>
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
autoHeight
|
autoHeight
|
||||||
|
sx={{
|
||||||
|
borderBottom: "none",
|
||||||
|
borderLeft: "none",
|
||||||
|
borderRight: "none"
|
||||||
|
}}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
disableSelectionOnClick
|
disableSelectionOnClick
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -221,6 +257,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
// sortingOrder={[ "asc", "desc" ]}
|
// sortingOrder={[ "asc", "desc" ]}
|
||||||
// sortModel={columnSortModel}
|
// sortModel={columnSortModel}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -392,14 +392,8 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
return JSON.stringify(new QQueryFilter([new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, [scriptRevisionId])]));
|
return JSON.stringify(new QQueryFilter([new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, [scriptRevisionId])]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
position: relative;
|
|
||||||
left: -356px;
|
|
||||||
width: calc(100% + 380px);
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container className="scriptViewer">
|
<Grid container className="scriptViewer" m={-2} pt={4} width={"calc(100% + 2rem)"}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
@ -430,20 +424,17 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<>
|
<>
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
|
||||||
<Typography variant="h5" p={2}></Typography>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
sx={{m: 1}}
|
sx={{m: 0, mb: 1, mt: -3}}
|
||||||
value={selectedTab}
|
value={selectedTab}
|
||||||
onChange={(event, newValue) => changeTab(newValue)}
|
onChange={(event, newValue) => changeTab(newValue)}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
>
|
>
|
||||||
<Tab label="Code" id="simple-tab-0" aria-controls="simple-tabpanel-0" sx={{width: "100px"}} />
|
<Tab label="Code" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||||
<Tab label="Logs" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "100px"}} />
|
<Tab label="Logs" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||||
<Tab label="Test" id="simple-tab-1" aria-controls="simple-tabpanel-2" sx={{width: "100px"}} />
|
<Tab label="Test" id="simple-tab-1" aria-controls="simple-tabpanel-2" />
|
||||||
<Tab label="Docs" id="simple-tab-1" aria-controls="simple-tabpanel-3" sx={{width: "100px"}} />
|
<Tab label="Docs" id="simple-tab-1" aria-controls="simple-tabpanel-3" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TabPanel index={0} value={selectedTab}>
|
<TabPanel index={0} value={selectedTab}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
@ -498,7 +489,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
editorProps={{$blockScrolling: true}}
|
editorProps={{$blockScrolling: true}}
|
||||||
setOptions={{useWorker: false}}
|
setOptions={{useWorker: false}}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="368px"
|
height="400px"
|
||||||
value={getSelectedFileCode()}
|
value={getSelectedFileCode()}
|
||||||
style={{borderTop: "1px solid lightgray", borderBottomRightRadius: "1rem"}}
|
style={{borderTop: "1px solid lightgray", borderBottomRightRadius: "1rem"}}
|
||||||
/>
|
/>
|
||||||
|
@ -30,7 +30,8 @@ import TableRow from "@mui/material/TableRow";
|
|||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import {useEffect, useMemo, useState} from "react";
|
||||||
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
|
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} from "react-table";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDInput from "qqq/components/legacy/MDInput";
|
import MDInput from "qqq/components/legacy/MDInput";
|
||||||
import MDPagination from "qqq/components/legacy/MDPagination";
|
import MDPagination from "qqq/components/legacy/MDPagination";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
@ -47,6 +48,8 @@ interface Props
|
|||||||
canSearch?: boolean;
|
canSearch?: boolean;
|
||||||
showTotalEntries?: boolean;
|
showTotalEntries?: boolean;
|
||||||
hidePaginationDropdown?: boolean;
|
hidePaginationDropdown?: boolean;
|
||||||
|
fixedStickyLastRow?: boolean;
|
||||||
|
fixedHeight?: number;
|
||||||
table: TableDataInput;
|
table: TableDataInput;
|
||||||
pagination?: {
|
pagination?: {
|
||||||
variant: "contained" | "gradient";
|
variant: "contained" | "gradient";
|
||||||
@ -56,6 +59,18 @@ interface Props
|
|||||||
noEndBorder?: boolean;
|
noEndBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DataTable.defaultProps = {
|
||||||
|
entriesPerPage: 10,
|
||||||
|
entriesPerPageOptions: ["5", "10", "15", "20", "25"],
|
||||||
|
canSearch: false,
|
||||||
|
showTotalEntries: true,
|
||||||
|
fixedStickyLastRow: false,
|
||||||
|
fixedHeight: null,
|
||||||
|
pagination: {variant: "gradient", color: "info"},
|
||||||
|
isSorted: true,
|
||||||
|
noEndBorder: false,
|
||||||
|
};
|
||||||
|
|
||||||
const NoMaxWidthTooltip = styled(({className, ...props}: TooltipProps) => (
|
const NoMaxWidthTooltip = styled(({className, ...props}: TooltipProps) => (
|
||||||
<Tooltip {...props} classes={{popper: className}} />
|
<Tooltip {...props} classes={{popper: className}} />
|
||||||
))({
|
))({
|
||||||
@ -71,6 +86,8 @@ function DataTable({
|
|||||||
hidePaginationDropdown,
|
hidePaginationDropdown,
|
||||||
canSearch,
|
canSearch,
|
||||||
showTotalEntries,
|
showTotalEntries,
|
||||||
|
fixedStickyLastRow,
|
||||||
|
fixedHeight,
|
||||||
table,
|
table,
|
||||||
pagination,
|
pagination,
|
||||||
isSorted,
|
isSorted,
|
||||||
@ -83,8 +100,77 @@ function DataTable({
|
|||||||
defaultValue = (entriesPerPage) ? entriesPerPage : "10";
|
defaultValue = (entriesPerPage) ? entriesPerPage : "10";
|
||||||
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
|
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
|
||||||
|
|
||||||
const columns = useMemo<any>(() => table.columns, [table]);
|
let widths = [];
|
||||||
|
for(let i = 0; i<table.columns.length; i++)
|
||||||
|
{
|
||||||
|
const column = table.columns[i];
|
||||||
|
if(column.type !== "hidden")
|
||||||
|
{
|
||||||
|
widths.push(table.columns[i].width ?? "1fr");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let showExpandColumn = false;
|
||||||
|
if(table.rows)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < table.rows.length; i++)
|
||||||
|
{
|
||||||
|
if (table.rows[i].subRows)
|
||||||
|
{
|
||||||
|
showExpandColumn = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnsToMemo = [...table.columns];
|
||||||
|
if(showExpandColumn)
|
||||||
|
{
|
||||||
|
widths.push("60px");
|
||||||
|
columnsToMemo.push(
|
||||||
|
{
|
||||||
|
///////////////////////////////
|
||||||
|
// Build our expander column //
|
||||||
|
///////////////////////////////
|
||||||
|
id: "__expander",
|
||||||
|
width: 60,
|
||||||
|
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
// use this block if we want to do expand-all //
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
// @ts-ignore
|
||||||
|
// header: ({getToggleAllRowsExpandedProps, isAllRowsExpanded}) => (
|
||||||
|
// <span {...getToggleAllRowsExpandedProps()}>
|
||||||
|
// {isAllRowsExpanded ? "yes" : "no"}
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
header: () => (<span />),
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
cell: ({row}) =>
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter to build the toggle for expanding a row //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
row.canExpand ? (
|
||||||
|
<span
|
||||||
|
{...row.getToggleRowExpandedProps({
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// We could use the row.depth property and paddingLeft to indicate the depth of the row //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// style: {paddingLeft: `${row.depth * 2}rem`,},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
|
||||||
|
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon>
|
||||||
|
</span>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useMemo<any>(() => columnsToMemo, [table]);
|
||||||
const data = useMemo<any>(() => table.rows, [table]);
|
const data = useMemo<any>(() => table.rows, [table]);
|
||||||
|
const gridTemplateColumns = widths.join(" ");
|
||||||
|
|
||||||
if (!columns || !data)
|
if (!columns || !data)
|
||||||
{
|
{
|
||||||
@ -95,6 +181,7 @@ function DataTable({
|
|||||||
{columns, data, initialState: {pageIndex: 0}},
|
{columns, data, initialState: {pageIndex: 0}},
|
||||||
useGlobalFilter,
|
useGlobalFilter,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
|
useExpanded,
|
||||||
usePagination
|
usePagination
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -113,7 +200,7 @@ function DataTable({
|
|||||||
previousPage,
|
previousPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
setGlobalFilter,
|
setGlobalFilter,
|
||||||
state: {pageIndex, pageSize, globalFilter},
|
state: {pageIndex, pageSize, globalFilter, expanded},
|
||||||
}: any = tableInstance;
|
}: any = tableInstance;
|
||||||
|
|
||||||
// Set the default value for the entries per page when component mounts
|
// Set the default value for the entries per page when component mounts
|
||||||
@ -193,11 +280,131 @@ function DataTable({
|
|||||||
entriesEnd = pageSize * (pageIndex + 1);
|
entriesEnd = pageSize * (pageIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTable(includeHead: boolean, rows: any, isFooter: boolean)
|
||||||
|
{
|
||||||
|
let boxStyle = {};
|
||||||
|
if(fixedStickyLastRow)
|
||||||
|
{
|
||||||
|
boxStyle = isFooter
|
||||||
|
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, overflow: "auto", scrollbarGutter: "stable"}
|
||||||
|
: {height: fixedHeight ? `${fixedHeight}px` : "360px", overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Box sx={boxStyle}>
|
||||||
|
<Table {...getTableProps()}>
|
||||||
|
{
|
||||||
|
includeHead && (
|
||||||
|
<Box component="thead" sx={{position: "sticky", top: 0, background: "white"}}>
|
||||||
|
{headerGroups.map((headerGroup: any, i: number) => (
|
||||||
|
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", gridTemplateColumns: gridTemplateColumns}}>
|
||||||
|
{headerGroup.headers.map((column: any) => (
|
||||||
|
column.type !== "hidden" && (
|
||||||
|
<DataTableHeadCell
|
||||||
|
key={i++}
|
||||||
|
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||||
|
align={column.align ? column.align : "left"}
|
||||||
|
sorted={setSortedValue(column)}
|
||||||
|
>
|
||||||
|
{column.render("header")}
|
||||||
|
</DataTableHeadCell>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map((row: any, key: any) =>
|
||||||
|
{
|
||||||
|
prepareRow(row);
|
||||||
|
|
||||||
|
let overrideNoEndBorder = false;
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// don't do an end-border on nested rows - unless they're the last one in a set //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(row.depth > 0)
|
||||||
|
{
|
||||||
|
overrideNoEndBorder = true;
|
||||||
|
if(key + 1 < rows.length && rows[key + 1].depth == 0)
|
||||||
|
{
|
||||||
|
overrideNoEndBorder = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////
|
||||||
|
// don't do end-border on the footer //
|
||||||
|
///////////////////////////////////////
|
||||||
|
if(isFooter)
|
||||||
|
{
|
||||||
|
overrideNoEndBorder = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: (row.depth > 0 ? "#FAFAFA" : "initial")}} key={key} {...row.getRowProps()}>
|
||||||
|
{row.cells.map((cell: any) => (
|
||||||
|
cell.column.type !== "hidden" && (
|
||||||
|
<DataTableBodyCell
|
||||||
|
key={key}
|
||||||
|
noBorder={noEndBorder || overrideNoEndBorder}
|
||||||
|
depth={row.depth}
|
||||||
|
align={cell.column.align ? cell.column.align : "left"}
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
cell.column.type === "default" && (
|
||||||
|
cell.value && "number" === typeof cell.value ? (
|
||||||
|
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
||||||
|
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cell.column.type === "htmlAndTooltip" && (
|
||||||
|
<DefaultCell isFooter={isFooter}>
|
||||||
|
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||||
|
<Box>
|
||||||
|
{parse(cell.value)}
|
||||||
|
</Box>
|
||||||
|
</NoMaxWidthTooltip>
|
||||||
|
</DefaultCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cell.column.type === "html" && (
|
||||||
|
<DefaultCell isFooter={isFooter}>{parse(cell.value)}</DefaultCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||||
|
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||||
|
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(cell.column.id === "__expander") && cell.render("cell")
|
||||||
|
}
|
||||||
|
</DataTableBodyCell>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer sx={{boxShadow: "none"}}>
|
<TableContainer sx={{boxShadow: "none"}}>
|
||||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && ! hidePaginationDropdown) || canSearch) ? (
|
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||||
{entriesPerPage && (hidePaginationDropdown === undefined || ! hidePaginationDropdown) && (
|
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disableClearable
|
disableClearable
|
||||||
@ -205,7 +412,7 @@ function DataTable({
|
|||||||
options={entries}
|
options={entries}
|
||||||
onChange={(event, newValues: any) =>
|
onChange={(event, newValues: any) =>
|
||||||
{
|
{
|
||||||
if(typeof newValues === "string")
|
if (typeof newValues === "string")
|
||||||
{
|
{
|
||||||
setEntriesPerPage(parseInt(newValues, 10));
|
setEntriesPerPage(parseInt(newValues, 10));
|
||||||
}
|
}
|
||||||
@ -240,82 +447,15 @@ function DataTable({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
<Table {...getTableProps()}>
|
|
||||||
<Box component="thead">
|
|
||||||
{headerGroups.map((headerGroup: any, i: number) => (
|
|
||||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()}>
|
|
||||||
{headerGroup.headers.map((column: any) => (
|
|
||||||
column.type !== "hidden" && (
|
|
||||||
<DataTableHeadCell
|
|
||||||
key={i++}
|
|
||||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
|
||||||
width={column.width ? column.width : "auto"}
|
|
||||||
align={column.align ? column.align : "left"}
|
|
||||||
sorted={setSortedValue(column)}
|
|
||||||
>
|
|
||||||
{column.render("header")}
|
|
||||||
</DataTableHeadCell>
|
|
||||||
)
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<TableBody {...getTableBodyProps()}>
|
|
||||||
{page.map((row: any, key: any) =>
|
|
||||||
{
|
|
||||||
prepareRow(row);
|
|
||||||
return (
|
|
||||||
<TableRow sx={{verticalAlign: "top"}} key={key} {...row.getRowProps()}>
|
|
||||||
{row.cells.map((cell: any) => (
|
|
||||||
cell.column.type !== "hidden" && (
|
|
||||||
<DataTableBodyCell
|
|
||||||
key={key}
|
|
||||||
noBorder={noEndBorder && rows.length - 1 === key}
|
|
||||||
align={cell.column.align ? cell.column.align : "left"}
|
|
||||||
{...cell.getCellProps()}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
cell.column.type === "default" && (
|
|
||||||
cell.value && "number" === typeof cell.value ? (
|
|
||||||
<DefaultCell>{cell.value.toLocaleString()}</DefaultCell>
|
|
||||||
) : (<DefaultCell>{cell.render("Cell")}</DefaultCell>)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
cell.column.type === "htmlAndTooltip" && (
|
|
||||||
<DefaultCell>
|
|
||||||
|
|
||||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
|
||||||
<Box>
|
|
||||||
{parse(cell.value)}
|
|
||||||
</Box>
|
|
||||||
</NoMaxWidthTooltip>
|
|
||||||
</DefaultCell>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
cell.column.type === "html" && (
|
fixedStickyLastRow ? (
|
||||||
<DefaultCell>{parse(cell.value)}</DefaultCell>
|
<>
|
||||||
)
|
{getTable(true, page.slice(0, page.length -1), false)}
|
||||||
|
{getTable(false, page.slice(page.length-1), true)}
|
||||||
|
</>
|
||||||
|
) : getTable(true, page, false)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
|
||||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
|
||||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</DataTableBodyCell>
|
|
||||||
)
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
@ -368,15 +508,4 @@ function DataTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declaring default props for DataTable
|
|
||||||
DataTable.defaultProps = {
|
|
||||||
entriesPerPage: 10,
|
|
||||||
entriesPerPageOptions: ["5", "10", "15", "20", "25"],
|
|
||||||
canSearch: false,
|
|
||||||
showTotalEntries: true,
|
|
||||||
pagination: {variant: "gradient", color: "info"},
|
|
||||||
isSorted: true,
|
|
||||||
noEndBorder: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DataTable;
|
export default DataTable;
|
||||||
|
@ -54,11 +54,13 @@ interface Props
|
|||||||
noRowsFoundHTML?: string;
|
noRowsFoundHTML?: string;
|
||||||
rowsPerPage?: number;
|
rowsPerPage?: number;
|
||||||
hidePaginationDropdown?: boolean;
|
hidePaginationDropdown?: boolean;
|
||||||
|
fixedStickyLastRow?: boolean;
|
||||||
|
fixedHeight?: number;
|
||||||
data: TableDataInput;
|
data: TableDataInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown}: Props): JSX.Element
|
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||||
|
|
||||||
@ -72,16 +74,17 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown}:
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={1}>
|
<Box py={1} mx={-2}>
|
||||||
{
|
{
|
||||||
data && data.columns && !noRowsFoundHTML ?
|
data && data.columns && !noRowsFoundHTML ?
|
||||||
<DataTable
|
<DataTable
|
||||||
table={data}
|
table={data}
|
||||||
entriesPerPage={rowsPerPage}
|
entriesPerPage={rowsPerPage}
|
||||||
hidePaginationDropdown={hidePaginationDropdown}
|
hidePaginationDropdown={hidePaginationDropdown}
|
||||||
|
fixedStickyLastRow={fixedStickyLastRow}
|
||||||
|
fixedHeight={fixedHeight}
|
||||||
showTotalEntries={false}
|
showTotalEntries={false}
|
||||||
isSorted={false}
|
isSorted={false}
|
||||||
noEndBorder
|
|
||||||
/>
|
/>
|
||||||
: noRowsFoundHTML ?
|
: noRowsFoundHTML ?
|
||||||
<Box p={3} pt={1} pb={1} sx={{textAlign: "center"}}>
|
<Box p={3} pt={1} pb={1} sx={{textAlign: "center"}}>
|
||||||
@ -114,7 +117,7 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown}:
|
|||||||
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}>
|
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}>
|
||||||
{Array(8).fill(0).map((_, j) =>
|
{Array(8).fill(0).map((_, j) =>
|
||||||
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
||||||
<DefaultCell><Skeleton /></DefaultCell>
|
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
||||||
</DataTableBodyCell>
|
</DataTableBodyCell>
|
||||||
)}
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@ -21,11 +21,16 @@
|
|||||||
|
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {htmlToText} from "html-to-text";
|
import {htmlToText} from "html-to-text";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||||
import Widget, {ExportDataButton, WidgetData} from "qqq/components/widgets/Widget";
|
import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
||||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
@ -43,6 +48,8 @@ TableWidget.defaultProps = {
|
|||||||
function TableWidget(props: Props): JSX.Element
|
function TableWidget(props: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
||||||
|
const [csv, setCsv] = useState(null as string);
|
||||||
|
const [fileName, setFileName] = useState(null as string);
|
||||||
|
|
||||||
const rows = props.widgetData?.rows;
|
const rows = props.widgetData?.rows;
|
||||||
const columns = props.widgetData?.columns;
|
const columns = props.widgetData?.columns;
|
||||||
@ -56,14 +63,8 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
setIsExportDisabled(isExportDisabled);
|
setIsExportDisabled(isExportDisabled);
|
||||||
|
|
||||||
}, [props.widgetMetaData, props.widgetData]);
|
|
||||||
|
|
||||||
const exportCallback = () =>
|
|
||||||
{
|
|
||||||
if (props.widgetData && rows && columns)
|
if (props.widgetData && rows && columns)
|
||||||
{
|
{
|
||||||
console.log(props.widgetData);
|
|
||||||
|
|
||||||
let csv = "";
|
let csv = "";
|
||||||
for (let j = 0; j < columns.length; j++)
|
for (let j = 0; j < columns.length; j++)
|
||||||
{
|
{
|
||||||
@ -98,16 +99,37 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
csv += "\n";
|
csv += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(csv);
|
setCsv(csv);
|
||||||
|
|
||||||
const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
|
setFileName(fileName)
|
||||||
|
|
||||||
|
console.log(`useEffect, setting fileName ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [props.widgetMetaData, props.widgetData]);
|
||||||
|
|
||||||
|
const onExportClick = () =>
|
||||||
|
{
|
||||||
|
if(csv)
|
||||||
|
{
|
||||||
HtmlUtils.download(fileName, csv);
|
HtmlUtils.download(fileName, csv);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
alert("There is no data available to export.");
|
alert("There is no data available to export.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||||
|
if(props.widgetMetaData?.showExportButton)
|
||||||
|
{
|
||||||
|
labelAdditionalElementsLeft.push(
|
||||||
|
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.25rem">
|
||||||
|
<Tooltip title="Export"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}><Icon sx={{color: colors.gray.main, fontSize: 1.125}}>save_alt</Icon></Button></Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget
|
<Widget
|
||||||
@ -116,12 +138,14 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
||||||
footerHTML={props.widgetData?.footerHTML}
|
footerHTML={props.widgetData?.footerHTML}
|
||||||
isChild={props.isChild}
|
isChild={props.isChild}
|
||||||
labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
|
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||||
>
|
>
|
||||||
<TableCard
|
<TableCard
|
||||||
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
||||||
rowsPerPage={props.widgetData?.rowsPerPage}
|
rowsPerPage={props.widgetData?.rowsPerPage}
|
||||||
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
||||||
|
fixedStickyLastRow={props.widgetData?.fixedStickyLastRow}
|
||||||
|
fixedHeight={props.widgetData?.fixedHeight}
|
||||||
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
|
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import {Theme} from "@mui/material/styles";
|
import {Theme} from "@mui/material/styles";
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
|
|
||||||
// Declaring prop types for DataTableBodyCell
|
// Declaring prop types for DataTableBodyCell
|
||||||
interface Props
|
interface Props
|
||||||
@ -40,14 +41,26 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
|||||||
py={1.5}
|
py={1.5}
|
||||||
px={3}
|
px={3}
|
||||||
sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({
|
sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({
|
||||||
fontSize: size.sm,
|
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||||
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${light.main}`,
|
fontSize: "0.875rem",
|
||||||
|
"@media (min-width: 1440px)": {
|
||||||
|
fontSize: "1rem"
|
||||||
|
},
|
||||||
|
"@media (max-width: 1440px)": {
|
||||||
|
fontSize: "0.875rem"
|
||||||
|
},
|
||||||
|
"&:nth-child(1)": {
|
||||||
|
paddingLeft: "1rem"
|
||||||
|
},
|
||||||
|
"&:last-child": {
|
||||||
|
paddingRight: "1rem"
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
display="initial"
|
display="initial"
|
||||||
width="max-content"
|
width="max-content"
|
||||||
color="text"
|
color={colors.dark.main}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -23,6 +23,7 @@ import Box from "@mui/material/Box";
|
|||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {Theme} from "@mui/material/styles";
|
import {Theme} from "@mui/material/styles";
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {useMaterialUIController} from "qqq/context";
|
import {useMaterialUIController} from "qqq/context";
|
||||||
|
|
||||||
// Declaring props types for DataTableHeadCell
|
// Declaring props types for DataTableHeadCell
|
||||||
@ -46,18 +47,28 @@ function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JS
|
|||||||
py={1.5}
|
py={1.5}
|
||||||
px={3}
|
px={3}
|
||||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||||
borderBottom: `${borderWidth[1]} solid ${light.main}`,
|
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||||
|
"&:nth-child(1)": {
|
||||||
|
paddingLeft: "1rem"
|
||||||
|
},
|
||||||
|
"&:last-child": {
|
||||||
|
paddingRight: "1rem"
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
{...rest}
|
{...rest}
|
||||||
sx={({typography: {size, fontWeightBold}}: Theme) => ({
|
sx={({typography: {size, fontWeightBold}}: Theme) => ({
|
||||||
position: "relative",
|
position: "relative",
|
||||||
opacity: "0.7",
|
color: colors.grey[700],
|
||||||
textAlign: align,
|
textAlign: align,
|
||||||
fontSize: size.xxs,
|
"@media (min-width: 1440px)": {
|
||||||
fontWeight: fontWeightBold,
|
fontSize: "1rem"
|
||||||
textTransform: "uppercase",
|
},
|
||||||
|
"@media (max-width: 1440px)": {
|
||||||
|
fontSize: "0.875rem"
|
||||||
|
},
|
||||||
|
fontWeight: 600,
|
||||||
cursor: sorted && "pointer",
|
cursor: sorted && "pointer",
|
||||||
userSelect: sorted && "none",
|
userSelect: sorted && "none",
|
||||||
})}
|
})}
|
||||||
|
@ -14,12 +14,31 @@ Coded by www.creative-tim.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
|
||||||
function DefaultCell({children}: { children: ReactNode }): JSX.Element
|
interface Props
|
||||||
|
{
|
||||||
|
isFooter: boolean
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function DefaultCell({isFooter, children}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
<MDTypography variant="button" color={colors.dark.main} sx={{
|
||||||
|
fontWeight: isFooter ? 600 : 500,
|
||||||
|
"@media (min-width: 1440px)": {
|
||||||
|
fontSize: "1rem"
|
||||||
|
},
|
||||||
|
"@media (max-width: 1440px)": {
|
||||||
|
fontSize: "0.875rem"
|
||||||
|
},
|
||||||
|
"& a": {
|
||||||
|
color: colors.blueGray.main
|
||||||
|
}
|
||||||
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
);
|
);
|
||||||
|
@ -38,11 +38,11 @@ function DashboardLayout({children}: { children: ReactNode }): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={({breakpoints, transitions, functions: {pxToRem}}) => ({
|
sx={({breakpoints, transitions, functions: {pxToRem}}) => ({
|
||||||
p: 3,
|
p: "20px",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
|
||||||
[breakpoints.up("xl")]: {
|
[breakpoints.up("xl")]: {
|
||||||
marginLeft: miniSidenav ? pxToRem(120) : pxToRem(274),
|
marginLeft: miniSidenav ? pxToRem(120) : pxToRem(245),
|
||||||
transition: transitions.create(["margin-left", "margin-right"], {
|
transition: transitions.create(["margin-left", "margin-right"], {
|
||||||
easing: transitions.easing.easeInOut,
|
easing: transitions.easing.easeInOut,
|
||||||
duration: transitions.duration.standard,
|
duration: transitions.duration.standard,
|
||||||
|
@ -62,10 +62,12 @@ import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDri
|
|||||||
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
||||||
import ValidationReview from "qqq/components/processes/ValidationReview";
|
import ValidationReview from "qqq/components/processes/ValidationReview";
|
||||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
|
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
process?: QProcessMetaData;
|
process?: QProcessMetaData;
|
||||||
@ -74,7 +76,7 @@ interface Props
|
|||||||
isModal?: boolean;
|
isModal?: boolean;
|
||||||
isWidget?: boolean;
|
isWidget?: boolean;
|
||||||
isReport?: boolean;
|
isReport?: boolean;
|
||||||
recordIds?: string | QQueryFilter;
|
recordIds?: string[] | QQueryFilter;
|
||||||
closeModalHandler?: (event: object, reason: string) => void;
|
closeModalHandler?: (event: object, reason: string) => void;
|
||||||
forceReInit?: number;
|
forceReInit?: number;
|
||||||
overrideLabel?: string;
|
overrideLabel?: string;
|
||||||
@ -88,6 +90,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{
|
{
|
||||||
const processNameParam = useParams().processName;
|
const processNameParam = useParams().processName;
|
||||||
const processName = process === null ? processNameParam : process.name;
|
const processName = process === null ? processNameParam : process.name;
|
||||||
|
let tableVariantLocalStorageKey: string | null = null;
|
||||||
|
if(table)
|
||||||
|
{
|
||||||
|
tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${table.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
// process state //
|
// process state //
|
||||||
@ -222,7 +229,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const download = (url: string, fileName: string) =>
|
const download = (url: string, fileName: string) =>
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// todo - this could be simplified. //
|
// todo - this could be simplified, i think? //
|
||||||
// it was originally built like this when we had to submit full access token to backend... //
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
@ -230,12 +237,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
xhr.responseType = "blob";
|
xhr.responseType = "blob";
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
////////////////////////////////////
|
|
||||||
// todo#authHeader - delete this. //
|
|
||||||
////////////////////////////////////
|
|
||||||
const qController = Client.getInstance();
|
|
||||||
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
|
|
||||||
@ -322,7 +323,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
<Grid m={3} mt={9} container>
|
<Grid m={3} mt={9} container>
|
||||||
<Grid item xs={0} lg={3} />
|
<Grid item xs={0} lg={3} />
|
||||||
<Grid item xs={12} lg={6}>
|
<Grid item xs={12} lg={6}>
|
||||||
<Card elevation={5}>
|
<Card>
|
||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<MDTypography variant="h5" component="div">
|
<MDTypography variant="h5" component="div">
|
||||||
Working
|
Working
|
||||||
@ -375,15 +376,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
// but our first use case, they're the same, so... this needs fixed. //
|
// but our first use case, they're the same, so... this needs fixed. //
|
||||||
// they also need to know the 'otherValues' in this process - e.g., for filtering //
|
// they also need to know the 'otherValues' in this process - e.g., for filtering //
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(formFields && processValues)
|
if (formFields && processValues)
|
||||||
{
|
{
|
||||||
Object.keys(formFields).forEach((key) =>
|
Object.keys(formFields).forEach((key) =>
|
||||||
{
|
{
|
||||||
if(formFields[key].possibleValueProps)
|
if (formFields[key].possibleValueProps)
|
||||||
{
|
{
|
||||||
if(processValues[key])
|
if (processValues[key])
|
||||||
{
|
{
|
||||||
formFields[key].possibleValueProps.initialDisplayValue = processValues[key]
|
formFields[key].possibleValueProps.initialDisplayValue = processValues[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
||||||
@ -392,7 +393,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
formFields[key].possibleValueProps.otherValues.set(otherKey, processValues[otherKey]);
|
formFields[key].possibleValueProps.otherValues.set(otherKey, processValues[otherKey]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -748,7 +749,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
formValidations[fieldName] = validation;
|
formValidations[fieldName] = validation;
|
||||||
};
|
};
|
||||||
|
|
||||||
if(tableMetaData)
|
if (tableMetaData)
|
||||||
{
|
{
|
||||||
console.log("Adding table name field... ?", tableMetaData.name);
|
console.log("Adding table name field... ?", tableMetaData.name);
|
||||||
addField("tableName", {type: "hidden", omitFromQDynamicForm: true}, tableMetaData.name, null);
|
addField("tableName", {type: "hidden", omitFromQDynamicForm: true}, tableMetaData.name, null);
|
||||||
@ -801,15 +802,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
Object.keys(dynamicFormFields).forEach((key: any) =>
|
Object.keys(dynamicFormFields).forEach((key: any) =>
|
||||||
{
|
{
|
||||||
if(dynamicFormFields[key].possibleValueProps)
|
if (dynamicFormFields[key].possibleValueProps)
|
||||||
{
|
{
|
||||||
dynamicFormFields[key].possibleValueProps.otherValues = dynamicFormFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
dynamicFormFields[key].possibleValueProps.otherValues = dynamicFormFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
||||||
Object.keys(initialValues).forEach((ivKey: any) =>
|
Object.keys(initialValues).forEach((ivKey: any) =>
|
||||||
{
|
{
|
||||||
dynamicFormFields[key].possibleValueProps.otherValues.set(ivKey, initialValues[ivKey]);
|
dynamicFormFields[key].possibleValueProps.otherValues.set(ivKey, initialValues[ivKey]);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
// disable all fields if this is a bulk edit form //
|
// disable all fields if this is a bulk edit form //
|
||||||
@ -1082,8 +1083,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
let queryStringPairsForInit = [];
|
let queryStringPairsForInit = [];
|
||||||
if (urlSearchParams.get("recordIds"))
|
if (urlSearchParams.get("recordIds"))
|
||||||
{
|
{
|
||||||
|
const recordIdsFromQueryString = urlSearchParams.get("recordIds").split(",");
|
||||||
|
const encodedRecordIds = recordIdsFromQueryString.map(r => encodeURIComponent(r)).join(",");
|
||||||
queryStringPairsForInit.push("recordsParam=recordIds");
|
queryStringPairsForInit.push("recordsParam=recordIds");
|
||||||
queryStringPairsForInit.push(`recordIds=${urlSearchParams.get("recordIds")}`);
|
queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
|
||||||
}
|
}
|
||||||
else if (urlSearchParams.get("filterJSON"))
|
else if (urlSearchParams.get("filterJSON"))
|
||||||
{
|
{
|
||||||
@ -1097,16 +1100,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
// }
|
// }
|
||||||
else if (recordIds)
|
else if (recordIds)
|
||||||
{
|
{
|
||||||
if (typeof recordIds === "string")
|
if (recordIds instanceof QQueryFilter)
|
||||||
{
|
|
||||||
queryStringPairsForInit.push("recordsParam=recordIds");
|
|
||||||
queryStringPairsForInit.push(`recordIds=${recordIds}`);
|
|
||||||
}
|
|
||||||
else if (recordIds instanceof QQueryFilter)
|
|
||||||
{
|
{
|
||||||
queryStringPairsForInit.push("recordsParam=filterJSON");
|
queryStringPairsForInit.push("recordsParam=filterJSON");
|
||||||
queryStringPairsForInit.push(`filterJSON=${JSON.stringify(recordIds)}`);
|
queryStringPairsForInit.push(`filterJSON=${JSON.stringify(recordIds)}`);
|
||||||
}
|
}
|
||||||
|
else if (typeof recordIds === "object" && recordIds.length)
|
||||||
|
{
|
||||||
|
const encodedRecordIds = recordIds.map(r => encodeURIComponent(r)).join(",");
|
||||||
|
queryStringPairsForInit.push("recordsParam=recordIds");
|
||||||
|
queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
||||||
|
{
|
||||||
|
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||||
|
queryStringPairsForInit.push(`tableVariant=${JSON.stringify(tableVariant)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -1154,9 +1164,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(tableMetaData)
|
if (tableMetaData)
|
||||||
{
|
{
|
||||||
queryStringPairsForInit.push(`tableName=${tableMetaData.name}`)
|
queryStringPairsForInit.push(`tableName=${encodeURIComponent(tableMetaData.name)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -1194,6 +1204,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
formData.append(key, values[key]);
|
formData.append(key, values[key]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
||||||
|
{
|
||||||
|
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||||
|
formData.append("tableVariant", JSON.stringify(tableVariant));
|
||||||
|
}
|
||||||
|
|
||||||
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
|
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
|
||||||
{
|
{
|
||||||
const bulkEditEnabledFields: string[] = [];
|
const bulkEditEnabledFields: string[] = [];
|
||||||
@ -1262,6 +1278,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{
|
{
|
||||||
mainCardStyles.background = "none";
|
mainCardStyles.background = "none";
|
||||||
mainCardStyles.boxShadow = "none";
|
mainCardStyles.boxShadow = "none";
|
||||||
|
mainCardStyles.border = "none";
|
||||||
mainCardStyles.minHeight = "";
|
mainCardStyles.minHeight = "";
|
||||||
mainCardStyles.alignItems = "stretch";
|
mainCardStyles.alignItems = "stretch";
|
||||||
mainCardStyles.flexGrow = 1;
|
mainCardStyles.flexGrow = 1;
|
||||||
|
@ -82,7 +82,8 @@ 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";
|
|
||||||
|
export const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -233,7 +234,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
||||||
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
||||||
const [recordIdsForProcess, setRecordIdsForProcess] = useState(null as string | QQueryFilter);
|
const [recordIdsForProcess, setRecordIdsForProcess] = useState([] as string[] | QQueryFilter);
|
||||||
const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string);
|
const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string);
|
||||||
const [columnStatsField, setColumnStatsField] = useState(null as QFieldMetaData);
|
const [columnStatsField, setColumnStatsField] = useState(null as QFieldMetaData);
|
||||||
const [columnStatsFieldTableName, setColumnStatsFieldTableName] = useState(null as string)
|
const [columnStatsFieldTableName, setColumnStatsFieldTableName] = useState(null as string)
|
||||||
@ -538,15 +539,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<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 && getTableVariantHeader()}
|
||||||
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
|
||||||
@ -554,19 +547,23 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{label}
|
{label}
|
||||||
{
|
{tableVariant && getTableVariantHeader()}
|
||||||
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>);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTableVariantHeader = () =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updateTable = () =>
|
const updateTable = () =>
|
||||||
{
|
{
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -925,11 +922,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
if (table.primaryKeyField !== "id")
|
if (table.primaryKeyField !== "id")
|
||||||
{
|
{
|
||||||
navigate(`${metaData.getTablePathByName(tableName)}/${params.row[tableMetaData.primaryKeyField]}`);
|
navigate(`${metaData.getTablePathByName(tableName)}/${encodeURIComponent(params.row[tableMetaData.primaryKeyField])}`);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
navigate(`${metaData.getTablePathByName(tableName)}/${params.id}`);
|
navigate(`${metaData.getTablePathByName(tableName)}/${encodeURIComponent(params.id)}`);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@ -1121,7 +1118,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
exportWindow.document.write(`<html lang="en">
|
exportWindow.document.write(`<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
* { font-family: "Roboto","Helvetica","Arial",sans-serif; }
|
* { font-family: "SF Pro Display","Roboto","Helvetica","Arial",sans-serif; }
|
||||||
</style>
|
</style>
|
||||||
<title>${filename}</title>
|
<title>${filename}</title>
|
||||||
<script>
|
<script>
|
||||||
@ -1139,8 +1136,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<body>
|
<body>
|
||||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||||
<form id="exportForm" method="post" action="${url}" >
|
<form id="exportForm" method="post" action="${url}" >
|
||||||
<!-- todo#authHeader - remove this. -->
|
|
||||||
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
|
||||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||||
<input type="hidden" name="filter" id="filter">
|
<input type="hidden" name="filter" id="filter">
|
||||||
</form>
|
</form>
|
||||||
@ -1189,17 +1184,17 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
if (selectFullFilterState === "filter")
|
if (selectFullFilterState === "filter")
|
||||||
{
|
{
|
||||||
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(tableMetaData, filterModel))}`;
|
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel)))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectFullFilterState === "filterSubset")
|
if (selectFullFilterState === "filterSubset")
|
||||||
{
|
{
|
||||||
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(tableMetaData, filterModel, selectionSubsetSize))}`;
|
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel, selectionSubsetSize)))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedIds.length > 0)
|
if (selectedIds.length > 0)
|
||||||
{
|
{
|
||||||
return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`;
|
return `?recordsParam=recordIds&recordIds=${selectedIds.map(r => encodeURIComponent(r)).join(",")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
@ -1217,11 +1212,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
else if (selectedIds.length > 0)
|
else if (selectedIds.length > 0)
|
||||||
{
|
{
|
||||||
setRecordIdsForProcess(selectedIds.join(","));
|
setRecordIdsForProcess(selectedIds);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
setRecordIdsForProcess("");
|
setRecordIdsForProcess([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}${getRecordsQueryString()}`);
|
navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}${getRecordsQueryString()}`);
|
||||||
@ -1887,10 +1882,27 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
// instead, try to just render a Goto Record button, in auto-open, and may-not-close modes //
|
// instead, try to just render a Goto Record button, in auto-open, and may-not-close modes //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET))
|
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET))
|
||||||
|
{
|
||||||
|
if(tableMetaData?.usesVariants && (!tableVariant || tableVariantPromptOpen))
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} autoOpen={true} buttonVisible={false} mayClose={false} />
|
<TableVariantDialog table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
|
||||||
|
{
|
||||||
|
setTableVariantPromptOpen(false);
|
||||||
|
setTableVariant(value);
|
||||||
|
}} />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} tableVariant={tableVariant} autoOpen={true} buttonVisible={false} mayClose={false} subHeader={
|
||||||
|
<Box mb={2}>
|
||||||
|
{getTableVariantHeader()}
|
||||||
|
</Box>
|
||||||
|
} />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2005,7 +2017,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
page={pageNumber}
|
page={pageNumber}
|
||||||
checkboxSelection
|
checkboxSelection
|
||||||
disableSelectionOnClick
|
disableSelectionOnClick
|
||||||
autoHeight
|
autoHeight={false}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
|
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
|
||||||
columns={columnsModel}
|
columns={columnsModel}
|
||||||
@ -2029,6 +2041,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
getRowId={(row) => row.__rowIndex}
|
getRowId={(row) => row.__rowIndex}
|
||||||
selectionModel={rowSelectionModel}
|
selectionModel={rowSelectionModel}
|
||||||
hideFooterSelectedRowCount={true}
|
hideFooterSelectedRowCount={true}
|
||||||
|
sx={{border: 0, height: "calc(100vh - 250px)"}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -190,7 +190,7 @@ function RecordDeveloperView({table}: Props): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<div key={fieldName}>
|
<div key={fieldName}>
|
||||||
<Card sx={{mb: 3}}>
|
<Card sx={{mb: 3}}>
|
||||||
<Typography variant="h6" p={2} pl={3} pb={1}>{field?.label}</Typography>
|
<Typography variant="h6" p={2} pl={3} pb={3}>{field?.label}</Typography>
|
||||||
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
|
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
|
||||||
{scriptId ?
|
{scriptId ?
|
||||||
|
@ -193,7 +193,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down)
|
||||||
}
|
}
|
||||||
}, [dotMenuOpen, keyboardHelpOpen, showEditChildForm, showAudit, metaData])
|
}, [dotMenuOpen, keyboardHelpOpen, showEditChildForm, showAudit, metaData, location])
|
||||||
|
|
||||||
const gotoCreate = () =>
|
const gotoCreate = () =>
|
||||||
{
|
{
|
||||||
@ -931,7 +931,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} table={tableMetaData} recordIds={id} closeModalHandler={closeModalProcess} />
|
<ProcessRun process={activeModalProcess} isModal={true} table={tableMetaData} recordIds={[id]} closeModalHandler={closeModalProcess} />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
@ -246,7 +246,7 @@ export default class DataGridUtils
|
|||||||
if (key === tableMetaData.primaryKeyField && linkBase)
|
if (key === tableMetaData.primaryKeyField && linkBase)
|
||||||
{
|
{
|
||||||
column.renderCell = (cellValues: any) => (
|
column.renderCell = (cellValues: any) => (
|
||||||
<Link to={`${linkBase}${cellValues.value}`} onClick={(e) => e.stopPropagation()}>{cellValues.value}</Link>
|
<Link to={`${linkBase}${encodeURIComponent(cellValues.value)}`} onClick={(e) => e.stopPropagation()}>{cellValues.value}</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -63,6 +63,10 @@ export default class HtmlUtils
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Download a server-side generated file (or the contents of a data: url)
|
** Download a server-side generated file (or the contents of a data: url)
|
||||||
|
**
|
||||||
|
** todo - this could be simplified (i think?)
|
||||||
|
** it was originally built like this when we had to submit full access token to backend...
|
||||||
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
static downloadUrlViaIFrame = (url: string, filename: string) =>
|
static downloadUrlViaIFrame = (url: string, filename: string) =>
|
||||||
{
|
{
|
||||||
@ -95,18 +99,6 @@ export default class HtmlUtils
|
|||||||
form.setAttribute("target", "downloadIframe");
|
form.setAttribute("target", "downloadIframe");
|
||||||
iframe.appendChild(form);
|
iframe.appendChild(form);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// todo#authHeader - remove after comfortable with sessionUUID //
|
|
||||||
// todo - this could be simplified (i think?) //
|
|
||||||
// it was originally built like this when we had to submit full access token to backend... //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const authorizationInput = document.createElement("input");
|
|
||||||
authorizationInput.setAttribute("type", "hidden");
|
|
||||||
authorizationInput.setAttribute("id", "authorizationInput");
|
|
||||||
authorizationInput.setAttribute("name", "Authorization");
|
|
||||||
authorizationInput.setAttribute("value", Client.getInstance().getAuthorizationHeaderValue());
|
|
||||||
form.appendChild(authorizationInput);
|
|
||||||
|
|
||||||
const downloadInput = document.createElement("input");
|
const downloadInput = document.createElement("input");
|
||||||
downloadInput.setAttribute("type", "hidden");
|
downloadInput.setAttribute("type", "hidden");
|
||||||
downloadInput.setAttribute("name", "download");
|
downloadInput.setAttribute("name", "download");
|
||||||
@ -118,15 +110,16 @@ export default class HtmlUtils
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Open a server-side generated file from a url in a new window (or a data: url)
|
** Open a server-side generated file from a url in a new window (or a data: url)
|
||||||
|
**
|
||||||
|
** todo - this could be simplified (i think?)
|
||||||
|
** it was originally built like this when we had to submit full access token to backend...
|
||||||
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
static openInNewWindow = (url: string, filename: string) =>
|
static openInNewWindow = (url: string, filename: string) =>
|
||||||
{
|
{
|
||||||
if(url.startsWith("data:"))
|
if(url.startsWith("data:"))
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// todo#authHeader - remove the Authorization input after comfortable with sessionUUID //
|
|
||||||
// todo - this could be simplified (i think?) //
|
|
||||||
// it was originally built like this when we had to submit full access token to backend... //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const openInWindow = window.open("", "_blank");
|
const openInWindow = window.open("", "_blank");
|
||||||
openInWindow.document.write(`<html lang="en">
|
openInWindow.document.write(`<html lang="en">
|
||||||
@ -141,7 +134,7 @@ export default class HtmlUtils
|
|||||||
openInWindow.document.write(`<html lang="en">
|
openInWindow.document.write(`<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
* { font-family: "Roboto","Helvetica","Arial",sans-serif; }
|
* { font-family: "SF Pro Display","Roboto","Helvetica","Arial",sans-serif; }
|
||||||
</style>
|
</style>
|
||||||
<title>${filename}</title>
|
<title>${filename}</title>
|
||||||
<script>
|
<script>
|
||||||
@ -154,7 +147,6 @@ export default class HtmlUtils
|
|||||||
<body>
|
<body>
|
||||||
Opening ${filename}...
|
Opening ${filename}...
|
||||||
<form id="exportForm" method="post" action="${url}" >
|
<form id="exportForm" method="post" action="${url}" >
|
||||||
<input type="hidden" name="Authorization" value="${Client.getInstance().getAuthorizationHeaderValue()}">
|
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>`);
|
</html>`);
|
||||||
|
@ -29,11 +29,18 @@ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException
|
|||||||
class Client
|
class Client
|
||||||
{
|
{
|
||||||
private static qController: QController;
|
private static qController: QController;
|
||||||
|
private static unauthorizedCallback: () => void;
|
||||||
|
|
||||||
private static handleException(exception: QException)
|
private static handleException(exception: QException)
|
||||||
{
|
{
|
||||||
// todo - check for 401 and clear cookie et al & logout?
|
|
||||||
console.log(`Caught Exception: ${JSON.stringify(exception)}`);
|
console.log(`Caught Exception: ${JSON.stringify(exception)}`);
|
||||||
|
|
||||||
|
if(exception && exception.status == "401" && Client.unauthorizedCallback)
|
||||||
|
{
|
||||||
|
console.log("This is a 401 - calling the unauthorized callback.");
|
||||||
|
Client.unauthorizedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
throw (exception);
|
throw (exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +53,11 @@ class Client
|
|||||||
|
|
||||||
return this.qController;
|
return this.qController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static setUnauthorizedCallback(unauthorizedCallback: () => void)
|
||||||
|
{
|
||||||
|
Client.unauthorizedCallback = unauthorizedCallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Client;
|
export default Client;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package com.kingsrook.qqq.materialdashboard.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
@ -11,6 +16,7 @@ import org.openqa.selenium.Dimension;
|
|||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
import org.openqa.selenium.chrome.ChromeDriver;
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
import org.openqa.selenium.chrome.ChromeOptions;
|
import org.openqa.selenium.chrome.ChromeOptions;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -18,7 +24,7 @@ import org.openqa.selenium.chrome.ChromeOptions;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class QBaseSeleniumTest
|
public class QBaseSeleniumTest
|
||||||
{
|
{
|
||||||
private static ChromeOptions chromeOptions;
|
protected static ChromeOptions chromeOptions;
|
||||||
|
|
||||||
protected WebDriver driver;
|
protected WebDriver driver;
|
||||||
protected QSeleniumJavalin qSeleniumJavalin;
|
protected QSeleniumJavalin qSeleniumJavalin;
|
||||||
@ -40,7 +46,7 @@ public class QBaseSeleniumTest
|
|||||||
String headless = System.getenv("QQQ_SELENIUM_HEADLESS");
|
String headless = System.getenv("QQQ_SELENIUM_HEADLESS");
|
||||||
if("true".equals(headless))
|
if("true".equals(headless))
|
||||||
{
|
{
|
||||||
chromeOptions.setHeadless(true);
|
chromeOptions.addArguments("--headless=new");
|
||||||
}
|
}
|
||||||
|
|
||||||
WebDriverManager.chromiumdriver().setup();
|
WebDriverManager.chromiumdriver().setup();
|
||||||
@ -52,16 +58,89 @@ public class QBaseSeleniumTest
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void beforeEach()
|
public void beforeEach()
|
||||||
{
|
{
|
||||||
|
manageDownloadsDirectory();
|
||||||
|
|
||||||
|
HashMap<String, Object> chromePrefs = new HashMap<>();
|
||||||
|
chromePrefs.put("profile.default_content_settings.popups", 0);
|
||||||
|
chromePrefs.put("download.default_directory", getDownloadsDirectory());
|
||||||
|
chromeOptions.setExperimentalOption("prefs", chromePrefs);
|
||||||
|
|
||||||
driver = new ChromeDriver(chromeOptions);
|
driver = new ChromeDriver(chromeOptions);
|
||||||
|
|
||||||
driver.manage().window().setSize(new Dimension(1700, 1300));
|
driver.manage().window().setSize(new Dimension(1700, 1300));
|
||||||
qSeleniumLib = new QSeleniumLib(driver);
|
qSeleniumLib = new QSeleniumLib(driver);
|
||||||
|
|
||||||
|
if(useInternalJavalin())
|
||||||
|
{
|
||||||
qSeleniumJavalin = new QSeleniumJavalin();
|
qSeleniumJavalin = new QSeleniumJavalin();
|
||||||
addJavalinRoutes(qSeleniumJavalin);
|
addJavalinRoutes(qSeleniumJavalin);
|
||||||
qSeleniumJavalin.start();
|
qSeleniumJavalin.start();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void manageDownloadsDirectory()
|
||||||
|
{
|
||||||
|
File downloadsDirectory = new File(getDownloadsDirectory());
|
||||||
|
if(!downloadsDirectory.exists())
|
||||||
|
{
|
||||||
|
if(!downloadsDirectory.mkdir())
|
||||||
|
{
|
||||||
|
fail("Could not create downloads directory: " + downloadsDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!downloadsDirectory.isDirectory())
|
||||||
|
{
|
||||||
|
fail("Downloads directory: " + downloadsDirectory + " is not a directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for(File file : CollectionUtils.nonNullArray(downloadsDirectory.listFiles()))
|
||||||
|
{
|
||||||
|
if(!file.delete())
|
||||||
|
{
|
||||||
|
fail("Could not remove a file from the downloads directory: " + file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
protected String getDownloadsDirectory()
|
||||||
|
{
|
||||||
|
return ("/tmp/selenium-downloads");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
protected List<File> getDownloadedFiles()
|
||||||
|
{
|
||||||
|
File[] downloadedFiles = CollectionUtils.nonNullArray((new File(getDownloadsDirectory())).listFiles());
|
||||||
|
return (Arrays.stream(downloadedFiles).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** control if the test needs to start its own javalin server, or if we're running
|
||||||
|
** in an environment where an external web server is being used.
|
||||||
|
*******************************************************************************/
|
||||||
|
protected boolean useInternalJavalin()
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -75,6 +154,8 @@ public class QBaseSeleniumTest
|
|||||||
.withRouteToFile("/metaData/authentication", "metaData/authentication.json")
|
.withRouteToFile("/metaData/authentication", "metaData/authentication.json")
|
||||||
.withRouteToFile("/metaData/table/person", "metaData/table/person.json")
|
.withRouteToFile("/metaData/table/person", "metaData/table/person.json")
|
||||||
.withRouteToFile("/metaData/table/city", "metaData/table/person.json")
|
.withRouteToFile("/metaData/table/city", "metaData/table/person.json")
|
||||||
|
.withRouteToFile("/metaData/table/script", "metaData/table/script.json")
|
||||||
|
.withRouteToFile("/metaData/table/scriptRevision", "metaData/table/scriptRevision.json")
|
||||||
.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package com.kingsrook.qqq.materialdashboard.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
@ -7,7 +28,7 @@ package com.kingsrook.qqq.materialdashboard.lib;
|
|||||||
public interface QQQMaterialDashboardSelectors
|
public interface QQQMaterialDashboardSelectors
|
||||||
{
|
{
|
||||||
String SIDEBAR_ITEM = ".MuiDrawer-paperAnchorDockedLeft li.MuiListItem-root";
|
String SIDEBAR_ITEM = ".MuiDrawer-paperAnchorDockedLeft li.MuiListItem-root";
|
||||||
String BREADCRUMB_HEADER = ".MuiToolbar-root h5";
|
String BREADCRUMB_HEADER = ".MuiToolbar-root h3";
|
||||||
|
|
||||||
String QUERY_GRID_CELL = ".MuiDataGrid-root .MuiDataGrid-cellContent";
|
String QUERY_GRID_CELL = ".MuiDataGrid-root .MuiDataGrid-cellContent";
|
||||||
String QUERY_FILTER_INPUT = ".customFilterPanel input.MuiInput-input";
|
String QUERY_FILTER_INPUT = ".customFilterPanel input.MuiInput-input";
|
||||||
|
@ -1,3 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package com.kingsrook.qqq.materialdashboard.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
@ -5,11 +26,16 @@ import java.io.File;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.JavascriptExecutor;
|
import org.openqa.selenium.JavascriptExecutor;
|
||||||
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
import org.openqa.selenium.OutputType;
|
import org.openqa.selenium.OutputType;
|
||||||
import org.openqa.selenium.StaleElementReferenceException;
|
import org.openqa.selenium.StaleElementReferenceException;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
@ -35,6 +61,8 @@ public class QSeleniumLib
|
|||||||
private boolean SCREENSHOTS_ENABLED = true;
|
private boolean SCREENSHOTS_ENABLED = true;
|
||||||
private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/";
|
private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/";
|
||||||
|
|
||||||
|
private boolean autoHighlight = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -96,6 +124,17 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for BASE_URL
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getBaseUrl()
|
||||||
|
{
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -175,7 +214,13 @@ public class QSeleniumLib
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public WebElement waitForSelector(String cssSelector)
|
public WebElement waitForSelector(String cssSelector)
|
||||||
{
|
{
|
||||||
return (waitForSelectorAll(cssSelector, 1).get(0));
|
WebElement element = waitForSelectorAll(cssSelector, 1).get(0);
|
||||||
|
|
||||||
|
Actions actions = new Actions(driver);
|
||||||
|
actions.moveToElement(element);
|
||||||
|
|
||||||
|
conditionallyAutoHighlight(element);
|
||||||
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -218,7 +263,7 @@ public class QSeleniumLib
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||||
if(elements.size() == 0)
|
if(elements.isEmpty())
|
||||||
{
|
{
|
||||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||||
return;
|
return;
|
||||||
@ -244,7 +289,7 @@ public class QSeleniumLib
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||||
if(elements.size() == 0)
|
if(elements.isEmpty())
|
||||||
{
|
{
|
||||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||||
return;
|
return;
|
||||||
@ -265,6 +310,31 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void waitForNumberOfWindowsToBe(int number)
|
||||||
|
{
|
||||||
|
LOG.debug("Waiting for number of windows (tabs) to be [" + number + "]");
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if(driver.getWindowHandles().size() == number)
|
||||||
|
{
|
||||||
|
LOG.debug("Number of windows (tabs) is [" + number + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleepABit();
|
||||||
|
}
|
||||||
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
|
|
||||||
|
fail("Failed waiting for number of windows (tabs) to be [" + number + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -293,10 +363,76 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void soonUnhighlightElement(WebElement element)
|
||||||
|
{
|
||||||
|
CompletableFuture.supplyAsync(() ->
|
||||||
|
{
|
||||||
|
SleepUtils.sleep(2, TimeUnit.SECONDS);
|
||||||
|
JavascriptExecutor js = (JavascriptExecutor) driver;
|
||||||
|
js.executeScript("arguments[0].setAttribute('style', 'background: unset; border: unset;');", element);
|
||||||
|
return (true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void switchToSecondaryTab()
|
||||||
|
{
|
||||||
|
String originalWindow = driver.getWindowHandle();
|
||||||
|
|
||||||
|
waitForNumberOfWindowsToBe(2);
|
||||||
|
|
||||||
|
Set<String> windowHandles = driver.getWindowHandles();
|
||||||
|
for(String windowHandle : windowHandles)
|
||||||
|
{
|
||||||
|
if(!windowHandle.equals(originalWindow))
|
||||||
|
{
|
||||||
|
driver.switchTo().window(windowHandle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fail("Failed to find a window handle not equal to the original window handle. Original=[" + originalWindow + "]. All=[" + windowHandles + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void closeSecondaryTab()
|
||||||
|
{
|
||||||
|
String originalWindow = driver.getWindowHandle();
|
||||||
|
driver.close();
|
||||||
|
|
||||||
|
Set<String> windowHandles = driver.getWindowHandles();
|
||||||
|
for(String windowHandle : windowHandles)
|
||||||
|
{
|
||||||
|
if(!windowHandle.equals(originalWindow))
|
||||||
|
{
|
||||||
|
driver.switchTo().window(windowHandle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fail("Failed to find a window handle not equal to the original window handle. Original=[" + originalWindow + "]. All=[" + windowHandles + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface Code<T>
|
public interface Code<T>
|
||||||
{
|
{
|
||||||
public T run();
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
T run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -346,6 +482,7 @@ public class QSeleniumLib
|
|||||||
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||||
Actions actions = new Actions(driver);
|
Actions actions = new Actions(driver);
|
||||||
actions.moveToElement(element);
|
actions.moveToElement(element);
|
||||||
|
conditionallyAutoHighlight(element);
|
||||||
return (element);
|
return (element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -353,6 +490,10 @@ public class QSeleniumLib
|
|||||||
{
|
{
|
||||||
LOG.debug("Caught a StaleElementReferenceException - will retry.");
|
LOG.debug("Caught a StaleElementReferenceException - will retry.");
|
||||||
}
|
}
|
||||||
|
catch(NoSuchElementException nsee)
|
||||||
|
{
|
||||||
|
LOG.debug("Caught a NoSuchElementException - will retry.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sleepABit();
|
sleepABit();
|
||||||
@ -365,6 +506,20 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void conditionallyAutoHighlight(WebElement element)
|
||||||
|
{
|
||||||
|
if(autoHighlight && System.getenv("CIRCLECI") == null)
|
||||||
|
{
|
||||||
|
highlightElement(element);
|
||||||
|
soonUnhighlightElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory
|
** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory
|
||||||
** for the test class simple name, filename = methodName.png.
|
** for the test class simple name, filename = methodName.png.
|
||||||
@ -394,7 +549,8 @@ public class QSeleniumLib
|
|||||||
destFile.mkdirs();
|
destFile.mkdirs();
|
||||||
if(destFile.exists())
|
if(destFile.exists())
|
||||||
{
|
{
|
||||||
destFile.delete();
|
String newFileName = destFile.getAbsolutePath().replaceFirst("\\.png", "-" + System.currentTimeMillis() + ".png");
|
||||||
|
destFile.renameTo(new File(newFileName));
|
||||||
}
|
}
|
||||||
FileUtils.moveFile(outputFile, destFile);
|
FileUtils.moveFile(outputFile, destFile);
|
||||||
LOG.info("Made screenshot at: " + destFile);
|
LOG.info("Made screenshot at: " + destFile);
|
||||||
@ -471,4 +627,48 @@ public class QSeleniumLib
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getLatestChromeDownloadedFileInfo()
|
||||||
|
{
|
||||||
|
driver.get("chrome://downloads/");
|
||||||
|
JavascriptExecutor js = (JavascriptExecutor) driver;
|
||||||
|
WebElement element = (WebElement) js.executeScript("return document.querySelector('downloads-manager').shadowRoot.querySelector('#mainContainer > iron-list > downloads-item').shadowRoot.querySelector('#content')");
|
||||||
|
return (element.getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for autoHighlight
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean getAutoHighlight()
|
||||||
|
{
|
||||||
|
return (this.autoHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for autoHighlight
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setAutoHighlight(boolean autoHighlight)
|
||||||
|
{
|
||||||
|
this.autoHighlight = autoHighlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for autoHighlight
|
||||||
|
*******************************************************************************/
|
||||||
|
public QSeleniumLib withAutoHighlight(boolean autoHighlight)
|
||||||
|
{
|
||||||
|
this.autoHighlight = autoHighlight;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.materialdashboard.tests;
|
||||||
|
|
||||||
|
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Test that goes to a record, clicks a link for another record, then
|
||||||
|
** hits 'e' on keyboard to edit the second record - and confirms that we're
|
||||||
|
** on the edit url for the second record, not the first (a former bug).
|
||||||
|
*******************************************************************************/
|
||||||
|
public class ClickLinkOnRecordThenEditShortcutTest extends QBaseSeleniumTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
||||||
|
{
|
||||||
|
super.addJavalinRoutes(qSeleniumJavalin);
|
||||||
|
qSeleniumJavalin.withRouteToFile("/data/script/1", "data/script/1.json");
|
||||||
|
qSeleniumJavalin.withRouteToFile("/data/scriptRevision/100", "data/scriptRevision/100.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testClickLinkOnRecordThenEditShortcutTest()
|
||||||
|
{
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/developer/script/1", "Hello, Script");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("A", "100").click();
|
||||||
|
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "actions").sendKeys("e");
|
||||||
|
assertTrue(qSeleniumLib.driver.getCurrentUrl().endsWith("/scriptRevision/100/edit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.materialdashboard.tests;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Tests for dashboard table widget with export button
|
||||||
|
*******************************************************************************/
|
||||||
|
public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
||||||
|
{
|
||||||
|
super.addJavalinRoutes(qSeleniumJavalin);
|
||||||
|
qSeleniumJavalin
|
||||||
|
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||||
|
.withRouteToFile("/data/city/count", "data/city/count.json");
|
||||||
|
|
||||||
|
qSeleniumJavalin.withRouteToString("/widget/SampleTableWidget", """
|
||||||
|
{
|
||||||
|
"label": "Sample Table Widget",
|
||||||
|
"footerHTML": "<span class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit' aria-hidden='true'><span class='dashboard-schedule-icon'>schedule</span></span>Updated at 2023-10-17 09:11:38 AM CDT",
|
||||||
|
"columns": [
|
||||||
|
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" },
|
||||||
|
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" }
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
|
||||||
|
{ "id": "2", "name": "<a href='/setup/person/2'>Marge B.</a>" },
|
||||||
|
{ "id": "3", "name": "<a href='/setup/person/3'>Bart J.</a>" }
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testDashboardTableWidgetExport() throws IOException
|
||||||
|
{
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/", "Greetings App");
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
// assert that the table widget rendered its header and some contents //
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget table a", "Homer S.");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// click the export button //
|
||||||
|
/////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelector("#SampleTableWidget h6")
|
||||||
|
.findElement(By.xpath("./.."))
|
||||||
|
.findElement(By.cssSelector("button"))
|
||||||
|
.click();
|
||||||
|
|
||||||
|
qSeleniumLib.waitForCondition("Should have downloaded 1 file", () -> getDownloadedFiles().size() == 1);
|
||||||
|
qSeleniumLib.waitForCondition("Expected file name", () -> getDownloadedFiles().get(0).getName().matches("Sample Table Widget.*.csv"));
|
||||||
|
File csvFile = getDownloadedFiles().get(0);
|
||||||
|
String fileContents = FileUtils.readFileToString(csvFile, StandardCharsets.UTF_8);
|
||||||
|
assertEquals("""
|
||||||
|
"Id","Name"
|
||||||
|
"1","Homer S."
|
||||||
|
"2","Marge B."
|
||||||
|
"3","Bart J."
|
||||||
|
""", fileContents);
|
||||||
|
|
||||||
|
qSeleniumLib.waitForever();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -110,7 +110,7 @@ public class SavedFiltersTest extends QBaseSeleniumTest
|
|||||||
//////////////////////
|
//////////////////////
|
||||||
qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filter").click();
|
qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filter").click();
|
||||||
addQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
|
addQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
|
||||||
qSeleniumLib.waitForSelectorContaining("H5", "Person").click();
|
qSeleniumLib.waitForSelectorContaining("H3", "Person").click();
|
||||||
qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People")
|
qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People")
|
||||||
.findElement(By.cssSelector("CIRCLE"));
|
.findElement(By.cssSelector("CIRCLE"));
|
||||||
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "2");
|
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "2");
|
||||||
|
@ -64,8 +64,6 @@ public class ScriptTableTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.waitForSelectorContaining("DIV.ace_line", "var hello;");
|
qSeleniumLib.waitForSelectorContaining("DIV.ace_line", "var hello;");
|
||||||
qSeleniumLib.waitForSelectorContaining("DIV", "2nd commit");
|
qSeleniumLib.waitForSelectorContaining("DIV", "2nd commit");
|
||||||
qSeleniumLib.waitForSelectorContaining("DIV", "Initial checkin");
|
qSeleniumLib.waitForSelectorContaining("DIV", "Initial checkin");
|
||||||
|
|
||||||
qSeleniumLib.waitForever();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
{
|
{
|
||||||
"tableName": "scriptRevision",
|
"tableName": "scriptRevision",
|
||||||
|
"recordLabel": "Hello, Script Revision",
|
||||||
"values": {
|
"values": {
|
||||||
"id": 100,
|
"id": "100",
|
||||||
|
"name": "Hello, Script Revision",
|
||||||
|
"sequenceNo": "22",
|
||||||
"commitMessage": "Initial checkin",
|
"commitMessage": "Initial checkin",
|
||||||
"author": "Jon Programmer",
|
"author": "Jon Programmer",
|
||||||
"createDate": "2023-02-18T00:47:51Z",
|
"createDate": "2023-02-18T00:47:51Z",
|
||||||
"modifyDate": "2023-02-18T00:47:51Z"
|
"modifyDate": "2023-02-18T00:47:51Z"
|
||||||
},
|
},
|
||||||
"displayValues": {
|
"displayValues": {
|
||||||
|
"id": "1",
|
||||||
|
"name": "Hello, Script Revision",
|
||||||
|
"scriptId": "1",
|
||||||
|
"sequenceNo": "22",
|
||||||
|
"createDate": "2023-02-18T00:47:51Z",
|
||||||
|
"modifyDate": "2023-02-18T00:47:51Z"
|
||||||
},
|
},
|
||||||
"associatedRecords": {
|
"associatedRecords": {
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -131,7 +131,8 @@
|
|||||||
"capabilities": [
|
"capabilities": [
|
||||||
"TABLE_COUNT",
|
"TABLE_COUNT",
|
||||||
"TABLE_GET",
|
"TABLE_GET",
|
||||||
"TABLE_QUERY"
|
"TABLE_QUERY",
|
||||||
|
"TABLE_UPDATE"
|
||||||
],
|
],
|
||||||
"readPermission": true,
|
"readPermission": true,
|
||||||
"insertPermission": true,
|
"insertPermission": true,
|
||||||
@ -301,8 +302,7 @@
|
|||||||
"label": "Greetings App",
|
"label": "Greetings App",
|
||||||
"iconName": "emoji_people",
|
"iconName": "emoji_people",
|
||||||
"widgets": [
|
"widgets": [
|
||||||
"PersonsByCreateDateBarChart",
|
"SampleTableWidget"
|
||||||
"QuickSightChartRenderer"
|
|
||||||
],
|
],
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
@ -737,139 +737,11 @@
|
|||||||
"icon": "/kr-icon.png"
|
"icon": "/kr-icon.png"
|
||||||
},
|
},
|
||||||
"widgets": {
|
"widgets": {
|
||||||
"parcelTrackingDetails": {
|
"SampleTableWidget": {
|
||||||
"name": "parcelTrackingDetails",
|
"name": "SampleTableWidget",
|
||||||
"label": "Tracking Details",
|
"label": "Sample Table Widget",
|
||||||
"type": "childRecordList"
|
|
||||||
},
|
|
||||||
"deposcoSalesOrderLineItems": {
|
|
||||||
"name": "deposcoSalesOrderLineItems",
|
|
||||||
"label": "Line Items",
|
|
||||||
"type": "childRecordList"
|
|
||||||
},
|
|
||||||
"TotalShipmentsByDayBarChart": {
|
|
||||||
"name": "TotalShipmentsByDayBarChart",
|
|
||||||
"label": "Total Shipments By Day",
|
|
||||||
"type": "chart"
|
|
||||||
},
|
|
||||||
"TotalShipmentsByMonthLineChart": {
|
|
||||||
"name": "TotalShipmentsByMonthLineChart",
|
|
||||||
"label": "Total Shipments By Month",
|
|
||||||
"type": "chart"
|
|
||||||
},
|
|
||||||
"YTDShipmentsByCarrierPieChart": {
|
|
||||||
"name": "YTDShipmentsByCarrierPieChart",
|
|
||||||
"label": "Shipments By Carrier Year To Date",
|
|
||||||
"type": "chart"
|
|
||||||
},
|
|
||||||
"TodaysShipmentsStatisticsCard": {
|
|
||||||
"name": "TodaysShipmentsStatisticsCard",
|
|
||||||
"label": "Today's Shipments",
|
|
||||||
"type": "statistics"
|
|
||||||
},
|
|
||||||
"ShipmentsInTransitStatisticsCard": {
|
|
||||||
"name": "ShipmentsInTransitStatisticsCard",
|
|
||||||
"label": "Shipments In Transit",
|
|
||||||
"type": "statistics"
|
|
||||||
},
|
|
||||||
"OpenOrdersStatisticsCard": {
|
|
||||||
"name": "OpenOrdersStatisticsCard",
|
|
||||||
"label": "Open Orders",
|
|
||||||
"type": "statistics"
|
|
||||||
},
|
|
||||||
"ShippingExceptionsStatisticsCard": {
|
|
||||||
"name": "ShippingExceptionsStatisticsCard",
|
|
||||||
"label": "Shipping Exceptions",
|
|
||||||
"type": "statistics"
|
|
||||||
},
|
|
||||||
"WarehouseLocationCards": {
|
|
||||||
"name": "WarehouseLocationCards",
|
|
||||||
"type": "location"
|
|
||||||
},
|
|
||||||
"TotalShipmentsStatisticsCard": {
|
|
||||||
"name": "TotalShipmentsStatisticsCard",
|
|
||||||
"label": "Total Shipments",
|
|
||||||
"type": "statistics"
|
|
||||||
},
|
|
||||||
"SuccessfulDeliveriesStatisticsCard": {
|
|
||||||
"name": "SuccessfulDeliveriesStatisticsCard",
|
|
||||||
"label": "Successful Deliveries",
|
|
||||||
"type": "statistics"
|
|
||||||
},
|
|
||||||
"ServiceFailuresStatisticsCard": {
|
|
||||||
"name": "ServiceFailuresStatisticsCard",
|
|
||||||
"label": "Service Failures",
|
|
||||||
"type": "statistics"
|
|
||||||
},
|
|
||||||
"CarrierVolumeLineChart": {
|
|
||||||
"name": "CarrierVolumeLineChart",
|
|
||||||
"label": "Carrier Volume By Month",
|
|
||||||
"type": "lineChart"
|
|
||||||
},
|
|
||||||
"YTDSpendByCarrierTable": {
|
|
||||||
"name": "YTDSpendByCarrierTable",
|
|
||||||
"label": "Spend By Carrier Year To Date",
|
|
||||||
"type": "table"
|
|
||||||
},
|
|
||||||
"TimeInTransitBarChart": {
|
|
||||||
"name": "TimeInTransitBarChart",
|
|
||||||
"label": "Time In Transit Last 30 Days",
|
|
||||||
"type": "chart"
|
|
||||||
},
|
|
||||||
"OpenBillingWorksheetsTable": {
|
|
||||||
"name": "OpenBillingWorksheetsTable",
|
|
||||||
"label": "Open Billing Worksheets",
|
|
||||||
"type": "table"
|
|
||||||
},
|
|
||||||
"AssociatedParcelInvoicesTable": {
|
|
||||||
"name": "AssociatedParcelInvoicesTable",
|
|
||||||
"label": "Associated Parcel Invoices",
|
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"icon": "receipt"
|
"showExportButton": true
|
||||||
},
|
|
||||||
"BillingWorksheetLinesTable": {
|
|
||||||
"name": "BillingWorksheetLinesTable",
|
|
||||||
"label": "Billing Worksheet Lines",
|
|
||||||
"type": "table"
|
|
||||||
},
|
|
||||||
"RatingIssuesWidget": {
|
|
||||||
"name": "RatingIssuesWidget",
|
|
||||||
"label": "Rating Issues",
|
|
||||||
"type": "html",
|
|
||||||
"icon": "warning",
|
|
||||||
"gridColumns": 6
|
|
||||||
},
|
|
||||||
"UnassignedParcelInvoicesTable": {
|
|
||||||
"name": "UnassignedParcelInvoicesTable",
|
|
||||||
"label": "Unassigned Parcel Invoices",
|
|
||||||
"type": "table"
|
|
||||||
},
|
|
||||||
"ParcelInvoiceSummaryWidget": {
|
|
||||||
"name": "ParcelInvoiceSummaryWidget",
|
|
||||||
"label": "Parcel Invoice Summary",
|
|
||||||
"type": "multiStatistics"
|
|
||||||
},
|
|
||||||
"ParcelInvoiceLineExceptionsSummaryWidget": {
|
|
||||||
"name": "ParcelInvoiceLineExceptionsSummaryWidget",
|
|
||||||
"label": "Parcel Invoice Line Exceptions",
|
|
||||||
"type": "multiStatistics"
|
|
||||||
},
|
|
||||||
"BillingWorksheetStatusStepper": {
|
|
||||||
"name": "BillingWorksheetStatusStepper",
|
|
||||||
"label": "Billing Worksheet Progress",
|
|
||||||
"type": "stepper",
|
|
||||||
"icon": "refresh",
|
|
||||||
"gridColumns": 6
|
|
||||||
},
|
|
||||||
"PersonsByCreateDateBarChart": {
|
|
||||||
"name": "PersonsByCreateDateBarChart",
|
|
||||||
"label": "Persons By Create Date",
|
|
||||||
"type": "barChart"
|
|
||||||
},
|
|
||||||
"QuickSightChartRenderer": {
|
|
||||||
"name": "QuickSightChartRenderer",
|
|
||||||
"label": "Quick Sight",
|
|
||||||
"type": "quickSightChart"
|
|
||||||
},
|
},
|
||||||
"scriptViewer": {
|
"scriptViewer": {
|
||||||
"name": "scriptViewer",
|
"name": "scriptViewer",
|
||||||
|
152
src/test/resources/fixtures/metaData/table/scriptRevision.json
Normal file
152
src/test/resources/fixtures/metaData/table/scriptRevision.json
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"table": {
|
||||||
|
"name": "scriptRevision",
|
||||||
|
"label": "Script Revision",
|
||||||
|
"isHidden": false,
|
||||||
|
"primaryKeyField": "id",
|
||||||
|
"iconName": "history_edu",
|
||||||
|
"fields": {
|
||||||
|
"scriptId": {
|
||||||
|
"name": "scriptId",
|
||||||
|
"label": "Script",
|
||||||
|
"type": "INTEGER",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": true,
|
||||||
|
"isHeavy": false,
|
||||||
|
"possibleValueSourceName": "script",
|
||||||
|
"displayFormat": "%s",
|
||||||
|
"adornments": [
|
||||||
|
{
|
||||||
|
"type": "SIZE",
|
||||||
|
"values": {
|
||||||
|
"width": "large"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "LINK",
|
||||||
|
"values": {
|
||||||
|
"toRecordFromTable": "script"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"apiName": {
|
||||||
|
"name": "apiName",
|
||||||
|
"label": "API Name",
|
||||||
|
"type": "STRING",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": true,
|
||||||
|
"isHeavy": false,
|
||||||
|
"possibleValueSourceName": "apiName",
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
|
"sequenceNo": {
|
||||||
|
"name": "sequenceNo",
|
||||||
|
"label": "Sequence No",
|
||||||
|
"type": "INTEGER",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": true,
|
||||||
|
"isHeavy": false,
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
|
"apiVersion": {
|
||||||
|
"name": "apiVersion",
|
||||||
|
"label": "API Version",
|
||||||
|
"type": "STRING",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": true,
|
||||||
|
"isHeavy": false,
|
||||||
|
"possibleValueSourceName": "apiVersion",
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
|
"commitMessage": {
|
||||||
|
"name": "commitMessage",
|
||||||
|
"label": "Commit Message",
|
||||||
|
"type": "STRING",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": true,
|
||||||
|
"isHeavy": false,
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
|
"modifyDate": {
|
||||||
|
"name": "modifyDate",
|
||||||
|
"label": "Modify Date",
|
||||||
|
"type": "DATE_TIME",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": false,
|
||||||
|
"isHeavy": false,
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"label": "Author",
|
||||||
|
"type": "STRING",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": true,
|
||||||
|
"isHeavy": false,
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"label": "Id",
|
||||||
|
"type": "INTEGER",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": false,
|
||||||
|
"isHeavy": false,
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
|
"createDate": {
|
||||||
|
"name": "createDate",
|
||||||
|
"label": "Create Date",
|
||||||
|
"type": "DATE_TIME",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": false,
|
||||||
|
"isHeavy": false,
|
||||||
|
"displayFormat": "%s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"name": "identity",
|
||||||
|
"label": "Identity",
|
||||||
|
"tier": "T1",
|
||||||
|
"fieldNames": [
|
||||||
|
"id",
|
||||||
|
"scriptId",
|
||||||
|
"sequenceNo"
|
||||||
|
],
|
||||||
|
"icon": {
|
||||||
|
"name": "badge"
|
||||||
|
},
|
||||||
|
"isHidden": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dates",
|
||||||
|
"label": "Dates",
|
||||||
|
"tier": "T3",
|
||||||
|
"fieldNames": [
|
||||||
|
"createDate",
|
||||||
|
"modifyDate"
|
||||||
|
],
|
||||||
|
"icon": {
|
||||||
|
"name": "calendar_month"
|
||||||
|
},
|
||||||
|
"isHidden": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exposedJoins": [],
|
||||||
|
"capabilities": [
|
||||||
|
"TABLE_COUNT",
|
||||||
|
"TABLE_GET",
|
||||||
|
"TABLE_QUERY",
|
||||||
|
"TABLE_INSERT",
|
||||||
|
"TABLE_UPDATE",
|
||||||
|
"QUERY_STATS"
|
||||||
|
],
|
||||||
|
"readPermission": true,
|
||||||
|
"insertPermission": true,
|
||||||
|
"editPermission": true,
|
||||||
|
"deletePermission": true,
|
||||||
|
"usesVariants": false
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user