Merged feature/CE-938-order-release-automation into integration/sprint-43

This commit is contained in:
2024-05-29 10:48:11 -05:00
9 changed files with 223 additions and 134 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
},
},
});

View File

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

View File

@ -44,9 +44,9 @@ import MDTypography from "qqq/components/legacy/MDTypography";
import HelpContent from "qqq/components/misc/HelpContent"; import HelpContent from "qqq/components/misc/HelpContent";
import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget"; 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 PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"; 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 {FieldRule, FieldRuleAction, FieldRuleTrigger} from "qqq/models/fields/FieldRules";
import HtmlUtils from "qqq/utils/HtmlUtils"; import HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
@ -88,7 +88,7 @@ EntityForm.defaultProps = {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void => let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
{ {
} };
function EntityForm(props: Props): JSX.Element function EntityForm(props: Props): JSX.Element
{ {
@ -119,11 +119,12 @@ function EntityForm(props: Props): JSX.Element
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [showEditChildForm, setShowEditChildForm] = useState(null as any); const [showEditChildForm, setShowEditChildForm] = useState(null as any);
const [modalDataChangedCounter, setModalDataChangedCount] = useState(0);
const [notAllowedError, setNotAllowedError] = useState(null as string); const [notAllowedError, setNotAllowedError] = useState(null as string);
const [formValuesJSON, setFormValuesJSON] = useState(""); 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); const {pageHeader, setPageHeader} = useContext(QContext);
@ -282,6 +283,8 @@ function EntityForm(props: Props): JSX.Element
setRenderedWidgetSections(newRenderedWidgetSections); setRenderedWidgetSections(newRenderedWidgetSections);
forceUpdate(); forceUpdate();
setModalDataChangedCount(modalDataChangedCounter + 1);
setShowEditChildForm(null); setShowEditChildForm(null);
} }
@ -291,7 +294,7 @@ function EntityForm(props: Props): JSX.Element
*******************************************************************************/ *******************************************************************************/
useEffect(() => useEffect(() =>
{ {
const newRenderedWidgetSections: {[name: string]: JSX.Element} = {}; const newRenderedWidgetSections: { [name: string]: JSX.Element } = {};
for (let widgetName in renderedWidgetSections) for (let widgetName in renderedWidgetSections)
{ {
const widgetMetaData = metaData.widgets.get(widgetName); 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 ** 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 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) for (let key in values)
{ {
@ -370,13 +372,13 @@ function EntityForm(props: Props): JSX.Element
*******************************************************************************/ *******************************************************************************/
function getWidgetSection(widgetMetaData: QWidgetMetaData, widgetData: any): JSX.Element function getWidgetSection(widgetMetaData: QWidgetMetaData, widgetData: any): JSX.Element
{ {
if(widgetMetaData.type == "childRecordList") if (widgetMetaData.type == "childRecordList")
{ {
widgetData.viewAllLink = null; widgetData.viewAllLink = null;
widgetMetaData.showExportButton = false; widgetMetaData.showExportButton = false;
return <RecordGridWidget return <RecordGridWidget
key={new Date().getTime()} // added so that editing values actually re-renders... key={`${formValues["tableName"]}-${modalDataChangedCounter}`}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
data={widgetData} data={widgetData}
disableRowClick 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... key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
isEditable={true} isEditable={true}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
recordValues={formValues} recordValues={formValues}
onSaveCallback={setFormFieldValuesFromWidget} onSaveCallback={setFormFieldValuesFromWidget}
/> />;
} }
if(widgetMetaData.type == "pivotTableSetup") if (widgetMetaData.type == "pivotTableSetup")
{ {
return <PivotTableSetupWidget return <PivotTableSetupWidget
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders... 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} widgetMetaData={widgetMetaData}
recordValues={formValues} recordValues={formValues}
onSaveCallback={setFormFieldValuesFromWidget} onSaveCallback={setFormFieldValuesFromWidget}
/> />;
} }
if(widgetMetaData.type == "dynamicForm") if (widgetMetaData.type == "dynamicForm")
{ {
return <DynamicFormWidget return <DynamicFormWidget
key={formValues["savedReportId"]} // todo - pull this from the metaData (could do so above too...) 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} recordValues={formValues}
record={record} record={record}
onSaveCallback={setFormFieldValuesFromWidget} 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) function setupFieldRules(tableMetaData: QTableMetaData)
{ {
const mdbMetaData = tableMetaData?.supplementalTableMetaData?.get("materialDashboard"); const mdbMetaData = tableMetaData?.supplementalTableMetaData?.get("materialDashboard");
if(!mdbMetaData) if (!mdbMetaData)
{ {
return; return;
} }
if(mdbMetaData.fieldRules) if (mdbMetaData.fieldRules)
{ {
const newFieldRules: FieldRule[] = []; const newFieldRules: FieldRule[] = [];
for (let i = 0; i < mdbMetaData.fieldRules.length; i++) 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 tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
{ {
const widget = metaData.widgets.get(section.widgetName); const widget = metaData?.widgets.get(section.widgetName);
if(widget) if (widget)
{ {
if(widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName")) if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
{ {
return (true); return (true);
} }
if(widget.type == "reportSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm") if (widget.type == "filterAndColumnsSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm")
{ {
return (true); return (true);
} }
@ -680,7 +691,7 @@ function EntityForm(props: Props): JSX.Element
} }
const hasFields = section.fieldNames && section.fieldNames.length > 0; const hasFields = section.fieldNames && section.fieldNames.length > 0;
if(hasFields) if (hasFields)
{ {
for (let j = 0; j < section.fieldNames.length; j++) for (let j = 0; j < section.fieldNames.length; j++)
{ {
@ -719,7 +730,7 @@ function EntityForm(props: Props): JSX.Element
} }
else 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)); const widgetData = await qController.widget(widgetMetaData.name, makeQueryStringWithIdAndObject(tableMetaData, defaultValues));
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData); 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[] = []; 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) 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 widgetData = await qController.widget(widgetName, makeQueryStringWithIdAndObject(tableMetaData, additionalQueryParamsForWidget));
const widgetMetaData = metaData.widgets.get(widgetName); 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). ** 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) 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) switch (fieldRule.action)
{ {
@ -1058,7 +1069,7 @@ function EntityForm(props: Props): JSX.Element
valueChangesToMake[fieldRule.targetField] = null; valueChangesToMake[fieldRule.targetField] = null;
break; break;
case FieldRuleAction.RELOAD_WIDGET: case FieldRuleAction.RELOAD_WIDGET:
const additionalQueryParamsForWidget: {[key: string]: any} = {}; const additionalQueryParamsForWidget: { [key: string]: any } = {};
additionalQueryParamsForWidget[fieldRule.sourceField] = newValue; additionalQueryParamsForWidget[fieldRule.sourceField] = newValue;
reloadWidget(fieldRule.targetWidget, additionalQueryParamsForWidget); reloadWidget(fieldRule.targetWidget, additionalQueryParamsForWidget);
} }
@ -1148,21 +1159,21 @@ function EntityForm(props: Props): JSX.Element
///////////////////////////////////////////////// /////////////////////////////////////////////////
// if we have values from formik, look at them // // 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 // // use stringified values as cheap/easy way to see if any are changed //
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
const newFormValuesJSON = JSON.stringify(values); 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), // // if the form is dirty (e.g., we're not doing the initial load), //
// then process rules for any changed fields // // then process rules for any changed fields //
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
if(dirty) if (dirty)
{ {
for (let fieldName in values) for (let fieldName in values)
{ {
@ -1194,7 +1205,7 @@ function EntityForm(props: Props): JSX.Element
setFieldValue(fieldName, valueChangesToMake[fieldName], false); setFieldValue(fieldName, valueChangesToMake[fieldName], false);
} }
setFormValues(formValues) setFormValues(formValues);
setFormValuesJSON(JSON.stringify(values)); setFormValuesJSON(JSON.stringify(values));
} }
} }

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 {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; 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 Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
@ -94,12 +95,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext); const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext);
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
// this component is used by <RecordQuery> - but that component has different usages - // // 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 // // 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 // // under the FilterAndColumnsSetupWidget). So, there are some behaviors we only want when //
// on the full-fledged query screen, such as changing the URL with saved view ids. // // we're on the full-fledged query screen, such as changing the URL with saved view ids. //
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
const isQueryScreen = queryScreenUsage == "queryScreen"; const isQueryScreen = queryScreenUsage == "queryScreen";
const openSavedViewsMenu = (event: any) => setSavedViewsMenu(event.currentTarget); 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 DividerWidget from "qqq/components/widgets/misc/Divider";
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget"; import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget"; 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 PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart"; import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget"; 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 ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
import StepperCard from "qqq/components/widgets/misc/StepperCard"; import StepperCard from "qqq/components/widgets/misc/StepperCard";
import USMapWidget from "qqq/components/widgets/misc/USMapWidget"; 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 && 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; 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 {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Collapse} from "@mui/material"; import {Alert, Collapse} from "@mui/material";
import Box from "@mui/material/Box"; 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 FilterUtils from "qqq/utils/qqq/FilterUtils";
import React, {useContext, useEffect, useRef, useState} from "react"; import React, {useContext, useEffect, useRef, useState} from "react";
interface ReportSetupWidgetProps interface FilterAndColumnsSetupWidgetProps
{ {
isEditable: boolean; isEditable: boolean;
widgetMetaData: QWidgetMetaData; widgetMetaData: QWidgetMetaData;
@ -50,7 +53,7 @@ interface ReportSetupWidgetProps
onSaveCallback?: (values: { [name: string]: any }) => void; onSaveCallback?: (values: { [name: string]: any }) => void;
} }
ReportSetupWidget.defaultProps = { FilterAndColumnsSetupWidget.defaultProps = {
onSaveCallback: null onSaveCallback: null
}; };
@ -80,9 +83,10 @@ const qController = Client.getInstance();
/******************************************************************************* /*******************************************************************************
** Component for editing the main setup of a report - that is: filter & columns ** 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 [modalOpen, setModalOpen] = useState(false);
const [hideColumns, setHideColumns] = useState(widgetMetaData?.defaultValues?.has("hideColumns") && widgetMetaData?.defaultValues?.get("hideColumns"));
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [alertContent, setAlertContent] = useState(null as string); const [alertContent, setAlertContent] = useState(null as string);
@ -101,15 +105,36 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
///////////////////////////// /////////////////////////////
// load values from record // // load values from record //
///////////////////////////// /////////////////////////////
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter; let columns: QQueryColumns = null;
let usingDefaultEmptyFilter = false; let usingDefaultEmptyFilter = false;
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
if (!queryFilter) if (!queryFilter)
{ {
queryFilter = new QQueryFilter(); 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"]) if (recordValues["columnsJson"])
{ {
columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]); columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]);
@ -120,11 +145,20 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
useEffect(() => 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 () => (async () =>
{ {
const tableMetaData = await qController.loadTableMetaData(recordValues["tableName"]); const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData); setTableMetaData(tableMetaData);
const queryFilterForFrontend = Object.assign({}, queryFilter); const queryFilterForFrontend = Object.assign({}, queryFilter);
@ -132,7 +166,21 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
setFrontendQueryFilter(queryFilterForFrontend); 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() 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"]) if (recordValues["tableName"])
{ {
setAlertContent(null);
setModalOpen(true); setModalOpen(true);
} }
} }
@ -272,7 +339,14 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
const labelAdditionalElementsRight: JSX.Element[] = []; const labelAdditionalElementsRight: JSX.Element[] = [];
if (isEditable) 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} />);
}
} }
@ -311,29 +385,31 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
</Box> </Box>
} }
</Box> </Box>
<Box pt="1rem"> {!hideColumns && (
<h5>Columns</h5> <Box pt="1rem">
<Box display="flex" flexWrap="wrap" fontSize="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() &&
} 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"}> !mayShowColumnsPreview() &&
{ <Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
isEditable && {
<Tooltip title={selectTableFirstTooltipTitle}> isEditable &&
<span><Button disabled={!recordValues["tableName"]} sx={unborderedButtonSX} onClick={openEditor}>+ Add Columns</Button></span> <Tooltip title={selectTableFirstTooltipTitle}>
</Tooltip> <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> {
} !isEditable && <Box color={colors.gray.main}>Your report has no columns.</Box>
</Box> }
} </Box>
}
</Box>
</Box> </Box>
</Box> )}
{ {
modalOpen && modalOpen &&
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}> <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 {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete"; import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent"; 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 {PivotTableGroupByElement} from "qqq/components/widgets/misc/PivotTableGroupByElement";
import {PivotTableValueElement} from "qqq/components/widgets/misc/PivotTableValueElement"; 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 Widget, {HeaderToggleComponent} from "qqq/components/widgets/Widget";
import {PivotObjectKey, PivotTableDefinition, PivotTableFunction, pivotTableFunctionLabels, PivotTableGroupBy, PivotTableValue} from "qqq/models/misc/PivotTableDefinitionModels"; import {PivotObjectKey, PivotTableDefinition, PivotTableFunction, pivotTableFunctionLabels, PivotTableGroupBy, PivotTableValue} from "qqq/models/misc/PivotTableDefinitionModels";
import QQueryColumns from "qqq/models/query/QQueryColumns"; import QQueryColumns from "qqq/models/query/QQueryColumns";

View File

@ -94,7 +94,7 @@ const BACKOFF_AMOUNT = 1.5;
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void => let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
{ {
} };
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
{ {
@ -134,7 +134,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [showErrorDetail, setShowErrorDetail] = useState(false); const [showErrorDetail, setShowErrorDetail] = useState(false);
const [showFullHelpText, setShowFullHelpText] = 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, helpHelpActive} = useContext(QContext); const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
@ -238,15 +238,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setShowFullHelpText(!showFullHelpText); setShowFullHelpText(!showFullHelpText);
}; };
const download = (processValues: {[key: string]: string}) => const download = (processValues: { [key: string]: string }) =>
{ {
let url; let url;
let fileName = processValues.downloadFileName; let fileName = processValues.downloadFileName;
if(processValues.serverFilePath) if (processValues.serverFilePath)
{ {
url = `/download/${encodeURIComponent(processValues.downloadFileName)}?filePath=${encodeURIComponent(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)}`; url = `/download/${encodeURIComponent(processValues.downloadFileName)}?storageTableName=${encodeURIComponent(processValues.storageTableName)}&storageReference=${encodeURIComponent(processValues.storageReference)}`;
} }
@ -291,19 +291,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
*******************************************************************************/ *******************************************************************************/
function renderWidget(widgetName: string) function renderWidget(widgetName: string)
{ {
if(!renderedWidgets[activeStep.name]) if (!renderedWidgets[activeStep.name])
{ {
renderedWidgets[activeStep.name] = {}; renderedWidgets[activeStep.name] = {};
setRenderedWidgets(renderedWidgets); setRenderedWidgets(renderedWidgets);
} }
if(renderedWidgets[activeStep.name][widgetName]) if (renderedWidgets[activeStep.name][widgetName])
{ {
return renderedWidgets[activeStep.name][widgetName]; return renderedWidgets[activeStep.name][widgetName];
} }
const widgetMetaData = qInstance.widgets.get(widgetName); const widgetMetaData = qInstance.widgets.get(widgetName);
if(!widgetMetaData) if (!widgetMetaData)
{ {
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>); return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
} }
@ -311,12 +311,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const queryStringParts: string[] = []; const queryStringParts: string[] = [];
for (let name in processValues) for (let name in processValues)
{ {
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`) queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`);
} }
const renderedWidget = (<Box m={-2}> const renderedWidget = (<Box m={-2}>
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} /> <DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} />
</Box>) </Box>);
renderedWidgets[activeStep.name][widgetName] = renderedWidget; renderedWidgets[activeStep.name][widgetName] = renderedWidget;
return renderedWidget; return renderedWidget;
} }
@ -367,8 +367,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
</MDTypography> </MDTypography>
<Box component="div" py={3}> <Box component="div" py={3}>
<Grid container justifyContent="flex-end" spacing={3}> <Grid container justifyContent="flex-end" spacing={3}>
{isModal ? <QCancelButton onClickHandler={handleCancelClicked} disabled={false} label="Close" /> {isModal ? <QCancelButton onClickHandler={() => handleCancelClicked(true)} disabled={false} label="Close" />
: !isWidget && <QCancelButton onClickHandler={handleCancelClicked} disabled={false} /> : !isWidget && <QCancelButton onClickHandler={() => handleCancelClicked(true)} disabled={false} />
} }
</Grid> </Grid>
</Box> </Box>
@ -500,7 +500,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// edit the formData object to just include those. // // edit the formData object to just include those. //
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
let formDataToUse = formData; let formDataToUse = formData;
if(component.values && component.values.includeFieldNames) if (component.values && component.values.includeFieldNames)
{ {
formDataToUse = Object.assign({}, formData); formDataToUse = Object.assign({}, formData);
@ -608,21 +608,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
} }
{ {
component.type === QComponentType.EDIT_FORM && component.type === QComponentType.EDIT_FORM &&
<> <>
{ {
component.values?.sectionLabel ? component.values?.sectionLabel ?
<Box py={1.5}> <Box py={1.5}>
<Card sx={{scrollMarginTop: "20px"}}> <Card sx={{scrollMarginTop: "20px"}}>
<MDTypography variant="h5" p={3} pl={2} pb={1}> <MDTypography variant="h5" p={3} pl={2} pb={1}>
{component.values?.sectionLabel} {component.values?.sectionLabel}
</MDTypography> </MDTypography>
<Box pt={0} p={2}> <Box pt={0} p={2}>
<QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} /> <QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
</Box> </Box>
</Card> </Card>
</Box> : <QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} /> </Box> : <QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
} }
</> </>
} }
{ {
component.type === QComponentType.VIEW_FORM && step.viewFields && ( component.type === QComponentType.VIEW_FORM && step.viewFields && (
@ -1079,14 +1079,14 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setProcessValues(qJobComplete.values); setProcessValues(qJobComplete.values);
setQJobRunning(null); setQJobRunning(null);
if(formikSetFieldValueFunction) if (formikSetFieldValueFunction)
{ {
////////////////////////////////// //////////////////////////////////
// reset field values in formik // // reset field values in formik //
////////////////////////////////// //////////////////////////////////
for (let key in qJobComplete.values) for (let key in qJobComplete.values)
{ {
if(Object.hasOwn(formFields, key)) if (Object.hasOwn(formFields, key))
{ {
console.log(`(re)setting form field [${key}] to [${qJobComplete.values[key]}]`); console.log(`(re)setting form field [${key}] to [${qJobComplete.values[key]}]`);
formikSetFieldValueFunction(key, qJobComplete.values[key]); formikSetFieldValueFunction(key, qJobComplete.values[key]);
@ -1098,7 +1098,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) // // 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; const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
if(updatedFrontendStepList) if (updatedFrontendStepList)
{ {
setSteps(updatedFrontendStepList); setSteps(updatedFrontendStepList);
} }
@ -1401,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) if (isModal && closeModalHandler)
{ {
closeModalHandler(null, "cancelClicked"); closeModalHandler(null, "cancelClicked");
@ -1415,6 +1427,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
navigate(path, {replace: true}); navigate(path, {replace: true});
}; };
const mainCardStyles: any = {}; const mainCardStyles: any = {};
const formStyles: any = {}; const formStyles: any = {};
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`; mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
@ -1490,8 +1503,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
<Box p={3}> <Box p={3}>
<Box pb={isWidget ? 6 : "initial"}> <Box pb={isWidget ? 6 : "initial"}>
{/*************************************************************************** {/***************************************************************************
** step content - e.g., the appropriate form or other screen for the step ** ** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/} ***************************************************************************/}
{getDynamicStepContent( {getDynamicStepContent(
activeStepIndex, activeStepIndex,
activeStep, activeStep,
@ -1507,8 +1520,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setFieldValue, setFieldValue,
)} )}
{/******************************** {/********************************
** back &| next/submit buttons ** ** 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"}> <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 ? ( {true || activeStepIndex === 0 ? (
<Box /> <Box />
@ -1526,7 +1539,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
)} )}
{ {
noMoreSteps && <QCancelButton noMoreSteps && <QCancelButton
onClickHandler={handleCancelClicked} onClickHandler={() => handleCancelClicked(true)}
label={isModal ? "Close" : "Return"} label={isModal ? "Close" : "Return"}
iconName={isModal ? "cancel" : "arrow_back"} iconName={isModal ? "cancel" : "arrow_back"}
disabled={isSubmitting} /> disabled={isSubmitting} />
@ -1537,7 +1550,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
<Grid container justifyContent="flex-end" spacing={3}> <Grid container justifyContent="flex-end" spacing={3}>
{ {
!isWidget && ( !isWidget && (
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} /> <QCancelButton onClickHandler={() => handleCancelClicked(false)} disabled={isSubmitting} />
) )
} }
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} /> <QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
@ -1552,7 +1565,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
</Box> </Box>
</Card> </Card>
</Form> </Form>
) );
}} }}
</Formik> </Formik>
); );

View File

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