Switch to use modal processes

This commit is contained in:
2022-10-07 09:48:44 -05:00
parent 0182db5f02
commit 60d440db09
9 changed files with 474 additions and 132 deletions

View File

@ -269,13 +269,21 @@ export default function App()
name: process.label, name: process.label,
key: process.name, key: process.name,
route: `${path}/${process.name}`, route: `${path}/${process.name}`,
component: <ProcessRun process={process} />, component: <EntityList table={table} launchProcess={process} />,
});
routeList.push({
name: process.label,
key: `${app.name}/${process.name}`,
route: `${path}/:id/${process.name}`,
component: <EntityView table={table} launchProcess={process} />,
}); });
}); });
const reportsForTable = QProcessUtils.getReportsForTable(metaData, table.name, true); const reportsForTable = QProcessUtils.getReportsForTable(metaData, table.name, true);
reportsForTable.forEach((report) => reportsForTable.forEach((report) =>
{ {
// todo - do we need some table/report routes here, that would go to EntityList and/or EntityView
routeList.push({ routeList.push({
name: report.label, name: report.label,
key: report.name, key: report.name,

View File

@ -102,8 +102,8 @@ function DashboardWidgets({widgetMetaDataList, entityPrimaryKey}: Props): JSX.El
}; };
const widgetCount = widgetMetaDataList ? widgetMetaDataList.length : 0; const widgetCount = widgetMetaDataList ? widgetMetaDataList.length : 0;
console.log(JSON.stringify(widgetMetaDataList)); // console.log(JSON.stringify(widgetMetaDataList));
console.log(widgetCount); // console.log(widgetCount);
return ( return (
widgetCount > 0 ? ( widgetCount > 0 ? (

View File

@ -68,7 +68,7 @@ function StepperCard({data}: Props): JSX.Element
} }
})(StepConnector); })(StepConnector);
console.log(`data ${JSON.stringify(data)}`); // console.log(`data ${JSON.stringify(data)}`);
return ( return (
<Stepper connector={<CustomizedConnector />} activeStep={activeStep} alternativeLabel sx={{paddingBottom: "0px", boxShadow: "none", background: "white"}}> <Stepper connector={<CustomizedConnector />} activeStep={activeStep} alternativeLabel sx={{paddingBottom: "0px", boxShadow: "none", background: "white"}}>

View File

@ -68,9 +68,9 @@ function TableCard({title, linkText, linkURL, noRowsFoundHTML, data, dropdownOpt
const [dropdownLabel, setDropdownLabel] = useState<string>(""); const [dropdownLabel, setDropdownLabel] = useState<string>("");
const [dropdownIcon, setDropdownIcon] = useState<string>(openArrowIcon); const [dropdownIcon, setDropdownIcon] = useState<string>(openArrowIcon);
console.log(`data: ${JSON.stringify(data?.rows)}`); // console.log(`data: ${JSON.stringify(data?.rows)}`);
console.log(`bool: ${data && data?.columns && !data?.rows}`); // console.log(`bool: ${data && data?.columns && !data?.rows}`);
console.log(`norowsfound: ${noRowsFoundHTML}`); // console.log(`norowsfound: ${noRowsFoundHTML}`);
const openDropdown = ({currentTarget}: any) => const openDropdown = ({currentTarget}: any) =>
{ {

View File

@ -21,6 +21,7 @@
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType"; import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
@ -30,14 +31,38 @@ import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryF
import {Alert, TablePagination} from "@mui/material"; import {Alert, TablePagination} from "@mui/material";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import LinearProgress from "@mui/material/LinearProgress"; import LinearProgress from "@mui/material/LinearProgress";
import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import {DataGridPro, getGridDateOperators, getGridNumericOperators, getGridStringOperators, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridExportMenuItemProps, GridFilterItem, GridFilterModel, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent} from "@mui/x-data-grid-pro"; import Modal from "@mui/material/Modal";
import {
DataGridPro, getGridDateOperators, getGridNumericOperators, getGridStringOperators,
GridCallbackDetails,
GridColDef,
GridColumnOrderChangeParams,
GridColumnVisibilityModel,
GridExportMenuItemProps,
GridFilterItem,
GridFilterModel,
GridRowId,
GridRowParams,
GridRowsProp,
GridSelectionModel,
GridSortItem,
GridSortModel,
GridToolbarColumnsButton,
GridToolbarContainer,
GridToolbarDensitySelector,
GridToolbarExportContainer,
GridToolbarFilterButton,
MuiEvent
} from "@mui/x-data-grid-pro";
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator"; import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from "react"; import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from "react";
import {Link, useNavigate, useParams, useSearchParams} from "react-router-dom"; import {Link, useLocation, useNavigate, useSearchParams} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import DashboardLayout from "qqq/components/DashboardLayout"; import DashboardLayout from "qqq/components/DashboardLayout";
import Footer from "qqq/components/Footer"; import Footer from "qqq/components/Footer";
@ -45,6 +70,7 @@ import Navbar from "qqq/components/Navbar";
import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons"; import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons";
import MDAlert from "qqq/components/Temporary/MDAlert"; import MDAlert from "qqq/components/Temporary/MDAlert";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import ProcessRun from "qqq/pages/process-run";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import QFilterUtils from "qqq/utils/QFilterUtils"; import QFilterUtils from "qqq/utils/QFilterUtils";
import QProcessUtils from "qqq/utils/QProcessUtils"; import QProcessUtils from "qqq/utils/QProcessUtils";
@ -58,8 +84,14 @@ const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
interface Props interface Props
{ {
table?: QTableMetaData; table?: QTableMetaData;
launchProcess?: QProcessMetaData;
} }
EntityList.defaultProps = {
table: null,
launchProcess: null
};
/******************************************************************************* /*******************************************************************************
** Get the default filter to use on the page - either from query string, or ** Get the default filter to use on the page - either from query string, or
** local storage, or a default (empty). ** local storage, or a default (empty).
@ -109,13 +141,17 @@ function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URLSearch
return ({items: []}); return ({items: []});
} }
function EntityList({table}: Props): JSX.Element function EntityList({table, launchProcess}: Props): JSX.Element
{ {
const tableNameParam = useParams().tableName; const tableName = table.name;
const tableName = table === null ? tableNameParam : table.name;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const qController = QClient.getInstance(); const qController = QClient.getInstance();
const location = useLocation();
const navigate = useNavigate();
const pathParts = location.pathname.split("/");
//////////////////////////////////////////// ////////////////////////////////////////////
// look for defaults in the local storage // // look for defaults in the local storage //
//////////////////////////////////////////// ////////////////////////////////////////////
@ -159,6 +195,7 @@ function EntityList({table}: Props): JSX.Element
const [, setFiltersMenu] = useState(null); const [, setFiltersMenu] = useState(null);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [totalRecords, setTotalRecords] = useState(0); const [totalRecords, setTotalRecords] = useState(0);
const [selectedIds, setSelectedIds] = useState([] as string[]); const [selectedIds, setSelectedIds] = useState([] as string[]);
@ -171,6 +208,10 @@ function EntityList({table}: Props): JSX.Element
const [gridMouseDownX, setGridMouseDownX] = useState(0); const [gridMouseDownX, setGridMouseDownX] = useState(0);
const [gridMouseDownY, setGridMouseDownY] = useState(0); const [gridMouseDownY, setGridMouseDownY] = useState(0);
const [pinnedColumns, setPinnedColumns] = useState({left: ["__check__", "id"]}); const [pinnedColumns, setPinnedColumns] = useState({left: ["__check__", "id"]});
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData)
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
const instance = useRef({timer: null}); const instance = useRef({timer: null});
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -191,6 +232,44 @@ function EntityList({table}: Props): JSX.Element
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
/////////////////////////////////////////////////////////////////////////////////////////
// monitor location changes - if our url looks like a process, then open that process. //
/////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
try
{
/////////////////////////////////////////////////////////////////
// the path for a process looks like: .../table/process //
// so if our tableName is in the -2 index, try to open process //
/////////////////////////////////////////////////////////////////
if(pathParts[pathParts.length - 2] === tableName)
{
const processName = pathParts[pathParts.length - 1];
const processList = allTableProcesses.filter(p => p.name.endsWith(processName));
if(processList.length > 0)
{
setActiveModalProcess(processList[0]);
return;
}
else
{
console.log(`Couldn't find process named ${processName}`);
}
}
}
catch(e)
{
console.log(e);
}
////////////////////////////////////////////////////////////////////////////////////
// if we didn't open a process... not sure what we do in the table/query use-case //
////////////////////////////////////////////////////////////////////////////////////
setActiveModalProcess(null);
}, [location]);
const buildQFilter = () => const buildQFilter = () =>
{ {
const qFilter = new QQueryFilter(); const qFilter = new QQueryFilter();
@ -315,6 +394,30 @@ function EntityList({table}: Props): JSX.Element
delete countResults[latestQueryId]; delete countResults[latestQueryId];
}, [receivedCountTimestamp]); }, [receivedCountTimestamp]);
const betweenOperator =
{
label: "Between",
value: "between",
getApplyFilterFn: (filterItem: GridFilterItem) =>
{
if (!Array.isArray(filterItem.value) || filterItem.value.length !== 2)
{
return null;
}
if (filterItem.value[0] == null || filterItem.value[1] == null)
{
return null;
}
// @ts-ignore
return ({value}) =>
{
return (value !== null && filterItem.value[0] <= value && value <= filterItem.value[1]);
};
},
// InputComponent: InputNumberInterval,
};
const booleanTrueOperator: GridFilterOperator = { const booleanTrueOperator: GridFilterOperator = {
label: "is yes", label: "is yes",
value: "isTrue", value: "isTrue",
@ -546,7 +649,6 @@ function EntityList({table}: Props): JSX.Element
localStorage.setItem(rowsPerPageLocalStorageKey, JSON.stringify(size)); localStorage.setItem(rowsPerPageLocalStorageKey, JSON.stringify(size));
}; };
const navigate = useNavigate();
const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) => const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) =>
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -657,7 +759,14 @@ function EntityList({table}: Props): JSX.Element
const metaData = await qController.loadMetaData(); const metaData = await qController.loadMetaData();
QValueUtils.qInstance = metaData; QValueUtils.qInstance = metaData;
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
if(launchingProcess)
{
setLaunchingProcess(null);
setActiveModalProcess(launchingProcess);
}
// reset rows to trigger rerender // reset rows to trigger rerender
setRows([]); setRows([]);
@ -766,36 +875,90 @@ function EntityList({table}: Props): JSX.Element
return ""; return "";
} }
function getRecordIdsForProcess() : string | QQueryFilter
{
if (selectFullFilterState === "filter")
{
return (buildQFilter());
}
if (selectedIds.length > 0)
{
return (selectedIds.join(","));
}
return "";
}
const openModalProcess = (process: QProcessMetaData = null) =>
{
navigate(`${process.name}${getRecordsQueryString()}`);
closeActionsMenu();
};
const closeModalProcess = (event: object, reason: string) =>
{
if(reason === "backdropClick")
{
return;
}
/////////////////////////////////////////////////////////////////////////
// when closing a modal process, navigate up to the table being viewed //
/////////////////////////////////////////////////////////////////////////
const newPath = location.pathname.split("/");
newPath.pop();
navigate(newPath.join("/"));
updateTable();
}
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
{
const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`));
if(processList.length > 0)
{
openModalProcess(processList[0]);
}
else
{
setAlertContent(`Could not find Bulk ${processLabelPart} process for this table.`);
}
}
const bulkLoadClicked = () => const bulkLoadClicked = () =>
{ {
navigate(`${tableName}.bulkInsert`); closeActionsMenu();
openBulkProcess("Insert", "Load");
}; };
const bulkEditClicked = () => const bulkEditClicked = () =>
{ {
closeActionsMenu();
if (getNoOfSelectedRecords() === 0) if (getNoOfSelectedRecords() === 0)
{ {
setAlertContent("No records were selected to Bulk Edit."); setAlertContent("No records were selected to Bulk Edit.");
return; return;
} }
navigate(`${tableName}.bulkEdit${getRecordsQueryString()}`); openBulkProcess("Edit", "Edit");
}; };
const bulkDeleteClicked = () => const bulkDeleteClicked = () =>
{ {
closeActionsMenu();
if (getNoOfSelectedRecords() === 0) if (getNoOfSelectedRecords() === 0)
{ {
setAlertContent("No records were selected to Bulk Delete."); setAlertContent("No records were selected to Bulk Delete.");
return; return;
} }
navigate(`${tableName}.bulkDelete${getRecordsQueryString()}`); openBulkProcess("Delete", "Delete");
}; };
const processClicked = (process: QProcessMetaData) => const processClicked = (process: QProcessMetaData) =>
{ {
// todo - let the process specify that it needs initial rows - err if none selected. // todo - let the process specify that it needs initial rows - err if none selected.
// alternatively, let a process itself have an initial screen to select rows... // alternatively, let a process itself have an initial screen to select rows...
navigate(`${process.name}${getRecordsQueryString()}`); openModalProcess(process);
}; };
// @ts-ignore // @ts-ignore
@ -910,12 +1073,24 @@ function EntityList({table}: Props): JSX.Element
onClose={closeActionsMenu} onClose={closeActionsMenu}
keepMounted keepMounted
> >
<MenuItem onClick={bulkLoadClicked}>Bulk Load</MenuItem> <MenuItem onClick={bulkLoadClicked}>
<MenuItem onClick={bulkEditClicked}>Bulk Edit</MenuItem> <ListItemIcon><Icon>library_add</Icon></ListItemIcon>
<MenuItem onClick={bulkDeleteClicked}>Bulk Delete</MenuItem> Bulk Load
{tableProcesses.length > 0 && <MenuItem divider />} </MenuItem>
<MenuItem onClick={bulkEditClicked}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
Bulk Edit
</MenuItem>
<MenuItem onClick={bulkDeleteClicked}>
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
Bulk Delete
</MenuItem>
{tableProcesses.length > 0 && <Divider />}
{tableProcesses.map((process) => ( {tableProcesses.map((process) => (
<MenuItem key={process.name} onClick={() => processClicked(process)}>{process.label}</MenuItem> <MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{process.label}
</MenuItem>
))} ))}
</Menu> </Menu>
); );
@ -1017,13 +1192,17 @@ function EntityList({table}: Props): JSX.Element
</MDBox> </MDBox>
</Card> </Card>
</MDBox> </MDBox>
{
activeModalProcess &&
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
<ProcessRun process={activeModalProcess} isModal={true} recordIds={getRecordIdsForProcess()} closeModalHandler={closeModalProcess} />
</Modal>
}
<Footer /> <Footer />
</DashboardLayout> </DashboardLayout>
); );
} }
EntityList.defaultProps = {
table: null,
};
export default EntityList; export default EntityList;

View File

@ -33,10 +33,13 @@ import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText"; import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle"; import DialogTitle from "@mui/material/DialogTitle";
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal";
import React, {useContext, useEffect, useReducer, useState} from "react"; import React, {useContext, useEffect, useReducer, useState} from "react";
import {useLocation, useNavigate, useSearchParams} from "react-router-dom"; import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
@ -47,6 +50,7 @@ import colors from "qqq/components/Temporary/colors";
import MDAlert from "qqq/components/Temporary/MDAlert"; import MDAlert from "qqq/components/Temporary/MDAlert";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import ProcessRun from "qqq/pages/process-run";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import QProcessUtils from "qqq/utils/QProcessUtils"; import QProcessUtils from "qqq/utils/QProcessUtils";
import QTableUtils from "qqq/utils/QTableUtils"; import QTableUtils from "qqq/utils/QTableUtils";
@ -59,15 +63,21 @@ interface Props
{ {
id: string; id: string;
table?: QTableMetaData; table?: QTableMetaData;
launchProcess?: QProcessMetaData
} }
function ViewContents({id, table}: Props): JSX.Element ViewContents.defaultProps = {
table: null,
launchProcess: null
};
function ViewContents({id, table, launchProcess}: Props): JSX.Element
{ {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const pathParts = location.pathname.split("/"); const pathParts = location.pathname.split("/");
const tableName = table ? table.name : pathParts[pathParts.length - 2]; const tableName = table.name;
const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>); const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>);
@ -79,13 +89,17 @@ function ViewContents({id, table}: Props): JSX.Element
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element); const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]); const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [tableWidgets, setTableWidgets] = useState([] as QWidgetMetaData[]); const [tableWidgets, setTableWidgets] = useState([] as QWidgetMetaData[]);
const [notFoundMessage, setNotFoundMessage] = useState(null); const [notFoundMessage, setNotFoundMessage] = useState(null);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const {setPageHeader} = useContext(QContext); const {setPageHeader} = useContext(QContext);
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData)
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
@ -101,9 +115,45 @@ function ViewContents({id, table}: Props): JSX.Element
setTableWidgets(null); setTableWidgets(null);
}; };
////////////////////////////////////////////////////////////////////////////////////////////////////
// monitor location changes - if we've clicked a link from viewing one record to viewing another, //
// we'll stay in this component, but we'll need to reload all data for the new record. //
// if, however, our url looks like a process, then open that process. //
////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() => useEffect(() =>
{ {
try
{
/////////////////////////////////////////////////////////////////
// the path for a process looks like: .../table/id/process //
// so if our tableName is in the -3 index, try to open process //
/////////////////////////////////////////////////////////////////
if(pathParts[pathParts.length - 3] === tableName)
{
const processName = pathParts[pathParts.length - 1];
const processList = allTableProcesses.filter(p => p.name.endsWith(processName));
if(processList.length > 0)
{
setActiveModalProcess(processList[0]);
return;
}
else
{
console.log(`Couldn't find process named ${processName}`);
}
}
}
catch(e)
{
console.log(e);
}
/////////////////////////////////////////////////////////////
// if we didn't open a process, assume we need to (re)load //
/////////////////////////////////////////////////////////////
reload(); reload();
setActiveModalProcess(null);
}, [location]); }, [location]);
if (!asyncLoadInited) if (!asyncLoadInited)
@ -112,10 +162,10 @@ function ViewContents({id, table}: Props): JSX.Element
(async () => (async () =>
{ {
////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
// load the table meta-data (if needed) // // load the full table meta-data (the one we took in is a partial) //
////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
const tableMetaData = table || await qController.loadTableMetaData(tableName); const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData); setTableMetaData(tableMetaData);
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
@ -123,7 +173,15 @@ function ViewContents({id, table}: Props): JSX.Element
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
const metaData = await qController.loadMetaData(); const metaData = await qController.loadMetaData();
QValueUtils.qInstance = metaData; QValueUtils.qInstance = metaData;
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); const processesForTable = QProcessUtils.getProcessesForTable(metaData, tableName);
setTableProcesses(processesForTable);
setAllTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
if(launchingProcess)
{
setLaunchingProcess(null);
setActiveModalProcess(launchingProcess);
}
///////////////////// /////////////////////
// load the record // // load the record //
@ -236,8 +294,7 @@ function ViewContents({id, table}: Props): JSX.Element
function processClicked(process: QProcessMetaData) function processClicked(process: QProcessMetaData)
{ {
const path = `${pathParts.slice(0, -1).join("/")}/${process.name}?recordIds=${id}`; openModalProcess(process);
navigate(path);
} }
const renderActionsMenu = ( const renderActionsMenu = (
@ -255,22 +312,50 @@ function ViewContents({id, table}: Props): JSX.Element
onClose={closeActionsMenu} onClose={closeActionsMenu}
keepMounted keepMounted
> >
<MenuItem onClick={() => navigate("edit")}>Edit</MenuItem> <MenuItem onClick={() => navigate("edit")}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
Edit
</MenuItem>
<MenuItem onClick={() => <MenuItem onClick={() =>
{ {
setActionsMenu(null); setActionsMenu(null);
handleClickDeleteButton(); handleClickDeleteButton();
}} }}
> >
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
Delete Delete
</MenuItem> </MenuItem>
{tableProcesses.length > 0 && <MenuItem divider />} {tableProcesses.length > 0 && <Divider />}
{tableProcesses.map((process) => ( {tableProcesses.map((process) => (
<MenuItem key={process.name} onClick={() => processClicked(process)}>{process.label}</MenuItem> <MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{process.label}
</MenuItem>
))} ))}
</Menu> </Menu>
); );
const openModalProcess = (process: QProcessMetaData = null) =>
{
navigate(process.name);
closeActionsMenu();
};
const closeModalProcess = (event: object, reason: string) =>
{
if(reason === "backdropClick")
{
return;
}
//////////////////////////////////////////////////////////////////////////
// when closing a modal process, navigate up to the record being viewed //
//////////////////////////////////////////////////////////////////////////
const newPath = location.pathname.split("/");
newPath.pop();
navigate(newPath.join("/"));
}
return ( return (
notFoundMessage notFoundMessage
? ?
@ -364,13 +449,17 @@ function ViewContents({id, table}: Props): JSX.Element
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
{
activeModalProcess &&
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
<ProcessRun process={activeModalProcess} isModal={true} recordIds={id} closeModalHandler={closeModalProcess} />
</Modal>
}
</MDBox> </MDBox>
); );
} }
ViewContents.defaultProps = {
table: null,
};
export default ViewContents; export default ViewContents;

View File

@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import {useParams} from "react-router-dom"; import {useParams} from "react-router-dom";
@ -29,9 +30,10 @@ import ViewContents from "./components/ViewContents";
interface Props interface Props
{ {
table?: QTableMetaData; table?: QTableMetaData;
launchProcess?: QProcessMetaData;
} }
function EntityView({table}: Props): JSX.Element function EntityView({table, launchProcess}: Props): JSX.Element
{ {
const {id} = useParams(); const {id} = useParams();
@ -41,7 +43,7 @@ function EntityView({table}: Props): JSX.Element
<Grid container> <Grid container>
<Grid item xs={12}> <Grid item xs={12}>
<MDBox mb={3}> <MDBox mb={3}>
<ViewContents id={id} /> <ViewContents table={table} id={id} launchProcess={launchProcess}/>
</MDBox> </MDBox>
</Grid> </Grid>
</Grid> </Grid>
@ -52,6 +54,7 @@ function EntityView({table}: Props): JSX.Element
EntityView.defaultProps = { EntityView.defaultProps = {
table: null, table: null,
launchProcess: null
}; };
export default EntityView; export default EntityView;

View File

@ -31,6 +31,7 @@ import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobEr
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning"; import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted"; import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Button, CircularProgress, Icon, TablePagination} from "@mui/material"; import {Button, CircularProgress, Icon, TablePagination} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -65,13 +66,16 @@ interface Props
{ {
process?: QProcessMetaData; process?: QProcessMetaData;
defaultProcessValues?: any; defaultProcessValues?: any;
isModal?: boolean
recordIds?: string | QQueryFilter
closeModalHandler?: (event: object, reason: string) => void
} }
const INITIAL_RETRY_MILLIS = 1_500; const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000; const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5; const BACKOFF_AMOUNT = 1.5;
function ProcessRun({process, defaultProcessValues}: Props): JSX.Element function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeModalHandler}: Props): JSX.Element
{ {
const processNameParam = useParams().processName; const processNameParam = useParams().processName;
const processName = process === null ? processNameParam : process.name; const processName = process === null ? processNameParam : process.name;
@ -248,6 +252,13 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
) )
} }
</MDTypography> </MDTypography>
<MDBox component="div" py={3}>
<Grid container justifyContent="flex-end" spacing={3}>
{isModal ? <QCancelButton onClickHandler={handleCancelClicked} disabled={false} label="Close" />
: <QCancelButton onClickHandler={handleCancelClicked} disabled={false} />
}
</Grid>
</MDBox>
</> </>
); );
} }
@ -294,7 +305,10 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
return ( return (
<> <>
<MDTypography variation="h5" component="div" fontWeight="bold">{step?.label}</MDTypography> <MDTypography variation="h5" component="div" fontWeight="bold">
{isModal ? `${process.label}: ` : ""}
{step?.label}
</MDTypography>
{step.components && ( {step.components && (
step.components.map((component: QFrontendComponent, index: number) => ( step.components.map((component: QFrontendComponent, index: number) => (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
@ -839,6 +853,19 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
// queryStringPairsForInit.push("recordsParam=filterId"); // queryStringPairsForInit.push("recordsParam=filterId");
// queryStringPairsForInit.push(`filterId=${urlSearchParams.get("filterId")}`); // queryStringPairsForInit.push(`filterId=${urlSearchParams.get("filterId")}`);
// } // }
else if(recordIds)
{
if(typeof recordIds === "string")
{
queryStringPairsForInit.push("recordsParam=recordIds");
queryStringPairsForInit.push(`recordIds=${recordIds}`);
}
else if (recordIds instanceof QQueryFilter)
{
queryStringPairsForInit.push("recordsParam=filterJSON");
queryStringPairsForInit.push(`filterJSON=${JSON.stringify(recordIds)}`);
}
}
try try
{ {
@ -964,6 +991,12 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
const handleCancelClicked = () => const handleCancelClicked = () =>
{ {
if(isModal && closeModalHandler)
{
closeModalHandler(null, "cancelClicked");
return;
}
const pathParts = location.pathname.split(/\//); const pathParts = location.pathname.split(/\//);
pathParts.pop(); pathParts.pop();
const path = pathParts.join("/"); const path = pathParts.join("/");
@ -971,8 +1004,8 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
}; };
const mainCardStyles: any = {}; const mainCardStyles: any = {};
mainCardStyles.minHeight = "calc(100vh - 400px)"; mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
if (!processError && (qJobRunning || activeStep === null)) if (!processError && (qJobRunning || activeStep === null) && !isModal)
{ {
mainCardStyles.background = "none"; mainCardStyles.background = "none";
mainCardStyles.boxShadow = "none"; mainCardStyles.boxShadow = "none";
@ -994,102 +1027,124 @@ function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
nextButtonIcon = "check"; nextButtonIcon = "check";
} }
return ( const body = (
<BaseLayout> <MDBox py={3} mb={20}>
<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={10} xl={8}>
<Grid item xs={12} lg={10} xl={8}> <Formik
<Formik enableReinitialize
enableReinitialize initialValues={initialValues}
initialValues={initialValues} validationSchema={validationScheme}
validationSchema={validationScheme} validation={validationFunction}
validation={validationFunction} onSubmit={handleSubmit}
onSubmit={handleSubmit} >
> {({
{({ values, errors, touched, isSubmitting, setFieldValue,
values, errors, touched, isSubmitting, setFieldValue, }) => (
}) => ( <Form id={formId} autoComplete="off">
<Form id={formId} autoComplete="off"> <Card sx={mainCardStyles}>
<Card sx={mainCardStyles}> <MDBox mx={2} mt={-3}>
<MDBox mx={2} mt={-3}> <Stepper activeStep={activeStepIndex} alternativeLabel>
<Stepper activeStep={activeStepIndex} alternativeLabel> {steps.map((step) => (
{steps.map((step) => ( <Step key={step.name}>
<Step key={step.name}> <StepLabel>{step.label}</StepLabel>
<StepLabel>{step.label}</StepLabel> </Step>
</Step> ))}
))} </Stepper>
</Stepper> </MDBox>
</MDBox>
<MDBox p={3}> <MDBox p={3}>
<MDBox> <MDBox>
{/*************************************************************************** {/***************************************************************************
** step content - e.g., the appropriate form or other screen for the step ** ** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/} ***************************************************************************/}
{getDynamicStepContent( {getDynamicStepContent(
activeStepIndex, activeStepIndex,
activeStep, activeStep,
{ {
values, values,
touched, touched,
formFields, formFields,
errors, errors,
}, },
processError, processError,
processValues, processValues,
recordConfig, recordConfig,
setFieldValue, setFieldValue,
)} )}
{/******************************** {/********************************
** back &| next/submit buttons ** ** back &| next/submit buttons **
********************************/} ********************************/}
<MDBox mt={6} width="100%" display="flex" justifyContent="space-between"> <MDBox mt={6} width="100%" display="flex" justifyContent="space-between">
{true || activeStepIndex === 0 ? ( {true || activeStepIndex === 0 ? (
<MDBox /> <MDBox />
) : ( ) : (
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton> <MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
)} )}
{processError || qJobRunning || !activeStep ? ( {processError || qJobRunning || !activeStep ? (
<MDBox /> <MDBox />
) : ( ) : (
<> <>
{formError && ( {formError && (
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth> <MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
{formError} {formError}
</MDTypography> </MDTypography>
)} )}
{ {
noMoreSteps && <QCancelButton onClickHandler={handleCancelClicked} label="Return" iconName="arrow_back" disabled={isSubmitting} /> noMoreSteps && <QCancelButton
} onClickHandler={handleCancelClicked}
{ label={isModal ? "Close" : "Return"}
!noMoreSteps && ( iconName={isModal ? "cancel" : "arrow_back"}
<MDBox component="div" py={3}> disabled={isSubmitting} />
<Grid container justifyContent="flex-end" spacing={3}> }
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} /> {
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} /> !noMoreSteps && (
</Grid> <MDBox component="div" py={3}>
</MDBox> <Grid container justifyContent="flex-end" spacing={3}>
) <QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
} <QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
</> </Grid>
)} </MDBox>
</MDBox> )
}
</>
)}
</MDBox> </MDBox>
</MDBox> </MDBox>
</Card> </MDBox>
</Form> </Card>
)} </Form>
</Formik> )}
</Grid> </Formik>
</Grid> </Grid>
</MDBox> </Grid>
</BaseLayout> </MDBox>
); );
if(isModal)
{
return (
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
{body}
</Box>
);
}
else
{
return (
<BaseLayout>
{body}
</BaseLayout>
);
}
} }
ProcessRun.defaultProps = { ProcessRun.defaultProps = {
process: null, process: null,
defaultProcessValues: {} defaultProcessValues: {},
isModal: false,
recordIds: null,
closeModalHandler: null
}; };
export default ProcessRun; export default ProcessRun;

View File

@ -136,3 +136,11 @@
{ {
align-items: flex-end; align-items: flex-end;
} }
/* google drive picker - make it be above our modal */
.picker,
.picker.picker-dialog-bg,
.picker.picker-dialog
{
z-index: 99999;
}