mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 14:48:43 +00:00
Compare commits
1 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
575ffe761f |
@ -6,7 +6,7 @@
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.83",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.82",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
|
2
pom.xml
2
pom.xml
@ -29,7 +29,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.20.0-SNAPSHOT</revision>
|
||||
<revision>0.19.0-SNAPSHOT</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
20
src/App.tsx
20
src/App.tsx
@ -73,14 +73,6 @@ export default function App()
|
||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||
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])
|
||||
@ -175,8 +167,18 @@ export default function App()
|
||||
console.log("Using existing sessionUUID cookie");
|
||||
}
|
||||
|
||||
/*
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. //
|
||||
// we turn off the shouldStoreNewToken block above, and turn on these 2 lines. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
localStorage.removeItem("accessToken");
|
||||
*/
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
||||
|
||||
setLoggedInUser(user);
|
||||
console.log("Token load complete.");
|
||||
@ -197,8 +199,8 @@ export default function App()
|
||||
// use a random token if anonymous or mock //
|
||||
/////////////////////////////////////////////
|
||||
console.log("Generating random token...");
|
||||
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
console.log("Token generation complete.");
|
||||
return;
|
||||
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface MaterialDashboardIconRoleNames
|
||||
{
|
||||
String TOP_RIGHT_INSIDE_CARD = "topRightInsideCard";
|
||||
}
|
@ -31,7 +31,7 @@ type Types = any;
|
||||
|
||||
const card: Types = {
|
||||
defaultProps: {
|
||||
elevation: 0
|
||||
elevation: 3
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@ -42,7 +42,7 @@ const card: Types = {
|
||||
wordWrap: "break-word",
|
||||
backgroundColor: white.main,
|
||||
backgroundClip: "border-box",
|
||||
border: `${borderWidth[1]} solid ${rgba(black.main, 0.25)}`,
|
||||
border: `${borderWidth[0]} solid ${rgba(black.main, 0.125)}`,
|
||||
borderRadius: borderRadius.xl,
|
||||
overflow: "visible",
|
||||
},
|
||||
|
@ -1,72 +1,68 @@
|
||||
/**
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
|
||||
Coded by www.creative-tim.com
|
||||
Coded by www.creative-tim.com
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import borders from "qqq/assets/theme/base/borders";
|
||||
import boxShadows from "qqq/assets/theme/base/boxShadows";
|
||||
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
|
||||
const {grey, white} = colors;
|
||||
const { grey, white } = colors;
|
||||
const { borderRadius } = borders;
|
||||
const { tabsBoxShadow } = boxShadows;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
|
||||
const tabs: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
position: "relative",
|
||||
borderRadius: 0,
|
||||
borderBottom: "1px solid",
|
||||
borderBottomColor: grey[400],
|
||||
minHeight: "unset",
|
||||
padding: "0",
|
||||
margin: "0"
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
position: "relative",
|
||||
backgroundColor: grey[100],
|
||||
borderRadius: borderRadius.xl,
|
||||
minHeight: "unset",
|
||||
padding: pxToRem(4),
|
||||
},
|
||||
|
||||
scroller: {
|
||||
marginLeft: "0.5rem"
|
||||
},
|
||||
flexContainer: {
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
zIndex: 10,
|
||||
},
|
||||
|
||||
flexContainer: {
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
width: "fit-content",
|
||||
zIndex: 10,
|
||||
},
|
||||
fixed: {
|
||||
overflow: "unset !important",
|
||||
overflowX: "unset !important",
|
||||
},
|
||||
|
||||
fixed: {
|
||||
overflow: "unset !important",
|
||||
overflowX: "unset !important",
|
||||
vertical: {
|
||||
"& .MuiTabs-indicator": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
|
||||
vertical: {
|
||||
"& .MuiTabs-indicator": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
|
||||
indicator: {
|
||||
height: "100%",
|
||||
borderRadius: 0,
|
||||
backgroundColor: white.main,
|
||||
borderBottom: "2px solid",
|
||||
borderBottomColor: colors.info.main,
|
||||
transition: "all 500ms ease",
|
||||
},
|
||||
},
|
||||
indicator: {
|
||||
height: "100%",
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: white.main,
|
||||
boxShadow: tabsBoxShadow.indicator,
|
||||
transition: "all 500ms ease",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default tabs;
|
||||
|
@ -1,17 +1,17 @@
|
||||
/**
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
|
||||
Coded by www.creative-tim.com
|
||||
Coded by www.creative-tim.com
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import typography from "qqq/assets/theme/base/typography";
|
||||
@ -21,50 +21,48 @@ import colors from "qqq/assets/theme/base/colors";
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
|
||||
const {size, fontWeightRegular} = typography;
|
||||
const {borderRadius} = borders;
|
||||
const {dark} = colors;
|
||||
const { size, fontWeightRegular } = typography;
|
||||
const { borderRadius } = borders;
|
||||
const { dark } = colors;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
|
||||
const tab: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
flex: "1 1 auto",
|
||||
textAlign: "center",
|
||||
maxWidth: "unset !important",
|
||||
minWidth: "unset !important",
|
||||
minHeight: "unset !important",
|
||||
fontSize: size.md,
|
||||
fontWeight: fontWeightRegular,
|
||||
textTransform: "none",
|
||||
lineHeight: "inherit",
|
||||
padding: "0.75rem 0.5rem 0.5rem",
|
||||
margin: "0 0.5rem",
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
color: `${dark.main} !important`,
|
||||
opacity: "1 !important",
|
||||
styleOverrides: {
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
flex: "1 1 auto",
|
||||
textAlign: "center",
|
||||
maxWidth: "unset !important",
|
||||
minWidth: "unset !important",
|
||||
minHeight: "unset !important",
|
||||
fontSize: size.md,
|
||||
fontWeight: fontWeightRegular,
|
||||
textTransform: "none",
|
||||
lineHeight: "inherit",
|
||||
padding: pxToRem(4),
|
||||
borderRadius: borderRadius.lg,
|
||||
color: `${dark.main} !important`,
|
||||
opacity: "1 !important",
|
||||
|
||||
"& .material-icons, .material-icons-round": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
|
||||
"& svg": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
"& .material-icons, .material-icons-round": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
|
||||
labelIcon: {
|
||||
paddingTop: pxToRem(4),
|
||||
"& svg": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
labelIcon: {
|
||||
paddingTop: pxToRem(4),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default tab;
|
||||
|
@ -24,7 +24,7 @@ import borders from "qqq/assets/theme/base/borders";
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
|
||||
const { black, light, white, dark } = colors;
|
||||
const { black, light } = colors;
|
||||
const { size, fontWeightRegular } = typography;
|
||||
const { borderRadius } = borders;
|
||||
|
||||
@ -39,20 +39,19 @@ const tooltip: Types = {
|
||||
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
maxWidth: pxToRem(300),
|
||||
backgroundColor: white.main,
|
||||
color: dark.main,
|
||||
maxWidth: pxToRem(200),
|
||||
backgroundColor: black.main,
|
||||
color: light.main,
|
||||
fontSize: size.sm,
|
||||
fontWeight: fontWeightRegular,
|
||||
textAlign: "left",
|
||||
textAlign: "center",
|
||||
borderRadius: borderRadius.md,
|
||||
opacity: 0.7,
|
||||
padding: "1rem",
|
||||
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 3px -2px, rgba(0, 0, 0, 0.14) 0px 3px 4px 0px, rgba(0, 0, 0, 0.12) 0px 1px 8px 0px"
|
||||
padding: `${pxToRem(5)} ${pxToRem(8)} ${pxToRem(4)}`,
|
||||
},
|
||||
|
||||
arrow: {
|
||||
color: white.main,
|
||||
color: black.main,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -19,14 +19,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Box, InputLabel} from "@mui/material";
|
||||
import {InputLabel} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {styled} from "@mui/material/styles";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {useFormikContext} from "formik";
|
||||
import React, {SyntheticEvent} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
const AntSwitch = styled(Switch)(({theme}) => ({
|
||||
width: 28,
|
||||
@ -61,9 +60,6 @@ const AntSwitch = styled(Switch)(({theme}) => ({
|
||||
duration: 200,
|
||||
}),
|
||||
},
|
||||
"&.nullSwitch .MuiSwitch-thumb": {
|
||||
width: 24,
|
||||
},
|
||||
"& .MuiSwitch-track": {
|
||||
borderRadius: 16 / 2,
|
||||
opacity: 1,
|
||||
@ -82,7 +78,6 @@ interface Props
|
||||
}
|
||||
|
||||
|
||||
|
||||
function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Element
|
||||
{
|
||||
const {setFieldValue} = useFormikContext();
|
||||
@ -101,10 +96,8 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
||||
setFieldValue(name, !value);
|
||||
}
|
||||
|
||||
const classNullSwitch = (value === null || value == undefined || `${value}` == "") ? "nullSwitch" : "";
|
||||
|
||||
return (
|
||||
<Box bgcolor={isDisabled ? colors.grey[200] : ""}>
|
||||
<>
|
||||
<InputLabel shrink={true}>{label}</InputLabel>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography
|
||||
@ -114,7 +107,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
||||
sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}>
|
||||
No
|
||||
</Typography>
|
||||
<AntSwitch className={classNullSwitch} name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||
<AntSwitch name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||
<Typography
|
||||
fontSize="0.875rem"
|
||||
color={value === true ? "auto" : "#bfbfbf"}
|
||||
@ -123,7 +116,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
||||
Yes
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -88,14 +88,7 @@ function QDynamicFormField({
|
||||
if (type === "checkbox")
|
||||
{
|
||||
getsBulkEditHtmlLabel = false;
|
||||
field = (<>
|
||||
<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />
|
||||
<Box mt={0.75}>
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</>);
|
||||
field = (<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />);
|
||||
}
|
||||
else if (type === "ace")
|
||||
{
|
||||
|
@ -426,11 +426,6 @@ function EntityForm(props: Props): JSX.Element
|
||||
actions.setSubmitting(true);
|
||||
await (async () =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we will be manipulating the values sent to the backend, so clone values so they remained unchanged for the form widgets //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const valuesToPost = JSON.parse(JSON.stringify(values));
|
||||
|
||||
for(let fieldName of tableMetaData.fields.keys())
|
||||
{
|
||||
const fieldMetaData = tableMetaData.fields.get(fieldName);
|
||||
@ -443,17 +438,17 @@ function EntityForm(props: Props): JSX.Element
|
||||
// changing from, say, 12:15:30 to just 12:15:00... this seems to get around that, for cases when the //
|
||||
// user didn't change the value in the field (but if the user did change the value, then we will submit it) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldMetaData.type === QFieldType.DATE_TIME && valuesToPost[fieldName])
|
||||
if(fieldMetaData.type === QFieldType.DATE_TIME && values[fieldName])
|
||||
{
|
||||
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${valuesToPost[fieldName]}]`)
|
||||
if (initialValues[fieldName] == valuesToPost[fieldName])
|
||||
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${values[fieldName]}]`)
|
||||
if (initialValues[fieldName] == values[fieldName])
|
||||
{
|
||||
console.log(" - Is the same, so, deleting from the post");
|
||||
delete (valuesToPost[fieldName]);
|
||||
delete (values[fieldName]);
|
||||
}
|
||||
else
|
||||
{
|
||||
valuesToPost[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(valuesToPost[fieldName]);
|
||||
values[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(values[fieldName]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -466,10 +461,10 @@ function EntityForm(props: Props): JSX.Element
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldMetaData.type === QFieldType.BLOB)
|
||||
{
|
||||
if(typeof valuesToPost[fieldName] === "string")
|
||||
if(typeof values[fieldName] === "string")
|
||||
{
|
||||
console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`);
|
||||
delete(valuesToPost[fieldName]);
|
||||
delete(values[fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -478,7 +473,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
// todo - audit that it's a dupe
|
||||
await qController
|
||||
.update(tableName, props.id, valuesToPost)
|
||||
.update(tableName, props.id, values)
|
||||
.then((record) =>
|
||||
{
|
||||
if (props.isModal)
|
||||
@ -511,7 +506,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
else
|
||||
{
|
||||
await qController
|
||||
.create(tableName, valuesToPost)
|
||||
.create(tableName, values)
|
||||
.then((record) =>
|
||||
{
|
||||
if (props.isModal)
|
||||
|
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Popper, InputAdornment} from "@mui/material";
|
||||
import {Popper} from "@mui/material";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Badge from "@mui/material/Badge";
|
||||
@ -34,8 +34,8 @@ import React, {useContext, useEffect, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
|
||||
import {navbar, navbarContainer, navbarRow, navbarMobileMenu, recentlyViewedMenu,} from "qqq/components/horseshoe/Styles";
|
||||
import {setTransparentNavbar, useMaterialUIController, setMiniSidenav} from "qqq/context";
|
||||
import {navbar, navbarContainer, navbarIconButton, navbarRow,} from "qqq/components/horseshoe/Styles";
|
||||
import {setTransparentNavbar, useMaterialUIController,} from "qqq/context";
|
||||
import HistoryUtils from "qqq/utils/HistoryUtils";
|
||||
|
||||
// Declaring prop types for NavBar
|
||||
@ -57,7 +57,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
{
|
||||
const [navbarType, setNavbarType] = useState<"fixed" | "absolute" | "relative" | "static" | "sticky">();
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {miniSidenav, transparentNavbar, fixedNavbar, darkMode,} = controller;
|
||||
const {transparentNavbar, fixedNavbar, darkMode,} = controller;
|
||||
const [openMenu, setOpenMenu] = useState<any>(false);
|
||||
const [history, setHistory] = useState<any>([] as HistoryEntry[]);
|
||||
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
|
||||
@ -105,8 +105,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
return () => window.removeEventListener("scroll", handleTransparentNavbar);
|
||||
}, [dispatch, fixedNavbar]);
|
||||
|
||||
const handleMiniSidenav = () => setMiniSidenav(dispatch, !miniSidenav);
|
||||
|
||||
const goToHistory = (path: string) =>
|
||||
{
|
||||
navigate(path);
|
||||
@ -164,15 +162,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
onChange={handleAutocompleteOnChange}
|
||||
PopperComponent={CustomPopper}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
sx={recentlyViewedMenu}
|
||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Icon sx={{position: "relative", right: "-1rem"}}>keyboard_arrow_down</Icon>
|
||||
</InputAdornment>
|
||||
)
|
||||
}} />}
|
||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" />}
|
||||
renderOption={(props, option: HistoryEntry) => (
|
||||
<Box {...props} component="li" key={option.id} sx={{width: "auto"}}>
|
||||
<Box sx={{width: "auto", px: "8px", whiteSpace: "overflow"}} key={option.id}>
|
||||
@ -185,6 +175,22 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Render the notifications menu
|
||||
const renderMenu = () => (
|
||||
<Menu
|
||||
anchorEl={openMenu}
|
||||
anchorReference={null}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
open={Boolean(openMenu)}
|
||||
onClose={handleCloseMenu}
|
||||
sx={{mt: 2}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Styles for the navbar icons
|
||||
const iconsStyle = ({
|
||||
palette: {dark, white, text},
|
||||
@ -234,22 +240,26 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
>
|
||||
<Toolbar sx={navbarContainer}>
|
||||
<Box color="inherit" mb={{xs: 1, md: 0}} sx={(theme) => navbarRow(theme, {isMini})}>
|
||||
<IconButton
|
||||
size="small"
|
||||
disableRipple
|
||||
color="inherit"
|
||||
sx={navbarMobileMenu}
|
||||
onClick={handleMiniSidenav}
|
||||
>
|
||||
<Icon sx={iconsStyle} fontSize="large">menu</Icon>
|
||||
</IconButton>
|
||||
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
||||
</Box>
|
||||
{isMini ? null : (
|
||||
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
||||
<Box pr={0} mr={-2} mt={-4}>
|
||||
<Box pr={1}>
|
||||
{renderHistory()}
|
||||
</Box>
|
||||
<Box color={light ? "white" : "inherit"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={navbarIconButton}
|
||||
onClick={handleOpenMenu}
|
||||
>
|
||||
<Badge badgeContent={0} color="error" variant="dot">
|
||||
<Icon sx={iconsStyle}>notifications</Icon>
|
||||
</Badge>
|
||||
</IconButton>
|
||||
{renderMenu()}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
|
@ -110,10 +110,11 @@ const navbarContainer = ({breakpoints}: Theme): any => ({
|
||||
const navbarRow = ({breakpoints}: Theme, {isMini}: any) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
|
||||
[breakpoints.up("md")]: {
|
||||
justifyContent: "stretch",
|
||||
justifyContent: isMini ? "space-between" : "stretch",
|
||||
width: isMini ? "100%" : "max-content",
|
||||
},
|
||||
|
||||
@ -145,27 +146,12 @@ const navbarDesktopMenu = ({breakpoints}: Theme) => ({
|
||||
display: "none !important",
|
||||
cursor: "pointer",
|
||||
|
||||
[breakpoints.down("sm")]: {
|
||||
[breakpoints.up("xl")]: {
|
||||
display: "inline-block !important",
|
||||
},
|
||||
});
|
||||
|
||||
const recentlyViewedMenu = ({breakpoints}: Theme) => ({
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: "0",
|
||||
padding: "0"
|
||||
},
|
||||
"& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
|
||||
border: "0"
|
||||
},
|
||||
display: "block",
|
||||
[breakpoints.down("md")]: {
|
||||
display: "none !important",
|
||||
},
|
||||
});
|
||||
|
||||
const navbarMobileMenu = ({breakpoints}: Theme) => ({
|
||||
left: "-0.75rem",
|
||||
display: "inline-block",
|
||||
lineHeight: 0,
|
||||
|
||||
@ -181,5 +167,4 @@ export {
|
||||
navbarIconButton,
|
||||
navbarDesktopMenu,
|
||||
navbarMobileMenu,
|
||||
recentlyViewedMenu
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
||||
const {palette, boxShadows, transitions, breakpoints, functions} = theme;
|
||||
const {transparentSidenav, whiteSidenav, miniSidenav, darkMode} = ownerState;
|
||||
|
||||
const sidebarWidth = 275;
|
||||
const sidebarWidth = 250;
|
||||
const {transparent, gradients, white, background} = palette;
|
||||
const {xxl} = boxShadows;
|
||||
const {pxToRem, linearGradient} = functions;
|
||||
@ -94,9 +94,6 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
||||
"& .MuiDrawer-paper": {
|
||||
boxShadow: xxl,
|
||||
border: "none",
|
||||
margin: "0",
|
||||
borderRadius: "0",
|
||||
height: "100%",
|
||||
|
||||
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
||||
},
|
||||
|
@ -34,6 +34,7 @@ import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
@ -195,8 +196,17 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
|
||||
return (
|
||||
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
||||
<DialogTitle>Go To...</DialogTitle>
|
||||
|
||||
<DialogTitle sx={{display: "flex"}}>
|
||||
<Box sx={{display: "flex", flexGrow: 1}}>
|
||||
Go To...
|
||||
</Box>
|
||||
<Box sx={{display: "flex"}}>
|
||||
<IconButton onClick={() =>
|
||||
{
|
||||
document.location.href = "/";
|
||||
}}><Icon sx={{align: "right"}} fontSize="small">close</Icon></IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{props.subHeader}
|
||||
{
|
||||
|
@ -28,12 +28,11 @@ interface TabPanelProps
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export default function TabPanel(props: TabPanelProps)
|
||||
{
|
||||
const {children, value, index, style, ...other} = props;
|
||||
const {children, value, index, ...other} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -41,7 +40,6 @@ export default function TabPanel(props: TabPanelProps)
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
style={style}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
|
@ -155,7 +155,7 @@ function ValidationReview({
|
||||
"false",
|
||||
"Skip Validation. Submit the records for immediate processing", (
|
||||
<div>
|
||||
If you choose this option, the input records will immediately be processed.
|
||||
If you choose this option, the records input records will immediately be processed.
|
||||
You will be told how many records were successfully processed, and which ones had issues after the processing is completed.
|
||||
<br />
|
||||
<br />
|
||||
|
@ -22,13 +22,11 @@ import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Q
|
||||
import {Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import parse from "html-react-parser";
|
||||
import React, {useContext, useEffect, useReducer, useState} from "react";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import TabPanel from "qqq/components/misc/TabPanel";
|
||||
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
||||
import HorizontalBarChart from "qqq/components/widgets/charts/barchart/HorizontalBarChart";
|
||||
import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLineChart";
|
||||
@ -46,7 +44,7 @@ import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
||||
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||
import Widget, {HeaderIcon, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import Widget, {WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
|
||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import TableWidget from "./tables/TableWidget";
|
||||
@ -60,10 +58,9 @@ interface Props
|
||||
tableName?: string;
|
||||
entityPrimaryKey?: string;
|
||||
omitWrappingGridContainer: boolean;
|
||||
areChildren?: boolean;
|
||||
childUrlParams?: string;
|
||||
parentWidgetMetaData?: QWidgetMetaData;
|
||||
wrapWidgetsInTabPanels: boolean;
|
||||
areChildren?: boolean
|
||||
childUrlParams?: string
|
||||
parentWidgetMetaData?: QWidgetMetaData
|
||||
}
|
||||
|
||||
DashboardWidgets.defaultProps = {
|
||||
@ -73,12 +70,12 @@ DashboardWidgets.defaultProps = {
|
||||
omitWrappingGridContainer: false,
|
||||
areChildren: false,
|
||||
childUrlParams: "",
|
||||
parentWidgetMetaData: null,
|
||||
wrapWidgetsInTabPanels: false,
|
||||
parentWidgetMetaData: null
|
||||
};
|
||||
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels}: Props): JSX.Element
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData}: Props): JSX.Element
|
||||
{
|
||||
const location = useLocation();
|
||||
const [widgetData, setWidgetData] = useState([] as any[]);
|
||||
const [widgetCounter, setWidgetCounter] = useState(0);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
@ -87,24 +84,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
let initialSelectedTab = 0;
|
||||
let selectedTabKey: string = null;
|
||||
if(parentWidgetMetaData && wrapWidgetsInTabPanels)
|
||||
{
|
||||
selectedTabKey = `qqq.widgets.selectedTabs.${parentWidgetMetaData.name}`
|
||||
if (localStorage.getItem(selectedTabKey))
|
||||
{
|
||||
initialSelectedTab = Number(localStorage.getItem(selectedTabKey));
|
||||
}
|
||||
}
|
||||
const [selectedTab, setSelectedTab] = useState(initialSelectedTab);
|
||||
|
||||
const changeTab = (newValue: number) =>
|
||||
{
|
||||
setSelectedTab(newValue);
|
||||
localStorage.setItem(selectedTabKey, String(newValue));
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setWidgetData([]);
|
||||
@ -123,15 +102,15 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
||||
setWidgetData(widgetData);
|
||||
setWidgetCounter(widgetCounter + 1);
|
||||
if (widgetData[i])
|
||||
if(widgetData[i])
|
||||
{
|
||||
widgetData[i]["errorLoading"] = false;
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
catch(e)
|
||||
{
|
||||
console.error(e);
|
||||
if (widgetData[i])
|
||||
if(widgetData[i])
|
||||
{
|
||||
widgetData[i]["errorLoading"] = true;
|
||||
}
|
||||
@ -144,7 +123,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
|
||||
const reloadWidget = async (index: number, data: string) =>
|
||||
{
|
||||
(async () =>
|
||||
(async() =>
|
||||
{
|
||||
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
||||
setCurrentUrlParams(urlParams);
|
||||
@ -161,7 +140,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData[index]["errorLoading"] = false;
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
catch(e)
|
||||
{
|
||||
console.error(e);
|
||||
if (widgetData[index])
|
||||
@ -172,7 +151,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
|
||||
forceUpdate();
|
||||
})();
|
||||
};
|
||||
}
|
||||
|
||||
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
||||
{
|
||||
@ -199,36 +178,36 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
}
|
||||
}
|
||||
|
||||
if (entityPrimaryKey)
|
||||
if(entityPrimaryKey)
|
||||
{
|
||||
paramMap.set("id", entityPrimaryKey);
|
||||
}
|
||||
|
||||
if (tableName)
|
||||
if(tableName)
|
||||
{
|
||||
paramMap.set("tableName", tableName);
|
||||
}
|
||||
|
||||
if (extraParams)
|
||||
if(extraParams)
|
||||
{
|
||||
let pairs = extraParams.split("&");
|
||||
for (let i = 0; i < pairs.length; i++)
|
||||
{
|
||||
let nameValue = pairs[i].split("=");
|
||||
if (nameValue.length == 2)
|
||||
if(nameValue.length == 2)
|
||||
{
|
||||
paramMap.set(nameValue[0], nameValue[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (childUrlParams)
|
||||
if(childUrlParams)
|
||||
{
|
||||
let pairs = childUrlParams.split("&");
|
||||
for (let i = 0; i < pairs.length; i++)
|
||||
{
|
||||
let nameValue = pairs[i].split("=");
|
||||
if (nameValue.length == 2)
|
||||
if(nameValue.length == 2)
|
||||
{
|
||||
paramMap.set(nameValue[0], nameValue[1]);
|
||||
}
|
||||
@ -248,16 +227,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
|
||||
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
|
||||
{
|
||||
const labelAdditionalComponentsRight: LabelComponent[] = [];
|
||||
if (widgetMetaData && widgetMetaData.icons)
|
||||
{
|
||||
const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard");
|
||||
if (topRightInsideCardIcon)
|
||||
{
|
||||
labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.color));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%"}}>
|
||||
{
|
||||
@ -269,7 +238,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetIndex={i}
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
reloadWidgetCallback={reloadWidget}
|
||||
storeDropdownSelections={widgetMetaData.storeDropdownSelections}
|
||||
/>
|
||||
)
|
||||
@ -301,9 +270,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
>
|
||||
<StackedBarChart data={widgetData[i]?.chartData} chartSubheaderData={widgetData[i]?.chartSubheaderData} />
|
||||
<StackedBarChart data={widgetData[i]?.chartData}/>
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
@ -413,12 +381,10 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
>
|
||||
<div>
|
||||
<PieChart
|
||||
chartData={widgetData[i]?.chartData}
|
||||
chartSubheaderData={widgetData[i]?.chartSubheaderData}
|
||||
description={widgetData[i]?.description}
|
||||
/>
|
||||
</div>
|
||||
@ -470,11 +436,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
{
|
||||
widgetMetaData.type === "fieldValueList" && (
|
||||
widgetData && widgetData[i] &&
|
||||
<FieldValueListWidget
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
/>
|
||||
<FieldValueListWidget
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
@ -495,62 +461,32 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const body: JSX.Element =
|
||||
(
|
||||
<>
|
||||
{
|
||||
widgetMetaDataList.map((widgetMetaData, i) =>
|
||||
{
|
||||
let renderedWidget = widgetMetaData ? renderWidget(widgetMetaData, i) : (<></>);
|
||||
|
||||
if (!omitWrappingGridContainer)
|
||||
{
|
||||
// @ts-ignore
|
||||
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
{renderedWidget}
|
||||
</Grid>);
|
||||
}
|
||||
|
||||
if (wrapWidgetsInTabPanels)
|
||||
{
|
||||
renderedWidget = (<TabPanel index={i} value={selectedTab} style={{padding: "1rem 0 0 1.5rem", width: "100%", marginBottom: "-1.5rem"}}>
|
||||
{renderedWidget}
|
||||
</TabPanel>);
|
||||
}
|
||||
|
||||
return (<React.Fragment key={`${widgetMetaData.name}-${i}`}>{renderedWidget}</React.Fragment>)
|
||||
})
|
||||
widgetMetaDataList.map((widgetMetaData, i) => (
|
||||
omitWrappingGridContainer
|
||||
? widgetMetaData && renderWidget(widgetMetaData, i)
|
||||
:
|
||||
widgetMetaData && <Grid id={widgetMetaData.name} key={`${widgetMetaData.name}-${i}`} item lg={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
{renderWidget(widgetMetaData, i)}
|
||||
</Grid>
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
const tabs = widgetMetaDataList && wrapWidgetsInTabPanels ?
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1.5}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
{widgetMetaDataList.map((widgetMetaData, i) => (
|
||||
<Tab key={widgetMetaData.name} label={widgetMetaData.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
: <></>
|
||||
|
||||
return (
|
||||
widgetCount > 0 ? (
|
||||
<>
|
||||
{tabs}
|
||||
{
|
||||
omitWrappingGridContainer ? body : (
|
||||
<Grid container spacing={3} pb={4}>
|
||||
{body}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</>
|
||||
omitWrappingGridContainer ? body :
|
||||
(
|
||||
<Grid container spacing={3} pb={4}>
|
||||
{body}
|
||||
</Grid>
|
||||
)
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ export interface ParentWidgetData
|
||||
dropdownNeedsSelectedText?: string;
|
||||
storeDropdownSelections?: boolean;
|
||||
icon?: string;
|
||||
layoutType: string;
|
||||
}
|
||||
|
||||
|
||||
@ -56,7 +55,7 @@ interface Props
|
||||
widgetMetaData?: QWidgetMetaData;
|
||||
widgetIndex: number;
|
||||
data: ParentWidgetData;
|
||||
reloadWidgetCallback?: (params: string) => void;
|
||||
reloadWidgetCallback?: (widgetIndex: number, params: string) => void;
|
||||
entityPrimaryKey?: string;
|
||||
tableName?: string;
|
||||
storeDropdownSelections?: boolean;
|
||||
@ -92,15 +91,10 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
||||
}
|
||||
}, [qInstance, data, childUrlParams]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setChildUrlParams(urlParams)
|
||||
}, [urlParams]);
|
||||
|
||||
const parentReloadWidgetCallback = (data: string) =>
|
||||
{
|
||||
setChildUrlParams(data);
|
||||
reloadWidgetCallback(data);
|
||||
reloadWidgetCallback(widgetIndex, data);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -118,7 +112,7 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
||||
reloadWidgetCallback={parentReloadWidgetCallback}
|
||||
>
|
||||
<Box sx={{height: "100%", width: "100%"}} px={px}>
|
||||
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType == "TABS"}/>
|
||||
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData}/>
|
||||
</Box>
|
||||
</Widget>
|
||||
) : null
|
||||
|
@ -30,7 +30,7 @@ import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import parse from "html-react-parser";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import {Link, NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import colors from "qqq/components/legacy/colors";
|
||||
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
||||
|
||||
@ -46,7 +46,6 @@ export interface WidgetData
|
||||
dropdownNeedsSelectedText?: string;
|
||||
hasPermission?: boolean;
|
||||
errorLoading?: boolean;
|
||||
|
||||
[other: string]: any;
|
||||
}
|
||||
|
||||
@ -54,7 +53,6 @@ export interface WidgetData
|
||||
interface Props
|
||||
{
|
||||
labelAdditionalComponentsLeft: LabelComponent[];
|
||||
labelAdditionalElementsLeft: JSX.Element[];
|
||||
labelAdditionalComponentsRight: LabelComponent[];
|
||||
widgetMetaData?: QWidgetMetaData;
|
||||
widgetData?: WidgetData;
|
||||
@ -72,7 +70,6 @@ Widget.defaultProps = {
|
||||
widgetMetaData: {},
|
||||
widgetData: {},
|
||||
labelAdditionalComponentsLeft: [],
|
||||
labelAdditionalElementsLeft: [],
|
||||
labelAdditionalComponentsRight: [],
|
||||
};
|
||||
|
||||
@ -91,50 +88,33 @@ export class LabelComponent
|
||||
{
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
return (<div>Unsupported component type</div>);
|
||||
};
|
||||
return (<div>Unsupported component type</div>)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
export class HeaderIcon extends LabelComponent
|
||||
export class HeaderLink extends LabelComponent
|
||||
{
|
||||
iconName: string;
|
||||
color: string;
|
||||
coloredBG: boolean;
|
||||
label: string;
|
||||
to: string
|
||||
|
||||
iconColor: string;
|
||||
bgColor: string;
|
||||
|
||||
constructor(iconName: string, color: string, coloredBG: boolean = true)
|
||||
constructor(label: string, to: string)
|
||||
{
|
||||
super();
|
||||
this.iconName = iconName;
|
||||
this.color = color;
|
||||
this.coloredBG = coloredBG;
|
||||
|
||||
this.iconColor = this.coloredBG ? "#FFFFFF" : this.color;
|
||||
this.bgColor = this.coloredBG ? this.color : "none";
|
||||
this.label = label;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
return (
|
||||
<Icon sx={{
|
||||
m: 2,
|
||||
mr: 0,
|
||||
mb: 0,
|
||||
width: "1.75rem",
|
||||
height: "1.75rem",
|
||||
color: this.iconColor,
|
||||
backgroundColor: this.bgColor,
|
||||
padding: "0.25rem",
|
||||
borderRadius: "0.25rem"
|
||||
}} fontSize="small">{this.iconName}</Icon>
|
||||
)
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,8 +141,8 @@ export class AddNewRecordButton extends LabelComponent
|
||||
|
||||
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 =>
|
||||
{
|
||||
@ -171,7 +151,35 @@ 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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -219,7 +227,7 @@ export class Dropdown extends LabelComponent
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -243,7 +251,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>
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -364,7 +372,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
|
||||
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;
|
||||
@ -386,7 +394,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
reloadWidget(dropdownData);
|
||||
reloadWidget(dropdownData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -414,7 +422,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const toggleFullScreenWidget = () =>
|
||||
{
|
||||
@ -426,14 +434,14 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
setFullScreenWidgetClassName("fullScreenWidget");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
||||
|
||||
const isSet = (v: any): boolean =>
|
||||
{
|
||||
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 //
|
||||
@ -442,42 +450,24 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
if (hasPermission)
|
||||
{
|
||||
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
||||
needLabelBox ||= (props.labelAdditionalElementsLeft && props.labelAdditionalElementsLeft.length > 0);
|
||||
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
||||
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
||||
needLabelBox ||= isSet(props.widgetData?.label);
|
||||
needLabelBox ||= isSet(props.widgetMetaData?.label);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// first look for a label in the widget data, which would override that in the metadata //
|
||||
// note - previously this had a ?: and one was pl={2}, the other was pl={3}... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label
|
||||
let labelElement = (
|
||||
<Typography sx={{position: "relative", top: -4, cursor: "default"}} variant="h6" fontWeight="medium" display="inline">
|
||||
{labelToUse}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
if(props.widgetMetaData.tooltip)
|
||||
{
|
||||
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>
|
||||
}
|
||||
|
||||
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
||||
const widgetContent =
|
||||
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||
{
|
||||
needLabelBox &&
|
||||
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} minHeight={"3.5rem"}>
|
||||
<Box pt={2} pb={1} ml={2}>
|
||||
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} height={"3.5rem"}>
|
||||
<Box pt={2} pb={1}>
|
||||
{
|
||||
hasPermission ?
|
||||
props.widgetMetaData?.icon && (
|
||||
<Box
|
||||
ml={1}
|
||||
mr={2}
|
||||
ml={3}
|
||||
mt={-4}
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -517,7 +507,20 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
)
|
||||
}
|
||||
{
|
||||
hasPermission && labelToUse && (labelElement)
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// first look for a label in the widget data, which would override that in the metadata //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
hasPermission && props.widgetData?.label ? (
|
||||
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={2} display="inline-block">
|
||||
{props.widgetData.label}
|
||||
</Typography>
|
||||
) : (
|
||||
hasPermission && props.widgetMetaData?.label && (
|
||||
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={3} display="inline-block">
|
||||
{props.widgetMetaData.label}
|
||||
</Typography>
|
||||
)
|
||||
)
|
||||
}
|
||||
{
|
||||
hasPermission && (
|
||||
@ -527,7 +530,6 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
})
|
||||
)
|
||||
}
|
||||
{props.labelAdditionalElementsLeft}
|
||||
</Box>
|
||||
<Box>
|
||||
{
|
||||
@ -577,7 +579,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>
|
||||
{widgetContent}
|
||||
</Card>
|
||||
: <span style={{width: "100%"}}>{widgetContent}</span>;
|
||||
: widgetContent;
|
||||
}
|
||||
|
||||
export default Widget;
|
||||
|
@ -28,7 +28,6 @@ import {Bar} from "react-chartjs-2";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {chartColors, DefaultChartData} from "qqq/components/widgets/charts/DefaultChartData";
|
||||
import ChartSubheaderWithData, {ChartSubheaderData} from "qqq/components/widgets/components/ChartSubheaderWithData";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
@ -40,40 +39,18 @@ ChartJS.register(
|
||||
);
|
||||
|
||||
export const options = {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
// todo - some configs around this
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: "circle",
|
||||
boxHeight: 8,
|
||||
boxWidth: 8,
|
||||
padding: 12,
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {display: false},
|
||||
grid: {offset: false},
|
||||
ticks: {autoSkip: false, maxRotation: 90}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
position: "right",
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -81,12 +58,10 @@ export const options = {
|
||||
interface Props
|
||||
{
|
||||
data: DefaultChartData;
|
||||
chartSubheaderData?: ChartSubheaderData;
|
||||
}
|
||||
|
||||
const {gradients} = colors;
|
||||
|
||||
function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
function StackedBarChart({data}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -95,30 +70,23 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
|
||||
const handleClick = (e: Array<{}>) =>
|
||||
{
|
||||
if (e && e.length > 0 && data?.urls && data?.urls.length)
|
||||
if(e && e.length > 0 && data?.urls && data?.urls.length)
|
||||
{
|
||||
// @ts-ignore
|
||||
navigate(data.urls[e[0]["index"]]);
|
||||
}
|
||||
console.log(e);
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (data)
|
||||
if(data)
|
||||
{
|
||||
data?.datasets.forEach((dataset: any, index: number) =>
|
||||
{
|
||||
if (!dataset.backgroundColor)
|
||||
{
|
||||
if (gradients[chartColors[index]])
|
||||
{
|
||||
dataset.backgroundColor = gradients[chartColors[index]].state;
|
||||
}
|
||||
else
|
||||
{
|
||||
dataset.backgroundColor = chartColors[index];
|
||||
}
|
||||
dataset.backgroundColor = gradients[chartColors[index]].state;
|
||||
}
|
||||
});
|
||||
setStateData(stateData);
|
||||
@ -127,13 +95,8 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
|
||||
|
||||
return data ? (
|
||||
<Box p={3} pt={1}>
|
||||
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||
<Box width="100%" height="300px">
|
||||
<Bar data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
</Box>
|
||||
</Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;
|
||||
<Box p={3}><Bar data={data} options={options} getElementsAtEvent={handleClick} /></Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} /> ;
|
||||
}
|
||||
|
||||
export default StackedBarChart;
|
||||
|
@ -30,7 +30,6 @@ import {useNavigate} from "react-router-dom";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {chartColors} from "qqq/components/widgets/charts/DefaultChartData";
|
||||
import configs from "qqq/components/widgets/charts/piechart/PieChartConfigs";
|
||||
import ChartSubheaderWithData, {ChartSubheaderData} from "qqq/components/widgets/components/ChartSubheaderWithData";
|
||||
|
||||
//////////////////////////////////////////
|
||||
// structure of expected bar chart data //
|
||||
@ -52,29 +51,25 @@ interface Props
|
||||
{
|
||||
description?: string;
|
||||
chartData: PieChartData;
|
||||
chartSubheaderData?: ChartSubheaderData;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Element
|
||||
function PieChart({description, chartData}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
|
||||
if (chartData && chartData.dataset)
|
||||
{
|
||||
if(!chartData.dataset.backgroundColors)
|
||||
{
|
||||
chartData.dataset.backgroundColors = chartColors;
|
||||
}
|
||||
chartData.dataset.backgroundColors = chartColors;
|
||||
}
|
||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (chartData)
|
||||
if(chartData)
|
||||
{
|
||||
setDataLoaded(true);
|
||||
}
|
||||
@ -82,22 +77,19 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
|
||||
|
||||
const handleClick = (e: Array<{}>) =>
|
||||
{
|
||||
if (e && e.length > 0 && chartData?.dataset?.urls && chartData?.dataset?.urls.length)
|
||||
if(e && e.length > 0 && chartData?.dataset?.urls && chartData?.dataset?.urls.length)
|
||||
{
|
||||
// @ts-ignore
|
||||
navigate(chartData.dataset.urls[e[0]["index"]]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1, border: 0}}>
|
||||
<Box mt={1}>
|
||||
<Box px={3}>
|
||||
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||
</Box>
|
||||
<Card sx={{minHeight: "400px", boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1}}>
|
||||
<Box mt={3}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={12} justifyContent="center">
|
||||
<Box width="100%" height="300px" py={2} pr={2} pl={2}>
|
||||
<Box width="100%" height="80%" py={2} pr={2} pl={2}>
|
||||
{useMemo(
|
||||
() => (
|
||||
<Pie data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
@ -106,35 +98,32 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
|
||||
)}
|
||||
</Box>
|
||||
{
|
||||
!chartData && (
|
||||
! chartData && (
|
||||
<Box sx={{
|
||||
position: "absolute",
|
||||
top: "40%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
<Skeleton sx={{width: "150px", height: "150px"}} variant="circular" />
|
||||
justifyContent: "center"}}>
|
||||
<Skeleton sx={{width: "150px", height: "150px"}} variant="circular"/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider />
|
||||
{
|
||||
description && (
|
||||
<>
|
||||
<Divider />
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Box pb={2} px={2} display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
|
||||
<MDTypography variant="button" color="text" fontWeight="light">
|
||||
{parse(description)}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Box pb={2} px={2} display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
|
||||
<MDTypography variant="button" color="text" fontWeight="light">
|
||||
{parse(description)}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
|
@ -30,16 +30,10 @@ function configs(labels: any, datasets: any)
|
||||
if (datasets.backgroundColors)
|
||||
{
|
||||
datasets.backgroundColors.forEach((color: string) =>
|
||||
{
|
||||
if (gradients[color])
|
||||
{
|
||||
backgroundColors.push(gradients[color].state);
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundColors.push(color);
|
||||
}
|
||||
});
|
||||
gradients[color]
|
||||
? backgroundColors.push(gradients[color].state)
|
||||
: backgroundColors.push(dark.main)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -64,33 +58,12 @@ function configs(labels: any, datasets: any)
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
maintainAspectRatio: true,
|
||||
responsive: true,
|
||||
aspectRatio: 2,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context: any)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// our labels already have the value in them - so just use the label in the //
|
||||
// tooltip (lib by default puts label + value, so we were duplicating value!) //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
return context.label;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: "circle",
|
||||
padding: 12,
|
||||
boxHeight: 8,
|
||||
boxWidth: 8,
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import {Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
export interface ChartSubheaderData
|
||||
{
|
||||
mainNumber: number;
|
||||
vsPreviousPercent: number;
|
||||
vsPreviousNumber: number;
|
||||
isUpVsPrevious: boolean;
|
||||
isGoodVsPrevious: boolean;
|
||||
vsDescription: string;
|
||||
mainNumberUrl: string;
|
||||
previousNumberUrl: string;
|
||||
}
|
||||
|
||||
interface Props
|
||||
{
|
||||
chartSubheaderData: ChartSubheaderData;
|
||||
}
|
||||
|
||||
const GOOD_COLOR = colors.success.main;
|
||||
const BAD_COLOR = colors.error.main;
|
||||
const UP_ICON = "arrow_drop_up";
|
||||
const DOWN_ICON = "arrow_drop_down";
|
||||
|
||||
function StackedBarChart({chartSubheaderData}: Props): JSX.Element
|
||||
{
|
||||
let color = "black";
|
||||
if (chartSubheaderData && chartSubheaderData.isGoodVsPrevious != null)
|
||||
{
|
||||
color = chartSubheaderData.isGoodVsPrevious ? GOOD_COLOR : BAD_COLOR;
|
||||
}
|
||||
|
||||
let iconName: string = null;
|
||||
if (chartSubheaderData && chartSubheaderData.isUpVsPrevious != null)
|
||||
{
|
||||
iconName = chartSubheaderData.isUpVsPrevious ? UP_ICON : DOWN_ICON;
|
||||
}
|
||||
|
||||
let mainNumberElement = <Typography variant="h2" display="inline">{ValueUtils.getFormattedNumber(chartSubheaderData.mainNumber)}</Typography>;
|
||||
if(chartSubheaderData.mainNumberUrl)
|
||||
{
|
||||
mainNumberElement = <Link to={chartSubheaderData.mainNumberUrl}>{mainNumberElement}</Link>
|
||||
}
|
||||
mainNumberElement = <Box pr={1}>{mainNumberElement}</Box>
|
||||
|
||||
let previousNumberElement = (
|
||||
<>
|
||||
<Typography display="inline" variant="body2" sx={{color: colors.black.main}}>
|
||||
{chartSubheaderData.vsDescription}
|
||||
{chartSubheaderData.vsPreviousNumber && (<> ({ValueUtils.getFormattedNumber(chartSubheaderData.vsPreviousNumber)})</>)}
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
|
||||
if(chartSubheaderData.previousNumberUrl)
|
||||
{
|
||||
previousNumberElement = <Link to={chartSubheaderData.previousNumberUrl}>{previousNumberElement}</Link>
|
||||
}
|
||||
|
||||
return chartSubheaderData ? (
|
||||
<Box display="inline-flex" alignItems="flex-end" flexWrap="wrap">
|
||||
{mainNumberElement}
|
||||
{
|
||||
chartSubheaderData.vsPreviousPercent != null && iconName != null && (
|
||||
<Box display="inline-flex" alignItems="flex-end" pb={1} ml={-0.5}>
|
||||
<Icon fontSize="medium" sx={{color: color}}>{iconName}</Icon>
|
||||
<Typography display="inline" variant="body2" sx={{color: color}}>{chartSubheaderData.vsPreviousPercent}%</Typography>
|
||||
{previousNumberElement}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "12px"}} />;
|
||||
}
|
||||
|
||||
export default StackedBarChart;
|
@ -286,15 +286,18 @@ export default function DataBagViewer({dataBagId}: Props): JSX.Element
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<>
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1, mt: -3}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Raw Data" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||
</Tabs>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
||||
<Typography variant="h5" p={2}></Typography>
|
||||
<Tabs
|
||||
sx={{m: 1}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Raw Data" id="simple-tab-0" aria-controls="simple-tabpanel-0" sx={{width: "150px"}} />
|
||||
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "150px"}} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel index={0} value={selectedTab}>
|
||||
<Grid container>
|
||||
|
@ -22,14 +22,10 @@
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
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 React, {useEffect, useState} from "react";
|
||||
import {useNavigate, Link} from "react-router-dom";
|
||||
import Widget, {AddNewRecordButton, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
@ -51,8 +47,6 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
const [records, setRecords] = useState([] as QRecord[])
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [allColumns, setAllColumns] = useState([])
|
||||
const [csv, setCsv] = useState(null as string);
|
||||
const [fileName, setFileName] = useState(null as string);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() =>
|
||||
@ -81,7 +75,6 @@ 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) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const allColumns = [... columns];
|
||||
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
@ -102,42 +95,39 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
setRows(rows);
|
||||
setRecords(records)
|
||||
setColumns(columns);
|
||||
|
||||
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 += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
||||
}
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||
|
||||
setCsv(csv);
|
||||
setFileName(fileName);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const exportCallback = () =>
|
||||
{
|
||||
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 += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
||||
}
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||
HtmlUtils.download(fileName, csv);
|
||||
}
|
||||
|
||||
///////////////////
|
||||
// view all link //
|
||||
///////////////////
|
||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||
const labelAdditionalComponentsLeft: LabelComponent[] = []
|
||||
if(data && 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>
|
||||
)
|
||||
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
|
||||
}
|
||||
|
||||
///////////////////
|
||||
@ -159,26 +149,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle))
|
||||
|
||||
////////////////////
|
||||
// add new button //
|
||||
@ -213,7 +184,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
<Widget
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={data}
|
||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
>
|
||||
<DataGridPro
|
||||
|
@ -430,17 +430,20 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<>
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1, mt: -3}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Code" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||
<Tab label="Logs" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||
<Tab label="Test" id="simple-tab-1" aria-controls="simple-tabpanel-2" />
|
||||
<Tab label="Docs" id="simple-tab-1" aria-controls="simple-tabpanel-3" />
|
||||
</Tabs>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
||||
<Typography variant="h5" p={2}></Typography>
|
||||
<Tabs
|
||||
sx={{m: 1}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Code" id="simple-tab-0" aria-controls="simple-tabpanel-0" sx={{width: "100px"}} />
|
||||
<Tab label="Logs" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "100px"}} />
|
||||
<Tab label="Test" id="simple-tab-1" aria-controls="simple-tabpanel-2" sx={{width: "100px"}} />
|
||||
<Tab label="Docs" id="simple-tab-1" aria-controls="simple-tabpanel-3" sx={{width: "100px"}} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel index={0} value={selectedTab}>
|
||||
<Grid container>
|
||||
@ -495,7 +498,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
editorProps={{$blockScrolling: true}}
|
||||
setOptions={{useWorker: false}}
|
||||
width="100%"
|
||||
height="400px"
|
||||
height="368px"
|
||||
value={getSelectedFileCode()}
|
||||
style={{borderTop: "1px solid lightgray", borderBottomRightRadius: "1rem"}}
|
||||
/>
|
||||
|
@ -30,7 +30,7 @@ import TableRow from "@mui/material/TableRow";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import parse from "html-react-parser";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} from "react-table";
|
||||
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import MDPagination from "qqq/components/legacy/MDPagination";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
@ -47,8 +47,6 @@ interface Props
|
||||
canSearch?: boolean;
|
||||
showTotalEntries?: boolean;
|
||||
hidePaginationDropdown?: boolean;
|
||||
fixedStickyLastRow?: boolean;
|
||||
fixedHeight?: number;
|
||||
table: TableDataInput;
|
||||
pagination?: {
|
||||
variant: "contained" | "gradient";
|
||||
@ -58,18 +56,6 @@ interface Props
|
||||
noEndBorder?: boolean;
|
||||
}
|
||||
|
||||
DataTable.defaultProps = {
|
||||
entriesPerPage: 10,
|
||||
entriesPerPageOptions: ["5", "10", "15", "20", "25"],
|
||||
canSearch: false,
|
||||
showTotalEntries: true,
|
||||
fixedStickyLastRow: false,
|
||||
fixedHeight: null,
|
||||
pagination: {variant: "gradient", color: "info"},
|
||||
isSorted: true,
|
||||
noEndBorder: false,
|
||||
};
|
||||
|
||||
const NoMaxWidthTooltip = styled(({className, ...props}: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{popper: className}} />
|
||||
))({
|
||||
@ -85,8 +71,6 @@ function DataTable({
|
||||
hidePaginationDropdown,
|
||||
canSearch,
|
||||
showTotalEntries,
|
||||
fixedStickyLastRow,
|
||||
fixedHeight,
|
||||
table,
|
||||
pagination,
|
||||
isSorted,
|
||||
@ -99,77 +83,8 @@ function DataTable({
|
||||
defaultValue = (entriesPerPage) ? entriesPerPage : "10";
|
||||
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
|
||||
|
||||
let widths = [];
|
||||
for(let i = 0; i<table.columns.length; i++)
|
||||
{
|
||||
const column = table.columns[i];
|
||||
if(column.type !== "hidden")
|
||||
{
|
||||
widths.push(table.columns[i].width ?? "1fr");
|
||||
}
|
||||
}
|
||||
|
||||
let showExpandColumn = false;
|
||||
if(table.rows)
|
||||
{
|
||||
for (let i = 0; i < table.rows.length; i++)
|
||||
{
|
||||
if (table.rows[i].subRows)
|
||||
{
|
||||
showExpandColumn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columnsToMemo = [...table.columns];
|
||||
if(showExpandColumn)
|
||||
{
|
||||
widths.push("60px");
|
||||
columnsToMemo.push(
|
||||
{
|
||||
///////////////////////////////
|
||||
// Build our expander column //
|
||||
///////////////////////////////
|
||||
id: "__expander",
|
||||
width: 60,
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// use this block if we want to do expand-all //
|
||||
////////////////////////////////////////////////
|
||||
// @ts-ignore
|
||||
// header: ({getToggleAllRowsExpandedProps, isAllRowsExpanded}) => (
|
||||
// <span {...getToggleAllRowsExpandedProps()}>
|
||||
// {isAllRowsExpanded ? "yes" : "no"}
|
||||
// </span>
|
||||
// ),
|
||||
header: () => (<span />),
|
||||
|
||||
// @ts-ignore
|
||||
cell: ({row}) =>
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter to build the toggle for expanding a row //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
row.canExpand ? (
|
||||
<span
|
||||
{...row.getToggleRowExpandedProps({
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// We could use the row.depth property and paddingLeft to indicate the depth of the row //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// style: {paddingLeft: `${row.depth * 2}rem`,},
|
||||
})}
|
||||
>
|
||||
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
|
||||
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon>
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const columns = useMemo<any>(() => columnsToMemo, [table]);
|
||||
const columns = useMemo<any>(() => table.columns, [table]);
|
||||
const data = useMemo<any>(() => table.rows, [table]);
|
||||
const gridTemplateColumns = widths.join(" ");
|
||||
|
||||
if (!columns || !data)
|
||||
{
|
||||
@ -180,7 +95,6 @@ function DataTable({
|
||||
{columns, data, initialState: {pageIndex: 0}},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
usePagination
|
||||
);
|
||||
|
||||
@ -199,7 +113,7 @@ function DataTable({
|
||||
previousPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: {pageIndex, pageSize, globalFilter, expanded},
|
||||
state: {pageIndex, pageSize, globalFilter},
|
||||
}: any = tableInstance;
|
||||
|
||||
// Set the default value for the entries per page when component mounts
|
||||
@ -279,45 +193,79 @@ function DataTable({
|
||||
entriesEnd = pageSize * (pageIndex + 1);
|
||||
}
|
||||
|
||||
function getTable(includeHead: boolean, rows: any, isFooter: boolean)
|
||||
{
|
||||
let boxStyle = {};
|
||||
if(fixedStickyLastRow)
|
||||
{
|
||||
boxStyle = isFooter ? {overflowY: "visible", borderTop: "0.0625rem solid #f0f2f5;"} : {height: fixedHeight ? `${fixedHeight}px` : "360px", overflowY: "auto"};
|
||||
}
|
||||
|
||||
const className = isFooter ? "hideScrollbars" : "";
|
||||
return <Box sx={boxStyle} className={className}>
|
||||
<Table {...getTableProps()}>
|
||||
{
|
||||
includeHead && (
|
||||
<Box component="thead" sx={{position: "sticky", top: 0, background: "white"}}>
|
||||
{headerGroups.map((headerGroup: any, i: number) => (
|
||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", gridTemplateColumns: gridTemplateColumns}}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
column.type !== "hidden" && (
|
||||
<DataTableHeadCell
|
||||
key={i++}
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
align={column.align ? column.align : "left"}
|
||||
sorted={setSortedValue(column)}
|
||||
>
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
return (
|
||||
<TableContainer sx={{boxShadow: "none"}}>
|
||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && ! hidePaginationDropdown) || canSearch) ? (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||
{entriesPerPage && (hidePaginationDropdown === undefined || ! hidePaginationDropdown) && (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Autocomplete
|
||||
disableClearable
|
||||
value={pageSize.toString()}
|
||||
options={entries}
|
||||
onChange={(event, newValues: any) =>
|
||||
{
|
||||
if(typeof newValues === "string")
|
||||
{
|
||||
setEntriesPerPage(parseInt(newValues, 10));
|
||||
}
|
||||
else
|
||||
{
|
||||
setEntriesPerPage(parseInt(newValues[0], 10));
|
||||
}
|
||||
}}
|
||||
size="small"
|
||||
sx={{width: "5rem"}}
|
||||
renderInput={(params) => <MDInput {...params} />}
|
||||
/>
|
||||
<MDTypography variant="caption" color="secondary">
|
||||
entries per page
|
||||
</MDTypography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)}
|
||||
{canSearch && (
|
||||
<Box width="12rem" ml="auto">
|
||||
<MDInput
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
size="small"
|
||||
fullWidth
|
||||
onChange={({currentTarget}: any) =>
|
||||
{
|
||||
setSearch(search);
|
||||
onSearchChange(currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
<Table {...getTableProps()}>
|
||||
<Box component="thead">
|
||||
{headerGroups.map((headerGroup: any, i: number) => (
|
||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
column.type !== "hidden" && (
|
||||
<DataTableHeadCell
|
||||
key={i++}
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
width={column.width ? column.width : "auto"}
|
||||
align={column.align ? column.align : "left"}
|
||||
sorted={setSortedValue(column)}
|
||||
>
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</Box>
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map((row: any, key: any) =>
|
||||
{page.map((row: any, key: any) =>
|
||||
{
|
||||
prepareRow(row);
|
||||
return (
|
||||
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: (row.depth > 0 ? "#FAFAFA" : "initial")}} key={key} {...row.getRowProps()}>
|
||||
<TableRow sx={{verticalAlign: "top"}} key={key} {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
@ -360,9 +308,6 @@ function DataTable({
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(cell.column.id === "__expander") && cell.render("cell")
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))}
|
||||
@ -371,65 +316,6 @@ function DataTable({
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer sx={{boxShadow: "none"}}>
|
||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Autocomplete
|
||||
disableClearable
|
||||
value={pageSize.toString()}
|
||||
options={entries}
|
||||
onChange={(event, newValues: any) =>
|
||||
{
|
||||
if (typeof newValues === "string")
|
||||
{
|
||||
setEntriesPerPage(parseInt(newValues, 10));
|
||||
}
|
||||
else
|
||||
{
|
||||
setEntriesPerPage(parseInt(newValues[0], 10));
|
||||
}
|
||||
}}
|
||||
size="small"
|
||||
sx={{width: "5rem"}}
|
||||
renderInput={(params) => <MDInput {...params} />}
|
||||
/>
|
||||
<MDTypography variant="caption" color="secondary">
|
||||
entries per page
|
||||
</MDTypography>
|
||||
</Box>
|
||||
)}
|
||||
{canSearch && (
|
||||
<Box width="12rem" ml="auto">
|
||||
<MDInput
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
size="small"
|
||||
fullWidth
|
||||
onChange={({currentTarget}: any) =>
|
||||
{
|
||||
setSearch(search);
|
||||
onSearchChange(currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{
|
||||
fixedStickyLastRow ? (
|
||||
<>
|
||||
{getTable(true, page.slice(0, page.length -1), false)}
|
||||
{getTable(false, page.slice(page.length-1), true)}
|
||||
</>
|
||||
) : getTable(true, page, false)
|
||||
}
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
@ -482,4 +368,15 @@ function DataTable({
|
||||
);
|
||||
}
|
||||
|
||||
// Declaring default props for DataTable
|
||||
DataTable.defaultProps = {
|
||||
entriesPerPage: 10,
|
||||
entriesPerPageOptions: ["5", "10", "15", "20", "25"],
|
||||
canSearch: false,
|
||||
showTotalEntries: true,
|
||||
pagination: {variant: "gradient", color: "info"},
|
||||
isSorted: true,
|
||||
noEndBorder: false,
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
|
@ -54,13 +54,11 @@ interface Props
|
||||
noRowsFoundHTML?: string;
|
||||
rowsPerPage?: number;
|
||||
hidePaginationDropdown?: boolean;
|
||||
fixedStickyLastRow?: boolean;
|
||||
fixedHeight?: number;
|
||||
data: TableDataInput;
|
||||
}
|
||||
|
||||
const qController = Client.getInstance();
|
||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight}: Props): JSX.Element
|
||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown}: Props): JSX.Element
|
||||
{
|
||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||
|
||||
@ -81,8 +79,6 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
||||
table={data}
|
||||
entriesPerPage={rowsPerPage}
|
||||
hidePaginationDropdown={hidePaginationDropdown}
|
||||
fixedStickyLastRow={fixedStickyLastRow}
|
||||
fixedHeight={fixedHeight}
|
||||
showTotalEntries={false}
|
||||
isSorted={false}
|
||||
noEndBorder
|
||||
|
@ -21,15 +21,11 @@
|
||||
|
||||
|
||||
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
|
||||
import {htmlToText} from "html-to-text";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||
import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
||||
import Widget, {ExportDataButton, WidgetData} from "qqq/components/widgets/Widget";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
@ -47,8 +43,6 @@ TableWidget.defaultProps = {
|
||||
function TableWidget(props: Props): JSX.Element
|
||||
{
|
||||
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 columns = props.widgetData?.columns;
|
||||
@ -62,8 +56,14 @@ function TableWidget(props: Props): JSX.Element
|
||||
}
|
||||
setIsExportDisabled(isExportDisabled);
|
||||
|
||||
}, [props.widgetMetaData, props.widgetData]);
|
||||
|
||||
const exportCallback = () =>
|
||||
{
|
||||
if (props.widgetData && rows && columns)
|
||||
{
|
||||
console.log(props.widgetData);
|
||||
|
||||
let csv = "";
|
||||
for (let j = 0; j < columns.length; j++)
|
||||
{
|
||||
@ -98,37 +98,16 @@ function TableWidget(props: Props): JSX.Element
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
setCsv(csv);
|
||||
console.log(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);
|
||||
}
|
||||
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 (
|
||||
<Widget
|
||||
@ -137,14 +116,12 @@ function TableWidget(props: Props): JSX.Element
|
||||
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
||||
footerHTML={props.widgetData?.footerHTML}
|
||||
isChild={props.isChild}
|
||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||
labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
|
||||
>
|
||||
<TableCard
|
||||
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
||||
rowsPerPage={props.widgetData?.rowsPerPage}
|
||||
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
||||
fixedStickyLastRow={props.widgetData?.fixedStickyLastRow}
|
||||
fixedHeight={props.widgetData?.fixedHeight}
|
||||
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
|
||||
/>
|
||||
</Widget>
|
||||
|
@ -229,7 +229,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const download = (url: string, fileName: string) =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - this could be simplified, i think? //
|
||||
// todo - this could be simplified. //
|
||||
// it was originally built like this when we had to submit full access token to backend... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let xhr = new XMLHttpRequest();
|
||||
@ -237,6 +237,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
xhr.responseType = "blob";
|
||||
let formData = new FormData();
|
||||
|
||||
////////////////////////////////////
|
||||
// todo#authHeader - delete this. //
|
||||
////////////////////////////////////
|
||||
const qController = Client.getInstance();
|
||||
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
||||
|
||||
// @ts-ignore
|
||||
xhr.send(formData);
|
||||
|
||||
@ -323,7 +329,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
<Grid m={3} mt={9} container>
|
||||
<Grid item xs={0} lg={3} />
|
||||
<Grid item xs={12} lg={6}>
|
||||
<Card>
|
||||
<Card elevation={5}>
|
||||
<Box p={3}>
|
||||
<MDTypography variant="h5" component="div">
|
||||
Working
|
||||
@ -1278,7 +1284,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
mainCardStyles.background = "none";
|
||||
mainCardStyles.boxShadow = "none";
|
||||
mainCardStyles.border = "none";
|
||||
mainCardStyles.minHeight = "";
|
||||
mainCardStyles.alignItems = "stretch";
|
||||
mainCardStyles.flexGrow = 1;
|
||||
|
@ -55,7 +55,7 @@ import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, G
|
||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||
import FormData from "form-data";
|
||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import {Navigate, NavigateFunction, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||
@ -752,26 +752,62 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
console.log(`Received error for query ${thisQueryId}`);
|
||||
console.log(error);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// special case for variant errors, if 500 and certain message, just clear out //
|
||||
// local storage of variant and reload the page (rather than black page of death) //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
var errorMessage;
|
||||
if (error && error.message)
|
||||
if(tableMetaData?.usesVariants)
|
||||
{
|
||||
errorMessage = error.message;
|
||||
}
|
||||
else if (error && error.response && error.response.data && error.response.data.error)
|
||||
{
|
||||
errorMessage = error.response.data.error;
|
||||
if (error.status == "500" && error.message.indexOf("Could not find Backend Variant") != -1)
|
||||
{
|
||||
if (table)
|
||||
{
|
||||
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${table.name}`;
|
||||
localStorage.removeItem(tableVariantLocalStorageKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error && error.message)
|
||||
{
|
||||
errorMessage = error.message;
|
||||
}
|
||||
else if (error && error.response && error.response.data && error.response.data.error)
|
||||
{
|
||||
errorMessage = error.response.data.error;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Unexpected error running query";
|
||||
}
|
||||
|
||||
setAlertContent(errorMessage);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Unexpected error running query";
|
||||
if (error && error.message)
|
||||
{
|
||||
errorMessage = error.message;
|
||||
}
|
||||
else if (error && error.response && error.response.data && error.response.data.error)
|
||||
{
|
||||
errorMessage = error.response.data.error;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Unexpected error running query";
|
||||
}
|
||||
|
||||
queryErrors[thisQueryId] = errorMessage;
|
||||
setQueryErrors(queryErrors);
|
||||
setReceivedQueryErrorTimestamp(new Date());
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
queryErrors[thisQueryId] = errorMessage;
|
||||
setQueryErrors(queryErrors);
|
||||
setReceivedQueryErrorTimestamp(new Date());
|
||||
|
||||
throw error;
|
||||
});
|
||||
})
|
||||
})();
|
||||
};
|
||||
|
||||
@ -1136,6 +1172,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
<body>
|
||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||
<form id="exportForm" method="post" action="${url}" >
|
||||
<!-- todo#authHeader - remove this. -->
|
||||
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
|
||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||
<input type="hidden" name="filter" id="filter">
|
||||
</form>
|
||||
@ -1887,7 +1925,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
return (
|
||||
<BaseLayout>
|
||||
<TableVariantDialog table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
|
||||
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
|
||||
{
|
||||
setTableVariantPromptOpen(false);
|
||||
setTableVariant(value);
|
||||
@ -2017,7 +2055,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
page={pageNumber}
|
||||
checkboxSelection
|
||||
disableSelectionOnClick
|
||||
autoHeight={false}
|
||||
autoHeight
|
||||
rows={rows}
|
||||
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
|
||||
columns={columnsModel}
|
||||
@ -2041,7 +2079,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
getRowId={(row) => row.__rowIndex}
|
||||
selectionModel={rowSelectionModel}
|
||||
hideFooterSelectedRowCount={true}
|
||||
sx={{border: 0, height: "calc(100vh - 250px)"}}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
@ -2058,7 +2095,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
{
|
||||
tableMetaData &&
|
||||
<TableVariantDialog table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
|
||||
<TableVariantDialog navigate={navigate} table={tableMetaData} isOpen={tableVariantPromptOpen} closeHandler={(value: QTableVariant) =>
|
||||
{
|
||||
setTableVariantPromptOpen(false);
|
||||
setTableVariant(value);
|
||||
@ -2090,7 +2127,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
||||
function TableVariantDialog(props: {navigate: NavigateFunction, isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
||||
{
|
||||
const [value, setValue] = useState(null)
|
||||
const [dropDownOpen, setDropDownOpen] = useState(false)
|
||||
@ -2139,7 +2176,17 @@ function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; clos
|
||||
|
||||
return variants && (
|
||||
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
||||
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
|
||||
<DialogTitle sx={{display: "flex"}}>
|
||||
<Box sx={{display: "flex", flexGrow: 1}}>
|
||||
{props.table.variantTableLabel}
|
||||
</Box>
|
||||
<Box sx={{display: "flex"}}>
|
||||
<IconButton onClick={() =>
|
||||
{
|
||||
document.location.href = "/";
|
||||
}}><Icon sx={{align: "right"}} fontSize="small">close</Icon></IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
||||
<Autocomplete
|
||||
|
@ -190,7 +190,7 @@ function RecordDeveloperView({table}: Props): JSX.Element
|
||||
return (
|
||||
<div key={fieldName}>
|
||||
<Card sx={{mb: 3}}>
|
||||
<Typography variant="h6" p={2} pl={3} pb={3}>{field?.label}</Typography>
|
||||
<Typography variant="h6" p={2} pl={3} pb={1}>{field?.label}</Typography>
|
||||
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
|
||||
{scriptId ?
|
||||
|
@ -441,17 +441,20 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
setPageHeader(record.recordLabel);
|
||||
|
||||
if(!launchingProcess)
|
||||
if(record)
|
||||
{
|
||||
try
|
||||
setPageHeader(record.recordLabel);
|
||||
|
||||
if(!launchingProcess)
|
||||
{
|
||||
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.error("Error pushing history: " + e);
|
||||
try
|
||||
{
|
||||
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.error("Error pushing history: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -564,14 +564,3 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
display: inline;
|
||||
right: .5rem
|
||||
}
|
||||
|
||||
.hideScrollbars::-webkit-scrollbar {
|
||||
background: transparent; /* Chrome/Safari/Webkit */
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.hideScrollbars {
|
||||
padding-right: 8px; /* pad-right for about half the width of a scrollbar.. */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
}
|
@ -63,10 +63,6 @@ export default class HtmlUtils
|
||||
|
||||
/*******************************************************************************
|
||||
** 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) =>
|
||||
{
|
||||
@ -99,6 +95,18 @@ export default class HtmlUtils
|
||||
form.setAttribute("target", "downloadIframe");
|
||||
iframe.appendChild(form);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo#authHeader - remove after comfortable with sessionUUID //
|
||||
// todo - this could be simplified (i think?) //
|
||||
// it was originally built like this when we had to submit full access token to backend... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const authorizationInput = document.createElement("input");
|
||||
authorizationInput.setAttribute("type", "hidden");
|
||||
authorizationInput.setAttribute("id", "authorizationInput");
|
||||
authorizationInput.setAttribute("name", "Authorization");
|
||||
authorizationInput.setAttribute("value", Client.getInstance().getAuthorizationHeaderValue());
|
||||
form.appendChild(authorizationInput);
|
||||
|
||||
const downloadInput = document.createElement("input");
|
||||
downloadInput.setAttribute("type", "hidden");
|
||||
downloadInput.setAttribute("name", "download");
|
||||
@ -110,16 +118,15 @@ export default class HtmlUtils
|
||||
|
||||
/*******************************************************************************
|
||||
** 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) =>
|
||||
{
|
||||
if(url.startsWith("data:"))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo#authHeader - remove the Authorization input after comfortable with sessionUUID //
|
||||
// todo - this could be simplified (i think?) //
|
||||
// it was originally built like this when we had to submit full access token to backend... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const openInWindow = window.open("", "_blank");
|
||||
openInWindow.document.write(`<html lang="en">
|
||||
@ -147,6 +154,7 @@ export default class HtmlUtils
|
||||
<body>
|
||||
Opening ${filename}...
|
||||
<form id="exportForm" method="post" action="${url}" >
|
||||
<input type="hidden" name="Authorization" value="${Client.getInstance().getAuthorizationHeaderValue()}">
|
||||
</form>
|
||||
</body>
|
||||
</html>`);
|
||||
|
@ -29,18 +29,11 @@ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException
|
||||
class Client
|
||||
{
|
||||
private static qController: QController;
|
||||
private static unauthorizedCallback: () => void;
|
||||
|
||||
private static handleException(exception: QException)
|
||||
{
|
||||
// todo - check for 401 and clear cookie et al & logout?
|
||||
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);
|
||||
}
|
||||
|
||||
@ -53,11 +46,6 @@ class Client
|
||||
|
||||
return this.qController;
|
||||
}
|
||||
|
||||
static setUnauthorizedCallback(unauthorizedCallback: () => void)
|
||||
{
|
||||
Client.unauthorizedCallback = unauthorizedCallback;
|
||||
}
|
||||
}
|
||||
|
||||
export default Client;
|
||||
|
@ -1,11 +1,6 @@
|
||||
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 io.github.bonigarcia.wdm.WebDriverManager;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@ -16,7 +11,6 @@ import org.openqa.selenium.Dimension;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -60,15 +54,7 @@ public class QBaseSeleniumTest
|
||||
@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.manage().window().setSize(new Dimension(1700, 1300));
|
||||
qSeleniumLib = new QSeleniumLib(driver);
|
||||
|
||||
@ -82,57 +68,6 @@ public class QBaseSeleniumTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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.
|
||||
|
@ -1,24 +1,3 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
@ -27,15 +6,11 @@ import java.time.Duration;
|
||||
import java.util.List;
|
||||
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.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.OutputType;
|
||||
import org.openqa.selenium.StaleElementReferenceException;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
@ -61,8 +36,6 @@ public class QSeleniumLib
|
||||
private boolean SCREENSHOTS_ENABLED = true;
|
||||
private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/";
|
||||
|
||||
private boolean autoHighlight = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -214,13 +187,7 @@ public class QSeleniumLib
|
||||
*******************************************************************************/
|
||||
public WebElement waitForSelector(String cssSelector)
|
||||
{
|
||||
WebElement element = waitForSelectorAll(cssSelector, 1).get(0);
|
||||
|
||||
Actions actions = new Actions(driver);
|
||||
actions.moveToElement(element);
|
||||
|
||||
conditionallyAutoHighlight(element);
|
||||
return element;
|
||||
return (waitForSelectorAll(cssSelector, 1).get(0));
|
||||
}
|
||||
|
||||
|
||||
@ -263,7 +230,7 @@ public class QSeleniumLib
|
||||
do
|
||||
{
|
||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||
if(elements.isEmpty())
|
||||
if(elements.size() == 0)
|
||||
{
|
||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||
return;
|
||||
@ -289,7 +256,7 @@ public class QSeleniumLib
|
||||
do
|
||||
{
|
||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||
if(elements.isEmpty())
|
||||
if(elements.size() == 0)
|
||||
{
|
||||
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||
return;
|
||||
@ -363,22 +330,6 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -429,10 +380,7 @@ public class QSeleniumLib
|
||||
@FunctionalInterface
|
||||
public interface Code<T>
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
T run();
|
||||
public T run();
|
||||
}
|
||||
|
||||
|
||||
@ -482,7 +430,6 @@ public class QSeleniumLib
|
||||
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||
Actions actions = new Actions(driver);
|
||||
actions.moveToElement(element);
|
||||
conditionallyAutoHighlight(element);
|
||||
return (element);
|
||||
}
|
||||
}
|
||||
@ -490,10 +437,6 @@ public class QSeleniumLib
|
||||
{
|
||||
LOG.debug("Caught a StaleElementReferenceException - will retry.");
|
||||
}
|
||||
catch(NoSuchElementException nsee)
|
||||
{
|
||||
LOG.debug("Caught a NoSuchElementException - will retry.");
|
||||
}
|
||||
}
|
||||
|
||||
sleepABit();
|
||||
@ -506,20 +449,6 @@ 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
|
||||
** for the test class simple name, filename = methodName.png.
|
||||
@ -549,8 +478,7 @@ public class QSeleniumLib
|
||||
destFile.mkdirs();
|
||||
if(destFile.exists())
|
||||
{
|
||||
String newFileName = destFile.getAbsolutePath().replaceFirst("\\.png", "-" + System.currentTimeMillis() + ".png");
|
||||
destFile.renameTo(new File(newFileName));
|
||||
destFile.delete();
|
||||
}
|
||||
FileUtils.moveFile(outputFile, destFile);
|
||||
LOG.info("Made screenshot at: " + destFile);
|
||||
@ -627,48 +555,4 @@ 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,9 +29,7 @@ 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).
|
||||
** Test for Associated Record Scripts functionality.
|
||||
*******************************************************************************/
|
||||
public class ClickLinkOnRecordThenEditShortcutTest extends QBaseSeleniumTest
|
||||
{
|
||||
|
@ -1,111 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
@ -302,7 +302,8 @@
|
||||
"label": "Greetings App",
|
||||
"iconName": "emoji_people",
|
||||
"widgets": [
|
||||
"SampleTableWidget"
|
||||
"PersonsByCreateDateBarChart",
|
||||
"QuickSightChartRenderer"
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
@ -737,11 +738,139 @@
|
||||
"icon": "/kr-icon.png"
|
||||
},
|
||||
"widgets": {
|
||||
"SampleTableWidget": {
|
||||
"name": "SampleTableWidget",
|
||||
"label": "Sample Table Widget",
|
||||
"parcelTrackingDetails": {
|
||||
"name": "parcelTrackingDetails",
|
||||
"label": "Tracking Details",
|
||||
"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",
|
||||
"showExportButton": true
|
||||
"icon": "receipt"
|
||||
},
|
||||
"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": {
|
||||
"name": "scriptViewer",
|
||||
|
Reference in New Issue
Block a user