Update to support opening child editing form via url (hash or sub-path); also proceses on record-view via hash

This commit is contained in:
2022-12-06 15:56:20 -06:00
parent ad239544f5
commit 80a3c0679e
7 changed files with 220 additions and 95 deletions

View File

@ -278,6 +278,16 @@ export default function App()
component: <EntityCreate table={table} />,
});
///////////////////////////////////////////////////////////////////////////////////////////////////////
// this is the path to open a modal-form when viewing a record, to create a different (child) record //
// it can also be done with a hash like: #/createChild=:childTableName //
///////////////////////////////////////////////////////////////////////////////////////////////////////
routeList.push({
key: `${app.name}.createChild`,
route: `${path}/:id/createChild/:childTableName`,
component: <EntityView table={table} />,
});
routeList.push({
name: `${app.label} View`,
key: `${app.name}.view`,
@ -302,6 +312,10 @@ export default function App()
const processesForTable = QProcessUtils.getProcessesForTable(metaData, table.name, true);
processesForTable.forEach((process) =>
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// paths to open modal process under its owning table. //
// note, processes can also be launched (at least initially on entityView screen) with a hash like: #/launchProcess=:processName //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
routeList.push({
name: process.label,
key: process.name,

View File

@ -34,7 +34,7 @@ import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import {Form, Formik} from "formik";
import React, {useContext, useReducer, useState} from "react";
import {useLocation, useNavigate, useParams} from "react-router-dom";
import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom";
import * as Yup from "yup";
import QContext from "QContext";
import {QCancelButton, QSaveButton} from "qqq/components/QButtons";
@ -67,11 +67,11 @@ EntityForm.defaultProps = {
disabledFields: {},
};
function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disabledFields}: Props): JSX.Element
function EntityForm(props: Props): JSX.Element
{
const qController = QClient.getInstance();
const tableNameParam = useParams().tableName;
const tableName = table === null ? tableNameParam : table.name;
const tableName = props.table === null ? tableNameParam : props.table.name;
const [formTitle, setFormTitle] = useState("");
const [validations, setValidations] = useState({});
@ -96,6 +96,34 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
const navigate = useNavigate();
const location = useLocation();
////////////////////////////////////////////////////////////////////
// first take defaultValues and disabledFields from props //
// but, also allow them to be sent in the hash, in the format of: //
// #/defaultValues={jsonName=value}/disabledFields={jsonName=any} //
////////////////////////////////////////////////////////////////////
let defaultValues = props.defaultValues;
let disabledFields = props.disabledFields;
const hashParts = location.hash.split("/");
for (let i = 0; i < hashParts.length; i++)
{
try
{
const parts = hashParts[i].split("=")
if (parts.length > 1 && parts[0] == "defaultValues")
{
defaultValues = JSON.parse(decodeURIComponent(parts[1])) as { [key: string]: any };
}
if (parts.length > 1 && parts[0] == "disabledFields")
{
disabledFields = JSON.parse(decodeURIComponent(parts[1])) as { [key: string]: any };
}
}
catch (e)
{}
}
function getFormSection(values: any, touched: any, formFields: any, errors: any): JSX.Element
{
const formData: any = {};
@ -142,13 +170,13 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
/////////////////////////////////////////////////////////////////////////////////
let record: QRecord = null;
let defaultDisplayValues = new Map<string, string>();
if (id !== null)
if (props.id !== null)
{
record = await qController.get(tableName, id);
record = await qController.get(tableName, props.id);
setRecord(record);
setFormTitle(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
if (!isModal)
if (!props.isModal)
{
setPageHeader(`Edit ${tableMetaData?.label}: ${record?.recordLabel}`);
}
@ -172,7 +200,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
///////////////////////////////////////////
setFormTitle(`Creating New ${tableMetaData?.label}`);
if (!isModal)
if (!props.isModal)
{
setPageHeader(`Creating New ${tableMetaData?.label}`);
}
@ -268,7 +296,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
// if id !== null - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////
if (id !== null || field.isEditable)
if (props.id !== null || field.isEditable)
{
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
}
@ -316,7 +344,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
// but if the user used the anchors on the page, this doesn't effectively cancel... //
// what we have here pushed a new history entry (I think?), so could be better //
///////////////////////////////////////////////////////////////////////////////////////
if (id !== null)
if (props.id !== null)
{
const path = `${location.pathname.replace(/\/edit$/, "")}`;
navigate(path, {replace: true});
@ -333,15 +361,15 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
actions.setSubmitting(true);
await (async () =>
{
if (id !== null)
if (props.id !== null)
{
await qController
.update(tableName, id, values)
.update(tableName, props.id, values)
.then((record) =>
{
if (isModal)
if (props.isModal)
{
closeModalHandler(null, "recordUpdated");
props.closeModalHandler(null, "recordUpdated");
}
else
{
@ -362,9 +390,9 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
.create(tableName, values)
.then((record) =>
{
if (isModal)
if (props.isModal)
{
closeModalHandler(null, "recordCreated");
props.closeModalHandler(null, "recordCreated");
}
else
{
@ -380,7 +408,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
})();
};
const formId = id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
let body;
if (noCapabilityError)
@ -399,7 +427,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
}
else
{
const cardElevation = isModal ? 3 : 1;
const cardElevation = props.isModal ? 3 : 1;
body = (
<MDBox mb={3}>
<Grid container spacing={3}>
@ -413,12 +441,12 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
</Grid>
<Grid container spacing={3}>
{
!isModal &&
!props.isModal &&
<Grid item xs={12} lg={3}>
<QRecordSidebar tableSections={tableSections} />
</Grid>
}
<Grid item xs={12} lg={isModal ? 12 : 9}>
<Grid item xs={12} lg={props.isModal ? 12 : 9}>
<Formik
initialValues={initialValues}
@ -475,7 +503,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
<MDBox component="div" p={3}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={isModal ? closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<QSaveButton disabled={isSubmitting} />
</Grid>
</MDBox>
@ -490,7 +518,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab
);
}
if (isModal)
if (props.isModal)
{
return (
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>

View File

@ -24,7 +24,7 @@ import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {DataGridPro} from "@mui/x-data-grid-pro";
import React, {useEffect, useState} from "react";
import DataGridUtils from "qqq/utils/DataGridUtils";
import Widget, {AddNewRecordButton, HeaderLink} from "./Widget";
import Widget, {AddNewRecordButton, HeaderLink, LabelComponent} from "./Widget";
interface Props
{
@ -65,15 +65,23 @@ function RecordGridWidget({title, data, reloadWidgetCallback}: Props): JSX.Eleme
}
}, [data]);
const labelAdditionalComponentsLeft: LabelComponent[] = []
if(data && data.viewAllLink)
{
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
}
const labelAdditionalComponentsRight: LabelComponent[] = []
if(data && data.canAddChildRecord)
{
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords))
}
return (
<Widget
label={title}
labelAdditionalComponentsLeft={[
new HeaderLink("View All", data.viewAllLink)
]}
labelAdditionalComponentsRight={[
new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords)
]}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
reloadWidgetCallback={reloadWidgetCallback}
>
<DataGridPro

View File

@ -26,14 +26,14 @@ import Card from "@mui/material/Card";
import Modal from "@mui/material/Modal";
import Typography from "@mui/material/Typography";
import React, {useState} from "react";
import {Link} from "react-router-dom";
import {Link, useNavigate} from "react-router-dom";
import EntityForm from "qqq/components/EntityForm";
interface Props
{
label: string;
labelAdditionalComponentsLeft: [LabelComponent];
labelAdditionalComponentsRight: [LabelComponent];
labelAdditionalComponentsLeft: LabelComponent[];
labelAdditionalComponentsRight: LabelComponent[];
children: JSX.Element;
reloadWidgetCallback?: (widgetIndex: number, params: string) => void;
}
@ -46,7 +46,7 @@ Widget.defaultProps = {
class LabelComponent
export class LabelComponent
{
}
@ -89,40 +89,13 @@ export class AddNewRecordButton extends LabelComponent
function Widget(props: React.PropsWithChildren<Props>): JSX.Element
{
const [showEditForm, setShowEditForm] = useState(null as any);
const navigate = useNavigate();
function openEditForm(table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any)
{
const showEditForm: any = {};
showEditForm.table = table;
showEditForm.id = id;
showEditForm.defaultValues = defaultValues;
showEditForm.disabledFields = disabledFields;
setShowEditForm(showEditForm);
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
}
const closeEditForm = (event: object, reason: string) =>
{
if (reason === "backdropClick")
{
return;
}
if (reason === "recordUpdated" || reason === "recordCreated")
{
if(props.reloadWidgetCallback)
{
props.reloadWidgetCallback(0, "ok");
}
else
{
window.location.reload()
}
}
setShowEditForm(null);
};
function renderComponent(component: LabelComponent)
{
if(component instanceof HeaderLink)
@ -152,7 +125,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
<Card sx={{width: "100%"}}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box py={2}>
<Typography variant="h6" fontWeight="medium" p={3} display="inline">
<Typography variant="h5" fontWeight="medium" p={3} display="inline">
{props.label}
</Typography>
{
@ -173,20 +146,6 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
</Box>
{props.children}
</Card>
{
showEditForm &&
<Modal open={showEditForm as boolean} onClose={(event, reason) => closeEditForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditForm}
table={showEditForm.table}
id={showEditForm.id}
defaultValues={showEditForm.defaultValues}
disabledFields={showEditForm.disabledFields} />
</div>
</Modal>
}
</>
);
}

View File

@ -795,7 +795,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
const closeModalProcess = (event: object, reason: string) =>
{
if (reason === "backdropClick")
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}

View File

@ -162,7 +162,7 @@ function EntityDeveloperView({table}: Props): JSX.Element
const closeEditingScript = (event: object, reason: string, alert: string = null) =>
{
if (reason === "backdropClick")
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}

View File

@ -46,6 +46,7 @@ import {useLocation, useNavigate, useParams, useSearchParams} from "react-router
import QContext from "QContext";
import BaseLayout from "qqq/components/BaseLayout";
import DashboardWidgets from "qqq/components/DashboardWidgets";
import EntityForm from "qqq/components/EntityForm";
import {QActionsMenuButton, QDeleteButton, QEditButton} from "qqq/components/QButtons";
import QRecordSidebar from "qqq/components/QRecordSidebar";
import colors from "qqq/components/Temporary/colors";
@ -60,7 +61,6 @@ import QValueUtils from "qqq/utils/QValueUtils";
const qController = QClient.getInstance();
// Declaring props types for ViewForm
interface Props
{
table?: QTableMetaData;
@ -70,7 +70,7 @@ interface Props
EntityView.defaultProps =
{
table: null,
launchProcess: null
launchProcess: null,
};
function EntityView({table, launchProcess}: Props): JSX.Element
@ -102,6 +102,7 @@ function EntityView({table, launchProcess}: Props): JSX.Element
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null);
@ -126,10 +127,16 @@ function EntityView({table, launchProcess}: Props): JSX.Element
{
try
{
/////////////////////////////////////////////////////////////////
const hashParts = location.hash.split("/");
///////////////////////////////////////////////////////////////////////////////////////////////
// the path for a process looks like: .../table/id/process //
// so if our tableName is in the -3 index, try to open process //
/////////////////////////////////////////////////////////////////
// the path for creating a child record looks like: .../table/id/createChild/:childTableName //
///////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////
// if our tableName is in the -3 index, try to open process //
//////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 3] === tableName)
{
const processName = pathParts[pathParts.length - 1];
@ -144,19 +151,69 @@ function EntityView({table, launchProcess}: Props): JSX.Element
console.log(`Couldn't find process named ${processName}`);
}
}
///////////////////////////////////////////////////////////////////////
// alternatively, look for a launchProcess specification in the hash //
// e.g., for non-natively rendered links to open the modal. //
///////////////////////////////////////////////////////////////////////
for (let i = 0; i < hashParts.length; i++)
{
const parts = hashParts[i].split("=")
if (parts.length > 1 && parts[0] == "launchProcess")
{
(async () =>
{
const processMetaData = await qController.loadProcessMetaData(parts[1])
setActiveModalProcess(processMetaData);
})();
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == "createChild")
{
(async () =>
{
const childTable = await qController.loadTableMetaData(pathParts[pathParts.length - 1])
const childId: any = null; // todo - for editing a child, not just creating one.
openEditChildForm(childTable, childId, null, null); // todo - defaults & disableds
})();
return;
}
/////////////////////////////////////////////////////////////////////
// alternatively, look for a createChild specification in the hash //
// e.g., for non-natively rendered links to open the modal. //
/////////////////////////////////////////////////////////////////////
for (let i = 0; i < hashParts.length; i++)
{
const parts = hashParts[i].split("=")
if (parts.length > 1 && parts[0] == "createChild")
{
(async () =>
{
const childTable = await qController.loadTableMetaData(parts[1])
const childId: any = null; // todo - for editing a child, not just creating one.
openEditChildForm(childTable, childId, null, null); // todo - defaults & disableds
})();
return;
}
}
}
catch (e)
{
console.log(e);
}
/////////////////////////////////////////////////////////////
// if we didn't open a process, assume we need to (re)load //
/////////////////////////////////////////////////////////////
reload();
///////////////////////////////////////////////////////////////////
// if we didn't open something, then, assume we need to (re)load //
///////////////////////////////////////////////////////////////////
setActiveModalProcess(null);
}, [location.pathname]);
reload();
}, [location.pathname, location.hash]);
if (!asyncLoadInited)
{
@ -275,7 +332,7 @@ function EntityView({table, launchProcess}: Props): JSX.Element
<Grid id={section.name} key={section.name} item lg={12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
<MDBox width="100%">
<Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}}>
<MDTypography variant="h6" p={3} pb={1}>
<MDTypography variant="h5" p={3} pb={1}>
{section.label}
</MDTypography>
<MDBox p={3} pt={0} flexDirection="column">
@ -395,7 +452,7 @@ function EntityView({table, launchProcess}: Props): JSX.Element
const closeModalProcess = (event: object, reason: string) =>
{
if (reason === "backdropClick")
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
@ -403,9 +460,53 @@ function EntityView({table, launchProcess}: Props): JSX.Element
//////////////////////////////////////////////////////////////////////////
// when closing a modal process, navigate up to the record being viewed //
//////////////////////////////////////////////////////////////////////////
if(location.hash)
{
navigate(location.pathname);
}
else
{
const newPath = location.pathname.split("/");
newPath.pop();
navigate(newPath.join("/"));
}
setActiveModalProcess(null);
};
function openEditChildForm(table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any)
{
const showEditChildForm: any = {};
showEditChildForm.table = table;
showEditChildForm.id = id;
showEditChildForm.defaultValues = defaultValues;
showEditChildForm.disabledFields = disabledFields;
setShowEditChildForm(showEditChildForm);
}
const closeEditChildForm = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
/////////////////////////////////////////////////
// navigate back up to the record being viewed //
/////////////////////////////////////////////////
if(location.hash)
{
navigate(location.pathname);
}
else
{
const newPath = location.pathname.split("/");
newPath.pop();
newPath.pop();
navigate(newPath.join("/"));
}
setShowEditChildForm(null);
};
return (
@ -515,6 +616,21 @@ function EntityView({table, launchProcess}: Props): JSX.Element
</Modal>
}
{
showEditChildForm &&
<Modal open={showEditChildForm as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditChildForm}
table={showEditChildForm.table}
id={showEditChildForm.id}
defaultValues={showEditChildForm.defaultValues}
disabledFields={showEditChildForm.disabledFields} />
</div>
</Modal>
}
</MDBox>
}
</MDBox>