diff --git a/.eslintrc.json b/.eslintrc.json index 1d1d0b2..b9242a5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -64,6 +64,7 @@ "no-unused-vars": "off", "no-plusplus": "off", "no-underscore-dangle": "off", + "no-else-return": "off", "spaced-comment": "off", "object-curly-spacing": [ "error", diff --git a/src/qqq/components/EntityForm/index.tsx b/src/qqq/components/EntityForm/index.tsx index 9f6e074..41d8a3f 100644 --- a/src/qqq/components/EntityForm/index.tsx +++ b/src/qqq/components/EntityForm/index.tsx @@ -74,7 +74,10 @@ function EntityForm({id}: Props): JSX.Element sortedKeys.forEach((key) => { const fieldMetaData = tableMetaData.fields.get(key); - fieldArray.push(fieldMetaData); + if (fieldMetaData.isEditable) + { + fieldArray.push(fieldMetaData); + } }); if (id !== null) diff --git a/src/qqq/components/QDynamicForm/index.tsx b/src/qqq/components/QDynamicForm/index.tsx index d54d9d3..25190c2 100644 --- a/src/qqq/components/QDynamicForm/index.tsx +++ b/src/qqq/components/QDynamicForm/index.tsx @@ -1,16 +1,22 @@ -/** - ========================================================= - * Material Dashboard 2 PRO React TS - v1.0.0 - ========================================================= - - * Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts - * Copyright 2022 Creative Tim (https://www.creative-tim.com) - - Coded by www.creative-tim.com - - ========================================================= - - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +/* + * 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 . */ // @mui material components @@ -21,32 +27,38 @@ import MDBox from "components/MDBox"; import MDTypography from "components/MDTypography"; // NewUser page components -import FormField from "layouts/pages/users/new-user/components/FormField"; -import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; +import {useFormikContext} from "formik"; +import React from "react"; +import QDynamicFormField from "qqq/components/QDynamicFormField"; interface Props { formLabel?: string; formData: any; primaryKeyId?: string; + bulkEditMode?: boolean; + bulkEditSwitchChangeHandler?: any } function QDynamicForm(props: Props): JSX.Element { - const {formData, formLabel, primaryKeyId} = props; + const { + formData, formLabel, primaryKeyId, bulkEditMode, bulkEditSwitchChangeHandler, + } = props; const { formFields, values, errors, touched, } = formData; - /* - const { - firstName: firstNameV, - lastName: lastNameV, - company: companyV, - email: emailV, - password: passwordV, - repeatPassword: repeatPasswordV, - } = values; - */ + const formikProps = useFormikContext(); + + const fileChanged = (event: React.FormEvent, field: any) => + { + formikProps.setFieldValue(field.name, event.currentTarget.files[0]); + }; + + const bulkEditSwitchChanged = (name: string, value: boolean) => + { + bulkEditSwitchChangeHandler(name, value); + }; return ( @@ -73,93 +85,46 @@ function QDynamicForm(props: Props): JSX.Element { values[fieldName] = ""; } + + if (field.type === "file") + { + return ( + + + ) => fileChanged(event, field)} + /> + + + {errors[fieldName] && You must select a file to proceed} + + + + + ); + } + + // todo? inputProps={{ autoComplete: "" }} + // todo? placeholder={password.placeholder} + // todo? success={!errors[fieldName] && touched[fieldName]} return ( - ); })} - {/* - - - 0 && !errors.firstName} - /> - - - 0 && !errors.lastName} - /> - - - - - - - - 0 && !errors.email} - /> - - - - - 0 && !errors.password} - inputProps={{ autoComplete: "" }} - /> - - - 0 && !errors.repeatPassword} - inputProps={{ autoComplete: "" }} - /> - - - */} ); @@ -168,6 +133,9 @@ function QDynamicForm(props: Props): JSX.Element QDynamicForm.defaultProps = { formLabel: undefined, primaryKeyId: undefined, + bulkEditMode: false, + bulkEditSwitchChangeHandler: () => + {}, }; export default QDynamicForm; diff --git a/src/qqq/components/QDynamicForm/utils/DynamicFormUtils.ts b/src/qqq/components/QDynamicForm/utils/DynamicFormUtils.ts index 1f1bad1..600be04 100644 --- a/src/qqq/components/QDynamicForm/utils/DynamicFormUtils.ts +++ b/src/qqq/components/QDynamicForm/utils/DynamicFormUtils.ts @@ -39,47 +39,81 @@ class DynamicFormUtils qqqFormFields.forEach((field) => { - let fieldType: string; - switch (field.type.toString()) - { - case QFieldType.DECIMAL: - case QFieldType.INTEGER: - fieldType = "number"; - break; - case QFieldType.DATE_TIME: - fieldType = "datetime-local"; - break; - case QFieldType.PASSWORD: - case QFieldType.TIME: - case QFieldType.DATE: - fieldType = field.type.toString(); - break; - case QFieldType.TEXT: - case QFieldType.HTML: - case QFieldType.STRING: - default: - fieldType = "text"; - } - - let label = field.label ? field.label : field.name; - label += field.isRequired ? " *" : ""; - - dynamicFormFields[field.name] = { - name: field.name, - label: label, - isRequired: field.isRequired, - type: fieldType, - // todo invalidMsg: "Zipcode is not valid (e.g. 70000).", - }; - - if (field.isRequired) - { - formValidations[field.name] = Yup.string().required(`${field.label} is required.`); - } + dynamicFormFields[field.name] = this.getDynamicField(field); + formValidations[field.name] = this.getValidationForField(field); }); return {dynamicFormFields, formValidations}; } + + public static getDynamicField(field: QFieldMetaData) + { + let fieldType: string; + switch (field.type.toString()) + { + case QFieldType.DECIMAL: + case QFieldType.INTEGER: + fieldType = "number"; + break; + case QFieldType.DATE_TIME: + fieldType = "datetime-local"; + break; + case QFieldType.PASSWORD: + case QFieldType.TIME: + case QFieldType.DATE: + fieldType = field.type.toString(); + break; + case QFieldType.BLOB: + fieldType = "file"; + break; + case QFieldType.TEXT: + case QFieldType.HTML: + case QFieldType.STRING: + default: + fieldType = "text"; + } + + let label = field.label ? field.label : field.name; + label += field.isRequired ? " *" : ""; + + return ({ + name: field.name, + label: label, + isRequired: field.isRequired, + type: fieldType, + // todo invalidMsg: "Zipcode is not valid (e.g. 70000).", + }); + } + + public static getValidationForField(field: QFieldMetaData) + { + if (field.isRequired) + { + return (Yup.string().required(`${field.label} is required.`)); + } + return (null); + } + + public static getFormDataForUploadForm(fieldName: string, fieldLabel: string, isRequired: boolean = true) + { + const dynamicFormFields: any = {}; + const formValidations: any = {}; + + dynamicFormFields[fieldName] = { + name: fieldName, + label: fieldLabel, + isRequired: isRequired, + type: "file", + // todo invalidMsg: "Zipcode is not valid (e.g. 70000).", + }; + + if (isRequired) + { + formValidations[fieldName] = Yup.string().required(`${fieldLabel} is required.`); + } + + return {dynamicFormFields, formValidations}; + } } export default DynamicFormUtils; diff --git a/src/qqq/components/QDynamicFormField/index.tsx b/src/qqq/components/QDynamicFormField/index.tsx new file mode 100644 index 0000000..4ce43e6 --- /dev/null +++ b/src/qqq/components/QDynamicFormField/index.tsx @@ -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 . + */ + +// formik components +import {ErrorMessage, Field} from "formik"; + +// Material Dashboard 2 PRO React TS components +import MDBox from "components/MDBox"; +import MDTypography from "components/MDTypography"; +import MDInput from "components/MDInput"; +import QDynamicForm from "qqq/components/QDynamicForm"; +import React, {useState} from "react"; +import Grid from "@mui/material/Grid"; +import Switch from "@mui/material/Switch"; + +// Declaring props types for FormField +interface Props +{ + label: string; + name: string; + [key: string]: any; + bulkEditMode?: boolean; + bulkEditSwitchChangeHandler?: any +} + +function QDynamicFormField({ + label, name, bulkEditMode, bulkEditSwitchChangeHandler, ...rest +}: Props): JSX.Element +{ + const [switchChecked, setSwitchChecked] = useState(false); + const [isDisabled, setIsDisabled] = useState(bulkEditMode); + + const field = () => ( + <> + + + + {!isDisabled && } + + + + ); + + const bulkEditSwitchChanged = () => + { + const newSwitchValue = !switchChecked; + setSwitchChecked(newSwitchValue); + setIsDisabled(!newSwitchValue); + bulkEditSwitchChangeHandler(name, newSwitchValue); + }; + + if (bulkEditMode) + { + return ( + + + + + + + + + + + ); + } + else + { + return ( + + {field()} + + ); + } +} + +QDynamicFormField.defaultProps = { + bulkEditMode: false, + bulkEditSwitchChangeHandler: () => + {}, +}; + +export default QDynamicFormField; diff --git a/src/qqq/pages/entity-list/index.tsx b/src/qqq/pages/entity-list/index.tsx index aacba74..59f8a01 100644 --- a/src/qqq/pages/entity-list/index.tsx +++ b/src/qqq/pages/entity-list/index.tsx @@ -39,7 +39,6 @@ import { GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, - GridToolbarExport, GridToolbarExportContainer, GridToolbarFilterButton, GridExportMenuItemProps, @@ -62,6 +61,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import QClient from "qqq/utils/QClient"; import Navbar from "qqq/components/Navbar"; +import Button from "@mui/material/Button"; import Footer from "../../components/Footer"; import QProcessUtils from "../../utils/QProcessUtils"; @@ -450,10 +450,82 @@ function EntityList({table}: Props): JSX.Element ); } + function getNoOfSelectedRecords() + { + if (selectFullFilterState === "filter") + { + return (totalRecords); + } + + return (selectedIds.length); + } + + function getRecordsQueryString() + { + if (selectFullFilterState === "filter") + { + return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter())}`; + } + + if (selectedIds.length > 0) + { + return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`; + } + + return ""; + } + + const bulkLoadClicked = () => + { + document.location.href = `/processes/${tableName}.bulkInsert${getRecordsQueryString()}`; + }; + + const bulkEditClicked = () => + { + if (getNoOfSelectedRecords() === 0) + { + setAlertContent("No records were selected to Bulk Edit."); + return; + } + document.location.href = `/processes/${tableName}.bulkEdit${getRecordsQueryString()}`; + }; + + const bulkDeleteClicked = () => + { + if (getNoOfSelectedRecords() === 0) + { + setAlertContent("No records were selected to Bulk Delete."); + return; + } + document.location.href = `/processes/${tableName}.bulkDelete${getRecordsQueryString()}`; + }; + function CustomToolbar() { + const [bulkActionsMenuAnchor, setBulkActionsMenuAnchor] = useState(null as HTMLElement); + const bulkActionsMenuOpen = Boolean(bulkActionsMenuAnchor); + + const openBulkActionsMenu = (event: React.MouseEvent) => + { + setBulkActionsMenuAnchor(event.currentTarget); + }; + + const closeBulkActionsMenu = () => + { + setBulkActionsMenuAnchor(null); + }; + return ( +
+ +
@@ -461,6 +533,29 @@ function EntityList({table}: Props): JSX.Element +
+ + + Bulk Load + Bulk Edit + Bulk Delete + +
{ selectFullFilterState === "checked" && ( @@ -503,21 +598,6 @@ function EntityList({table}: Props): JSX.Element ); } - function getRecordsQueryString() - { - if (selectFullFilterState === "filter") - { - return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter())}`; - } - - if (selectedIds.length > 0) - { - return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`; - } - - return ""; - } - const renderActionsMenu = ( - - Error - -
{processError}
- - ); - } - - if (step === null) - { - console.log("in getDynamicStepContent. No step yet, so returning 'loading'"); - return
Loading...
; - } - - console.log(`in getDynamicStepContent. the step looks like: ${JSON.stringify(step)}`); - - return ( - <> - {step.formFields && } - {step.viewFields && ( -
- {step.viewFields.map((field: QFieldMetaData) => ( -
- - {field.label} - : - - {" "} - {processValues[field.name]} -
- ))} -
- )} - {step.recordListFields && ( -
- Records - {" "} -
- - - -
- )} - - ); + console.log(`${prefix}: ${JSON.stringify(formValidations)} `); } -function trace(name: string, isComponent: boolean = false) -{ - if (isComponent) - { - console.log(`COMPONENT: ${name}`); - } - else - { - console.log(` function: ${name}`); - } -} - -const qController = new QController(""); - function ProcessRun(): JSX.Element { const {processName} = useParams(); + + /////////////////// + // process state // + /////////////////// const [processUUID, setProcessUUID] = useState(null as string); const [jobUUID, setJobUUID] = useState(null as string); + const [qJobRunning, setQJobRunning] = useState(null as QJobRunning); + const [qJobRunningDate, setQJobRunningDate] = useState(null as Date); const [activeStepIndex, setActiveStepIndex] = useState(0); const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData); const [newStep, setNewStep] = useState(null); @@ -159,33 +76,239 @@ function ProcessRun(): JSX.Element const [needInitialLoad, setNeedInitialLoad] = useState(true); const [processMetaData, setProcessMetaData] = useState(null); 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 onLastStep = activeStepIndex === steps.length - 2; + const noMoreSteps = activeStepIndex === steps.length - 1; + + //////////////// + // form state // + //////////////// const [formId, setFormId] = useState(""); const [formFields, setFormFields] = useState({}); const [initialValues, setInitialValues] = useState({}); const [validationScheme, setValidationScheme] = useState(null); const [validationFunction, setValidationFunction] = useState(null); - const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false); - const [needRecords, setNeedRecords] = useState(false); - const [processError, setProcessError] = useState(null as string); - const [recordConfig, setRecordConfig] = useState({} as any); - const onLastStep = activeStepIndex === steps.length - 2; - const noMoreSteps = activeStepIndex === steps.length - 1; + const [formError, setFormError] = useState(null as string); - trace("ProcessRun", true); + /////////////////////// + // record list state // + /////////////////////// + const [needRecords, setNeedRecords] = useState(false); + const [recordConfig, setRecordConfig] = useState({} as any); + const [pageNumber, setPageNumber] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + ////////////////////////////// + // state for bulk edit form // + ////////////////////////////// + const [disabledBulkEditFields, setDisabledBulkEditFields] = useState({} as any); + + const doesStepHaveComponent = (step: QFrontendStepMetaData, type: QComponentType): boolean => + { + if (step.components) + { + for (let i = 0; i < step.components.length; i++) + { + if (step.components[i].type === type) + { + return (true); + } + } + } + return (false); + }; + + ////////////////////////////////////////////////////////////// + // event handler for the bulk-edit field-enabled checkboxes // + ////////////////////////////////////////////////////////////// + const bulkEditSwitchChanged = (name: string, switchValue: boolean) => + { + const newDisabledBulkEditFields = JSON.parse(JSON.stringify(disabledBulkEditFields)); + newDisabledBulkEditFields[name] = !switchValue; + setDisabledBulkEditFields(newDisabledBulkEditFields); + }; + + const formatViewValue = (value: any): JSX.Element => + { + if (value === null || value === undefined) + { + return --; + } + + if (typeof value === "string") + { + return ( + <> + {value.split(/\n/).map((value: string, index: number) => ( + // eslint-disable-next-line react/no-array-index-key + + {value} +
+
+ ))} + + ); + } + + return ({value}); + }; + + //////////////////////////////////////////////////// + // generate the main form body content for a step // + //////////////////////////////////////////////////// + const getDynamicStepContent = ( + stepIndex: number, + step: any, + formData: any, + processError: string, + processValues: any, + recordConfig: any, + ): JSX.Element => + { + if (processError) + { + return ( + <> + + Error + + + {processError} + + + ); + } + + if (qJobRunning) + { + return ( + <> + + {" "} + Working + + + + + + + + {qJobRunning?.message} +
+ {qJobRunning.current && qJobRunning.total && ( +
{`${qJobRunning.current.toLocaleString()} of ${qJobRunning.total.toLocaleString()}`}
+ )} + + {`Updated at ${qJobRunningDate.toLocaleTimeString()}`} + +
+
+
+ + ); + } + + if (step === null) + { + console.log("in getDynamicStepContent. No step yet, so returning 'loading'"); + return
Loading...
; + } + + return ( + <> + {step?.label} + {step.components && ( + step.components.map((component: QFrontendComponent, index: number) => ( + // eslint-disable-next-line react/no-array-index-key +
+ { + component.type === QComponentType.HELP_TEXT && ( + + {component.values.text} + + ) + } +
+ )))} + {(step.formFields) && ( + + )} + {step.viewFields && ( +
+ {step.viewFields.map((field: QFieldMetaData) => ( + + + {field.label} + :   + + + {formatViewValue(processValues[field.name])} + + + ))} +
+ )} + {(step.recordListFields && recordConfig.columns) && ( +
+ Records + {" "} +
+ + row.__idForDataGridPro__} + paginationMode="server" + pagination + density="compact" + loading={recordConfig.loading} + disableColumnFilter + /> + +
+ )} + + ); + }; + + const handlePageChange = (page: number) => + { + setPageNumber(page); + }; + + const handleRowsPerPageChange = (size: number) => + { + setRowsPerPage(size); + }; function buildNewRecordConfig() { const newRecordConfig = {} as any; - newRecordConfig.pageNo = 1; - newRecordConfig.rowsPerPage = 20; + newRecordConfig.pageNo = pageNumber; + newRecordConfig.rowsPerPage = rowsPerPage; newRecordConfig.columns = [] as GridColDef[]; newRecordConfig.rows = []; newRecordConfig.totalRecords = 0; - newRecordConfig.handleRowsPerPageChange = null; - newRecordConfig.handlePageChange = null; + newRecordConfig.handleRowsPerPageChange = handleRowsPerPageChange; + newRecordConfig.handlePageChange = handlePageChange; newRecordConfig.handleRowClick = null; newRecordConfig.loading = true; return (newRecordConfig); @@ -196,8 +319,6 @@ function ProcessRun(): JSX.Element ////////////////////////////////////////////////////////////////////////////////////////////////////////////// useEffect(() => { - trace("updateActiveStep"); - if (!processMetaData) { console.log("No process meta data yet, so returning early"); @@ -249,10 +370,27 @@ function ProcessRun(): JSX.Element initialValues[field.name] = processValues[field.name]; }); + //////////////////////////////////////////////////// + // disable all fields if this is a bulk edit form // + //////////////////////////////////////////////////// + if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM)) + { + const newDisabledBulkEditFields: any = {}; + activeStep.formFields.forEach((field) => + { + newDisabledBulkEditFields[field.name] = true; + dynamicFormFields[field.name].isRequired = false; + formValidations[field.name] = null; + }); + setDisabledBulkEditFields(newDisabledBulkEditFields); + } + setFormFields(dynamicFormFields); setInitialValues(initialValues); setValidationScheme(Yup.object().shape(formValidations)); setValidationFunction(null); + + logFormValidations("Post-disable thingie", formValidations); } else { @@ -263,24 +401,65 @@ function ProcessRun(): JSX.Element setValidationScheme(null); setValidationFunction(() => true); } - - //////////////////////////////////////////////////////////////////////////////////////////// - // if there are fields to load, build a record config, and set the needRecords state flag // - //////////////////////////////////////////////////////////////////////////////////////////// - if (activeStep.recordListFields) - { - const newRecordConfig = buildNewRecordConfig(); - activeStep.recordListFields.forEach((field) => - { - newRecordConfig.columns.push({field: field.name, headerName: field.label, width: 200}); - }); - setRecordConfig(newRecordConfig); - setNeedRecords(true); - } } - }, [newStep]); + }, [newStep, rowsPerPage, pageNumber]); - // when we need to load records, do so, async + ///////////////////////////////////////////////////////////////////////////////////////////// + // if there are records to load: build a record config, and set the needRecords state flag // + ///////////////////////////////////////////////////////////////////////////////////////////// + useEffect(() => + { + if (activeStep && activeStep.recordListFields) + { + const newRecordConfig = buildNewRecordConfig(); + activeStep.recordListFields.forEach((field) => + { + newRecordConfig.columns.push({ + field: field.name, headerName: field.label, width: 200, sortable: false, + }); + }); + setRecordConfig(newRecordConfig); + setNeedRecords(true); + } + }, [activeStep, rowsPerPage, pageNumber]); + + ///////////////////////////////////////////////////// + // handle a bulk-edit enabled-switch being checked // + ///////////////////////////////////////////////////// + useEffect(() => + { + if (activeStep && activeStep.formFields) + { + console.log("In useEffect for disabledBulkEditFields"); + console.log(disabledBulkEditFields); + + const newDynamicFormFields: any = {}; + const newFormValidations: any = {}; + activeStep.formFields.forEach((field) => + { + const fieldName = field.name; + const isDisabled = disabledBulkEditFields[fieldName]; + + newDynamicFormFields[field.name] = DynamicFormUtils.getDynamicField(field); + newFormValidations[field.name] = DynamicFormUtils.getValidationForField(field); + + if (isDisabled) + { + newDynamicFormFields[field.name].isRequired = false; + newFormValidations[field.name] = null; + } + + logFormValidations("Upon update", newFormValidations); + + setFormFields(newDynamicFormFields); + setValidationScheme(Yup.object().shape(newFormValidations)); + }); + } + }, [disabledBulkEditFields]); + + //////////////////////////////////////////////// + // when we need to load records, do so, async // + //////////////////////////////////////////////// useEffect(() => { if (needRecords) @@ -288,13 +467,15 @@ function ProcessRun(): JSX.Element setNeedRecords(false); (async () => { - const records = await qController.processRecords( + const response = await QClient.getInstance().processRecords( processName, processUUID, - recordConfig.rowsPerPage * (recordConfig.pageNo - 1), + recordConfig.rowsPerPage * recordConfig.pageNo, recordConfig.rowsPerPage, ); + const {records} = response; + ///////////////////////////////////////////////////////////////////////////////////////// // re-construct the recordConfig object, so the setState call triggers a new rendering // ///////////////////////////////////////////////////////////////////////////////////////// @@ -306,14 +487,11 @@ function ProcessRun(): JSX.Element records.forEach((record) => { const row = Object.fromEntries(record.values.entries()); - if (!row.id) - { - row.id = ++rowId; - } + row.__idForDataGridPro__ = ++rowId; newRecordConfig.rows.push(row); }); - // todo count? - newRecordConfig.totalRecords = records.length; + + newRecordConfig.totalRecords = response.totalRecords; setRecordConfig(newRecordConfig); })(); } @@ -326,8 +504,9 @@ function ProcessRun(): JSX.Element { if (lastProcessResponse) { - trace("handleProcessResponse"); setLastProcessResponse(null); + + setQJobRunning(null); if (lastProcessResponse instanceof QJobComplete) { const qJobComplete = lastProcessResponse as QJobComplete; @@ -345,6 +524,8 @@ function ProcessRun(): JSX.Element else if (lastProcessResponse instanceof QJobRunning) { const qJobRunning = lastProcessResponse as QJobRunning; + setQJobRunning(qJobRunning); + setQJobRunningDate(new Date()); setNeedToCheckJobStatus(true); } else if (lastProcessResponse instanceof QJobError) @@ -363,13 +544,12 @@ function ProcessRun(): JSX.Element { if (needToCheckJobStatus) { - trace("checkJobStatus"); setNeedToCheckJobStatus(false); (async () => { setTimeout(async () => { - const processResponse = await qController.processJobStatus( + const processResponse = await QClient.getInstance().processJobStatus( processName, processUUID, jobUUID, @@ -385,7 +565,6 @@ function ProcessRun(): JSX.Element ////////////////////////////////////////////////////////////////////////////////////////// if (needInitialLoad) { - trace("initialLoad"); setNeedInitialLoad(false); (async () => { @@ -411,15 +590,28 @@ function ProcessRun(): JSX.Element console.log(`@dk: Query String for init: ${queryStringForInit}`); - const processMetaData = await qController.loadProcessMetaData(processName); - // console.log(processMetaData); - setProcessMetaData(processMetaData); - setSteps(processMetaData.frontendSteps); + try + { + const processMetaData = await QClient.getInstance().loadProcessMetaData(processName); + setProcessMetaData(processMetaData); + setSteps(processMetaData.frontendSteps); + } + catch (e) + { + setProcessError("Error loading process definition."); + return; + } - const processResponse = await qController.processInit(processName, queryStringForInit); - setProcessUUID(processResponse.processUUID); - setLastProcessResponse(processResponse); - // console.log(processResponse); + try + { + const processResponse = await QClient.getInstance().processInit(processName, queryStringForInit); + setProcessUUID(processResponse.processUUID); + setLastProcessResponse(processResponse); + } + catch (e) + { + setProcessError("Error initializing process."); + } })(); } @@ -429,7 +621,6 @@ function ProcessRun(): JSX.Element ////////////////////////////////////////////////////////////////////////////////////////////////////// const handleBack = () => { - trace("handleBack"); setNewStep(activeStepIndex - 1); }; @@ -438,23 +629,44 @@ function ProcessRun(): JSX.Element ////////////////////////////////////////////////////////////////////////////////////////// const handleSubmit = async (values: any, actions: any) => { - trace("handleSubmit"); + setFormError(null); - // todo - post? - let queryString = ""; + const formData = new FormData(); Object.keys(values).forEach((key) => { - queryString += `${key}=${encodeURIComponent(values[key])}&`; + formData.append(key, values[key]); }); - actions.setSubmitting(false); - actions.resetForm(); + if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM)) + { + const bulkEditEnabledFields: string[] = []; + activeStep.formFields.forEach((field) => + { + if (!disabledBulkEditFields[field.name]) + { + bulkEditEnabledFields.push(field.name); + } + }); - const processResponse = await qController.processStep( + if (bulkEditEnabledFields.length === 0) + { + setFormError("You must edit at least one field to continue."); + return; + } + formData.append("bulkEditEnabledFields", bulkEditEnabledFields.join(",")); + } + + const formDataHeaders = { + "content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366", + }; + + console.log("Calling processStep..."); + const processResponse = await QClient.getInstance().processStep( processName, processUUID, activeStep.name, - queryString, + formData, + formDataHeaders, ); setLastProcessResponse(processResponse); }; @@ -529,17 +741,24 @@ function ProcessRun(): JSX.Element back )} - {noMoreSteps || processError ? ( + {noMoreSteps || processError || qJobRunning ? ( ) : ( - - {onLastStep ? "submit" : "next"} - + <> + {formError && ( + + {formError} + + )} + + {onLastStep ? "submit" : "next"} + + )} diff --git a/src/qqq/utils/QClient.ts b/src/qqq/utils/QClient.ts index 9f46ef1..a8153cb 100644 --- a/src/qqq/utils/QClient.ts +++ b/src/qqq/utils/QClient.ts @@ -31,7 +31,7 @@ class QClient { private static qController: QController; - private static getInstance() + public static getInstance() { if (this.qController == null) {