SPRINT-18: fixed to dashboards, removed and moved around all the things

This commit is contained in:
Tim Chamberlain
2023-01-04 11:40:21 -06:00
parent e49f178738
commit 267580b44b
460 changed files with 9717 additions and 11057 deletions

View File

@ -0,0 +1,197 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {colors} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
import {useGoogleLogin} from "@react-oauth/google";
import {useFormikContext} from "formik";
import React, {useEffect, useState} from "react";
import useDrivePicker from "react-google-drive-picker";
import MDTypography from "qqq/components/legacy/MDTypography";
interface Props
{
showDefaultFoldersView: boolean;
showSharedDrivesView: boolean;
qInstance: QInstance;
}
export function GoogleDriveFolderPicker({showDefaultFoldersView, showSharedDrivesView, qInstance}: Props): JSX.Element
{
const clientId = "649816208522-m6oa971vqicrc1hlam7333pt4qck0tm8.apps.googleusercontent.com";
const appApiKey = "AIzaSyBhXK34CF2fUfCgUS1VIHoKZbHxEBuHtDM";
if(!clientId)
{
console.error("Missing environmentValue GOOGLE_APP_CLIENT_ID")
}
if(!appApiKey)
{
console.error("Missing environmentValue GOOGLE_APP_API_KEY")
}
const [ selectedGoogleFolderName, setSelectedGoogleFolderName ] = useState(null as string);
const [ selectedGoogleFolderId, setSelectedGoogleFolderId ] = useState(null as string);
const [ googleToken, setGoogleToken ] = useState(null as string); // maybe get from cookie/local-storage
const [ errorMessage, setErrorMessage ] = useState(null as string);
const [ openPicker, authResponse ] = useDrivePicker();
const formikProps = useFormikContext();
const loginOrOpenPicker = (token: string) =>
{
if(token)
{
handleOpenPicker(token);
}
else
{
login();
}
};
const driveScope = "https://www.googleapis.com/auth/drive"
const login = useGoogleLogin({
scope: driveScope,
onSuccess: tokenResponse =>
{
console.log("Token response");
console.log(tokenResponse);
if(tokenResponse.scope.indexOf(driveScope) == -1)
{
setErrorMessage("You must allow access to Google Drive after you sign in. Please try again.")
return;
}
else
{
setErrorMessage(null)
}
setGoogleToken(tokenResponse.access_token)
handleOpenPicker(tokenResponse.access_token);
}
});
const handleOpenPicker = (token: string) =>
{
// @ts-ignore
const google = window.google
const customViewsArray: any[] = [];
if(showDefaultFoldersView)
{
customViewsArray.push(new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setIncludeFolders(true)
.setMode(google.picker.DocsViewMode.LIST)
.setSelectFolderEnabled(true));
}
if(showSharedDrivesView)
{
customViewsArray.push(new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setEnableDrives(true)
.setIncludeFolders(true)
.setMode(google.picker.DocsViewMode.LIST)
.setSelectFolderEnabled(true));
}
openPicker({
clientId: clientId,
developerKey: appApiKey,
viewId: "FOLDERS",
token: token, // pass oauth token in case you already have one
disableDefaultView: (customViewsArray.length > 0), // if we have any custom views, then disable the default.
showUploadView: false,
showUploadFolders: false,
supportDrives: true,
multiselect: false,
setSelectFolderEnabled: true,
setIncludeFolders: true,
customViews: customViewsArray,
callbackFunction: (data) =>
{
if (data.action === "cancel")
{
console.log("User clicked cancel/close button");
setSelectedGoogleFolderId(null);
setSelectedGoogleFolderName(null);
}
else if (data.action === "picked")
{
console.log(data);
const mimeType = data.docs[0].mimeType;
if(mimeType === "application/vnd.google-apps.folder")
{
setSelectedGoogleFolderId(data.docs[0].id);
setSelectedGoogleFolderName(data.docs[0].name);
setErrorMessage(null)
}
else
{
setSelectedGoogleFolderId(null);
setSelectedGoogleFolderName(null);
setErrorMessage("You selected a file, but a folder is required.")
}
}
else
{
console.log("Called with un-recognized action:");
console.log(data);
}
},
});
};
useEffect(() =>
{
formikProps.setFieldValue("googleDriveAccessToken", googleToken);
formikProps.setFieldValue("googleDriveFolderId", selectedGoogleFolderId);
formikProps.setFieldValue("googleDriveFolderName", selectedGoogleFolderName);
}, [selectedGoogleFolderId, selectedGoogleFolderName])
return (
<Grid item xs={12} sm={6}>
<Box mb={1.5}>
<Box display="flex" alignItems="center">
<Button variant="outlined" onClick={() => loginOrOpenPicker(googleToken)}>
<span style={{color: colors.lightBlue[500]}}>Select Google Drive Folder</span>
</Button>
<Box ml={1} fontSize={"1rem"}>
{selectedGoogleFolderName}
</Box>
</Box>
<Box mt={0.75}>
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
<div className="fieldErrorMessage">{errorMessage}</div>
</MDTypography>
</Box>
</Box>
</Grid>
);
}
GoogleDriveFolderPicker.defaultProps = {
showDefaultFoldersView: true,
showSharedDrivesView: true
};

