/* * 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 {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 {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, Typography} from "@mui/material"; import Box from "@mui/material/Box"; 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 MDTypography from "qqq/components/legacy/MDTypography"; import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip"; import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"; import {ProcessSummaryLine} from "qqq/models/processes/ProcessSummaryLine"; import {renderSectionOfFields} from "qqq/pages/records/view/RecordView"; import Client from "qqq/utils/qqq/Client"; import TableUtils from "qqq/utils/qqq/TableUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; import React, {useEffect, useState} from "react"; interface Props { qInstance: QInstance, process: QProcessMetaData, table: QTableMetaData, processValues: any, step: QFrontendStepMetaData, previewRecords: QRecord[], formValues: any, doFullValidationRadioChangedHandler: any, loadingRecords?: boolean } //////////////////////////////////////////////////////////////////////////// // e.g., for bulk-load, where we want to show associations under a record // // the processValue will have these data, to drive this screen. // //////////////////////////////////////////////////////////////////////////// interface AssociationPreview { tableName: string; widgetName: string; associationName: string; } /******************************************************************************* ** 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 ValidationReview({ qInstance, process, table = null, processValues, step, previewRecords = [], formValues, doFullValidationRadioChangedHandler, loadingRecords }: Props): JSX.Element { const [previewRecordIndex, setPreviewRecordIndex] = useState(0); const [sourceTableMetaData, setSourceTableMetaData] = useState(null as QTableMetaData); const [previewTableMetaData, setPreviewTableMetaData] = useState(null as QTableMetaData); const [childTableMetaData, setChildTableMetaData] = useState({} as { [name: string]: QTableMetaData }); const [associationPreviewsByWidgetName, setAssociationPreviewsByWidgetName] = useState({} as { [widgetName: string]: AssociationPreview }); if (processValues.sourceTable && !sourceTableMetaData) { (async () => { const sourceTableMetaData = await Client.getInstance().loadTableMetaData(processValues.sourceTable); setSourceTableMetaData(sourceTableMetaData); })(); } //////////////////////////////////////////////////////////////////////////////////////// // load meta-data and set up associations-data structure, if so directed from backend // //////////////////////////////////////////////////////////////////////////////////////// useEffect(() => { if (processValues.formatPreviewRecordUsingTableLayout && !previewTableMetaData) { (async () => { const previewTableMetaData = await Client.getInstance().loadTableMetaData(processValues.formatPreviewRecordUsingTableLayout); setPreviewTableMetaData(previewTableMetaData); })(); } try { const previewRecordAssociatedTableNames: string[] = processValues.previewRecordAssociatedTableNames ?? []; const previewRecordAssociatedWidgetNames: string[] = processValues.previewRecordAssociatedWidgetNames ?? []; const previewRecordAssociationNames: string[] = processValues.previewRecordAssociationNames ?? []; const associationPreviewsByWidgetName: { [widgetName: string]: AssociationPreview } = {}; for (let i = 0; i < Math.min(previewRecordAssociatedTableNames.length, previewRecordAssociatedWidgetNames.length, previewRecordAssociationNames.length); i++) { const associationPreview = {tableName: previewRecordAssociatedTableNames[i], widgetName: previewRecordAssociatedWidgetNames[i], associationName: previewRecordAssociationNames[i]}; associationPreviewsByWidgetName[associationPreview.widgetName] = associationPreview; } setAssociationPreviewsByWidgetName(associationPreviewsByWidgetName); if (Object.keys(associationPreviewsByWidgetName)) { (async () => { for (let key in associationPreviewsByWidgetName) { const associationPreview = associationPreviewsByWidgetName[key]; childTableMetaData[associationPreview.tableName] = await Client.getInstance().loadTableMetaData(associationPreview.tableName); setChildTableMetaData(Object.assign({}, childTableMetaData)); } })(); } } catch (e) { console.log(`Error setting up association previews: ${e}`); } }, []); /*************************************************************************** ** ***************************************************************************/ 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 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 !== undefined && sourceTableMetaData && ( {`Input: ${ValueUtils.getFormattedNumber(processValues.recordCount)} ${sourceTableMetaData?.label} record${processValues.recordCount === 1 ? "" : "s"}.`} ) } { processValues?.supportsFullValidation && formValues && formValues.doFullValidation !== undefined && ( <> How would you like to proceed? {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 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.
), )}
) }
); const postValidationList = ( { processValues?.recordCount !== undefined && sourceTableMetaData && ( Validation complete on {` ${ValueUtils.getFormattedNumber(processValues.recordCount)} ${sourceTableMetaData?.label} ${processValues.recordCount === 1 ? "record." : "records."}`} ) } { processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTableMetaData, qInstance))) } ); const recordPreviewWidget = step.recordListFields && ( Preview { loadingRecords ? Loading... : <> { processValues?.previewMessage && previewRecords && previewRecords.length > 0 ? ( <> {processValues?.previewMessage} 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 && !processValues.formatPreviewRecordUsingTableLayout && previewRecords[previewRecordIndex] && step.recordListFields.map((field) => ( {`${field.label}:`} {" "}   {" "} {ValueUtils.getDisplayValue(field, previewRecords[previewRecordIndex], "view")} )) } { previewRecords && processValues.formatPreviewRecordUsingTableLayout && previewRecords[previewRecordIndex] && } { previewRecords && previewRecords.length > 0 && ( {`Preview ${previewRecordIndex + 1} of ${previewRecords.length}`} ) } ); return ( {processValues.validationSummary ? postValidationList : preValidationList} {recordPreviewWidget} ); } interface PreviewRecordUsingTableLayoutProps { index: number record: QRecord, tableMetaData: QTableMetaData, qInstance: QInstance, associationPreviewsByWidgetName: { [widgetName: string]: AssociationPreview }, childTableMetaData: { [name: string]: QTableMetaData }, } function PreviewRecordUsingTableLayout({record, tableMetaData, qInstance, associationPreviewsByWidgetName, childTableMetaData, index}: PreviewRecordUsingTableLayoutProps): JSX.Element { if (!tableMetaData) { return (Loading...); } const renderedSections: JSX.Element[] = []; const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData); for (let i = 0; i < tableSections.length; i++) { const section = tableSections[i]; if (section.isHidden) { continue; } if (section.fieldNames) { renderedSections.push(

{section.label}

{renderSectionOfFields(section.name, section.fieldNames, tableMetaData, false, record, undefined, {label: {fontWeight: "500"}})}
); } else if (section.widgetName) { const widget = qInstance.widgets.get(section.widgetName); if (widget) { let data: ChildRecordListData = null; if (associationPreviewsByWidgetName[section.widgetName]) { const associationPreview = associationPreviewsByWidgetName[section.widgetName]; const associationRecords = record.associatedRecords?.get(associationPreview.associationName) ?? []; data = { canAddChildRecord: false, childTableMetaData: childTableMetaData[associationPreview.tableName], defaultValuesForNewChildRecords: {}, disabledFieldsForNewChildRecords: {}, queryOutput: {records: associationRecords}, totalRows: associationRecords.length, tablePath: "", title: "", viewAllLink: "", }; renderedSections.push( { data &&

{section.label}

}
); } } } } return <>{renderedSections}; } export default ValidationReview;