mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
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:
@ -6,7 +6,7 @@
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.83",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.85",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
@ -42,6 +42,7 @@
|
||||
"react-dom": "18.0.0",
|
||||
"react-github-btn": "1.2.1",
|
||||
"react-google-drive-picker": "^1.2.0",
|
||||
"react-markdown": "9.0.1",
|
||||
"react-router-dom": "6.2.1",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-table": "7.7.0",
|
||||
|
@ -36,7 +36,7 @@ import {LicenseInfo} from "@mui/x-license-pro";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
||||
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 CommandMenu from "CommandMenu";
|
||||
import QContext from "QContext";
|
||||
@ -226,6 +226,7 @@ export default function App()
|
||||
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
|
||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||
const {pathname} = useLocation();
|
||||
const [queryParams] = useSearchParams();
|
||||
|
||||
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
|
||||
const [sideNavRoutes, setSideNavRoutes] = useState([]);
|
||||
@ -659,6 +660,8 @@ export default function App()
|
||||
const [tableProcesses, setTableProcesses] = useState(null);
|
||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||
|
||||
return (
|
||||
|
||||
appRoutes && (
|
||||
@ -669,6 +672,7 @@ export default function App()
|
||||
tableProcesses: tableProcesses,
|
||||
dotMenuOpen: dotMenuOpen,
|
||||
keyboardHelpOpen: keyboardHelpOpen,
|
||||
helpHelpActive: helpHelpActive,
|
||||
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
|
||||
setAccentColor: (accentColor: string) => setAccentColor(accentColor),
|
||||
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
|
||||
@ -700,4 +704,4 @@ export default function App()
|
||||
</QContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -51,6 +51,7 @@ interface QContext
|
||||
///////////////////////////////////
|
||||
pathToLabelMap?: {[path: string]: string};
|
||||
branding?: QBrandingMetaData;
|
||||
helpHelpActive?: boolean;
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
@ -59,6 +60,7 @@ const defaultState = {
|
||||
dotMenuOpen: false,
|
||||
keyboardHelpOpen: false,
|
||||
pathToLabelMap: {},
|
||||
helpHelpActive: false,
|
||||
};
|
||||
|
||||
const QContext = createContext<QContext>(defaultState);
|
||||
|
@ -29,8 +29,8 @@ import React, {SyntheticEvent} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
const AntSwitch = styled(Switch)(({theme}) => ({
|
||||
width: 28,
|
||||
height: 16,
|
||||
width: 32,
|
||||
height: 20,
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
"&:active": {
|
||||
@ -54,18 +54,19 @@ const AntSwitch = styled(Switch)(({theme}) => ({
|
||||
},
|
||||
"& .MuiSwitch-thumb": {
|
||||
boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)",
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
transition: theme.transitions.create([ "width" ], {
|
||||
duration: 200,
|
||||
}),
|
||||
},
|
||||
"&.nullSwitch .MuiSwitch-thumb": {
|
||||
width: 24,
|
||||
width: 28,
|
||||
},
|
||||
"& .MuiSwitch-track": {
|
||||
borderRadius: 16 / 2,
|
||||
height: 20,
|
||||
borderRadius: 20 / 2,
|
||||
opacity: 1,
|
||||
backgroundColor:
|
||||
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 (
|
||||
<Box bgcolor={isDisabled ? colors.grey[200] : ""}>
|
||||
<InputLabel shrink={true}>{label}</InputLabel>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Stack direction="row" spacing={1} alignItems="center" height="37px">
|
||||
<Typography
|
||||
fontSize="0.875rem"
|
||||
fontSize="1rem"
|
||||
color={value === false ? "auto" : "#bfbfbf" }
|
||||
onClick={(e) => setSwitch(e, false)}
|
||||
sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}>
|
||||
@ -116,7 +117,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
||||
</Typography>
|
||||
<AntSwitch className={classNullSwitch} name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||
<Typography
|
||||
fontSize="0.875rem"
|
||||
fontSize="1rem"
|
||||
color={value === true ? "auto" : "#bfbfbf"}
|
||||
onClick={(e) => setSwitch(e, true)}
|
||||
sx={{cursor: value === true || isDisabled ? "inherit" : "pointer"}}>
|
||||
|
@ -32,6 +32,7 @@ import React, {useState} from "react";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import HelpContent from "qqq/components/misc/HelpContent";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
interface Props
|
||||
@ -41,16 +42,13 @@ interface Props
|
||||
bulkEditMode?: boolean;
|
||||
bulkEditSwitchChangeHandler?: any;
|
||||
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 {
|
||||
formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler,
|
||||
} = props;
|
||||
const {
|
||||
formFields, values, errors, touched,
|
||||
} = formData;
|
||||
const {formFields, values, errors, touched} = formData;
|
||||
|
||||
const formikProps = useFormikContext();
|
||||
const [fileName, setFileName] = useState(null as string);
|
||||
@ -70,8 +68,8 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
{
|
||||
setFileName(null);
|
||||
formikProps.setFieldValue(fieldName, null);
|
||||
props.record?.values.delete(fieldName)
|
||||
props.record?.displayValues.delete(fieldName)
|
||||
record?.values.delete(fieldName)
|
||||
record?.displayValues.delete(fieldName)
|
||||
};
|
||||
|
||||
const bulkEditSwitchChanged = (name: string, value: boolean) =>
|
||||
@ -79,6 +77,7 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
bulkEditSwitchChangeHandler(name, value);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box lineHeight={0}>
|
||||
@ -96,29 +95,38 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
&& Object.keys(formFields).map((fieldName: any) =>
|
||||
{
|
||||
const field = formFields[fieldName];
|
||||
if (field.omitFromQDynamicForm)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (values[fieldName] === undefined)
|
||||
{
|
||||
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")
|
||||
{
|
||||
const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB});
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
<Box mb={1.5}>
|
||||
|
||||
<InputLabel shrink={true}>{field.label}</InputLabel>
|
||||
{labelElement}
|
||||
{
|
||||
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:
|
||||
<Box display="inline-flex" pl={1}>
|
||||
{ValueUtils.getDisplayValue(pseudoField, props.record, "view")}
|
||||
{ValueUtils.getDisplayValue(pseudoField, record, "view")}
|
||||
<Tooltip placement="bottom" title="Remove current file">
|
||||
<Icon className="blobIcon" fontSize="small" onClick={(e) => removeFile(fieldName)}>delete</Icon>
|
||||
</Tooltip>
|
||||
@ -162,18 +170,20 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
{labelElement}
|
||||
<DynamicSelect
|
||||
tableName={field.possibleValueProps.tableName}
|
||||
processName={field.possibleValueProps.processName}
|
||||
fieldName={fieldName}
|
||||
isEditable={field.isEditable}
|
||||
fieldLabel={field.label}
|
||||
fieldLabel=""
|
||||
initialValue={values[fieldName]}
|
||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
||||
bulkEditMode={bulkEditMode}
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
otherValues={otherValuesMap}
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@ -182,9 +192,11 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
// todo? placeholder={password.placeholder}
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
{labelElement}
|
||||
<QDynamicFormField
|
||||
id={field.name}
|
||||
type={field.type}
|
||||
label={field.label}
|
||||
label=""
|
||||
isEditable={field.isEditable}
|
||||
name={fieldName}
|
||||
displayFormat={field.displayFormat}
|
||||
@ -195,6 +207,7 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
|
||||
formFieldObject={field}
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
@ -207,6 +220,7 @@ function QDynamicForm(props: Props): JSX.Element
|
||||
QDynamicForm.defaultProps = {
|
||||
formLabel: undefined,
|
||||
bulkEditMode: false,
|
||||
helpRoles: ["ALL_SCREENS"],
|
||||
bulkEditSwitchChangeHandler: () =>
|
||||
{
|
||||
},
|
||||
|
@ -25,6 +25,7 @@ import Switch from "@mui/material/Switch";
|
||||
import {ErrorMessage, Field, useFormikContext} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
@ -52,6 +53,7 @@ function QDynamicFormField({
|
||||
{
|
||||
const [switchChecked, setSwitchChecked] = useState(false);
|
||||
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
|
||||
const {inputBorderColor} = colors;
|
||||
|
||||
const {setFieldValue} = useFormikContext();
|
||||
|
||||
@ -122,7 +124,7 @@ function QDynamicFormField({
|
||||
width="100%"
|
||||
height="300px"
|
||||
value={value}
|
||||
style={{border: "1px solid gray"}}
|
||||
style={{border: `1px solid ${inputBorderColor}`, borderRadius: "0.75rem"}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -131,7 +133,7 @@ function QDynamicFormField({
|
||||
{
|
||||
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) =>
|
||||
{
|
||||
if (e.key === "Enter")
|
||||
@ -171,6 +173,14 @@ function QDynamicFormField({
|
||||
id={`bulkEditSwitch-${name}`}
|
||||
checked={switchChecked}
|
||||
onClick={bulkEditSwitchChanged}
|
||||
sx={{top: "-4px",
|
||||
"& .MuiSwitch-track": {
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
top: -3,
|
||||
position: "relative"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box width="100%" sx={{background: (type == "checkbox" && isDisabled) ? "#f0f2f5!important" : "initial"}}>
|
||||
|
@ -89,6 +89,7 @@ class DynamicFormUtils
|
||||
label += field.isRequired ? " *" : "";
|
||||
|
||||
return ({
|
||||
fieldMetaData: field,
|
||||
name: field.name,
|
||||
label: label,
|
||||
isRequired: field.isRequired,
|
||||
|
@ -29,6 +29,7 @@ import Switch from "@mui/material/Switch";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {ErrorMessage, useFormikContext} from "formik";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
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 [searchTerm, setSearchTerm] = useState(null);
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const {inputBorderColor} = colors;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (
|
||||
<li {...props} key={option.id}>
|
||||
<li {...props} key={option.id} style={{fontSize: "1rem"}}>
|
||||
{content}
|
||||
</li>
|
||||
);
|
||||
@ -250,7 +252,22 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
<Box>
|
||||
<Autocomplete
|
||||
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}
|
||||
fullWidth
|
||||
onOpen={() =>
|
||||
@ -305,7 +322,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
<TextField
|
||||
{...params}
|
||||
label={fieldLabel}
|
||||
variant="standard"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
type="search"
|
||||
InputProps={{
|
||||
@ -341,6 +358,14 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
id={`bulkEditSwitch-${fieldName}`}
|
||||
checked={switchChecked}
|
||||
onClick={bulkEditSwitchChanged}
|
||||
sx={{top: "-4px",
|
||||
"& .MuiSwitch-track": {
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
top: -3,
|
||||
position: "relative"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
|
@ -37,10 +37,12 @@ import React, {useContext, useEffect, useReducer, useState} from "react";
|
||||
import {useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import * as Yup from "yup";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import QDynamicForm from "qqq/components/forms/DynamicForm";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import HelpContent from "qqq/components/misc/HelpContent";
|
||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
@ -79,6 +81,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
const [validations, setValidations] = useState({});
|
||||
const [initialValues, setInitialValues] = useState({} as { [key: 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 [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]);
|
||||
|
||||
@ -151,7 +154,9 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
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)
|
||||
@ -330,6 +335,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
/////////////////////////////////////
|
||||
const dynamicFormFieldsBySection = new Map<string, any>();
|
||||
let t1sectionName;
|
||||
let t1section;
|
||||
const nonT1Sections: QTableSection[] = [];
|
||||
for (let i = 0; i < tableSections.length; i++)
|
||||
{
|
||||
@ -382,6 +388,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
if (section.tier === "T1")
|
||||
{
|
||||
t1sectionName = section.name;
|
||||
t1section = section;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -389,6 +396,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
setT1SectionName(t1sectionName);
|
||||
setT1Section(t1section);
|
||||
setNonT1Sections(nonT1Sections);
|
||||
setFormFields(dynamicFormFieldsBySection);
|
||||
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`;
|
||||
|
||||
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)
|
||||
{
|
||||
body = (
|
||||
@ -573,23 +594,26 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
else
|
||||
{
|
||||
const cardElevation = props.isModal ? 3 : 1;
|
||||
const cardElevation = props.isModal ? 3 : 0;
|
||||
body = (
|
||||
<Box mb={3}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
{alertContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
{warningContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
{
|
||||
(alertContent || warningContent) &&
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
{alertContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
{warningContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
<Grid container spacing={3}>
|
||||
{
|
||||
!props.isModal &&
|
||||
@ -627,10 +651,11 @@ function EntityForm(props: Props): JSX.Element
|
||||
<MDTypography variant="h5">{formTitle}</MDTypography>
|
||||
</Box>
|
||||
</Box>
|
||||
{t1section && getSectionHelp(t1section)}
|
||||
{
|
||||
t1sectionName && formFields ? (
|
||||
<Box pb={1} px={3}>
|
||||
<Box p={3} width="100%">
|
||||
<Box px={3}>
|
||||
<Box pb={"0.25rem"} width="100%">
|
||||
{getFormSection(values, touched, formFields.get(t1sectionName), errors)}
|
||||
</Box>
|
||||
</Box>
|
||||
@ -644,8 +669,9 @@ function EntityForm(props: Props): JSX.Element
|
||||
<MDTypography variant="h6" p={3} pb={1}>
|
||||
{section.label}
|
||||
</MDTypography>
|
||||
{getSectionHelp(section)}
|
||||
<Box pb={1} px={3}>
|
||||
<Box p={3} width="100%">
|
||||
<Box pb={"0.75rem"} width="100%">
|
||||
{getFormSection(values, touched, formFields.get(section.name), errors)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -69,7 +69,15 @@ export default styled(TextField)(({theme, ownerState}: { theme?: Theme; ownerSta
|
||||
});
|
||||
|
||||
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",
|
||||
...(error && errorStyles()),
|
||||
...(success && successStyles()),
|
||||
|
139
src/qqq/components/misc/HelpContent.tsx
Normal file
139
src/qqq/components/misc/HelpContent.tsx
Normal 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;
|
@ -414,228 +414,238 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
//////////////////////////////////////////////////
|
||||
// render all of the components for this screen //
|
||||
//////////////////////////////////////////////////
|
||||
step.components && (step.components.map((component: QFrontendComponent, index: number) => (
|
||||
<div key={index}>
|
||||
{
|
||||
component.type === QComponentType.HELP_TEXT && (
|
||||
component.values.previewText ?
|
||||
<>
|
||||
<Box mt={1}>
|
||||
<Button onClick={toggleShowFullHelpText} startIcon={<Icon>{showFullHelpText ? "expand_less" : "expand_more"}</Icon>} sx={{pl: 1}}>
|
||||
{showFullHelpText ? "Hide " : "Show "}
|
||||
{component.values.previewText}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box mt={1} style={{display: showFullHelpText ? "block" : "none"}}>
|
||||
<Typography variant="body2" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
:
|
||||
<MDTypography variant="button" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</MDTypography>
|
||||
)
|
||||
}
|
||||
{
|
||||
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>
|
||||
step.components && (step.components.map((component: QFrontendComponent, index: number) =>
|
||||
{
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]
|
||||
if(component.type == QComponentType.BULK_EDIT_FORM)
|
||||
{
|
||||
helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{
|
||||
component.type === QComponentType.HELP_TEXT && (
|
||||
component.values.previewText ?
|
||||
<>
|
||||
<Box mt={1}>
|
||||
<Button onClick={toggleShowFullHelpText} startIcon={<Icon>{showFullHelpText ? "expand_less" : "expand_more"}</Icon>} sx={{pl: 1}}>
|
||||
{showFullHelpText ? "Hide " : "Show "}
|
||||
{component.values.previewText}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box mt={1} style={{display: showFullHelpText ? "block" : "none"}}>
|
||||
<Typography variant="body2" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
:
|
||||
<MDTypography variant="button" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</MDTypography>
|
||||
)
|
||||
}
|
||||
{
|
||||
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 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)
|
||||
<Grid item xs={12} lg={9}>
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionFormFields = {};
|
||||
for (let i = 0; i < section.fieldNames.length; i++)
|
||||
{
|
||||
const fieldName = section.fieldNames[i];
|
||||
if (formFields[fieldName])
|
||||
localTableSections.map((section: QTableSection, index: number) =>
|
||||
{
|
||||
// @ts-ignore
|
||||
sectionFormFields[fieldName] = formFields[fieldName];
|
||||
}
|
||||
}
|
||||
const name = section.name;
|
||||
|
||||
if (Object.keys(sectionFormFields).length > 0)
|
||||
{
|
||||
const sectionFormData = {
|
||||
formFields: sectionFormFields,
|
||||
values: values,
|
||||
errors: errors,
|
||||
touched: touched
|
||||
};
|
||||
if (section.isHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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} />
|
||||
const sectionFormFields = {};
|
||||
for (let i = 0; i < section.fieldNames.length; i++)
|
||||
{
|
||||
const fieldName = section.fieldNames[i];
|
||||
if (formFields[fieldName])
|
||||
{
|
||||
// @ts-ignore
|
||||
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>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<br />);
|
||||
}
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<br />);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.EDIT_FORM && (
|
||||
<QDynamicForm formData={formData} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
||||
<div>
|
||||
{step.viewFields.map((field: QFieldMetaData) => (
|
||||
field.hasAdornment(AdornmentType.ERROR) ? (
|
||||
processValues[field.name] && (
|
||||
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableMetaData?.name};`} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.EDIT_FORM && (
|
||||
<QDynamicForm formData={formData} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
||||
<div>
|
||||
{step.viewFields.map((field: QFieldMetaData) => (
|
||||
field.hasAdornment(AdornmentType.ERROR) ? (
|
||||
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}>
|
||||
<MDTypography variant="button" fontWeight="regular">
|
||||
<MDTypography variant="button" fontWeight="bold">
|
||||
{field.label}
|
||||
:
|
||||
</MDTypography>
|
||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
||||
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<Box key={field.name} display="flex" py={1} pr={2}>
|
||||
<MDTypography variant="button" fontWeight="bold">
|
||||
{field.label}
|
||||
:
|
||||
</MDTypography>
|
||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
||||
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")}
|
||||
)))
|
||||
}
|
||||
</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>
|
||||
)))
|
||||
}
|
||||
</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 && (
|
||||
<ValidationReview
|
||||
qInstance={qInstance}
|
||||
process={processMetaData}
|
||||
table={tableMetaData}
|
||||
processValues={processValues}
|
||||
step={step}
|
||||
previewRecords={records}
|
||||
formValues={formData.values}
|
||||
doFullValidationRadioChangedHandler={(event: any) =>
|
||||
{
|
||||
const {value} = event.currentTarget;
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VALIDATION_REVIEW_SCREEN && (
|
||||
<ValidationReview
|
||||
qInstance={qInstance}
|
||||
process={processMetaData}
|
||||
table={tableMetaData}
|
||||
processValues={processValues}
|
||||
step={step}
|
||||
previewRecords={records}
|
||||
formValues={formData.values}
|
||||
doFullValidationRadioChangedHandler={(event: any) =>
|
||||
{
|
||||
const {value} = event.currentTarget;
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// call the formik function to set the value in this field. //
|
||||
//////////////////////////////////////////////////////////////
|
||||
setFieldValue("doFullValidation", value);
|
||||
//////////////////////////////////////////////////////////////
|
||||
// call the formik function to set the value in this field. //
|
||||
//////////////////////////////////////////////////////////////
|
||||
setFieldValue("doFullValidation", value);
|
||||
|
||||
setOverrideOnLastStep(value !== "true");
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.PROCESS_SUMMARY_RESULTS && (
|
||||
<ProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && (
|
||||
// todo - make these booleans configurable (values on the component)
|
||||
<GoogleDriveFolderPickerWrapper showSharedDrivesView={true} showDefaultFoldersView={false} qInstance={qInstance} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && (
|
||||
<div>
|
||||
<MDTypography variant="button" fontWeight="bold">Records</MDTypography>
|
||||
{" "}
|
||||
<br />
|
||||
<Box height="100%">
|
||||
<DataGridPro
|
||||
components={{Pagination: CustomPagination}}
|
||||
page={recordConfig.pageNo}
|
||||
disableSelectionOnClick
|
||||
autoHeight
|
||||
rows={recordConfig.rows}
|
||||
columns={recordConfig.columns}
|
||||
rowBuffer={10}
|
||||
rowCount={recordConfig.totalRecords}
|
||||
pageSize={recordConfig.rowsPerPage}
|
||||
rowsPerPageOptions={[10, 25, 50]}
|
||||
onPageSizeChange={recordConfig.handleRowsPerPageChange}
|
||||
onPageChange={recordConfig.handlePageChange}
|
||||
onRowClick={recordConfig.handleRowClick}
|
||||
getRowId={(row) => row.__idForDataGridPro__}
|
||||
paginationMode="server"
|
||||
pagination
|
||||
density="compact"
|
||||
loading={recordConfig.loading}
|
||||
disableColumnFilter
|
||||
/>
|
||||
setOverrideOnLastStep(value !== "true");
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.PROCESS_SUMMARY_RESULTS && (
|
||||
<ProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && (
|
||||
// todo - make these booleans configurable (values on the component)
|
||||
<GoogleDriveFolderPickerWrapper showSharedDrivesView={true} showDefaultFoldersView={false} qInstance={qInstance} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && (
|
||||
<div>
|
||||
<MDTypography variant="button" fontWeight="bold">Records</MDTypography>
|
||||
{" "}
|
||||
<br />
|
||||
<Box height="100%">
|
||||
<DataGridPro
|
||||
components={{Pagination: CustomPagination}}
|
||||
page={recordConfig.pageNo}
|
||||
disableSelectionOnClick
|
||||
autoHeight
|
||||
rows={recordConfig.rows}
|
||||
columns={recordConfig.columns}
|
||||
rowBuffer={10}
|
||||
rowCount={recordConfig.totalRecords}
|
||||
pageSize={recordConfig.rowsPerPage}
|
||||
rowsPerPageOptions={[10, 25, 50]}
|
||||
onPageSizeChange={recordConfig.handleRowsPerPageChange}
|
||||
onPageChange={recordConfig.handlePageChange}
|
||||
onRowClick={recordConfig.handleRowClick}
|
||||
getRowId={(row) => row.__idForDataGridPro__}
|
||||
paginationMode="server"
|
||||
pagination
|
||||
density="compact"
|
||||
loading={recordConfig.loading}
|
||||
disableColumnFilter
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.HTML && (
|
||||
processValues[`${step.name}.html`] &&
|
||||
<Box fontSize="1rem">
|
||||
{parse(processValues[`${step.name}.html`])}
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.HTML && (
|
||||
processValues[`${step.name}.html`] &&
|
||||
<Box fontSize="1rem">
|
||||
{parse(processValues[`${step.name}.html`])}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)))}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}))
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -34,12 +34,8 @@ function EntityCreate({table}: Props): JSX.Element
|
||||
{
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Box mt={4}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} lg={12}>
|
||||
<EntityForm table={table} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box mb={3}>
|
||||
<EntityForm table={table} />
|
||||
</Box>
|
||||
</BaseLayout>
|
||||
);
|
||||
|
@ -43,18 +43,8 @@ function EntityEdit({table, isCopy}: Props): JSX.Element
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Box mt={4}>
|
||||
<Grid container spacing={3}>
|
||||
<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 mb={3}>
|
||||
<EntityForm table={table} id={id} isCopy={isCopy} />
|
||||
</Box>
|
||||
</BaseLayout>
|
||||
);
|
||||
|
@ -45,13 +45,16 @@ import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import AuditBody from "qqq/components/audits/AuditBody";
|
||||
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import EntityForm from "qqq/components/forms/EntityForm";
|
||||
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
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 [record, setRecord] = useState(null as QRecord);
|
||||
const [tableSections, setTableSections] = useState([] as QTableSection[]);
|
||||
const [t1Section, setT1Section] = useState(null as QTableSection);
|
||||
const [t1SectionName, setT1SectionName] = useState(null as string);
|
||||
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
|
||||
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 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))
|
||||
{
|
||||
@ -351,6 +355,23 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
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)
|
||||
{
|
||||
setAsyncLoadInited(true);
|
||||
@ -502,15 +523,24 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
let [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||
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 (
|
||||
<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}}> </div>
|
||||
</Typography>
|
||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
||||
</Typography>
|
||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
||||
</Typography>
|
||||
</>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@ -531,6 +561,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
<Typography variant="h6" p={3} pb={1}>
|
||||
{section.label}
|
||||
</Typography>
|
||||
{getSectionHelp(section)}
|
||||
<Box p={3} pt={0} flexDirection="column">
|
||||
{fields}
|
||||
</Box>
|
||||
@ -549,6 +580,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
setT1SectionElement(sectionFieldElements.get(section.name));
|
||||
setT1SectionName(section.name);
|
||||
setT1Section(section);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -879,6 +911,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{renderActionsMenu}
|
||||
</Box>
|
||||
</Box>
|
||||
{t1Section && getSectionHelp(t1Section)}
|
||||
{t1SectionElement ? (<Box p={3} pt={0}>{t1SectionElement}</Box>) : null}
|
||||
</Card>
|
||||
</Grid>
|
||||
|
@ -100,9 +100,12 @@
|
||||
}
|
||||
|
||||
/* 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 *
|
||||
@ -564,3 +567,33 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
display: inline;
|
||||
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;
|
||||
}
|
@ -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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
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 {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
||||
import {GridColumnHeaderParams} from "@mui/x-data-grid/models/params/gridColumnHeaderParams";
|
||||
import React from "react";
|
||||
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 ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
@ -310,6 +313,20 @@ export default class DataGridUtils
|
||||
(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);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user