From adb2b4613d011c0a1ee66794fc25cdd7828759a2 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 7 Dec 2023 11:59:28 -0600 Subject: [PATCH 1/9] 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); } From 68f652f3f3c6e510426e3cafdc0ce318817a5aac Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 7 Dec 2023 12:00:23 -0600 Subject: [PATCH 2/9] Fixes for styles (spacing) in header record grid widget --- src/qqq/components/widgets/Widget.tsx | 6 ++++-- src/qqq/components/widgets/misc/RecordGridWidget.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index 7c65007..c2f3fcb 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -56,6 +56,7 @@ interface Props labelAdditionalComponentsLeft: LabelComponent[]; labelAdditionalElementsLeft: JSX.Element[]; labelAdditionalComponentsRight: LabelComponent[]; + labelBoxAdditionalSx?: any; widgetMetaData?: QWidgetMetaData; widgetData?: WidgetData; children: JSX.Element; @@ -75,6 +76,7 @@ Widget.defaultProps = { labelAdditionalComponentsLeft: [], labelAdditionalElementsLeft: [], labelAdditionalComponentsRight: [], + labelBoxAdditionalSx: {}, omitPadding: false, }; @@ -174,7 +176,7 @@ export class AddNewRecordButton extends LabelComponent render = (args: LabelComponentRenderArgs): JSX.Element => { return ( - + ); @@ -552,7 +554,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element { needLabelBox && - + { hasPermission ? diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 99a8b2c..53e4135 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -135,7 +135,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element if(data && data.viewAllLink) { labelAdditionalElementsLeft.push( - + View All ) @@ -175,8 +175,8 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element if(widgetMetaData?.showExportButton) { labelAdditionalElementsLeft.push( - - + + ); } @@ -216,6 +216,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element widgetData={data} labelAdditionalElementsLeft={labelAdditionalElementsLeft} labelAdditionalComponentsRight={labelAdditionalComponentsRight} + labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}} > Date: Thu, 7 Dec 2023 12:00:49 -0600 Subject: [PATCH 3/9] make overflow w/ a max-height, since sticky. --- src/qqq/components/misc/RecordSidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qqq/components/misc/RecordSidebar.tsx b/src/qqq/components/misc/RecordSidebar.tsx index 4c8a06b..25b6ac3 100644 --- a/src/qqq/components/misc/RecordSidebar.tsx +++ b/src/qqq/components/misc/RecordSidebar.tsx @@ -76,7 +76,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P return ( - borderRadius.lg, position: "sticky", top: stickyTop}}> + { sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => ( From 8c7a7ae43ec45589e42d3df9c20d429f7edc8ffa Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 7 Dec 2023 12:06:33 -0600 Subject: [PATCH 4/9] Update webdrivermanager version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c3dd9dc..e52b9d2 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ io.github.bonigarcia webdrivermanager - 5.4.1 + 5.6.2 test From fc5637b133b62f3d15a960f62a04f4f86f6cd608 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 7 Dec 2023 12:11:15 -0600 Subject: [PATCH 5/9] Update circleci/browser-tools version --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c6bf029..6220ee9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 orbs: node: circleci/node@5.1.0 - browser-tools: circleci/browser-tools@1.4.5 + browser-tools: circleci/browser-tools@1.4.6 executors: java17: From be393884cc78ee9daf1b4f80bfe34c23c48d128f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 13 Dec 2023 15:19:46 -0600 Subject: [PATCH 6/9] CE-752 Final style updates for helpContent --- src/qqq/assets/theme/components/tooltip.ts | 2 +- src/qqq/components/forms/DynamicForm.tsx | 4 +- src/qqq/components/forms/DynamicSelect.tsx | 47 +++++++++++-------- .../query/FilterCriteriaRowValues.tsx | 2 + 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/qqq/assets/theme/components/tooltip.ts b/src/qqq/assets/theme/components/tooltip.ts index b894f88..b708bee 100644 --- a/src/qqq/assets/theme/components/tooltip.ts +++ b/src/qqq/assets/theme/components/tooltip.ts @@ -48,7 +48,7 @@ const tooltip: Types = { borderRadius: borderRadius.md, opacity: 0.7, 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: { diff --git a/src/qqq/components/forms/DynamicForm.tsx b/src/qqq/components/forms/DynamicForm.tsx index 2ea2c46..663d210 100644 --- a/src/qqq/components/forms/DynamicForm.tsx +++ b/src/qqq/components/forms/DynamicForm.tsx @@ -108,10 +108,10 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa let formattedHelpContent = ; if(formattedHelpContent) { - formattedHelpContent = {formattedHelpContent} + formattedHelpContent = {formattedHelpContent} } - const labelElement = + const labelElement = diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index f7d6abe..6c40a0e 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -50,6 +50,7 @@ interface Props bulkEditMode?: boolean; bulkEditSwitchChangeHandler?: any; otherValues?: Map; + variant: "standard" | "outlined"; } DynamicSelect.defaultProps = { @@ -64,6 +65,7 @@ DynamicSelect.defaultProps = { isMultiple: false, bulkEditMode: false, otherValues: new Map(), + variant: "outlined", bulkEditSwitchChangeHandler: () => { }, @@ -71,7 +73,7 @@ DynamicSelect.defaultProps = { 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 [options, setOptions] = useState([]); @@ -246,28 +248,35 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe 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 = ( @@ -322,7 +331,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe valueChangeHandler(null, 0, value)} + variant="standard" /> ; case ValueMode.PVS_MULTI: @@ -242,6 +243,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC initialValues={initialValues} inForm={false} onChange={(value: any) => valueChangeHandler(null, "all", value)} + variant="standard" /> ; } From f3b02c291ffd1014fe71c6dfdda02c634137525f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 15 Dec 2023 15:33:46 -0600 Subject: [PATCH 7/9] store column order & widths in local storage; also fix variants header in goto menu --- src/qqq/pages/records/query/RecordQuery.tsx | 84 ++++++++++++++++++--- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 4510fe0..986c9f0 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -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 {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; 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 Autocomplete from "@mui/material/Autocomplete"; import Box from "@mui/material/Box"; @@ -51,7 +50,7 @@ import MenuItem from "@mui/material/MenuItem"; import Modal from "@mui/material/Modal"; import TextField from "@mui/material/TextField"; 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 FormData from "form-data"; 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 ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage"; 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 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 rowsPerPageLocalStorageKey = `${ROWS_PER_PAGE_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 columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_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 defaultDensity = "standard" as GridDensity; let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns; + let defaultColumnOrdering = null as string[]; + let defaultColumnWidths = {} as {[fieldName: string]: number}; let seenJoinTables: {[tableName: string]: boolean} = {}; let defaultTableVariant: QTableVariant = null; @@ -168,6 +173,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { 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)) { defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey)); @@ -646,6 +659,38 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element let linkBase = metaData.getTablePath(table); linkBase += linkBase.endsWith("/") ? "" : "/"; 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); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -987,11 +1032,24 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setVisibleJoinTables(newVisibleJoinTables); } + + /******************************************************************************* + ** Event handler for column ordering change + *******************************************************************************/ const handleColumnOrderChange = (columnOrderChangeParams: GridColumnOrderChangeParams) => { - // TODO: make local storaged - console.log(JSON.stringify(columnsModel)); - console.log(columnOrderChangeParams); + const columnOrdering = gridApiRef.current.state.columns.all; + localStorage.setItem(columnOrderingLocalStorageKey, JSON.stringify(columnOrdering)); + }; + + + /******************************************************************************* + ** 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) => @@ -1896,13 +1954,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 = {getTableVariantHeader()} + } + return ( - - {getTableVariantHeader()} - - } /> + ); } @@ -2033,6 +2096,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element columnVisibilityModel={columnVisibilityModel} onColumnVisibilityModelChange={handleColumnVisibilityChange} onColumnOrderChange={handleColumnOrderChange} + onColumnResize={handleColumnResize} onSelectionModelChange={selectionChanged} onSortModelChange={handleSortChangeForDataGrid} sortingOrder={["asc", "desc"]} From 4a0e123f905623e2dc06594962d31353235007f6 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 18 Dec 2023 10:21:24 -0600 Subject: [PATCH 8/9] Fix exporting - cell type default, if value was number, was being lost in call to htmlToText. --- .../components/widgets/tables/TableWidget.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/qqq/components/widgets/tables/TableWidget.tsx b/src/qqq/components/widgets/tables/TableWidget.tsx index 3ca4d5e..51d14ce 100644 --- a/src/qqq/components/widgets/tables/TableWidget.tsx +++ b/src/qqq/components/widgets/tables/TableWidget.tsx @@ -86,14 +86,18 @@ function TableWidget(props: Props): JSX.Element } const cell = rows[i][columns[j].accessor]; - const text = htmlToText(cell, - { - selectors: [ - {selector: "a", format: "inline"}, - {selector: ".MuiIcon-root", format: "skip"}, - {selector: ".button", format: "skip"} - ] - }); + let text = cell; + if(columns[j].type != "default") + { + text = htmlToText(cell, + { + selectors: [ + {selector: "a", format: "inline"}, + {selector: ".MuiIcon-root", format: "skip"}, + {selector: ".button", format: "skip"} + ] + }); + } csv += `"${ValueUtils.cleanForCsv(text)}"`; } csv += "\n"; From 77e341df3a7aa3c9705194f95a35265e1e389091 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 18 Dec 2023 10:21:41 -0600 Subject: [PATCH 9/9] Add margin-top for helpContent with UL + P --- src/qqq/styles/qqq-override-styles.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index 1ad6e22..ad51d57 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -582,7 +582,8 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } margin-bottom: 0.25rem; } -.MuiTooltip-tooltip .helpContent P + P +.MuiTooltip-tooltip .helpContent P + P, +.MuiTooltip-tooltip .helpContent UL + P { margin-top: 1rem; }