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 = ( {tableProcesses.map((process) => ( - {process.label} + {process.label} ))} @@ -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 = ( + + {tableProcesses.map((process) => ( + + {process.label} + + ))} + + ); + 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;