Compare commits

..

25 Commits

Author SHA1 Message Date
71a1bfaa6b CE-938: fixed filter display text, dashboard widget style change 2024-05-29 12:58:06 -05:00
d9e9a0be08 CE-938 Add calls to processCancel 2024-05-28 16:27:51 -05:00
aefb282a0e CE-938 update qfc to 1.0.102 - adding processCancel 2024-05-28 16:27:03 -05:00
fb57718c1c Add some ?.'s around metaData.widgets (in case an instance has no widgets) 2024-05-28 16:26:24 -05:00
ba213b038b Removing cypress 2024-05-28 16:25:56 -05:00
69daf47021 CE-938: renamed reportSetup widget to filterAndColumns 2024-05-22 15:30:13 -05:00
1d24b9b40c CE-938: updated 'flashing' occurring in child widget whenever any form fields are changed, instead of only the data in the widget 2024-05-22 12:38:16 -05:00
f44ba8d6d3 CE-938: improvements to the report setup widget 2024-05-21 18:26:35 -05:00
dc131d5189 qqq-frontend-core 1.0.101 2024-05-15 19:44:12 -05:00
2b5cc1610f For CE-1280 - add helpContent to process steps, along with css for help content to do standard app style alerts 2024-05-15 19:42:25 -05:00
a36bdb1474 Merge pull request #60 from Kingsrook/feature/CE-1240-out-of-stock-summary-page
Feature/ce 1240 out of stock summary page
2024-05-15 19:20:59 -05:00
c2926d26e8 Merge pull request #59 from Kingsrook/feature/CE-1180-order-address-validation
Feature/ce 1180 order address validation
2024-05-15 19:17:49 -05:00
eb42a86655 CE-1180 Update to only set formik values for fields that are in the form (to avoid setting, e.g., 'backend only' type fields, like the extract code-reference 2024-05-15 09:01:38 -05:00
b7f715f832 Change to javascript-scroll into view, rather than use anchors (they don't work upon reload anyway, due to async loading, and they broke record-view-by-key); also move overflow down an element in the stack, to make border-radius look better 2024-05-14 22:29:22 -05:00
16a08cfd42 Fix, don't push record-view/process urls into history 2024-05-14 22:28:25 -05:00
f5919c66ab Add whitespace nowrap to goto button 2024-05-14 20:29:21 -05:00
0831a87674 CE-1180 Add joins to request for the record 2024-05-13 08:46:46 -05:00
dd5cd459ce CE-1180 reset formik form values (to latest values from backend) after each backend step 2024-05-13 08:46:11 -05:00
c200cc9fab CE-1180 Add a null-check in getFieldandTable 2024-05-10 15:51:54 -05:00
17f378131d CE-1180 Add a null-check in ensureOrderBysFromJoinTablesAreVisibleTables (not sure why, but feels safe and good) 2024-05-10 15:51:37 -05:00
376a7a342e do toLowerCase on both sides of a contains check... also better fail message for not-contains text. 2024-05-10 15:50:29 -05:00
fcadea3192 CE-1180 Better support for processes w/ dynamic flows, by using updtedFrontnedStepList;
- fixed min-height on the bar w/ steps (e.g., before they're known)
- updated EDIT_FORM to support values: "includeFieldNames" - to do a sub-set of field names (so you can organize them a bit across multiple EDIT_FORM's) and "sectionLabel"
2024-05-10 15:49:39 -05:00
086ab775fc CE-1180 Update qqq-frontend-core to 1.0.100 💯 🎉 - updatedFrontendStepList in process output 2024-05-10 15:46:43 -05:00
5693661d20 CE-1180 Updated to support multi-value keys. also, support tables (e.g., api tables) w/o a primary key, by redirecting a successful UK lookup to the (new) /key?queryString record-view screen 2024-05-10 15:45:44 -05:00
8c9224aceb CE-1180 Add new table/key?queryString endpoint, to look up a record by a (unique) key and then view it 2024-05-10 15:33:56 -05:00
19 changed files with 1773 additions and 1469 deletions

View File

@ -1,12 +0,0 @@
import {defineConfig} from "cypress";
export default defineConfig({
e2e: {
viewportHeight: 1000,
viewportWidth: 1200,
setupNodeEvents(on, config)
{
// implement node event listeners here
},
},
});

2174
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.99",
"@kingsrook/qqq-frontend-core": "1.0.102",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",

View File

@ -49,6 +49,7 @@ import EntityEdit from "qqq/pages/records/edit/RecordEdit";
import RecordQuery from "qqq/pages/records/query/RecordQuery";
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
import RecordView from "qqq/pages/records/view/RecordView";
import RecordViewByUniqueKey from "qqq/pages/records/view/RecordViewByUniqueKey";
import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import Client from "qqq/utils/qqq/Client";
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
@ -392,6 +393,13 @@ export default function App()
component: <RecordView table={table} />,
});
routeList.push({
name: `${app.label} View`,
key: `${app.name}.view`,
route: `${path}/key`,
component: <RecordViewByUniqueKey table={table} />,
});
routeList.push({
name: `${app.label}`,
key: `${app.name}.edit`,

View File

@ -44,9 +44,9 @@ import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import ReportSetupWidget from "qqq/components/widgets/misc/ReportSetupWidget";
import {FieldRule, FieldRuleAction, FieldRuleTrigger} from "qqq/models/fields/FieldRules";
import HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client";
@ -88,7 +88,7 @@ EntityForm.defaultProps = {
////////////////////////////////////////////////////////////////////////////
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
{
}
};
function EntityForm(props: Props): JSX.Element
{
@ -119,11 +119,12 @@ function EntityForm(props: Props): JSX.Element
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
const [modalDataChangedCounter, setModalDataChangedCount] = useState(0);
const [notAllowedError, setNotAllowedError] = useState(null as string);
const [formValuesJSON, setFormValuesJSON] = useState("");
const [formValues, setFormValues] = useState({} as {[name: string]: any});
const [formValues, setFormValues] = useState({} as { [name: string]: any });
const {pageHeader, setPageHeader} = useContext(QContext);
@ -282,6 +283,8 @@ function EntityForm(props: Props): JSX.Element
setRenderedWidgetSections(newRenderedWidgetSections);
forceUpdate();
setModalDataChangedCount(modalDataChangedCounter + 1);
setShowEditChildForm(null);
}
@ -291,7 +294,7 @@ function EntityForm(props: Props): JSX.Element
*******************************************************************************/
useEffect(() =>
{
const newRenderedWidgetSections: {[name: string]: JSX.Element} = {};
const newRenderedWidgetSections: { [name: string]: JSX.Element } = {};
for (let widgetName in renderedWidgetSections)
{
const widgetMetaData = metaData.widgets.get(widgetName);
@ -351,12 +354,11 @@ function EntityForm(props: Props): JSX.Element
}
/*******************************************************************************
** if we have a widget that wants to set form-field values, they can take this
** function in as a callback, and then call it with their values.
*******************************************************************************/
function setFormFieldValuesFromWidget(values: {[name: string]: any})
function setFormFieldValuesFromWidget(values: { [name: string]: any })
{
for (let key in values)
{
@ -370,13 +372,13 @@ function EntityForm(props: Props): JSX.Element
*******************************************************************************/
function getWidgetSection(widgetMetaData: QWidgetMetaData, widgetData: any): JSX.Element
{
if(widgetMetaData.type == "childRecordList")
if (widgetMetaData.type == "childRecordList")
{
widgetData.viewAllLink = null;
widgetMetaData.showExportButton = false;
return <RecordGridWidget
key={new Date().getTime()} // added so that editing values actually re-renders...
key={`${formValues["tableName"]}-${modalDataChangedCounter}`}
widgetMetaData={widgetMetaData}
data={widgetData}
disableRowClick
@ -388,18 +390,27 @@ function EntityForm(props: Props): JSX.Element
/>;
}
if(widgetMetaData.type == "reportSetup")
if (widgetMetaData.type == "filterAndColumnsSetup")
{
return <ReportSetupWidget
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the widget metadata specifies a table name, set form values to that so widget knows which to use //
// (for the case when it is not being specified by a separate field in the record) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
if (widgetMetaData?.defaultValues?.has("tableName"))
{
formValues["tableName"] = widgetMetaData?.defaultValues.get("tableName");
}
return <FilterAndColumnsSetupWidget
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
isEditable={true}
widgetMetaData={widgetMetaData}
recordValues={formValues}
onSaveCallback={setFormFieldValuesFromWidget}
/>
/>;
}
if(widgetMetaData.type == "pivotTableSetup")
if (widgetMetaData.type == "pivotTableSetup")
{
return <PivotTableSetupWidget
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
@ -407,10 +418,10 @@ function EntityForm(props: Props): JSX.Element
widgetMetaData={widgetMetaData}
recordValues={formValues}
onSaveCallback={setFormFieldValuesFromWidget}
/>
/>;
}
if(widgetMetaData.type == "dynamicForm")
if (widgetMetaData.type == "dynamicForm")
{
return <DynamicFormWidget
key={formValues["savedReportId"]} // todo - pull this from the metaData (could do so above too...)
@ -420,10 +431,10 @@ function EntityForm(props: Props): JSX.Element
recordValues={formValues}
record={record}
onSaveCallback={setFormFieldValuesFromWidget}
/>
/>;
}
return (<Box>Unsupported widget type: {widgetMetaData.type}</Box>)
return (<Box>Unsupported widget type: {widgetMetaData.type}</Box>);
}
@ -449,12 +460,12 @@ function EntityForm(props: Props): JSX.Element
function setupFieldRules(tableMetaData: QTableMetaData)
{
const mdbMetaData = tableMetaData?.supplementalTableMetaData?.get("materialDashboard");
if(!mdbMetaData)
if (!mdbMetaData)
{
return;
}
if(mdbMetaData.fieldRules)
if (mdbMetaData.fieldRules)
{
const newFieldRules: FieldRule[] = [];
for (let i = 0; i < mdbMetaData.fieldRules.length; i++)
@ -488,15 +499,15 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
{
const widget = metaData.widgets.get(section.widgetName);
if(widget)
const widget = metaData?.widgets.get(section.widgetName);
if (widget)
{
if(widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
{
return (true);
}
if(widget.type == "reportSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm")
if (widget.type == "filterAndColumnsSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm")
{
return (true);
}
@ -680,7 +691,7 @@ function EntityForm(props: Props): JSX.Element
}
const hasFields = section.fieldNames && section.fieldNames.length > 0;
if(hasFields)
if (hasFields)
{
for (let j = 0; j < section.fieldNames.length; j++)
{
@ -719,7 +730,7 @@ function EntityForm(props: Props): JSX.Element
}
else
{
const widgetMetaData = metaData.widgets.get(section.widgetName);
const widgetMetaData = metaData?.widgets.get(section.widgetName);
const widgetData = await qController.widget(widgetMetaData.name, makeQueryStringWithIdAndObject(tableMetaData, defaultValues));
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData);
@ -1000,19 +1011,19 @@ function EntityForm(props: Props): JSX.Element
/*******************************************************************************
**
*******************************************************************************/
function makeQueryStringWithIdAndObject(tableMetaData: QTableMetaData, object: {[key: string]: any})
function makeQueryStringWithIdAndObject(tableMetaData: QTableMetaData, object: { [key: string]: any })
{
const queryParamsArray: string[] = [];
if(props.id)
if (props.id)
{
queryParamsArray.push(`${tableMetaData.primaryKeyField}=${encodeURIComponent(props.id)}`)
queryParamsArray.push(`${tableMetaData.primaryKeyField}=${encodeURIComponent(props.id)}`);
}
if(object)
if (object)
{
for (let key in object)
{
queryParamsArray.push(`${key}=${encodeURIComponent(object[key])}`)
queryParamsArray.push(`${key}=${encodeURIComponent(object[key])}`);
}
}
@ -1023,7 +1034,7 @@ function EntityForm(props: Props): JSX.Element
/*******************************************************************************
**
*******************************************************************************/
async function reloadWidget(widgetName: string, additionalQueryParamsForWidget: {[key: string]: any })
async function reloadWidget(widgetName: string, additionalQueryParamsForWidget: { [key: string]: any })
{
const widgetData = await qController.widget(widgetName, makeQueryStringWithIdAndObject(tableMetaData, additionalQueryParamsForWidget));
const widgetMetaData = metaData.widgets.get(widgetName);
@ -1045,11 +1056,11 @@ function EntityForm(props: Props): JSX.Element
/*******************************************************************************
** process a form-field having a changed value (e.g., apply field rules).
*******************************************************************************/
function handleChangedFieldValue(fieldName: string, oldValue: any, newValue: any, valueChangesToMake: {[fieldName: string]: any})
function handleChangedFieldValue(fieldName: string, oldValue: any, newValue: any, valueChangesToMake: { [fieldName: string]: any })
{
for (let fieldRule of fieldRules)
{
if(fieldRule.trigger == FieldRuleTrigger.ON_CHANGE && fieldRule.sourceField == fieldName)
if (fieldRule.trigger == FieldRuleTrigger.ON_CHANGE && fieldRule.sourceField == fieldName)
{
switch (fieldRule.action)
{
@ -1058,7 +1069,7 @@ function EntityForm(props: Props): JSX.Element
valueChangesToMake[fieldRule.targetField] = null;
break;
case FieldRuleAction.RELOAD_WIDGET:
const additionalQueryParamsForWidget: {[key: string]: any} = {};
const additionalQueryParamsForWidget: { [key: string]: any } = {};
additionalQueryParamsForWidget[fieldRule.sourceField] = newValue;
reloadWidget(fieldRule.targetWidget, additionalQueryParamsForWidget);
}
@ -1148,21 +1159,21 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////
// if we have values from formik, look at them //
/////////////////////////////////////////////////
if(values)
if (values)
{
////////////////////////////////////////////////////////////////////////
// use stringified values as cheap/easy way to see if any are changed //
////////////////////////////////////////////////////////////////////////
const newFormValuesJSON = JSON.stringify(values);
if(formValuesJSON != newFormValuesJSON)
if (formValuesJSON != newFormValuesJSON)
{
const valueChangesToMake: {[fieldName: string]: any} = {};
const valueChangesToMake: { [fieldName: string]: any } = {};
////////////////////////////////////////////////////////////////////
// if the form is dirty (e.g., we're not doing the initial load), //
// then process rules for any changed fields //
////////////////////////////////////////////////////////////////////
if(dirty)
if (dirty)
{
for (let fieldName in values)
{
@ -1194,7 +1205,7 @@ function EntityForm(props: Props): JSX.Element
setFieldValue(fieldName, valueChangesToMake[fieldName], false);
}
setFormValues(formValues)
setFormValues(formValues);
setFormValuesJSON(JSON.stringify(values));
}
}

View File

@ -35,6 +35,7 @@ import DialogTitle from "@mui/material/DialogTitle";
import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import TextField from "@mui/material/TextField";
import {any} from "prop-types";
import React, {useState} from "react";
import {useNavigate} from "react-router-dom";
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
@ -71,7 +72,12 @@ function hasGotoFieldNames(tableMetaData: QTableMetaData): boolean
function GotoRecordDialog(props: Props): JSX.Element
{
const fields: QFieldMetaData[] = [];
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is an array of array of fields. //
// that is - each entry in the top-level array is a set of fields that can be used together to goto a record //
// such as (pkey), (ukey-field1,ukey-field2). //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
const options: QFieldMetaData[][] = [];
let pkey = props?.tableMetaData?.fields.get(props?.tableMetaData?.primaryKeyField);
let addedPkey = false;
@ -82,31 +88,38 @@ function GotoRecordDialog(props: Props): JSX.Element
{
for (let i = 0; i < mdbMetaData.gotoFieldNames.length; i++)
{
// todo - multi-field keys!!
let fieldName = mdbMetaData.gotoFieldNames[i][0];
let field = props.tableMetaData.fields.get(fieldName);
if (field)
const option: QFieldMetaData[] = [];
options.push(option);
for (let j = 0; j < mdbMetaData.gotoFieldNames[i].length; j++)
{
fields.push(field);
if (field.name == pkey.name)
let fieldName = mdbMetaData.gotoFieldNames[i][j];
let field = props.tableMetaData.fields.get(fieldName);
if (field)
{
addedPkey = true;
option.push(field);
if (pkey != null && field.name == pkey.name)
{
addedPkey = true;
}
}
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// if pkey wasn't in the gotoField options meta-data, go ahead add it as an option here //
//////////////////////////////////////////////////////////////////////////////////////////
if (pkey && !addedPkey)
{
fields.unshift(pkey);
options.unshift([pkey]);
}
const makeInitialValues = () =>
{
const rs = {} as { [field: string]: string };
fields.forEach((field) => rs[field.name] = "");
options.forEach((option) => option.forEach((field) => rs[field.name] = ""));
return (rs);
};
@ -141,11 +154,16 @@ function GotoRecordDialog(props: Props): JSX.Element
}
else if (e.key == "Enter" && targetId?.startsWith("gotoInput-"))
{
const index = targetId?.replaceAll("gotoInput-", "");
const parts = targetId?.split(/-/);
const index = parts[1];
document.getElementById("gotoButton-" + index).click();
}
};
/***************************************************************************
** event handler for close button
***************************************************************************/
const closeRequested = () =>
{
if (props.mayClose)
@ -154,10 +172,47 @@ function GotoRecordDialog(props: Props): JSX.Element
}
};
const goClicked = async (fieldName: string) =>
/*******************************************************************************
** function to say if an option's submit button should be disabled
*******************************************************************************/
const isOptionSubmitButtonDisabled = (optionIndex: number) =>
{
let anyFieldsInThisOptionHaveAValue = false;
options[optionIndex].forEach((field) =>
{
if(values[field.name])
{
anyFieldsInThisOptionHaveAValue = true;
}
})
if(!anyFieldsInThisOptionHaveAValue)
{
return (true);
}
return (false);
}
/***************************************************************************
** event handler for clicking an 'option's go/submit button
***************************************************************************/
const optionGoClicked = async (optionIndex: number) =>
{
setError("");
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, null, "AND", null, 10);
const criteria: QFilterCriteria[] = [];
const queryStringParts: string[] = [];
options[optionIndex].forEach((field) =>
{
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]))
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`)
})
const filter = new QQueryFilter(criteria, null, null, "AND", null, 10);
try
{
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant);
@ -168,12 +223,26 @@ function GotoRecordDialog(props: Props): JSX.Element
}
else if (queryResult.length == 1)
{
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
if(options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
{
/////////////////////////////////////////////////
// navigate by pkey, if that's how we searched //
/////////////////////////////////////////////////
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
}
else
{
/////////////////////////////////
// else navigate by unique-key //
/////////////////////////////////
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/key/?${queryStringParts.join("&")}`);
}
close();
}
else
{
setError("More than 1 record found...");
setError("More than 1 record was found...");
setTimeout(() => setError(""), 3000);
}
}
@ -187,7 +256,7 @@ function GotoRecordDialog(props: Props): JSX.Element
if (props.tableMetaData)
{
if (fields.length == 0 && !error)
if (options.length == 0 && !error)
{
setError("This table is not configured for this feature.");
}
@ -200,31 +269,38 @@ function GotoRecordDialog(props: Props): JSX.Element
<DialogContent>
{props.subHeader}
{
fields.map((field, index) =>
(
<Grid key={field.name} container alignItems="center" py={1}>
<Grid item xs={3} textAlign="right" pr={2}>
{field.label}
</Grid>
<Grid item xs={6}>
<TextField
id={`gotoInput-${index}`}
autoFocus={index == 0}
autoComplete="off"
inputProps={{width: "100%"}}
onChange={(e) => handleChange(field.name, e.target.value)}
value={values[field.name]}
sx={{width: "100%"}}
onFocus={event => event.target.select()}
/>
</Grid>
<Grid item xs={1} pl={2}>
<MDButton id={`gotoButton-${index}`} type="submit" variant="gradient" color="info" size="small" onClick={() => goClicked(field.name)} fullWidth startIcon={<Icon>double_arrow</Icon>} disabled={`${values[field.name]}`.length == 0}>
Go
</MDButton>
</Grid>
</Grid>
))
options.map((option, optionIndex) =>
<Box key={optionIndex}>
{
option.map((field, index) =>
(
<Grid key={field.name} container alignItems="center" py={1}>
<Grid item xs={3} textAlign="right" pr={2}>
{field.label}
</Grid>
<Grid item xs={6}>
<TextField
id={`gotoInput-${optionIndex}-${index}`}
autoFocus={optionIndex == 0 && index == 0}
autoComplete="off"
inputProps={{width: "100%"}}
onChange={(e) => handleChange(field.name, e.target.value)}
value={values[field.name]}
sx={{width: "100%"}}
onFocus={event => event.target.select()}
/>
</Grid>
<Grid item xs={1} pl={2}>
{
(index == option.length - 1) &&
<MDButton id={`gotoButton-${optionIndex}`} type="submit" variant="gradient" color="info" size="small" onClick={() => optionGoClicked(optionIndex)} fullWidth startIcon={<Icon>double_arrow</Icon>} disabled={isOptionSubmitButtonDisabled(optionIndex)}>Go</MDButton>
}
</Grid>
</Grid>
))
}
</Box>
)
}
{
error &&
@ -282,7 +358,7 @@ export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element
return (
<React.Fragment>
{
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto}>Go To...</Button>
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} sx={{whiteSpace: "nowrap"}}>Go To...</Button>
}
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} tableVariant={props.tableVariant} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} subHeader={props.subHeader} />
</React.Fragment>

View File

@ -22,7 +22,7 @@
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import Box from "@mui/material/Box";
import {Box} from "@mui/material";
import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon";
import {Theme} from "@mui/material/styles";
@ -76,12 +76,12 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
return (
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "auto", maxHeight: "calc(100vh - 2rem)"}}>
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "hidden", maxHeight: "calc(100vh - 2rem)"}}>
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none", overflow: "auto", height: "100%"}}>
{
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
<HashLink key={`section-link-${entry.name}`} to={`#${entry.name}`}>
<Box key={`section-link-${entry.name}`} onClick={() => document.getElementById(entry.name).scrollIntoView()} sx={{cursor: "pointer"}}>
<Box key={`section-${entry.name}`} component="li" pt={key === 0 ? 0 : 1}>
<MDTypography
variant="button"
@ -112,7 +112,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
</MDTypography>
</Box>
</HashLink>
</Box>
)) : null
}
</Box>

View File

@ -25,7 +25,8 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Box, Button} from "@mui/material";
import {Alert, Button} from "@mui/material";
import Box from "@mui/material/Box";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
@ -94,12 +95,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext);
/////////////////////////////////////////////////////////////////////////////////////////
// this component is used by <RecordQuery> - but that component has different usages - //
// e.g., the full-fledged query screen, but also, within other screens (e.g., a modal //
// under the ReportSetupWidget). So, there are some behaviors we only want when we're //
// on the full-fledged query screen, such as changing the URL with saved view ids. //
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
// this component is used by <RecordQuery> - but that component has different usages - //
// e.g., the full-fledged query screen, but also, within other screens (e.g., a modal //
// under the FilterAndColumnsSetupWidget). So, there are some behaviors we only want when //
// we're on the full-fledged query screen, such as changing the URL with saved view ids. //
/////////////////////////////////////////////////////////////////////////////////////////////
const isQueryScreen = queryScreenUsage == "queryScreen";
const openSavedViewsMenu = (event: any) => setSavedViewsMenu(event.currentTarget);

View File

@ -40,10 +40,10 @@ import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
import DividerWidget from "qqq/components/widgets/misc/Divider";
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget";
import ReportSetupWidget from "qqq/components/widgets/misc/ReportSetupWidget";
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
import StepperCard from "qqq/components/widgets/misc/StepperCard";
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
@ -598,9 +598,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
)
}
{
widgetMetaData.type === "reportSetup" && (
widgetMetaData.type === "filterAndColumnsSetup" && (
widgetData && widgetData[i] && widgetData[i].queryParams &&
<ReportSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
{
}} />
)

View File

@ -22,6 +22,9 @@
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Collapse} from "@mui/material";
import Box from "@mui/material/Box";
@ -42,7 +45,7 @@ import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
import React, {useContext, useEffect, useRef, useState} from "react";
interface ReportSetupWidgetProps
interface FilterAndColumnsSetupWidgetProps
{
isEditable: boolean;
widgetMetaData: QWidgetMetaData;
@ -50,7 +53,7 @@ interface ReportSetupWidgetProps
onSaveCallback?: (values: { [name: string]: any }) => void;
}
ReportSetupWidget.defaultProps = {
FilterAndColumnsSetupWidget.defaultProps = {
onSaveCallback: null
};
@ -80,9 +83,10 @@ const qController = Client.getInstance();
/*******************************************************************************
** Component for editing the main setup of a report - that is: filter & columns
*******************************************************************************/
export default function ReportSetupWidget({isEditable, widgetMetaData, recordValues, onSaveCallback}: ReportSetupWidgetProps): JSX.Element
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element
{
const [modalOpen, setModalOpen] = useState(false);
const [hideColumns, setHideColumns] = useState(widgetMetaData?.defaultValues?.has("hideColumns") && widgetMetaData?.defaultValues?.get("hideColumns"));
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [alertContent, setAlertContent] = useState(null as string);
@ -101,15 +105,36 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
/////////////////////////////
// load values from record //
/////////////////////////////
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
let columns: QQueryColumns = null;
let usingDefaultEmptyFilter = false;
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
if (!queryFilter)
{
queryFilter = new QQueryFilter();
usingDefaultEmptyFilter = true;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there is no queryFilter provided, see if there are default fields from which a query should be seeded //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
const defaultFilterFields = getDefaultFilterFieldNames(widgetMetaData);
if (defaultFilterFields?.length > 0)
{
defaultFilterFields.forEach((fieldName: string) =>
{
if (recordValues[fieldName])
{
queryFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [recordValues[fieldName]]));
}
});
queryFilter.addOrderBy(new QFilterOrderBy("id", false));
queryFilter = Object.assign({}, queryFilter);
}
else
{
usingDefaultEmptyFilter = true;
}
}
let columns: QQueryColumns = null;
if (recordValues["columnsJson"])
{
columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]);
@ -120,11 +145,20 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
//////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if (recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
////////////////////////////////////////////////////////////////////////////////////////
// if a default table name specified, use it, otherwise use it from the record values //
////////////////////////////////////////////////////////////////////////////////////////
let tableName = widgetMetaData?.defaultValues?.get("tableName");
if (!tableName && recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
{
tableName = recordValues["tableName"];
}
if (tableName)
{
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(recordValues["tableName"]);
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
const queryFilterForFrontend = Object.assign({}, queryFilter);
@ -132,7 +166,21 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
setFrontendQueryFilter(queryFilterForFrontend);
})();
}
}, [recordValues]);
}, [JSON.stringify(recordValues)]);
/*******************************************************************************
**
*******************************************************************************/
function getDefaultFilterFieldNames(widgetMetaData: QWidgetMetaData)
{
if (widgetMetaData?.defaultValues?.has("filterDefaultFieldNames"))
{
return (widgetMetaData.defaultValues.get("filterDefaultFieldNames").split(","));
}
return ([]);
}
/*******************************************************************************
@ -140,8 +188,27 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
*******************************************************************************/
function openEditor()
{
let missingRequiredFields = [] as string[];
getDefaultFilterFieldNames(widgetMetaData)?.forEach((fieldName: string) =>
{
if (!recordValues[fieldName])
{
missingRequiredFields.push(tableMetaData.fields.get(fieldName).label);
}
});
////////////////////////////////////////////////////////////////////
// display an alert and return if any required fields are missing //
////////////////////////////////////////////////////////////////////
if (missingRequiredFields.length > 0)
{
setAlertContent("The following fields must first be selected to add Additional Order Filters: '" + missingRequiredFields.join(", ") + "'");
return;
}
if (recordValues["tableName"])
{
setAlertContent(null);
setModalOpen(true);
}
}
@ -272,7 +339,14 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
const labelAdditionalElementsRight: JSX.Element[] = [];
if (isEditable)
{
labelAdditionalElementsRight.push(<HeaderLinkButtonComponent key="filterAndColumnsHeader" label="Edit Filters and Columns" onClickCallback={openEditor} disabled={tableMetaData == null} disabledTooltip={selectTableFirstTooltipTitle} />);
if (!hideColumns)
{
labelAdditionalElementsRight.push(<HeaderLinkButtonComponent key="filterAndColumnsHeader" label="Edit Filters and Columns" onClickCallback={openEditor} disabled={tableMetaData == null} disabledTooltip={selectTableFirstTooltipTitle} />);
}
else
{
labelAdditionalElementsRight.push(<HeaderLinkButtonComponent key="filterAndColumnsHeader" label="Edit Filters" onClickCallback={openEditor} disabled={tableMetaData == null} disabledTooltip={selectTableFirstTooltipTitle} />);
}
}
@ -306,34 +380,36 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
</Tooltip>
}
{
!isEditable && <Box color={colors.gray.main}>Your report has no filters.</Box>
!isEditable && <Box color={colors.gray.main}>No filters are configured.</Box>
}
</Box>
}
</Box>
<Box pt="1rem">
<h5>Columns</h5>
<Box display="flex" flexWrap="wrap" fontSize="1rem">
{
mayShowColumnsPreview() &&
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
}
{
!mayShowColumnsPreview() &&
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
{
isEditable &&
<Tooltip title={selectTableFirstTooltipTitle}>
<span><Button disabled={!recordValues["tableName"]} sx={unborderedButtonSX} onClick={openEditor}>+ Add Columns</Button></span>
</Tooltip>
}
{
!isEditable && <Box color={colors.gray.main}>Your report has no columns.</Box>
}
</Box>
}
{!hideColumns && (
<Box pt="1rem">
<h5>Columns</h5>
<Box display="flex" flexWrap="wrap" fontSize="1rem">
{
mayShowColumnsPreview() &&
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
}
{
!mayShowColumnsPreview() &&
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
{
isEditable &&
<Tooltip title={selectTableFirstTooltipTitle}>
<span><Button disabled={!recordValues["tableName"]} sx={unborderedButtonSX} onClick={openEditor}>+ Add Columns</Button></span>
</Tooltip>
}
{
!isEditable && <Box color={colors.gray.main}>No columns are selected.</Box>
}
</Box>
}
</Box>
</Box>
</Box>
)}
{
modalOpen &&
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>

View File

@ -39,9 +39,9 @@ import colors from "qqq/assets/theme/base/colors";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import {buttonSX, unborderedButtonSX} from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
import {PivotTableGroupByElement} from "qqq/components/widgets/misc/PivotTableGroupByElement";
import {PivotTableValueElement} from "qqq/components/widgets/misc/PivotTableValueElement";
import {buttonSX, unborderedButtonSX} from "qqq/components/widgets/misc/ReportSetupWidget";
import Widget, {HeaderToggleComponent} from "qqq/components/widgets/Widget";
import {PivotObjectKey, PivotTableDefinition, PivotTableFunction, pivotTableFunctionLabels, PivotTableGroupBy, PivotTableValue} from "qqq/models/misc/PivotTableDefinitionModels";
import QQueryColumns from "qqq/models/query/QQueryColumns";

View File

@ -35,8 +35,7 @@ import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJob
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
import Box from "@mui/material/Box";
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Step from "@mui/material/Step";
@ -48,12 +47,14 @@ import FormData from "form-data";
import {Form, Formik} from "formik";
import parse from "html-react-parser";
import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import {QCancelButton, QSubmitButton} from "qqq/components/buttons/DefaultButtons";
import QDynamicForm from "qqq/components/forms/DynamicForm";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import MDButton from "qqq/components/legacy/MDButton";
import MDProgress from "qqq/components/legacy/MDProgress";
import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
@ -87,6 +88,14 @@ const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5;
////////////////////////////////////////////////////////////////////////////
// define a function that we can make referenes to, which we'll overwrite //
// with formik's setFieldValue function, once we're inside formik. //
////////////////////////////////////////////////////////////////////////////
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
{
};
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
{
const processNameParam = useParams().processName;
@ -125,9 +134,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [showErrorDetail, setShowErrorDetail] = useState(false);
const [showFullHelpText, setShowFullHelpText] = useState(false);
const [renderedWidgets, setRenderedWidgets] = useState({} as {[step: string]: {[widgetName: string]: any}});
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext);
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for setting the processError state - call this function, which will also set the isUserFacingError state //
@ -229,15 +238,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setShowFullHelpText(!showFullHelpText);
};
const download = (processValues: {[key: string]: string}) =>
const download = (processValues: { [key: string]: string }) =>
{
let url;
let fileName = processValues.downloadFileName;
if(processValues.serverFilePath)
if (processValues.serverFilePath)
{
url = `/download/${encodeURIComponent(processValues.downloadFileName)}?filePath=${encodeURIComponent(processValues.serverFilePath)}`;
}
else if(processValues.storageTableName && processValues.storageReference)
else if (processValues.storageTableName && processValues.storageReference)
{
url = `/download/${encodeURIComponent(processValues.downloadFileName)}?storageTableName=${encodeURIComponent(processValues.storageTableName)}&storageReference=${encodeURIComponent(processValues.storageReference)}`;
}
@ -282,19 +291,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
*******************************************************************************/
function renderWidget(widgetName: string)
{
if(!renderedWidgets[activeStep.name])
if (!renderedWidgets[activeStep.name])
{
renderedWidgets[activeStep.name] = {};
setRenderedWidgets(renderedWidgets);
}
if(renderedWidgets[activeStep.name][widgetName])
if (renderedWidgets[activeStep.name][widgetName])
{
return renderedWidgets[activeStep.name][widgetName];
}
const widgetMetaData = qInstance.widgets.get(widgetName);
if(!widgetMetaData)
if (!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
@ -302,12 +311,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const queryStringParts: string[] = [];
for (let name in processValues)
{
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`)
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`);
}
const renderedWidget = (<Box m={-2}>
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} />
</Box>)
</Box>);
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
return renderedWidget;
}
@ -358,8 +367,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
</MDTypography>
<Box component="div" py={3}>
<Grid container justifyContent="flex-end" spacing={3}>
{isModal ? <QCancelButton onClickHandler={handleCancelClicked} disabled={false} label="Close" />
: !isWidget && <QCancelButton onClickHandler={handleCancelClicked} disabled={false} />
{isModal ? <QCancelButton onClickHandler={() => handleCancelClicked(true)} disabled={false} label="Close" />
: !isWidget && <QCancelButton onClickHandler={() => handleCancelClicked(true)} disabled={false} />
}
</Grid>
</Box>
@ -446,6 +455,13 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
});
}
/////////////////////////////////////
// screen(step)-level help content //
/////////////////////////////////////
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
return (
<>
{
@ -460,6 +476,13 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
</MDTypography>
}
{
showHelp &&
<Box fontSize={"0.875rem"} color={colors.blueGray.main} pb={2}>
{formattedHelpContent}
</Box>
}
{
//////////////////////////////////////////////////
// render all of the components for this screen //
@ -472,6 +495,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"];
}
//////////////////////////////////////////////////////////////////////////
// if the component specifies a sub-set of field names to include, then //
// edit the formData object to just include those. //
//////////////////////////////////////////////////////////////////////////
let formDataToUse = formData;
if (component.values && component.values.includeFieldNames)
{
formDataToUse = Object.assign({}, formData);
formDataToUse.formFields = {};
for (let i = 0; i < component.values.includeFieldNames.length; i++)
{
const fieldName = component.values.includeFieldNames[i];
formDataToUse.formFields[fieldName] = formData.formFields[fieldName];
}
}
return (
<div key={index}>
{
@ -567,9 +607,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
)
}
{
component.type === QComponentType.EDIT_FORM && (
<QDynamicForm formData={formData} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
)
component.type === QComponentType.EDIT_FORM &&
<>
{
component.values?.sectionLabel ?
<Box py={1.5}>
<Card sx={{scrollMarginTop: "20px"}}>
<MDTypography variant="h5" p={3} pl={2} pb={1}>
{component.values?.sectionLabel}
</MDTypography>
<Box pt={0} p={2}>
<QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
</Box>
</Card>
</Box> : <QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
}
</>
}
{
component.type === QComponentType.VIEW_FORM && step.viewFields && (
@ -960,7 +1013,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}
});
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, fullFieldList, tableMetaData.name, null, null);
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, fullFieldList, tableMetaData?.name, null, null);
setFormFields(newDynamicFormFields);
setValidationScheme(Yup.object().shape(newFormValidations));
@ -1026,6 +1079,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);
@ -1324,8 +1401,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
});
};
const handleCancelClicked = () =>
/*******************************************************************************
**
*******************************************************************************/
const handleCancelClicked = (isClose: boolean) =>
{
//////////////////////////////////////////////////////////////////
// unless this is a 'close', then tell backend we're cancelling //
//////////////////////////////////////////////////////////////////
if (!isClose)
{
Client.getInstance().processCancel(processName, processUUID);
}
if (isModal && closeModalHandler)
{
closeModalHandler(null, "cancelClicked");
@ -1338,6 +1427,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
navigate(path, {replace: true});
};
const mainCardStyles: any = {};
const formStyles: any = {};
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
@ -1385,89 +1475,98 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
>
{({
values, errors, touched, isSubmitting, setFieldValue,
}) => (
<Form style={formStyles} id={formId} autoComplete="off">
<Card sx={mainCardStyles}>
{
!isWidget && (
<Box mx={2} mt={-3}>
<Stepper activeStep={activeStepIndex} alternativeLabel>
{steps.map((step) => (
<Step key={step.name}>
<StepLabel>{step.label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
)
}
}) =>
{
///////////////////////////////////////////////////////////////////
// once we're in the formik form, use its setFieldValue function //
// over top of the default one we created globally //
///////////////////////////////////////////////////////////////////
formikSetFieldValueFunction = setFieldValue;
<Box p={3}>
<Box pb={isWidget ? 6 : "initial"}>
{/***************************************************************************
** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/}
{getDynamicStepContent(
activeStepIndex,
activeStep,
{
values,
touched,
formFields,
errors,
},
processError,
processValues,
recordConfig,
setFieldValue,
)}
{/********************************
** back &| next/submit buttons **
********************************/}
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
{true || activeStepIndex === 0 ? (
<Box />
) : (
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
)}
{processError || qJobRunning || !activeStep ? (
<Box />
) : (
<>
{formError && (
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
{formError}
</MDTypography>
)}
{
noMoreSteps && <QCancelButton
onClickHandler={handleCancelClicked}
label={isModal ? "Close" : "Return"}
iconName={isModal ? "cancel" : "arrow_back"}
disabled={isSubmitting} />
}
{
!noMoreSteps && (
<Box component="div" py={3}>
<Grid container justifyContent="flex-end" spacing={3}>
{
!isWidget && (
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
)
}
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
</Grid>
</Box>
)
}
</>
return (
<Form style={formStyles} id={formId} autoComplete="off">
<Card sx={mainCardStyles}>
{
!isWidget && (
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
<Stepper activeStep={activeStepIndex} alternativeLabel>
{steps.map((step) => (
<Step key={step.name}>
<StepLabel>{step.label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
)
}
<Box p={3}>
<Box pb={isWidget ? 6 : "initial"}>
{/***************************************************************************
** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/}
{getDynamicStepContent(
activeStepIndex,
activeStep,
{
values,
touched,
formFields,
errors,
},
processError,
processValues,
recordConfig,
setFieldValue,
)}
{/********************************
** back &| next/submit buttons **
********************************/}
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
{true || activeStepIndex === 0 ? (
<Box />
) : (
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
)}
{processError || qJobRunning || !activeStep ? (
<Box />
) : (
<>
{formError && (
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
{formError}
</MDTypography>
)}
{
noMoreSteps && <QCancelButton
onClickHandler={() => handleCancelClicked(true)}
label={isModal ? "Close" : "Return"}
iconName={isModal ? "cancel" : "arrow_back"}
disabled={isSubmitting} />
}
{
!noMoreSteps && (
<Box component="div" py={3}>
<Grid container justifyContent="flex-end" spacing={3}>
{
!isWidget && (
<QCancelButton onClickHandler={() => handleCancelClicked(false)} disabled={isSubmitting} />
)
}
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
</Grid>
</Box>
)
}
</>
)}
</Box>
</Box>
</Box>
</Box>
</Card>
</Form>
)}
</Card>
</Form>
);
}}
</Formik>
);

View File

@ -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))

View File

@ -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<string>
{
const visibleJoinTables = new Set<string>();
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<string, boolean>);
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<string> =>
{
const visibleJoinTables = new Set<string>();
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
{

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 (<div>Loading...</div>);
}
else if (record)
{
return (<RecordView table={table} record={record} />);
}
else if (errorMessage)
{
return (<BaseLayout>
<Box className="recordView">
<Grid container>
<Grid item xs={12}>
<Box mb={3}>
{
<Alert color="error" sx={{mb: 3}}>{errorMessage}</Alert>
}
</Box>
</Grid>
</Grid>
</Box>
</BaseLayout>);
}
}

View File

@ -421,6 +421,14 @@ input[type="search"]::-webkit-search-results-decoration
font-size: 2rem !important;
}
.dashboard-order-release-icon
{
font-size: 1.5rem !important;
position: relative;
top: -5px;
margin-right: 8px;
}
.dashboard-schedule-icon
{
font-size: 1.1rem !important;
@ -653,6 +661,11 @@ input[type="search"]::-webkit-search-results-decoration
min-height: unset !important;
}
.MuiDataGrid-columnHeaders
{
scrollbar-gutter: stable;
}
/* new style for toggle buttons */
.MuiToggleButtonGroup-root
{
@ -708,3 +721,66 @@ input[type="search"]::-webkit-search-results-decoration
color: white;
background-color: #0062FF !important;
}
.helpContentAlert
{
padding: 6px 16px;
font-size: 1rem;
font-weight: 300;
line-height: 1.6;
display: flex;
}
.helpContentAlert .MuiAlert-icon
{
display: flex;
margin-right: 12px;
padding: 7px 0;
font-size: 22px;
opacity: 0.9;
}
.helpContentAlert .MuiAlert-icon .material-icons-round
{
display: inline-block;
width: 1em;
height: 1em;
}
.helpContentAlert .MuiAlert-message
{
padding: 8px 0;
}
.helpContentAlert.success
{
background-color: rgb(240, 248, 241);
color: rgb(44, 76, 46);
}
.helpContentAlert.success .MuiAlert-icon .material-icons-round
{
color: #4CAF50;
}
.helpContentAlert.warning
{
background-color: rgb(254, 245, 234);
color: rgb(100, 65, 20);
}
.helpContentAlert.warning .MuiAlert-icon .material-icons-round
{
color: #fb8c00;
}
.helpContentAlert.error
{
background-color: rgb(254, 239, 238);
color: rgb(98, 41, 37);
}
.helpContentAlert.error .MuiAlert-icon .material-icons-round
{
color: #F44335;
}

View File

@ -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);

View File

@ -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.");
}

View File

@ -822,7 +822,7 @@
"reportSetupWidget": {
"name": "reportSetupWidget",
"label": "Filters and Columns",
"type": "reportSetup",
"type": "filterAndColumnsSetup",
"isCard": true,
"storeDropdownSelections": false,
"showReloadButton": true,