mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 07:08:44 +00:00
Compare commits
47 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
a26f939859 | |||
e3c511ef6d | |||
e993fcb949 | |||
e144cf3ec7 | |||
6bd6b0370b | |||
448560c427 | |||
791b50b893 | |||
b968705a01 | |||
0949ee9f78 | |||
f41b71d3c7 | |||
57fefe9671 | |||
8a018c34f6 | |||
1b4f70a547 | |||
1f343abbb5 | |||
37a18bbe0d | |||
98cc2ceb00 | |||
e351883d73 | |||
25599d0ca6 | |||
01d18902d7 | |||
580d4a90c9 | |||
eeb1b37d18 | |||
da0947b538 | |||
0c76371d59 | |||
19aebd631a | |||
5aac9ce069 | |||
7ea50dd7bb | |||
53d5bc58c1 | |||
eac166b877 | |||
f49ac38e24 | |||
28bdfc19e8 | |||
fa076733fb | |||
8bebef1abe | |||
37fa578a59 | |||
b6b7d8d8b3 | |||
7bf515554d | |||
069cbf52e1 | |||
7fa42a6eb5 | |||
efc423e819 | |||
a268219156 | |||
9ec442e218 | |||
f1dacea6f5 | |||
b9d81e730f | |||
c7622c12f5 | |||
953c4cc569 | |||
63430e1283 | |||
f189083a5a | |||
efcf137a0f |
@ -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:
|
||||||
@ -115,7 +115,7 @@ workflows:
|
|||||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore: /dev/
|
ignore: /main/
|
||||||
tags:
|
tags:
|
||||||
ignore: /(version|snapshot)-.*/
|
ignore: /(version|snapshot)-.*/
|
||||||
deploy:
|
deploy:
|
||||||
@ -124,7 +124,7 @@ workflows:
|
|||||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: /dev/
|
only: /main/
|
||||||
tags:
|
tags:
|
||||||
only: /(version|snapshot)-.*/
|
only: /(version|snapshot)-.*/
|
||||||
|
|
||||||
|
2101
package-lock.json
generated
2101
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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.79",
|
"@kingsrook/qqq-frontend-core": "1.0.82",
|
||||||
"@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",
|
||||||
|
20
pom.xml
20
pom.xml
@ -29,7 +29,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.16.0-SNAPSHOT</revision>
|
<revision>0.19.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>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-backend-core</artifactId>
|
<artifactId>qqq-backend-core</artifactId>
|
||||||
<version>feature-CTLE-503-optimization-weather-api-data-20230701.011918-3</version>
|
<version>0.17.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
@ -83,7 +83,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.bonigarcia</groupId>
|
<groupId>io.github.bonigarcia</groupId>
|
||||||
<artifactId>webdrivermanager</artifactId>
|
<artifactId>webdrivermanager</artifactId>
|
||||||
<version>5.3.1</version>
|
<version>5.4.1</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>
|
||||||
|
|
||||||
|
119
src/App.tsx
119
src/App.tsx
@ -33,7 +33,8 @@ 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 React, {JSXElementConstructor, Key, ReactElement, useContext, useEffect, useState,} from "react";
|
import jwt_decode from "jwt-decode";
|
||||||
|
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";
|
||||||
import {Md5} from "ts-md5/dist/md5";
|
import {Md5} from "ts-md5/dist/md5";
|
||||||
@ -57,11 +58,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
|||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
export const SESSION_ID_COOKIE_NAME = "sessionId";
|
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
||||||
|
|
||||||
export default function App()
|
export default function App()
|
||||||
{
|
{
|
||||||
const [, setCookie, removeCookie] = useCookies([SESSION_ID_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);
|
||||||
@ -69,8 +70,70 @@ export default function App()
|
|||||||
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
const [branding, setBranding] = useState({} as QBrandingMetaData);
|
||||||
const [metaData, setMetaData] = useState({} as QInstance);
|
const [metaData, setMetaData] = useState({} as QInstance);
|
||||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||||
|
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
// tell the client how to do a logout if it sees a 401 //
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
Client.setUnauthorizedCallback(() =>
|
||||||
|
{
|
||||||
|
logout();
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const oldJSON: any = jwt_decode(oldToken);
|
||||||
|
const newJSON: any = jwt_decode(newToken);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the old (local storage) token is expired, then we need to store the new one //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const oldExp = oldJSON["exp"];
|
||||||
|
if(oldExp * 1000 < (new Date().getTime()))
|
||||||
|
{
|
||||||
|
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||||
|
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
delete newJSON["exp"]
|
||||||
|
delete newJSON["iat"]
|
||||||
|
delete oldJSON["exp"]
|
||||||
|
delete oldJSON["iat"]
|
||||||
|
|
||||||
|
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||||
|
if(different)
|
||||||
|
{
|
||||||
|
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||||
|
}
|
||||||
|
return (different);
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.log("Caught in shouldStoreNewToken: " + e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (loadingToken)
|
if (loadingToken)
|
||||||
@ -92,20 +155,38 @@ export default function App()
|
|||||||
{
|
{
|
||||||
console.log("Loading token from auth0...");
|
console.log("Loading token from auth0...");
|
||||||
const accessToken = await getAccessTokenSilently();
|
const accessToken = await getAccessTokenSilently();
|
||||||
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
const lsAccessToken = localStorage.getItem("accessToken");
|
||||||
// we've stopped using session id cook with auth0, so make sure it is not set. //
|
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
{
|
||||||
removeCookie(SESSION_ID_COOKIE_NAME);
|
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||||
|
const newSessionUuid = await qController.manageSession(accessToken, null);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||||
|
|
||||||
|
localStorage.setItem("accessToken", 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();
|
||||||
|
|
||||||
|
setLoggedInUser(user);
|
||||||
console.log("Token load complete.");
|
console.log("Token load complete.");
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
{
|
{
|
||||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
localStorage.removeItem("accessToken")
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
logout();
|
logout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -116,9 +197,9 @@ 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(null);
|
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
qController.setGotAuthentication();
|
||||||
|
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||||
console.log("Token generation complete.");
|
console.log("Token generation complete.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -149,7 +230,7 @@ export default function App()
|
|||||||
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
|
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
|
||||||
const [sideNavRoutes, setSideNavRoutes] = useState([]);
|
const [sideNavRoutes, setSideNavRoutes] = useState([]);
|
||||||
const [appRoutes, setAppRoutes] = useState(null as any);
|
const [appRoutes, setAppRoutes] = useState(null as any);
|
||||||
const [pathToLabelMap, setPathToLabelMap] = useState({} as {[path: string]: string});
|
const [pathToLabelMap, setPathToLabelMap] = useState({} as { [path: string]: string });
|
||||||
|
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
// load qqq meta data to make more routes //
|
// load qqq meta data to make more routes //
|
||||||
@ -267,14 +348,14 @@ export default function App()
|
|||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: app.name,
|
key: app.name,
|
||||||
route: path,
|
route: path,
|
||||||
component: <RecordQuery table={table} key={table.name}/>,
|
component: <RecordQuery table={table} key={table.name} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
name: `${app.label}`,
|
name: `${app.label}`,
|
||||||
key: app.name,
|
key: app.name,
|
||||||
route: `${path}/savedFilter/:id`,
|
route: `${path}/savedFilter/:id`,
|
||||||
component: <RecordQuery table={table} key={table.name}/>,
|
component: <RecordQuery table={table} key={table.name} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
routeList.push({
|
routeList.push({
|
||||||
@ -429,11 +510,11 @@ export default function App()
|
|||||||
|
|
||||||
let profileRoutes = {};
|
let profileRoutes = {};
|
||||||
const gravatarBase = "https://www.gravatar.com/avatar/";
|
const gravatarBase = "https://www.gravatar.com/avatar/";
|
||||||
const hash = Md5.hashStr(user?.email || "user");
|
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
||||||
const profilePicture = `${gravatarBase}${hash}`;
|
const profilePicture = `${gravatarBase}${hash}`;
|
||||||
profileRoutes = {
|
profileRoutes = {
|
||||||
type: "collapse",
|
type: "collapse",
|
||||||
name: user?.name,
|
name: loggedInUser?.name ?? "Anonymous",
|
||||||
key: "username",
|
key: "username",
|
||||||
noCollapse: true,
|
noCollapse: true,
|
||||||
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
||||||
@ -469,7 +550,7 @@ export default function App()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pathToLabelMap: {[path: string]: string} = {}
|
const pathToLabelMap: {[path: string]: string} = {}
|
||||||
for(let i =0; i<appRoutesList.length; i++)
|
for (let i = 0; i < appRoutesList.length; i++)
|
||||||
{
|
{
|
||||||
const route = appRoutesList[i];
|
const route = appRoutesList[i];
|
||||||
pathToLabelMap[route.route] = route.name;
|
pathToLabelMap[route.route] = route.name;
|
||||||
@ -495,7 +576,10 @@ export default function App()
|
|||||||
{
|
{
|
||||||
if ((e as QException).status === "401")
|
if ((e as QException).status === "401")
|
||||||
{
|
{
|
||||||
|
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
localStorage.removeItem("accessToken")
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
// todo - this is auth0 logout... make more generic //
|
// todo - this is auth0 logout... make more generic //
|
||||||
@ -596,7 +680,7 @@ export default function App()
|
|||||||
}}>
|
}}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<CommandMenu metaData={metaData}/>
|
<CommandMenu metaData={metaData} />
|
||||||
<Sidenav
|
<Sidenav
|
||||||
color={sidenavColor}
|
color={sidenavColor}
|
||||||
icon={branding.icon}
|
icon={branding.icon}
|
||||||
@ -604,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}
|
||||||
/>
|
/>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import {useAuth0} from "@auth0/auth0-react";
|
import {useAuth0} from "@auth0/auth0-react";
|
||||||
import React, {useEffect} from "react";
|
import React, {useEffect} from "react";
|
||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
import {SESSION_ID_COOKIE_NAME} from "App";
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -33,13 +33,13 @@ interface Props
|
|||||||
function HandleAuthorizationError({errorMessage}: Props)
|
function HandleAuthorizationError({errorMessage}: Props)
|
||||||
{
|
{
|
||||||
|
|
||||||
const [, , removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
|
const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const {logout} = useAuth0();
|
const {logout} = useAuth0();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
logout();
|
logout();
|
||||||
removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"});
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -346,6 +346,12 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
const fieldName = section.fieldNames[j];
|
const fieldName = section.fieldNames[j];
|
||||||
const field = tableMetaData.fields.get(fieldName);
|
const field = tableMetaData.fields.get(fieldName);
|
||||||
|
|
||||||
|
if(!field)
|
||||||
|
{
|
||||||
|
console.log(`Omitting un-found field ${fieldName} from form`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
|
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
|
||||||
// || (or) we're on the insert screen in which case, only show editable fields. //
|
// || (or) we're on the insert screen in which case, only show editable fields. //
|
||||||
|
@ -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,21 +158,30 @@ 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)
|
|
||||||
{
|
{
|
||||||
setError("Record not found.");
|
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant)
|
||||||
setTimeout(() => setError(""), 3000);
|
if (queryResult.length == 0)
|
||||||
|
{
|
||||||
|
setError("Record not found.");
|
||||||
|
setTimeout(() => setError(""), 3000);
|
||||||
|
}
|
||||||
|
else if (queryResult.length == 1)
|
||||||
|
{
|
||||||
|
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setError("More than 1 record found...");
|
||||||
|
setTimeout(() => setError(""), 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(queryResult.length == 1)
|
catch(e)
|
||||||
{
|
{
|
||||||
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${queryResult[0].values.get(props.tableMetaData.primaryKeyField)}`);
|
// @ts-ignore
|
||||||
close();
|
setError(`Error: ${(e && e.message) ? e.message : e}`);
|
||||||
}
|
setTimeout(() => setError(""), 6000);
|
||||||
else
|
|
||||||
{
|
|
||||||
setError("More than 1 record found...");
|
|
||||||
setTimeout(() => setError(""), 3000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ 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/components/legacy/colors";
|
||||||
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ export interface WidgetData
|
|||||||
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;
|
||||||
@ -70,6 +72,7 @@ Widget.defaultProps = {
|
|||||||
widgetMetaData: {},
|
widgetMetaData: {},
|
||||||
widgetData: {},
|
widgetData: {},
|
||||||
labelAdditionalComponentsLeft: [],
|
labelAdditionalComponentsLeft: [],
|
||||||
|
labelAdditionalElementsLeft: [],
|
||||||
labelAdditionalComponentsRight: [],
|
labelAdditionalComponentsRight: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -88,34 +91,8 @@ 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
|
|
||||||
{
|
|
||||||
label: string;
|
|
||||||
to: string
|
|
||||||
|
|
||||||
constructor(label: string, to: string)
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.label = label;
|
|
||||||
this.to = to;
|
|
||||||
}
|
|
||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
|
||||||
{this.to ? <Link to={this.to}>{this.label}</Link> : null}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -141,8 +118,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 +128,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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -227,7 +176,7 @@ export class Dropdown extends LabelComponent
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -251,7 +200,7 @@ export class ReloadControl extends LabelComponent
|
|||||||
<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>refresh</Icon></Button></Tooltip>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -372,7 +321,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 +343,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadWidget(dropdownData)
|
reloadWidget(dropdownData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +371,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 +383,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,6 +399,7 @@ 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);
|
||||||
@ -530,6 +480,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{props.labelAdditionalElementsLeft}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
|
@ -216,7 +216,7 @@ export default function DataBagViewer({dataBagId}: Props): JSX.Element
|
|||||||
primaryTypographyProps={{fontSize: "1rem"}}
|
primaryTypographyProps={{fontSize: "1rem"}}
|
||||||
secondaryTypographyProps={{fontSize: ".85rem"}}
|
secondaryTypographyProps={{fontSize: ".85rem"}}
|
||||||
primary={
|
primary={
|
||||||
<div style={{whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}} title={version.values.get("commitMessage")}>
|
<div style={{overflow: "hidden", textOverflow: "ellipsis", maxHeight: "5rem"}} title={version.values.get("commitMessage")}>
|
||||||
{currentVersionId == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
|
{currentVersionId == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
|
||||||
{version.values.get("commitMessage")}
|
{version.values.get("commitMessage")}
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,10 +22,14 @@
|
|||||||
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 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 +51,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 +81,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,39 +102,42 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
setRows(rows);
|
setRows(rows);
|
||||||
setRecords(records)
|
setRecords(records)
|
||||||
setColumns(columns);
|
setColumns(columns);
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const exportCallback = () =>
|
let csv = "";
|
||||||
{
|
for (let i = 0; i < allColumns.length; i++)
|
||||||
let csv = "";
|
|
||||||
for (let i = 0; i < allColumns.length; i++)
|
|
||||||
{
|
|
||||||
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`
|
|
||||||
}
|
|
||||||
csv += "\n";
|
|
||||||
|
|
||||||
for (let i = 0; i < records.length; i++)
|
|
||||||
{
|
|
||||||
for (let j = 0; j < allColumns.length; j++)
|
|
||||||
{
|
{
|
||||||
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
|
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`
|
||||||
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
|
||||||
}
|
}
|
||||||
csv += "\n";
|
csv += "\n";
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
for (let i = 0; i < records.length; i++)
|
||||||
HtmlUtils.download(fileName, csv);
|
{
|
||||||
}
|
for (let j = 0; j < allColumns.length; j++)
|
||||||
|
{
|
||||||
|
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
|
||||||
|
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
||||||
|
}
|
||||||
|
csv += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".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 +159,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.375rem">
|
||||||
|
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon>save_alt</Icon></Button></Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////
|
////////////////////
|
||||||
// add new button //
|
// add new button //
|
||||||
@ -184,7 +213,7 @@ 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}
|
||||||
>
|
>
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
|
@ -314,7 +314,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
primaryTypographyProps={{fontSize: "1rem"}}
|
primaryTypographyProps={{fontSize: "1rem"}}
|
||||||
secondaryTypographyProps={{fontSize: ".85rem"}}
|
secondaryTypographyProps={{fontSize: ".85rem"}}
|
||||||
primary={
|
primary={
|
||||||
<div style={{whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}} title={version.values.get("commitMessage")}>
|
<div style={{overflow: "hidden", textOverflow: "ellipsis", maxHeight: "5rem"}} title={version.values.get("commitMessage")}>
|
||||||
{scriptRecord.values.get("currentScriptRevisionId") == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
|
{scriptRecord.values.get("currentScriptRevisionId") == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
|
||||||
{version.values.get("commitMessage")}
|
{version.values.get("commitMessage")}
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,11 +21,15 @@
|
|||||||
|
|
||||||
|
|
||||||
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 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 +47,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 +62,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 +98,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.375rem">
|
||||||
|
<Tooltip title="Export"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}><Icon>save_alt</Icon></Button></Tooltip>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget
|
<Widget
|
||||||
@ -116,7 +137,7 @@ 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}
|
||||||
|
@ -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 //
|
||||||
@ -221,13 +228,14 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
const download = (url: string, fileName: string) =>
|
const download = (url: string, fileName: string) =>
|
||||||
{
|
{
|
||||||
const qController = Client.getInstance();
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo - this could be simplified, i think? //
|
||||||
|
// it was originally built like this when we had to submit full access token to backend... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", url);
|
xhr.open("POST", url);
|
||||||
xhr.responseType = "blob";
|
xhr.responseType = "blob";
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
@ -368,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>();
|
||||||
@ -385,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 (
|
||||||
@ -741,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);
|
||||||
@ -794,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 //
|
||||||
@ -985,6 +993,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
setQJobRunning(null);
|
setQJobRunning(null);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.warn(`Process response was not of an expected type (need an npm clean?) ${JSON.stringify(lastProcessResponse)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [lastProcessResponse]);
|
}, [lastProcessResponse]);
|
||||||
|
|
||||||
@ -1071,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"))
|
||||||
{
|
{
|
||||||
@ -1086,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
|
||||||
@ -1143,6 +1164,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tableMetaData)
|
||||||
|
{
|
||||||
|
queryStringPairsForInit.push(`tableName=${encodeURIComponent(tableMetaData.name)}`);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const processResponse = await Client.getInstance().processInit(processName, queryStringPairsForInit.join("&"));
|
const processResponse = await Client.getInstance().processInit(processName, queryStringPairsForInit.join("&"));
|
||||||
@ -1178,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[] = [];
|
||||||
|
@ -88,7 +88,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
}
|
}
|
||||||
const processResult = await qController.processRun("columnStats", formData);
|
const processResult = await qController.processRun("columnStats", formData);
|
||||||
|
|
||||||
setStatusString(null)
|
setStatusString(null);
|
||||||
if (processResult instanceof QJobError)
|
if (processResult instanceof QJobError)
|
||||||
{
|
{
|
||||||
const jobError = processResult as QJobError;
|
const jobError = processResult as QJobError;
|
||||||
@ -107,7 +107,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
const newStatsFields = [] as QFieldMetaData[];
|
const newStatsFields = [] as QFieldMetaData[];
|
||||||
for(let i = 0; i<statFieldObjects.length; i++)
|
for(let i = 0; i<statFieldObjects.length; i++)
|
||||||
{
|
{
|
||||||
newStatsFields.push(new QFieldMetaData(statFieldObjects[i]))
|
newStatsFields.push(new QFieldMetaData(statFieldObjects[i]));
|
||||||
}
|
}
|
||||||
setStatsFields(newStatsFields);
|
setStatsFields(newStatsFields);
|
||||||
}
|
}
|
||||||
@ -139,15 +139,15 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
fakeTableMetaData.fields = new Map<string, QFieldMetaData>();
|
fakeTableMetaData.fields = new Map<string, QFieldMetaData>();
|
||||||
fakeTableMetaData.fields.set(fieldMetaData.name, fieldMetaData);
|
fakeTableMetaData.fields.set(fieldMetaData.name, fieldMetaData);
|
||||||
fakeTableMetaData.fields.set("count", new QFieldMetaData({name: "count", label: "Count", type: "INTEGER"}));
|
fakeTableMetaData.fields.set("count", new QFieldMetaData({name: "count", label: "Count", type: "INTEGER"}));
|
||||||
|
fakeTableMetaData.fields.set("percent", new QFieldMetaData({name: "percent", label: "Percent", type: "DECIMAL"}));
|
||||||
fakeTableMetaData.sections = [] as QTableSection[];
|
fakeTableMetaData.sections = [] as QTableSection[];
|
||||||
fakeTableMetaData.sections.push(new QTableSection({fieldNames: [fieldMetaData.name, "count"]}));
|
fakeTableMetaData.sections.push(new QTableSection({fieldNames: [fieldMetaData.name, "count", "percent"]}));
|
||||||
|
|
||||||
const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData);
|
const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData);
|
||||||
const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, null, null, "bySection");
|
const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, null, null, "bySection");
|
||||||
|
|
||||||
columns.forEach((c) =>
|
columns.forEach((c) =>
|
||||||
{
|
{
|
||||||
c.width = 200;
|
|
||||||
c.filterable = false;
|
c.filterable = false;
|
||||||
c.hideable = false;
|
c.hideable = false;
|
||||||
})
|
})
|
||||||
@ -162,7 +162,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
function CustomPagination()
|
function CustomPagination()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box pr={3}>
|
<Box pr={3} fontSize="0.85rem">
|
||||||
{rows && rows.length && countDistinct && rows.length < countDistinct ? <span>Showing the first {rows.length.toLocaleString()} of {countDistinct.toLocaleString()} values</span> : <></>}
|
{rows && rows.length && countDistinct && rows.length < countDistinct ? <span>Showing the first {rows.length.toLocaleString()} of {countDistinct.toLocaleString()} values</span> : <></>}
|
||||||
{rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length == 1 ? <span>Showing the only value</span> : <></>}
|
{rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length == 1 ? <span>Showing the only value</span> : <></>}
|
||||||
{rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length > 1 ? <span>Showing all {rows.length.toLocaleString()} values</span> : <></>}
|
{rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length > 1 ? <span>Showing all {rows.length.toLocaleString()} values</span> : <></>}
|
||||||
@ -172,9 +172,9 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
|
|
||||||
const refresh = () =>
|
const refresh = () =>
|
||||||
{
|
{
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
setStatusString("Refreshing...")
|
setStatusString("Refreshing...");
|
||||||
}
|
};
|
||||||
|
|
||||||
const doExport = () =>
|
const doExport = () =>
|
||||||
{
|
{
|
||||||
@ -188,7 +188,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
|
|
||||||
const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
HtmlUtils.download(fileName, csv);
|
HtmlUtils.download(fileName, csv);
|
||||||
}
|
};
|
||||||
|
|
||||||
function Loading()
|
function Loading()
|
||||||
{
|
{
|
||||||
|
@ -71,6 +71,7 @@ import DataGridUtils from "qqq/utils/DataGridUtils";
|
|||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||||
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
||||||
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
|
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
|
||||||
@ -81,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
|
||||||
{
|
{
|
||||||
@ -232,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)
|
||||||
@ -537,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
|
||||||
@ -553,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);
|
||||||
@ -628,6 +626,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
let models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, null, searchParams, filterLocalStorageKey, sortLocalStorageKey);
|
let models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, null, searchParams, filterLocalStorageKey, sortLocalStorageKey);
|
||||||
setFilterModel(models.filter);
|
setFilterModel(models.filter);
|
||||||
setColumnSortModel(models.sort);
|
setColumnSortModel(models.sort);
|
||||||
|
setWarningAlert(models.warning);
|
||||||
|
|
||||||
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage));
|
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -708,16 +708,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
if (tableMetaData?.exposedJoins)
|
if (tableMetaData?.exposedJoins)
|
||||||
{
|
{
|
||||||
const visibleJoinTables = getVisibleJoinTables();
|
const visibleJoinTables = getVisibleJoinTables();
|
||||||
|
queryJoins = TableUtils.getQueryJoins(tableMetaData, visibleJoinTables);
|
||||||
queryJoins = [];
|
|
||||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
|
||||||
{
|
|
||||||
const join = tableMetaData.exposedJoins[i];
|
|
||||||
if (visibleJoinTables.has(join.joinTable.name))
|
|
||||||
{
|
|
||||||
queryJoins.push(new QueryJoin(join.joinTable.name, true, "LEFT"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -931,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);
|
||||||
}
|
}
|
||||||
@ -1145,7 +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}" >
|
||||||
<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>
|
||||||
@ -1194,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 "";
|
||||||
@ -1222,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()}`);
|
||||||
@ -1400,6 +1390,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null);
|
const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null);
|
||||||
handleFilterChange(models.filter);
|
handleFilterChange(models.filter);
|
||||||
handleSortChange(models.sort, models.filter);
|
handleSortChange(models.sort, models.filter);
|
||||||
|
setWarningAlert(models.warning);
|
||||||
|
|
||||||
localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString());
|
localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -1431,35 +1423,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return (qRecord);
|
return (qRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldAndTable = (fieldName: string): [QFieldMetaData, QTableMetaData] =>
|
|
||||||
{
|
|
||||||
if(fieldName.indexOf(".") > -1)
|
|
||||||
{
|
|
||||||
const nameParts = fieldName.split(".", 2);
|
|
||||||
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
|
|
||||||
{
|
|
||||||
const join = tableMetaData?.exposedJoins[i];
|
|
||||||
if(join?.joinTable.name == nameParts[0])
|
|
||||||
{
|
|
||||||
return ([join.joinTable.fields.get(nameParts[1]), join.joinTable]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ([tableMetaData.fields.get(fieldName), tableMetaData]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyColumnValues = async (column: GridColDef) =>
|
const copyColumnValues = async (column: GridColDef) =>
|
||||||
{
|
{
|
||||||
let data = "";
|
let data = "";
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
if (latestQueryResults && latestQueryResults.length)
|
if (latestQueryResults && latestQueryResults.length)
|
||||||
{
|
{
|
||||||
let [qFieldMetaData, fieldTable] = getFieldAndTable(column.field);
|
let [qFieldMetaData, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field);
|
||||||
for (let i = 0; i < latestQueryResults.length; i++)
|
for (let i = 0; i < latestQueryResults.length; i++)
|
||||||
{
|
{
|
||||||
let record = latestQueryResults[i] as QRecord;
|
let record = latestQueryResults[i] as QRecord;
|
||||||
@ -1489,7 +1459,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setFilterForColumnStats(buildQFilter(tableMetaData, filterModel));
|
setFilterForColumnStats(buildQFilter(tableMetaData, filterModel));
|
||||||
setColumnStatsFieldName(column.field);
|
setColumnStatsFieldName(column.field);
|
||||||
|
|
||||||
const [field, fieldTable] = getFieldAndTable(column.field);
|
const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field);
|
||||||
setColumnStatsField(field);
|
setColumnStatsField(field);
|
||||||
setColumnStatsFieldTableName(fieldTable.name);
|
setColumnStatsFieldTableName(fieldTable.name);
|
||||||
};
|
};
|
||||||
@ -1913,9 +1883,26 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
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 (
|
||||||
|
<BaseLayout>
|
||||||
|
<TableVariantDialog table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
|
||||||
|
{
|
||||||
|
setTableVariantPromptOpen(false);
|
||||||
|
setTableVariant(value);
|
||||||
|
}} />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} autoOpen={true} buttonVisible={false} mayClose={false} />
|
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} tableVariant={tableVariant} autoOpen={true} buttonVisible={false} mayClose={false} subHeader={
|
||||||
|
<Box mb={2}>
|
||||||
|
{getTableVariantHeader()}
|
||||||
|
</Box>
|
||||||
|
} />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1962,7 +1949,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
(warningAlert) ? (
|
(warningAlert) ? (
|
||||||
<Collapse in={Boolean(warningAlert)}>
|
<Collapse in={Boolean(warningAlert)}>
|
||||||
<Alert color="warning" sx={{mb: 3}} onClose={() => setWarningAlert(null)}>{warningAlert}</Alert>
|
<Alert color="warning" icon={<Icon>warning</Icon>} sx={{mb: 3}} onClose={() => setWarningAlert(null)}>{warningAlert}</Alert>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
|
|||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
|
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
|
||||||
import {Alert, Typography} from "@mui/material";
|
import {Alert, Typography} from "@mui/material";
|
||||||
import Avatar from "@mui/material/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
@ -103,7 +104,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
||||||
const [actionsMenu, setActionsMenu] = useState(null);
|
const [actionsMenu, setActionsMenu] = useState(null);
|
||||||
const [notFoundMessage, setNotFoundMessage] = useState(null as string);
|
const [notFoundMessage, setNotFoundMessage] = useState(null as string);
|
||||||
const [errorMessage, setErrorMessage] = useState(null as string)
|
const [errorMessage, setErrorMessage] = useState(null as string);
|
||||||
const [successMessage, setSuccessMessage] = useState(null as string);
|
const [successMessage, setSuccessMessage] = useState(null as string);
|
||||||
const [warningMessage, setWarningMessage] = useState(null as string);
|
const [warningMessage, setWarningMessage] = useState(null as string);
|
||||||
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
||||||
@ -192,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 = () =>
|
||||||
{
|
{
|
||||||
@ -325,6 +326,31 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
reload();
|
reload();
|
||||||
}, [location.pathname, location.hash]);
|
}, [location.pathname, location.hash]);
|
||||||
|
|
||||||
|
const getVisibleJoinTables = (tableMetaData: QTableMetaData): Set<string> =>
|
||||||
|
{
|
||||||
|
const visibleJoinTables = new Set<string>();
|
||||||
|
|
||||||
|
for (let i = 0; i < tableMetaData?.sections.length; i++)
|
||||||
|
{
|
||||||
|
const section = tableMetaData?.sections[i];
|
||||||
|
if (section.isHidden || !section.fieldNames || !section.fieldNames.length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.fieldNames.forEach((fieldName) =>
|
||||||
|
{
|
||||||
|
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||||
|
if(tableForField && tableForField.name != tableMetaData.name)
|
||||||
|
{
|
||||||
|
visibleJoinTables.add(tableForField.name);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (visibleJoinTables);
|
||||||
|
};
|
||||||
|
|
||||||
if (!asyncLoadInited)
|
if (!asyncLoadInited)
|
||||||
{
|
{
|
||||||
setAsyncLoadInited(true);
|
setAsyncLoadInited(true);
|
||||||
@ -368,13 +394,20 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
setActiveModalProcess(launchingProcess);
|
setActiveModalProcess(launchingProcess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let queryJoins: QueryJoin[] = null;
|
||||||
|
const visibleJoinTables = getVisibleJoinTables(tableMetaData);
|
||||||
|
if(visibleJoinTables.size > 0)
|
||||||
|
{
|
||||||
|
queryJoins = TableUtils.getQueryJoins(tableMetaData, visibleJoinTables);
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////
|
/////////////////////
|
||||||
// load the record //
|
// load the record //
|
||||||
/////////////////////
|
/////////////////////
|
||||||
let record: QRecord;
|
let record: QRecord;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
record = await qController.get(tableName, id, tableVariant);
|
record = await qController.get(tableName, id, tableVariant, null, queryJoins);
|
||||||
setRecord(record);
|
setRecord(record);
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
@ -465,17 +498,22 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
const fields = (
|
const fields = (
|
||||||
<Box key={section.name} display="flex" flexDirection="column" py={1} pr={2}>
|
<Box key={section.name} display="flex" flexDirection="column" py={1} pr={2}>
|
||||||
{
|
{
|
||||||
section.fieldNames.map((fieldName: string) => (
|
section.fieldNames.map((fieldName: string) =>
|
||||||
<Box key={fieldName} flexDirection="row" pr={2}>
|
{
|
||||||
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)">
|
let [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||||
{tableMetaData.fields.get(fieldName).label}:
|
let label = field.label;
|
||||||
<div style={{display: "inline-block", width: 0}}> </div>
|
return (
|
||||||
</Typography>
|
<Box key={fieldName} flexDirection="row" pr={2}>
|
||||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
|
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)">
|
||||||
{ValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record, "view")}
|
{label}:
|
||||||
</Typography>
|
<div style={{display: "inline-block", width: 0}}> </div>
|
||||||
</Box>
|
</Typography>
|
||||||
))
|
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
|
||||||
|
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@ -893,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -259,7 +259,6 @@ export default class DataGridUtils
|
|||||||
public static makeColumnFromField = (field: QFieldMetaData, tableMetaData: QTableMetaData, namePrefix?: string, labelPrefix?: string): GridColDef =>
|
public static makeColumnFromField = (field: QFieldMetaData, tableMetaData: QTableMetaData, namePrefix?: string, labelPrefix?: string): GridColDef =>
|
||||||
{
|
{
|
||||||
let columnType = "string";
|
let columnType = "string";
|
||||||
let columnWidth = 200;
|
|
||||||
let filterOperators: GridFilterOperator<any>[] = QGridStringOperators;
|
let filterOperators: GridFilterOperator<any>[] = QGridStringOperators;
|
||||||
|
|
||||||
if (field.possibleValueSourceName)
|
if (field.possibleValueSourceName)
|
||||||
@ -273,28 +272,18 @@ export default class DataGridUtils
|
|||||||
case QFieldType.DECIMAL:
|
case QFieldType.DECIMAL:
|
||||||
case QFieldType.INTEGER:
|
case QFieldType.INTEGER:
|
||||||
columnType = "number";
|
columnType = "number";
|
||||||
columnWidth = 100;
|
|
||||||
|
|
||||||
if (field.name === tableMetaData.primaryKeyField && field.label.length < 3)
|
|
||||||
{
|
|
||||||
columnWidth = 75;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterOperators = QGridNumericOperators;
|
filterOperators = QGridNumericOperators;
|
||||||
break;
|
break;
|
||||||
case QFieldType.DATE:
|
case QFieldType.DATE:
|
||||||
columnType = "date";
|
columnType = "date";
|
||||||
columnWidth = 100;
|
|
||||||
filterOperators = QGridDateOperators;
|
filterOperators = QGridDateOperators;
|
||||||
break;
|
break;
|
||||||
case QFieldType.DATE_TIME:
|
case QFieldType.DATE_TIME:
|
||||||
columnType = "dateTime";
|
columnType = "dateTime";
|
||||||
columnWidth = 200;
|
|
||||||
filterOperators = QGridDateTimeOperators;
|
filterOperators = QGridDateTimeOperators;
|
||||||
break;
|
break;
|
||||||
case QFieldType.BOOLEAN:
|
case QFieldType.BOOLEAN:
|
||||||
columnType = "string"; // using boolean gives an odd 'no' for nulls.
|
columnType = "string"; // using boolean gives an odd 'no' for nulls.
|
||||||
columnWidth = 75;
|
|
||||||
filterOperators = QGridBooleanOperators;
|
filterOperators = QGridBooleanOperators;
|
||||||
break;
|
break;
|
||||||
case QFieldType.BLOB:
|
case QFieldType.BLOB:
|
||||||
@ -305,6 +294,31 @@ export default class DataGridUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headerName = labelPrefix ? labelPrefix + field.label : field.label;
|
||||||
|
let fieldName = namePrefix ? namePrefix + field.name : field.name;
|
||||||
|
|
||||||
|
const column: GridColDef = {
|
||||||
|
field: fieldName,
|
||||||
|
type: columnType,
|
||||||
|
headerName: headerName,
|
||||||
|
width: DataGridUtils.getColumnWidthForField(field, tableMetaData),
|
||||||
|
renderCell: null as any,
|
||||||
|
filterOperators: filterOperators,
|
||||||
|
};
|
||||||
|
|
||||||
|
column.renderCell = (cellValues: any) => (
|
||||||
|
(cellValues.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (column);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static getColumnWidthForField = (field: QFieldMetaData, table?: QTableMetaData): number =>
|
||||||
|
{
|
||||||
if (field.hasAdornment(AdornmentType.SIZE))
|
if (field.hasAdornment(AdornmentType.SIZE))
|
||||||
{
|
{
|
||||||
const sizeAdornment = field.getAdornment(AdornmentType.SIZE);
|
const sizeAdornment = field.getAdornment(AdornmentType.SIZE);
|
||||||
@ -318,7 +332,7 @@ export default class DataGridUtils
|
|||||||
]);
|
]);
|
||||||
if (widths.has(width))
|
if (widths.has(width))
|
||||||
{
|
{
|
||||||
columnWidth = widths.get(width);
|
return widths.get(width);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -326,23 +340,31 @@ export default class DataGridUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let headerName = labelPrefix ? labelPrefix + field.label : field.label;
|
if(field.possibleValueSourceName)
|
||||||
let fieldName = namePrefix ? namePrefix + field.name : field.name;
|
{
|
||||||
|
return (200);
|
||||||
|
}
|
||||||
|
|
||||||
const column: GridColDef = {
|
switch (field.type)
|
||||||
field: fieldName,
|
{
|
||||||
type: columnType,
|
case QFieldType.DECIMAL:
|
||||||
headerName: headerName,
|
case QFieldType.INTEGER:
|
||||||
width: columnWidth,
|
|
||||||
renderCell: null as any,
|
|
||||||
filterOperators: filterOperators,
|
|
||||||
};
|
|
||||||
|
|
||||||
column.renderCell = (cellValues: any) => (
|
if (table && field.name === table.primaryKeyField && field.label.length < 3)
|
||||||
(cellValues.value)
|
{
|
||||||
);
|
return (75);
|
||||||
|
}
|
||||||
|
|
||||||
return (column);
|
return (100);
|
||||||
|
case QFieldType.DATE:
|
||||||
|
return (100);
|
||||||
|
case QFieldType.DATE_TIME:
|
||||||
|
return (200);
|
||||||
|
case QFieldType.BOOLEAN:
|
||||||
|
return (75);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,13 +99,6 @@ export default class HtmlUtils
|
|||||||
form.setAttribute("target", "downloadIframe");
|
form.setAttribute("target", "downloadIframe");
|
||||||
iframe.appendChild(form);
|
iframe.appendChild(form);
|
||||||
|
|
||||||
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");
|
||||||
@ -113,11 +110,17 @@ 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:"))
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const openInWindow = window.open("", "_blank");
|
const openInWindow = window.open("", "_blank");
|
||||||
openInWindow.document.write(`<html lang="en">
|
openInWindow.document.write(`<html lang="en">
|
||||||
<body style="margin: 0">
|
<body style="margin: 0">
|
||||||
@ -144,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;
|
||||||
|
@ -31,6 +31,7 @@ import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilt
|
|||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression";
|
import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression";
|
||||||
import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
|
import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
|
||||||
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
|
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
|
||||||
@ -375,10 +376,11 @@ class FilterUtils
|
|||||||
** Get the default filter to use on the page - either from given filter string, query string param, or
|
** Get the default filter to use on the page - either from given filter string, query string param, or
|
||||||
** local storage, or a default (empty).
|
** local storage, or a default (empty).
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static async determineFilterAndSortModels(qController: QController, tableMetaData: QTableMetaData, filterString: string, searchParams: URLSearchParams, filterLocalStorageKey: string, sortLocalStorageKey: string): Promise<{ filter: GridFilterModel, sort: GridSortItem[] }>
|
public static async determineFilterAndSortModels(qController: QController, tableMetaData: QTableMetaData, filterString: string, searchParams: URLSearchParams, filterLocalStorageKey: string, sortLocalStorageKey: string): Promise<{ filter: GridFilterModel, sort: GridSortItem[], warning: string }>
|
||||||
{
|
{
|
||||||
let defaultFilter = {items: []} as GridFilterModel;
|
let defaultFilter = {items: []} as GridFilterModel;
|
||||||
let defaultSort = [] as GridSortItem[];
|
let defaultSort = [] as GridSortItem[];
|
||||||
|
let warningParts = [] as string[];
|
||||||
|
|
||||||
if (tableMetaData && tableMetaData.fields !== undefined)
|
if (tableMetaData && tableMetaData.fields !== undefined)
|
||||||
{
|
{
|
||||||
@ -396,30 +398,11 @@ class FilterUtils
|
|||||||
for (let i = 0; i < qQueryFilter?.criteria?.length; i++)
|
for (let i = 0; i < qQueryFilter?.criteria?.length; i++)
|
||||||
{
|
{
|
||||||
const criteria = qQueryFilter.criteria[i];
|
const criteria = qQueryFilter.criteria[i];
|
||||||
let fieldTable = tableMetaData;
|
let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
|
||||||
let field = null;
|
|
||||||
if (criteria.fieldName.indexOf(".") > -1)
|
|
||||||
{
|
|
||||||
const nameParts = criteria.fieldName.split(".", 2);
|
|
||||||
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
|
|
||||||
{
|
|
||||||
const joinTable = tableMetaData.exposedJoins[i].joinTable;
|
|
||||||
if (joinTable.name == nameParts[0])
|
|
||||||
{
|
|
||||||
fieldTable = joinTable;
|
|
||||||
field = joinTable.fields.get(nameParts[1]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
field = tableMetaData.fields.get(criteria.fieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field == null)
|
if (field == null)
|
||||||
{
|
{
|
||||||
console.log("Couldn't find field for filter: " + criteria.fieldName);
|
console.log("Couldn't find field for filter: " + criteria.fieldName);
|
||||||
|
warningParts.push("Your filter contained an unrecognized field name: " + criteria.fieldName)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,12 +432,15 @@ class FilterUtils
|
|||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// replace objects that look like expressions with expression instances //
|
// replace objects that look like expressions with expression instances //
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
for(let i = 0; i < values.length; i++)
|
if(values && values.length)
|
||||||
{
|
{
|
||||||
const expression = this.gridCriteriaValueToExpression(values[i])
|
for (let i = 0; i < values.length; i++)
|
||||||
if(expression)
|
|
||||||
{
|
{
|
||||||
values[i] = expression;
|
const expression = this.gridCriteriaValueToExpression(values[i])
|
||||||
|
if (expression)
|
||||||
|
{
|
||||||
|
values[i] = expression;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,7 +483,7 @@ class FilterUtils
|
|||||||
localStorage.setItem(sortLocalStorageKey, JSON.stringify(defaultSort));
|
localStorage.setItem(sortLocalStorageKey, JSON.stringify(defaultSort));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({filter: defaultFilter, sort: defaultSort});
|
return ({filter: defaultFilter, sort: defaultSort, warning: warningParts.length > 0 ? "Warning: " + warningParts.join("; ") : ""});
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
{
|
{
|
||||||
@ -548,7 +534,7 @@ class FilterUtils
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({filter: defaultFilter, sort: defaultSort});
|
return ({filter: defaultFilter, sort: defaultSort, warning: warningParts.length > 0 ? "Warning: " + warningParts.join("; ") : ""});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,8 +19,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
|
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Utility class for working with QQQ Tables
|
** Utility class for working with QQQ Tables
|
||||||
@ -28,7 +30,6 @@ import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTa
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
class TableUtils
|
class TableUtils
|
||||||
{
|
{
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -85,6 +86,61 @@ class TableUtils
|
|||||||
})]);
|
})]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
|
||||||
|
{
|
||||||
|
if (fieldName.indexOf(".") > -1)
|
||||||
|
{
|
||||||
|
const nameParts = fieldName.split(".", 2);
|
||||||
|
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
|
||||||
|
{
|
||||||
|
const join = tableMetaData?.exposedJoins[i];
|
||||||
|
if (join?.joinTable.name == nameParts[0])
|
||||||
|
{
|
||||||
|
return ([join.joinTable.fields.get(nameParts[1]), join.joinTable]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ([tableMetaData.fields.get(fieldName), tableMetaData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static getQueryJoins(tableMetaData: QTableMetaData, visibleJoinTables: Set<string>): QueryJoin[]
|
||||||
|
{
|
||||||
|
const queryJoins = [];
|
||||||
|
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||||
|
{
|
||||||
|
const join = tableMetaData.exposedJoins[i];
|
||||||
|
if (visibleJoinTables.has(join.joinTable.name))
|
||||||
|
{
|
||||||
|
let joinName = null;
|
||||||
|
if (join.joinPath && join.joinPath.length == 1 && join.joinPath[0].name)
|
||||||
|
{
|
||||||
|
joinName = join.joinPath[0].name;
|
||||||
|
}
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// todo - what about a join with a longer path? it would be nice to pass such joinNames through there too, //
|
||||||
|
// but what, that would actually be multiple queryJoins? needs a fair amount of thought. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
queryJoins.push(new QueryJoin(join.joinTable.name, true, "LEFT", null, null, joinName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryJoins;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TableUtils;
|
export default TableUtils;
|
||||||
|
@ -49,6 +49,7 @@ module.exports = function (app)
|
|||||||
app.use("/data/*", getRequestHandler());
|
app.use("/data/*", getRequestHandler());
|
||||||
app.use("/widget/*", getRequestHandler());
|
app.use("/widget/*", getRequestHandler());
|
||||||
app.use("/serverInfo", getRequestHandler());
|
app.use("/serverInfo", getRequestHandler());
|
||||||
|
app.use("/manageSession", getRequestHandler());
|
||||||
app.use("/processes", getRequestHandler());
|
app.use("/processes", getRequestHandler());
|
||||||
app.use("/reports", getRequestHandler());
|
app.use("/reports", getRequestHandler());
|
||||||
app.use("/images", getRequestHandler());
|
app.use("/images", getRequestHandler());
|
||||||
|
@ -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;
|
||||||
@ -52,15 +58,88 @@ 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);
|
||||||
|
|
||||||
qSeleniumJavalin = new QSeleniumJavalin();
|
if(useInternalJavalin())
|
||||||
addJavalinRoutes(qSeleniumJavalin);
|
{
|
||||||
qSeleniumJavalin.start();
|
qSeleniumJavalin = new QSeleniumJavalin();
|
||||||
|
addJavalinRoutes(qSeleniumJavalin);
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
@ -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,111 @@
|
|||||||
|
/*
|
||||||
|
* 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.assertj.core.api.Assertions.assertThat;
|
||||||
|
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);
|
||||||
|
File csvFile = getDownloadedFiles().get(0);
|
||||||
|
assertThat(csvFile.getName()).matches("Sample Table Widget.*.csv");
|
||||||
|
String fileContents = FileUtils.readFileToString(csvFile, StandardCharsets.UTF_8);
|
||||||
|
assertEquals("""
|
||||||
|
"Id","Name"
|
||||||
|
"1","Homer S."
|
||||||
|
"2","Marge B."
|
||||||
|
"3","Bart J."
|
||||||
|
""", fileContents);
|
||||||
|
|
||||||
|
// qSeleniumLib.waitForever();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* 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.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.ThisOrLastPeriod;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.QQQMaterialDashboardSelectors;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Test for the record query screen when a filter is given in the URL
|
||||||
|
*******************************************************************************/
|
||||||
|
public class QueryScreenFilterInUrlTest extends QBaseSeleniumTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
||||||
|
{
|
||||||
|
super.addJavalinRoutes(qSeleniumJavalin);
|
||||||
|
qSeleniumJavalin
|
||||||
|
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||||
|
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||||
|
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
|
||||||
|
.withRouteToFile("/data/person/variants", "data/person/variants.json")
|
||||||
|
.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testUrlWithFilter()
|
||||||
|
{
|
||||||
|
////////////////////////////////////////
|
||||||
|
// not-blank -- criteria w/ no values //
|
||||||
|
////////////////////////////////////////
|
||||||
|
String filterJSON = JsonUtils.toJson(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria("annualSalary", QCriteriaOperator.IS_NOT_BLANK)));
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
|
waitForQueryToHaveRan();
|
||||||
|
assertFilterButtonBadge(1);
|
||||||
|
clickFilterButton();
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"is not empty\"]");
|
||||||
|
|
||||||
|
///////////////////////////////
|
||||||
|
// between on a number field //
|
||||||
|
///////////////////////////////
|
||||||
|
filterJSON = JsonUtils.toJson(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria("annualSalary", QCriteriaOperator.BETWEEN, 1701, 74656)));
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
|
waitForQueryToHaveRan();
|
||||||
|
assertFilterButtonBadge(1);
|
||||||
|
clickFilterButton();
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"is between\"]");
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"1701\"]");
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"74656\"]");
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// not-equals on a possible-value field //
|
||||||
|
//////////////////////////////////////////
|
||||||
|
filterJSON = JsonUtils.toJson(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria("homeCityId", QCriteriaOperator.NOT_EQUALS, 1)));
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
|
waitForQueryToHaveRan();
|
||||||
|
assertFilterButtonBadge(1);
|
||||||
|
clickFilterButton();
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"does not equal\"]");
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"St. Louis\"]");
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// an IN for a possible-value field //
|
||||||
|
//////////////////////////////////////
|
||||||
|
filterJSON = JsonUtils.toJson(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria("homeCityId", QCriteriaOperator.IN, 1, 2)));
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
|
waitForQueryToHaveRan();
|
||||||
|
assertFilterButtonBadge(1);
|
||||||
|
clickFilterButton();
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"is any of\"]");
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "St. Louis");
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "Chesterfield");
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// greater than a date-time expression //
|
||||||
|
/////////////////////////////////////////
|
||||||
|
filterJSON = JsonUtils.toJson(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria("createDate", QCriteriaOperator.GREATER_THAN, NowWithOffset.minus(5, ChronoUnit.DAYS))));
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
|
waitForQueryToHaveRan();
|
||||||
|
assertFilterButtonBadge(1);
|
||||||
|
clickFilterButton();
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"is after\"]");
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"5 days ago\"]");
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
// multiple criteria //
|
||||||
|
///////////////////////
|
||||||
|
filterJSON = JsonUtils.toJson(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.STARTS_WITH, "Dar"))
|
||||||
|
.withCriteria(new QFilterCriteria("createDate", QCriteriaOperator.LESS_THAN_OR_EQUALS, ThisOrLastPeriod.this_(ChronoUnit.YEARS))));
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
|
waitForQueryToHaveRan();
|
||||||
|
assertFilterButtonBadge(2);
|
||||||
|
clickFilterButton();
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"is at or before\"]");
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"start of this year\"]");
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"starts with\"]");
|
||||||
|
qSeleniumLib.waitForSelector("input[value=\"Dar\"]");
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
// remove one //
|
||||||
|
////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiIcon-root", "close").click();
|
||||||
|
assertFilterButtonBadge(1);
|
||||||
|
|
||||||
|
qSeleniumLib.waitForever();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private WebElement assertFilterButtonBadge(int valueInBadge)
|
||||||
|
{
|
||||||
|
return qSeleniumLib.waitForSelectorContaining(".MuiBadge-root", String.valueOf(valueInBadge));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private WebElement waitForQueryToHaveRan()
|
||||||
|
{
|
||||||
|
return qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void clickFilterButton()
|
||||||
|
{
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filter").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"label": "St. Louis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"label": "Chesterfield"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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": [
|
||||||
@ -25,4 +33,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -74,6 +74,15 @@
|
|||||||
"isEditable": true,
|
"isEditable": true,
|
||||||
"displayFormat": "%s"
|
"displayFormat": "%s"
|
||||||
},
|
},
|
||||||
|
"homeCityId": {
|
||||||
|
"name": "homeCityId",
|
||||||
|
"label": "Home City",
|
||||||
|
"type": "INTEGER",
|
||||||
|
"possibleValueSourceName": "city",
|
||||||
|
"isRequired": false,
|
||||||
|
"isEditable": true,
|
||||||
|
"displayFormat": "%s"
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"label": "Email",
|
"label": "Email",
|
||||||
|
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