{
@@ -567,9 +612,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
)
}
{
- component.type === QComponentType.EDIT_FORM && (
-
- )
+ component.type === QComponentType.EDIT_FORM &&
+ <>
+ {
+ component.values?.sectionLabel ?
+
+
+
+ {component.values?.sectionLabel}
+
+
+
+
+
+ :
+ }
+ >
}
{
component.type === QComponentType.VIEW_FORM && step.viewFields && (
@@ -1026,6 +1084,30 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setProcessValues(qJobComplete.values);
setQJobRunning(null);
+ if(formikSetFieldValueFunction)
+ {
+ //////////////////////////////////
+ // reset field values in formik //
+ //////////////////////////////////
+ for (let key in qJobComplete.values)
+ {
+ if(Object.hasOwn(formFields, key))
+ {
+ console.log(`(re)setting form field [${key}] to [${qJobComplete.values[key]}]`);
+ formikSetFieldValueFunction(key, qJobComplete.values[key]);
+ }
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
+ if(updatedFrontendStepList)
+ {
+ setSteps(updatedFrontendStepList);
+ }
+
if (activeStep && activeStep.recordListFields)
{
setNeedRecords(true);
@@ -1385,89 +1467,98 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
>
{({
values, errors, touched, isSubmitting, setFieldValue,
- }) => (
-
+ )
+ }}
);
diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx
index 31e8d63..18e3220 100644
--- a/src/qqq/pages/records/query/RecordQuery.tsx
+++ b/src/qqq/pages/records/query/RecordQuery.tsx
@@ -867,7 +867,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
for (let i = 0; i < queryFilter?.orderBys?.length; i++)
{
const fieldName = queryFilter.orderBys[i].fieldName;
- if (fieldName.indexOf(".") > -1)
+ if (fieldName != null && fieldName.indexOf(".") > -1)
{
const joinTableName = fieldName.replaceAll(/\..*/g, "");
if (!vjtToUse.has(joinTableName))
diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx
index e34bc28..75c2d3d 100644
--- a/src/qqq/pages/records/view/RecordView.tsx
+++ b/src/qqq/pages/records/view/RecordView.tsx
@@ -74,12 +74,14 @@ const qController = Client.getInstance();
interface Props
{
table?: QTableMetaData;
+ record?: QRecord;
launchProcess?: QProcessMetaData;
}
RecordView.defaultProps =
{
table: null,
+ record: null,
launchProcess: null,
};
@@ -127,10 +129,39 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
}
+/***************************************************************************
+**
+***************************************************************************/
+export function getVisibleJoinTables(tableMetaData: QTableMetaData): Set
+{
+ const visibleJoinTables = new Set();
+
+ for (let i = 0; i < tableMetaData?.sections.length; i++)
+ {
+ const section = tableMetaData?.sections[i];
+ if (section.isHidden || !section.fieldNames || !section.fieldNames.length)
+ {
+ continue;
+ }
+
+ section.fieldNames.forEach((fieldName) =>
+ {
+ const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
+ if (tableForField && tableForField.name != tableMetaData.name)
+ {
+ visibleJoinTables.add(tableForField.name);
+ }
+ });
+ }
+
+ return (visibleJoinTables);
+}
+
+
/*******************************************************************************
** Record View Screen component.
*******************************************************************************/
-function RecordView({table, launchProcess}: Props): JSX.Element
+function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.Element
{
const {id} = useParams();
@@ -147,7 +178,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map);
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
const [metaData, setMetaData] = useState(null as QInstance);
- const [record, setRecord] = useState(null as QRecord);
+ const [record, setRecord] = useState(overrideRecord ?? null as QRecord);
const [tableSections, setTableSections] = useState([] as QTableSection[]);
const [t1Section, setT1Section] = useState(null as QTableSection);
const [t1SectionName, setT1SectionName] = useState(null as string);
@@ -381,31 +412,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
reload();
}, [location.pathname, location.hash]);
- const getVisibleJoinTables = (tableMetaData: QTableMetaData): Set =>
- {
- const visibleJoinTables = new Set();
-
- for (let i = 0; i < tableMetaData?.sections.length; i++)
- {
- const section = tableMetaData?.sections[i];
- if (section.isHidden || !section.fieldNames || !section.fieldNames.length)
- {
- continue;
- }
-
- section.fieldNames.forEach((fieldName) =>
- {
- const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
- if (tableForField && tableForField.name != tableMetaData.name)
- {
- visibleJoinTables.add(tableForField.name);
- }
- });
- }
-
- return (visibleJoinTables);
- };
-
/*******************************************************************************
** get an element (or empty) to use as help content for a section
@@ -481,7 +487,18 @@ function RecordView({table, launchProcess}: Props): JSX.Element
let record: QRecord;
try
{
- record = await qController.get(tableName, id, tableVariant, null, queryJoins);
+ ////////////////////////////////////////////////////////////////////////////
+ // if the component took in a record object, then we don't need to GET it //
+ ////////////////////////////////////////////////////////////////////////////
+ if(overrideRecord)
+ {
+ record = overrideRecord;
+ }
+ else
+ {
+ record = await qController.get(tableName, id, tableVariant, null, queryJoins);
+ }
+
setRecord(record);
recordAnalytics({category: "tableEvents", action: "view", label: tableMetaData?.label + " / " + record?.recordLabel});
}
@@ -518,7 +535,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
setPageHeader(record.recordLabel);
- if (!launchingProcess)
+ if (!launchingProcess && !activeModalProcess)
{
try
{
diff --git a/src/qqq/pages/records/view/RecordViewByUniqueKey.tsx b/src/qqq/pages/records/view/RecordViewByUniqueKey.tsx
new file mode 100644
index 0000000..5c94d08
--- /dev/null
+++ b/src/qqq/pages/records/view/RecordViewByUniqueKey.tsx
@@ -0,0 +1,163 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
+import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
+import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
+import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
+import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
+import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
+import {Alert, Box} from "@mui/material";
+import Grid from "@mui/material/Grid";
+import BaseLayout from "qqq/layouts/BaseLayout";
+import RecordView, {getVisibleJoinTables} from "qqq/pages/records/view/RecordView";
+import Client from "qqq/utils/qqq/Client";
+import TableUtils from "qqq/utils/qqq/TableUtils";
+import React, {useEffect, useState} from "react";
+import {useSearchParams} from "react-router-dom";
+
+interface RecordViewByUniqueKeyProps
+{
+ table: QTableMetaData;
+}
+
+RecordViewByUniqueKey.defaultProps = {};
+
+const qController = Client.getInstance();
+
+/***************************************************************************
+ ** Wrapper around RecordView, that reads a unique key from the query string,
+ ** looks for a record matching that key, and shows that record.
+ ***************************************************************************/
+export default function RecordViewByUniqueKey({table}: RecordViewByUniqueKeyProps): JSX.Element
+{
+ const tableName = table.name;
+
+ const [asyncLoadInited, setAsyncLoadInited] = useState(false);
+ const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
+ const [doneLoading, setDoneLoading] = useState(false);
+ const [record, setRecord] = useState(null as QRecord);
+ const [errorMessage, setErrorMessage] = useState(null as string);
+
+ const [queryParams] = useSearchParams();
+
+ if (!asyncLoadInited)
+ {
+ setAsyncLoadInited(true);
+
+ (async () =>
+ {
+ const tableMetaData = await qController.loadTableMetaData(tableName);
+ setTableMetaData(tableMetaData);
+
+ const criteria: QFilterCriteria[] = [];
+ for (let [name, value] of queryParams.entries())
+ {
+ criteria.push(new QFilterCriteria(name, QCriteriaOperator.EQUALS, [value]));
+ if(!tableMetaData.fields.has(name))
+ {
+ setErrorMessage(`Query-string parameter [${name}] is not a defined field on the ${tableMetaData.label} table.`);
+ setDoneLoading(true);
+ return;
+ }
+ }
+
+ let queryJoins: QueryJoin[] = null;
+ const visibleJoinTables = getVisibleJoinTables(tableMetaData);
+ if (visibleJoinTables.size > 0)
+ {
+ queryJoins = TableUtils.getQueryJoins(tableMetaData, visibleJoinTables);
+ }
+
+ const filter = new QQueryFilter(criteria, null, null, "AND", 0, 2);
+ qController.query(tableName, filter, queryJoins)
+ .then((queryResult) =>
+ {
+ setDoneLoading(true);
+ if (queryResult.length == 1)
+ {
+ setRecord(queryResult[0]);
+ }
+ else if (queryResult.length == 0)
+ {
+ setErrorMessage(`No ${tableMetaData.label} record was found matching the given values.`);
+ }
+ else if (queryResult.length > 1)
+ {
+ setErrorMessage(`More than one ${tableMetaData.label} record was found matching the given values.`);
+ }
+ })
+ .catch((error) =>
+ {
+ setDoneLoading(true);
+ console.log(error);
+ if (error && error.message)
+ {
+ setErrorMessage(error.message);
+ }
+ else if (error && error.response && error.response.data && error.response.data.error)
+ {
+ setErrorMessage(error.response.data.error);
+ }
+ else
+ {
+ setErrorMessage("Unexpected error running query");
+ }
+ });
+ })();
+ }
+
+ useEffect(() =>
+ {
+ if (asyncLoadInited)
+ {
+ setAsyncLoadInited(false);
+ setDoneLoading(false);
+ setRecord(null);
+ }
+ }, [queryParams]);
+
+ if (!doneLoading)
+ {
+ return (Loading...
);
+ }
+ else if (record)
+ {
+ return ();
+ }
+ else if (errorMessage)
+ {
+ return (
+
+
+
+
+ {
+ {errorMessage}
+ }
+
+
+
+
+ );
+ }
+}
diff --git a/src/qqq/utils/qqq/TableUtils.ts b/src/qqq/utils/qqq/TableUtils.ts
index 1a218c6..31df936 100644
--- a/src/qqq/utils/qqq/TableUtils.ts
+++ b/src/qqq/utils/qqq/TableUtils.ts
@@ -133,6 +133,11 @@ class TableUtils
*******************************************************************************/
public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
{
+ if(!fieldName)
+ {
+ return [null, null];
+ }
+
if (fieldName.indexOf(".") > -1)
{
const nameParts = fieldName.split(".", 2);
diff --git a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java
index 9671353..e052739 100755
--- a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java
+++ b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java
@@ -335,7 +335,7 @@ public class QSeleniumLib
return;
}
- if(elements.stream().noneMatch(e -> e.getText().toLowerCase().contains(textContains)))
+ if(elements.stream().noneMatch(e -> e.getText().toLowerCase().contains(textContains.toLowerCase())))
{
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "] containing text [" + textContains + "]");
return;
@@ -345,7 +345,7 @@ public class QSeleniumLib
}
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
- fail("Failed for non-existence of element matching selector [" + cssSelector + "] after [" + WAIT_SECONDS + "] seconds.");
+ fail("Failed for non-existence of element matching selector [" + cssSelector + "] containing text [" + textContains + "] after [" + WAIT_SECONDS + "] seconds.");
}