Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a155097d89 | ||
|
|
6594214084 | ||
|
|
8d99a11f7e | ||
|
|
e89ce56864 | ||
|
|
914aac19b0 | ||
|
|
7ef04f0afa | ||
|
|
352fa68da5 | ||
|
|
dafc96b0fa | ||
|
|
c524052391 | ||
|
|
b018d7fe1e |
No files matched your search
Generated
+22935
-22851
File diff suppressed because it is too large.
Load diff
+116
-115
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
+79
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+29
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
||||
+51
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
|
||||
+207
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user