mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
33 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
0879cb4f80 | |||
f1b0618e9d | |||
95f1fa83bb | |||
4b0e12ba47 | |||
6cfb1e04ed | |||
0d763cbfc8 | |||
279840e77a | |||
51b2f5bb5a | |||
02fe351084 | |||
25fa2e82ea | |||
24b4674208 | |||
04630fd154 | |||
1503e2a1d5 | |||
3d7502531d | |||
d0a7db28fe | |||
95244a8aba | |||
45f247785c | |||
9e6d5c10fb | |||
4e0b13ad02 | |||
7f57a11e00 | |||
83da3a3a0a | |||
b59ed8c8c1 | |||
7101420124 | |||
b903e6bef9 | |||
970c9f262c | |||
9313988f9b | |||
123d1742e7 | |||
47fca52437 | |||
44b92690ab | |||
64fe2305ad | |||
91d38a1d15 | |||
60a8baff35 | |||
81b46408b4 |
@ -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.82",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.83",
|
||||
"@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.19.0-SNAPSHOT</revision>
|
||||
<revision>0.20.0-SNAPSHOT</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface MaterialDashboardIconRoleNames
|
||||
{
|
||||
String TOP_RIGHT_INSIDE_CARD = "topRightInsideCard";
|
||||
}
|
@ -31,7 +31,7 @@ type Types = any;
|
||||
|
||||
const card: Types = {
|
||||
defaultProps: {
|
||||
elevation: 3
|
||||
elevation: 0
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@ -42,7 +42,7 @@ const card: Types = {
|
||||
wordWrap: "break-word",
|
||||
backgroundColor: white.main,
|
||||
backgroundClip: "border-box",
|
||||
border: `${borderWidth[0]} solid ${rgba(black.main, 0.125)}`,
|
||||
border: `${borderWidth[1]} solid ${rgba(black.main, 0.25)}`,
|
||||
borderRadius: borderRadius.xl,
|
||||
overflow: "visible",
|
||||
},
|
||||
|
@ -15,15 +15,11 @@ Coded by www.creative-tim.com
|
||||
|
||||
// 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 { borderRadius } = borders;
|
||||
const { tabsBoxShadow } = boxShadows;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
@ -32,15 +28,22 @@ const tabs: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
position: "relative",
|
||||
backgroundColor: grey[100],
|
||||
borderRadius: borderRadius.xl,
|
||||
borderRadius: 0,
|
||||
borderBottom: "1px solid",
|
||||
borderBottomColor: grey[400],
|
||||
minHeight: "unset",
|
||||
padding: pxToRem(4),
|
||||
padding: "0",
|
||||
margin: "0"
|
||||
},
|
||||
|
||||
scroller: {
|
||||
marginLeft: "0.5rem"
|
||||
},
|
||||
|
||||
flexContainer: {
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
width: "fit-content",
|
||||
zIndex: 10,
|
||||
},
|
||||
|
||||
@ -57,9 +60,10 @@ const tabs: Types = {
|
||||
|
||||
indicator: {
|
||||
height: "100%",
|
||||
borderRadius: borderRadius.lg,
|
||||
borderRadius: 0,
|
||||
backgroundColor: white.main,
|
||||
boxShadow: tabsBoxShadow.indicator,
|
||||
borderBottom: "2px solid",
|
||||
borderBottomColor: colors.info.main,
|
||||
transition: "all 500ms ease",
|
||||
},
|
||||
},
|
||||
|
@ -43,8 +43,10 @@ const tab: Types = {
|
||||
fontWeight: fontWeightRegular,
|
||||
textTransform: "none",
|
||||
lineHeight: "inherit",
|
||||
padding: pxToRem(4),
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: "0.75rem 0.5rem 0.5rem",
|
||||
margin: "0 0.5rem",
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
color: `${dark.main} !important`,
|
||||
opacity: "1 !important",
|
||||
|
||||
|
@ -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 } = colors;
|
||||
const { black, light, white, dark } = colors;
|
||||
const { size, fontWeightRegular } = typography;
|
||||
const { borderRadius } = borders;
|
||||
|
||||
@ -39,19 +39,20 @@ const tooltip: Types = {
|
||||
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
maxWidth: pxToRem(200),
|
||||
backgroundColor: black.main,
|
||||
color: light.main,
|
||||
maxWidth: pxToRem(300),
|
||||
backgroundColor: white.main,
|
||||
color: dark.main,
|
||||
fontSize: size.sm,
|
||||
fontWeight: fontWeightRegular,
|
||||
textAlign: "center",
|
||||
textAlign: "left",
|
||||
borderRadius: borderRadius.md,
|
||||
opacity: 0.7,
|
||||
padding: `${pxToRem(5)} ${pxToRem(8)} ${pxToRem(4)}`,
|
||||
padding: "1rem",
|
||||
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 3px -2px, rgba(0, 0, 0, 0.14) 0px 3px 4px 0px, rgba(0, 0, 0, 0.12) 0px 1px 8px 0px"
|
||||
},
|
||||
|
||||
arrow: {
|
||||
color: black.main,
|
||||
color: white.main,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -19,13 +19,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {InputLabel} from "@mui/material";
|
||||
import {Box, InputLabel} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {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,
|
||||
@ -60,6 +61,9 @@ const AntSwitch = styled(Switch)(({theme}) => ({
|
||||
duration: 200,
|
||||
}),
|
||||
},
|
||||
"&.nullSwitch .MuiSwitch-thumb": {
|
||||
width: 24,
|
||||
},
|
||||
"& .MuiSwitch-track": {
|
||||
borderRadius: 16 / 2,
|
||||
opacity: 1,
|
||||
@ -78,6 +82,7 @@ interface Props
|
||||
}
|
||||
|
||||
|
||||
|
||||
function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Element
|
||||
{
|
||||
const {setFieldValue} = useFormikContext();
|
||||
@ -96,8 +101,10 @@ 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
|
||||
@ -107,7 +114,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
||||
sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}>
|
||||
No
|
||||
</Typography>
|
||||
<AntSwitch name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||
<AntSwitch className={classNullSwitch} name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||
<Typography
|
||||
fontSize="0.875rem"
|
||||
color={value === true ? "auto" : "#bfbfbf"}
|
||||
@ -116,7 +123,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
||||
Yes
|
||||
</Typography>
|
||||
</Stack>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,14 @@ function QDynamicFormField({
|
||||
if (type === "checkbox")
|
||||
{
|
||||
getsBulkEditHtmlLabel = false;
|
||||
field = (<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />);
|
||||
field = (<>
|
||||
<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />
|
||||
<Box mt={0.75}>
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</>);
|
||||
}
|
||||
else if (type === "ace")
|
||||
{
|
||||
|
@ -426,6 +426,11 @@ 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);
|
||||
@ -438,17 +443,17 @@ function EntityForm(props: Props): JSX.Element
|
||||
// changing from, say, 12:15:30 to just 12:15:00... this seems to get around that, for cases when the //
|
||||
// user didn't change the value in the field (but if the user did change the value, then we will submit it) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldMetaData.type === QFieldType.DATE_TIME && values[fieldName])
|
||||
if(fieldMetaData.type === QFieldType.DATE_TIME && valuesToPost[fieldName])
|
||||
{
|
||||
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${values[fieldName]}]`)
|
||||
if (initialValues[fieldName] == values[fieldName])
|
||||
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${valuesToPost[fieldName]}]`)
|
||||
if (initialValues[fieldName] == valuesToPost[fieldName])
|
||||
{
|
||||
console.log(" - Is the same, so, deleting from the post");
|
||||
delete (values[fieldName]);
|
||||
delete (valuesToPost[fieldName]);
|
||||
}
|
||||
else
|
||||
{
|
||||
values[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(values[fieldName]);
|
||||
valuesToPost[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(valuesToPost[fieldName]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,10 +466,10 @@ function EntityForm(props: Props): JSX.Element
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldMetaData.type === QFieldType.BLOB)
|
||||
{
|
||||
if(typeof values[fieldName] === "string")
|
||||
if(typeof valuesToPost[fieldName] === "string")
|
||||
{
|
||||
console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`);
|
||||
delete(values[fieldName]);
|
||||
delete(valuesToPost[fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -473,7 +478,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
// todo - audit that it's a dupe
|
||||
await qController
|
||||
.update(tableName, props.id, values)
|
||||
.update(tableName, props.id, valuesToPost)
|
||||
.then((record) =>
|
||||
{
|
||||
if (props.isModal)
|
||||
@ -506,7 +511,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
else
|
||||
{
|
||||
await qController
|
||||
.create(tableName, values)
|
||||
.create(tableName, valuesToPost)
|
||||
.then((record) =>
|
||||
{
|
||||
if (props.isModal)
|
||||
|
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Popper} from "@mui/material";
|
||||
import {Popper, InputAdornment} from "@mui/material";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import 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, navbarIconButton, navbarRow,} from "qqq/components/horseshoe/Styles";
|
||||
import {setTransparentNavbar, useMaterialUIController,} from "qqq/context";
|
||||
import {navbar, navbarContainer, navbarRow, navbarMobileMenu, recentlyViewedMenu,} from "qqq/components/horseshoe/Styles";
|
||||
import {setTransparentNavbar, useMaterialUIController, setMiniSidenav} 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 {transparentNavbar, fixedNavbar, darkMode,} = controller;
|
||||
const {miniSidenav, transparentNavbar, fixedNavbar, darkMode,} = controller;
|
||||
const [openMenu, setOpenMenu] = useState<any>(false);
|
||||
const [history, setHistory] = useState<any>([] as HistoryEntry[]);
|
||||
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
|
||||
@ -105,6 +105,8 @@ 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);
|
||||
@ -162,7 +164,15 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
onChange={handleAutocompleteOnChange}
|
||||
PopperComponent={CustomPopper}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" />}
|
||||
sx={recentlyViewedMenu}
|
||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Icon sx={{position: "relative", right: "-1rem"}}>keyboard_arrow_down</Icon>
|
||||
</InputAdornment>
|
||||
)
|
||||
}} />}
|
||||
renderOption={(props, option: HistoryEntry) => (
|
||||
<Box {...props} component="li" key={option.id} sx={{width: "auto"}}>
|
||||
<Box sx={{width: "auto", px: "8px", whiteSpace: "overflow"}} key={option.id}>
|
||||
@ -175,22 +185,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Render the notifications menu
|
||||
const renderMenu = () => (
|
||||
<Menu
|
||||
anchorEl={openMenu}
|
||||
anchorReference={null}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
open={Boolean(openMenu)}
|
||||
onClose={handleCloseMenu}
|
||||
sx={{mt: 2}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Styles for the navbar icons
|
||||
const iconsStyle = ({
|
||||
palette: {dark, white, text},
|
||||
@ -240,26 +234,22 @@ 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={1}>
|
||||
<Box pr={0} mr={-2} mt={-4}>
|
||||
{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,11 +110,10 @@ const navbarContainer = ({breakpoints}: Theme): any => ({
|
||||
const navbarRow = ({breakpoints}: Theme, {isMini}: any) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
|
||||
[breakpoints.up("md")]: {
|
||||
justifyContent: isMini ? "space-between" : "stretch",
|
||||
justifyContent: "stretch",
|
||||
width: isMini ? "100%" : "max-content",
|
||||
},
|
||||
|
||||
@ -146,12 +145,27 @@ const navbarDesktopMenu = ({breakpoints}: Theme) => ({
|
||||
display: "none !important",
|
||||
cursor: "pointer",
|
||||
|
||||
[breakpoints.up("xl")]: {
|
||||
[breakpoints.down("sm")]: {
|
||||
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,
|
||||
|
||||
@ -167,4 +181,5 @@ 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 = 250;
|
||||
const sidebarWidth = 275;
|
||||
const {transparent, gradients, white, background} = palette;
|
||||
const {xxl} = boxShadows;
|
||||
const {pxToRem, linearGradient} = functions;
|
||||
@ -94,6 +94,9 @@ 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()),
|
||||
},
|
||||
|
@ -28,11 +28,12 @@ interface TabPanelProps
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export default function TabPanel(props: TabPanelProps)
|
||||
{
|
||||
const {children, value, index, ...other} = props;
|
||||
const {children, value, index, style, ...other} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -40,6 +41,7 @@ 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 records input records will immediately be processed.
|
||||
If you choose this option, the input records will immediately be processed.
|
||||
You will be told how many records were successfully processed, and which ones had issues after the processing is completed.
|
||||
<br />
|
||||
<br />
|
||||
|
@ -22,11 +22,13 @@ 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";
|
||||
@ -44,7 +46,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, {WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
|
||||
import Widget, {HeaderIcon, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import TableWidget from "./tables/TableWidget";
|
||||
@ -58,9 +60,10 @@ interface Props
|
||||
tableName?: string;
|
||||
entityPrimaryKey?: string;
|
||||
omitWrappingGridContainer: boolean;
|
||||
areChildren?: boolean
|
||||
childUrlParams?: string
|
||||
parentWidgetMetaData?: QWidgetMetaData
|
||||
areChildren?: boolean;
|
||||
childUrlParams?: string;
|
||||
parentWidgetMetaData?: QWidgetMetaData;
|
||||
wrapWidgetsInTabPanels: boolean;
|
||||
}
|
||||
|
||||
DashboardWidgets.defaultProps = {
|
||||
@ -70,12 +73,12 @@ DashboardWidgets.defaultProps = {
|
||||
omitWrappingGridContainer: false,
|
||||
areChildren: false,
|
||||
childUrlParams: "",
|
||||
parentWidgetMetaData: null
|
||||
parentWidgetMetaData: null,
|
||||
wrapWidgetsInTabPanels: false,
|
||||
};
|
||||
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData}: Props): JSX.Element
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels}: Props): JSX.Element
|
||||
{
|
||||
const location = useLocation();
|
||||
const [widgetData, setWidgetData] = useState([] as any[]);
|
||||
const [widgetCounter, setWidgetCounter] = useState(0);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
@ -84,6 +87,24 @@ 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([]);
|
||||
@ -151,7 +172,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
|
||||
forceUpdate();
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
||||
{
|
||||
@ -227,6 +248,16 @@ 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%"}}>
|
||||
{
|
||||
@ -238,7 +269,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetIndex={i}
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
reloadWidgetCallback={reloadWidget}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
storeDropdownSelections={widgetMetaData.storeDropdownSelections}
|
||||
/>
|
||||
)
|
||||
@ -270,8 +301,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
>
|
||||
<StackedBarChart data={widgetData[i]?.chartData}/>
|
||||
<StackedBarChart data={widgetData[i]?.chartData} chartSubheaderData={widgetData[i]?.chartSubheaderData} />
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
@ -381,10 +413,12 @@ 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>
|
||||
@ -461,32 +495,62 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const body: JSX.Element =
|
||||
(
|
||||
<>
|
||||
{
|
||||
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>
|
||||
))
|
||||
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>)
|
||||
})
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
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 ? (
|
||||
omitWrappingGridContainer ? body :
|
||||
(
|
||||
<>
|
||||
{tabs}
|
||||
{
|
||||
omitWrappingGridContainer ? body : (
|
||||
<Grid container spacing={3} pb={4}>
|
||||
{body}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ export interface ParentWidgetData
|
||||
dropdownNeedsSelectedText?: string;
|
||||
storeDropdownSelections?: boolean;
|
||||
icon?: string;
|
||||
layoutType: string;
|
||||
}
|
||||
|
||||
|
||||
@ -55,7 +56,7 @@ interface Props
|
||||
widgetMetaData?: QWidgetMetaData;
|
||||
widgetIndex: number;
|
||||
data: ParentWidgetData;
|
||||
reloadWidgetCallback?: (widgetIndex: number, params: string) => void;
|
||||
reloadWidgetCallback?: (params: string) => void;
|
||||
entityPrimaryKey?: string;
|
||||
tableName?: string;
|
||||
storeDropdownSelections?: boolean;
|
||||
@ -91,10 +92,15 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
||||
}
|
||||
}, [qInstance, data, childUrlParams]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setChildUrlParams(urlParams)
|
||||
}, [urlParams]);
|
||||
|
||||
const parentReloadWidgetCallback = (data: string) =>
|
||||
{
|
||||
setChildUrlParams(data);
|
||||
reloadWidgetCallback(widgetIndex, data);
|
||||
reloadWidgetCallback(data);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -112,7 +118,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}/>
|
||||
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType == "TABS"}/>
|
||||
</Box>
|
||||
</Widget>
|
||||
) : null
|
||||
|
@ -96,6 +96,49 @@ export class LabelComponent
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
export class HeaderIcon extends LabelComponent
|
||||
{
|
||||
iconName: string;
|
||||
color: string;
|
||||
coloredBG: boolean;
|
||||
|
||||
iconColor: string;
|
||||
bgColor: string;
|
||||
|
||||
constructor(iconName: string, color: string, coloredBG: boolean = true)
|
||||
{
|
||||
super();
|
||||
this.iconName = iconName;
|
||||
this.color = color;
|
||||
this.coloredBG = coloredBG;
|
||||
|
||||
this.iconColor = this.coloredBG ? "#FFFFFF" : this.color;
|
||||
this.bgColor = this.coloredBG ? this.color : "none";
|
||||
}
|
||||
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -406,18 +449,35 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
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%"}} height={"3.5rem"}>
|
||||
<Box pt={2} pb={1}>
|
||||
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} minHeight={"3.5rem"}>
|
||||
<Box pt={2} pb={1} ml={2}>
|
||||
{
|
||||
hasPermission ?
|
||||
props.widgetMetaData?.icon && (
|
||||
<Box
|
||||
ml={3}
|
||||
ml={1}
|
||||
mr={2}
|
||||
mt={-4}
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -457,20 +517,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
)
|
||||
}
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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 && labelToUse && (labelElement)
|
||||
}
|
||||
{
|
||||
hasPermission && (
|
||||
@ -530,7 +577,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>
|
||||
{widgetContent}
|
||||
</Card>
|
||||
: widgetContent;
|
||||
: <span style={{width: "100%"}}>{widgetContent}</span>;
|
||||
}
|
||||
|
||||
export default Widget;
|
||||
|
@ -28,6 +28,7 @@ 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,
|
||||
@ -39,18 +40,40 @@ 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: {offset: false},
|
||||
grid: {display: false},
|
||||
ticks: {autoSkip: false, maxRotation: 90}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
position: "right",
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -58,10 +81,12 @@ export const options = {
|
||||
interface Props
|
||||
{
|
||||
data: DefaultChartData;
|
||||
chartSubheaderData?: ChartSubheaderData;
|
||||
}
|
||||
|
||||
const {gradients} = colors;
|
||||
function StackedBarChart({data}: Props): JSX.Element
|
||||
|
||||
function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -76,7 +101,7 @@ function StackedBarChart({data}: Props): JSX.Element
|
||||
navigate(data.urls[e[0]["index"]]);
|
||||
}
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@ -85,9 +110,16 @@ function StackedBarChart({data}: Props): JSX.Element
|
||||
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];
|
||||
}
|
||||
}
|
||||
});
|
||||
setStateData(stateData);
|
||||
}
|
||||
@ -95,7 +127,12 @@ function StackedBarChart({data}: Props): JSX.Element
|
||||
|
||||
|
||||
return data ? (
|
||||
<Box p={3}><Bar data={data} options={options} getElementsAtEvent={handleClick} /></Box>
|
||||
<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"}} />;
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ 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 //
|
||||
@ -51,20 +52,24 @@ interface Props
|
||||
{
|
||||
description?: string;
|
||||
chartData: PieChartData;
|
||||
chartSubheaderData?: ChartSubheaderData;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
function PieChart({description, chartData}: Props): JSX.Element
|
||||
function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
|
||||
if (chartData && chartData.dataset)
|
||||
{
|
||||
if(!chartData.dataset.backgroundColors)
|
||||
{
|
||||
chartData.dataset.backgroundColors = chartColors;
|
||||
}
|
||||
}
|
||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
||||
|
||||
useEffect(() =>
|
||||
@ -82,14 +87,17 @@ function PieChart({description, chartData}: Props): JSX.Element
|
||||
// @ts-ignore
|
||||
navigate(chartData.dataset.urls[e[0]["index"]]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card sx={{minHeight: "400px", boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1}}>
|
||||
<Box mt={3}>
|
||||
<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>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={12} justifyContent="center">
|
||||
<Box width="100%" height="80%" py={2} pr={2} pl={2}>
|
||||
<Box width="100%" height="300px" py={2} pr={2} pl={2}>
|
||||
{useMemo(
|
||||
() => (
|
||||
<Pie data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
@ -105,16 +113,18 @@ function PieChart({description, chartData}: Props): JSX.Element
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
justifyContent: "center"}}>
|
||||
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">
|
||||
@ -124,6 +134,7 @@ function PieChart({description, chartData}: Props): JSX.Element
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
|
@ -30,10 +30,16 @@ function configs(labels: any, datasets: any)
|
||||
if (datasets.backgroundColors)
|
||||
{
|
||||
datasets.backgroundColors.forEach((color: string) =>
|
||||
gradients[color]
|
||||
? backgroundColors.push(gradients[color].state)
|
||||
: backgroundColors.push(dark.main)
|
||||
);
|
||||
{
|
||||
if (gradients[color])
|
||||
{
|
||||
backgroundColors.push(gradients[color].state);
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundColors.push(color);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -58,12 +64,33 @@ function configs(labels: any, datasets: any)
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: true,
|
||||
maintainAspectRatio: false,
|
||||
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: {
|
||||
|
105
src/qqq/components/widgets/components/ChartSubheaderWithData.tsx
Normal file
105
src/qqq/components/widgets/components/ChartSubheaderWithData.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import {Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
export interface ChartSubheaderData
|
||||
{
|
||||
mainNumber: number;
|
||||
vsPreviousPercent: number;
|
||||
vsPreviousNumber: number;
|
||||
isUpVsPrevious: boolean;
|
||||
isGoodVsPrevious: boolean;
|
||||
vsDescription: string;
|
||||
mainNumberUrl: string;
|
||||
previousNumberUrl: string;
|
||||
}
|
||||
|
||||
interface Props
|
||||
{
|
||||
chartSubheaderData: ChartSubheaderData;
|
||||
}
|
||||
|
||||
const GOOD_COLOR = colors.success.main;
|
||||
const BAD_COLOR = colors.error.main;
|
||||
const UP_ICON = "arrow_drop_up";
|
||||
const DOWN_ICON = "arrow_drop_down";
|
||||
|
||||
function StackedBarChart({chartSubheaderData}: Props): JSX.Element
|
||||
{
|
||||
let color = "black";
|
||||
if (chartSubheaderData && chartSubheaderData.isGoodVsPrevious != null)
|
||||
{
|
||||
color = chartSubheaderData.isGoodVsPrevious ? GOOD_COLOR : BAD_COLOR;
|
||||
}
|
||||
|
||||
let iconName: string = null;
|
||||
if (chartSubheaderData && chartSubheaderData.isUpVsPrevious != null)
|
||||
{
|
||||
iconName = chartSubheaderData.isUpVsPrevious ? UP_ICON : DOWN_ICON;
|
||||
}
|
||||
|
||||
let mainNumberElement = <Typography variant="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,18 +286,15 @@ export default function DataBagViewer({dataBagId}: Props): JSX.Element
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
||||
<Typography variant="h5" p={2}></Typography>
|
||||
<Tabs
|
||||
sx={{m: 1}}
|
||||
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" sx={{width: "150px"}} />
|
||||
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "150px"}} />
|
||||
<Tab label="Raw Data" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel index={0} value={selectedTab}>
|
||||
<Grid container>
|
||||
|
@ -430,20 +430,17 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
||||
<Typography variant="h5" p={2}></Typography>
|
||||
<Tabs
|
||||
sx={{m: 1}}
|
||||
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" 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"}} />
|
||||
<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>
|
||||
|
||||
<TabPanel index={0} value={selectedTab}>
|
||||
<Grid container>
|
||||
@ -498,7 +495,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
editorProps={{$blockScrolling: true}}
|
||||
setOptions={{useWorker: false}}
|
||||
width="100%"
|
||||
height="368px"
|
||||
height="400px"
|
||||
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} from "react-table";
|
||||
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} 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,6 +47,8 @@ interface Props
|
||||
canSearch?: boolean;
|
||||
showTotalEntries?: boolean;
|
||||
hidePaginationDropdown?: boolean;
|
||||
fixedStickyLastRow?: boolean;
|
||||
fixedHeight?: number;
|
||||
table: TableDataInput;
|
||||
pagination?: {
|
||||
variant: "contained" | "gradient";
|
||||
@ -56,6 +58,18 @@ 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}} />
|
||||
))({
|
||||
@ -71,6 +85,8 @@ function DataTable({
|
||||
hidePaginationDropdown,
|
||||
canSearch,
|
||||
showTotalEntries,
|
||||
fixedStickyLastRow,
|
||||
fixedHeight,
|
||||
table,
|
||||
pagination,
|
||||
isSorted,
|
||||
@ -83,8 +99,77 @@ function DataTable({
|
||||
defaultValue = (entriesPerPage) ? entriesPerPage : "10";
|
||||
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
|
||||
|
||||
const columns = useMemo<any>(() => table.columns, [table]);
|
||||
let widths = [];
|
||||
for(let i = 0; i<table.columns.length; i++)
|
||||
{
|
||||
const column = table.columns[i];
|
||||
if(column.type !== "hidden")
|
||||
{
|
||||
widths.push(table.columns[i].width ?? "1fr");
|
||||
}
|
||||
}
|
||||
|
||||
let showExpandColumn = false;
|
||||
if(table.rows)
|
||||
{
|
||||
for (let i = 0; i < table.rows.length; i++)
|
||||
{
|
||||
if (table.rows[i].subRows)
|
||||
{
|
||||
showExpandColumn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columnsToMemo = [...table.columns];
|
||||
if(showExpandColumn)
|
||||
{
|
||||
widths.push("60px");
|
||||
columnsToMemo.push(
|
||||
{
|
||||
///////////////////////////////
|
||||
// Build our expander column //
|
||||
///////////////////////////////
|
||||
id: "__expander",
|
||||
width: 60,
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// use this block if we want to do expand-all //
|
||||
////////////////////////////////////////////////
|
||||
// @ts-ignore
|
||||
// header: ({getToggleAllRowsExpandedProps, isAllRowsExpanded}) => (
|
||||
// <span {...getToggleAllRowsExpandedProps()}>
|
||||
// {isAllRowsExpanded ? "yes" : "no"}
|
||||
// </span>
|
||||
// ),
|
||||
header: () => (<span />),
|
||||
|
||||
// @ts-ignore
|
||||
cell: ({row}) =>
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter to build the toggle for expanding a row //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
row.canExpand ? (
|
||||
<span
|
||||
{...row.getToggleRowExpandedProps({
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// We could use the row.depth property and paddingLeft to indicate the depth of the row //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// style: {paddingLeft: `${row.depth * 2}rem`,},
|
||||
})}
|
||||
>
|
||||
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
|
||||
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon>
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const columns = useMemo<any>(() => columnsToMemo, [table]);
|
||||
const data = useMemo<any>(() => table.rows, [table]);
|
||||
const gridTemplateColumns = widths.join(" ");
|
||||
|
||||
if (!columns || !data)
|
||||
{
|
||||
@ -95,6 +180,7 @@ function DataTable({
|
||||
{columns, data, initialState: {pageIndex: 0}},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
usePagination
|
||||
);
|
||||
|
||||
@ -113,7 +199,7 @@ function DataTable({
|
||||
previousPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: {pageIndex, pageSize, globalFilter},
|
||||
state: {pageIndex, pageSize, globalFilter, expanded},
|
||||
}: any = tableInstance;
|
||||
|
||||
// Set the default value for the entries per page when component mounts
|
||||
@ -193,6 +279,101 @@ 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>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.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()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
key={key}
|
||||
noBorder={noEndBorder && rows.length - 1 === key}
|
||||
align={cell.column.align ? cell.column.align : "left"}
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{
|
||||
cell.column.type === "default" && (
|
||||
cell.value && "number" === typeof cell.value ? (
|
||||
<DefaultCell>{cell.value.toLocaleString()}</DefaultCell>
|
||||
) : (<DefaultCell>{cell.render("Cell")}</DefaultCell>)
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "htmlAndTooltip" && (
|
||||
<DefaultCell>
|
||||
|
||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||
<Box>
|
||||
{parse(cell.value)}
|
||||
</Box>
|
||||
</NoMaxWidthTooltip>
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "html" && (
|
||||
<DefaultCell>{parse(cell.value)}</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(cell.column.id === "__expander") && cell.render("cell")
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer sx={{boxShadow: "none"}}>
|
||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||
@ -240,82 +421,15 @@ function DataTable({
|
||||
)}
|
||||
</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()}>
|
||||
{page.map((row: any, key: any) =>
|
||||
{
|
||||
prepareRow(row);
|
||||
return (
|
||||
<TableRow sx={{verticalAlign: "top"}} key={key} {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
key={key}
|
||||
noBorder={noEndBorder && rows.length - 1 === key}
|
||||
align={cell.column.align ? cell.column.align : "left"}
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{
|
||||
cell.column.type === "default" && (
|
||||
cell.value && "number" === typeof cell.value ? (
|
||||
<DefaultCell>{cell.value.toLocaleString()}</DefaultCell>
|
||||
) : (<DefaultCell>{cell.render("Cell")}</DefaultCell>)
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "htmlAndTooltip" && (
|
||||
<DefaultCell>
|
||||
|
||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||
<Box>
|
||||
{parse(cell.value)}
|
||||
</Box>
|
||||
</NoMaxWidthTooltip>
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "html" && (
|
||||
<DefaultCell>{parse(cell.value)}</DefaultCell>
|
||||
)
|
||||
fixedStickyLastRow ? (
|
||||
<>
|
||||
{getTable(true, page.slice(0, page.length -1), false)}
|
||||
{getTable(false, page.slice(page.length-1), true)}
|
||||
</>
|
||||
) : getTable(true, page, false)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||
)
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
@ -368,15 +482,4 @@ function DataTable({
|
||||
);
|
||||
}
|
||||
|
||||
// Declaring default props for DataTable
|
||||
DataTable.defaultProps = {
|
||||
entriesPerPage: 10,
|
||||
entriesPerPageOptions: ["5", "10", "15", "20", "25"],
|
||||
canSearch: false,
|
||||
showTotalEntries: true,
|
||||
pagination: {variant: "gradient", color: "info"},
|
||||
isSorted: true,
|
||||
noEndBorder: false,
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
|
@ -54,11 +54,13 @@ interface Props
|
||||
noRowsFoundHTML?: string;
|
||||
rowsPerPage?: number;
|
||||
hidePaginationDropdown?: boolean;
|
||||
fixedStickyLastRow?: boolean;
|
||||
fixedHeight?: number;
|
||||
data: TableDataInput;
|
||||
}
|
||||
|
||||
const qController = Client.getInstance();
|
||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown}: Props): JSX.Element
|
||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight}: Props): JSX.Element
|
||||
{
|
||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||
|
||||
@ -79,6 +81,8 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown}:
|
||||
table={data}
|
||||
entriesPerPage={rowsPerPage}
|
||||
hidePaginationDropdown={hidePaginationDropdown}
|
||||
fixedStickyLastRow={fixedStickyLastRow}
|
||||
fixedHeight={fixedHeight}
|
||||
showTotalEntries={false}
|
||||
isSorted={false}
|
||||
noEndBorder
|
||||
|
@ -143,6 +143,8 @@ function TableWidget(props: Props): JSX.Element
|
||||
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>
|
||||
|
@ -323,7 +323,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 elevation={5}>
|
||||
<Card>
|
||||
<Box p={3}>
|
||||
<MDTypography variant="h5" component="div">
|
||||
Working
|
||||
@ -1278,6 +1278,7 @@ 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;
|
||||
|
@ -2017,7 +2017,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
page={pageNumber}
|
||||
checkboxSelection
|
||||
disableSelectionOnClick
|
||||
autoHeight
|
||||
autoHeight={false}
|
||||
rows={rows}
|
||||
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
|
||||
columns={columnsModel}
|
||||
@ -2041,6 +2041,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
getRowId={(row) => row.__rowIndex}
|
||||
selectionModel={rowSelectionModel}
|
||||
hideFooterSelectedRowCount={true}
|
||||
sx={{border: 0, height: "calc(100vh - 250px)"}}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
|
@ -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={1}>{field?.label}</Typography>
|
||||
<Typography variant="h6" p={2} pl={3} pb={3}>{field?.label}</Typography>
|
||||
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
|
||||
{scriptId ?
|
||||
|
@ -564,3 +564,14 @@ 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+ */
|
||||
}
|
Reference in New Issue
Block a user