View File

@ -0,0 +1,48 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {GoogleOAuthProvider} from "@react-oauth/google";
import React from "react";
import {GoogleDriveFolderPicker} from "qqq/components/processes/GoogleDriveFolderPicker";
interface Props
{
showDefaultFoldersView: boolean;
showSharedDrivesView: boolean;
qInstance: QInstance;
}
export function GoogleDriveFolderPickerWrapper({showDefaultFoldersView, showSharedDrivesView, qInstance}: Props): JSX.Element
{
const clientId = qInstance.environmentValues.get("GOOGLE_APP_CLIENT_ID") || process.env.REACT_APP_GOOGLE_APP_CLIENT_ID;
return (
<GoogleOAuthProvider clientId={clientId}>
<GoogleDriveFolderPicker showDefaultFoldersView={showDefaultFoldersView} showSharedDrivesView={showSharedDrivesView} qInstance={qInstance} />
</GoogleOAuthProvider>
);
}
GoogleDriveFolderPickerWrapper.defaultProps = {
showDefaultFoldersView: true,
showSharedDrivesView: true
};

View File

@ -0,0 +1,105 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon";
import {ReactNode} from "react";
import MDTypography from "qqq/components/legacy/MDTypography";
interface Props
{
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
isReport?: boolean;
title: string;
percentage?: {
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "white";
amount: string | number;
label: string;
};
icon: ReactNode;
[key: string]: any;
}
function ProcessLinkCard({
color, isReport, title, percentage, icon,
}: Props): JSX.Element
{
return (
<Card>
<Box display="flex" justifyContent="space-between" pt={3} px={2}>
<Box
color={color === "light" ? "dark" : "white"}
borderRadius="xl"
display="flex"
justifyContent="center"
alignItems="center"
width="4rem"
height="4rem"
mt={-3}
sx={{backgroundColor: color}}
>
<Icon fontSize="medium" color="inherit">
{icon}
</Icon>
</Box>
<Box textAlign="right" lineHeight={1.25}>
<MDTypography variant="button" fontWeight="bold" color="text">
{title}
</MDTypography>
</Box>
</Box>
<Divider />
<Box pb={2} px={2}>
<MDTypography component="p" variant="button" color="text" display="flex">
<MDTypography
component="span"
variant="button"
fontWeight="bold"
color={percentage.color}
>
{percentage.amount}
</MDTypography>
{
isReport
? `Click here to access the ${title} report.`
: `Click here to run the process called ${title}.`
}
{percentage.label}
</MDTypography>
</Box>
</Card>
);
}
ProcessLinkCard.defaultProps = {
color: "info",
isReport: false,
percentage: {
color: "success",
text: "",
label: "",
},
};
export default ProcessLinkCard;

View File

