diff --git a/src/qqq/pages/entity-list/index.tsx b/src/qqq/pages/entity-list/index.tsx
index 79fc6be..c086991 100644
--- a/src/qqq/pages/entity-list/index.tsx
+++ b/src/qqq/pages/entity-list/index.tsx
@@ -24,7 +24,15 @@ import MenuItem from "@mui/material/MenuItem";
import Divider from "@mui/material/Divider";
import Link from "@mui/material/Link";
import { makeStyles } from "@mui/material";
-import { DataGrid, GridColDef, GridRowParams, GridRowsProp } from "@mui/x-data-grid";
+import {
+ DataGrid,
+ GridCallbackDetails,
+ GridColDef,
+ GridRowId,
+ GridRowParams,
+ GridRowsProp,
+ GridSelectionModel,
+} from "@mui/x-data-grid";
// Material Dashboard 2 PRO React TS components
import DashboardLayout from "examples/LayoutContainers/DashboardLayout";
@@ -39,6 +47,7 @@ import { QTableMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/
import { useParams } from "react-router-dom";
import QClient from "qqq/utils/QClient";
import Footer from "../../components/Footer";
+import QProcessUtils from "../../utils/QProcessUtils";
// Declaring props types for DefaultCell
interface Props {
@@ -56,6 +65,7 @@ function EntityList({ table }: Props): JSX.Element {
const [pageNumber, setPageNumber] = useState(0);
const [totalRecords, setTotalRecords] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
+ const [selectedIds, setSelectedIds] = useState([] as string[]);
const [columns, setColumns] = useState([] as GridColDef[]);
const [rows, setRows] = useState([] as GridRowsProp[]);
const [loading, setLoading] = useState(true);
@@ -117,26 +127,33 @@ function EntityList({ table }: Props): JSX.Element {
document.location.href = `/${tableName}/${params.id}`;
};
+ const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) => {
+ const newSelectedIds: string[] = [];
+ selectionModel.forEach((value: GridRowId) => {
+ newSelectedIds.push(value as string);
+ });
+ setSelectedIds(newSelectedIds);
+ };
+
if (tableName !== tableState) {
(async () => {
setTableState(tableName);
const metaData = await QClient.loadMetaData();
- const matchingProcesses: QProcessMetaData[] = [];
- const processKeys = [...metaData.processes.keys()];
- processKeys.forEach((key) => {
- const process = metaData.processes.get(key);
- if (process.tableName === tableName) {
- matchingProcesses.push(process);
- }
- });
- setTableProcesses(matchingProcesses);
+ setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName));
// reset rows to trigger rerender
setRows([]);
})();
}
+ function getRecordsQueryString() {
+ if (selectedIds.length > 0) {
+ return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`;
+ }
+ return "";
+ }
+
const renderActionsMenu = (
@@ -237,6 +254,7 @@ function EntityList({ table }: Props): JSX.Element {
paginationMode="server"
density="compact"
loading={loading}
+ onSelectionModelChange={selectionChanged}
/>
diff --git a/src/qqq/pages/entity-view/components/ViewContents/index.tsx b/src/qqq/pages/entity-view/components/ViewContents/index.tsx
index 97b03f7..2611927 100644
--- a/src/qqq/pages/entity-view/components/ViewContents/index.tsx
+++ b/src/qqq/pages/entity-view/components/ViewContents/index.tsx
@@ -30,11 +30,16 @@ import Button from "@mui/material/Button";
// qqq imports
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
+import { QProcessMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
// Material Dashboard 2 PRO React TS components
import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+import Icon from "@mui/material/Icon";
import MDButton from "../../../../../components/MDButton";
+import QProcessUtils from "../../../../utils/QProcessUtils";
const qController = new QController("");
@@ -50,8 +55,13 @@ function ViewContents({ id }: Props): JSX.Element {
const [nameValues, setNameValues] = useState([] as JSX.Element[]);
const [open, setOpen] = useState(false);
const [tableMetaData, setTableMetaData] = useState(null);
+ const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
+ const [actionsMenu, setActionsMenu] = useState(null);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
+ const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
+ const closeActionsMenu = () => setActionsMenu(null);
+
if (!asyncLoadInited) {
setAsyncLoadInited(true);
@@ -59,6 +69,9 @@ function ViewContents({ id }: Props): JSX.Element {
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
+ const metaData = await qController.loadMetaData();
+ setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName));
+
const foundRecord = await qController.get(tableName, id);
nameValues.push(
@@ -112,12 +125,42 @@ function ViewContents({ id }: Props): JSX.Element {
const editPath = `/${tableName}/${id}/edit`;
+ const renderActionsMenu = (
+
+ );
+
return (
-
- Viewing {tableMetaData?.label} ({id})
-
+
+
+ Viewing {tableMetaData?.label} ({id})
+
+ {tableProcesses.length > 0 && (
+
+ actions
+ keyboard_arrow_down
+
+ )}
+ {renderActionsMenu}
+
{nameValues}
diff --git a/src/qqq/pages/process-run/index.tsx b/src/qqq/pages/process-run/index.tsx
index 7e0a095..c4bc93f 100644
--- a/src/qqq/pages/process-run/index.tsx
+++ b/src/qqq/pages/process-run/index.tsx
@@ -37,13 +37,15 @@ import Footer from "examples/Footer";
// ProcessRun layout schemas for form and form fields
import * as Yup from "yup";
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
+import { QFieldMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import { QFrontendStepMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
-import { useParams } from "react-router-dom";
+import { useLocation, useParams } from "react-router-dom";
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
import { QJobStarted } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
import { QJobComplete } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import { QJobError } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import { QJobRunning } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
+import { DataGrid, GridColDef, GridRowParams, GridRowsProp } from "@mui/x-data-grid";
import QDynamicForm from "../../components/QDynamicForm";
import MDTypography from "../../../components/MDTypography";
@@ -51,16 +53,13 @@ function getDynamicStepContent(
stepIndex: number,
step: any,
formData: any,
- processError: string
+ processError: string,
+ processValues: any,
+ recordConfig: any
): JSX.Element {
const { formFields, values, errors, touched } = formData;
// console.log(`in getDynamicStepContent: step label ${step?.label}`);
- if (!Object.keys(formFields).length) {
- // console.log("in getDynamicStepContent. No fields yet, so returning 'loading'");
- return Loading...
;
- }
-
if (processError) {
return (
<>
@@ -72,7 +71,51 @@ function getDynamicStepContent(
);
}
- return ;
+ if (!Object.keys(formFields).length) {
+ // console.log("in getDynamicStepContent. No fields 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:
+
+
+
+
+ )}
+ >
+ );
}
function trace(name: string, isComponent: boolean = false) {
@@ -104,12 +147,17 @@ function ProcessRun(): JSX.Element {
const [initialValues, setInitialValues] = useState({});
const [validations, setValidations] = useState({});
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;
trace("ProcessRun", true);
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // handle moving to another step in the process - e.g., after the backend told us what screen to show next. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() => {
trace("updateActiveStep");
@@ -141,23 +189,79 @@ function ProcessRun(): JSX.Element {
setActiveStep(activeStep);
setFormId(activeStep.name);
- const initialValues: any = {};
- activeStep.formFields.forEach((field) => {
- initialValues[field.name] = processValues[field.name];
- });
+ ///////////////////////////////////////////////////
+ // if this step has form fields, set up the form //
+ ///////////////////////////////////////////////////
+ if (activeStep.formFields) {
+ const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(
+ activeStep.formFields
+ );
- const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(
- activeStep.formFields
- );
+ const initialValues: any = {};
+ activeStep.formFields.forEach((field) => {
+ initialValues[field.name] = processValues[field.name];
+ });
+
+ setFormFields(dynamicFormFields);
+ setInitialValues(initialValues);
+ setValidations(Yup.object().shape(formValidations));
+ }
+
+ if (activeStep.recordListFields) {
+ const newRecordConfig = {} as any;
+ newRecordConfig.pageNo = 1;
+ newRecordConfig.rowsPerPage = 20;
+ newRecordConfig.columns = [] as GridColDef[];
+ newRecordConfig.rows = [];
+ newRecordConfig.totalRecords = 0;
+ newRecordConfig.handleRowsPerPageChange = null;
+ newRecordConfig.handlePageChange = null;
+ newRecordConfig.handleRowClick = null;
+ newRecordConfig.loading = true;
+
+ activeStep.recordListFields.forEach((field) => {
+ newRecordConfig.columns.push({ field: field.name, headerName: field.label });
+ });
+
+ setRecordConfig(newRecordConfig);
+ setNeedRecords(true);
+ }
- setFormFields(dynamicFormFields);
- setInitialValues(initialValues);
- setValidations(Yup.object().shape(formValidations));
// console.log(`in updateActiveStep: formFields ${JSON.stringify(dynamicFormFields)}`);
// console.log(`in updateActiveStep: initialValues ${JSON.stringify(initialValues)}`);
}
}, [newStep]);
+ useEffect(() => {
+ if (needRecords) {
+ setNeedRecords(false);
+ (async () => {
+ const records = await qController.processRecords(
+ processName,
+ processUUID,
+ recordConfig.rowsPerPage * (recordConfig.pageNo - 1),
+ recordConfig.rowsPerPage
+ );
+ recordConfig.loading = false;
+ recordConfig.rows = [];
+ let rowId = 0;
+ records.forEach((record) => {
+ const row = Object.fromEntries(record.values.entries());
+ if (!row.id) {
+ row.id = ++rowId;
+ }
+ recordConfig.rows.push(row);
+ });
+ // todo count?
+ recordConfig.totalRecords = records.length;
+ setRecordConfig(recordConfig);
+ })();
+ }
+ }, [needRecords]);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // handle a response from the server - e.g., after starting a backend job, or getting its status/result //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() => {
if (lastProcessResponse) {
trace("handleProcessResponse");
@@ -183,53 +287,82 @@ function ProcessRun(): JSX.Element {
}
}, [lastProcessResponse]);
+ /////////////////////////////////////////////////////////////////////////
+ // while a backend async job is running, periodically check its status //
+ /////////////////////////////////////////////////////////////////////////
useEffect(() => {
if (needToCheckJobStatus) {
trace("checkJobStatus");
setNeedToCheckJobStatus(false);
(async () => {
- const processResponse = await qController.processJobStatus(
- processName,
- processUUID,
- jobUUID
- );
- setLastProcessResponse(processResponse);
+ setTimeout(async () => {
+ const processResponse = await qController.processJobStatus(
+ processName,
+ processUUID,
+ jobUUID
+ );
+ setLastProcessResponse(processResponse);
+ }, 1500);
})();
}
}, [needToCheckJobStatus]);
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // do the initial load of data for the process - that is, meta data, plus the init step //
+ //////////////////////////////////////////////////////////////////////////////////////////
if (needInitialLoad) {
trace("initialLoad");
setNeedInitialLoad(false);
(async () => {
+ const { search } = useLocation();
+ const urlSearchParams = new URLSearchParams(search);
+ let queryStringForInit = null;
+ if (urlSearchParams.get("recordIds")) {
+ queryStringForInit = `recordsParam=recordIds&recordIds=${urlSearchParams.get("recordIds")}`;
+ } else if (urlSearchParams.get("filterJSON")) {
+ queryStringForInit = `recordsParam=filterJSON&filterJSON=${urlSearchParams.get(
+ "filterJSON"
+ )}`;
+ }
+ // todo once saved filters exist
+ //else if(urlSearchParams.get("filterId")) {
+ // queryStringForInit = `recordsParam=filterId&filterId=${urlSearchParams.get("filterId")}`
+ // }
+
+ console.log(`@dk: Query String for init: ${queryStringForInit}`);
+
const processMetaData = await qController.loadProcessMetaData(processName);
// console.log(processMetaData);
setProcessMetaData(processMetaData);
setSteps(processMetaData.frontendSteps);
- const processResponse = await qController.processInit(processName);
+ const processResponse = await qController.processInit(processName, queryStringForInit);
setProcessUUID(processResponse.processUUID);
setLastProcessResponse(processResponse);
// console.log(processResponse);
})();
}
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // handle the back button - todo - not really done at all //
+ // e.g., qqq needs to say when back is or isn't allowed, and we need to hit the backend upon backs. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
const handleBack = () => {
trace("handleBack");
setNewStep(activeStepIndex - 1);
};
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // handle user submitting the form - which in qqq means moving forward from any screen. //
+ //////////////////////////////////////////////////////////////////////////////////////////
const handleSubmit = async (values: any, actions: any) => {
trace("handleSubmit");
- // eslint-disable-next-line no-alert
- // alert(JSON.stringify(values, null, 2));
+ // todo - post?
let queryString = "";
Object.keys(values).forEach((key) => {
queryString += `${key}=${encodeURIComponent(values[key])}&`;
});
- // eslint-disable-next-line no-alert
- // alert(queryString);
actions.setSubmitting(false);
actions.resetForm();
@@ -281,7 +414,9 @@ function ProcessRun(): JSX.Element {
formFields,
errors,
},
- processError
+ processError,
+ processValues,
+ recordConfig
)}
{/********************************
** back &| next/submit buttons **
diff --git a/src/qqq/utils/QProcessUtils.ts b/src/qqq/utils/QProcessUtils.ts
new file mode 100644
index 0000000..150af12
--- /dev/null
+++ b/src/qqq/utils/QProcessUtils.ts
@@ -0,0 +1,43 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { QProcessMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
+import { QInstance } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
+
+/*******************************************************************************
+ ** Utility class for working with QQQ Processes
+ **
+ *******************************************************************************/
+class QProcessUtils {
+ public static getProcessesForTable(metaData: QInstance, tableName: string): QProcessMetaData[] {
+ const matchingProcesses: QProcessMetaData[] = [];
+ const processKeys = [...metaData.processes.keys()];
+ processKeys.forEach((key) => {
+ const process = metaData.processes.get(key);
+ if (process.tableName === tableName) {
+ matchingProcesses.push(process);
+ }
+ });
+ return matchingProcesses;
+ }
+}
+
+export default QProcessUtils;