diff --git a/src/App.tsx b/src/App.tsx index 4510c97..296db35 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -278,6 +278,16 @@ export default function App() component: , }); + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // 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: , + }); + 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, diff --git a/src/qqq/components/EntityForm/index.tsx b/src/qqq/components/EntityForm/index.tsx index 49ab1ed..32d4b3f 100644 --- a/src/qqq/components/EntityForm/index.tsx +++ b/src/qqq/components/EntityForm/index.tsx @@ -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(); - 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 = ( @@ -413,12 +441,12 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab { - !isModal && + !props.isModal && } - + - + @@ -490,7 +518,7 @@ function EntityForm({table, isModal, id, closeModalHandler, defaultValues, disab ); } - if (isModal) + if (props.isModal) { return ( diff --git a/src/qqq/pages/dashboards/Widgets/RecordGridWidget.tsx b/src/qqq/pages/dashboards/Widgets/RecordGridWidget.tsx index 7ab339a..8f0a826 100644 --- a/src/qqq/pages/dashboards/Widgets/RecordGridWidget.tsx +++ b/src/qqq/pages/dashboards/Widgets/RecordGridWidget.tsx @@ -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 ( 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): 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): JSX.Element - + {props.label} { @@ -173,20 +146,6 @@ function Widget(props: React.PropsWithChildren): JSX.Element {props.children} - { - showEditForm && - closeEditForm(event, reason)}> -
- -
-
- } ); } diff --git a/src/qqq/pages/entity-list/EntityList.tsx b/src/qqq/pages/entity-list/EntityList.tsx index bfcd30b..b5f5e13 100644 --- a/src/qqq/pages/entity-list/EntityList.tsx +++ b/src/qqq/pages/entity-list/EntityList.tsx @@ -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; } diff --git a/src/qqq/pages/entity-view/EntityDeveloperView.tsx b/src/qqq/pages/entity-view/EntityDeveloperView.tsx index b92a446..84b25ec 100644 --- a/src/qqq/pages/entity-view/EntityDeveloperView.tsx +++ b/src/qqq/pages/entity-view/EntityDeveloperView.tsx @@ -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; } diff --git a/src/qqq/pages/entity-view/EntityView.tsx b/src/qqq/pages/entity-view/EntityView.tsx index a7225eb..c95dc6d 100644 --- a/src/qqq/pages/entity-view/EntityView.tsx +++ b/src/qqq/pages/entity-view/EntityView.tsx @@ -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 { - ///////////////////////////////////////////////////////////////// - // the path for a process looks like: .../table/id/process // - // so if our tableName is in the -3 index, try to open process // - ///////////////////////////////////////////////////////////////// + const hashParts = location.hash.split("/"); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // the path for a process looks like: .../table/id/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 - + {section.label} @@ -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 // ////////////////////////////////////////////////////////////////////////// - const newPath = location.pathname.split("/"); - newPath.pop(); - navigate(newPath.join("/")); + 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 } + { + showEditChildForm && + closeEditChildForm(event, reason)}> +
+ +
+
+ } +
}