Merge branch 'feature/sprint-10' into feature/QQQ-38-app-home-widgets

This commit is contained in:
Tim Chamberlain
2022-09-01 16:28:31 -05:00
15 changed files with 1274 additions and 185 deletions

View File

@ -78,6 +78,7 @@
}
}
],
"jsx-one-expression-per-line": "off",
"max-len": "off",
"no-console": "off",
"no-constant-condition": "off",

View File

@ -344,8 +344,8 @@ function EntityForm({table, id}: Props): JSX.Element
<MDBox component="div" p={3}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={handleCancelClicked} />
<QSaveButton />
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
<QSaveButton disabled={isSubmitting} />
</Grid>
</MDBox>

View File

@ -42,11 +42,16 @@ export function QCreateNewButton(): JSX.Element
);
}
export function QSaveButton(): JSX.Element
interface QSaveButtonProps
{
disabled: boolean
}
export function QSaveButton({disabled}: QSaveButtonProps): JSX.Element
{
return (
<MDBox ml={3} width={standardWidth}>
<MDButton type="submit" variant="gradient" color="info" size="small" fullWidth startIcon={<Icon>save</Icon>}>
<MDButton type="submit" variant="gradient" color="info" size="small" fullWidth startIcon={<Icon>save</Icon>} disabled={disabled}>
Save
</MDButton>
</MDBox>
@ -108,15 +113,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 (
<MDBox ml="auto" width={standardWidth}>
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>cancel</Icon>} onClick={onClickHandler}>
Cancel
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
{label}
</MDButton>
</MDBox>
);
}
QCancelButton.defaultProps = {
label: "cancel",
iconName: "cancel",
};
interface QSubmitButtonProps
{
label?: string
iconName?: string
disabled: boolean
}
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
{
return (
<MDBox ml={3} width={standardWidth}>
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
{label}
</MDButton>
</MDBox>
);
}
QSubmitButton.defaultProps = {
label: "Submit",
iconName: "check",
};

View File

