Compare commits

...

59 Commits

Author SHA1 Message Date
be393884cc CE-752 Final style updates for helpContent 2023-12-13 15:19:46 -06:00
fc5637b133 Update circleci/browser-tools version 2023-12-07 12:11:15 -06:00
8c7a7ae43e Update webdrivermanager version 2023-12-07 12:06:33 -06:00
ca83dbd83b make overflow w/ a max-height, since sticky. 2023-12-07 12:00:49 -06:00
68f652f3f3 Fixes for styles (spacing) in header record grid widget 2023-12-07 12:00:23 -06:00
adb2b4613d CE-752 Add help content concept to QQQ (fields and table sections at this time); redesign form fields (borders now) 2023-12-07 12:00:00 -06:00
c94f518422 Add padding under widgets if there are also app sections 2023-11-22 11:44:18 -06:00
d293db5136 Fixes to lineHeight (looks better when wraped (e.g., mobile)) 2023-11-22 11:43:52 -06:00
2ace79c08a Improve previous change here (overflow:hidden) to also do whiteSpace:wrap and fix lineHeight 2023-11-22 11:43:30 -06:00
c31db7ac32 CE-740 Remove margin above widget dropdowns (tighter when they stack) 2023-11-21 11:03:36 -06:00
dd887037c2 alt for user's avatar use proper user object 2023-11-21 11:03:08 -06:00
92516a2eb0 CE-740 Better table styles (scrolling, gutters, bg & border) 2023-11-21 11:02:55 -06:00
13a918441c CE-740 turn off trying to disable back & forth arrows 2023-11-21 08:59:57 -06:00
9e1c68b1fd Truncate long username rather than give horizontal scroll 2023-11-21 08:40:45 -06:00
d5c6985bc4 New style & functionality (null-label, default-from-data) for widget dropdowns 2023-11-21 08:37:43 -06:00
b8be374a01 Change tooltip to have value after colon, not in parens 2023-11-21 08:22:53 -06:00
2ef118a433 Fix small alignment of text 2023-11-21 08:22:34 -06:00
b1d685b5b1 CE-740 footer weight 600 vs body 500 2023-11-16 19:00:23 -06:00
87edebb79f CE-740 label weight 600 2023-11-16 18:29:27 -06:00
c722081ae7 CE-740 Fix body left margin 2023-11-16 18:15:21 -06:00
ca52466b79 CE-740 Reload button match export button color & size 2023-11-16 16:20:41 -06:00
dd45079ecd CE-740 export button standard color 2023-11-16 16:20:25 -06:00
aa7f9e93f1 CE-740 Border & tooltip for the current use-case 2023-11-16 16:20:11 -06:00
c899e5712b CE-740 Removing reduant padding 2023-11-16 12:19:47 -06:00
2a8bed1093 CE-740 Undo padding-left for non-card labels... i want it for labels on parents, but it made wrong inside-tabs... 2023-11-16 10:12:45 -06:00
627dd3c9f5 CE-740 more widget padding changes 2023-11-16 10:01:20 -06:00
40ac89dac3 CE-740 more SF Pro Display 2023-11-16 10:00:14 -06:00
e9223a1c23 Try to make test more robust (try-again on filename in case it catches the .crdownload file) 2023-11-16 09:59:59 -06:00
eeb121ff12 CE-740 more SF Pro Display 2023-11-16 09:55:47 -06:00
455869c96b CE-740 Actually ran test to get h3 & h5's right 2023-11-16 08:33:21 -06:00
90861b33a4 CE-740 Change h5 expects to h3 2023-11-16 08:16:15 -06:00
6be18627a7 CE-740 Update BREADCRUMB_HEADER selector (h5 became h3) 2023-11-15 20:11:14 -06:00
2255451745 CE-740 Update HeaderIcon to support icon from path (file) instead of name; padding adjustments (put 24/16 on the cards) 2023-11-15 19:59:58 -06:00
f9b29e932a CE-740 Add SF Pro Display as first in fontFamily 2023-11-15 19:58:55 -06:00
c86bfcff4d CE-740 Export button style 2023-11-15 19:58:35 -06:00
c8fe46c5bf CE-740 Remove margin/padding (in the card now) 2023-11-15 19:58:08 -06:00
8de2f2ce33 CE-740 Revert grid-item padding hack (borke a lot of things) 2023-11-15 19:58:01 -06:00
036c2253b1 CE-740 Remove margin/padding (in the card now) 2023-11-15 19:57:49 -06:00
6c6c1cfe3d CE-740 Border, padding adjustments 2023-11-15 19:57:39 -06:00
754010df3d CE-740 Revert grid-item padding hack (borke a lot of things) 2023-11-15 19:57:25 -06:00
9d7315e773 Add %'s to tooltips 2023-11-15 19:57:07 -06:00
12f13983ea CE-740 Remove margin/padding (in the card now) 2023-11-15 19:56:47 -06:00
ab0fb977fb CE-740 font weight 2023-11-15 19:56:26 -06:00
7cb3f2284d CE-740 line colors 2023-11-15 19:56:10 -06:00
6bdd8ed935 CE-740 font sizes, colors, and padding 2023-11-15 19:55:47 -06:00
4f0469a04c CE-740 line colors 2023-11-15 19:55:19 -06:00
f1c3b93049 CE-740 Widget and tab spacing adjustments; pass iconPath down into HeaderIcon 2023-11-15 19:54:59 -06:00
1c1cfc6d75 CE-740 Add blueGray and grayLines colors 2023-11-15 19:54:18 -06:00
87d0c7d478 CE-740 chart subhead style edits 2023-11-15 13:39:16 -06:00
fbcee2b819 CE-740 make all h3 same size; update styles on recently-viewed 2023-11-15 13:15:53 -06:00
c1065099e5 CE-740 Style on page header; dark.main color; sticky app header bar margin & width 2023-11-15 12:11:35 -06:00
adcfa86f73 CE-740 style on / 2023-11-15 11:37:25 -06:00
ad306728c2 CE-740 Update icon size 2023-11-15 11:34:18 -06:00
5969f1a6ba CE-740 Update breadcrumb size, weight, and color 2023-11-15 11:34:18 -06:00
6c3bfa776a CE-740 Adjusting grid padding to 20px; overall body padding to 20px, and margin-let to account for resized sidenav 2023-11-15 11:34:18 -06:00
924e657531 CE-740 Changed sidebar width from 275 to 245 2023-11-15 11:34:17 -06:00
b52d0977cb Make date sticky 2023-11-10 10:48:36 -06:00
ea728d3cf0 fix to selenium test by updating pom dependency 2023-11-08 17:09:22 -06:00
e5430101fa updates to put blob data back into data that is posted to backend on record edits 2023-11-08 08:57:42 -06:00
64 changed files with 1472 additions and 733 deletions

View File

@ -2,7 +2,7 @@ version: 2.1
orbs: orbs:
node: circleci/node@5.1.0 node: circleci/node@5.1.0
browser-tools: circleci/browser-tools@1.4.5 browser-tools: circleci/browser-tools@1.4.6
executors: executors:
java17: java17:

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.83", "@kingsrook/qqq-frontend-core": "1.0.85",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",
@ -42,6 +42,7 @@
"react-dom": "18.0.0", "react-dom": "18.0.0",
"react-github-btn": "1.2.1", "react-github-btn": "1.2.1",
"react-google-drive-picker": "^1.2.0", "react-google-drive-picker": "^1.2.0",
"react-markdown": "9.0.1",
"react-router-dom": "6.2.1", "react-router-dom": "6.2.1",
"react-router-hash-link": "2.4.3", "react-router-hash-link": "2.4.3",
"react-table": "7.7.0", "react-table": "7.7.0",

View File

@ -77,13 +77,13 @@
<dependency> <dependency>
<groupId>org.seleniumhq.selenium</groupId> <groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId> <artifactId>selenium-java</artifactId>
<version>4.10.0</version> <version>4.15.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.github.bonigarcia</groupId> <groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId> <artifactId>webdrivermanager</artifactId>
<version>5.4.1</version> <version>5.6.2</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -36,7 +36,7 @@ import {LicenseInfo} from "@mui/x-license-pro";
import jwt_decode from "jwt-decode"; import jwt_decode from "jwt-decode";
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react"; import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
import {useCookies} from "react-cookie"; import {useCookies} from "react-cookie";
import {Navigate, Route, Routes, useLocation,} from "react-router-dom"; import {Navigate, Route, Routes, useLocation, useSearchParams,} from "react-router-dom";
import {Md5} from "ts-md5/dist/md5"; import {Md5} from "ts-md5/dist/md5";
import CommandMenu from "CommandMenu"; import CommandMenu from "CommandMenu";
import QContext from "QContext"; import QContext from "QContext";
@ -226,6 +226,7 @@ export default function App()
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller; const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
const [onMouseEnter, setOnMouseEnter] = useState(false); const [onMouseEnter, setOnMouseEnter] = useState(false);
const {pathname} = useLocation(); const {pathname} = useLocation();
const [queryParams] = useSearchParams();
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true); const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
const [sideNavRoutes, setSideNavRoutes] = useState([]); const [sideNavRoutes, setSideNavRoutes] = useState([]);
@ -517,7 +518,7 @@ export default function App()
name: loggedInUser?.name ?? "Anonymous", name: loggedInUser?.name ?? "Anonymous",
key: "username", key: "username",
noCollapse: true, noCollapse: true,
icon: <Avatar src={profilePicture} alt="{user?.name}" />, icon: <Avatar src={profilePicture} alt="{loggedInUser?.name}" />,
}; };
setProfileRoutes(profileRoutes); setProfileRoutes(profileRoutes);
@ -659,6 +660,8 @@ export default function App()
const [tableProcesses, setTableProcesses] = useState(null); const [tableProcesses, setTableProcesses] = useState(null);
const [dotMenuOpen, setDotMenuOpen] = useState(false); const [dotMenuOpen, setDotMenuOpen] = useState(false);
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false); const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
return ( return (
appRoutes && ( appRoutes && (
@ -669,6 +672,7 @@ export default function App()
tableProcesses: tableProcesses, tableProcesses: tableProcesses,
dotMenuOpen: dotMenuOpen, dotMenuOpen: dotMenuOpen,
keyboardHelpOpen: keyboardHelpOpen, keyboardHelpOpen: keyboardHelpOpen,
helpHelpActive: helpHelpActive,
setPageHeader: (header: string | JSX.Element) => setPageHeader(header), setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
setAccentColor: (accentColor: string) => setAccentColor(accentColor), setAccentColor: (accentColor: string) => setAccentColor(accentColor),
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData), setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
@ -700,4 +704,4 @@ export default function App()
</QContext.Provider> </QContext.Provider>
) )
); );
} }

View File