@ -0,0 +1,107 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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 {ListItem} from "@mui/material";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import List from "@mui/material/List";
import ListItemText from "@mui/material/ListItemText";
import React, {useState} from "react";
import {ProcessSummaryLine} from "qqq/models/processes/ProcessSummaryLine";
import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
interface Props
{
qInstance: QInstance;
process: QProcessMetaData;
table: QTableMetaData;
processValues: any;
step: QFrontendStepMetaData;
}
/*******************************************************************************
** This is the process summary result component.
*******************************************************************************/
function ProcessSummaryResults({
qInstance, process, table = null, processValues, step,
}: Props): JSX.Element
{
const [sourceTableMetaData, setSourceTableMetaData] = useState(null as QTableMetaData);
if(processValues.sourceTable && !sourceTableMetaData)
{
(async () =>
{
const sourceTableMetaData = await Client.getInstance().loadTableMetaData(processValues.sourceTable)
setSourceTableMetaData(sourceTableMetaData);
})();
}
const resultValidationList = (
<List sx={{mt: 2}}>
{
processValues?.recordCount !== undefined && sourceTableMetaData && (
<ListItem sx={{my: 2}}>
<ListItemText primaryTypographyProps={{fontSize: 16}}>
{ValueUtils.getFormattedNumber(processValues.recordCount)}
{" "}
{sourceTableMetaData.label}
{processValues.recordCount === 1 ? " record was" : " records were"} processed.
</ListItemText>
</ListItem>
)
}
<List>
{
processValues.processResults && processValues.processResults.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTableMetaData, qInstance, true)))
}
</List>
</List>
);
return (
<Box m={3} mt={6}>
<Grid container>
<Grid item xs={0} lg={2} />
<Grid item xs={12} lg={8}>
<Box border="1px solid rgb(70%, 70%, 70%)" borderRadius="lg" p={2} mt={2}>
<Box mt={-5} p={1} sx={{width: "fit-content", backgroundColor: "success"}} borderRadius=".25em" width="initial" color="white">
<Box display="flex" alignItems="center" color="white">
{process.iconName && <Icon fontSize="medium" sx={{mr: 1}}>{process.iconName}</Icon>}
Process Summary
</Box>
</Box>
{resultValidationList}
</Box>
</Grid>
<Grid item xs={0} lg={2} />
</Grid>
</Box>
);
}
export default ProcessSummaryResults;

View File

