mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
Merged main into feature/select-text-on-child-tables
This commit is contained in:
@ -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",
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
||||||
|
@ -48,7 +48,7 @@ const tooltip: Types = {
|
|||||||
borderRadius: borderRadius.md,
|
borderRadius: borderRadius.md,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
padding: "1rem",
|
padding: "1rem",
|
||||||
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 3px -2px, rgba(0, 0, 0, 0.14) 0px 3px 4px 0px, rgba(0, 0, 0, 0.12) 0px 1px 8px 0px"
|
boxShadow: "0px 0px 12px rgba(128, 128, 128, 0.40)"
|
||||||
},
|
},
|
||||||
|
|
||||||
arrow: {
|
arrow: {
|
||||||
|
@ -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"}}>
|
||||||
|
@ -32,6 +32,7 @@ import React, {useState} from "react";
|
|||||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
import HelpContent from "qqq/components/misc/HelpContent";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
@ -41,16 +42,13 @@ interface Props
|
|||||||
bulkEditMode?: boolean;
|
bulkEditMode?: boolean;
|
||||||
bulkEditSwitchChangeHandler?: any;
|
bulkEditSwitchChangeHandler?: any;
|
||||||
record?: QRecord;
|
record?: QRecord;
|
||||||
|
helpRoles?: string[];
|
||||||
|
helpContentKeyPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function QDynamicForm(props: Props): JSX.Element
|
function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler, record, helpRoles, helpContentKeyPrefix}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const {
|
const {formFields, values, errors, touched} = formData;
|
||||||
formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler,
|
|
||||||
} = props;
|
|
||||||
const {
|
|
||||||
formFields, values, errors, touched,
|
|
||||||
} = formData;
|
|
||||||
|
|
||||||
const formikProps = useFormikContext();
|
const formikProps = useFormikContext();
|
||||||
const [fileName, setFileName] = useState(null as string);
|
const [fileName, setFileName] = useState(null as string);
|
||||||
@ -70,8 +68,8 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
setFileName(null);
|
setFileName(null);
|
||||||
formikProps.setFieldValue(fieldName, null);
|
formikProps.setFieldValue(fieldName, null);
|
||||||
props.record?.values.delete(fieldName)
|
record?.values.delete(fieldName)
|
||||||
props.record?.displayValues.delete(fieldName)
|
record?.displayValues.delete(fieldName)
|
||||||
};
|
};
|
||||||
|
|
||||||
const bulkEditSwitchChanged = (name: string, value: boolean) =>
|
const bulkEditSwitchChanged = (name: string, value: boolean) =>
|
||||||
@ -79,6 +77,7 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
bulkEditSwitchChangeHandler(name, value);
|
bulkEditSwitchChangeHandler(name, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box lineHeight={0}>
|
<Box lineHeight={0}>
|
||||||
@ -96,29 +95,38 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
&& Object.keys(formFields).map((fieldName: any) =>
|
&& Object.keys(formFields).map((fieldName: any) =>
|
||||||
{
|
{
|
||||||
const field = formFields[fieldName];
|
const field = formFields[fieldName];
|
||||||
|
if (field.omitFromQDynamicForm)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (values[fieldName] === undefined)
|
if (values[fieldName] === undefined)
|
||||||
{
|
{
|
||||||
values[fieldName] = "";
|
values[fieldName] = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.omitFromQDynamicForm)
|
let formattedHelpContent = <HelpContent helpContents={field.fieldMetaData.helpContents} roles={helpRoles} helpContentKey={`${helpContentKeyPrefix ?? ""}field:${fieldName}`} />;
|
||||||
|
if(formattedHelpContent)
|
||||||
{
|
{
|
||||||
return null;
|
formattedHelpContent = <Box color="#757575" fontSize="0.875rem" mt="-0.25rem">{formattedHelpContent}</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const labelElement = <Box fontSize="1rem" fontWeight="500" marginBottom="0.25rem">
|
||||||
|
<label htmlFor={field.name}>{field.label}</label>
|
||||||
|
</Box>
|
||||||
|
|
||||||
if (field.type === "file")
|
if (field.type === "file")
|
||||||
{
|
{
|
||||||
const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB});
|
const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB});
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} sm={6} key={fieldName}>
|
<Grid item xs={12} sm={6} key={fieldName}>
|
||||||
<Box mb={1.5}>
|
<Box mb={1.5}>
|
||||||
|
{labelElement}
|
||||||
<InputLabel shrink={true}>{field.label}</InputLabel>
|
|
||||||
{
|
{
|
||||||
props.record && props.record.values.get(fieldName) && <Box fontSize="0.875rem" pb={1}>
|
record && record.values.get(fieldName) && <Box fontSize="0.875rem" pb={1}>
|
||||||
Current File:
|
Current File:
|
||||||
<Box display="inline-flex" pl={1}>
|
<Box display="inline-flex" pl={1}>
|
||||||
{ValueUtils.getDisplayValue(pseudoField, props.record, "view")}
|
{ValueUtils.getDisplayValue(pseudoField, record, "view")}
|
||||||
<Tooltip placement="bottom" title="Remove current file">
|
<Tooltip placement="bottom" title="Remove current file">
|
||||||
<Icon className="blobIcon" fontSize="small" onClick={(e) => removeFile(fieldName)}>delete</Icon>
|
<Icon className="blobIcon" fontSize="small" onClick={(e) => removeFile(fieldName)}>delete</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -162,18 +170,20 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} sm={6} key={fieldName}>
|
<Grid item xs={12} sm={6} key={fieldName}>
|
||||||
|
{labelElement}
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={field.possibleValueProps.tableName}
|
tableName={field.possibleValueProps.tableName}
|
||||||
processName={field.possibleValueProps.processName}
|
processName={field.possibleValueProps.processName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
isEditable={field.isEditable}
|
isEditable={field.isEditable}
|
||||||
fieldLabel={field.label}
|
fieldLabel=""
|
||||||
initialValue={values[fieldName]}
|
initialValue={values[fieldName]}
|
||||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
||||||
bulkEditMode={bulkEditMode}
|
bulkEditMode={bulkEditMode}
|
||||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||||
otherValues={otherValuesMap}
|
otherValues={otherValuesMap}
|
||||||
/>
|
/>
|
||||||
|
{formattedHelpContent}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -182,9 +192,11 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
// todo? placeholder={password.placeholder}
|
// todo? placeholder={password.placeholder}
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} sm={6} key={fieldName}>
|
<Grid item xs={12} sm={6} key={fieldName}>
|
||||||
|
{labelElement}
|
||||||
<QDynamicFormField
|
<QDynamicFormField
|
||||||
|
id={field.name}
|
||||||
type={field.type}
|
type={field.type}
|
||||||
label={field.label}
|
label=""
|
||||||
isEditable={field.isEditable}
|
isEditable={field.isEditable}
|
||||||
name={fieldName}
|
name={fieldName}
|
||||||
displayFormat={field.displayFormat}
|
displayFormat={field.displayFormat}
|
||||||
@ -195,6 +207,7 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
|
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
|
||||||
formFieldObject={field}
|
formFieldObject={field}
|
||||||
/>
|
/>
|
||||||
|
{formattedHelpContent}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -207,6 +220,7 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
QDynamicForm.defaultProps = {
|
QDynamicForm.defaultProps = {
|
||||||
formLabel: undefined,
|
formLabel: undefined,
|
||||||
bulkEditMode: false,
|
bulkEditMode: false,
|
||||||
|
helpRoles: ["ALL_SCREENS"],
|
||||||
bulkEditSwitchChangeHandler: () =>
|
bulkEditSwitchChangeHandler: () =>
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
|
@ -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"}}>
|
||||||
|
@ -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,
|
||||||
|
@ -29,6 +29,7 @@ import Switch from "@mui/material/Switch";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import {ErrorMessage, useFormikContext} from "formik";
|
import {ErrorMessage, useFormikContext} from "formik";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ interface Props
|
|||||||
bulkEditMode?: boolean;
|
bulkEditMode?: boolean;
|
||||||
bulkEditSwitchChangeHandler?: any;
|
bulkEditSwitchChangeHandler?: any;
|
||||||
otherValues?: Map<string, any>;
|
otherValues?: Map<string, any>;
|
||||||
|
variant: "standard" | "outlined";
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicSelect.defaultProps = {
|
DynamicSelect.defaultProps = {
|
||||||
@ -63,6 +65,7 @@ DynamicSelect.defaultProps = {
|
|||||||
isMultiple: false,
|
isMultiple: false,
|
||||||
bulkEditMode: false,
|
bulkEditMode: false,
|
||||||
otherValues: new Map<string, any>(),
|
otherValues: new Map<string, any>(),
|
||||||
|
variant: "outlined",
|
||||||
bulkEditSwitchChangeHandler: () =>
|
bulkEditSwitchChangeHandler: () =>
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
@ -70,12 +73,13 @@ DynamicSelect.defaultProps = {
|
|||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props)
|
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant}: Props)
|
||||||
{
|
{
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState(null);
|
const [searchTerm, setSearchTerm] = useState(null);
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
const {inputBorderColor} = colors;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
|
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
|
||||||
@ -230,7 +234,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
|||||||
// attributes. so, doing this, w/ key=id, seemed to fix it. //
|
// attributes. so, doing this, w/ key=id, seemed to fix it. //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
return (
|
return (
|
||||||
<li {...props} key={option.id}>
|
<li {...props} key={option.id} style={{fontSize: "1rem"}}>
|
||||||
{content}
|
{content}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@ -244,13 +248,35 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
|||||||
bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
|
bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log(`default value: ${JSON.stringify(defaultValue)}`);
|
////////////////////////////////////////////
|
||||||
|
// for outlined style, adjust some styles //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
let autocompleteSX = {};
|
||||||
|
if (variant == "outlined")
|
||||||
|
{
|
||||||
|
autocompleteSX = {
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: "0.75rem",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
padding: "0.5rem",
|
||||||
|
background: isDisabled ? "#f0f2f5!important" : "initial",
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root .MuiAutocomplete-input": {
|
||||||
|
padding: "0",
|
||||||
|
fontSize: "1rem"
|
||||||
|
},
|
||||||
|
"& .Mui-disabled .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderColor: inputBorderColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const autocomplete = (
|
const autocomplete = (
|
||||||
<Box>
|
<Box>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
id={overrideId ?? fieldName}
|
id={overrideId ?? fieldName}
|
||||||
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}}
|
sx={autocompleteSX}
|
||||||
open={open}
|
open={open}
|
||||||
fullWidth
|
fullWidth
|
||||||
onOpen={() =>
|
onOpen={() =>
|
||||||
@ -305,7 +331,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
|||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label={fieldLabel}
|
label={fieldLabel}
|
||||||
variant="standard"
|
variant={variant}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
type="search"
|
type="search"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -341,6 +367,14 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
|||||||
id={`bulkEditSwitch-${fieldName}`}
|
id={`bulkEditSwitch-${fieldName}`}
|
||||||
checked={switchChecked}
|
checked={switchChecked}
|
||||||
onClick={bulkEditSwitchChanged}
|
onClick={bulkEditSwitchChanged}
|
||||||
|
sx={{top: "-4px",
|
||||||
|
"& .MuiSwitch-track": {
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 10,
|
||||||
|
top: -3,
|
||||||
|
position: "relative"
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box width="100%">
|
<Box width="100%">
|
||||||
|
@ -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,9 +594,11 @@ 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}>
|
||||||
|
{
|
||||||
|
(alertContent || warningContent) &&
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
{alertContent ? (
|
{alertContent ? (
|
||||||
@ -590,6 +613,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
) : ("")}
|
) : ("")}
|
||||||
</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>
|
||||||
|
@ -69,7 +69,15 @@ export default styled(TextField)(({theme, ownerState}: { theme?: Theme; ownerSta
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
backgroundColor: disabled ? `${grey[200]} !important` : transparent.main,
|
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()),
|
||||||
|
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;
|
@ -76,7 +76,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: stickyTop}}>
|
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "auto", maxHeight: "calc(100vh - 200px)"}}>
|
||||||
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
|
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
|
||||||
{
|
{
|
||||||
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
||||||
|
@ -215,6 +215,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
initialDisplayValue={selectedPossibleValue?.label}
|
initialDisplayValue={selectedPossibleValue?.label}
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
||||||
|
variant="standard"
|
||||||
/>
|
/>
|
||||||
</Box>;
|
</Box>;
|
||||||
case ValueMode.PVS_MULTI:
|
case ValueMode.PVS_MULTI:
|
||||||
@ -242,6 +243,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
||||||
|
variant="standard"
|
||||||
/>
|
/>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ interface Props
|
|||||||
labelAdditionalComponentsLeft: LabelComponent[];
|
labelAdditionalComponentsLeft: LabelComponent[];
|
||||||
labelAdditionalElementsLeft: JSX.Element[];
|
labelAdditionalElementsLeft: JSX.Element[];
|
||||||
labelAdditionalComponentsRight: LabelComponent[];
|
labelAdditionalComponentsRight: LabelComponent[];
|
||||||
|
labelBoxAdditionalSx?: any;
|
||||||
widgetMetaData?: QWidgetMetaData;
|
widgetMetaData?: QWidgetMetaData;
|
||||||
widgetData?: WidgetData;
|
widgetData?: WidgetData;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
@ -75,6 +76,7 @@ Widget.defaultProps = {
|
|||||||
labelAdditionalComponentsLeft: [],
|
labelAdditionalComponentsLeft: [],
|
||||||
labelAdditionalElementsLeft: [],
|
labelAdditionalElementsLeft: [],
|
||||||
labelAdditionalComponentsRight: [],
|
labelAdditionalComponentsRight: [],
|
||||||
|
labelBoxAdditionalSx: {},
|
||||||
omitPadding: false,
|
omitPadding: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -174,7 +176,7 @@ export class AddNewRecordButton extends LabelComponent
|
|||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="0.25rem">
|
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="-0.5rem">
|
||||||
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@ -552,7 +554,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||||
{
|
{
|
||||||
needLabelBox &&
|
needLabelBox &&
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} minHeight={"2.5rem"}>
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"}>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
hasPermission ?
|
hasPermission ?
|
||||||
|
@ -138,7 +138,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
if(data && data.viewAllLink)
|
if(data && data.viewAllLink)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(
|
labelAdditionalElementsLeft.push(
|
||||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative">
|
||||||
<Link to={data.viewAllLink}>View All</Link>
|
<Link to={data.viewAllLink}>View All</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
@ -178,8 +178,8 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
if(widgetMetaData?.showExportButton)
|
if(widgetMetaData?.showExportButton)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(
|
labelAdditionalElementsLeft.push(
|
||||||
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.25rem">
|
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative">
|
||||||
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.125}}>save_alt</Icon></Button></Tooltip>
|
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.25}}>save_alt</Icon></Button></Tooltip>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -250,6 +250,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
widgetData={data}
|
widgetData={data}
|
||||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
|
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
||||||
>
|
>
|
||||||
<Box mx={-2} mb={-3}>
|
<Box mx={-2} mb={-3}>
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
|
@ -86,7 +86,10 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cell = rows[i][columns[j].accessor];
|
const cell = rows[i][columns[j].accessor];
|
||||||
const text = htmlToText(cell,
|
let text = cell;
|
||||||
|
if(columns[j].type != "default")
|
||||||
|
{
|
||||||
|
text = htmlToText(cell,
|
||||||
{
|
{
|
||||||
selectors: [
|
selectors: [
|
||||||
{selector: "a", format: "inline"},
|
{selector: "a", format: "inline"},
|
||||||
@ -94,6 +97,7 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
{selector: ".button", format: "skip"}
|
{selector: ".button", format: "skip"}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
}
|
||||||
csv += `"${ValueUtils.cleanForCsv(text)}"`;
|
csv += `"${ValueUtils.cleanForCsv(text)}"`;
|
||||||
}
|
}
|
||||||
csv += "\n";
|
csv += "\n";
|
||||||
|
@ -414,7 +414,15 @@ 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) =>
|
||||||
|
{
|
||||||
|
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]
|
||||||
|
if(component.type == QComponentType.BULK_EDIT_FORM)
|
||||||
|
{
|
||||||
|
helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
{
|
{
|
||||||
component.type === QComponentType.HELP_TEXT && (
|
component.type === QComponentType.HELP_TEXT && (
|
||||||
@ -454,8 +462,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} lg={9}>
|
<Grid item xs={12} lg={9}>
|
||||||
|
{
|
||||||
{localTableSections.map((section: QTableSection, index: number) =>
|
localTableSections.map((section: QTableSection, index: number) =>
|
||||||
{
|
{
|
||||||
const name = section.name;
|
const name = section.name;
|
||||||
|
|
||||||
@ -491,7 +499,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{section.label}
|
{section.label}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
<Box px={2}>
|
<Box px={2}>
|
||||||
<QDynamicForm formData={sectionFormData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
|
<QDynamicForm formData={sectionFormData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableMetaData?.name};`} />
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
@ -501,16 +509,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{
|
{
|
||||||
return (<br />);
|
return (<br />);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
|
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableMetaData?.name};`} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
component.type === QComponentType.EDIT_FORM && (
|
component.type === QComponentType.EDIT_FORM && (
|
||||||
<QDynamicForm formData={formData} />
|
<QDynamicForm formData={formData} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@ -635,7 +643,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)))}
|
);
|
||||||
|
}))
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -34,12 +34,8 @@ function EntityCreate({table}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<Box mt={4}>
|
<Box mb={3}>
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12} lg={12}>
|
|
||||||
<EntityForm table={table} />
|
<EntityForm table={table} />
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
</Box>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
|
@ -43,18 +43,8 @@ function EntityEdit({table, isCopy}: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<Box mt={4}>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12} lg={12}>
|
|
||||||
<Box mb={3}>
|
<Box mb={3}>
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<EntityForm table={table} id={id} isCopy={isCopy} />
|
<EntityForm table={table} id={id} isCopy={isCopy} />
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
</Box>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
|
@ -30,7 +30,6 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo
|
|||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
|
|
||||||
import {Alert, Collapse, TablePagination, Typography} from "@mui/material";
|
import {Alert, Collapse, TablePagination, Typography} from "@mui/material";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
@ -51,7 +50,7 @@ import MenuItem from "@mui/material/MenuItem";
|
|||||||
import Modal from "@mui/material/Modal";
|
import Modal from "@mui/material/Modal";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue, GridColumnResizeParams} from "@mui/x-data-grid-pro";
|
||||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||||
@ -80,6 +79,8 @@ const COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort";
|
|||||||
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
|
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
|
||||||
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
||||||
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
|
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
|
||||||
|
const COLUMN_ORDERING_LOCAL_STORAGE_KEY_ROOT = "qqq.columnOrdering";
|
||||||
|
const COLUMN_WIDTHS_LOCAL_STORAGE_KEY_ROOT = "qqq.columnWidths";
|
||||||
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
||||||
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||||
|
|
||||||
@ -137,6 +138,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const sortLocalStorageKey = `${COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const sortLocalStorageKey = `${COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const rowsPerPageLocalStorageKey = `${ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const rowsPerPageLocalStorageKey = `${ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const pinnedColumnsLocalStorageKey = `${PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const pinnedColumnsLocalStorageKey = `${PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
|
const columnOrderingLocalStorageKey = `${COLUMN_ORDERING_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
|
const columnWidthsLocalStorageKey = `${COLUMN_WIDTHS_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const seenJoinTablesLocalStorageKey = `${SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const seenJoinTablesLocalStorageKey = `${SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
@ -147,6 +150,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
let defaultRowsPerPage = 10;
|
let defaultRowsPerPage = 10;
|
||||||
let defaultDensity = "standard" as GridDensity;
|
let defaultDensity = "standard" as GridDensity;
|
||||||
let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns;
|
let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns;
|
||||||
|
let defaultColumnOrdering = null as string[];
|
||||||
|
let defaultColumnWidths = {} as {[fieldName: string]: number};
|
||||||
let seenJoinTables: {[tableName: string]: boolean} = {};
|
let seenJoinTables: {[tableName: string]: boolean} = {};
|
||||||
let defaultTableVariant: QTableVariant = null;
|
let defaultTableVariant: QTableVariant = null;
|
||||||
|
|
||||||
@ -168,6 +173,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
defaultPinnedColumns = JSON.parse(localStorage.getItem(pinnedColumnsLocalStorageKey));
|
defaultPinnedColumns = JSON.parse(localStorage.getItem(pinnedColumnsLocalStorageKey));
|
||||||
}
|
}
|
||||||
|
if (localStorage.getItem(columnOrderingLocalStorageKey))
|
||||||
|
{
|
||||||
|
defaultColumnOrdering = JSON.parse(localStorage.getItem(columnOrderingLocalStorageKey));
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(columnWidthsLocalStorageKey))
|
||||||
|
{
|
||||||
|
defaultColumnWidths = JSON.parse(localStorage.getItem(columnWidthsLocalStorageKey));
|
||||||
|
}
|
||||||
if (localStorage.getItem(rowsPerPageLocalStorageKey))
|
if (localStorage.getItem(rowsPerPageLocalStorageKey))
|
||||||
{
|
{
|
||||||
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
|
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
|
||||||
@ -646,6 +659,38 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
let linkBase = metaData.getTablePath(table);
|
let linkBase = metaData.getTablePath(table);
|
||||||
linkBase += linkBase.endsWith("/") ? "" : "/";
|
linkBase += linkBase.endsWith("/") ? "" : "/";
|
||||||
const columns = DataGridUtils.setupGridColumns(tableMetaData, linkBase, metaData, "alphabetical");
|
const columns = DataGridUtils.setupGridColumns(tableMetaData, linkBase, metaData, "alphabetical");
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
// if there's a column-ordering (e.g., from local storage), apply it //
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
if(defaultColumnOrdering)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// note - may need to put this in its own function, e.g., for restoring "Saved Columns" when we add that //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
columns.sort((a: GridColDef, b: GridColDef) =>
|
||||||
|
{
|
||||||
|
const aIndex = defaultColumnOrdering.indexOf(a.field);
|
||||||
|
const bIndex = defaultColumnOrdering.indexOf(b.field);
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
// if there are column widths (e.g., from local storage), apply them //
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
if(defaultColumnWidths)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < columns.length; i++)
|
||||||
|
{
|
||||||
|
const width = defaultColumnWidths[columns[i].field];
|
||||||
|
if(width)
|
||||||
|
{
|
||||||
|
columns[i].width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setColumnsModel(columns);
|
setColumnsModel(columns);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -963,11 +1008,24 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setVisibleJoinTables(newVisibleJoinTables);
|
setVisibleJoinTables(newVisibleJoinTables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Event handler for column ordering change
|
||||||
|
*******************************************************************************/
|
||||||
const handleColumnOrderChange = (columnOrderChangeParams: GridColumnOrderChangeParams) =>
|
const handleColumnOrderChange = (columnOrderChangeParams: GridColumnOrderChangeParams) =>
|
||||||
{
|
{
|
||||||
// TODO: make local storaged
|
const columnOrdering = gridApiRef.current.state.columns.all;
|
||||||
console.log(JSON.stringify(columnsModel));
|
localStorage.setItem(columnOrderingLocalStorageKey, JSON.stringify(columnOrdering));
|
||||||
console.log(columnOrderChangeParams);
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Event handler for column resizing
|
||||||
|
*******************************************************************************/
|
||||||
|
const handleColumnResize = (params: GridColumnResizeParams, event: MuiEvent, details: GridCallbackDetails) =>
|
||||||
|
{
|
||||||
|
defaultColumnWidths[params.colDef.field] = params.width;
|
||||||
|
localStorage.setItem(columnWidthsLocalStorageKey, JSON.stringify(defaultColumnWidths));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true, isChangeFromDataGrid = false) =>
|
const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true, isChangeFromDataGrid = false) =>
|
||||||
@ -1872,13 +1930,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the table uses variants, then put the variant-selector into the goto dialog //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let gotoVariantSubHeader = <></>;
|
||||||
|
if(tableMetaData?.usesVariants)
|
||||||
|
{
|
||||||
|
gotoVariantSubHeader = <Box mb={2}>{getTableVariantHeader()}</Box>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} tableVariant={tableVariant} autoOpen={true} buttonVisible={false} mayClose={false} subHeader={
|
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} tableVariant={tableVariant} autoOpen={true} buttonVisible={false} mayClose={false} subHeader={gotoVariantSubHeader} />
|
||||||
<Box mb={2}>
|
|
||||||
{getTableVariantHeader()}
|
|
||||||
</Box>
|
|
||||||
} />
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2009,6 +2072,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
columnVisibilityModel={columnVisibilityModel}
|
columnVisibilityModel={columnVisibilityModel}
|
||||||
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||||
onColumnOrderChange={handleColumnOrderChange}
|
onColumnOrderChange={handleColumnOrderChange}
|
||||||
|
onColumnResize={handleColumnResize}
|
||||||
onSelectionModelChange={selectionChanged}
|
onSelectionModelChange={selectionChanged}
|
||||||
onSortModelChange={handleSortChangeForDataGrid}
|
onSortModelChange={handleSortChangeForDataGrid}
|
||||||
sortingOrder={["asc", "desc"]}
|
sortingOrder={["asc", "desc"]}
|
||||||
|
@ -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}}> </div>
|
<div style={{display: "inline-block", width: 0}}> </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>
|
||||||
|
@ -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,34 @@ 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,
|
||||||
|
.MuiTooltip-tooltip .helpContent UL + 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 {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 {GridCallbackDetails, GridColDef, GridFilterItem, GridRowParams, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
|
import {GridColDef, GridFilterItem, GridRowsProp, MuiEvent} 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 React, {useRef, useState} from "react";
|
import {GridColumnHeaderParams} from "@mui/x-data-grid/models/params/gridColumnHeaderParams";
|
||||||
import {Link, NavigateFunction, useNavigate} from "react-router-dom";
|
import React from "react";
|
||||||
|
import {Link, NavigateFunction} 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";
|
||||||
|
|
||||||
@ -340,6 +343,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user