@ -51,6 +51,7 @@ interface QContext
/////////////////////////////////// ///////////////////////////////////
pathToLabelMap?: {[path: string]: string}; pathToLabelMap?: {[path: string]: string};
branding?: QBrandingMetaData; branding?: QBrandingMetaData;
helpHelpActive?: boolean;
} }
const defaultState = { const defaultState = {
@ -59,6 +60,7 @@ const defaultState = {
dotMenuOpen: false, dotMenuOpen: false,
keyboardHelpOpen: false, keyboardHelpOpen: false,
pathToLabelMap: {}, pathToLabelMap: {},
helpHelpActive: false,
}; };
const QContext = createContext<QContext>(defaultState); const QContext = createContext<QContext>(defaultState);

View File

@ -149,7 +149,7 @@ interface Types {
} }
const baseProperties = { const baseProperties = {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
fontWeightLighter: 100, fontWeightLighter: 100,
fontWeightLight: 300, fontWeightLight: 300,
fontWeightRegular: 400, fontWeightRegular: 400,

View File

@ -78,6 +78,19 @@ interface Types
light: string; light: string;
main: string; main: string;
focus: string; focus: string;
}
blueGray:
| {
main: string;
}
gray:
| {
main: string;
focus: string;
}
grayLines:
| {
main: string;
} }
| any; | any;
primary: ColorsTypes | any; primary: ColorsTypes | any;
@ -174,6 +187,19 @@ const colors: Types = {
focus: "#ffffff", focus: "#ffffff",
}, },
blueGray: {
main: "#546E7A"
},
gray: {
main: "#757575",
focus: "#757575",
},
grayLines: {
main: "#D6D6D6"
},
black: { black: {
light: "#000000", light: "#000000",
main: "#000000", main: "#000000",
@ -216,7 +242,7 @@ const colors: Types = {
}, },
dark: { dark: {
main: "#344767", main: "#212121",
focus: "#2c3c58", focus: "#2c3c58",
}, },

View File

@ -149,7 +149,7 @@ interface Types {
} }
const baseProperties = { const baseProperties = {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
fontWeightLighter: 100, fontWeightLighter: 100,
fontWeightLight: 300, fontWeightLight: 300,
fontWeightRegular: 400, fontWeightRegular: 400,
@ -199,9 +199,10 @@ const typography: Types = {
}, },
h3: { h3: {
fontSize: pxToRem(30), fontSize: "1.75rem",
lineHeight: 1.375, lineHeight: 1.375,
...baseHeadingProperties, ...baseHeadingProperties,
fontWeight: 600
}, },
h4: { h4: {
@ -217,9 +218,10 @@ const typography: Types = {
}, },
h6: { h6: {
fontSize: pxToRem(16), fontSize: "1.125rem",
lineHeight: 1.625, lineHeight: 1.625,
...baseHeadingProperties, ...baseHeadingProperties,
fontWeight: 500
}, },
subtitle1: { subtitle1: {

View File

@ -42,7 +42,7 @@ const card: Types = {
wordWrap: "break-word", wordWrap: "break-word",
backgroundColor: white.main, backgroundColor: white.main,
backgroundClip: "border-box", backgroundClip: "border-box",
border: `${borderWidth[1]} solid ${rgba(black.main, 0.25)}`, border: `${borderWidth[1]} solid ${colors.grayLines.main}`,
borderRadius: borderRadius.xl, borderRadius: borderRadius.xl,
overflow: "visible", overflow: "visible",
}, },

View File

@ -33,7 +33,10 @@ const tabs: Types = {
borderBottomColor: grey[400], borderBottomColor: grey[400],
minHeight: "unset", minHeight: "unset",
padding: "0", padding: "0",
margin: "0" margin: "0",
"& button": {
fontWeight: 500
}
}, },
scroller: { scroller: {

View File

@ -48,7 +48,7 @@ const tooltip: Types = {
borderRadius: borderRadius.md, borderRadius: borderRadius.md,
opacity: 0.7, opacity: 0.7,
padding: "1rem", 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" boxShadow: "0px 0px 12px rgba(128, 128, 128, 0.40)"
}, },
arrow: { arrow: {

View File

@ -402,14 +402,16 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
return ( return (
<Box key={audit0.values.get("id")} className="auditGroupBlock"> <Box key={audit0.values.get("id")} className="auditGroupBlock">
<Box display="flex" flexDirection="row" justifyContent="center" fontSize={14}> <Box position="sticky" top="0" zIndex={3}>
<Box borderTop={1} mt={1.25} mr={1} width="100%" borderColor="#B0B0B0" /> <Box display="flex" flexDirection="row" justifyContent="center" fontSize={14} position={"relative"} top={"-1px"} pb={"6px"} sx={{backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 80%, rgba(255,255,255,0))"}}>
<Box whiteSpace="nowrap"> <Box borderTop={1} mt={1.25} mr={1} width="100%" borderColor="#B0B0B0" />
{ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]} <Box whiteSpace="nowrap">
{timestampParts[0] == todayFormatted ? " (Today)" : ""} {ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]}
{timestampParts[0] == yesterdayFormatted ? " (Yesterday)" : ""} {timestampParts[0] == todayFormatted ? " (Today)" : ""}
{timestampParts[0] == yesterdayFormatted ? " (Yesterday)" : ""}
</Box>
<Box borderTop={1} mt={1.25} ml={1} width="100%" borderColor="#B0B0B0" />
</Box> </Box>
<Box borderTop={1} mt={1.25} ml={1} width="100%" borderColor="#B0B0B0" />
</Box> </Box>
{ {

View File

@ -29,8 +29,8 @@ import React, {SyntheticEvent} from "react";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
const AntSwitch = styled(Switch)(({theme}) => ({ const AntSwitch = styled(Switch)(({theme}) => ({
width: 28, width: 32,
height: 16, height: 20,
padding: 0, padding: 0,
display: "flex", display: "flex",
"&:active": { "&:active": {
@ -54,18 +54,19 @@ const AntSwitch = styled(Switch)(({theme}) => ({
}, },
"& .MuiSwitch-thumb": { "& .MuiSwitch-thumb": {
boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)", boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)",
width: 12, width: 16,
height: 12, height: 16,
borderRadius: 6, borderRadius: 8,
transition: theme.transitions.create([ "width" ], { transition: theme.transitions.create([ "width" ], {
duration: 200, duration: 200,
}), }),
}, },
"&.nullSwitch .MuiSwitch-thumb": { "&.nullSwitch .MuiSwitch-thumb": {
width: 24, width: 28,
}, },
"& .MuiSwitch-track": { "& .MuiSwitch-track": {
borderRadius: 16 / 2, height: 20,
borderRadius: 20 / 2,
opacity: 1, opacity: 1,
backgroundColor: backgroundColor:
theme.palette.mode === "dark" ? "rgba(255,255,255,.35)" : "rgba(0,0,0,.25)", theme.palette.mode === "dark" ? "rgba(255,255,255,.35)" : "rgba(0,0,0,.25)",
@ -106,9 +107,9 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
return ( return (
<Box bgcolor={isDisabled ? colors.grey[200] : ""}> <Box bgcolor={isDisabled ? colors.grey[200] : ""}>
<InputLabel shrink={true}>{label}</InputLabel> <InputLabel shrink={true}>{label}</InputLabel>
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center" height="37px">
<Typography <Typography
fontSize="0.875rem" fontSize="1rem"
color={value === false ? "auto" : "#bfbfbf" } color={value === false ? "auto" : "#bfbfbf" }
onClick={(e) => setSwitch(e, false)} onClick={(e) => setSwitch(e, false)}
sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}> sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}>
@ -116,7 +117,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
</Typography> </Typography>
<AntSwitch className={classNullSwitch} name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} /> <AntSwitch className={classNullSwitch} name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
<Typography <Typography
fontSize="0.875rem" fontSize="1rem"
color={value === true ? "auto" : "#bfbfbf"} color={value === true ? "auto" : "#bfbfbf"}
onClick={(e) => setSwitch(e, true)} onClick={(e) => setSwitch(e, true)}
sx={{cursor: value === true || isDisabled ? "inherit" : "pointer"}}> sx={{cursor: value === true || isDisabled ? "inherit" : "pointer"}}>

View File

@ -32,6 +32,7 @@ import React, {useState} from "react";
import QDynamicFormField from "qqq/components/forms/DynamicFormField"; import QDynamicFormField from "qqq/components/forms/DynamicFormField";
import DynamicSelect from "qqq/components/forms/DynamicSelect"; import DynamicSelect from "qqq/components/forms/DynamicSelect";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
interface Props interface Props
@ -41,16 +42,13 @@ interface Props
bulkEditMode?: boolean; bulkEditMode?: boolean;
bulkEditSwitchChangeHandler?: any; bulkEditSwitchChangeHandler?: any;
record?: QRecord; record?: QRecord;
helpRoles?: string[];
helpContentKeyPrefix?: string;
} }
function QDynamicForm(props: Props): JSX.Element function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler, record, helpRoles, helpContentKeyPrefix}: Props): JSX.Element
{ {
const { const {formFields, values, errors, touched} = formData;
formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler,
} = props;
const {
formFields, values, errors, touched,
} = formData;
const formikProps = useFormikContext(); const formikProps = useFormikContext();
const [fileName, setFileName] = useState(null as string); const [fileName, setFileName] = useState(null as string);
@ -70,8 +68,8 @@ function QDynamicForm(props: Props): JSX.Element
{ {
setFileName(null); setFileName(null);
formikProps.setFieldValue(fieldName, null); formikProps.setFieldValue(fieldName, null);
props.record?.values.delete(fieldName) record?.values.delete(fieldName)
props.record?.displayValues.delete(fieldName) record?.displayValues.delete(fieldName)
}; };
const bulkEditSwitchChanged = (name: string, value: boolean) => const bulkEditSwitchChanged = (name: string, value: boolean) =>
@ -79,6 +77,7 @@ function QDynamicForm(props: Props): JSX.Element
bulkEditSwitchChangeHandler(name, value); bulkEditSwitchChangeHandler(name, value);
}; };
return ( return (
<Box> <Box>
<Box lineHeight={0}> <Box lineHeight={0}>
@ -96,29 +95,38 @@ function QDynamicForm(props: Props): JSX.Element
&& Object.keys(formFields).map((fieldName: any) => && Object.keys(formFields).map((fieldName: any) =>
{ {
const field = formFields[fieldName]; const field = formFields[fieldName];
if (field.omitFromQDynamicForm)
{
return null;
}
if (values[fieldName] === undefined) if (values[fieldName] === undefined)
{ {
values[fieldName] = ""; values[fieldName] = "";
} }
if (field.omitFromQDynamicForm) let formattedHelpContent = <HelpContent helpContents={field.fieldMetaData.helpContents} roles={helpRoles} helpContentKey={`${helpContentKeyPrefix ?? ""}field:${fieldName}`} />;
if(formattedHelpContent)
{ {
return null; formattedHelpContent = <Box color="#757575" fontSize="0.875rem" mt="-0.25rem">{formattedHelpContent}</Box>
} }
const labelElement = <Box fontSize="1rem" fontWeight="500" marginBottom="0.25rem">
<label htmlFor={field.name}>{field.label}</label>
</Box>
if (field.type === "file") if (field.type === "file")
{ {
const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB}); const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB});
return ( return (
<Grid item xs={12} sm={6} key={fieldName}> <Grid item xs={12} sm={6} key={fieldName}>
<Box mb={1.5}> <Box mb={1.5}>
{labelElement}
<InputLabel shrink={true}>{field.label}</InputLabel>
{ {
props.record && props.record.values.get(fieldName) && <Box fontSize="0.875rem" pb={1}> record && record.values.get(fieldName) && <Box fontSize="0.875rem" pb={1}>
Current File: Current File:
<Box display="inline-flex" pl={1}> <Box display="inline-flex" pl={1}>
{ValueUtils.getDisplayValue(pseudoField, props.record, "view")} {ValueUtils.getDisplayValue(pseudoField, record, "view")}
<Tooltip placement="bottom" title="Remove current file"> <Tooltip placement="bottom" title="Remove current file">
<Icon className="blobIcon" fontSize="small" onClick={(e) => removeFile(fieldName)}>delete</Icon> <Icon className="blobIcon" fontSize="small" onClick={(e) => removeFile(fieldName)}>delete</Icon>
</Tooltip> </Tooltip>
@ -162,18 +170,20 @@ function QDynamicForm(props: Props): JSX.Element
return ( return (
<Grid item xs={12} sm={6} key={fieldName}> <Grid item xs={12} sm={6} key={fieldName}>
{labelElement}
<DynamicSelect <DynamicSelect
tableName={field.possibleValueProps.tableName} tableName={field.possibleValueProps.tableName}
processName={field.possibleValueProps.processName} processName={field.possibleValueProps.processName}
fieldName={fieldName} fieldName={fieldName}
isEditable={field.isEditable} isEditable={field.isEditable}
fieldLabel={field.label} fieldLabel=""
initialValue={values[fieldName]} initialValue={values[fieldName]}
initialDisplayValue={field.possibleValueProps.initialDisplayValue} initialDisplayValue={field.possibleValueProps.initialDisplayValue}
bulkEditMode={bulkEditMode} bulkEditMode={bulkEditMode}
bulkEditSwitchChangeHandler={bulkEditSwitchChanged} bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
otherValues={otherValuesMap} otherValues={otherValuesMap}
/> />
{formattedHelpContent}
</Grid> </Grid>
); );
} }
@ -182,9 +192,11 @@ function QDynamicForm(props: Props): JSX.Element
// todo? placeholder={password.placeholder} // todo? placeholder={password.placeholder}
return ( return (
<Grid item xs={12} sm={6} key={fieldName}> <Grid item xs={12} sm={6} key={fieldName}>
{labelElement}
<QDynamicFormField <QDynamicFormField
id={field.name}
type={field.type} type={field.type}
label={field.label} label=""
isEditable={field.isEditable} isEditable={field.isEditable}
name={fieldName} name={fieldName}
displayFormat={field.displayFormat} displayFormat={field.displayFormat}
@ -195,6 +207,7 @@ function QDynamicForm(props: Props): JSX.Element
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]} success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
formFieldObject={field} formFieldObject={field}
/> />
{formattedHelpContent}
</Grid> </Grid>
); );
})} })}
@ -207,6 +220,7 @@ function QDynamicForm(props: Props): JSX.Element
QDynamicForm.defaultProps = { QDynamicForm.defaultProps = {
formLabel: undefined, formLabel: undefined,
bulkEditMode: false, bulkEditMode: false,
helpRoles: ["ALL_SCREENS"],
bulkEditSwitchChangeHandler: () => bulkEditSwitchChangeHandler: () =>
{ {
}, },

View File

@ -25,6 +25,7 @@ import Switch from "@mui/material/Switch";
import {ErrorMessage, Field, useFormikContext} from "formik"; import {ErrorMessage, Field, useFormikContext} from "formik";
import React, {useState} from "react"; import React, {useState} from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import colors from "qqq/assets/theme/base/colors";
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch"; import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
import MDInput from "qqq/components/legacy/MDInput"; import MDInput from "qqq/components/legacy/MDInput";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
@ -52,6 +53,7 @@ function QDynamicFormField({
{ {
const [switchChecked, setSwitchChecked] = useState(false); const [switchChecked, setSwitchChecked] = useState(false);
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode); const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
const {inputBorderColor} = colors;
const {setFieldValue} = useFormikContext(); const {setFieldValue} = useFormikContext();
@ -122,7 +124,7 @@ function QDynamicFormField({
width="100%" width="100%"
height="300px" height="300px"
value={value} value={value}
style={{border: "1px solid gray"}} style={{border: `1px solid ${inputBorderColor}`, borderRadius: "0.75rem"}}
/> />
</> </>
); );
@ -131,7 +133,7 @@ function QDynamicFormField({
{ {
field = ( field = (
<> <>
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled} <Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
onKeyPress={(e: any) => onKeyPress={(e: any) =>
{ {
if (e.key === "Enter") if (e.key === "Enter")
@ -171,6 +173,14 @@ function QDynamicFormField({
id={`bulkEditSwitch-${name}`} id={`bulkEditSwitch-${name}`}
checked={switchChecked} checked={switchChecked}
onClick={bulkEditSwitchChanged} onClick={bulkEditSwitchChanged}
sx={{top: "-4px",
"& .MuiSwitch-track": {
height: 20,
borderRadius: 10,
top: -3,
position: "relative"
}
}}
/> />
</Box> </Box>
<Box width="100%" sx={{background: (type == "checkbox" && isDisabled) ? "#f0f2f5!important" : "initial"}}> <Box width="100%" sx={{background: (type == "checkbox" && isDisabled) ? "#f0f2f5!important" : "initial"}}>

View File

@ -89,6 +89,7 @@ class DynamicFormUtils
label += field.isRequired ? " *" : ""; label += field.isRequired ? " *" : "";
return ({ return ({
fieldMetaData: field,
name: field.name, name: field.name,
label: label, label: label,
isRequired: field.isRequired, isRequired: field.isRequired,

View File

@ -29,6 +29,7 @@ import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {ErrorMessage, useFormikContext} from "formik"; import {ErrorMessage, useFormikContext} from "formik";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
@ -49,6 +50,7 @@ interface Props
bulkEditMode?: boolean; bulkEditMode?: boolean;
bulkEditSwitchChangeHandler?: any; bulkEditSwitchChangeHandler?: any;
otherValues?: Map<string, any>; otherValues?: Map<string, any>;
variant: "standard" | "outlined";
} }
DynamicSelect.defaultProps = { DynamicSelect.defaultProps = {
@ -63,6 +65,7 @@ DynamicSelect.defaultProps = {
isMultiple: false, isMultiple: false,
bulkEditMode: false, bulkEditMode: false,
otherValues: new Map<string, any>(), otherValues: new Map<string, any>(),
variant: "outlined",
bulkEditSwitchChangeHandler: () => bulkEditSwitchChangeHandler: () =>
{ {
}, },
@ -70,12 +73,13 @@ DynamicSelect.defaultProps = {
const qController = Client.getInstance(); const qController = Client.getInstance();
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props) function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant}: Props)
{ {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [options, setOptions] = useState<readonly QPossibleValue[]>([]); const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
const [searchTerm, setSearchTerm] = useState(null); const [searchTerm, setSearchTerm] = useState(null);
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const {inputBorderColor} = colors;
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - // // default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
@ -230,7 +234,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
// attributes. so, doing this, w/ key=id, seemed to fix it. // // attributes. so, doing this, w/ key=id, seemed to fix it. //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
return ( return (
<li {...props} key={option.id}> <li {...props} key={option.id} style={{fontSize: "1rem"}}>
{content} {content}
</li> </li>
); );
@ -244,13 +248,35 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
bulkEditSwitchChangeHandler(fieldName, newSwitchValue); bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
}; };
// console.log(`default value: ${JSON.stringify(defaultValue)}`); ////////////////////////////////////////////
// for outlined style, adjust some styles //
////////////////////////////////////////////
let autocompleteSX = {};
if (variant == "outlined")
{
autocompleteSX = {
"& .MuiOutlinedInput-root": {
borderRadius: "0.75rem",
},
"& .MuiInputBase-root": {
padding: "0.5rem",
background: isDisabled ? "#f0f2f5!important" : "initial",
},
"& .MuiOutlinedInput-root .MuiAutocomplete-input": {
padding: "0",
fontSize: "1rem"
},
"& .Mui-disabled .MuiOutlinedInput-notchedOutline": {
borderColor: inputBorderColor
}
}
}
const autocomplete = ( const autocomplete = (
<Box> <Box>
<Autocomplete <Autocomplete
id={overrideId ?? fieldName} id={overrideId ?? fieldName}
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}} sx={autocompleteSX}
open={open} open={open}
fullWidth fullWidth
onOpen={() => onOpen={() =>
@ -305,7 +331,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
<TextField <TextField
{...params} {...params}
label={fieldLabel} label={fieldLabel}
variant="standard" variant={variant}
autoComplete="off" autoComplete="off"
type="search" type="search"
InputProps={{ InputProps={{
@ -341,6 +367,14 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
id={`bulkEditSwitch-${fieldName}`} id={`bulkEditSwitch-${fieldName}`}
checked={switchChecked} checked={switchChecked}
onClick={bulkEditSwitchChanged} onClick={bulkEditSwitchChanged}
sx={{top: "-4px",
"& .MuiSwitch-track": {
height: 20,
borderRadius: 10,
top: -3,
position: "relative"
}
}}
/> />
</Box> </Box>
<Box width="100%"> <Box width="100%">

View File

@ -37,10 +37,12 @@ import React, {useContext, useEffect, useReducer, useState} from "react";
import {useLocation, useNavigate, useParams} from "react-router-dom"; import {useLocation, useNavigate, useParams} from "react-router-dom";
import * as Yup from "yup"; import * as Yup from "yup";
import QContext from "QContext"; import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import QDynamicForm from "qqq/components/forms/DynamicForm"; import QDynamicForm from "qqq/components/forms/DynamicForm";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import HtmlUtils from "qqq/utils/HtmlUtils"; import HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
@ -79,6 +81,7 @@ function EntityForm(props: Props): JSX.Element
const [validations, setValidations] = useState({}); const [validations, setValidations] = useState({});
const [initialValues, setInitialValues] = useState({} as { [key: string]: any }); const [initialValues, setInitialValues] = useState({} as { [key: string]: any });
const [formFields, setFormFields] = useState(null as Map<string, any>); const [formFields, setFormFields] = useState(null as Map<string, any>);
const [t1section, setT1Section] = useState(null as QTableSection);
const [t1sectionName, setT1SectionName] = useState(null as string); const [t1sectionName, setT1SectionName] = useState(null as string);
const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]); const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]);
@ -151,7 +154,9 @@ function EntityForm(props: Props): JSX.Element
{ {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
return <QDynamicForm formData={formData} record={record} />;
const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
return <QDynamicForm formData={formData} record={record} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableName};`} />;
} }
if (!asyncLoadInited) if (!asyncLoadInited)
@ -330,6 +335,7 @@ function EntityForm(props: Props): JSX.Element
///////////////////////////////////// /////////////////////////////////////
const dynamicFormFieldsBySection = new Map<string, any>(); const dynamicFormFieldsBySection = new Map<string, any>();
let t1sectionName; let t1sectionName;
let t1section;
const nonT1Sections: QTableSection[] = []; const nonT1Sections: QTableSection[] = [];
for (let i = 0; i < tableSections.length; i++) for (let i = 0; i < tableSections.length; i++)
{ {
@ -382,6 +388,7 @@ function EntityForm(props: Props): JSX.Element
if (section.tier === "T1") if (section.tier === "T1")
{ {
t1sectionName = section.name; t1sectionName = section.name;
t1section = section;
} }
else else
{ {
@ -389,6 +396,7 @@ function EntityForm(props: Props): JSX.Element
} }
} }
setT1SectionName(t1sectionName); setT1SectionName(t1sectionName);
setT1Section(t1section);
setNonT1Sections(nonT1Sections); setNonT1Sections(nonT1Sections);
setFormFields(dynamicFormFieldsBySection); setFormFields(dynamicFormFieldsBySection);
setValidations(Yup.object().shape(formValidations)); setValidations(Yup.object().shape(formValidations));
@ -471,6 +479,10 @@ function EntityForm(props: Props): JSX.Element
console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`); console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`);
delete(valuesToPost[fieldName]); delete(valuesToPost[fieldName]);
} }
else
{
valuesToPost[fieldName] = values[fieldName];
}
} }
} }
@ -548,6 +560,19 @@ function EntityForm(props: Props): JSX.Element
const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`; const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
let body; let body;
const getSectionHelp = (section: QTableSection) =>
{
const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
const formattedHelpContent = <HelpContent helpContents={section.helpContents} roles={helpRoles} helpContentKey={`table:${tableMetaData.name};section:${section.name}`} />;
return formattedHelpContent && (
<Box px={"1.5rem"} fontSize={"0.875rem"}>
{formattedHelpContent}
</Box>
)
}
if (notAllowedError) if (notAllowedError)
{ {
body = ( body = (
@ -569,23 +594,26 @@ function EntityForm(props: Props): JSX.Element
} }
else else
{ {
const cardElevation = props.isModal ? 3 : 1; const cardElevation = props.isModal ? 3 : 0;
body = ( body = (
<Box mb={3}> <Box mb={3}>
<Grid container spacing={3}> {
<Grid item xs={12}> (alertContent || warningContent) &&
{alertContent ? ( <Grid container spacing={3}>
<Box mb={3}> <Grid item xs={12}>
<Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert> {alertContent ? (
</Box> <Box mb={3}>
) : ("")} <Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert>
{warningContent ? ( </Box>
<Box mb={3}> ) : ("")}
<Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert> {warningContent ? (
</Box> <Box mb={3}>
) : ("")} <Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert>
</Box>
) : ("")}
</Grid>
</Grid> </Grid>
</Grid> }
<Grid container spacing={3}> <Grid container spacing={3}>
{ {
!props.isModal && !props.isModal &&
@ -623,10 +651,11 @@ function EntityForm(props: Props): JSX.Element
<MDTypography variant="h5">{formTitle}</MDTypography> <MDTypography variant="h5">{formTitle}</MDTypography>
</Box> </Box>
</Box> </Box>
{t1section && getSectionHelp(t1section)}
{ {
t1sectionName && formFields ? ( t1sectionName && formFields ? (
<Box pb={1} px={3}> <Box px={3}>
<Box p={3} width="100%"> <Box pb={"0.25rem"} width="100%">
{getFormSection(values, touched, formFields.get(t1sectionName), errors)} {getFormSection(values, touched, formFields.get(t1sectionName), errors)}
</Box> </Box>
</Box> </Box>
@ -640,8 +669,9 @@ function EntityForm(props: Props): JSX.Element
<MDTypography variant="h6" p={3} pb={1}> <MDTypography variant="h6" p={3} pb={1}>
{section.label} {section.label}
</MDTypography> </MDTypography>
{getSectionHelp(section)}
<Box pb={1} px={3}> <Box pb={1} px={3}>
<Box p={3} width="100%"> <Box pb={"0.75rem"} width="100%">
{getFormSection(values, touched, formFields.get(section.name), errors)} {getFormSection(values, touched, formFields.get(section.name), errors)}
</Box> </Box>
</Box> </Box>

View File

@ -25,6 +25,7 @@ import Icon from "@mui/material/Icon";
import {ReactNode, useContext} from "react"; import {ReactNode, useContext} from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
interface Props interface Props
@ -112,43 +113,35 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
<Box mr={{xs: 0, xl: 8}}> <Box mr={{xs: 0, xl: 8}}>
<MuiBreadcrumbs <MuiBreadcrumbs
sx={{ sx={{
fontSize: "1.125rem",
fontWeight: "500",
color: colors.dark.main,
"& li": {
lineHeight: "unset!important"
},
"& a": {
color: colors.gray.main
},
"& .MuiBreadcrumbs-separator": { "& .MuiBreadcrumbs-separator": {
color: ({palette: {white, grey}}) => (light ? white.main : grey[600]), fontSize: "1.125rem",
fontWeight: "500",
color: colors.dark.main
}, },
}} }}
> >
<Link to="/"> <Link to="/">
<MDTypography <Icon sx={{fontSize: "1.25rem!important", position: "relative", top: "0.25rem"}}>{icon}</Icon>
component="span"
variant="body2"
color={light ? "white" : "dark"}
opacity={light ? 0.8 : 0.5}
sx={{lineHeight: 0}}
>
<Icon>{icon}</Icon>
</MDTypography>
</Link> </Link>
{fullRoutes.map((fullRoute: string) => ( {fullRoutes.map((fullRoute: string) => (
<Link to={fullRoute} key={fullRoute}> <Link to={fullRoute} key={fullRoute}>
<MDTypography {fullPathToLabel(fullRoute, fullRoute.replace(/.*\//, ""))}
component="span"
variant="button"
fontWeight="regular"
textTransform="capitalize"
color={light ? "white" : "dark"}
opacity={light ? 0.8 : 0.5}
sx={{lineHeight: 0}}
>
{fullPathToLabel(fullRoute, fullRoute.replace(/.*\//, ""))}
</MDTypography>
</Link> </Link>
))} ))}
</MuiBreadcrumbs> </MuiBreadcrumbs>
<MDTypography <MDTypography
pt={1} pt={1}
fontWeight="bold"
textTransform="capitalize" textTransform="capitalize"
variant="h5" variant="h3"
color={light ? "white" : "dark"} color={light ? "white" : "dark"}
noWrap noWrap
> >

View File

@ -159,7 +159,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
options={history} options={history}
autoHighlight autoHighlight
blurOnSelect blurOnSelect
style={{width: "200px"}} style={{width: "16rem"}}
onOpen={handleHistoryOnOpen} onOpen={handleHistoryOnOpen}
onChange={handleAutocompleteOnChange} onChange={handleAutocompleteOnChange}
PopperComponent={CustomPopper} PopperComponent={CustomPopper}

View File

@ -20,6 +20,7 @@
*/ */
import {Theme} from "@mui/material/styles"; import {Theme} from "@mui/material/styles";
import colors from "qqq/assets/theme/base/colors";
function navbar(theme: Theme | any, ownerState: any) function navbar(theme: Theme | any, ownerState: any)
{ {
@ -151,6 +152,16 @@ const navbarDesktopMenu = ({breakpoints}: Theme) => ({
}); });
const recentlyViewedMenu = ({breakpoints}: Theme) => ({ const recentlyViewedMenu = ({breakpoints}: Theme) => ({
"& .MuiInputLabel-root": {
color: colors.gray.main,
fontWeight: "500",
fontSize: "1rem"
},
"& .MuiInputAdornment-root": {
marginTop: "0.5rem",
color: colors.gray.main,
fontSize: "1rem"
},
"& .MuiOutlinedInput-root": { "& .MuiOutlinedInput-root": {
borderRadius: "0", borderRadius: "0",
padding: "0" padding: "0"

View File

@ -27,7 +27,7 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
const {palette, boxShadows, transitions, breakpoints, functions} = theme; const {palette, boxShadows, transitions, breakpoints, functions} = theme;
const {transparentSidenav, whiteSidenav, miniSidenav, darkMode} = ownerState; const {transparentSidenav, whiteSidenav, miniSidenav, darkMode} = ownerState;
const sidebarWidth = 275; const sidebarWidth = 245;
const {transparent, gradients, white, background} = palette; const {transparent, gradients, white, background} = palette;
const {xxl} = boxShadows; const {xxl} = boxShadows;
const {pxToRem, linearGradient} = functions; const {pxToRem, linearGradient} = functions;

View File

@ -64,7 +64,8 @@ function collapseItem(theme: Theme, ownerState: any)
borderRadius: borderRadius.md, borderRadius: borderRadius.md,
cursor: "pointer", cursor: "pointer",
userSelect: "none", userSelect: "none",
whiteSpace: "nowrap", whiteSpace: "wrap",
overflow: "hidden",
boxShadow: active && !whiteSidenav && !darkMode && !transparentSidenav ? md : "none", boxShadow: active && !whiteSidenav && !darkMode && !transparentSidenav ? md : "none",
[breakpoints.up("xl")]: { [breakpoints.up("xl")]: {
transition: transitions.create(["box-shadow", "background-color"], { transition: transitions.create(["box-shadow", "background-color"], {
@ -73,6 +74,10 @@ function collapseItem(theme: Theme, ownerState: any)
}), }),
}, },
"& .MuiListItemText-primary": {
lineHeight: "revert"
},
"&:hover, &:focus": { "&:hover, &:focus": {
backgroundColor: backgroundColor:
transparentSidenav && !darkMode transparentSidenav && !darkMode

View File

@ -69,7 +69,15 @@ export default styled(TextField)(({theme, ownerState}: { theme?: Theme; ownerSta
}); });
return { return {
backgroundColor: disabled ? `${grey[200]} !important` : transparent.main, "& .MuiInputBase-root": {
backgroundColor: disabled ? `${grey[200]} !important` : transparent.main,
borderRadius: "0.75rem",
},
"& input": {
backgroundColor: `${transparent.main}!important`,
padding: "0.5rem",
fontSize: "1rem",
},
pointerEvents: disabled ? "none" : "auto", pointerEvents: disabled ? "none" : "auto",
...(error && errorStyles()), ...(error && errorStyles()),
...(success && successStyles()), ...(success && successStyles()),

View File

@ -149,7 +149,7 @@ interface Types {
} }
const baseProperties = { const baseProperties = {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
fontWeightLighter: 100, fontWeightLighter: 100,
fontWeightLight: 300, fontWeightLight: 300,
fontWeightRegular: 400, fontWeightRegular: 400,

View File

@ -153,7 +153,7 @@ interface Types
} }
const baseProperties = { const baseProperties = {
fontFamily: "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", fontFamily: "\"SF Pro Display\", \"Roboto\", \"Helvetica\", \"Arial\", sans-serif",
fontWeightLighter: 100, fontWeightLighter: 100,
fontWeightLight: 300, fontWeightLight: 300,
fontWeightRegular: 400, fontWeightRegular: 400,

View File

@ -0,0 +1,139 @@
/*
* 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/>.
*/
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
import Box from "@mui/material/Box";
import parse from "html-react-parser";
import React, {useContext} from "react";
import Markdown from "react-markdown";
import QContext from "QContext";
interface Props
{
helpContents: QHelpContent[];
roles: string[];
heading?: string;
helpContentKey?: string;
}
HelpContent.defaultProps = {};
/*******************************************************************************
** format some content - meaning, change it from string to JSX element(s) or string.
** does a parse() for HTML, and a <Markdown> for markdown, else just text.
*******************************************************************************/
const formatHelpContent = (content: string, format: string): string | JSX.Element | JSX.Element[] =>
{
if (format == "HTML")
{
return parse(content);
}
else if (format == "MARKDOWN")
{
return (<Markdown>{content}</Markdown>)
}
return content;
}
/*******************************************************************************
** return the first help content from the list that matches the first role
** in the roles list.
*******************************************************************************/
const getMatchingHelpContent = (helpContents: QHelpContent[], roles: string[]): QHelpContent =>
{
if (helpContents)
{
if (helpContents.length == 1 && helpContents[0].roles.size == 0)
{
//////////////////////////////////////////////////////////////////////////////////////////////////
// if there's only 1 entry, and it has no roles, then assume user wanted it globally and use it //
//////////////////////////////////////////////////////////////////////////////////////////////////
return (helpContents[0]);
}
else
{
for (let i = 0; i < roles.length; i++)
{
for (let j = 0; j < helpContents.length; j++)
{
if (helpContents[j].roles.has(roles[i]))
{
return(helpContents[j])
}
}
}
}
}
return (null);
}
/*******************************************************************************
** test if a list of help contents would find any matches from a list of roles.
*******************************************************************************/
export const hasHelpContent = (helpContents: QHelpContent[], roles: string[]) =>
{
return getMatchingHelpContent(helpContents, roles) != null;
}
/*******************************************************************************
** component that renders a box of formatted help content, from a list of
** helpContents (from meta-data), and for a list of roles (based on what screen
*******************************************************************************/
function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX.Element
{
const {helpHelpActive} = useContext(QContext);
let selectedHelpContent = getMatchingHelpContent(helpContents, roles);
let content = null;
if (helpHelpActive)
{
if (!selectedHelpContent)
{
selectedHelpContent = new QHelpContent({content: ""});
}
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
}
else if(selectedHelpContent)
{
content = selectedHelpContent.content;
}
///////////////////////////////////////////////////
// if content was found, format it and return it //
///////////////////////////////////////////////////
if (content)
{
return <Box display="inline" className="helpContent">
{heading && <span className="header">{heading}</span>}
{formatHelpContent(content, selectedHelpContent.format)}
</Box>;
}
return (null);
}
export default HelpContent;

View File

@ -76,7 +76,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
return ( return (
<Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: stickyTop}}> <Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "auto", maxHeight: "calc(100vh - 200px)"}}>
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}> <Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
{ {
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => ( sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (

View File

@ -215,6 +215,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
initialDisplayValue={selectedPossibleValue?.label} initialDisplayValue={selectedPossibleValue?.label}
inForm={false} inForm={false}
onChange={(value: any) => valueChangeHandler(null, 0, value)} onChange={(value: any) => valueChangeHandler(null, 0, value)}
variant="standard"
/> />
</Box>; </Box>;
case ValueMode.PVS_MULTI: case ValueMode.PVS_MULTI:
@ -242,6 +243,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
initialValues={initialValues} initialValues={initialValues}
inForm={false} inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)} onChange={(value: any) => valueChangeHandler(null, "all", value)}
variant="standard"
/> />
</Box>; </Box>;
} }

View File

@ -254,7 +254,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard"); const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard");
if (topRightInsideCardIcon) if (topRightInsideCardIcon)
{ {
labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.color)); labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.path, topRightInsideCardIcon.color));
} }
} }
@ -343,7 +343,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
reloadWidgetCallback={(data) => reloadWidget(i, data)} reloadWidgetCallback={(data) => reloadWidget(i, data)}
widgetData={widgetData[i]} widgetData={widgetData[i]}
> >
<Box px={3} pt={0} pb={2}> <Box>
<MDTypography component="div" variant="button" color="text" fontWeight="light"> <MDTypography component="div" variant="button" color="text" fontWeight="light">
{ {
widgetData && widgetData[i] && widgetData[i].html ? ( widgetData && widgetData[i] && widgetData[i].html ? (
@ -497,6 +497,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
); );
}; };
if(wrapWidgetsInTabPanels)
{
omitWrappingGridContainer = true;
}
const body: JSX.Element = const body: JSX.Element =
( (
<> <>
@ -515,7 +520,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
if (wrapWidgetsInTabPanels) if (wrapWidgetsInTabPanels)
{ {
renderedWidget = (<TabPanel index={i} value={selectedTab} style={{padding: "1rem 0 0 1.5rem", width: "100%", marginBottom: "-1.5rem"}}> renderedWidget = (<TabPanel index={i} value={selectedTab} style={{
padding: 0,
margin: "-1rem",
marginBottom: "-3.5rem",
width: "calc(100% + 2rem)"
}}>
{renderedWidget} {renderedWidget}
</TabPanel>); </TabPanel>);
} }
@ -528,7 +538,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
const tabs = widgetMetaDataList && wrapWidgetsInTabPanels ? const tabs = widgetMetaDataList && wrapWidgetsInTabPanels ?
<Tabs <Tabs
sx={{m: 0, mb: 1.5}} sx={{m: 0, mb: 1.5, ml: -2, mr: -2, mt: -3,
"& .MuiTabs-scroller": {
ml: 0
}
}}
value={selectedTab} value={selectedTab}
onChange={(event, newValue) => changeTab(newValue)} onChange={(event, newValue) => changeTab(newValue)}
variant="standard" variant="standard"
@ -545,7 +559,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
{tabs} {tabs}
{ {
omitWrappingGridContainer ? body : ( omitWrappingGridContainer ? body : (
<Grid container spacing={3} pb={4}> <Grid container spacing={2.5}>
{body} {body}
</Grid> </Grid>
) )

View File

@ -106,9 +106,15 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// if this parent widget is in card form, and its children are too, then we need some px // // if this parent widget is in card form, and its children are too, then we need some px //
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
const px = (widgetMetaData && widgetMetaData.isCard && widgets && widgets[0] && widgets[0].isCard) ? 3 : 0; const parentIsCard = widgetMetaData && widgetMetaData.isCard;
const childrenAreCards = widgetMetaData && widgets && widgets[0] && widgets[0].isCard;
const px = (parentIsCard && childrenAreCards) ? 3 : 0;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is a parent, which is not a card, then we need to omit the padding, i think, on the Widget that ultimately gets rendered //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const omitPadding = !parentIsCard;
// @ts-ignore
return ( return (
qInstance && data ? ( qInstance && data ? (
<Widget <Widget
@ -116,6 +122,7 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
widgetData={data} widgetData={data}
storeDropdownSelections={storeDropdownSelections} storeDropdownSelections={storeDropdownSelections}
reloadWidgetCallback={parentReloadWidgetCallback} reloadWidgetCallback={parentReloadWidgetCallback}
omitPadding={omitPadding}
> >
<Box sx={{height: "100%", width: "100%"}} px={px}> <Box sx={{height: "100%", width: "100%"}} px={px}>
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType == "TABS"}/> <DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType == "TABS"}/>

View File

@ -25,14 +25,13 @@ import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import LinearProgress from "@mui/material/LinearProgress";
import Tooltip from "@mui/material/Tooltip/Tooltip"; import Tooltip from "@mui/material/Tooltip/Tooltip";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import parse from "html-react-parser"; import parse from "html-react-parser";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {NavigateFunction, useNavigate} from "react-router-dom"; import {NavigateFunction, useNavigate} from "react-router-dom";
import colors from "qqq/components/legacy/colors"; import colors from "qqq/assets/theme/base/colors";
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu"; import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
export interface WidgetData export interface WidgetData
{ {
@ -43,6 +42,7 @@ export interface WidgetData
id: string, id: string,
label: string label: string
}[][]; }[][];
dropdownDefaultValueList?: string[];
dropdownNeedsSelectedText?: string; dropdownNeedsSelectedText?: string;
hasPermission?: boolean; hasPermission?: boolean;
errorLoading?: boolean; errorLoading?: boolean;
@ -56,6 +56,7 @@ interface Props
labelAdditionalComponentsLeft: LabelComponent[]; labelAdditionalComponentsLeft: LabelComponent[];
labelAdditionalElementsLeft: JSX.Element[]; labelAdditionalElementsLeft: JSX.Element[];
labelAdditionalComponentsRight: LabelComponent[]; labelAdditionalComponentsRight: LabelComponent[];
labelBoxAdditionalSx?: any;
widgetMetaData?: QWidgetMetaData; widgetMetaData?: QWidgetMetaData;
widgetData?: WidgetData; widgetData?: WidgetData;
children: JSX.Element; children: JSX.Element;
@ -64,6 +65,7 @@ interface Props
isChild?: boolean; isChild?: boolean;
footerHTML?: string; footerHTML?: string;
storeDropdownSelections?: boolean; storeDropdownSelections?: boolean;
omitPadding: boolean;
} }
Widget.defaultProps = { Widget.defaultProps = {
@ -74,6 +76,8 @@ Widget.defaultProps = {
labelAdditionalComponentsLeft: [], labelAdditionalComponentsLeft: [],
labelAdditionalElementsLeft: [], labelAdditionalElementsLeft: [],
labelAdditionalComponentsRight: [], labelAdditionalComponentsRight: [],
labelBoxAdditionalSx: {},
omitPadding: false,
}; };
@ -102,16 +106,18 @@ export class LabelComponent
export class HeaderIcon extends LabelComponent export class HeaderIcon extends LabelComponent
{ {
iconName: string; iconName: string;
iconPath: string;
color: string; color: string;
coloredBG: boolean; coloredBG: boolean;
iconColor: string; iconColor: string;
bgColor: string; bgColor: string;
constructor(iconName: string, color: string, coloredBG: boolean = true) constructor(iconName: string, iconPath: string, color: string, coloredBG: boolean = true)
{ {
super(); super();
this.iconName = iconName; this.iconName = iconName;
this.iconPath = iconPath;
this.color = color; this.color = color;
this.coloredBG = coloredBG; this.coloredBG = coloredBG;
@ -122,20 +128,23 @@ export class HeaderIcon extends LabelComponent
render = (args: LabelComponentRenderArgs): JSX.Element => render = (args: LabelComponentRenderArgs): JSX.Element =>
{ {
return ( const styles = {
<Icon sx={{ width: "1.75rem",
m: 2, height: "1.75rem",
mr: 0, color: this.iconColor,
mb: 0, backgroundColor: this.bgColor,
width: "1.75rem", borderRadius: "0.25rem"
height: "1.75rem", };
color: this.iconColor,
backgroundColor: this.bgColor, if (this.iconPath)
padding: "0.25rem", {
borderRadius: "0.25rem" return (<Box sx={{textAlign: "center", ...styles}}><img src={this.iconPath} width="16" height="16" /></Box>);
}} fontSize="small">{this.iconName}</Icon> }
) else
} {
return (<Icon sx={{padding: "0.25rem", ...styles}} fontSize="small">{this.iconName}</Icon>);
}
};
} }
@ -167,7 +176,7 @@ export class AddNewRecordButton extends LabelComponent
render = (args: LabelComponentRenderArgs): JSX.Element => render = (args: LabelComponentRenderArgs): JSX.Element =>
{ {
return ( return (
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="0.25rem"> <Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="-0.5rem">
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button> <Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
</Typography> </Typography>
); );
@ -181,41 +190,111 @@ export class AddNewRecordButton extends LabelComponent
export class Dropdown extends LabelComponent export class Dropdown extends LabelComponent
{ {
label: string; label: string;
dropdownMetaData: any;
options: DropdownOption[]; options: DropdownOption[];
dropdownDefaultValue?: string;
dropdownName: string; dropdownName: string;
onChangeCallback: any; onChangeCallback: any;
constructor(label: string, options: DropdownOption[], dropdownName: string, onChangeCallback: any) constructor(label: string, dropdownMetaData: any, options: DropdownOption[], dropdownDefaultValue: string, dropdownName: string, onChangeCallback: any)
{ {
super(); super();
this.label = label; this.label = label;
this.dropdownMetaData = dropdownMetaData;
this.options = options; this.options = options;
this.dropdownDefaultValue = dropdownDefaultValue;
this.dropdownName = dropdownName; this.dropdownName = dropdownName;
this.onChangeCallback = onChangeCallback; this.onChangeCallback = onChangeCallback;
} }
render = (args: LabelComponentRenderArgs): JSX.Element => render = (args: LabelComponentRenderArgs): JSX.Element =>
{ {
const label = `Select ${this.label}`;
let defaultValue = null; let defaultValue = null;
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${this.dropdownName}`; const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${this.dropdownName}`;
if (args.widgetProps.storeDropdownSelections) if (args.widgetProps.storeDropdownSelections)
{ {
/////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////
// see if an existing value is stored in local storage, and if so set it in dropdown // // see if an existing value is stored in local storage, and if so set it in dropdown //
/////////////////////////////////////////////////////////////////////////////////////// // originally we used the full object from localStorage - but - in case the label //
defaultValue = JSON.parse(localStorage.getItem(localStorageKey)); // changed since it was stored, we'll instead just find the option by id (or in case that //
args.dropdownData[args.componentIndex] = defaultValue?.id; // option isn't available anymore, then we'll select nothing instead of a missing value //
////////////////////////////////////////////////////////////////////////////////////////////
try
{
const localStorageOption = JSON.parse(localStorage.getItem(localStorageKey));
if(localStorageOption)
{
const id = localStorageOption.id;
for (let i = 0; i < this.options.length; i++)
{
if (this.options[i].id == id)
{
defaultValue = this.options[i]
args.dropdownData[args.componentIndex] = defaultValue?.id;
}
}
}
}
catch(e)
{
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e)
}
}
/////////////////////////////////////////////////////////////////////////////////////////////
// if there wasn't a value selected, but there is a default from the backend, then use it. //
/////////////////////////////////////////////////////////////////////////////////////////////
if (defaultValue == null && this.dropdownDefaultValue != null)
{
for (let i = 0; i < this.options.length; i++)
{
if(this.options[i].id == this.dropdownDefaultValue)
{
defaultValue = this.options[i];
args.dropdownData[args.componentIndex] = defaultValue?.id;
if (args.widgetProps.storeDropdownSelections)
{
localStorage.setItem(localStorageKey, JSON.stringify(defaultValue));
}
this.onChangeCallback(label, defaultValue);
break;
}
}
}
/////////////////////////////////////////////////////////////////////////////
// if there's a 'label for null value' (and no default from the backend), //
// then add that as an option (and select it if nothing else was selected) //
/////////////////////////////////////////////////////////////////////////////
let options = this.options;
if (this.dropdownMetaData.labelForNullValue && !this.dropdownDefaultValue)
{
const nullOption = {id: null as string, label: this.dropdownMetaData.labelForNullValue};
options = [nullOption, ...this.options];
if (!defaultValue)
{
defaultValue = nullOption;
}
} }
return ( return (
<Box my={2} sx={{float: "right"}}> <Box mb={2} sx={{float: "right"}}>
<DropdownMenu <WidgetDropdownMenu
name={this.dropdownName} name={this.dropdownName}
defaultValue={defaultValue} defaultValue={defaultValue}
sx={{width: 200, marginLeft: "15px"}} sx={{marginLeft: "1rem"}}
label={`Select ${this.label}`} label={label}
dropdownOptions={this.options} startIcon={this.dropdownMetaData.startIconName}
allowBackAndForth={this.dropdownMetaData.allowBackAndForth}
backAndForthInverted={this.dropdownMetaData.backAndForthInverted}
disableClearable={this.dropdownMetaData.disableClearable}
dropdownOptions={options}
onChangeCallback={this.onChangeCallback} onChangeCallback={this.onChangeCallback}
width={this.dropdownMetaData.width ?? 225}
/> />
</Box> </Box>
); );
@ -239,8 +318,8 @@ export class ReloadControl extends LabelComponent
render = (args: LabelComponentRenderArgs): JSX.Element => render = (args: LabelComponentRenderArgs): JSX.Element =>
{ {
return ( return (
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem"> <Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.175rem">
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip> <Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon sx={{color: colors.gray.main, fontSize: 1.125}}>refresh</Icon></Button></Tooltip>
</Typography> </Typography>
); );
}; };
@ -325,7 +404,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) => props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) =>
{ {
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`); // console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], dropdownData, props.widgetData.dropdownNameList[index], handleDataChange)); let defaultValue = null;
if(props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
{
defaultValue = props.widgetData.dropdownDefaultValueList[index];
}
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
}); });
setLabelComponentsRight(updatedStateLabelComponentsRight); setLabelComponentsRight(updatedStateLabelComponentsRight);
} }
@ -453,16 +537,16 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
// first look for a label in the widget data, which would override that in the metadata // // 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}... // // note - previously this had a ?: and one was pl={2}, the other was pl={3}... //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
let labelElement = ( let labelElement = (
<Typography sx={{position: "relative", top: -4, cursor: "default"}} variant="h6" fontWeight="medium" display="inline"> <Typography sx={{cursor: "default", pl: "auto", pt: props.widgetMetaData.type == "parentWidget" ? "1rem" : "auto", fontWeight: 600}} variant="h6" display="inline">
{labelToUse} {labelToUse}
</Typography> </Typography>
); );
if(props.widgetMetaData.tooltip) if (props.widgetMetaData.tooltip)
{ {
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip> labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>;
} }
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true; const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
@ -470,26 +554,22 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}> <Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
{ {
needLabelBox && needLabelBox &&
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} minHeight={"3.5rem"}> <Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"}>
<Box pt={2} pb={1} ml={2}> <Box>
{ {
hasPermission ? hasPermission ?
props.widgetMetaData?.icon && ( props.widgetMetaData?.icon && (
<Box <Box ml={1} mr={2} mt={-4} sx={{
ml={1} display: "flex",
mr={2} justifyContent: "center",
mt={-4} alignItems: "center",
sx={{ width: "64px",
display: "flex", height: "64px",
justifyContent: "center", borderRadius: "8px",
alignItems: "center", background: colors.info.main,
width: "64px", color: "#ffffff",
height: "64px", float: "left"
borderRadius: "8px", }}
background: colors.info.main,
color: "#ffffff",
float: "left"
}}
> >
<Icon fontSize="medium" color="inherit"> <Icon fontSize="medium" color="inherit">
{props.widgetMetaData.icon} {props.widgetMetaData.icon}
@ -497,20 +577,17 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
</Box> </Box>
) : ) :
( (
<Box <Box ml={3} mt={-4} sx={{
ml={3} display: "flex",
mt={-4} justifyContent: "center",
sx={{ alignItems: "center",
display: "flex", width: "64px",
justifyContent: "center", height: "64px",
alignItems: "center", borderRadius: "8px",
width: "64px", background: colors.info.main,
height: "64px", color: "#ffffff",
borderRadius: "8px", float: "left"
background: colors.info.main, }}
color: "#ffffff",
float: "left"
}}
> >
<Icon fontSize="medium" color="inherit">lock</Icon> <Icon fontSize="medium" color="inherit">lock</Icon>
</Box> </Box>
@ -542,7 +619,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
</Box> </Box>
} }
{ {
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0"}} /> : <Box height="0.375rem" />) ///////////////////////////////////////////////////////////////////
// turning this off... for now. maybe make a property in future //
///////////////////////////////////////////////////////////////////
/*
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0", mx:-2}} /> : <Box height="0.375rem" />)
*/
} }
{ {
errorLoading ? ( errorLoading ? (
@ -552,7 +634,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
</Box> </Box>
) : ( ) : (
hasPermission && props.widgetData?.dropdownNeedsSelectedText ? ( hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
<Box pb={3} pr={3} sx={{width: "100%", textAlign: "right"}}> <Box pb={3} sx={{width: "100%", textAlign: "right"}}>
<Typography variant="body2"> <Typography variant="body2">
{props.widgetData?.dropdownNeedsSelectedText} {props.widgetData?.dropdownNeedsSelectedText}
</Typography> </Typography>
@ -573,11 +655,12 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
} }
</Box>; </Box>;
const padding = props.omitPadding ? "auto" : "24px 16px";
return props.widgetMetaData?.isCard return props.widgetMetaData?.isCard
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}> ? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%", p: padding}} className={fullScreenWidgetClassName}>
{widgetContent} {widgetContent}
</Card> </Card>
: <span style={{width: "100%"}}>{widgetContent}</span>; : <span style={{width: "100%", padding: padding}}>{widgetContent}</span>;
} }
export default Widget; export default Widget;

View File

@ -45,18 +45,39 @@ export const options = {
animation: { animation: {
duration: 0 duration: 0
}, },
elements: {
bar: {
borderRadius: 4
}
},
plugins: { plugins: {
tooltip: { tooltip: {
// todo - some configs around this // todo - some configs around this
enabled: false callbacks: {
title: function(context: any)
{
return ("");
},
label: function(context: any)
{
if(context.dataset.label.startsWith(context.label))
{
return `${context.label}: ${context.formattedValue}`;
}
else
{
return ("");
}
}
}
}, },
legend: { legend: {
position: "bottom", position: "bottom",
labels: { labels: {
usePointStyle: true, usePointStyle: true,
pointStyle: "circle", pointStyle: "circle",
boxHeight: 8, boxHeight: 6,
boxWidth: 8, boxWidth: 6,
padding: 12, padding: 12,
font: { font: {
size: 14 size: 14
@ -127,7 +148,7 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
return data ? ( return data ? (
<Box p={3} pt={1}> <Box>
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)} {chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
<Box width="100%" height="300px"> <Box width="100%" height="300px">
<Bar data={data} options={options} getElementsAtEvent={handleClick} /> <Bar data={data} options={options} getElementsAtEvent={handleClick} />

View File

@ -63,7 +63,7 @@ const options = {
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },
@ -86,7 +86,7 @@ const options = {
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },

View File

@ -67,7 +67,7 @@ const options = {
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },
@ -88,7 +88,7 @@ const options = {
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },

View File

@ -81,7 +81,7 @@ const options = {
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },
@ -107,7 +107,7 @@ const options = {
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },

View File

@ -69,7 +69,7 @@ function configs(labels: any, datasets: any)
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },
@ -90,7 +90,7 @@ function configs(labels: any, datasets: any)
font: { font: {
size: 14, size: 14,
weight: 300, weight: 300,
family: "Roboto", family: "SF Pro Display,Roboto",
style: "normal", style: "normal",
lineHeight: 2, lineHeight: 2,
}, },

View File

@ -91,49 +91,41 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
return ( return (
<Card sx={{boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1, border: 0}}> <Card sx={{boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1, border: 0}}>
<Box mt={1}> <Box>
<Box px={3}> <Box>
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)} {chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
</Box> </Box>
<Grid container alignItems="center"> <Box width="100%" height="300px">
<Grid item xs={12} justifyContent="center"> {useMemo(
<Box width="100%" height="300px" py={2} pr={2} pl={2}> () => (
{useMemo( <Pie data={data} options={options} getElementsAtEvent={handleClick} />
() => ( ),
<Pie data={data} options={options} getElementsAtEvent={handleClick} /> [chartData]
), )}
[chartData] </Box>
)} {
!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" />
</Box> </Box>
{ )
!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" />
</Box>
)
}
</Grid>
</Grid>
{ {
description && ( description && (
<> <>
<Divider /> <Divider />
<Grid container> <Box display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
<Grid item xs={12}> <MDTypography variant="button" color="text" fontWeight="light">
<Box pb={2} px={2} display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto"> {parse(description)}
<MDTypography variant="button" color="text" fontWeight="light"> </MDTypography>
{parse(description)} </Box>
</MDTypography>
</Box>
</Grid>
</Grid>
</> </>
) )
} }

View File

@ -71,11 +71,27 @@ function configs(labels: any, datasets: any)
callbacks: { callbacks: {
label: function(context: any) label: function(context: any)
{ {
let percentSuffix = "";
try
{
//////////////////////////////////////////////////////////////////////////
// make percent by dividing this slice's value by the sum of all values //
//////////////////////////////////////////////////////////////////////////
const thisSlice = context.dataset.data[context.dataIndex];
const sum = context.dataset.data.reduce((acc: number, val: number) => acc + val, 0);
percentSuffix = " (" + Number(100 * thisSlice / sum).toFixed(1) + "%)";
}
catch(e)
{
// leave percentSuffix empty
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// our labels already have the value in them - so just use the label in the // // 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!) // // tooltip (lib by default puts label + value, so we were duplicating value!) //
// oh, and we add percent if we can //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
return context.label; return context.label + percentSuffix;
} }
} }
}, },

View File

@ -65,7 +65,7 @@ function StackedBarChart({chartSubheaderData}: Props): JSX.Element
iconName = chartSubheaderData.isUpVsPrevious ? UP_ICON : DOWN_ICON; iconName = chartSubheaderData.isUpVsPrevious ? UP_ICON : DOWN_ICON;
} }
let mainNumberElement = <Typography variant="h2" display="inline">{ValueUtils.getFormattedNumber(chartSubheaderData.mainNumber)}</Typography>; let mainNumberElement = <Typography variant="h3" display="inline">{ValueUtils.getFormattedNumber(chartSubheaderData.mainNumber)}</Typography>;
if(chartSubheaderData.mainNumberUrl) if(chartSubheaderData.mainNumberUrl)
{ {
mainNumberElement = <Link to={chartSubheaderData.mainNumberUrl}>{mainNumberElement}</Link> mainNumberElement = <Link to={chartSubheaderData.mainNumberUrl}>{mainNumberElement}</Link>
@ -74,7 +74,7 @@ function StackedBarChart({chartSubheaderData}: Props): JSX.Element
let previousNumberElement = ( let previousNumberElement = (
<> <>
<Typography display="inline" variant="body2" sx={{color: colors.black.main}}> <Typography display="block" variant="body2" sx={{color: colors.gray.main, fontSize: ".875rem", fontWeight: 500}}>
&nbsp;{chartSubheaderData.vsDescription} &nbsp;{chartSubheaderData.vsDescription}
{chartSubheaderData.vsPreviousNumber && (<>&nbsp;({ValueUtils.getFormattedNumber(chartSubheaderData.vsPreviousNumber)})</>)} {chartSubheaderData.vsPreviousNumber && (<>&nbsp;({ValueUtils.getFormattedNumber(chartSubheaderData.vsPreviousNumber)})</>)}
</Typography> </Typography>
@ -91,9 +91,9 @@ function StackedBarChart({chartSubheaderData}: Props): JSX.Element
{mainNumberElement} {mainNumberElement}
{ {
chartSubheaderData.vsPreviousPercent != null && iconName != null && ( chartSubheaderData.vsPreviousPercent != null && iconName != null && (
<Box display="inline-flex" alignItems="flex-end" pb={1} ml={-0.5}> <Box display="inline-flex" alignItems="baseline" pb={0.5} ml={-0.5}>
<Icon fontSize="medium" sx={{color: color}}>{iconName}</Icon> <Icon fontSize="medium" sx={{color: color, alignSelf: "flex-end"}}>{iconName}</Icon>
<Typography display="inline" variant="body2" sx={{color: color}}>{chartSubheaderData.vsPreviousPercent}%</Typography> <Typography display="inline" variant="body2" sx={{color: color, fontSize: ".875rem", fontWeight: 500}}>{chartSubheaderData.vsPreviousPercent}%</Typography>
{previousNumberElement} {previousNumberElement}
</Box> </Box>
) )

View File

@ -1,179 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Collapse, Theme} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import {SxProps} from "@mui/system";
import {Field, Form, Formik} from "formik";
import React, {useState} from "react";
import MDInput from "qqq/components/legacy/MDInput";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
export interface DropdownOption
{
id: string;
label: string;
}
/////////////////////////
// inputs and defaults //
/////////////////////////
interface Props
{
name: string;
defaultValue?: any;
label?: string;
dropdownOptions?: DropdownOption[];
onChangeCallback?: (dropdownLabel: string, data: any) => void;
sx?: SxProps<Theme>;
}
interface StartAndEndDate
{
startDate?: string,
endDate?: string
}
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
{
const customTimeValues: StartAndEndDate = {};
if(defaultValue && defaultValue.id)
{
var parts = defaultValue.id.split(",");
if(parts.length >= 2)
{
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
}
if(parts.length >= 3)
{
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
}
}
return (customTimeValues);
}
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
{
const backendTimeValues: StartAndEndDate = {};
if(frontendDefaultValues && frontendDefaultValues.startDate)
{
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
}
if(frontendDefaultValues && frontendDefaultValues.endDate)
{
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
}
return (backendTimeValues);
}
function DropdownMenu({name, defaultValue, label, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
{
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
const handleOnChange = (event: any, newValue: any, reason: string) =>
{
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom"
setCustomTimesVisible(isTimeframeCustom);
if(isTimeframeCustom)
{
callOnChangeCallbackIfCustomTimeframeHasDateValues();
}
else
{
onChangeCallback(label, newValue);
}
};
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
{
if(customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
{
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
}
}
let customTimes = <></>;
if (name == "timeframe")
{
const handleSubmit = async (values: any, actions: any) =>
{
};
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
{
customTimeValuesFrontend[fieldName] = event.target.value;
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
clearTimeout(debounceTimeout);
const newDebounceTimeout = setTimeout(() =>
{
callOnChangeCallbackIfCustomTimeframeHasDateValues();
}, 500);
setDebounceTimeout(newDebounceTimeout);
};
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
<Collapse orientation="horizontal" in={customTimesVisible}>
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
{({}) => (
<Form id="timeframe-form" autoComplete="off">
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
</Form>
)}
</Formik>
</Collapse>
</Box>;
}
return (
dropdownOptions ? (
<span style={{whiteSpace: "nowrap", display: "flex"}} className="dashboardDropdownMenu">
<Autocomplete
defaultValue={defaultValue}
size="small"
disablePortal
id={`${label}-combo-box`}
options={dropdownOptions}
sx={{...sx, cursor: "pointer", display: "inline-block"}}
onChange={handleOnChange}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params: any) => <TextField {...params} label={label} />}
renderOption={(props, option: DropdownOption) => (
<li {...props} style={{whiteSpace: "normal"}}>{option.label}</li>
)}
/>
{customTimes}
</span>
) : null
);
}
export default DropdownMenu;

View File

@ -0,0 +1,335 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Collapse, Theme, InputAdornment} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField";
import {SxProps} from "@mui/system";
import {Field, Form, Formik} from "formik";
import React, {useState} from "react";
import colors from "qqq/assets/theme/base/colors";
import MDInput from "qqq/components/legacy/MDInput";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
export interface DropdownOption
{
id: string;
label: string;
}
/////////////////////////
// inputs and defaults //
/////////////////////////
interface Props
{
name: string;
defaultValue?: any;
label?: string;
startIcon?: string;
width?: number;
disableClearable?: boolean;
allowBackAndForth?: boolean;
backAndForthInverted?: boolean;
dropdownOptions?: DropdownOption[];
onChangeCallback?: (dropdownLabel: string, data: any) => void;
sx?: SxProps<Theme>;
}
interface StartAndEndDate
{
startDate?: string,
endDate?: string
}
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
{
const customTimeValues: StartAndEndDate = {};
if (defaultValue && defaultValue.id)
{
var parts = defaultValue.id.split(",");
if (parts.length >= 2)
{
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
}
if (parts.length >= 3)
{
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
}
}
return (customTimeValues);
}
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
{
const backendTimeValues: StartAndEndDate = {};
if (frontendDefaultValues && frontendDefaultValues.startDate)
{
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
}
if (frontendDefaultValues && frontendDefaultValues.endDate)
{
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
}
return (backendTimeValues);
}
function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disableClearable, allowBackAndForth, backAndForthInverted, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
{
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState(defaultValue);
const [inputValue, setInputValue] = useState("");
const [backDisabled, setBackDisabled] = useState(false);
const [forthDisabled, setForthDisabled] = useState(false);
const doForceOpen = (event: React.MouseEvent<HTMLDivElement>) =>
{
setIsOpen(true);
};
function getSelectedIndex(value: DropdownOption)
{
let currentIndex = null;
for (let i = 0; i < dropdownOptions.length; i++)
{
if (value && dropdownOptions[i].id == value.id)
{
currentIndex = i;
break;
}
}
return currentIndex;
}
const navigateBackAndForth = (event: React.MouseEvent, direction: -1 | 1) =>
{
event.stopPropagation();
let currentIndex = getSelectedIndex(value);
if (currentIndex == null)
{
console.log("No current value.... TODO");
return;
}
if (currentIndex == 0 && direction == -1)
{
console.log("Can't go -1");
return;
}
if (currentIndex == dropdownOptions.length - 1 && direction == 1)
{
console.log("Can't go +1");
return;
}
handleOnChange(event, dropdownOptions[currentIndex + direction], "navigatedBackAndForth");
};
const handleOnChange = (event: any, newValue: any, reason: string) =>
{
setValue(newValue);
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom";
setCustomTimesVisible(isTimeframeCustom);
if (isTimeframeCustom)
{
callOnChangeCallbackIfCustomTimeframeHasDateValues();
}
else
{
onChangeCallback(label, newValue);
}
/* this had bugs (seemed to not take immediate effect?), so don't use for now.
let currentIndex = getSelectedIndex(value);
if(currentIndex == 0)
{
backAndForthInverted ? setForthDisabled(true) : setBackDisabled(true);
}
else
{
backAndForthInverted ? setForthDisabled(false) : setBackDisabled(false);
}
if (currentIndex == dropdownOptions.length - 1)
{
backAndForthInverted ? setBackDisabled(true) : setForthDisabled(true);
}
else
{
backAndForthInverted ? setBackDisabled(false) : setForthDisabled(false);
}
*/
};
const handleOnInputChange = (event: any, newValue: any, reason: string) =>
{
setInputValue(newValue);
};
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
{
if (customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
{
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
}
};
let customTimes = <></>;
if (name == "timeframe")
{
const handleSubmit = async (values: any, actions: any) =>
{
};
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
{
customTimeValuesFrontend[fieldName] = event.target.value;
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
clearTimeout(debounceTimeout);
const newDebounceTimeout = setTimeout(() =>
{
callOnChangeCallbackIfCustomTimeframeHasDateValues();
}, 500);
setDebounceTimeout(newDebounceTimeout);
};
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
<Collapse orientation="horizontal" in={customTimesVisible}>
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
{({}) => (
<Form id="timeframe-form" autoComplete="off">
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
</Form>
)}
</Formik>
</Collapse>
</Box>;
}
const startAdornment = startIcon ? <Icon sx={{fontSize: "1.25rem!important", color: colors.gray.main, paddingLeft: allowBackAndForth ? "auto" : "0.25rem", width: allowBackAndForth ? "1.5rem" : "1.75rem"}}>{startIcon}</Icon> : null;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we tried this end-adornment, for a different style of down-arrow - but by using it, we then messed something else up (i forget what), so... not used right now //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const endAdornment = <InputAdornment position="end" sx={{position: "absolute", right: allowBackAndForth ? "-0.5rem" : "0.5rem"}}><Icon sx={{fontSize: "1.75rem!important", color: colors.gray.main}}>keyboard_arrow_down</Icon></InputAdornment>;
const fontSize = "1rem";
let optionPaddingLeftRems = 0.75;
if(startIcon)
{
optionPaddingLeftRems += allowBackAndForth ? 1.5 : 1.75
}
if(allowBackAndForth)
{
optionPaddingLeftRems += 2.5;
}
return (
dropdownOptions ? (
<Box sx={{whiteSpace: "nowrap", display: "flex",
"& .MuiPopperUnstyled-root": {
border: `1px solid ${colors.grayLines.main}`,
borderTop: "none",
borderRadius: "0 0 0.75rem 0.75rem",
padding: 0,
}, "& .MuiPaper-rounded": {
borderRadius: "0 0 0.75rem 0.75rem",
}
}} className="dashboardDropdownMenu">
<Autocomplete
id={`${label}-combo-box`}
defaultValue={defaultValue}
value={value}
onChange={handleOnChange}
inputValue={inputValue}
onInputChange={handleOnInputChange}
isOptionEqualToValue={(option, value) => option.id === value.id}
open={isOpen}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
size="small"
disablePortal
disableClearable={disableClearable}
options={dropdownOptions}
sx={{
...sx,
cursor: "pointer",
display: "inline-block",
"& .MuiOutlinedInput-notchedOutline": {
border: "none"
},
}}
renderInput={(params: any) =>
<>
<Box sx={{width: `${width}px`, background: "white", borderRadius: isOpen ? "0.75rem 0.75rem 0 0" : "0.75rem", border: `1px solid ${colors.grayLines.main}`, "& *": {cursor: "pointer"}}} display="flex" alignItems="center" onClick={(event) => doForceOpen(event)}>
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? 1 : -1)} disabled={backDisabled}><Icon>navigate_before</Icon></IconButton>}
<TextField {...params} placeholder={label} sx={{
"& .MuiInputBase-input": {
fontSize: fontSize
}
}} InputProps={{...params.InputProps, startAdornment: startAdornment/*, endAdornment: endAdornment*/}}
/>
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? -1 : 1)} disabled={forthDisabled}><Icon>navigate_next</Icon></IconButton>}
</Box>
</>
}
renderOption={(props, option: DropdownOption) => (
<li {...props} style={{whiteSpace: "normal", fontSize: fontSize, paddingLeft: `${optionPaddingLeftRems}rem`}}>{option.label}</li>
)}
noOptionsText={<Box fontSize={fontSize}>No options found</Box>}
slotProps={{
popper: {
sx: {
width: `${width}px!important`
}
}
}}
/>
{customTimes}
</Box>
) : null
);
}
export default WidgetDropdownMenu;

View File

@ -22,6 +22,7 @@
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import Tooltip from "@mui/material/Tooltip/Tooltip"; import Tooltip from "@mui/material/Tooltip/Tooltip";
@ -134,7 +135,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
if(data && data.viewAllLink) if(data && data.viewAllLink)
{ {
labelAdditionalElementsLeft.push( labelAdditionalElementsLeft.push(
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem"> <Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative">
<Link to={data.viewAllLink}>View All</Link> <Link to={data.viewAllLink}>View All</Link>
</Typography> </Typography>
) )
@ -174,8 +175,8 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
if(widgetMetaData?.showExportButton) if(widgetMetaData?.showExportButton)
{ {
labelAdditionalElementsLeft.push( labelAdditionalElementsLeft.push(
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem"> <Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative">
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon>save_alt</Icon></Button></Tooltip> <Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.25}}>save_alt</Icon></Button></Tooltip>
</Typography> </Typography>
); );
} }
@ -215,41 +216,49 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
widgetData={data} widgetData={data}
labelAdditionalElementsLeft={labelAdditionalElementsLeft} labelAdditionalElementsLeft={labelAdditionalElementsLeft}
labelAdditionalComponentsRight={labelAdditionalComponentsRight} labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
> >
<DataGridPro <Box mx={-2} mb={-3}>
autoHeight <DataGridPro
rows={rows} autoHeight
disableSelectionOnClick sx={{
columns={columns} borderBottom: "none",
rowBuffer={10} borderLeft: "none",
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} borderRight: "none"
onRowClick={handleRowClick} }}
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells... rows={rows}
// components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}} disableSelectionOnClick
// pinnedColumns={pinnedColumns} columns={columns}
// onPinnedColumnsChange={handlePinnedColumnsChange} rowBuffer={10}
// pagination getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
// paginationMode="server" onRowClick={handleRowClick}
// rowsPerPageOptions={[20]} // getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
// sortingMode="server" // components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
// filterMode="server" // pinnedColumns={pinnedColumns}
// page={pageNumber} // onPinnedColumnsChange={handlePinnedColumnsChange}
// checkboxSelection // pagination
rowCount={data && data.totalRows} // paginationMode="server"
// onPageSizeChange={handleRowsPerPageChange} // rowsPerPageOptions={[20]}
// onStateChange={handleStateChange} // sortingMode="server"
// density={density} // filterMode="server"
// loading={loading} // page={pageNumber}
// filterModel={filterModel} // checkboxSelection
// onFilterModelChange={handleFilterChange} rowCount={data && data.totalRows}
// columnVisibilityModel={columnVisibilityModel} // onPageSizeChange={handleRowsPerPageChange}
// onColumnVisibilityModelChange={handleColumnVisibilityChange} // onStateChange={handleStateChange}
// onColumnOrderChange={handleColumnOrderChange} // density={density}
// onSelectionModelChange={selectionChanged} // loading={loading}
// onSortModelChange={handleSortChange} // filterModel={filterModel}
// sortingOrder={[ "asc", "desc" ]} // onFilterModelChange={handleFilterChange}
// sortModel={columnSortModel} // columnVisibilityModel={columnVisibilityModel}
/> // onColumnVisibilityModelChange={handleColumnVisibilityChange}
// onColumnOrderChange={handleColumnOrderChange}
// onSelectionModelChange={selectionChanged}
// onSortModelChange={handleSortChange}
// sortingOrder={[ "asc", "desc" ]}
// sortModel={columnSortModel}
/>
</Box>
</Widget> </Widget>
); );
} }

View File

@ -392,14 +392,8 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
return JSON.stringify(new QQueryFilter([new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, [scriptRevisionId])])); return JSON.stringify(new QQueryFilter([new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, [scriptRevisionId])]));
} }
/*
position: relative;
left: -356px;
width: calc(100% + 380px);
*/
return ( return (
<Grid container className="scriptViewer"> <Grid container className="scriptViewer" m={-2} pt={4} width={"calc(100% + 2rem)"}>
<Grid item xs={12}> <Grid item xs={12}>
<Box> <Box>
{ {

View File

@ -31,6 +31,7 @@ import Tooltip from "@mui/material/Tooltip";
import parse from "html-react-parser"; import parse from "html-react-parser";
import {useEffect, useMemo, useState} from "react"; import {useEffect, useMemo, useState} from "react";
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} from "react-table"; import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} from "react-table";
import colors from "qqq/assets/theme/base/colors";
import MDInput from "qqq/components/legacy/MDInput"; import MDInput from "qqq/components/legacy/MDInput";
import MDPagination from "qqq/components/legacy/MDPagination"; import MDPagination from "qqq/components/legacy/MDPagination";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
@ -284,11 +285,12 @@ function DataTable({
let boxStyle = {}; let boxStyle = {};
if(fixedStickyLastRow) if(fixedStickyLastRow)
{ {
boxStyle = isFooter ? {overflowY: "visible", borderTop: "0.0625rem solid #f0f2f5;"} : {height: fixedHeight ? `${fixedHeight}px` : "360px", overflowY: "auto"}; boxStyle = isFooter
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, overflow: "auto", scrollbarGutter: "stable"}
: {height: fixedHeight ? `${fixedHeight}px` : "360px", overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
} }
const className = isFooter ? "hideScrollbars" : ""; return <Box sx={boxStyle}>
return <Box sx={boxStyle} className={className}>
<Table {...getTableProps()}> <Table {...getTableProps()}>
{ {
includeHead && ( includeHead && (
@ -316,27 +318,50 @@ function DataTable({
{rows.map((row: any, key: any) => {rows.map((row: any, key: any) =>
{ {
prepareRow(row); prepareRow(row);
let overrideNoEndBorder = false;
//////////////////////////////////////////////////////////////////////////////////
// don't do an end-border on nested rows - unless they're the last one in a set //
//////////////////////////////////////////////////////////////////////////////////
if(row.depth > 0)
{
overrideNoEndBorder = true;
if(key + 1 < rows.length && rows[key + 1].depth == 0)
{
overrideNoEndBorder = false;
}
}
///////////////////////////////////////
// don't do end-border on the footer //
///////////////////////////////////////
if(isFooter)
{
overrideNoEndBorder = true;
}
return ( return (
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: (row.depth > 0 ? "#FAFAFA" : "initial")}} key={key} {...row.getRowProps()}> <TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: (row.depth > 0 ? "#FAFAFA" : "initial")}} key={key} {...row.getRowProps()}>
{row.cells.map((cell: any) => ( {row.cells.map((cell: any) => (
cell.column.type !== "hidden" && ( cell.column.type !== "hidden" && (
<DataTableBodyCell <DataTableBodyCell
key={key} key={key}
noBorder={noEndBorder && rows.length - 1 === key} noBorder={noEndBorder || overrideNoEndBorder}
depth={row.depth}
align={cell.column.align ? cell.column.align : "left"} align={cell.column.align ? cell.column.align : "left"}
{...cell.getCellProps()} {...cell.getCellProps()}
> >
{ {
cell.column.type === "default" && ( cell.column.type === "default" && (
cell.value && "number" === typeof cell.value ? ( cell.value && "number" === typeof cell.value ? (
<DefaultCell>{cell.value.toLocaleString()}</DefaultCell> <DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
) : (<DefaultCell>{cell.render("Cell")}</DefaultCell>) ) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
) )
} }
{ {
cell.column.type === "htmlAndTooltip" && ( cell.column.type === "htmlAndTooltip" && (
<DefaultCell> <DefaultCell isFooter={isFooter}>
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}> <NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
<Box> <Box>
{parse(cell.value)} {parse(cell.value)}
@ -347,7 +372,7 @@ function DataTable({
} }
{ {
cell.column.type === "html" && ( cell.column.type === "html" && (
<DefaultCell>{parse(cell.value)}</DefaultCell> <DefaultCell isFooter={isFooter}>{parse(cell.value)}</DefaultCell>
) )
} }
{ {
@ -369,6 +394,7 @@ function DataTable({
</TableRow> </TableRow>
); );
})} })}
</TableBody> </TableBody>
</Table> </Table>
</Box> </Box>

View File

@ -74,7 +74,7 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
}, []); }, []);
return ( return (
<Box py={1}> <Box py={1} mx={-2}>
{ {
data && data.columns && !noRowsFoundHTML ? data && data.columns && !noRowsFoundHTML ?
<DataTable <DataTable
@ -85,7 +85,6 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
fixedHeight={fixedHeight} fixedHeight={fixedHeight}
showTotalEntries={false} showTotalEntries={false}
isSorted={false} isSorted={false}
noEndBorder
/> />
: noRowsFoundHTML ? : noRowsFoundHTML ?
<Box p={3} pt={1} pb={1} sx={{textAlign: "center"}}> <Box p={3} pt={1} pb={1} sx={{textAlign: "center"}}>
@ -118,7 +117,7 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}> <TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}>
{Array(8).fill(0).map((_, j) => {Array(8).fill(0).map((_, j) =>
<DataTableBodyCell key={`cell-${i}-${j}`} align="center"> <DataTableBodyCell key={`cell-${i}-${j}`} align="center">
<DefaultCell><Skeleton /></DefaultCell> <DefaultCell isFooter={false}><Skeleton /></DefaultCell>
</DataTableBodyCell> </DataTableBodyCell>
)} )}
</TableRow> </TableRow>

View File

@ -28,6 +28,7 @@ import Typography from "@mui/material/Typography";
// @ts-ignore // @ts-ignore
import {htmlToText} from "html-to-text"; import {htmlToText} from "html-to-text";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import colors from "qqq/assets/theme/base/colors";
import TableCard from "qqq/components/widgets/tables/TableCard"; import TableCard from "qqq/components/widgets/tables/TableCard";
import Widget, {WidgetData} from "qqq/components/widgets/Widget"; import Widget, {WidgetData} from "qqq/components/widgets/Widget";
import HtmlUtils from "qqq/utils/HtmlUtils"; import HtmlUtils from "qqq/utils/HtmlUtils";
@ -124,8 +125,8 @@ function TableWidget(props: Props): JSX.Element
if(props.widgetMetaData?.showExportButton) if(props.widgetMetaData?.showExportButton)
{ {
labelAdditionalElementsLeft.push( labelAdditionalElementsLeft.push(
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem"> <Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.25rem">
<Tooltip title="Export"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}><Icon>save_alt</Icon></Button></Tooltip> <Tooltip title="Export"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}><Icon sx={{color: colors.gray.main, fontSize: 1.125}}>save_alt</Icon></Button></Tooltip>
</Typography> </Typography>
); );
} }

View File

@ -22,6 +22,7 @@
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import {Theme} from "@mui/material/styles"; import {Theme} from "@mui/material/styles";
import {ReactNode} from "react"; import {ReactNode} from "react";
import colors from "qqq/assets/theme/base/colors";
// Declaring prop types for DataTableBodyCell // Declaring prop types for DataTableBodyCell
interface Props interface Props
@ -40,14 +41,26 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
py={1.5} py={1.5}
px={3} px={3}
sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({ sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({
fontSize: size.sm, borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${colors.grayLines.main}`,
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${light.main}`, fontSize: "0.875rem",
"@media (min-width: 1440px)": {
fontSize: "1rem"
},
"@media (max-width: 1440px)": {
fontSize: "0.875rem"
},
"&:nth-child(1)": {
paddingLeft: "1rem"
},
"&:last-child": {
paddingRight: "1rem"
}
})} })}
> >
<Box <Box
display="initial" display="initial"
width="max-content" width="max-content"
color="text" color={colors.dark.main}
> >
{children} {children}
</Box> </Box>

View File

@ -23,6 +23,7 @@ import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {Theme} from "@mui/material/styles"; import {Theme} from "@mui/material/styles";
import {ReactNode} from "react"; import {ReactNode} from "react";
import colors from "qqq/assets/theme/base/colors";
import {useMaterialUIController} from "qqq/context"; import {useMaterialUIController} from "qqq/context";
// Declaring props types for DataTableHeadCell // Declaring props types for DataTableHeadCell
@ -46,18 +47,28 @@ function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JS
py={1.5} py={1.5}
px={3} px={3}
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({ sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
borderBottom: `${borderWidth[1]} solid ${light.main}`, borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
"&:nth-child(1)": {
paddingLeft: "1rem"
},
"&:last-child": {
paddingRight: "1rem"
},
})} })}
> >
<Box <Box
{...rest} {...rest}
sx={({typography: {size, fontWeightBold}}: Theme) => ({ sx={({typography: {size, fontWeightBold}}: Theme) => ({
position: "relative", position: "relative",
opacity: "0.7", color: colors.grey[700],
textAlign: align, textAlign: align,
fontSize: size.xxs, "@media (min-width: 1440px)": {
fontWeight: fontWeightBold, fontSize: "1rem"
textTransform: "uppercase", },
"@media (max-width: 1440px)": {
fontSize: "0.875rem"
},
fontWeight: 600,
cursor: sorted && "pointer", cursor: sorted && "pointer",
userSelect: sorted && "none", userSelect: sorted && "none",
})} })}

View File

@ -14,12 +14,31 @@ Coded by www.creative-tim.com
*/ */
import {ReactNode} from "react"; import {ReactNode} from "react";
import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
function DefaultCell({children}: { children: ReactNode }): JSX.Element interface Props
{
isFooter: boolean
children: ReactNode;
}
function DefaultCell({isFooter, children}: Props): JSX.Element
{ {
return ( return (
<MDTypography variant="button" fontWeight="regular" color="text"> <MDTypography variant="button" color={colors.dark.main} sx={{
fontWeight: isFooter ? 600 : 500,
"@media (min-width: 1440px)": {
fontSize: "1rem"
},
"@media (max-width: 1440px)": {
fontSize: "0.875rem"
},
"& a": {
color: colors.blueGray.main
}
}}>
{children} {children}
</MDTypography> </MDTypography>
); );

View File

@ -38,11 +38,11 @@ function DashboardLayout({children}: { children: ReactNode }): JSX.Element
return ( return (
<Box <Box
sx={({breakpoints, transitions, functions: {pxToRem}}) => ({ sx={({breakpoints, transitions, functions: {pxToRem}}) => ({
p: 3, p: "20px",
position: "relative", position: "relative",
[breakpoints.up("xl")]: { [breakpoints.up("xl")]: {
marginLeft: miniSidenav ? pxToRem(120) : pxToRem(274), marginLeft: miniSidenav ? pxToRem(120) : pxToRem(245),
transition: transitions.create(["margin-left", "margin-right"], { transition: transitions.create(["margin-left", "margin-right"], {
easing: transitions.easing.easeInOut, easing: transitions.easing.easeInOut,
duration: transitions.duration.standard, duration: transitions.duration.standard,

View File

@ -204,7 +204,9 @@ function AppHome({app}: Props): JSX.Element
<BaseLayout> <BaseLayout>
<Box> <Box>
{app.widgets && ( {app.widgets && (
<DashboardWidgets widgetMetaDataList={widgets} /> <Box pb={app.sections ? 2.375 : 0}>
<DashboardWidgets widgetMetaDataList={widgets} />
</Box>
)} )}
<Grid container spacing={3}> <Grid container spacing={3}>
{ {

View File

@ -414,228 +414,238 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
////////////////////////////////////////////////// //////////////////////////////////////////////////
// render all of the components for this screen // // render all of the components for this screen //
////////////////////////////////////////////////// //////////////////////////////////////////////////
step.components && (step.components.map((component: QFrontendComponent, index: number) => ( step.components && (step.components.map((component: QFrontendComponent, index: number) =>
<div key={index}> {
{ let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]
component.type === QComponentType.HELP_TEXT && ( if(component.type == QComponentType.BULK_EDIT_FORM)
component.values.previewText ? {
<> helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
<Box mt={1}> }
<Button onClick={toggleShowFullHelpText} startIcon={<Icon>{showFullHelpText ? "expand_less" : "expand_more"}</Icon>} sx={{pl: 1}}>
{showFullHelpText ? "Hide " : "Show "} return (
{component.values.previewText} <div key={index}>
</Button> {
</Box> component.type === QComponentType.HELP_TEXT && (
<Box mt={1} style={{display: showFullHelpText ? "block" : "none"}}> component.values.previewText ?
<Typography variant="body2" color="info"> <>
{ValueUtils.breakTextIntoLines(component.values.text)} <Box mt={1}>
</Typography> <Button onClick={toggleShowFullHelpText} startIcon={<Icon>{showFullHelpText ? "expand_less" : "expand_more"}</Icon>} sx={{pl: 1}}>
</Box> {showFullHelpText ? "Hide " : "Show "}
</> {component.values.previewText}
: </Button>
<MDTypography variant="button" color="info"> </Box>
{ValueUtils.breakTextIntoLines(component.values.text)} <Box mt={1} style={{display: showFullHelpText ? "block" : "none"}}>
</MDTypography> <Typography variant="body2" color="info">
) {ValueUtils.breakTextIntoLines(component.values.text)}
} </Typography>
{ </Box>
component.type === QComponentType.BULK_EDIT_FORM && ( </>
tableMetaData && localTableSections ? :
<Grid container spacing={3} mt={2}> <MDTypography variant="button" color="info">
{ {ValueUtils.breakTextIntoLines(component.values.text)}
localTableSections.length == 0 && </MDTypography>
<Grid item xs={12}> )
<Alert color="error">There are no editable fields on this table.</Alert> }
{
component.type === QComponentType.BULK_EDIT_FORM && (
tableMetaData && localTableSections ?
<Grid container spacing={3} mt={2}>
{
localTableSections.length == 0 &&
<Grid item xs={12}>
<Alert color="error">There are no editable fields on this table.</Alert>
</Grid>
}
<Grid item xs={12} lg={3}>
{
localTableSections.length > 0 && <QRecordSidebar tableSections={localTableSections} stickyTop="20px" />
}
</Grid> </Grid>
} <Grid item xs={12} lg={9}>
<Grid item xs={12} lg={3}>
{
localTableSections.length > 0 && <QRecordSidebar tableSections={localTableSections} stickyTop="20px" />
}
</Grid>
<Grid item xs={12} lg={9}>
{localTableSections.map((section: QTableSection, index: number) =>
{
const name = section.name;
if (section.isHidden)
{ {
return; localTableSections.map((section: QTableSection, index: number) =>
}
const sectionFormFields = {};
for (let i = 0; i < section.fieldNames.length; i++)
{
const fieldName = section.fieldNames[i];
if (formFields[fieldName])
{ {
// @ts-ignore const name = section.name;
sectionFormFields[fieldName] = formFields[fieldName];
}
}
if (Object.keys(sectionFormFields).length > 0) if (section.isHidden)
{ {
const sectionFormData = { return;
formFields: sectionFormFields, }
values: values,
errors: errors,
touched: touched
};
return ( const sectionFormFields = {};
<Box key={name} pb={3}> for (let i = 0; i < section.fieldNames.length; i++)
<Card id={name} sx={{scrollMarginTop: "20px"}} elevation={5}> {
<MDTypography variant="h5" p={3} pb={1}> const fieldName = section.fieldNames[i];
{section.label} if (formFields[fieldName])
</MDTypography> {
<Box px={2}> // @ts-ignore
<QDynamicForm formData={sectionFormData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} /> sectionFormFields[fieldName] = formFields[fieldName];
}
}
if (Object.keys(sectionFormFields).length > 0)
{
const sectionFormData = {
formFields: sectionFormFields,
values: values,
errors: errors,
touched: touched
};
return (
<Box key={name} pb={3}>
<Card id={name} sx={{scrollMarginTop: "20px"}} elevation={5}>
<MDTypography variant="h5" p={3} pb={1}>
{section.label}
</MDTypography>
<Box px={2}>
<QDynamicForm formData={sectionFormData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableMetaData?.name};`} />
</Box>
</Card>
</Box> </Box>
</Card> );
</Box> }
); else
{
return (<br />);
}
})
} }
else </Grid>
{
return (<br />);
}
}
)}
</Grid> </Grid>
</Grid> : <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableMetaData?.name};`} />
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} /> )
) }
} {
{ component.type === QComponentType.EDIT_FORM && (
component.type === QComponentType.EDIT_FORM && ( <QDynamicForm formData={formData} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
<QDynamicForm formData={formData} /> )
) }
} {
{ component.type === QComponentType.VIEW_FORM && step.viewFields && (
component.type === QComponentType.VIEW_FORM && step.viewFields && ( <div>
<div> {step.viewFields.map((field: QFieldMetaData) => (
{step.viewFields.map((field: QFieldMetaData) => ( field.hasAdornment(AdornmentType.ERROR) ? (
field.hasAdornment(AdornmentType.ERROR) ? ( processValues[field.name] && (
processValues[field.name] && ( <Box key={field.name} display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="regular">
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")}
</MDTypography>
</Box>
)
) : (
<Box key={field.name} display="flex" py={1} pr={2}> <Box key={field.name} display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="regular"> <MDTypography variant="button" fontWeight="bold">
{field.label}
: &nbsp;
</MDTypography>
<MDTypography variant="button" fontWeight="regular" color="text">
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")} {ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")}
</MDTypography> </MDTypography>
</Box> </Box>
) )))
) : ( }
<Box key={field.name} display="flex" py={1} pr={2}> </div>
<MDTypography variant="button" fontWeight="bold"> )
{field.label} }
: &nbsp; {
</MDTypography> component.type === QComponentType.DOWNLOAD_FORM && (
<MDTypography variant="button" fontWeight="regular" color="text"> <Grid container display="flex" justifyContent="center">
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")} <Grid item xs={12} sm={12} xl={8} m={3} p={3} mt={6} sx={{border: "1px solid gray", borderRadius: "1rem"}}>
<Box mx={2} mt={-6} p={1} sx={{width: "fit-content", borderColor: "rgb(70%, 70%, 70%)", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "#FFFFFF"}} width="initial" color="white">
Download
</Box>
<Box display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="bold" onClick={() => download(`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`, processValues.downloadFileName)} sx={{cursor: "pointer"}}>
<Box display="flex" alignItems="center" gap={1} py={1} pr={2}>
<Icon fontSize="large">download_for_offline</Icon>
{processValues.downloadFileName}
</Box>
</MDTypography> </MDTypography>
</Box> </Box>
))) </Grid>
}
</div>
)
}
{
component.type === QComponentType.DOWNLOAD_FORM && (
<Grid container display="flex" justifyContent="center">
<Grid item xs={12} sm={12} xl={8} m={3} p={3} mt={6} sx={{border: "1px solid gray", borderRadius: "1rem"}}>
<Box mx={2} mt={-6} p={1} sx={{width: "fit-content", borderColor: "rgb(70%, 70%, 70%)", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "#FFFFFF"}} width="initial" color="white">
Download
</Box>
<Box display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="bold" onClick={() => download(`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`, processValues.downloadFileName)} sx={{cursor: "pointer"}}>
<Box display="flex" alignItems="center" gap={1} py={1} pr={2}>
<Icon fontSize="large">download_for_offline</Icon>
{processValues.downloadFileName}
</Box>
</MDTypography>
</Box>
</Grid> </Grid>
</Grid> )
) }
} {
{ component.type === QComponentType.VALIDATION_REVIEW_SCREEN && (
component.type === QComponentType.VALIDATION_REVIEW_SCREEN && ( <ValidationReview
<ValidationReview qInstance={qInstance}
qInstance={qInstance} process={processMetaData}
process={processMetaData} table={tableMetaData}
table={tableMetaData} processValues={processValues}
processValues={processValues} step={step}
step={step} previewRecords={records}
previewRecords={records} formValues={formData.values}
formValues={formData.values} doFullValidationRadioChangedHandler={(event: any) =>
doFullValidationRadioChangedHandler={(event: any) => {
{ const {value} = event.currentTarget;
const {value} = event.currentTarget;
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
// call the formik function to set the value in this field. // // call the formik function to set the value in this field. //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
setFieldValue("doFullValidation", value); setFieldValue("doFullValidation", value);
setOverrideOnLastStep(value !== "true"); setOverrideOnLastStep(value !== "true");
}} }}
/> />
) )
} }
{ {
component.type === QComponentType.PROCESS_SUMMARY_RESULTS && ( component.type === QComponentType.PROCESS_SUMMARY_RESULTS && (
<ProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} /> <ProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} />
) )
} }
{ {
component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && ( component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && (
// todo - make these booleans configurable (values on the component) // todo - make these booleans configurable (values on the component)
<GoogleDriveFolderPickerWrapper showSharedDrivesView={true} showDefaultFoldersView={false} qInstance={qInstance} /> <GoogleDriveFolderPickerWrapper showSharedDrivesView={true} showDefaultFoldersView={false} qInstance={qInstance} />
) )
} }
{ {
component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && ( component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && (
<div> <div>
<MDTypography variant="button" fontWeight="bold">Records</MDTypography> <MDTypography variant="button" fontWeight="bold">Records</MDTypography>
{" "} {" "}
<br /> <br />
<Box height="100%"> <Box height="100%">
<DataGridPro <DataGridPro
components={{Pagination: CustomPagination}} components={{Pagination: CustomPagination}}
page={recordConfig.pageNo} page={recordConfig.pageNo}
disableSelectionOnClick disableSelectionOnClick
autoHeight autoHeight
rows={recordConfig.rows} rows={recordConfig.rows}
columns={recordConfig.columns} columns={recordConfig.columns}
rowBuffer={10} rowBuffer={10}
rowCount={recordConfig.totalRecords} rowCount={recordConfig.totalRecords}
pageSize={recordConfig.rowsPerPage} pageSize={recordConfig.rowsPerPage}
rowsPerPageOptions={[10, 25, 50]} rowsPerPageOptions={[10, 25, 50]}
onPageSizeChange={recordConfig.handleRowsPerPageChange} onPageSizeChange={recordConfig.handleRowsPerPageChange}
onPageChange={recordConfig.handlePageChange} onPageChange={recordConfig.handlePageChange}
onRowClick={recordConfig.handleRowClick} onRowClick={recordConfig.handleRowClick}
getRowId={(row) => row.__idForDataGridPro__} getRowId={(row) => row.__idForDataGridPro__}
paginationMode="server" paginationMode="server"
pagination pagination
density="compact" density="compact"
loading={recordConfig.loading} loading={recordConfig.loading}
disableColumnFilter disableColumnFilter
/> />
</Box>
</div>
)
}
{
component.type === QComponentType.HTML && (
processValues[`${step.name}.html`] &&
<Box fontSize="1rem">
{parse(processValues[`${step.name}.html`])}
</Box> </Box>
</div> )
) }
} </div>
{ );
component.type === QComponentType.HTML && ( }))
processValues[`${step.name}.html`] && }
<Box fontSize="1rem">
{parse(processValues[`${step.name}.html`])}
</Box>
)
}
</div>
)))}
</> </>
); );
}; };

View File

@ -34,12 +34,8 @@ function EntityCreate({table}: Props): JSX.Element
{ {
return ( return (
<BaseLayout> <BaseLayout>
<Box mt={4}> <Box mb={3}>
<Grid container spacing={3}> <EntityForm table={table} />
<Grid item xs={12} lg={12}>
<EntityForm table={table} />
</Grid>
</Grid>
</Box> </Box>
</BaseLayout> </BaseLayout>
); );

View File

@ -43,18 +43,8 @@ function EntityEdit({table, isCopy}: Props): JSX.Element
return ( return (
<BaseLayout> <BaseLayout>
<Box mt={4}> <Box mb={3}>
<Grid container spacing={3}> <EntityForm table={table} id={id} isCopy={isCopy} />
<Grid item xs={12} lg={12}>
<Box mb={3}>
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityForm table={table} id={id} isCopy={isCopy} />
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
</Box> </Box>
</BaseLayout> </BaseLayout>
); );

View File

@ -1118,7 +1118,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
exportWindow.document.write(`<html lang="en"> exportWindow.document.write(`<html lang="en">
<head> <head>
<style> <style>
* { font-family: "Roboto","Helvetica","Arial",sans-serif; } * { font-family: "SF Pro Display","Roboto","Helvetica","Arial",sans-serif; }
</style> </style>
<title>${filename}</title> <title>${filename}</title>
<script> <script>

View File

@ -45,13 +45,16 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal"; import Modal from "@mui/material/Modal";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import React, {useContext, useEffect, useState} from "react"; import React, {useContext, useEffect, useState} from "react";
import {useLocation, useNavigate, useParams} from "react-router-dom"; import {useLocation, useNavigate, useParams} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import AuditBody from "qqq/components/audits/AuditBody"; import AuditBody from "qqq/components/audits/AuditBody";
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons"; import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons";
import EntityForm from "qqq/components/forms/EntityForm"; import EntityForm from "qqq/components/forms/EntityForm";
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog"; import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
@ -98,6 +101,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [metaData, setMetaData] = useState(null as QInstance); const [metaData, setMetaData] = useState(null as QInstance);
const [record, setRecord] = useState(null as QRecord); const [record, setRecord] = useState(null as QRecord);
const [tableSections, setTableSections] = useState([] as QTableSection[]); const [tableSections, setTableSections] = useState([] as QTableSection[]);
const [t1Section, setT1Section] = useState(null as QTableSection);
const [t1SectionName, setT1SectionName] = useState(null as string); const [t1SectionName, setT1SectionName] = useState(null as string);
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element); const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]); const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]);
@ -117,7 +121,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen} = useContext(QContext); const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive} = useContext(QContext);
if (localStorage.getItem(tableVariantLocalStorageKey)) if (localStorage.getItem(tableVariantLocalStorageKey))
{ {
@ -351,6 +355,23 @@ function RecordView({table, launchProcess}: Props): JSX.Element
return (visibleJoinTables); return (visibleJoinTables);
}; };
/*******************************************************************************
** get an element (or empty) to use as help content for a section
*******************************************************************************/
const getSectionHelp = (section: QTableSection) =>
{
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
const formattedHelpContent = <HelpContent helpContents={section.helpContents} roles={helpRoles} helpContentKey={`table:${tableName};section:${section.name}`} />;
return formattedHelpContent && (
<Box px={"1.5rem"} fontSize={"0.875rem"} color={colors.blueGray.main}>
{formattedHelpContent}
</Box>
)
}
if (!asyncLoadInited) if (!asyncLoadInited)
{ {
setAsyncLoadInited(true); setAsyncLoadInited(true);
@ -502,15 +523,24 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{ {
let [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName); let [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
let label = field.label; let label = field.label;
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
const showHelp = helpHelpActive || hasHelpContent(field.helpContents, helpRoles);
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={label} helpContentKey={`table:${tableName};field:${fieldName}`} />;
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default"}}>{label}:</Typography>
return ( return (
<Box key={fieldName} flexDirection="row" pr={2}> <Box key={fieldName} flexDirection="row" pr={2}>
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)"> <>
{label}: {
showHelp && formattedHelpContent ? <Tooltip title={formattedHelpContent}>{labelElement}</Tooltip> : labelElement
}
<div style={{display: "inline-block", width: 0}}>&nbsp;</div> <div style={{display: "inline-block", width: 0}}>&nbsp;</div>
</Typography> <Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)"> {ValueUtils.getDisplayValue(field, record, "view", fieldName)}
{ValueUtils.getDisplayValue(field, record, "view", fieldName)} </Typography>
</Typography> </>
</Box> </Box>
) )
}) })
@ -531,6 +561,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
<Typography variant="h6" p={3} pb={1}> <Typography variant="h6" p={3} pb={1}>
{section.label} {section.label}
</Typography> </Typography>
{getSectionHelp(section)}
<Box p={3} pt={0} flexDirection="column"> <Box p={3} pt={0} flexDirection="column">
{fields} {fields}
</Box> </Box>
@ -549,6 +580,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{ {
setT1SectionElement(sectionFieldElements.get(section.name)); setT1SectionElement(sectionFieldElements.get(section.name));
setT1SectionName(section.name); setT1SectionName(section.name);
setT1Section(section);
} }
else else
{ {
@ -879,6 +911,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{renderActionsMenu} {renderActionsMenu}
</Box> </Box>
</Box> </Box>
{t1Section && getSectionHelp(t1Section)}
{t1SectionElement ? (<Box p={3} pt={0}>{t1SectionElement}</Box>) : null} {t1SectionElement ? (<Box p={3} pt={0}>{t1SectionElement}</Box>) : null}
</Card> </Card>
</Grid> </Grid>

View File

@ -100,9 +100,12 @@
} }
/* move the green check / red x down to align with the calendar icon */ /* move the green check / red x down to align with the calendar icon */
.MuiFormControl-root .MuiFormControl-root:has(input[type="datetime-local"]),
.MuiFormControl-root:has(input[type="date"]),
.MuiFormControl-root:has(input[type="time"]),
.MuiFormControl-root:has(.MuiInputBase-inputAdornedEnd)
{ {
background-position-y: 1.4rem !important; background-position: right 2rem center;
} }
.MuiInputAdornment-sizeMedium * .MuiInputAdornment-sizeMedium *
@ -565,13 +568,32 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
right: .5rem right: .5rem
} }
.hideScrollbars::-webkit-scrollbar { /* help-content */
background: transparent; /* Chrome/Safari/Webkit */ .helpContent
width: 0; {
color: #757575;
} }
.hideScrollbars { .helpContent .header
padding-right: 8px; /* pad-right for about half the width of a scrollbar.. */ {
scrollbar-width: none; /* Firefox */ color: #212121;
-ms-overflow-style: none; /* IE 10+ */ font-weight: 500;
display: block;
margin-bottom: 0.25rem;
}
.MuiTooltip-tooltip .helpContent P + P
{
margin-top: 1rem;
}
.helpContent UL
{
margin-left: 1rem;
}
/* for query screen column-header tooltips, move them up a little bit, to be more closely attached to the text. */
.dataGridHeaderTooltip
{
top: -1.25rem;
} }

View File

@ -25,10 +25,13 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import {GridColDef, GridFilterItem, GridRowsProp} from "@mui/x-data-grid-pro"; import {GridColDef, GridFilterItem, GridRowsProp} from "@mui/x-data-grid-pro";
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator"; import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
import {GridColumnHeaderParams} from "@mui/x-data-grid/models/params/gridColumnHeaderParams";
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import {buildQGridPvsOperators, QGridBlobOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators"; import {buildQGridPvsOperators, QGridBlobOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
@ -310,6 +313,20 @@ export default class DataGridUtils
(cellValues.value) (cellValues.value)
); );
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here?
if(showHelp)
{
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={headerName} helpContentKey={`table:${tableMetaData.name};field:${fieldName}`} />;
column.renderHeader = (params: GridColumnHeaderParams) => (
<Tooltip title={formattedHelpContent}>
<div className="MuiDataGrid-columnHeaderTitle" style={{lineHeight: "initial"}}>
{headerName}
</div>
</Tooltip>
);
}
return (column); return (column);
} }

View File

@ -134,7 +134,7 @@ export default class HtmlUtils
openInWindow.document.write(`<html lang="en"> openInWindow.document.write(`<html lang="en">
<head> <head>
<style> <style>
* { font-family: "Roboto","Helvetica","Arial",sans-serif; } * { font-family: "SF Pro Display","Roboto","Helvetica","Arial",sans-serif; }
</style> </style>
<title>${filename}</title> <title>${filename}</title>
<script> <script>

View File

@ -46,7 +46,7 @@ public class QBaseSeleniumTest
String headless = System.getenv("QQQ_SELENIUM_HEADLESS"); String headless = System.getenv("QQQ_SELENIUM_HEADLESS");
if("true".equals(headless)) if("true".equals(headless))
{ {
chromeOptions.setHeadless(true); chromeOptions.addArguments("--headless=new");
} }
WebDriverManager.chromiumdriver().setup(); WebDriverManager.chromiumdriver().setup();

View File

@ -1,3 +1,24 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.materialdashboard.lib; package com.kingsrook.qqq.materialdashboard.lib;
@ -7,7 +28,7 @@ package com.kingsrook.qqq.materialdashboard.lib;
public interface QQQMaterialDashboardSelectors public interface QQQMaterialDashboardSelectors
{ {
String SIDEBAR_ITEM = ".MuiDrawer-paperAnchorDockedLeft li.MuiListItem-root"; String SIDEBAR_ITEM = ".MuiDrawer-paperAnchorDockedLeft li.MuiListItem-root";
String BREADCRUMB_HEADER = ".MuiToolbar-root h5"; String BREADCRUMB_HEADER = ".MuiToolbar-root h3";
String QUERY_GRID_CELL = ".MuiDataGrid-root .MuiDataGrid-cellContent"; String QUERY_GRID_CELL = ".MuiDataGrid-root .MuiDataGrid-cellContent";
String QUERY_FILTER_INPUT = ".customFilterPanel input.MuiInput-input"; String QUERY_FILTER_INPUT = ".customFilterPanel input.MuiInput-input";

View File

@ -30,7 +30,6 @@ import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -95,8 +94,8 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
.click(); .click();
qSeleniumLib.waitForCondition("Should have downloaded 1 file", () -> getDownloadedFiles().size() == 1); qSeleniumLib.waitForCondition("Should have downloaded 1 file", () -> getDownloadedFiles().size() == 1);
qSeleniumLib.waitForCondition("Expected file name", () -> getDownloadedFiles().get(0).getName().matches("Sample Table Widget.*.csv"));
File csvFile = getDownloadedFiles().get(0); File csvFile = getDownloadedFiles().get(0);
assertThat(csvFile.getName()).matches("Sample Table Widget.*.csv");
String fileContents = FileUtils.readFileToString(csvFile, StandardCharsets.UTF_8); String fileContents = FileUtils.readFileToString(csvFile, StandardCharsets.UTF_8);
assertEquals(""" assertEquals("""
"Id","Name" "Id","Name"
@ -105,7 +104,7 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
"3","Bart J." "3","Bart J."
""", fileContents); """, fileContents);
// qSeleniumLib.waitForever(); qSeleniumLib.waitForever();
} }
} }

View File

@ -110,7 +110,7 @@ public class SavedFiltersTest extends QBaseSeleniumTest
////////////////////// //////////////////////
qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filter").click(); qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filter").click();
addQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or"); addQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
qSeleniumLib.waitForSelectorContaining("H5", "Person").click(); qSeleniumLib.waitForSelectorContaining("H3", "Person").click();
qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People") qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People")
.findElement(By.cssSelector("CIRCLE")); .findElement(By.cssSelector("CIRCLE"));
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "2"); qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "2");