@ -0,0 +1,56 @@
/*
* 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 LinearProgress from "@mui/material/LinearProgress";
import {styled, Theme} from "@mui/material/styles";
export default styled(LinearProgress)(
({theme, ownerState}: { theme?: Theme | any; ownerState: any }) =>
{
const {palette, functions} = theme;
const {color, value, variant} = ownerState;
const {text, gradients} = palette;
const {linearGradient} = functions;
// background value
let backgroundValue;
if (variant === "gradient")
{
backgroundValue = gradients[color]
? linearGradient(gradients[color].main, gradients[color].state)
: linearGradient(gradients.info.main, gradients.info.state);
}
else
{
backgroundValue = palette[color] ? palette[color].main : palette.info.main;
}
return {
"& .MuiLinearProgress-bar": {
background: backgroundValue,
width: `${value}%`,
color: text.main,
},
};
}
);

View File

@ -0,0 +1,60 @@
/*
* 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 {FC, forwardRef} from "react";
import MDProgressRoot from "components/MDProgress/MDProgressRoot";
import MDTypography from "components/MDTypography";
// Delcare props types for MDProgress
interface Props {
variant?: "contained" | "gradient";
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
value: number;
label?: boolean;
[key: string]: any;
}
const MDProgress: FC<Props> = forwardRef(({variant, color, value, label, ...rest}, ref) => (
<>
{label && (
<MDTypography variant="button" fontWeight="medium" color="text">
{value}%
</MDTypography>
)}
<MDProgressRoot
{...rest}
ref={ref}
variant="determinate"
value={value}
ownerState={{color, value, variant}}
/>
</>
));
// Declaring default props for MDProgress
MDProgress.defaultProps = {
variant: "contained",
color: "info",
value: 0,
label: false,
};
export default MDProgress;

View File

@ -22,7 +22,6 @@
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
@ -33,9 +32,9 @@ import Icon from "@mui/material/Icon";
import LinearProgress from "@mui/material/LinearProgress";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridFilterModel, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, GridExportMenuItemProps, MuiEvent,} from "@mui/x-data-grid-pro";
import React, {useCallback, useEffect, useReducer, useRef, useState,} from "react";
import {Link, useNavigate, useParams, useSearchParams,} from "react-router-dom";
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridFilterModel, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, GridExportMenuItemProps, MuiEvent} from "@mui/x-data-grid-pro";
import React, {useCallback, useEffect, useReducer, useRef, useState} from "react";
import {Link, useNavigate, useParams, useSearchParams} from "react-router-dom";
import DashboardLayout from "qqq/components/DashboardLayout";
import Footer from "qqq/components/Footer";
import Navbar from "qqq/components/Navbar";
@ -43,6 +42,7 @@ import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons";
import MDAlert from "qqq/components/Temporary/MDAlert";
import MDBox from "qqq/components/Temporary/MDBox";
import QClient from "qqq/utils/QClient";
import QFilterUtils from "qqq/utils/QFilterUtils";
import QProcessUtils from "qqq/utils/QProcessUtils";
import QValueUtils from "qqq/utils/QValueUtils";
@ -55,6 +55,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;
@ -70,7 +116,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))
{
@ -80,11 +126,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);
@ -124,46 +165,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();
@ -178,13 +179,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));
});
}

View File

@ -0,0 +1,97 @@
/*
* 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 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 from "react";
import MDBox from "components/MDBox";
import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine";
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 sourceTable = qInstance.tables.get(processValues.sourceTable);
const resultValidationList = (
<List sx={{mt: 2}}>
{
processValues?.recordCount !== undefined && sourceTable && (
<ListItem sx={{my: 2}}>
<ListItemText primaryTypographyProps={{fontSize: 16}}>
{processValues.recordCount.toLocaleString()}
{" "}
{sourceTable.label}
{" "}
records were processed.
</ListItemText>
</ListItem>
)
}
<List>
{
processValues.processResults && processValues.processResults.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTable, qInstance, true)))
}
</List>
</List>
);
return (
<MDBox m={3} mt={6}>
<Grid container>
<Grid item xs={0} lg={2} />
<Grid item xs={12} lg={8}>
<MDBox border="1px solid rgb(70%, 70%, 70%)" borderRadius="lg" p={2} mt={2}>
<MDBox mt={-5} p={1} sx={{width: "fit-content"}} bgColor="success" borderRadius=".25em" width="initial" color="white">
<MDBox display="flex" alignItems="center" color="white">
{process.iconName && <Icon fontSize="medium" sx={{mr: 1}}>{process.iconName}</Icon>}
Process Summary
</MDBox>
</MDBox>
{resultValidationList}
</MDBox>
</Grid>
<Grid item xs={0} lg={2} />
</Grid>
</MDBox>
);
}
export default QProcessSummaryResults;

View File

@ -0,0 +1,289 @@
/*
* 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 {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
{
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 sourceTable = qInstance.tables.get(processValues.sourceTable);
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) => (
<Tooltip {...props} classes={{popper: className}} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 500,
textAlign: "left",
},
});
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 && sourceTable && (
<ListItem sx={{my: 2}}>
<ListItemText primaryTypographyProps={{fontSize: 16}}>
{`Input: ${processValues.recordCount.toLocaleString()} ${sourceTable?.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 && sourceTable && (
<ListItem sx={{my: 2}}>
<ListItemText primaryTypographyProps={{fontSize: 16}}>
Validation complete on
{` ${processValues.recordCount.toLocaleString()} ${sourceTable?.label} `}
records.
</ListItemText>
</ListItem>
)
}
<List>
{
processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTable, qInstance)))
}
</List>
</List>
);
const recordPreviewWidget = step.recordListFields && (
<MDBox border="1px solid rgb(70%, 70%, 70%)" borderRadius="lg" p={2} mt={2}>
<MDBox mx={2} mt={-5} p={1} sx={{width: "fit-content", borderWidth: "2px", borderStyle: "solid"}} bgColor="white" borderColor="orange" borderRadius=".25em" width="initial" color="white">
<MDTypography sx={{color: "warning"}}>Preview</MDTypography>
</MDBox>
<MDBox p={3} pb={0}>
<MDTypography color="body" variant="body2" component="div" mb={2}>
<MDBox 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>
</>
)
}
</MDBox>
</MDTypography>
<MDTypography color="body" variant="body2" component="div">
{
previewRecords && previewRecords[previewRecordIndex] && step.recordListFields.map((field) => (
<MDBox key={field.name} style={{marginBottom: "12px"}}>
<b>{`${field.label}:`}</b>
{" "}
&nbsp;
{" "}
{QValueUtils.getDisplayValue(field, previewRecords[previewRecordIndex])}
</MDBox>
))
}
{
previewRecords && previewRecords.length > 0 && (
<MDBox 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>
</MDBox>
)
}
</MDTypography>
</MDBox>
</MDBox>
);
return (
<MDBox 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>
</MDBox>
);
}
export default QValidationReview;

View File

@ -24,12 +24,14 @@ import {QComponentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QC
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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
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 {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
import {CircularProgress, TablePagination} from "@mui/material";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Button, Icon, CircularProgress, TablePagination} from "@mui/material";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Step from "@mui/material/Step";
@ -38,16 +40,20 @@ import Stepper from "@mui/material/Stepper";
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
import FormData from "form-data";
import {Form, Formik} from "formik";
import React, {useEffect, useState, Fragment} from "react";
import {useLocation, useParams} from "react-router-dom";
import React, {Fragment, useEffect, useState} from "react";
import {useLocation, useParams, useNavigate} from "react-router-dom";
import * as Yup from "yup";
import BaseLayout from "qqq/components/BaseLayout";
import {QCancelButton, QSubmitButton} from "qqq/components/QButtons";
import QDynamicForm from "qqq/components/QDynamicForm";
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
import MDBox from "qqq/components/Temporary/MDBox";
import MDButton from "qqq/components/Temporary/MDButton";
import MDProgress from "qqq/components/Temporary/MDProgress";
import MDTypography from "qqq/components/Temporary/MDTypography";
import QValidationReview from "qqq/pages/process-run/components/QValidationReview";
import QClient from "qqq/utils/QClient";
import QProcessSummaryResults from "./components/QProcessSummaryResults";
interface Props
{
@ -77,12 +83,21 @@ 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 [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;
@ -103,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)
@ -159,7 +178,7 @@ function ProcessRun({process}: Props): JSX.Element
{
if (value === null || value === undefined)
{
return <span></span>;
return <span>&nbsp;</span>;
}
if (typeof value === "string")
@ -180,6 +199,11 @@ function ProcessRun({process}: Props): JSX.Element
return (<span>{value}</span>);
};
const toggleShowErrorDetail = () =>
{
setShowErrorDetail(!showErrorDetail);
};
////////////////////////////////////////////////////
// generate the main form body content for a step //
////////////////////////////////////////////////////
@ -190,60 +214,79 @@ function ProcessRun({process}: Props): JSX.Element
processError: string,
processValues: any,
recordConfig: any,
setFieldValue: any,
): JSX.Element =>
{
if (processError)
{
return (
<>
<MDTypography color="error" variant="h5">
<MDTypography color="error" variant="h3" component="div">
Error
</MDTypography>
<MDTypography color="body" variant="button">
{processError}
An error occurred while running the process:
{" "}
{process.label}
<MDBox mt={3} display="flex" justifyContent="center">
<MDBox display="flex" flexDirection="column" alignItems="center">
<Button onClick={toggleShowErrorDetail} startIcon={<Icon>{showErrorDetail ? "expand_less" : "expand_more"}</Icon>}>
{showErrorDetail ? "Hide " : "Show "}
detailed error message
</Button>
<MDBox mt={1} style={{display: showErrorDetail ? "block" : "none"}}>
{processError}
</MDBox>
</MDBox>
</MDBox>
</MDTypography>
</>
);
}
if (qJobRunning)
if (qJobRunning || step === null)
{
return (
<>
<MDTypography variant="h5">
{" "}
Working
</MDTypography>
<Grid container>
<Grid item padding={1}>
<CircularProgress color="info" />
</Grid>
<Grid item>
<MDTypography color="body" variant="button">
{qJobRunning?.message}
<br />
{qJobRunning.current && qJobRunning.total && (
<div>{`${qJobRunning.current.toLocaleString()} of ${qJobRunning.total.toLocaleString()}`}</div>
)}
<i>
{`Updated at ${qJobRunningDate.toLocaleTimeString()}`}
</i>
</MDTypography>
</Grid>
<Grid m={3} mt={9} container>
<Grid item xs={0} lg={3} />
<Grid item xs={12} lg={6}>
<Card>
<MDBox p={3}>
<MDTypography variant="h5" component="div">
Working
</MDTypography>
<Grid container>
<Grid item padding={2}>
<CircularProgress color="info" />
</Grid>
<Grid item padding={1}>
<MDTypography color="body" variant="button">
{qJobRunning?.message}
<br />
{qJobRunning?.current && qJobRunning?.total && (
<>
<div>{`${qJobRunning.current.toLocaleString()} of ${qJobRunning.total.toLocaleString()}`}</div>
<MDBox width="20rem">
<MDProgress variant="gradient" value={100 * (qJobRunning.current / qJobRunning.total)} color="success" />
</MDBox>
</>
)}
{
qJobRunningDate && (<i>{`Updated at ${qJobRunningDate?.toLocaleTimeString()}`}</i>)
}
</MDTypography>
</Grid>
</Grid>
</MDBox>
</Card>
</Grid>
</>
</Grid>
);
}
if (step === null)
{
console.log("in getDynamicStepContent. No step yet, so returning 'loading'");
return <div>Loading...</div>;
}
return (
<>
<MDTypography variation="h5" fontWeight="bold">{step?.label}</MDTypography>
<MDTypography variation="h5" component="div" fontWeight="bold">{step?.label}</MDTypography>
{step.components && (
step.components.map((component: QFrontendComponent, index: number) => (
// eslint-disable-next-line react/no-array-index-key
@ -255,60 +298,96 @@ function ProcessRun({process}: Props): JSX.Element
</MDTypography>
)
}
{
component.type === QComponentType.BULK_EDIT_FORM && (
<QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
)
}
{
component.type === QComponentType.EDIT_FORM && (
<QDynamicForm formData={formData} />
)
}
{
component.type === QComponentType.VIEW_FORM && step.viewFields && (
<div>
{step.viewFields.map((field: QFieldMetaData) => (
<MDBox key={field.name} display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="bold">
{field.label}
: &nbsp;
</MDTypography>
<MDTypography variant="button" fontWeight="regular" color="text">
{formatViewValue(processValues[field.name])}
</MDTypography>
</MDBox>
))}
</div>
)
}
{
component.type === QComponentType.VALIDATION_REVIEW_SCREEN && (
<QValidationReview
qInstance={qInstance}
process={processMetaData}
table={tableMetaData}
processValues={processValues}
step={step}
previewRecords={records}
formValues={formData.values}
doFullValidationRadioChangedHandler={(event: any) =>
{
const {value} = event.currentTarget;
//////////////////////////////////////////////////////////////
// call the formik function to set the value in this field. //
//////////////////////////////////////////////////////////////
setFieldValue("doFullValidation", value);
setOverrideOnLastStep(value !== "true");
}}
/>
)
}
{
component.type === QComponentType.PROCESS_SUMMARY_RESULTS && (
<QProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} />
)
}
{
component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && (
<div>
<MDTypography variant="button" fontWeight="bold">Records</MDTypography>
{" "}
<br />
<MDBox height="100%">
<DataGridPro
components={{Pagination: CustomPagination}}
page={recordConfig.pageNo}
disableSelectionOnClick
autoHeight
rows={recordConfig.rows}
columns={recordConfig.columns}
rowBuffer={10}
rowCount={recordConfig.totalRecords}
pageSize={recordConfig.rowsPerPage}
rowsPerPageOptions={[10, 25, 50]}
onPageSizeChange={recordConfig.handleRowsPerPageChange}
onPageChange={recordConfig.handlePageChange}
onRowClick={recordConfig.handleRowClick}
getRowId={(row) => row.__idForDataGridPro__}
paginationMode="server"
pagination
density="compact"
loading={recordConfig.loading}
disableColumnFilter
/>
</MDBox>
</div>
)
}
</div>
)))}
{step.formFields && (
<QDynamicForm
formData={formData}
bulkEditMode={doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM)}
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
/>
)}
{step.viewFields && (
<div>
{step.viewFields.map((field: QFieldMetaData) => (
<MDBox key={field.name} display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="bold">
{field.label}
: &nbsp;
</MDTypography>
<MDTypography variant="button" fontWeight="regular" color="text">
{formatViewValue(processValues[field.name])}
</MDTypography>
</MDBox>
))}
</div>
)}
{(step.recordListFields && recordConfig.columns) && (
<div>
<MDTypography variant="button" fontWeight="bold">Records</MDTypography>
{" "}
<br />
<MDBox height="100%">
<DataGridPro
components={{Pagination: CustomPagination}}
page={recordConfig.pageNo}
disableSelectionOnClick
autoHeight
rows={recordConfig.rows}
columns={recordConfig.columns}
rowBuffer={10}
rowCount={recordConfig.totalRecords}
pageSize={recordConfig.rowsPerPage}
rowsPerPageOptions={[10, 25, 50]}
onPageSizeChange={recordConfig.handleRowsPerPageChange}
onPageChange={recordConfig.handlePageChange}
onRowClick={recordConfig.handleRowClick}
getRowId={(row) => row.__idForDataGridPro__}
paginationMode="server"
pagination
density="compact"
loading={recordConfig.loading}
disableColumnFilter
/>
</MDBox>
</div>
)}
</>
);
};
@ -370,6 +449,7 @@ function ProcessRun({process}: Props): JSX.Element
setProcessError(`Unknown process step ${newStep}.`);
}
setActiveStepIndex(newIndex);
setOverrideOnLastStep(null);
if (steps)
{
@ -412,6 +492,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
{
/////////////////////////////////////////////////////////////////////////
@ -490,6 +590,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 //
@ -530,6 +631,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)
{
@ -550,6 +657,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]);
@ -562,13 +670,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,
@ -612,8 +725,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"))
{
@ -632,11 +744,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)
{
@ -702,6 +838,9 @@ function ProcessRun({process}: Props): JSX.Element
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
};
setProcessValues({});
setRecords([]);
setOverrideOnLastStep(null);
setLastProcessResponse(new QJobRunning({message: "Working..."}));
setTimeout(async () =>
@ -717,15 +856,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 (!processError && (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 (
<BaseLayout>
<MDBox py={3} mb={20}>
<Grid
container
justifyContent="center"
alignItems="center"
sx={{height: "100%", mt: 8}}
>
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
<Grid item xs={12} lg={8}>
<Formik
enableReinitialize
@ -735,10 +901,10 @@ function ProcessRun({process}: Props): JSX.Element
onSubmit={handleSubmit}
>
{({
values, errors, touched, isSubmitting,
values, errors, touched, isSubmitting, setFieldValue,
}) => (
<Form id={formId} autoComplete="off">
<Card sx={{minHeight: "calc(100vh - 400px)"}}>
<Card sx={mainCardStyles}>
<MDBox mx={2} mt={-3}>
<Stepper activeStep={activeStepIndex} alternativeLabel>
{steps.map((step) => (
@ -748,6 +914,7 @@ function ProcessRun({process}: Props): JSX.Element
))}
</Stepper>
</MDBox>
<MDBox p={3}>
<MDBox>
{/***************************************************************************
@ -765,28 +932,18 @@ function ProcessRun({process}: Props): JSX.Element
processError,
processValues,
recordConfig,
setFieldValue,
)}
{/********************************
** back &| next/submit buttons **
********************************/}
<MDBox
mt={2}
width="100%"
display="flex"
justifyContent="space-between"
>
<MDBox mt={6} width="100%" display="flex" justifyContent="space-between">
{true || activeStepIndex === 0 ? (
<MDBox />
) : (
<MDButton
variant="gradient"
color="light"
onClick={handleBack}
>
back
</MDButton>
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
)}
{noMoreSteps || processError || qJobRunning ? (
{processError || qJobRunning || !activeStep ? (
<MDBox />
) : (
<>
@ -795,14 +952,19 @@ function ProcessRun({process}: Props): JSX.Element
{formError}
</MDTypography>
)}
<MDButton
disabled={isSubmitting}
type="submit"
variant="gradient"
color="dark"
>
{onLastStep ? "submit" : "next"}
</MDButton>
{
noMoreSteps && <QCancelButton onClickHandler={handleCancelClicked} label="Return" iconName="arrow_back" disabled={isSubmitting} />
}
{
!noMoreSteps && (
<MDBox component="div" py={3}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
</Grid>
</MDBox>
)
}
</>
)}
</MDBox>