@ -0,0 +1,291 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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 {Box, Button, FormControlLabel, ListItem, Radio, RadioGroup} 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 React, {useState} from "react";
import MDTypography from "qqq/components/legacy/MDTypography";
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
import {ProcessSummaryLine} from "qqq/models/processes/ProcessSummaryLine";
import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
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 ValidationReview({
qInstance, process, table = null, processValues, step, previewRecords = [], formValues, doFullValidationRadioChangedHandler,
}: Props): JSX.Element
{
const [previewRecordIndex, setPreviewRecordIndex] = useState(0);
const [sourceTableMetaData, setSourceTableMetaData] = useState(null as QTableMetaData);
if(processValues.sourceTable && !sourceTableMetaData)
{
(async () =>
{
const sourceTableMetaData = await Client.getInstance().loadTableMetaData(processValues.sourceTable)
setSourceTableMetaData(sourceTableMetaData);
})();
}
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 (
<ListItem sx={{pl: 2}}>
<FormControlLabel
value={value}
control={<Radio />}
label={(
<ListItemText primaryTypographyProps={{fontSize: 16, pt: 0.625}}>
{`${labelWords.join(" ")} `}
<span style={{whiteSpace: "nowrap"}}>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
{lastWord}.&nbsp;<CustomWidthTooltip title={tooltipHTML}>
<IconButton sx={{py: 0}}><Icon fontSize="small">info_outlined</Icon></IconButton>
{/* eslint-disable-next-line react/jsx-closing-tag-location */}
</CustomWidthTooltip>
</span>
</ListItemText>
)}
/>
</ListItem>
);
};
const preValidationList = (
<List sx={{mt: 2}}>
{
processValues?.recordCount !== undefined && sourceTableMetaData && (
<ListItem sx={{my: 2}}>
<ListItemText primaryTypographyProps={{fontSize: 16}}>
{`Input: ${ValueUtils.getFormattedNumber(processValues.recordCount)} ${sourceTableMetaData?.label} record${processValues.recordCount === 1 ? "" : "s"}.`}
</ListItemText>
</ListItem>
)
}
{
processValues?.supportsFullValidation && formValues && formValues.doFullValidation !== undefined && (
<>
<ListItem sx={{mb: 1, mt: 6}}>
<ListItemText primaryTypographyProps={{fontSize: 16}}>How would you like to proceed?</ListItemText>
</ListItem>
<List className="doFullValidationRadios">
<RadioGroup name="doFullValidation" value={formValues.doFullValidation} onChange={doFullValidationRadioChangedHandler}>
{buildDoFullValidationRadioListItem(
"true",
"Perform Validation on all records before processing", (
<div>
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.
<br />
<br />
Running this validation may take several minutes, depending on the complexity of the work, and the number of records.
<br />
<br />
Choose this option if you want more information about what will happen, and you are willing to wait for that information.
</div>
),
)}
{buildDoFullValidationRadioListItem(
"false",
"Skip Validation. Submit the records for immediate processing", (
<div>
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.
<br />
<br />
Choose this option if you feel that you do not need this information, or are not willing to wait for it.
</div>
),
)}
</RadioGroup>
</List>
</>
)
}
</List>
);
const postValidationList = (
<List sx={{mt: 2}}>
{
processValues?.recordCount !== undefined && sourceTableMetaData && (
<ListItem sx={{my: 2}}>
<ListItemText primaryTypographyProps={{fontSize: 16}}>
Validation complete on
{` ${ValueUtils.getFormattedNumber(processValues.recordCount)} ${sourceTableMetaData?.label} ${processValues.recordCount === 1 ? "record." : "records."}`}
</ListItemText>
</ListItem>
)
}
<List>
{
processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTableMetaData, qInstance)))
}
</List>
</List>
);
const recordPreviewWidget = step.recordListFields && (
<Box border="1px solid rgb(70%, 70%, 70%)" borderRadius="lg" p={2} mt={2}>
<Box mx={2} mt={-5} p={1} sx={{width: "fit-content", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "white"}} borderColor="orange" width="initial" color="white">
<MDTypography sx={{color: "warning"}}>Preview</MDTypography>
</Box>
<Box p={3} pb={0}>
<MDTypography color="body" variant="body2" component="div" mb={2}>
<Box display="flex">
{
processValues?.previewMessage && previewRecords && previewRecords.length > 0 ? (
<>
<i>{processValues?.previewMessage}</i>
<CustomWidthTooltip
title={(
<div>
Note that the number of preview records available may be fewer than the total number of records being processed.
</div>
)}
>
<IconButton sx={{py: 0}}><Icon fontSize="small">info_outlined</Icon></IconButton>
</CustomWidthTooltip>
</>
) : (
<>
<i>No record previews are available at this time.</i>
<CustomWidthTooltip
title={(
<div>
{
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.
</>
)
}
</div>
)}
>
<IconButton sx={{py: 0}}><Icon fontSize="small">info_outlined</Icon></IconButton>
</CustomWidthTooltip>
</>
)
}
</Box>
</MDTypography>
<MDTypography color="body" variant="body2" component="div">
<Box sx={{maxHeight: "calc(100vh - 640px)", overflow: "auto", minHeight: "300px", marginRight: "-40px"}}>
<Box sx={{paddingRight: "40px"}}>
{
previewRecords && previewRecords[previewRecordIndex] && step.recordListFields.map((field) => (
<Box key={field.name} style={{marginBottom: "12px"}}>
<b>{`${field.label}:`}</b>
{" "}
&nbsp;
{" "}
{ValueUtils.getDisplayValue(field, previewRecords[previewRecordIndex], "view")}
</Box>
))
}
</Box>
</Box>
{
previewRecords && previewRecords.length > 0 && (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Button startIcon={<Icon>navigate_before</Icon>} onClick={() => updatePreviewRecordIndex(-1)} disabled={previewRecordIndex <= 0}>Previous</Button>
<span>
{`Preview ${previewRecordIndex + 1} of ${previewRecords.length}`}
</span>
<Button endIcon={<Icon>navigate_next</Icon>} onClick={() => updatePreviewRecordIndex(1)} disabled={previewRecordIndex >= previewRecords.length - 1}>Next</Button>
</Box>
)
}
</MDTypography>
</Box>
</Box>
);
return (
<Box m={3}>
<Grid container spacing={2}>
<Grid item xs={12} lg={6}>
<MDTypography color="body" variant="button">
{processValues.validationSummary ? postValidationList : preValidationList}
</MDTypography>
</Grid>
<Grid item xs={12} lg={6} mt={3}>
{recordPreviewWidget}
</Grid>
</Grid>
</Box>
);
}
export default ValidationReview;