From 0882b92b27508965b1fc087e580e94d3388e312b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 29 Aug 2022 13:30:29 -0500 Subject: [PATCH 1/5] Checkpoint - new validation & summary screens --- .eslintrc.json | 1 + src/qqq/components/EntityForm/index.tsx | 4 +- src/qqq/components/QButtons/index.tsx | 49 +- src/qqq/pages/entity-list/index.tsx | 114 +++-- .../components/QProcessSummaryResults.tsx | 95 ++++ .../components/QValidationReview.tsx | 286 ++++++++++++ src/qqq/pages/process-run/index.tsx | 418 ++++++++++++------ .../process-run/model/ProcessSummaryLine.tsx | 138 ++++++ src/qqq/utils/QFilterUtils.ts | 229 ++++++++++ src/qqq/utils/QValueUtils.ts | 5 + 10 files changed, 1129 insertions(+), 210 deletions(-) create mode 100644 src/qqq/pages/process-run/components/QProcessSummaryResults.tsx create mode 100644 src/qqq/pages/process-run/components/QValidationReview.tsx create mode 100644 src/qqq/pages/process-run/model/ProcessSummaryLine.tsx create mode 100644 src/qqq/utils/QFilterUtils.ts diff --git a/.eslintrc.json b/.eslintrc.json index d9b248b..bbc7b81 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -57,6 +57,7 @@ } ], "import/order": "off", + "jsx-one-expression-per-line": "off", "max-len": "off", "no-console": "off", "no-constant-condition": "off", diff --git a/src/qqq/components/EntityForm/index.tsx b/src/qqq/components/EntityForm/index.tsx index 2a2329a..1cec5b8 100644 --- a/src/qqq/components/EntityForm/index.tsx +++ b/src/qqq/components/EntityForm/index.tsx @@ -334,8 +334,8 @@ function EntityForm({table, id}: Props): JSX.Element - - + + diff --git a/src/qqq/components/QButtons/index.tsx b/src/qqq/components/QButtons/index.tsx index 867ee66..6f54183 100644 --- a/src/qqq/components/QButtons/index.tsx +++ b/src/qqq/components/QButtons/index.tsx @@ -24,6 +24,7 @@ import {Link} from "react-router-dom"; import MDButton from "components/MDButton"; import Icon from "@mui/material/Icon"; import React from "react"; +import EntityForm from "qqq/components/EntityForm"; // eslint-disable import/prefer-default-export @@ -42,11 +43,16 @@ export function QCreateNewButton(): JSX.Element ); } -export function QSaveButton(): JSX.Element +interface QSaveButtonProps +{ + disabled: boolean +} + +export function QSaveButton({disabled}: QSaveButtonProps): JSX.Element { return ( - save}> + save} disabled={disabled}> Save @@ -108,15 +114,48 @@ export function QActionsMenuButton({isOpen, onClickHandler}: QActionsMenuButtonP interface QCancelButtonProps { onClickHandler: any; + disabled: boolean; + label?: string; + iconName?: string } -export function QCancelButton({onClickHandler}: QCancelButtonProps): JSX.Element +export function QCancelButton({ + onClickHandler, disabled, label, iconName, +}: QCancelButtonProps): JSX.Element { return ( - cancel} onClick={onClickHandler}> - Cancel + {iconName}} onClick={onClickHandler} disabled={disabled}> + {label} ); } + +QCancelButton.defaultProps = { + label: "cancel", + iconName: "cancel", +}; + +interface QSubmitButtonProps +{ + label?: string + iconName?: string + disabled: boolean +} + +export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element +{ + return ( + + {iconName}} disabled={disabled}> + {label} + + + ); +} + +QSubmitButton.defaultProps = { + label: "Submit", + iconName: "check", +}; diff --git a/src/qqq/pages/entity-list/index.tsx b/src/qqq/pages/entity-list/index.tsx index 3ac6cc4..3a6ceca 100644 --- a/src/qqq/pages/entity-list/index.tsx +++ b/src/qqq/pages/entity-list/index.tsx @@ -20,9 +20,7 @@ */ import React, { - SyntheticEvent, - useCallback, - useEffect, useReducer, useRef, useState, + useCallback, useEffect, useReducer, useRef, useState, } from "react"; import { Link, useNavigate, useParams, useSearchParams, @@ -33,13 +31,14 @@ import Card from "@mui/material/Card"; import Icon from "@mui/material/Icon"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import {Alert, Pagination, TablePagination} from "@mui/material"; +import {Alert, TablePagination} from "@mui/material"; import { DataGridPro, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, + GridExportMenuItemProps, GridFilterModel, GridRowId, GridRowParams, @@ -52,15 +51,12 @@ import { GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, - GridExportMenuItemProps, MuiEvent, } from "@mui/x-data-grid-pro"; // Material Dashboard 2 PRO React TS components import DashboardLayout from "examples/LayoutContainers/DashboardLayout"; -import DashboardNavbar from "examples/Navbars/DashboardNavbar"; import MDBox from "components/MDBox"; -import MDButton from "components/MDButton"; import MDAlert from "components/MDAlert"; // QQQ @@ -80,6 +76,7 @@ import QProcessUtils from "../../utils/QProcessUtils"; import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons"; import QValueUtils from "qqq/utils/QValueUtils"; import LinearProgress from "@mui/material/LinearProgress"; +import QFilterUtils from "qqq/utils/QFilterUtils"; const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT = "qqq.columnVisibility"; const COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort"; @@ -90,6 +87,52 @@ interface Props table?: QTableMetaData; } +/******************************************************************************* + ** Get the default filter to use on the page - either from query string, or + ** local storage, or a default (empty). + *******************************************************************************/ +function getDefaultFilter(searchParams: URLSearchParams, filterLocalStorageKey: string): GridFilterModel +{ + if (searchParams.has("filter")) + { + try + { + const qQueryFilter = JSON.parse(searchParams.get("filter")) as QQueryFilter; + console.log(`Got a filter from the query string: ${JSON.stringify(qQueryFilter)}`); + + ////////////////////////////////////////////////////////////////// + // translate from a qqq-style filter to one that the grid wants // + ////////////////////////////////////////////////////////////////// + const defaultFilter = {items: []} as GridFilterModel; + let id = 1; + qQueryFilter.criteria.forEach((criteria) => + { + defaultFilter.items.push({ + columnField: criteria.fieldName, + operatorValue: QFilterUtils.qqqCriteriaOperatorToGrid(criteria.operator), + value: QFilterUtils.qqqCriteriaValuesToGrid(criteria.operator, criteria.values), + id: id++, // not sure what this id is!! + }); + }); + + return (defaultFilter); + } + catch (e) + { + console.warn("Error parsing filter from query string", e); + } + } + + if (localStorage.getItem(filterLocalStorageKey)) + { + const defaultFilter = JSON.parse(localStorage.getItem(filterLocalStorageKey)); + console.log(`Got default from LS: ${JSON.stringify(defaultFilter)}`); + return (defaultFilter); + } + + return ({items: []}); +} + function EntityList({table}: Props): JSX.Element { const tableNameParam = useParams().tableName; @@ -105,7 +148,7 @@ function EntityList({table}: Props): JSX.Element const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; let defaultSort = [] as GridSortItem[]; let defaultVisibility = {}; - let _defaultFilter = {items: []} as GridFilterModel; + const _defaultFilter = getDefaultFilter(searchParams, filterLocalStorageKey); if (localStorage.getItem(sortLocalStorageKey)) { @@ -115,11 +158,6 @@ function EntityList({table}: Props): JSX.Element { defaultVisibility = JSON.parse(localStorage.getItem(columnVisibilityLocalStorageKey)); } - if (localStorage.getItem(filterLocalStorageKey)) - { - _defaultFilter = JSON.parse(localStorage.getItem(filterLocalStorageKey)); - console.log(`Got default from LS: ${JSON.stringify(_defaultFilter)}`); - } const [filterModel, setFilterModel] = useState(_defaultFilter); const [columnSortModel, setColumnSortModel] = useState(defaultSort); @@ -159,46 +197,6 @@ function EntityList({table}: Props): JSX.Element const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const closeActionsMenu = () => setActionsMenu(null); - const translateCriteriaOperator = (operator: string) => - { - switch (operator) - { - case "contains": - return QCriteriaOperator.CONTAINS; - case "startsWith": - return QCriteriaOperator.STARTS_WITH; - case "endsWith": - return QCriteriaOperator.ENDS_WITH; - case "is": - case "equals": - case "=": - return QCriteriaOperator.EQUALS; - case "isNot": - case "!=": - return QCriteriaOperator.NOT_EQUALS; - case "after": - case ">": - return QCriteriaOperator.GREATER_THAN; - case "onOrAfter": - case ">=": - return QCriteriaOperator.GREATER_THAN_OR_EQUALS; - case "before": - case "<": - return QCriteriaOperator.LESS_THAN; - case "onOrBefore": - case "<=": - return QCriteriaOperator.LESS_THAN_OR_EQUALS; - case "isEmpty": - return QCriteriaOperator.IS_BLANK; - case "isNotEmpty": - return QCriteriaOperator.IS_NOT_BLANK; - // case "is any of": - // TODO: handle this case - default: - return QCriteriaOperator.EQUALS; - } - }; - const buildQFilter = () => { const qFilter = new QQueryFilter(); @@ -213,13 +211,9 @@ function EntityList({table}: Props): JSX.Element { filterModel.items.forEach((item) => { - const operator = translateCriteriaOperator(item.operatorValue); - let criteria = new QFilterCriteria(item.columnField, operator, [item.value]); - if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK) - { - criteria = new QFilterCriteria(item.columnField, operator, null); - } - qFilter.addCriteria(criteria); + const operator = QFilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue); + const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value); + qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values)); }); } diff --git a/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx b/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx new file mode 100644 index 0000000..f4a1ac5 --- /dev/null +++ b/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx @@ -0,0 +1,95 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 React from "react"; +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; +import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; +import List from "@mui/material/List"; +import {ListItem} from "@mui/material"; +import ListItemText from "@mui/material/ListItemText"; +import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine"; +import MDBox from "components/MDBox"; +import Grid from "@mui/material/Grid"; +import Icon from "@mui/material/Icon"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; + +interface Props +{ + qInstance: QInstance; + process: QProcessMetaData; + table: QTableMetaData; + processValues: any; + step: QFrontendStepMetaData; +} + +/******************************************************************************* + ** This is the process summary result component. + *******************************************************************************/ +function QProcessSummaryResults({ + qInstance, process, table = null, processValues, step, +}: Props): JSX.Element +{ + const resultValidationList = ( + + { + processValues?.recordCount && table && ( + + + {processValues.recordCount.toLocaleString()} + {" "} + {table.label} + {" "} + records were processed. + + + ) + } + + { + processValues.processResults && processValues.processResults.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, table, qInstance, true))) + } + + + ); + + return ( + + + + + + + + {process.iconName} + {`${process.label} : ${step.label}`} + + + {resultValidationList} + + + + + + ); +} + +export default QProcessSummaryResults; diff --git a/src/qqq/pages/process-run/components/QValidationReview.tsx b/src/qqq/pages/process-run/components/QValidationReview.tsx new file mode 100644 index 0000000..d5f4357 --- /dev/null +++ b/src/qqq/pages/process-run/components/QValidationReview.tsx @@ -0,0 +1,286 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 List from "@mui/material/List"; +import { + Button, FormControlLabel, ListItem, Radio, RadioGroup, tooltipClasses, TooltipProps, +} from "@mui/material"; +import ListItemText from "@mui/material/ListItemText"; +import Icon from "@mui/material/Icon"; +import MDBox from "components/MDBox"; +import MDTypography from "components/MDTypography"; +import Grid from "@mui/material/Grid"; +import React, {useState} from "react"; +import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; +import QValueUtils from "qqq/utils/QValueUtils"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine"; +import {Field} from "formik"; +import Tooltip from "@mui/material/Tooltip"; +import IconButton from "@mui/material/IconButton"; +import {styled} from "@mui/material/styles"; +import QTableUtils from "qqq/utils/QTableUtils"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; + +interface Props +{ + qInstance: QInstance; + process: QProcessMetaData; + table: QTableMetaData; + processValues: any; + step: QFrontendStepMetaData; + previewRecords: QRecord[]; + formValues: any; + doFullValidationRadioChangedHandler: any +} + +/******************************************************************************* + ** This is the process validation/review component - where the user may be prompted + ** to do a full validation or skip it. It's the same screen that shows validation + ** results when they are available. + *******************************************************************************/ +function QValidationReview({ + qInstance, process, table = null, processValues, step, previewRecords = [], formValues, doFullValidationRadioChangedHandler, +}: Props): JSX.Element +{ + const [previewRecordIndex, setPreviewRecordIndex] = useState(0); + + const updatePreviewRecordIndex = (offset: number) => + { + let newIndex = previewRecordIndex + offset; + if (newIndex < 0) + { + newIndex = 0; + } + if (newIndex >= previewRecords.length - 1) + { + newIndex = previewRecords.length - 1; + } + + setPreviewRecordIndex(newIndex); + }; + + const CustomWidthTooltip = styled(({className, ...props}: TooltipProps) => ( + + ))({ + [`& .${tooltipClasses.tooltip}`]: { + maxWidth: 500, + textAlign: "left", + }, + }); + + const preValidationList = ( + + { + processValues?.recordCount && table && ( + + + You selected + {` ${processValues.recordCount.toLocaleString()} ${table?.label} `} + records. + + + ) + } + { + processValues?.supportsFullValidation && formValues && formValues.doFullValidation !== undefined && ( + <> + + How would you like to proceed? + + + + + } + label={( + + Perform Validation on all records before processing. + + If you choose this option, a Validation step will run on all of the records that you selected. + You will then be told how many can process successfully, and how many have issues. +
+
+ Running this validation may take several minutes, depending on the complexity of the work, and the number of records. +
+
+ Choose this option if you want more information about what will happen, and you are willing to wait for that information. + + )} + > + info_outlined +
+
+ )} + /> +
+ + } + label={( + + Skip Validation. Submit the records for immediate processing. + + If you choose this option, the records you selected will immediately be processed. + You will be told how many records were successfully processed, and which ones had issues after the processing is completed. +
+
+ Choose this option if you feel that you do not need this information, or are not willing to wait for it. + + )} + > + info_outlined +
+
+ )} + /> +
+
+
+ + ) + } +
+ ); + + const postValidationList = ( + + { + processValues?.recordCount && table && ( + + + Validation complete on + {` ${processValues.recordCount.toLocaleString()} ${table?.label} `} + records. + + + ) + } + + { + processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, table, qInstance))) + } + + + ); + + const recordPreviewWidget = step.recordListFields && ( + + + Preview + + + + + { + previewRecords && previewRecords.length > 0 ? ( + <> + This is a preview of the records that will be created. + + Note that the number of preview records available may be fewer than the total number of records being processed. + + )} + > + info_outlined + + + ) : ( + <> + No record previews are available at this time. + + { + processValues.validationSummary ? ( + <> + It appears as though this process does not contain any valid records. + + ) : ( + <> + If you choose to Perform Validation, and there are any valid records, then you will see a preview here. + + ) + } + + )} + > + info_outlined + + + ) + } + + + + { + previewRecords && previewRecords[previewRecordIndex] && step.recordListFields.map((field) => ( + + {`${field.label}:`} + {" "} +   + {" "} + {QValueUtils.getDisplayValue(field, previewRecords[previewRecordIndex])} + + )) + } + { + previewRecords && previewRecords.length > 0 && ( + + + + {`Preview ${previewRecordIndex + 1} of ${previewRecords.length}`} + + + + ) + } + + + + ); + + return ( + + + + + {processValues.validationSummary ? postValidationList : preValidationList} + + + + {recordPreviewWidget} + + + + ); +} + +export default QValidationReview; diff --git a/src/qqq/pages/process-run/index.tsx b/src/qqq/pages/process-run/index.tsx index ccc034b..262b07a 100644 --- a/src/qqq/pages/process-run/index.tsx +++ b/src/qqq/pages/process-run/index.tsx @@ -19,47 +19,46 @@ * along with this program. If not, see . */ -import React, {useEffect, useState, Fragment} from "react"; +import * as Yup from "yup"; +import {CircularProgress, TablePagination} from "@mui/material"; +import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro"; // formik components import {Form, Formik} from "formik"; +import React, {Fragment, useEffect, useState} from "react"; +import {useLocation, useNavigate, useParams} from "react-router-dom"; -// @mui material components -import Grid from "@mui/material/Grid"; +import BaseLayout from "qqq/components/BaseLayout"; import Card from "@mui/material/Card"; -import Stepper from "@mui/material/Stepper"; -import Step from "@mui/material/Step"; -import StepLabel from "@mui/material/StepLabel"; - +import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils"; +import FormData from "form-data"; +import Grid from "@mui/material/Grid"; // Material Dashboard 2 PRO React TS components import MDBox from "components/MDBox"; import MDButton from "components/MDButton"; - -// Material Dashboard 2 PRO React TS examples components -import DashboardLayout from "examples/LayoutContainers/DashboardLayout"; - -import * as Yup from "yup"; +import MDProgress from "components/MDProgress"; +import MDTypography from "../../../components/MDTypography"; +import QClient from "qqq/utils/QClient"; +import {QComponentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QComponentType"; +import QDynamicForm from "../../components/QDynamicForm"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; +import {QFrontendComponent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendComponent"; import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; -import {useLocation, useParams} from "react-router-dom"; -import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils"; -import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted"; import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning"; -import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro"; -import {QFrontendComponent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendComponent"; -import {QComponentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QComponentType"; -import FormData from "form-data"; -import QClient from "qqq/utils/QClient"; -import {CircularProgress, TablePagination} from "@mui/material"; -import QDynamicForm from "../../components/QDynamicForm"; -import MDTypography from "../../../components/MDTypography"; -import Footer from "examples/Footer"; +import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; -import Navbar from "qqq/components/Navbar"; -import BaseLayout from "qqq/components/BaseLayout"; import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; +import Step from "@mui/material/Step"; +import StepLabel from "@mui/material/StepLabel"; +import Stepper from "@mui/material/Stepper"; +import QValidationReview from "qqq/pages/process-run/components/QValidationReview"; +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import QProcessSummaryResults from "./components/QProcessSummaryResults"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {QCancelButton, QSubmitButton} from "qqq/components/QButtons"; +import {formatDate} from "@fullcalendar/react"; interface Props { @@ -89,12 +88,16 @@ function ProcessRun({process}: Props): JSX.Element const [steps, setSteps] = useState([] as QFrontendStepMetaData[]); const [needInitialLoad, setNeedInitialLoad] = useState(true); const [processMetaData, setProcessMetaData] = useState(null); + const [tableMetaData, setTableMetaData] = useState(null); + const [qInstance, setQInstance] = useState(null as QInstance); const [processValues, setProcessValues] = useState({} as any); const [processError, setProcessError] = useState(null as string); const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false); const [lastProcessResponse, setLastProcessResponse] = useState( null as QJobStarted | QJobComplete | QJobError | QJobRunning, ); + + const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean); const onLastStep = activeStepIndex === steps.length - 2; const noMoreSteps = activeStepIndex === steps.length - 1; @@ -115,12 +118,16 @@ function ProcessRun({process}: Props): JSX.Element const [recordConfig, setRecordConfig] = useState({} as any); const [pageNumber, setPageNumber] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const [records, setRecords] = useState([] as QRecord[]); ////////////////////////////// // state for bulk edit form // ////////////////////////////// const [disabledBulkEditFields, setDisabledBulkEditFields] = useState({} as any); + const navigate = useNavigate(); + const location = useLocation(); + const doesStepHaveComponent = (step: QFrontendStepMetaData, type: QComponentType): boolean => { if (step.components) @@ -171,7 +178,7 @@ function ProcessRun({process}: Props): JSX.Element { if (value === null || value === undefined) { - return ; + return  ; } if (typeof value === "string") @@ -202,13 +209,14 @@ function ProcessRun({process}: Props): JSX.Element processError: string, processValues: any, recordConfig: any, + setFieldValue: any, ): JSX.Element => { if (processError) { return ( <> - + Error @@ -218,44 +226,49 @@ function ProcessRun({process}: Props): JSX.Element ); } - if (qJobRunning) + if (qJobRunning || step === null) { return ( - <> - - {" "} - Working - - - - - - - - {qJobRunning?.message} -
- {qJobRunning.current && qJobRunning.total && ( -
{`${qJobRunning.current.toLocaleString()} of ${qJobRunning.total.toLocaleString()}`}
- )} - - {`Updated at ${qJobRunningDate.toLocaleTimeString()}`} - -
-
+ + + + + + + Working + + + + + + + + {qJobRunning?.message} +
+ {qJobRunning?.current && qJobRunning?.total && ( + <> +
{`${qJobRunning.current.toLocaleString()} of ${qJobRunning.total.toLocaleString()}`}
+ + + + + )} + { + qJobRunningDate && ({`Updated at ${qJobRunningDate?.toLocaleTimeString()}`}) + } +
+
+
+
+
- +
); } - if (step === null) - { - console.log("in getDynamicStepContent. No step yet, so returning 'loading'"); - return
Loading...
; - } - return ( <> - {step?.label} + {step?.label} {step.components && ( step.components.map((component: QFrontendComponent, index: number) => ( // eslint-disable-next-line react/no-array-index-key @@ -267,60 +280,97 @@ function ProcessRun({process}: Props): JSX.Element
) } + { + component.type === QComponentType.BULK_EDIT_FORM && ( + + ) + } + { + component.type === QComponentType.EDIT_FORM && ( + + ) + } + { + component.type === QComponentType.VIEW_FORM && step.viewFields && ( +
+ {step.viewFields.map((field: QFieldMetaData) => ( + + + {field.label} + :   + + + {formatViewValue(processValues[field.name])} + + + ))} +
+ ) + } + { + component.type === QComponentType.VALIDATION_REVIEW_SCREEN && ( + + { + const {value} = event.currentTarget; + + ////////////////////////////////////////////////////////////// + // call the formik function to set the value in this field. // + ////////////////////////////////////////////////////////////// + setFieldValue("doFullValidation", value); + + // eslint-disable-next-line no-unneeded-ternary + setOverrideOnLastStep(value === "true" ? false : true); + }} + /> + ) + } + { + component.type === QComponentType.PROCESS_SUMMARY_RESULTS && ( + + ) + } + { + component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && ( +
+ Records + {" "} +
+ + row.__idForDataGridPro__} + paginationMode="server" + pagination + density="compact" + loading={recordConfig.loading} + disableColumnFilter + /> + +
+ ) + } )))} - {step.formFields && ( - - )} - {step.viewFields && ( -
- {step.viewFields.map((field: QFieldMetaData) => ( - - - {field.label} - :   - - - {formatViewValue(processValues[field.name])} - - - ))} -
- )} - {(step.recordListFields && recordConfig.columns) && ( -
- Records - {" "} -
- - row.__idForDataGridPro__} - paginationMode="server" - pagination - density="compact" - loading={recordConfig.loading} - disableColumnFilter - /> - -
- )} ); }; @@ -382,6 +432,7 @@ function ProcessRun({process}: Props): JSX.Element setProcessError(`Unknown process step ${newStep}.`); } setActiveStepIndex(newIndex); + setOverrideOnLastStep(null); if (steps) { @@ -424,6 +475,26 @@ function ProcessRun({process}: Props): JSX.Element setValidationScheme(Yup.object().shape(formValidations)); setValidationFunction(null); } + else if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN)) + { + //////////////////////////////////////// + // this component requires this field // + //////////////////////////////////////// + const dynamicFormFields: any = {}; + dynamicFormFields.doFullValidation = {type: "radio"}; + + const initialValues: any = {}; + initialValues.doFullValidation = "true"; + setOverrideOnLastStep(false); + + const formValidations: any = {}; + formValidations.doFullValidation = null; + + setFormFields(dynamicFormFields); + setInitialValues(initialValues); + setValidationScheme(Yup.object().shape(formValidations)); + setValidationFunction(null); + } else { ///////////////////////////////////////////////////////////////////////// @@ -502,6 +573,7 @@ function ProcessRun({process}: Props): JSX.Element ); const {records} = response; + setRecords(records); ///////////////////////////////////////////////////////////////////////////////////////// // re-construct the recordConfig object, so the setState call triggers a new rendering // @@ -542,6 +614,12 @@ function ProcessRun({process}: Props): JSX.Element setJobUUID(null); setNewStep(qJobComplete.nextStep); setProcessValues(qJobComplete.values); + setQJobRunning(null); + + if (activeStep && activeStep.recordListFields) + { + setNeedRecords(true); + } } else if (lastProcessResponse instanceof QJobStarted) { @@ -562,6 +640,7 @@ function ProcessRun({process}: Props): JSX.Element console.log(`Got an error from the backend... ${qJobError.error}`); setJobUUID(null); setProcessError(qJobError.error); + setQJobRunning(null); } } }, [lastProcessResponse]); @@ -574,13 +653,18 @@ function ProcessRun({process}: Props): JSX.Element if (needToCheckJobStatus) { setNeedToCheckJobStatus(false); + if (!processUUID || !jobUUID) + { + console.log(`Missing processUUID[${processUUID}] or jobUUID[${jobUUID}], so returning without checking job status`); + return; + } + (async () => { setTimeout(async () => { try { - console.log("OK"); const processResponse = await QClient.getInstance().processJobStatus( processName, processUUID, @@ -624,8 +708,7 @@ function ProcessRun({process}: Props): JSX.Element setNeedInitialLoad(false); (async () => { - const {search} = useLocation(); - const urlSearchParams = new URLSearchParams(search); + const urlSearchParams = new URLSearchParams(location.search); let queryStringForInit = null; if (urlSearchParams.get("recordIds")) { @@ -644,11 +727,35 @@ function ProcessRun({process}: Props): JSX.Element // queryStringForInit = `recordsParam=filterId&filterId=${urlSearchParams.get("filterId")}` // } + try + { + const qInstance = await QClient.getInstance().loadMetaData(); + setQInstance(qInstance); + } + catch (e) + { + setProcessError("Error loading process definition."); + return; + } + try { const processMetaData = await QClient.getInstance().loadProcessMetaData(processName); setProcessMetaData(processMetaData); setSteps(processMetaData.frontendSteps); + if (processMetaData.tableName) + { + try + { + const tableMetaData = await QClient.getInstance().loadTableMetaData(processMetaData.tableName); + setTableMetaData(tableMetaData); + } + catch (e) + { + setProcessError("Error loading process's table definition."); + return; + } + } } catch (e) { @@ -714,6 +821,8 @@ function ProcessRun({process}: Props): JSX.Element "content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366", }; + setProcessValues({}); + setRecords([]); setLastProcessResponse(new QJobRunning({message: "Working..."})); setTimeout(async () => @@ -729,15 +838,42 @@ function ProcessRun({process}: Props): JSX.Element }); }; + const handleCancelClicked = () => + { + const pathParts = location.pathname.split(/\//); + pathParts.pop(); + const path = pathParts.join("/"); + navigate(path, {replace: true}); + }; + + const mainCardStyles: any = {}; + mainCardStyles.minHeight = "calc(100vh - 400px)"; + if (qJobRunning || activeStep === null) + { + mainCardStyles.background = "none"; + mainCardStyles.boxShadow = "none"; + } + + let nextButtonLabel = "Next"; + let nextButtonIcon = "arrow_forward"; + if (overrideOnLastStep !== null) + { + if (overrideOnLastStep) + { + nextButtonLabel = "Submit"; + nextButtonIcon = "check"; + } + } + else if (onLastStep) + { + nextButtonLabel = "Submit"; + nextButtonIcon = "check"; + } + return ( - + {({ - values, errors, touched, isSubmitting, + values, errors, touched, isSubmitting, setFieldValue, }) => (
- + {steps.map((step) => ( @@ -760,6 +896,7 @@ function ProcessRun({process}: Props): JSX.Element ))} + {/*************************************************************************** @@ -777,28 +914,18 @@ function ProcessRun({process}: Props): JSX.Element processError, processValues, recordConfig, + setFieldValue, )} {/******************************** ** back &| next/submit buttons ** ********************************/} - + {true || activeStepIndex === 0 ? ( ) : ( - - back - + back )} - {noMoreSteps || processError || qJobRunning ? ( + {processError || qJobRunning || !activeStep ? ( ) : ( <> @@ -807,14 +934,19 @@ function ProcessRun({process}: Props): JSX.Element {formError} )} - - {onLastStep ? "submit" : "next"} - + { + noMoreSteps && + } + { + !noMoreSteps && ( + + + + + + + ) + } )} diff --git a/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx b/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx new file mode 100644 index 0000000..55dacf9 --- /dev/null +++ b/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx @@ -0,0 +1,138 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; +import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; +import {ListItem} from "@mui/material"; +import Icon from "@mui/material/Icon"; +import ListItemText from "@mui/material/ListItemText"; +import Tooltip from "@mui/material/Tooltip"; +import {Link} from "react-router-dom"; +import IconButton from "@mui/material/IconButton"; +import React from "react"; +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; +import QTableUtils from "qqq/utils/QTableUtils"; +import MDBox from "components/MDBox"; + +/******************************************************************************* + ** Entity that corresponds to qqq backend's ProcessSummaryLine - with methods + ** to help display properly in a process review screen. + *******************************************************************************/ +// eslint-disable-next-line import/prefer-default-export +export class ProcessSummaryLine +{ + status: "OK" | "INFO" | "WARNING" | "ERROR"; + + count: number; + + message: string; + + primaryKeys: any[]; + + constructor(processSummaryLine: any) + { + this.status = processSummaryLine.status; + this.count = processSummaryLine.count; + this.message = processSummaryLine.message; + this.primaryKeys = processSummaryLine.primaryKeys; + } + + getProcessSummaryListItem(i: number, table: QTableMetaData, qInstance: QInstance, isResultScreen: boolean = false): JSX.Element + { + return ( + + + {this.getIcon(isResultScreen)} + + {this.count.toLocaleString()} + {" "} + {this.message} + + { + table && this.primaryKeys && ( + + + open_in_new + + + ) + } + + + ); + } + + private getColor(): "success" | "info" | "warning" | "error" | "secondary" + { + if (this.status === "OK") + { + return "success"; + } + else if (this.status === "INFO") + { + return "info"; + } + else if (this.status === "WARNING") + { + return "warning"; + } + else if (this.status === "ERROR") + { + return "error"; + } + else + { + return "secondary"; + } + } + + private getIcon(isResultScreen: boolean): string + { + if (this.status === "OK") + { + return isResultScreen ? "check" : "arrow_forward"; + } + else if (this.status === "INFO") + { + return "info"; + } + else if (this.status === "WARNING") + { + return "warning_amber"; + } + else if (this.status === "ERROR") + { + return "report"; + } + return ""; + } + + private getLinkToRecords(table: QTableMetaData, qInstance: QInstance): string + { + const tablePath = qInstance.getTablePath(table); + const filter = new QQueryFilter([new QFilterCriteria(table.primaryKeyField, QCriteriaOperator.IN, this.primaryKeys)]); + console.log("Link to records:"); + console.log(filter); + return (`${tablePath}?filter=${JSON.stringify(filter)}`); + } +} diff --git a/src/qqq/utils/QFilterUtils.ts b/src/qqq/utils/QFilterUtils.ts new file mode 100644 index 0000000..5728661 --- /dev/null +++ b/src/qqq/utils/QFilterUtils.ts @@ -0,0 +1,229 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; +import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; + +/******************************************************************************* + ** Utility class for working with QQQ Filters + ** + *******************************************************************************/ +class QFilterUtils +{ + /******************************************************************************* + ** Convert a grid operator to a QQQ Criteria Operator. + *******************************************************************************/ + public static gridCriteriaOperatorToQQQ = (operator: string): QCriteriaOperator => + { + switch (operator) + { + case "contains": + return QCriteriaOperator.CONTAINS; + case "startsWith": + return QCriteriaOperator.STARTS_WITH; + case "endsWith": + return QCriteriaOperator.ENDS_WITH; + case "is": + case "equals": + case "=": + return QCriteriaOperator.EQUALS; + case "isNot": + case "!=": + return QCriteriaOperator.NOT_EQUALS; + case "after": + case ">": + return QCriteriaOperator.GREATER_THAN; + case "onOrAfter": + case ">=": + return QCriteriaOperator.GREATER_THAN_OR_EQUALS; + case "before": + case "<": + return QCriteriaOperator.LESS_THAN; + case "onOrBefore": + case "<=": + return QCriteriaOperator.LESS_THAN_OR_EQUALS; + case "isEmpty": + return QCriteriaOperator.IS_BLANK; + case "isNotEmpty": + return QCriteriaOperator.IS_NOT_BLANK; + case "isAny": + return QCriteriaOperator.IN; + case "isNone": // todo - verify - not seen in UI + return QCriteriaOperator.NOT_IN; + default: + return QCriteriaOperator.EQUALS; + } + }; + + /******************************************************************************* + ** Convert a qqq criteria operator to one expected by the grid. + *******************************************************************************/ + public static qqqCriteriaOperatorToGrid = (operator: QCriteriaOperator, fieldType: QFieldType = QFieldType.STRING): string => + { + switch (operator) + { + case QCriteriaOperator.EQUALS: + switch (fieldType) + { + case QFieldType.INTEGER: + case QFieldType.DECIMAL: + return ("="); + case QFieldType.DATE: + case QFieldType.TIME: + case QFieldType.DATE_TIME: + return ("equals"); + case QFieldType.BOOLEAN: + case QFieldType.STRING: + case QFieldType.TEXT: + case QFieldType.HTML: + case QFieldType.PASSWORD: + case QFieldType.BLOB: + default: + return ("is"); + } + case QCriteriaOperator.NOT_EQUALS: + switch (fieldType) + { + case QFieldType.INTEGER: + case QFieldType.DECIMAL: + return ("!="); + case QFieldType.DATE: + case QFieldType.TIME: + case QFieldType.DATE_TIME: + case QFieldType.BOOLEAN: + case QFieldType.STRING: + case QFieldType.TEXT: + case QFieldType.HTML: + case QFieldType.PASSWORD: + case QFieldType.BLOB: + default: + return ("isNot"); + } + case QCriteriaOperator.IN: + return ("isAny"); + case QCriteriaOperator.NOT_IN: + return ("isNone"); // todo verify - not seen in UI + case QCriteriaOperator.STARTS_WITH: + return ("startsWith"); + case QCriteriaOperator.ENDS_WITH: + return ("endsWith"); + case QCriteriaOperator.CONTAINS: + return ("contains"); + case QCriteriaOperator.NOT_STARTS_WITH: + return (""); // todo - not supported in grid? + case QCriteriaOperator.NOT_ENDS_WITH: + return (""); // todo - not supported in grid? + case QCriteriaOperator.NOT_CONTAINS: + return (""); // todo - not supported in grid? + case QCriteriaOperator.LESS_THAN: + switch (fieldType) + { + case QFieldType.DATE: + case QFieldType.TIME: + case QFieldType.DATE_TIME: + return ("before"); + default: + return ("<"); + } + case QCriteriaOperator.LESS_THAN_OR_EQUALS: + switch (fieldType) + { + case QFieldType.DATE: + case QFieldType.TIME: + case QFieldType.DATE_TIME: + return ("onOrBefore"); + default: + return ("<="); + } + case QCriteriaOperator.GREATER_THAN: + switch (fieldType) + { + case QFieldType.DATE: + case QFieldType.TIME: + case QFieldType.DATE_TIME: + return ("after"); + default: + return (">"); + } + case QCriteriaOperator.GREATER_THAN_OR_EQUALS: + switch (fieldType) + { + case QFieldType.DATE: + case QFieldType.TIME: + case QFieldType.DATE_TIME: + return ("onOrAfter"); + default: + return (">="); + } + case QCriteriaOperator.IS_BLANK: + return ("isEmpty"); + case QCriteriaOperator.IS_NOT_BLANK: + return ("isNotEmpty"); + case QCriteriaOperator.BETWEEN: + return (""); // todo - not supported in grid? + case QCriteriaOperator.NOT_BETWEEN: + return (""); // todo - not supported in grid? + default: + console.warn(`Unhandled criteria operator: ${operator}`); + return ("="); + } + }; + + /******************************************************************************* + ** the values object needs handled differently based on cardinality of the operator. + ** that is - qqq always wants a list, but the grid provides it differently per-operator. + ** for single-values (the default), we must wrap it in an array. + ** for non-values (e.g., blank), set it to null. + ** for list-values, it's already in an array, so don't wrap it. + *******************************************************************************/ + public static gridCriteriaValueToQQQ = (operator: QCriteriaOperator, value: any): any[] => + { + if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK) + { + return (null); + } + else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN) + { + return (value); + } + + return ([value]); + }; + + /******************************************************************************* + ** + *******************************************************************************/ + public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[]): any | any[] => + { + if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK) + { + return (null); // todo - verify + } + else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN) + { + return (values); + } + + return (values[0]); + }; +} + +export default QFilterUtils; diff --git a/src/qqq/utils/QValueUtils.ts b/src/qqq/utils/QValueUtils.ts index 00bdb87..062617c 100644 --- a/src/qqq/utils/QValueUtils.ts +++ b/src/qqq/utils/QValueUtils.ts @@ -56,6 +56,11 @@ class QValueUtils return (displayValue); } + if (displayValue === undefined && rawValue !== undefined) + { + return (rawValue); + } + return (displayValue); } } From 8b2364b596b3526e7c77a66bc8eaf13fbc20c972 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 29 Aug 2022 14:38:59 -0500 Subject: [PATCH 2/5] Update to qqq-frontend-core 1.0.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b332395..7145fd9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@fullcalendar/interaction": "5.10.0", "@fullcalendar/react": "5.10.0", "@fullcalendar/timegrid": "5.10.0", - "@kingsrook/qqq-frontend-core": "1.0.12", + "@kingsrook/qqq-frontend-core": "1.0.13", "@mui/icons-material": "5.4.1", "@mui/material": "5.4.1", "@mui/styled-engine": "5.4.1", From 3eb1cd73a7ea34457c0cf86bc12d05bd400d14fe Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 30 Aug 2022 13:47:26 -0500 Subject: [PATCH 3/5] QQQ-37 checkpoint - cleanup of various kinds on process validation & result --- .../components/QProcessSummaryResults.tsx | 12 +- .../components/QValidationReview.tsx | 129 +++++++++--------- src/qqq/pages/process-run/index.tsx | 37 ++++- .../process-run/model/ProcessSummaryLine.tsx | 39 ++++-- src/qqq/styles/qqq-override-styles.css | 7 + 5 files changed, 138 insertions(+), 86 deletions(-) diff --git a/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx b/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx index f4a1ac5..68f5b83 100644 --- a/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx +++ b/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx @@ -48,15 +48,17 @@ function QProcessSummaryResults({ qInstance, process, table = null, processValues, step, }: Props): JSX.Element { + const sourceTable = qInstance.tables.get(processValues.sourceTable); + const resultValidationList = ( { - processValues?.recordCount && table && ( + processValues?.recordCount !== undefined && sourceTable && ( {processValues.recordCount.toLocaleString()} {" "} - {table.label} + {sourceTable.label} {" "} records were processed. @@ -65,7 +67,7 @@ function QProcessSummaryResults({ } { - processValues.processResults && processValues.processResults.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, table, qInstance, true))) + processValues.processResults && processValues.processResults.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTable, qInstance, true))) } @@ -79,8 +81,8 @@ function QProcessSummaryResults({ - {process.iconName} - {`${process.label} : ${step.label}`} + {process.iconName && {process.iconName}} + Process Summary {resultValidationList} diff --git a/src/qqq/pages/process-run/components/QValidationReview.tsx b/src/qqq/pages/process-run/components/QValidationReview.tsx index d5f4357..472a17c 100644 --- a/src/qqq/pages/process-run/components/QValidationReview.tsx +++ b/src/qqq/pages/process-run/components/QValidationReview.tsx @@ -64,6 +64,7 @@ function QValidationReview({ }: Props): JSX.Element { const [previewRecordIndex, setPreviewRecordIndex] = useState(0); + const sourceTable = qInstance.tables.get(processValues.sourceTable); const updatePreviewRecordIndex = (offset: number) => { @@ -89,15 +90,44 @@ function QValidationReview({ }, }); + const buildDoFullValidationRadioListItem = (value: "true" | "false", labelText: string, tooltipHTML: JSX.Element): JSX.Element => + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // split up the label into words - then we'll display the last word by itself with a non-breaking space, no-wrap-glued to the button. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const labelWords = labelText.split(" "); + const lastWord = labelWords[labelWords.length - 1]; + labelWords.splice(labelWords.length - 1, 1); + + return ( + + } + label={( + + {`${labelWords.join(" ")} `} + + {/* eslint-disable-next-line react/jsx-one-expression-per-line */} + {lastWord}.  + info_outlined + {/* eslint-disable-next-line react/jsx-closing-tag-location */} + + + + )} + /> + + ); + }; + const preValidationList = ( { - processValues?.recordCount && table && ( + processValues?.recordCount !== undefined && sourceTable && ( - You selected - {` ${processValues.recordCount.toLocaleString()} ${table?.label} `} - records. + {`Input: ${processValues.recordCount.toLocaleString()} ${sourceTable?.label} record${processValues.recordCount === 1 ? "" : "s"}.`} ) @@ -108,59 +138,36 @@ function QValidationReview({ How would you like to proceed? - + - - } - label={( - - Perform Validation on all records before processing. - - If you choose this option, a Validation step will run on all of the records that you selected. - You will then be told how many can process successfully, and how many have issues. -
-
- Running this validation may take several minutes, depending on the complexity of the work, and the number of records. -
-
- Choose this option if you want more information about what will happen, and you are willing to wait for that information. - - )} - > - info_outlined -
-
- )} - /> -
- - } - label={( - - Skip Validation. Submit the records for immediate processing. - - If you choose this option, the records you selected will immediately be processed. - You will be told how many records were successfully processed, and which ones had issues after the processing is completed. -
-
- Choose this option if you feel that you do not need this information, or are not willing to wait for it. - - )} - > - info_outlined -
-
- )} - /> -
+ {buildDoFullValidationRadioListItem( + "true", + "Perform Validation on all records before processing", ( +
+ If you choose this option, a Validation step will run on all of the input records. + You will then be told how many can process successfully, and how many have issues. +
+
+ Running this validation may take several minutes, depending on the complexity of the work, and the number of records. +
+
+ Choose this option if you want more information about what will happen, and you are willing to wait for that information. +
+ ), + )} + + {buildDoFullValidationRadioListItem( + "false", + "Skip Validation. Submit the records for immediate processing", ( +
+ If you choose this option, the records input records will immediately be processed. + You will be told how many records were successfully processed, and which ones had issues after the processing is completed. +
+
+ Choose this option if you feel that you do not need this information, or are not willing to wait for it. +
+ ), + )}
@@ -172,11 +179,11 @@ function QValidationReview({ const postValidationList = ( { - processValues?.recordCount && table && ( + processValues?.recordCount !== undefined && sourceTable && ( Validation complete on - {` ${processValues.recordCount.toLocaleString()} ${table?.label} `} + {` ${processValues.recordCount.toLocaleString()} ${sourceTable?.label} `} records. @@ -184,7 +191,7 @@ function QValidationReview({ } { - processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, table, qInstance))) + processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTable, qInstance))) } @@ -199,9 +206,9 @@ function QValidationReview({ { - previewRecords && previewRecords.length > 0 ? ( + processValues?.previewMessage && previewRecords && previewRecords.length > 0 ? ( <> - This is a preview of the records that will be created. + {processValues?.previewMessage} diff --git a/src/qqq/pages/process-run/index.tsx b/src/qqq/pages/process-run/index.tsx index 262b07a..b06e0db 100644 --- a/src/qqq/pages/process-run/index.tsx +++ b/src/qqq/pages/process-run/index.tsx @@ -21,7 +21,9 @@ import * as Yup from "yup"; -import {CircularProgress, TablePagination} from "@mui/material"; +import { + Button, CircularProgress, Icon, TablePagination, +} from "@mui/material"; import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro"; // formik components import {Form, Formik} from "formik"; @@ -96,8 +98,13 @@ function ProcessRun({process}: Props): JSX.Element const [lastProcessResponse, setLastProcessResponse] = useState( null as QJobStarted | QJobComplete | QJobError | QJobRunning, ); + const [showErrorDetail, setShowErrorDetail] = useState(false); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // the validation screen - it can change whether next is actually the final step or not... so, use this state field to track that. // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean); + const onLastStep = activeStepIndex === steps.length - 2; const noMoreSteps = activeStepIndex === steps.length - 1; @@ -199,6 +206,11 @@ function ProcessRun({process}: Props): JSX.Element return ({value}); }; + const toggleShowErrorDetail = () => + { + setShowErrorDetail(!showErrorDetail); + }; + //////////////////////////////////////////////////// // generate the main form body content for a step // //////////////////////////////////////////////////// @@ -216,11 +228,24 @@ function ProcessRun({process}: Props): JSX.Element { return ( <> - + Error - {processError} + An error occurred while running the process: + {" "} + {process.label} + + + + + {processError} + + + ); @@ -326,8 +351,7 @@ function ProcessRun({process}: Props): JSX.Element ////////////////////////////////////////////////////////////// setFieldValue("doFullValidation", value); - // eslint-disable-next-line no-unneeded-ternary - setOverrideOnLastStep(value === "true" ? false : true); + setOverrideOnLastStep(value !== "true"); }} /> ) @@ -823,6 +847,7 @@ function ProcessRun({process}: Props): JSX.Element setProcessValues({}); setRecords([]); + setOverrideOnLastStep(null); setLastProcessResponse(new QJobRunning({message: "Working..."})); setTimeout(async () => @@ -848,7 +873,7 @@ function ProcessRun({process}: Props): JSX.Element const mainCardStyles: any = {}; mainCardStyles.minHeight = "calc(100vh - 400px)"; - if (qJobRunning || activeStep === null) + if (!processError && (qJobRunning || activeStep === null)) { mainCardStyles.background = "none"; mainCardStyles.boxShadow = "none"; diff --git a/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx b/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx index 55dacf9..04a0b3d 100644 --- a/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx +++ b/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx @@ -59,24 +59,37 @@ export class ProcessSummaryLine getProcessSummaryListItem(i: number, table: QTableMetaData, qInstance: QInstance, isResultScreen: boolean = false): JSX.Element { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // split up the message into words - then we'll display the last word by itself with a non-breaking space, no-wrap-glued to the button. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const messageWords = this.message ? this.message.split(" ") : []; + const lastWord = messageWords.length > 1 ? messageWords[messageWords.length - 1] : ""; + if (messageWords.length > 1) + { + messageWords.splice(messageWords.length - 1, 1); + } + return ( {this.getIcon(isResultScreen)} - {this.count.toLocaleString()} - {" "} - {this.message} + {/* work hard to prevent the icon from falling down to the next line by itself... */} + {`${this.count.toLocaleString()} ${messageWords.join(" ")} `} + { + (table && this.primaryKeys) ? ( + + {/* eslint-disable-next-line react/jsx-one-expression-per-line */} + {lastWord}  + + open_in_new + + {/* eslint-disable-next-line react/jsx-closing-tag-location */} + + + ) : {lastWord} + } - { - table && this.primaryKeys && ( - - - open_in_new - - - ) - } ); @@ -131,8 +144,6 @@ export class ProcessSummaryLine { const tablePath = qInstance.getTablePath(table); const filter = new QQueryFilter([new QFilterCriteria(table.primaryKeyField, QCriteriaOperator.IN, this.primaryKeys)]); - console.log("Link to records:"); - console.log(filter); return (`${tablePath}?filter=${JSON.stringify(filter)}`); } } diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index 4f865a0..4559740 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -110,3 +110,10 @@ margin-left: 40px; font-size: 14px; } + +/* Help make the radio, text, and icon wrap in a good way */ +.doFullValidationRadios label +{ + display: flex; + align-items: flex-start; +} From 328280864c2703323fed9ee06f5fdfcf914f8b0f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 31 Aug 2022 16:22:28 -0500 Subject: [PATCH 4/5] Updated imports --- .../components/QProcessSummaryResults.tsx | 14 ++++---- .../components/QValidationReview.tsx | 32 ++++++++----------- .../process-run/model/ProcessSummaryLine.tsx | 11 +++---- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx b/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx index 68f5b83..3b1bf4c 100644 --- a/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx +++ b/src/qqq/pages/process-run/components/QProcessSummaryResults.tsx @@ -19,18 +19,18 @@ * along with this program. If not, see . */ -import React from "react"; +import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; -import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; -import List from "@mui/material/List"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {ListItem} from "@mui/material"; -import ListItemText from "@mui/material/ListItemText"; -import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine"; -import MDBox from "components/MDBox"; import Grid from "@mui/material/Grid"; import Icon from "@mui/material/Icon"; -import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import List from "@mui/material/List"; +import ListItemText from "@mui/material/ListItemText"; +import React from "react"; +import MDBox from "components/MDBox"; +import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine"; interface Props { diff --git a/src/qqq/pages/process-run/components/QValidationReview.tsx b/src/qqq/pages/process-run/components/QValidationReview.tsx index 472a17c..027e25b 100644 --- a/src/qqq/pages/process-run/components/QValidationReview.tsx +++ b/src/qqq/pages/process-run/components/QValidationReview.tsx @@ -19,28 +19,24 @@ * along with this program. If not, see . */ -import List from "@mui/material/List"; -import { - Button, FormControlLabel, ListItem, Radio, RadioGroup, tooltipClasses, TooltipProps, -} from "@mui/material"; -import ListItemText from "@mui/material/ListItemText"; -import Icon from "@mui/material/Icon"; -import MDBox from "components/MDBox"; -import MDTypography from "components/MDTypography"; -import Grid from "@mui/material/Grid"; -import React, {useState} from "react"; import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; -import QValueUtils from "qqq/utils/QValueUtils"; -import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; -import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine"; -import {Field} from "formik"; -import Tooltip from "@mui/material/Tooltip"; -import IconButton from "@mui/material/IconButton"; -import {styled} from "@mui/material/styles"; -import QTableUtils from "qqq/utils/QTableUtils"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {Button, FormControlLabel, ListItem, Radio, RadioGroup, tooltipClasses, TooltipProps} from "@mui/material"; +import Grid from "@mui/material/Grid"; +import Icon from "@mui/material/Icon"; +import IconButton from "@mui/material/IconButton"; +import List from "@mui/material/List"; +import ListItemText from "@mui/material/ListItemText"; +import {styled} from "@mui/material/styles"; +import Tooltip from "@mui/material/Tooltip"; +import React, {useState} from "react"; +import MDBox from "components/MDBox"; +import MDTypography from "components/MDTypography"; +import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine"; +import QValueUtils from "qqq/utils/QValueUtils"; interface Props { diff --git a/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx b/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx index 04a0b3d..92fafa9 100644 --- a/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx +++ b/src/qqq/pages/process-run/model/ProcessSummaryLine.tsx @@ -19,19 +19,18 @@ * along with this program. If not, see . */ +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; -import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; -import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; +import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; +import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {ListItem} from "@mui/material"; import Icon from "@mui/material/Icon"; +import IconButton from "@mui/material/IconButton"; import ListItemText from "@mui/material/ListItemText"; import Tooltip from "@mui/material/Tooltip"; -import {Link} from "react-router-dom"; -import IconButton from "@mui/material/IconButton"; import React from "react"; -import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; -import QTableUtils from "qqq/utils/QTableUtils"; +import {Link} from "react-router-dom"; import MDBox from "components/MDBox"; /******************************************************************************* From 8c00bf5aa5d7077d898fabb9a8b0bd43dacb79e0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 31 Aug 2022 16:26:54 -0500 Subject: [PATCH 5/5] Updated imports --- src/qqq/utils/QFilterUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qqq/utils/QFilterUtils.ts b/src/qqq/utils/QFilterUtils.ts index 5728661..42a3774 100644 --- a/src/qqq/utils/QFilterUtils.ts +++ b/src/qqq/utils/QFilterUtils.ts @@ -19,8 +19,8 @@ * along with this program. If not, see . */ -import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; /******************************************************************************* ** Utility class for working with QQQ Filters