CE-752 Add help content concept to QQQ (fields and table sections at this time); redesign form fields (borders now)

This commit is contained in:
2023-12-07 11:59:28 -06:00
parent c94f518422
commit adb2b4613d
17 changed files with 595 additions and 285 deletions

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

@ -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([]);
@ -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),

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

@ -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.5rem">{formattedHelpContent}</Box>
} }
const labelElement = <Box fontSize="1rem" fontWeight="500">
<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";
@ -76,6 +77,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
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 +232,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>
); );
@ -250,7 +252,22 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
<Box> <Box>
<Autocomplete <Autocomplete
id={overrideId ?? fieldName} id={overrideId ?? fieldName}
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}} sx={{
"& .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
}
}}
open={open} open={open}
fullWidth fullWidth
onOpen={() => onOpen={() =>
@ -305,7 +322,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
<TextField <TextField
{...params} {...params}
label={fieldLabel} label={fieldLabel}
variant="standard" variant="outlined"
autoComplete="off" autoComplete="off"
type="search" type="search"
InputProps={{ InputProps={{
@ -341,6 +358,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));
@ -552,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 = (
@ -573,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 &&
@ -627,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>
@ -644,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

@ -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

@ -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

@ -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

@ -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 *
@ -564,3 +567,33 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
display: inline; display: inline;
right: .5rem right: .5rem
} }
/* help-content */
.helpContent
{
color: #757575;
}
.helpContent .header
{
color: #212121;
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);
} }