From adb2b4613d011c0a1ee66794fc25cdd7828759a2 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 7 Dec 2023 11:59:28 -0600 Subject: [PATCH] CE-752 Add help content concept to QQQ (fields and table sections at this time); redesign form fields (borders now) --- package.json | 3 +- src/App.tsx | 8 +- src/QContext.tsx | 2 + .../components/forms/BooleanFieldSwitch.tsx | 21 +- src/qqq/components/forms/DynamicForm.tsx | 48 +- src/qqq/components/forms/DynamicFormField.tsx | 14 +- src/qqq/components/forms/DynamicFormUtils.ts | 1 + src/qqq/components/forms/DynamicSelect.tsx | 31 +- src/qqq/components/forms/EntityForm.tsx | 62 ++- .../components/legacy/MDInput/MDInputRoot.tsx | 10 +- src/qqq/components/misc/HelpContent.tsx | 139 ++++++ src/qqq/pages/processes/ProcessRun.tsx | 418 +++++++++--------- src/qqq/pages/records/create/RecordCreate.tsx | 8 +- src/qqq/pages/records/edit/RecordEdit.tsx | 14 +- src/qqq/pages/records/view/RecordView.tsx | 47 +- src/qqq/styles/qqq-override-styles.css | 37 +- src/qqq/utils/DataGridUtils.tsx | 17 + 17 files changed, 595 insertions(+), 285 deletions(-) create mode 100644 src/qqq/components/misc/HelpContent.tsx diff --git a/package.json b/package.json index 9afa8ef..38b4755 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 9ae855e..48578b1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() ) ); -} +} \ No newline at end of file diff --git a/src/QContext.tsx b/src/QContext.tsx index b13c8e5..b90413b 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -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(defaultState); diff --git a/src/qqq/components/forms/BooleanFieldSwitch.tsx b/src/qqq/components/forms/BooleanFieldSwitch.tsx index 00cbc62..530dfeb 100644 --- a/src/qqq/components/forms/BooleanFieldSwitch.tsx +++ b/src/qqq/components/forms/BooleanFieldSwitch.tsx @@ -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 ( {label} - + setSwitch(e, false)} sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}> @@ -116,7 +117,7 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme setSwitch(e, true)} sx={{cursor: value === true || isDisabled ? "inherit" : "pointer"}}> diff --git a/src/qqq/components/forms/DynamicForm.tsx b/src/qqq/components/forms/DynamicForm.tsx index d98ecd2..2ea2c46 100644 --- a/src/qqq/components/forms/DynamicForm.tsx +++ b/src/qqq/components/forms/DynamicForm.tsx @@ -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 ( @@ -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 = ; + if(formattedHelpContent) { - return null; + formattedHelpContent = {formattedHelpContent} } + const labelElement = + + + if (field.type === "file") { const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB}); return ( - - {field.label} + {labelElement} { - props.record && props.record.values.get(fieldName) && + record && record.values.get(fieldName) && Current File: - {ValueUtils.getDisplayValue(pseudoField, props.record, "view")} + {ValueUtils.getDisplayValue(pseudoField, record, "view")} removeFile(fieldName)}>delete @@ -162,18 +170,20 @@ function QDynamicForm(props: Props): JSX.Element return ( + {labelElement} + {formattedHelpContent} ); } @@ -182,9 +192,11 @@ function QDynamicForm(props: Props): JSX.Element // todo? placeholder={password.placeholder} return ( + {labelElement} + {formattedHelpContent} ); })} @@ -207,6 +220,7 @@ function QDynamicForm(props: Props): JSX.Element QDynamicForm.defaultProps = { formLabel: undefined, bulkEditMode: false, + helpRoles: ["ALL_SCREENS"], bulkEditSwitchChangeHandler: () => { }, diff --git a/src/qqq/components/forms/DynamicFormField.tsx b/src/qqq/components/forms/DynamicFormField.tsx index 9126eb8..1869e84 100644 --- a/src/qqq/components/forms/DynamicFormField.tsx +++ b/src/qqq/components/forms/DynamicFormField.tsx @@ -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 = ( <> - { 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" + } + }} /> diff --git a/src/qqq/components/forms/DynamicFormUtils.ts b/src/qqq/components/forms/DynamicFormUtils.ts index 59c060e..93ea346 100644 --- a/src/qqq/components/forms/DynamicFormUtils.ts +++ b/src/qqq/components/forms/DynamicFormUtils.ts @@ -89,6 +89,7 @@ class DynamicFormUtils label += field.isRequired ? " *" : ""; return ({ + fieldMetaData: field, name: field.name, label: label, isRequired: field.isRequired, diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index 2f375f3..f7d6abe 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -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([]); 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 ( -
  • +
  • {content}
  • ); @@ -250,7 +252,22 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe @@ -305,7 +322,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index e539c37..8289c1a 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -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); + 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
    Loading...
    ; } - return ; + + const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"] + return ; } if (!asyncLoadInited) @@ -330,6 +335,7 @@ function EntityForm(props: Props): JSX.Element ///////////////////////////////////// const dynamicFormFieldsBySection = new Map(); 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 = ; + + return formattedHelpContent && ( + + {formattedHelpContent} + + ) + } + 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 = ( - - - {alertContent ? ( - - setAlertContent(null)}>{alertContent} - - ) : ("")} - {warningContent ? ( - - setWarningContent(null)}>{warningContent} - - ) : ("")} + { + (alertContent || warningContent) && + + + {alertContent ? ( + + setAlertContent(null)}>{alertContent} + + ) : ("")} + {warningContent ? ( + + setWarningContent(null)}>{warningContent} + + ) : ("")} + - + } { !props.isModal && @@ -627,10 +651,11 @@ function EntityForm(props: Props): JSX.Element {formTitle}
    + {t1section && getSectionHelp(t1section)} { t1sectionName && formFields ? ( - - + + {getFormSection(values, touched, formFields.get(t1sectionName), errors)} @@ -644,8 +669,9 @@ function EntityForm(props: Props): JSX.Element {section.label} + {getSectionHelp(section)} - + {getFormSection(values, touched, formFields.get(section.name), errors)} diff --git a/src/qqq/components/legacy/MDInput/MDInputRoot.tsx b/src/qqq/components/legacy/MDInput/MDInputRoot.tsx index 9c05184..fbb7ba6 100644 --- a/src/qqq/components/legacy/MDInput/MDInputRoot.tsx +++ b/src/qqq/components/legacy/MDInput/MDInputRoot.tsx @@ -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()), diff --git a/src/qqq/components/misc/HelpContent.tsx b/src/qqq/components/misc/HelpContent.tsx new file mode 100644 index 0000000..fb04d8c --- /dev/null +++ b/src/qqq/components/misc/HelpContent.tsx @@ -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 . + */ + +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 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 ({content}) + } + + 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 + {heading && {heading}} + {formatHelpContent(content, selectedHelpContent.format)} + ; + } + + return (null); +} + +export default HelpContent; diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index 7c954f5..a8fa84e 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -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) => ( -
    - { - component.type === QComponentType.HELP_TEXT && ( - component.values.previewText ? - <> - - - - - - {ValueUtils.breakTextIntoLines(component.values.text)} - - - - : - - {ValueUtils.breakTextIntoLines(component.values.text)} - - ) - } - { - component.type === QComponentType.BULK_EDIT_FORM && ( - tableMetaData && localTableSections ? - - { - localTableSections.length == 0 && - - There are no editable fields on this table. + 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 ( +
    + { + component.type === QComponentType.HELP_TEXT && ( + component.values.previewText ? + <> + + + + + + {ValueUtils.breakTextIntoLines(component.values.text)} + + + + : + + {ValueUtils.breakTextIntoLines(component.values.text)} + + ) + } + { + component.type === QComponentType.BULK_EDIT_FORM && ( + tableMetaData && localTableSections ? + + { + localTableSections.length == 0 && + + There are no editable fields on this table. + + } + + { + localTableSections.length > 0 && + } - } - - { - localTableSections.length > 0 && - } - - - - {localTableSections.map((section: QTableSection, index: number) => - { - const name = section.name; - - if (section.isHidden) + { - 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 ( - - - - {section.label} - - - + 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 ( + + + + {section.label} + + + + + - - - ); + ); + } + else + { + return (
    ); + } + }) } - else - { - return (
    ); - } - } - )} +
    -
    - : - ) - } - { - component.type === QComponentType.EDIT_FORM && ( - - ) - } - { - component.type === QComponentType.VIEW_FORM && step.viewFields && ( -
    - {step.viewFields.map((field: QFieldMetaData) => ( - field.hasAdornment(AdornmentType.ERROR) ? ( - processValues[field.name] && ( + : + ) + } + { + component.type === QComponentType.EDIT_FORM && ( + + ) + } + { + component.type === QComponentType.VIEW_FORM && step.viewFields && ( +
    + {step.viewFields.map((field: QFieldMetaData) => ( + field.hasAdornment(AdornmentType.ERROR) ? ( + processValues[field.name] && ( + + + {ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")} + + + ) + ) : ( - + + {field.label} + :   + + {ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")} - ) - ) : ( - - - {field.label} - :   - - - {ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")} + ))) + } +
    + ) + } + { + component.type === QComponentType.DOWNLOAD_FORM && ( + + + + Download + + + download(`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`, processValues.downloadFileName)} sx={{cursor: "pointer"}}> + + download_for_offline + {processValues.downloadFileName} + - ))) - } -
    - ) - } - { - component.type === QComponentType.DOWNLOAD_FORM && ( - - - - Download - - - download(`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`, processValues.downloadFileName)} sx={{cursor: "pointer"}}> - - download_for_offline - {processValues.downloadFileName} - - - + - - ) - } - { - component.type === QComponentType.VALIDATION_REVIEW_SCREEN && ( - - { - const {value} = event.currentTarget; + ) + } + { + component.type === QComponentType.VALIDATION_REVIEW_SCREEN && ( + + { + 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 && ( - - ) - } - { - component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && ( - // todo - make these booleans configurable (values on the component) - - ) - } - { - component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && ( -
    - Records - {" "} -
    - - row.__idForDataGridPro__} - paginationMode="server" - pagination - density="compact" - loading={recordConfig.loading} - disableColumnFilter - /> + setOverrideOnLastStep(value !== "true"); + }} + /> + ) + } + { + component.type === QComponentType.PROCESS_SUMMARY_RESULTS && ( + + ) + } + { + component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && ( + // todo - make these booleans configurable (values on the component) + + ) + } + { + component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && ( +
    + Records + {" "} +
    + + row.__idForDataGridPro__} + paginationMode="server" + pagination + density="compact" + loading={recordConfig.loading} + disableColumnFilter + /> + +
    + ) + } + { + component.type === QComponentType.HTML && ( + processValues[`${step.name}.html`] && + + {parse(processValues[`${step.name}.html`])} -
    - ) - } - { - component.type === QComponentType.HTML && ( - processValues[`${step.name}.html`] && - - {parse(processValues[`${step.name}.html`])} - - ) - } -
    - )))} + ) + } +
    + ); + })) + } ); }; diff --git a/src/qqq/pages/records/create/RecordCreate.tsx b/src/qqq/pages/records/create/RecordCreate.tsx index f85aa9d..9ceec15 100644 --- a/src/qqq/pages/records/create/RecordCreate.tsx +++ b/src/qqq/pages/records/create/RecordCreate.tsx @@ -34,12 +34,8 @@ function EntityCreate({table}: Props): JSX.Element { return ( - - - - - - + + ); diff --git a/src/qqq/pages/records/edit/RecordEdit.tsx b/src/qqq/pages/records/edit/RecordEdit.tsx index 9d84298..e159d6a 100644 --- a/src/qqq/pages/records/edit/RecordEdit.tsx +++ b/src/qqq/pages/records/edit/RecordEdit.tsx @@ -43,18 +43,8 @@ function EntityEdit({table, isCopy}: Props): JSX.Element return ( - - - - - - - - - - - - + + ); diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index 733f712..b5c3a8e 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -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 = ; + + return formattedHelpContent && ( + + {formattedHelpContent} + + ) + } + + 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 = ; + + const labelElement = {label}: + return ( - - {label}: + <> + { + showHelp && formattedHelpContent ? {labelElement} : labelElement + }
     
    -
    - - {ValueUtils.getDisplayValue(field, record, "view", fieldName)} - + + {ValueUtils.getDisplayValue(field, record, "view", fieldName)} + +
    ) }) @@ -531,6 +561,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element {section.label} + {getSectionHelp(section)} {fields} @@ -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}
    + {t1Section && getSectionHelp(t1Section)} {t1SectionElement ? ({t1SectionElement}) : null}
    diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index a48a7c1..1ad6e22 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -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; +} \ No newline at end of file diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index 7ca67da..222e347 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -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 = ; + column.renderHeader = (params: GridColumnHeaderParams) => ( + +
    + {headerName} +
    +
    + ); + } + return (column); }