Compare commits

...
10 Commits
Author SHA1 Message Date
Darin Kelkhoff a155097d89 chore: update qqq version to feature build 2025-12-16 22:16:02 -06:00
Darin Kelkhoff 6594214084 feat(formAdjusters): Add ability to disable a form in a form adjuster.
e.g., based on values in a record, make it non-editable.

a future enhancement (which doesn't replace this, but just makes it even better) adds similar functionality on the view screen, to disable even getting to the edit screen.
2025-12-16 16:34:28 -06:00
Darin Kelkhoff 8d99a11f7e feat(fieldAdornmentWidgets): Update to use a virtual field for the view screen, in an alternate section - to display the json of the filter (as the test expected), since now view screen tries to render the widget (but doesn't quite have the right data for it). this could improve, but this proves the point, and gets us passing again 2025-12-16 16:24:10 -06:00
Darin Kelkhoff e89ce56864 feat(fieldAdornmentWidgets): Switch to use DashboardWidgets to (in theory) support any widget type... with new omitPaddingOnWidget param, to pass through to the <Widget> 2025-12-16 16:24:10 -06:00
Darin Kelkhoff 914aac19b0 feat(virtualFieldsAndAlternativeSections): Support for alternative sections in a table, virtual fields, and AdornmentType.WIDGET (to render FieldValueAsWidget) 2025-12-16 16:22:09 -06:00
Darin Kelkhoff 7ef04f0afa feat(fieldAdornmentWidgets): Initial checkin 2025-12-16 16:22:09 -06:00
Darin Kelkhoff 352fa68da5 fix(widgetBlocks): Add IconBlock 2025-12-16 16:22:09 -06:00
Darin Kelkhoff dafc96b0fa fix(widgetBlocks): avoid NPE on missing text 2025-12-16 16:22:09 -06:00
Darin Kelkhoff c524052391 feat(virtualFields): increase qqq-frontend-core patch version 2025-12-16 16:22:01 -06:00
Darin Kelkhoff b018d7fe1e refactor: Fix name of the exported function in this file 2025-12-16 16:20:27 -06:00
16 changed files with 23686 additions and 22995 deletions

No files matched your search

+22935 -22851
View File
File diff suppressed because it is too large. Load diff
+116 -115
View File
@@ -1,117 +1,118 @@
{
"name": "qqq-frontend-material-dashboard",
"version": "0.26.0-SNAPSHOT",
"description": "QQQ Default Dashboard",
"dependencies": {
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
"@mui/system": "5.11.1",
"@mui/x-data-grid": "5.17.23",
"@mui/x-data-grid-pro": "5.17.23",
"@mui/x-date-pickers": "7.1.1",
"@mui/x-license-pro": "5.12.3",
"@qrunio/qqq-frontend-core": "1.3.7-SNAPSHOT",
"@react-jvectormap/core": "1.0.1",
"@react-jvectormap/unitedstates": "1.0.1",
"@react-oauth/google": "0.2.8",
"@types/prop-types": "^15.7.5",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3",
"chart.js": "3.4.1",
"chroma-js": "2.4.2",
"cmdk": "0.2.0",
"datejs": "1.0.0-rc3",
"dayjs": "1.11.10",
"downshift": "3.2.10",
"faker": "5.5.3",
"form-data": "4.0.0",
"formik": "2.2.9",
"html-react-parser": "1.4.8",
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6",
"jwt-decode": "3.1.2",
"lodash": "4.17.21",
"oidc-client-ts": "2.4.1",
"rapidoc": "9.3.4",
"react": "18.0.0",
"react-ace": "10.1.0",
"react-chartjs-2": "3.0.4",
"react-cookie": "4.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.0.0",
"react-dropzone": "14.3.5",
"react-ga4": "2.1.0",
"react-github-btn": "1.2.1",
"react-google-drive-picker": "^1.2.0",
"react-markdown": "9.0.1",
"react-oidc-context": "2.3.1",
"react-router-dom": "6.2.1",
"react-router-hash-link": "2.4.3",
"react-table": "7.7.0",
"sass": "1.63.4",
"ts-md5": "1.2.11",
"yup": "0.32.11"
},
"scripts": {
"build": "react-scripts build",
"clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
"npm-install": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
"test": "react-scripts test"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@svgr/webpack": "6.5.1",
"@types/chroma-js": "2.1.3",
"@types/faker": "5.5.9",
"@types/react-table": "7.7.9",
"@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
"eslint": "8.8.0",
"eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-react": "7.28.0",
"react-scripts": "5.0.1",
"typescript": "4.9.4"
},
"overrides": {
"@svgr/webpack": "$@svgr/webpack"
},
"main": "qqq-frontend-material-dashboard.js",
"module": "lib/qqq-frontend-material-dashboard.js",
"files": [
"lib",
"package.json"
]
"name": "qqq-frontend-material-dashboard",
"version": "0.26.0-SNAPSHOT",
"description": "QQQ Default Dashboard",
"dependencies": {
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
"@mui/system": "5.11.1",
"@mui/x-data-grid": "5.17.23",
"@mui/x-data-grid-pro": "5.17.23",
"@mui/x-date-pickers": "7.1.1",
"@mui/x-license-pro": "5.12.3",
"@qrunio/qqq-frontend-core": "1.3.8-SNAPSHOT",
"@react-jvectormap/core": "1.0.1",
"@react-jvectormap/unitedstates": "1.0.1",
"@react-oauth/google": "0.2.8",
"@types/prop-types": "^15.7.5",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3",
"chart.js": "3.4.1",
"chroma-js": "2.4.2",
"cmdk": "0.2.0",
"datejs": "1.0.0-rc3",
"dayjs": "1.11.10",
"downshift": "3.2.10",
"faker": "5.5.3",
"form-data": "4.0.0",
"formik": "2.2.9",
"html-react-parser": "1.4.8",
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6",
"jwt-decode": "3.1.2",
"lodash": "4.17.21",
"oidc-client-ts": "2.4.1",
"rapidoc": "9.3.4",
"react": "18.0.0",
"react-ace": "10.1.0",
"react-chartjs-2": "3.0.4",
"react-cookie": "4.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.0.0",
"react-dropzone": "14.3.5",
"react-ga4": "2.1.0",
"react-github-btn": "1.2.1",
"react-google-drive-picker": "^1.2.0",
"react-markdown": "9.0.1",
"react-oidc-context": "2.3.1",
"react-router-dom": "6.2.1",
"react-router-hash-link": "2.4.3",
"react-table": "7.7.0",
"sass": "1.63.4",
"ts-md5": "1.2.11",
"yup": "0.32.11"
},
"scripts": {
"build": "react-scripts build",
"build-and-publish": "react-scripts build && rm -rvf src/main/resources/material-dashboard; mkdir -p src/main/resources/material-dashboard; cp -rv build/* src/main/resources/material-dashboard",
"clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
"npm-install": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
"test": "react-scripts test"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@svgr/webpack": "6.5.1",
"@types/chroma-js": "2.1.3",
"@types/faker": "5.5.9",
"@types/react-table": "7.7.9",
"@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
"eslint": "8.8.0",
"eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-react": "7.28.0",
"react-scripts": "5.0.1",
"typescript": "4.9.4"
},
"overrides": {
"@svgr/webpack": "$@svgr/webpack"
},
"main": "qqq-frontend-material-dashboard.js",
"module": "lib/qqq-frontend-material-dashboard.js",
"files": [
"lib",
"package.json"
]
}
+1 -1
View File
@@ -57,7 +57,7 @@
<properties>
<revision>0.27.0-SNAPSHOT</revision>
<qqq.version>0.31.0-20251021.000349-20</qqq.version>
<qqq.version>0.31.0-integration-without-spa-change-aac53e76f-20251216.225537-1</qqq.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>17</maven.compiler.source>
@@ -40,6 +40,8 @@ public class FormAdjusterOutput
private Set<String> fieldsToClear = null;
private Map<String, QFieldSection> updatedSectionMetaData = null;
private Boolean isFormDisabled = false;
private String formDisabledMessage = null;
/*******************************************************************************
@@ -202,4 +204,81 @@ public class FormAdjusterOutput
return (this);
}
/*******************************************************************************
* Getter for isFormDisabled
* @see #withIsFormDisabled(Boolean)
*******************************************************************************/
public Boolean getIsFormDisabled()
{
return (this.isFormDisabled);
}
/*******************************************************************************
* Setter for isFormDisabled
* @see #withIsFormDisabled(Boolean)
*******************************************************************************/
public void setIsFormDisabled(Boolean isFormDisabled)
{
this.isFormDisabled = isFormDisabled;
}
/*******************************************************************************
* Fluent setter for isFormDisabled
*
* @param isFormDisabled
* boolean, defaults to false. If set to true, then the frontend should disable
* the form, fully blocking edits.
* @return this
*******************************************************************************/
public FormAdjusterOutput withIsFormDisabled(Boolean isFormDisabled)
{
this.isFormDisabled = isFormDisabled;
return (this);
}
/*******************************************************************************
* Getter for formDisabledMessage
* @see #withFormDisabledMessage(String)
*******************************************************************************/
public String getFormDisabledMessage()
{
return (this.formDisabledMessage);
}
/*******************************************************************************
* Setter for formDisabledMessage
* @see #withFormDisabledMessage(String)
*******************************************************************************/
public void setFormDisabledMessage(String formDisabledMessage)
{
this.formDisabledMessage = formDisabledMessage;
}
/*******************************************************************************
* Fluent setter for formDisabledMessage
*
* @param formDisabledMessage
* Message the frontend should display to the user when the form is disabled
* (see withIsFormDisabled()). If this message isn't set, then the frontend
* will display a generic message.
* @return this
*******************************************************************************/
public FormAdjusterOutput withFormDisabledMessage(String formDisabledMessage)
{
this.formDisabledMessage = formDisabledMessage;
return (this);
}
}
+37 -3
View File
@@ -26,6 +26,7 @@ import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import Modal from "@mui/material/Modal";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import {Capability} from "@qrunio/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@qrunio/qqq-frontend-core/lib/model/metaData/QFieldType";
@@ -117,6 +118,10 @@ function EntityForm(props: Props): JSX.Element
const [alertContent, setAlertContent] = useState("");
const [warningContent, setWarningContent] = useState("");
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
const [saveDisabledTooltip, setSaveDisabledTooltip] = useState(null as string);
const [mayCloseAlert, setMayCloseAlert] = useState(true);
const [mayCloseWarning, setMayCloseWarning] = useState(true);
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
@@ -772,6 +777,31 @@ function EntityForm(props: Props): JSX.Element
{
updateSections(table, response.updatedSectionMetaData, true);
}
///////////////////////////////////////////
// disable all the things if so directed //
///////////////////////////////////////////
if (response?.isFormDisabled)
{
let message = response.formDisabledMessage;
if(!message)
{
if((props.id && props.isCopy) || !props.id)
{
message = "You are not allowed to create a new record.";
}
else
{
message = "You are not allowed to edit this record.";
}
}
setAlertContent(message);
setSaveDisabledTooltip(message);
table.fields.forEach((field) => field.isEditable = false);
setIsSaveDisabled(true);
setMayCloseAlert(false);
}
};
@@ -1621,12 +1651,12 @@ function EntityForm(props: Props): JSX.Element
<Grid item xs={12}>
{alertContent ? (
<Box mb={3}>
<Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert>
<Alert severity="error" onClose={mayCloseAlert ? () => setAlertContent(null) : undefined}>{alertContent}</Alert>
</Box>
) : ("")}
{warningContent ? (
<Box mb={3}>
<Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert>
<Alert severity="warning" onClose={mayCloseWarning ? () => setWarningContent(null) : undefined}>{warningContent}</Alert>
</Box>
) : ("")}
</Grid>
@@ -1753,7 +1783,11 @@ function EntityForm(props: Props): JSX.Element
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<QSaveButton disabled={isSubmitting} label={props.saveButtonLabel} iconName={props.saveButtonIcon} />
<Tooltip title={saveDisabledTooltip}>
<Box>
<QSaveButton disabled={isSubmitting || isSaveDisabled} label={props.saveButtonLabel} iconName={props.saveButtonIcon} />
</Box>
</Tooltip>
</Grid>
</Box>
}
@@ -75,6 +75,7 @@ interface Props
entityPrimaryKey?: string;
record?: QRecord;
omitWrappingGridContainer: boolean;
omitPaddingOnWidget: boolean;
areChildren?: boolean;
childUrlParams?: string;
parentWidgetMetaData?: QWidgetMetaData;
@@ -90,6 +91,7 @@ DashboardWidgets.defaultProps = {
tableName: null,
entityPrimaryKey: null,
omitWrappingGridContainer: false,
omitPaddingOnWidget: false,
areChildren: false,
childUrlParams: "",
parentWidgetMetaData: null,
@@ -100,7 +102,7 @@ DashboardWidgets.defaultProps = {
screen: "dashboard",
};
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels, actionCallback, initialWidgetDataList, values, screen}: Props): JSX.Element
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, omitPaddingOnWidget, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels, actionCallback, initialWidgetDataList, values, screen}: Props): JSX.Element
{
const [widgetData, setWidgetData] = useState(initialWidgetDataList == null ? [] as any[] : initialWidgetDataList);
const [errorsLoading, setErrorsLoading] = useState([] as any[]);
@@ -468,12 +470,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
message = "Error: " + e.message;
}
return (<Widget widgetMetaData={widgetMetaData}>
return (<Widget widgetMetaData={widgetMetaData} omitPadding={omitPaddingOnWidget}>
<Alert severity="error">{message}</Alert>
</Widget>);
}
return (<Widget widgetMetaData={widgetMetaData}>
return (<Widget widgetMetaData={widgetMetaData} omitPadding={omitPaddingOnWidget}>
{
skeleton ? skeleton : <Skeleton></Skeleton>
}
@@ -596,6 +598,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
isChild={areChildren}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<StackedBarChart data={widgetData[i]?.chartData} chartSubheaderData={widgetData[i]?.chartSubheaderData} />
</Widget>
@@ -610,6 +613,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
showReloadControl={false}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
@@ -625,6 +629,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
reloadWidgetCallback={(data) => reloadWidget(i, data)}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
<Box padding="1rem" sx={{width: "100%"}}>
@@ -642,6 +647,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
widgetData={widgetData[i]}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<Box>
<MDTypography component="div" variant="button" color="text" fontWeight="light">
@@ -675,6 +681,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
reloadWidgetCallback={(data) => reloadWidget(i, data)}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<StatisticsCard
widgetMetaData={widgetMetaData}
@@ -718,6 +725,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
isChild={areChildren}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<div>
<PieChart
@@ -753,6 +761,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
isChild={areChildren}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<DefaultLineChart sx={{alignItems: "center"}}
data={widgetData[i]?.chartData}
@@ -797,6 +806,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
isChild={areChildren}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} actionCallback={actionCallback} values={values} />
</Widget>
@@ -811,6 +821,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
isChild={areChildren}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
omitPadding={omitPaddingOnWidget}
>
<WidgetBlock widgetMetaData={widgetMetaData} block={widgetData[i]} />
</Widget>
@@ -819,7 +830,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
{
widgetMetaData.type === "dataBagViewer" && (
widgetData && widgetData[i] && widgetData[i].queryParams &&
<Widget widgetMetaData={widgetMetaData}>
<Widget widgetMetaData={widgetMetaData} omitPadding={omitPaddingOnWidget}>
<DataBagViewer dataBagId={widgetData[i].queryParams.id} />
</Widget>
)
@@ -827,7 +838,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
{
widgetMetaData.type === "scriptViewer" && (
widgetData && widgetData[i] && widgetData[i].queryParams &&
<Widget widgetMetaData={widgetMetaData}>
<Widget widgetMetaData={widgetMetaData} omitPadding={omitPaddingOnWidget}>
<ScriptViewer scriptId={widgetData[i].queryParams.id} />
</Widget>
)
@@ -880,7 +891,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
{
widgetMetaData.type === "customComponent" && (
widgetData && widgetData[i] &&
<Widget widgetMetaData={widgetMetaData}>
<Widget widgetMetaData={widgetMetaData} omitPadding={omitPaddingOnWidget}>
<CustomComponentWidget widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} />
</Widget>
)
@@ -24,6 +24,7 @@ import {QWidgetMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QWid
import {Alert, Skeleton} from "@mui/material";
import ButtonBlock from "qqq/components/widgets/blocks/ButtonBlock";
import AudioBlock from "qqq/components/widgets/blocks/AudioBlock";
import IconBlock from "qqq/components/widgets/blocks/IconBlock";
import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock";
import React from "react";
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
@@ -97,6 +98,8 @@ export default function WidgetBlock({widgetMetaData, block, actionCallback, valu
return (<AudioBlock widgetMetaData={widgetMetaData} data={block} />);
case "IMAGE":
return (<ImageBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
case "ICON":
return (<IconBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
default:
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
}
@@ -0,0 +1,41 @@
/*
* 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 Icon from "@mui/material/Icon";
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
import React from "react";
/*******************************************************************************
** Block that renders ... an <Icon> tag
**
** <Icon>{name}</Icon>
*******************************************************************************/
export default function IconBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
{
return (
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<span style={{color: data.styles?.color, lineHeight: 0, fontSize: data.styles?.fontSize}}>
<Icon color={data.styles?.color}>{data.values?.name}</Icon>
</span>
</BlockElementWrapper>
);
}
@@ -30,7 +30,7 @@ import React from "react";
**
** <audio src=${path} ${autoPlay} ${showControls} />
*******************************************************************************/
export default function AudioBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
export default function ImageBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
{
let imageStyle: any = {};
@@ -138,7 +138,7 @@ export default function TextBlock({widgetMetaData, data}: StandardBlockComponent
}
const text = data.values.interpolatedText ?? data.values.text;
const lines = text.split("\n");
const lines = (text ?? "").split("\n");
const startIcon = data.values.startIcon?.name ? <Icon>{data.values.startIcon.name}</Icon> : null;
const endIcon = data.values.endIcon?.name ? <Icon>{data.values.endIcon.name}</Icon> : null;
@@ -0,0 +1,124 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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 {Box, Skeleton} from "@mui/material";
import {AdornmentType} from "@qrunio/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QFieldMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QWidgetMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@qrunio/qqq-frontend-core/lib/model/QRecord";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useRef, useState} from "react";
const qController = Client.getInstance();
interface Props
{
field: QFieldMetaData;
record: QRecord;
}
/***************************************************************************
* Render a field for a view-only screen (e.g., not a form), using a widget.
* The field is expected to have an adornment of type WIDGET, with a value
* "widgetName", which should be a valid widget name within the qInstance.
***************************************************************************/
export function FieldValueAsWidget({field, record}: Props): JSX.Element
{
const [widgetName] = useState(field?.getAdornment(AdornmentType.WIDGET)?.getValue("widgetName"));
const [widgetMetaData, setWidgetMetaData] = useState(null as QWidgetMetaData);
const [error, setError] = useState(null as string);
const [ready, setReady] = useState(false);
const skeletonWrapperRef = useRef();
const widgetData = record.values.get(field.name);
useEffect(() =>
{
(async () =>
{
try
{
const metaData = await qController.loadMetaData();
const widgetMetaData = metaData.widgets.get(widgetName);
if (!widgetMetaData)
{
setError(`Could not load widget [${widgetName}]`);
}
else
{
setWidgetMetaData(widgetMetaData);
setReady(true);
}
}
catch (e)
{
setError(`${e}`);
}
})();
}, []);
///////////////////////
// if error, show it //
///////////////////////
if (error)
{
return (<span className="FieldValueAsWidget" style={{color: "red", fontSize: "0.875rem"}}>
<span>Error: {error}</span>
</span>
);
}
///////////////////////////////////////////////////////////////////////////////////////
// make skeleton fade in after a bit, so if content is fast enough, it doesn't flash //
///////////////////////////////////////////////////////////////////////////////////////
setTimeout(() =>
{
try
{
if (skeletonWrapperRef.current)
{
//@ts-ignore
skeletonWrapperRef.current.style.opacity = "100";
}
}
catch (e)
{
// ignore
}
}, 250);
/////////////////////////////////
// if not ready, show skeleton //
/////////////////////////////////
if (!ready)
{
return (<div ref={skeletonWrapperRef} id="it" style={{opacity: 0, transition: "opacity 0.75s ease"}}><Skeleton /></div>);
}
/////////////////////////////////////////////
// assuming all is good, render the widget //
/////////////////////////////////////////////
return (<Box component="span" sx={{"& .widget": {padding: "0 !important", display: "inline-block"}}}>
<DashboardWidgets key={`field:${field.name}`} widgetMetaDataList={[widgetMetaData]} initialWidgetDataList={[widgetData]} omitWrappingGridContainer={true} omitPaddingOnWidget={true} screen="recordView" />
</Box>);
}
+35 -12
View File
@@ -19,16 +19,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QException} from "@qrunio/qqq-frontend-core/lib/exceptions/QException";
import {Capability} from "@qrunio/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@qrunio/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QTableVariant} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableVariant";
import {QRecord} from "@qrunio/qqq-frontend-core/lib/model/QRecord";
import {QueryJoin} from "@qrunio/qqq-frontend-core/lib/model/query/QueryJoin";
import {Alert, Typography} from "@mui/material";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
@@ -48,6 +38,17 @@ import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import {SxProps} from "@mui/system";
import {QException} from "@qrunio/qqq-frontend-core/lib/exceptions/QException";
import {AdornmentType} from "@qrunio/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {Capability} from "@qrunio/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@qrunio/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QTableVariant} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableVariant";
import {QRecord} from "@qrunio/qqq-frontend-core/lib/model/QRecord";
import {QueryJoin} from "@qrunio/qqq-frontend-core/lib/model/query/QueryJoin";
import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import AuditBody from "qqq/components/audits/AuditBody";
@@ -61,6 +62,7 @@ import ShareModal from "qqq/components/sharing/ShareModal";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessRun from "qqq/pages/processes/ProcessRun";
import {FieldValueAsWidget} from "qqq/pages/records/view/FieldValueAsWidget";
import HistoryUtils from "qqq/utils/HistoryUtils";
import HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client";
@@ -100,6 +102,14 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
{
let [field, tableForField] = tableMetaData ? TableUtils.getFieldAndTable(tableMetaData, fieldName) : fieldMap ? [fieldMap[fieldName], null] : [null, null];
if(field == null)
{
if(tableMetaData?.virtualFields?.has(fieldName))
{
field = tableMetaData.virtualFields.get(fieldName);
}
}
if (field != null)
{
let label = field.label;
@@ -111,6 +121,13 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default", ...(styleOverrides?.label ?? {})}}>{label}:</Typography>;
if(field.hasAdornment(AdornmentType.WIDGET))
{
return (<Grid item key={fieldName} lg={gridColumns} flexDirection="column" pr={2}>
<FieldValueAsWidget field={field} record={record} />
</Grid>);
}
return (
<Grid item key={fieldName} lg={gridColumns} flexDirection="column" pr={2}>
<>
@@ -594,7 +611,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
/////////////////////////////////////////////////
// define the sections, e.g., for the left-bar //
/////////////////////////////////////////////////
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData);
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, null, undefined, "RECORD_VIEW");
setTableSections(tableSections);
////////////////////////////////////////////////////
@@ -604,7 +621,13 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
const nonT1TableSections = [];
for (let i = 0; i < tableSections.length; i++)
{
const section = tableSections[i];
let section = tableSections[i];
if(section.alternatives?.has("RECORD_VIEW"))
{
section = section.alternatives.get("RECORD_VIEW")
}
if (section.isHidden)
{
continue;
+9 -3
View File
@@ -44,7 +44,7 @@ class TableUtils
** By default sections w/ widget names are excluded -- but -- to include them,
** provide the metaData plus list of allowedWidgetTypes.
*******************************************************************************/
public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData, allowedFieldNames: any = null, additionalInclusionPredicate?: (section: QTableSection) => boolean): QTableSection[]
public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData, allowedFieldNames: any = null, additionalInclusionPredicate?: (section: QTableSection) => boolean, alternativeType?: string): QTableSection[]
{
/////////////////////////////////////////////////////////////////
// if the table has sections, then filter them and return some //
@@ -72,7 +72,12 @@ class TableUtils
const allowedSections: QTableSection[] = [];
for (let i = 0; i < tableMetaData.sections.length; i++)
{
const section = tableMetaData.sections[i];
let section = tableMetaData.sections[i];
if(alternativeType && section.alternatives?.has(alternativeType))
{
section = section.alternatives.get(alternativeType);
}
let includeSection = false;
for (let j = 0; j < section.fieldNames?.length; j++)
@@ -101,8 +106,9 @@ class TableUtils
////////////////////////////////////////////////////////////////
// if there are no filters to apply, then return all sections //
// (potentially mapped to an alternative, by the type) //
////////////////////////////////////////////////////////////////
return (tableMetaData.sections);
return (tableMetaData.sections.map((section) => section.alternatives?.get(alternativeType) ?? section));
}
///////////////////////////////////////////////////////////////////////////////////////////////
@@ -23,9 +23,12 @@ package com.kingsrook.qqq.frontend.materialdashboard.seleniumwithqapplication.te
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
@@ -39,8 +42,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSectionAlternativeType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.frontend.materialdashboard.junit.TestUtils;
@@ -85,8 +90,11 @@ public class FilterWidgetOnFieldIT extends QBaseSeleniumWithQApplicationTest
.withFieldAdornment(new FieldAdornment(AdornmentType.WIDGET)
.withValue(AdornmentType.WidgetValues.WIDGET_NAME, widget.getName())))
.withSection(new QFieldSection("identity", new QIcon(), Tier.T1, List.of("id", "name")))
.withSection(new QFieldSection("filter", new QIcon(), Tier.T2, List.of("filterJSON")))
.withPrimaryKeyField("id");
.withSection(new QFieldSection("filter", new QIcon(), Tier.T2, List.of("filterJSON"))
.withAlternative(QFieldSectionAlternativeType.RECORD_VIEW, (s) -> s.setFieldNames(List.of("filterVirtual"))))
.withVirtualField(new QVirtualFieldMetaData("filterVirtual", QFieldType.STRING).withLabel("Person Filter"))
.withPrimaryKeyField("id")
.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(TableCustomizer.class));
qInstance.addTable(table);
PeopleAppProducer.addTableToGreetingsApp(qInstance, table.getName());
@@ -122,6 +130,25 @@ public class FilterWidgetOnFieldIT extends QBaseSeleniumWithQApplicationTest
/***************************************************************************
*
***************************************************************************/
public static class TableCustomizer implements TableCustomizerInterface
{
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
for(QRecord record : records)
{
record.setValue("filterVirtual", record.getValueString("filterJSON"));
}
return (records);
}
}
/*******************************************************************************
**
*******************************************************************************/
@@ -30,12 +30,15 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.FilterAndColumnsSetupData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
@@ -59,6 +62,7 @@ import org.junit.jupiter.api.Test;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -70,6 +74,10 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
{
private static final String TABLE_NAME = "tableWithFormAdjuster";
public static final String MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_NAME = "may-not-edit-custom";
public static final String MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_NAME = "may-not-edit-generic";
public static final Integer MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID = 1;
public static final Integer MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID = 2;
/***************************************************************************
@@ -114,6 +122,20 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
/***************************************************************************
*
***************************************************************************/
@Override
protected void setupTestData() throws QException
{
new InsertAction().execute(new InsertInput(TABLE_NAME).withRecords(List.of(
new QRecord().withValue("id", MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID).withValue("name", MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_NAME),
new QRecord().withValue("id", MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID).withValue("name", MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_NAME)
)));
}
/***************************************************************************
*
***************************************************************************/
@@ -125,6 +147,15 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
@Override
public FormAdjusterOutput execute(FormAdjusterInput input) throws QException
{
if(MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_NAME.equals(input.getFieldValue("name")))
{
return (new FormAdjusterOutput().withIsFormDisabled(true).withFormDisabledMessage("No soup for you."));
}
else if(MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_NAME.equals(input.getFieldValue("name")))
{
return (new FormAdjusterOutput().withIsFormDisabled(true));
}
Set<String> names = Collections.emptySet();
if("name".equals(input.getFieldName()))
{
@@ -217,6 +248,26 @@ public class FormAdjusterIT extends QBaseSeleniumWithQApplicationTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDisablingForm()
{
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/" + TABLE_NAME + "/" + MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID + "/edit",
"Edit Table With Form Adjuster: " + MAY_NOT_EDIT_WITH_CUSTOM_MESSAGE_ID);
qfmdSeleniumLib.waitForAlert("No soup for you.");
assertThatThrownBy(() -> qfmdSeleniumLib.inputTextField("size", "XL"));
qSeleniumLib.waitForSelectorContaining("button[disabled]", "save");
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/" + TABLE_NAME + "/" + MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID + "/edit",
"Edit Table With Form Adjuster: " + MAY_NOT_EDIT_WITH_DEFAULT_MESSAGE_ID);
qfmdSeleniumLib.waitForAlert("You are not allowed to edit this record");
qSeleniumLib.waitForSelectorContaining("button[disabled]", "save");
}
/***************************************************************************
*
***************************************************************************/
@@ -0,0 +1,207 @@
/*
* 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/>.
*/
package com.kingsrook.qqq.frontend.materialdashboard.seleniumwithqapplication.tests;
import java.time.LocalDate;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.CompositeWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.icon.IconBlockData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.icon.IconValues;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextBlockData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextValues;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSectionAlternativeType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.frontend.materialdashboard.seleniumwithqapplication.lib.QBaseSeleniumWithQApplicationTest;
import com.kingsrook.qqq.frontend.materialdashboard.seleniumwithqapplication.metadata.MemoryBackendProducer;
import com.kingsrook.qqq.frontend.materialdashboard.seleniumwithqapplication.metadata.PeopleAppProducer;
import org.junit.jupiter.api.Test;
/*******************************************************************************
* Tests for some new features being added together:
* 1) virtual field on a table
* 2) used in an alternative section
* 3) rendered as a widget on the view screen
*******************************************************************************/
public class VirtualFieldInAlternativeSectionViewedAsWidgetIT extends QBaseSeleniumWithQApplicationTest
{
private static final String TABLE_NAME = "tableWithVirtualFieldInAlternativeSectionViewedAsWidget";
/***************************************************************************
*
***************************************************************************/
@Override
protected void customizeQInstance(QInstance qInstance) throws QException
{
QWidgetMetaData widget = new QWidgetMetaData()
.withName("genericCompositeWidget")
.withType(WidgetType.COMPOSITE.getType())
.withCodeReference(new QCodeReference(NoopWidgetRenderer.class));
qInstance.addWidget(widget);
QTableMetaData table = new QTableMetaData()
.withName(TABLE_NAME)
.withBackendName(MemoryBackendProducer.NAME)
.withRecordLabelFormatAndFields("%s", "name")
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("name", QFieldType.STRING).withIsRequired(true))
.withField(new QFieldMetaData("type", QFieldType.STRING))
.withField(new QFieldMetaData("homeTown", QFieldType.STRING))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withSection(new QFieldSection("identity", new QIcon(), Tier.T1, List.of("id", "name", "homeTown"))
.withAlternative(QFieldSectionAlternativeType.RECORD_VIEW, (s) -> s.setFieldNames(List.of("mergedField", "homeTown"))))
.withSection(new QFieldSection("hidden", new QIcon(), Tier.T2, List.of("type"))
.withAlternative(QFieldSectionAlternativeType.RECORD_VIEW, (s) -> s.setIsHidden(true)))
.withSection(new QFieldSection("dates", new QIcon(), Tier.T3, List.of("createDate", "modifyDate"))
.withAlternative(QFieldSectionAlternativeType.RECORD_VIEW, (s) -> s.getFieldNames().add(0, "currentDate")))
.withPrimaryKeyField("id");
table.withVirtualField(new QVirtualFieldMetaData("mergedField", QFieldType.STRING)
.withFieldAdornment(new FieldAdornment().withType(AdornmentType.WIDGET)
.withValue(AdornmentType.WidgetValues.WIDGET_NAME, widget.getName())));
table.withVirtualField(new QVirtualFieldMetaData("currentDate", QFieldType.DATE));
table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(TableCustomizer.class));
qInstance.addTable(table);
PeopleAppProducer.addTableToGreetingsApp(qInstance, table.getName());
}
/***************************************************************************
*
***************************************************************************/
@Override
protected void setupTestData() throws QException
{
new InsertAction().execute(new InsertInput(TABLE_NAME).withRecord(new QRecord()
.withValue("id", 1)
.withValue("name", "Homer")
.withValue("type", "Cartoon")
.withValue("homeTown", "Springville")
));
}
/***************************************************************************
*
***************************************************************************/
public static class NoopWidgetRenderer extends AbstractWidgetRenderer
{
/***************************************************************************
**
***************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return null;
}
}
/***************************************************************************
*
***************************************************************************/
public static class TableCustomizer implements TableCustomizerInterface
{
/***************************************************************************
*
***************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records)
{
for(QRecord record : records)
{
CompositeWidgetData widgetData = new CompositeWidgetData().withLayout(CompositeWidgetData.Layout.FLEX_ROW_WRAPPED)
.withStyles(new BaseStyles().withPadding(BaseStyles.Directional.ofY("8")))
.withBlocks(List.of(
new IconBlockData().withValues(new IconValues().withName("gavel")),
new TextBlockData().withValues(new TextValues(record.getValueString("id") + "|" + record.getValueString("name") + "|" + record.getValueString("type")))
));
record.setValue("mergedField", widgetData);
record.setValue("currentDate", LocalDate.parse("2025-12-09"));
}
return records;
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/" + TABLE_NAME + "/1", "Homer");
qSeleniumLib.waitForSelectorContaining(".material-icons-round", "gavel");
qSeleniumLib.waitForSelectorContaining(".compositeWidget", "1|Homer|Cartoon");
qSeleniumLib.waitForSelectorContaining(".sidebar-section", "Identity");
qSeleniumLib.waitForSelectorContainingToNotExist(".sidebar-section", "Hidden");
qSeleniumLib.waitForSelectorContaining(".sidebar-section", "Dates");
qSeleniumLib.waitForSelectorContainingToNotExist("h6", "Hidden");
qSeleniumLib.waitForSelectorContaining("h6", "Dates");
qfmdSeleniumLib.waitForViewScreenFieldValue("Home Town", "Springville");
qfmdSeleniumLib.waitForViewScreenFieldValue("Current Date", "2025-12-09");
// qSeleniumLib.waitForever();
}
}