View File

@ -0,0 +1,148 @@
/*
* 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
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 React from "react";
import {Link} from "react-router-dom";
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
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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 (
<ListItem key={i} sx={{pl: 4, my: 2}}>
<MDBox display="flex" alignItems="top">
<Icon fontSize="medium" sx={{mr: 1}} color={this.getColor()}>{this.getIcon(isResultScreen)}</Icon>
<ListItemText primaryTypographyProps={{fontSize: 16}}>
{/* work hard to prevent the icon from falling down to the next line by itself... */}
{`${this.count.toLocaleString()} ${messageWords.join(" ")} `}
{
(table && this.primaryKeys) ? (
<span style={{whiteSpace: "nowrap"}}>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
{lastWord}&nbsp;<Link target="_blank" to={this.getLinkToRecords(table, qInstance)}>
<Tooltip title="See these records in a new tab" sx={{py: 0}}>
<IconButton sx={{py: 0}}><Icon fontSize="small">open_in_new</Icon></IconButton>
</Tooltip>
{/* eslint-disable-next-line react/jsx-closing-tag-location */}
</Link>
</span>
) : <span>{lastWord}</span>
}
</ListItemText>
</MDBox>
</ListItem>
);
}
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)]);
return (`${tablePath}?filter=${JSON.stringify(filter)}`);
}
}

View File

@ -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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
**
*******************************************************************************/
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;

View File

@ -55,6 +55,11 @@ class QValueUtils
return (displayValue);
}
if (displayValue === undefined && rawValue !== undefined)
{
return (rawValue);
}
return (displayValue);
}
}