mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 07:08:44 +00:00
Compare commits
4 Commits
snapshot-f
...
snapshot-i
Author | SHA1 | Date | |
---|---|---|---|
cdb1a456ef | |||
5769f95419 | |||
1941ec6b41 | |||
f3ae5ee10b |
@ -2,7 +2,7 @@ version: 2.1
|
|||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
node: circleci/node@5.1.0
|
node: circleci/node@5.1.0
|
||||||
browser-tools: circleci/browser-tools@1.4.7
|
browser-tools: circleci/browser-tools@1.4.6
|
||||||
|
|
||||||
executors:
|
executors:
|
||||||
java17:
|
java17:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.87",
|
"@kingsrook/qqq-frontend-core": "1.0.85",
|
||||||
"@mui/icons-material": "5.4.1",
|
"@mui/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.11.1",
|
"@mui/material": "5.11.1",
|
||||||
"@mui/styles": "5.11.1",
|
"@mui/styles": "5.11.1",
|
||||||
|
2
pom.xml
2
pom.xml
@ -66,7 +66,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-backend-core</artifactId>
|
<artifactId>qqq-backend-core</artifactId>
|
||||||
<version>feature-CE-876-develop-missing-widget-types-20240221.002945-1</version>
|
<version>feature-CE-798-quick-filters-20240123.205854-1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
|
@ -354,7 +354,8 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
<Grid container columnSpacing={5} rowSpacing={1}>
|
<Grid container columnSpacing={5} rowSpacing={1}>
|
||||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>n</span>Create a New Record</Grid>
|
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>n</span>Create a New Record</Grid>
|
||||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>r</span>Refresh the Query</Grid>
|
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>r</span>Refresh the Query</Grid>
|
||||||
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Builder (Advanced mode only)</Grid>
|
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>c</span>Open the Columns Panel</Grid>
|
||||||
|
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Panel</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Typography variant="h6" pt={3}>Record View</Typography>
|
<Typography variant="h6" pt={3}>Record View</Typography>
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* QQQ - Low-code Application Framework for Engineers.
|
|
||||||
* Copyright (C) 2021-2023. 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.model.metadata;
|
|
||||||
|
|
||||||
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QSupplementalAppMetaData;
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** app-level meta-data for this module (handled as QSupplementalTableMetaData)
|
|
||||||
*******************************************************************************/
|
|
||||||
public class MaterialDashboardAppMetaData extends QSupplementalAppMetaData
|
|
||||||
{
|
|
||||||
private Boolean showAppLabelOnHomeScreen = true;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Override
|
|
||||||
public boolean includeInFullFrontendMetaData()
|
|
||||||
{
|
|
||||||
return (true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Override
|
|
||||||
public String getType()
|
|
||||||
{
|
|
||||||
return ("materialDashboard");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Getter for showAppLabelOnHomeScreen
|
|
||||||
*******************************************************************************/
|
|
||||||
public Boolean getShowAppLabelOnHomeScreen()
|
|
||||||
{
|
|
||||||
return (this.showAppLabelOnHomeScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Setter for showAppLabelOnHomeScreen
|
|
||||||
*******************************************************************************/
|
|
||||||
public void setShowAppLabelOnHomeScreen(Boolean showAppLabelOnHomeScreen)
|
|
||||||
{
|
|
||||||
this.showAppLabelOnHomeScreen = showAppLabelOnHomeScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Fluent setter for showAppLabelOnHomeScreen
|
|
||||||
*******************************************************************************/
|
|
||||||
public MaterialDashboardAppMetaData withShowAppLabelOnHomeScreen(Boolean showAppLabelOnHomeScreen)
|
|
||||||
{
|
|
||||||
this.showAppLabelOnHomeScreen = showAppLabelOnHomeScreen;
|
|
||||||
return (this);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -28,5 +28,4 @@ package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
|||||||
public interface MaterialDashboardIconRoleNames
|
public interface MaterialDashboardIconRoleNames
|
||||||
{
|
{
|
||||||
String TOP_RIGHT_INSIDE_CARD = "topRightInsideCard";
|
String TOP_RIGHT_INSIDE_CARD = "topRightInsideCard";
|
||||||
String TOP_LEFT_INSIDE_CARD = "topLeftInsideCard";
|
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ import QContext from "QContext";
|
|||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
helpContents: null | QHelpContent | QHelpContent[];
|
helpContents: QHelpContent[];
|
||||||
roles: string[];
|
roles: string[];
|
||||||
heading?: string;
|
heading?: string;
|
||||||
helpContentKey?: string;
|
helpContentKey?: string;
|
||||||
@ -93,27 +93,9 @@ const getMatchingHelpContent = (helpContents: QHelpContent[], roles: string[]):
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** test if a list of help contents would find any matches from a list of roles.
|
** test if a list of help contents would find any matches from a list of roles.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export const hasHelpContent = (helpContents: null | QHelpContent | QHelpContent[], roles: string[]) =>
|
export const hasHelpContent = (helpContents: QHelpContent[], roles: string[]) =>
|
||||||
{
|
{
|
||||||
return getMatchingHelpContent(nullOrSingletonOrArrayToArray(helpContents), roles) != null;
|
return getMatchingHelpContent(helpContents, roles) != null;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
const nullOrSingletonOrArrayToArray = (helpContents: null | QHelpContent | QHelpContent[]): QHelpContent[] =>
|
|
||||||
{
|
|
||||||
let array: QHelpContent[] = [];
|
|
||||||
if(Array.isArray(helpContents))
|
|
||||||
{
|
|
||||||
array = helpContents;
|
|
||||||
}
|
|
||||||
else if(helpContents != null)
|
|
||||||
{
|
|
||||||
array.push(helpContents);
|
|
||||||
}
|
|
||||||
return (array);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -124,8 +106,7 @@ const nullOrSingletonOrArrayToArray = (helpContents: null | QHelpContent | QHelp
|
|||||||
function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX.Element
|
function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const {helpHelpActive} = useContext(QContext);
|
const {helpHelpActive} = useContext(QContext);
|
||||||
const helpContentsArray = nullOrSingletonOrArrayToArray(helpContents);
|
let selectedHelpContent = getMatchingHelpContent(helpContents, roles);
|
||||||
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
|
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
if (helpHelpActive)
|
if (helpHelpActive)
|
||||||
|
@ -45,7 +45,7 @@ export default function ExportMenuItem(props: QExportMenuItemProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!totalRecords}
|
disabled={totalRecords === 0}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -398,7 +398,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
|||||||
criteria.values = [];
|
criteria.values = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newValue.valueMode && !newValue.implicitValues)
|
if(newValue.valueMode)
|
||||||
{
|
{
|
||||||
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
||||||
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
||||||
|
@ -30,7 +30,7 @@ import Box from "@mui/material/Box";
|
|||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import React, {SyntheticEvent, useContext, useReducer, useState} from "react";
|
import React, {SyntheticEvent, useContext, useState} from "react";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||||
import {getDefaultCriteriaValue, getOperatorOptions, getValueModeRequiredCount, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
import {getDefaultCriteriaValue, getOperatorOptions, getValueModeRequiredCount, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
||||||
@ -148,10 +148,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const [isMouseOver, setIsMouseOver] = useState(false);
|
const [isMouseOver, setIsMouseOver] = useState(false);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? criteriaParam as QFilterCriteriaWithId : null);
|
||||||
// copy the criteriaParam to a new object in here - so changes won't apply until user closes the menu //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? Object.assign({}, criteriaParam) as QFilterCriteriaWithId : null);
|
|
||||||
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
|
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
|
||||||
|
|
||||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
||||||
@ -161,11 +158,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
|
|
||||||
const {accentColor} = useContext(QContext);
|
const {accentColor} = useContext(QContext);
|
||||||
|
|
||||||
//////////////////////
|
|
||||||
// ole' faithful... //
|
|
||||||
//////////////////////
|
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
@ -190,30 +182,15 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (criteriaParamIsCriteria(criteriaParam) && JSON.stringify(criteriaParam) !== JSON.stringify(criteria))
|
if (criteriaParamIsCriteria(criteriaParam) && JSON.stringify(criteriaParam) !== JSON.stringify(criteria))
|
||||||
{
|
{
|
||||||
if(isOpen)
|
const newCriteria = criteriaParam as QFilterCriteriaWithId;
|
||||||
{
|
setCriteria(newCriteria);
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
||||||
// this was firing too-often for case where: there was a criteria originally //
|
setOperatorSelectedValue(operatorOption);
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
setOperatorInputValue(operatorOption.label);
|
||||||
console.log("Not handling outside change (A), because dropdown is-open");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// copy the criteriaParam to a new object in here - so changes won't apply until user closes the menu //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const newCriteria = Object.assign({}, criteriaParam) as QFilterCriteriaWithId;
|
|
||||||
setCriteria(newCriteria);
|
|
||||||
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
|
||||||
setOperatorSelectedValue(operatorOption);
|
|
||||||
setOperatorInputValue(operatorOption.label);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Test if we need to construct a new criteria object
|
** Test if we need to construct a new criteria object
|
||||||
** This is (at least for some cases) for when the criteria gets changed
|
|
||||||
** from outside of this component - e.g., a reset on the query screen
|
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const criteriaNeedsReset = (): boolean =>
|
const criteriaNeedsReset = (): boolean =>
|
||||||
{
|
{
|
||||||
@ -222,16 +199,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
||||||
if(criteria.operator !== defaultOperatorOption?.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue()))
|
if(criteria.operator !== defaultOperatorOption?.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue()))
|
||||||
{
|
{
|
||||||
if(isOpen)
|
|
||||||
{
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// this was firing too-often for case where: there was no criteria originally, //
|
|
||||||
// so, by adding this is-open check, we eliminated those. //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
console.log("Not handling outside change (B), because dropdown is-open");
|
|
||||||
return (false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,7 +207,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Construct a new criteria object - resetting the values tied to the operator
|
** Construct a new criteria object - resetting the values tied to the oprator
|
||||||
** autocomplete at the same time.
|
** autocomplete at the same time.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const makeNewCriteria = (): QFilterCriteria =>
|
const makeNewCriteria = (): QFilterCriteria =>
|
||||||
@ -274,11 +241,6 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const closeMenu = () =>
|
const closeMenu = () =>
|
||||||
{
|
{
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// when closing the menu, that's when we'll update the criteria from the caller //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
updateCriteria(criteria, false, false);
|
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
@ -309,7 +271,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
criteria.values = [];
|
criteria.values = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newValue.valueMode && !newValue.implicitValues)
|
if(newValue.valueMode)
|
||||||
{
|
{
|
||||||
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
||||||
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
||||||
@ -324,8 +286,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
setOperatorInputValue("");
|
setOperatorInputValue("");
|
||||||
}
|
}
|
||||||
|
|
||||||
setCriteria(criteria);
|
updateCriteria(criteria, false, false);
|
||||||
forceUpdate();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -359,8 +320,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
criteria.values[valueIndex] = value;
|
criteria.values[valueIndex] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCriteria(criteria);
|
updateCriteria(criteria, true, false);
|
||||||
forceUpdate();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -440,18 +400,16 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
buttonAdditionalStyles.color = "white !important";
|
buttonAdditionalStyles.color = "white !important";
|
||||||
buttonClassName = "filterActive";
|
buttonClassName = "filterActive";
|
||||||
|
|
||||||
let valuesString = FilterUtils.getValuesString(fieldMetaData, criteria, 1, "+N");
|
let valuesString = FilterUtils.getValuesString(fieldMetaData, criteria);
|
||||||
|
if(fieldMetaData.type == QFieldType.BOOLEAN)
|
||||||
///////////////////////////////////////////
|
|
||||||
// don't show the Equals or In operators //
|
|
||||||
///////////////////////////////////////////
|
|
||||||
let operatorString = (<>{operatorSelectedValue.label} </>);
|
|
||||||
if(operatorSelectedValue.value == QCriteriaOperator.EQUALS || operatorSelectedValue.value == QCriteriaOperator.IN)
|
|
||||||
{
|
{
|
||||||
operatorString = (<></>)
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for booleans, in here, the operator-label is "equals yes" or "equals no", so we don't want the values string //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
valuesString = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonContent = (<><span style={{fontWeight: 700}}>{buttonContent}:</span> <span style={{fontWeight: 400}}>{operatorString}{valuesString}</span></>);
|
buttonContent = (<><span style={{fontWeight: 700}}>{buttonContent}:</span> <span style={{fontWeight: 400}}>{operatorSelectedValue.label} {valuesString}</span></>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mouseEvents =
|
const mouseEvents =
|
||||||
@ -520,12 +478,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
|
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
id={"criteriaOperator"}
|
id={"criteriaOperator"}
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||||
// ok, so, by default, if you type an 'o' as the first letter in the FilterCriteriaRowValues box, //
|
|
||||||
// something is causing THIS element to become selected, if the first letter in its label is 'O'. //
|
|
||||||
// ... work around is to put invisible ‌ entity as first character in label instead... //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
renderInput={(params) => (<TextField {...params} label={<>‌Operator</>} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
|
||||||
options={operatorOptions}
|
options={operatorOptions}
|
||||||
value={operatorSelectedValue as any}
|
value={operatorSelectedValue as any}
|
||||||
inputValue={operatorInputValue}
|
inputValue={operatorInputValue}
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
|
||||||
import {Box, Skeleton} from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
|
||||||
|
|
||||||
|
|
||||||
interface CompositeData
|
|
||||||
{
|
|
||||||
blocks: BlockData[];
|
|
||||||
styleOverrides?: any;
|
|
||||||
layout?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface CompositeWidgetProps
|
|
||||||
{
|
|
||||||
widgetMetaData: QWidgetMetaData;
|
|
||||||
data: CompositeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Widget which is a list of Blocks.
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element
|
|
||||||
{
|
|
||||||
if (!data || !data.blocks)
|
|
||||||
{
|
|
||||||
return (<Skeleton />);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// note - these layouts are defined in qqq in the CompositeWidgetData.Layout enum //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
let layout = data?.layout;
|
|
||||||
let boxStyle: any = {};
|
|
||||||
if (layout == "FLEX_ROW_WRAPPED")
|
|
||||||
{
|
|
||||||
boxStyle.display = "flex";
|
|
||||||
boxStyle.flexDirection = "row";
|
|
||||||
boxStyle.flexWrap = "wrap";
|
|
||||||
boxStyle.gap = "0.5rem";
|
|
||||||
}
|
|
||||||
else if (layout == "FLEX_ROW_SPACE_BETWEEN")
|
|
||||||
{
|
|
||||||
boxStyle.display = "flex";
|
|
||||||
boxStyle.flexDirection = "row";
|
|
||||||
boxStyle.justifyContent = "space-between"
|
|
||||||
boxStyle.gap = "0.25rem";
|
|
||||||
}
|
|
||||||
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
|
||||||
{
|
|
||||||
boxStyle.display = "flex";
|
|
||||||
boxStyle.flexDirection = "column";
|
|
||||||
boxStyle.fontSize = "0.875rem";
|
|
||||||
boxStyle.fontWeight = 400;
|
|
||||||
boxStyle.borderRight = "1px solid #D0D0D0";
|
|
||||||
}
|
|
||||||
else if (layout == "BADGES_WRAPPER")
|
|
||||||
{
|
|
||||||
boxStyle.display = "flex";
|
|
||||||
boxStyle.gap = "0.25rem";
|
|
||||||
boxStyle.padding = "0 0.25rem";
|
|
||||||
boxStyle.fontSize = "0.875rem";
|
|
||||||
boxStyle.fontWeight = 400;
|
|
||||||
boxStyle.border = "1px solid gray";
|
|
||||||
boxStyle.borderRadius = "0.5rem";
|
|
||||||
boxStyle.background = "#FFFFFF";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.styleOverrides)
|
|
||||||
{
|
|
||||||
boxStyle = {...boxStyle, ...data.styleOverrides};
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<Box sx={boxStyle} className="compositeWidget">
|
|
||||||
{
|
|
||||||
data.blocks.map((block: BlockData, index) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Box>);
|
|
||||||
|
|
||||||
}
|
|
@ -35,7 +35,6 @@ import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLin
|
|||||||
import SmallLineChart from "qqq/components/widgets/charts/linechart/SmallLineChart";
|
import SmallLineChart from "qqq/components/widgets/charts/linechart/SmallLineChart";
|
||||||
import PieChart from "qqq/components/widgets/charts/piechart/PieChart";
|
import PieChart from "qqq/components/widgets/charts/piechart/PieChart";
|
||||||
import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
|
import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
|
||||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
|
||||||
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
||||||
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
||||||
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
|
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
|
||||||
@ -48,7 +47,6 @@ import ParentWidget from "qqq/components/widgets/ParentWidget";
|
|||||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||||
import Widget, {HeaderIcon, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, LabelComponent} from "qqq/components/widgets/Widget";
|
import Widget, {HeaderIcon, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, LabelComponent} from "qqq/components/widgets/Widget";
|
||||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import TableWidget from "./tables/TableWidget";
|
import TableWidget from "./tables/TableWidget";
|
||||||
@ -256,17 +254,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard");
|
const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard");
|
||||||
if (topRightInsideCardIcon)
|
if (topRightInsideCardIcon)
|
||||||
{
|
{
|
||||||
labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.path, topRightInsideCardIcon.color, "topRightInsideCard"));
|
labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.path, topRightInsideCardIcon.color));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelAdditionalComponentsLeft: LabelComponent[] = [];
|
|
||||||
if (widgetMetaData && widgetMetaData.icons)
|
|
||||||
{
|
|
||||||
const topLeftInsideCardIcon = widgetMetaData.icons.get("topLeftInsideCard");
|
|
||||||
if (topLeftInsideCardIcon)
|
|
||||||
{
|
|
||||||
labelAdditionalComponentsLeft.push(new HeaderIcon(topLeftInsideCardIcon.name, topLeftInsideCardIcon.path, topLeftInsideCardIcon.color, "topLeftInsideCard"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +302,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
>
|
||||||
<StackedBarChart data={widgetData[i]?.chartData} chartSubheaderData={widgetData[i]?.chartSubheaderData} />
|
<StackedBarChart data={widgetData[i]?.chartData} chartSubheaderData={widgetData[i]?.chartSubheaderData} />
|
||||||
</Widget>
|
</Widget>
|
||||||
@ -327,8 +314,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
showReloadControl={false}
|
showReloadControl={false}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
>
|
||||||
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
|
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
|
||||||
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
|
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
|
||||||
@ -342,8 +327,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
>
|
||||||
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
|
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
|
||||||
<Box padding="1rem" sx={{width: "100%"}}>
|
<Box padding="1rem" sx={{width: "100%"}}>
|
||||||
@ -359,8 +342,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<MDTypography component="div" variant="button" color="text" fontWeight="light">
|
<MDTypography component="div" variant="button" color="text" fontWeight="light">
|
||||||
@ -392,11 +373,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
>
|
||||||
<StatisticsCard
|
<StatisticsCard
|
||||||
widgetMetaData={widgetMetaData}
|
|
||||||
data={widgetData[i]}
|
data={widgetData[i]}
|
||||||
increaseIsGood={true}
|
increaseIsGood={true}
|
||||||
/>
|
/>
|
||||||
@ -436,7 +414,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<PieChart
|
<PieChart
|
||||||
@ -472,8 +449,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
isChild={areChildren}
|
isChild={areChildren}
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
>
|
||||||
<DefaultLineChart sx={{alignItems: "center"}}
|
<DefaultLineChart sx={{alignItems: "center"}}
|
||||||
data={widgetData[i]?.chartData}
|
data={widgetData[i]?.chartData}
|
||||||
@ -502,34 +477,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
widgetMetaData.type === "composite" && (
|
|
||||||
<Widget
|
|
||||||
widgetMetaData={widgetMetaData}
|
|
||||||
widgetData={widgetData[i]}
|
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
|
||||||
isChild={areChildren}
|
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
|
||||||
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} />
|
|
||||||
</Widget>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
widgetMetaData.type === "block" && (
|
|
||||||
<Widget
|
|
||||||
widgetMetaData={widgetMetaData}
|
|
||||||
widgetData={widgetData[i]}
|
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
|
||||||
isChild={areChildren}
|
|
||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
|
||||||
>
|
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={widgetData[i]} />
|
|
||||||
</Widget>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "dataBagViewer" && (
|
widgetMetaData.type === "dataBagViewer" && (
|
||||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
||||||
@ -576,6 +523,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
renderedWidget = (<TabPanel index={i} value={selectedTab} style={{
|
renderedWidget = (<TabPanel index={i} value={selectedTab} style={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
margin: "-1rem",
|
margin: "-1rem",
|
||||||
|
marginBottom: "-3.5rem",
|
||||||
width: "calc(100% + 2rem)"
|
width: "calc(100% + 2rem)"
|
||||||
}}>
|
}}>
|
||||||
{renderedWidget}
|
{renderedWidget}
|
||||||
|
@ -33,7 +33,6 @@ import Client from "qqq/utils/qqq/Client";
|
|||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
export interface ParentWidgetData
|
export interface ParentWidgetData
|
||||||
{
|
{
|
||||||
label?: string;
|
|
||||||
dropdownLabelList: string[];
|
dropdownLabelList: string[];
|
||||||
dropdownNameList: string[];
|
dropdownNameList: string[];
|
||||||
dropdownDataList: {
|
dropdownDataList: {
|
||||||
@ -43,7 +42,6 @@ export interface ParentWidgetData
|
|||||||
childWidgetNameList: string[];
|
childWidgetNameList: string[];
|
||||||
dropdownNeedsSelectedText?: string;
|
dropdownNeedsSelectedText?: string;
|
||||||
storeDropdownSelections?: boolean;
|
storeDropdownSelections?: boolean;
|
||||||
csvData?: any[][];
|
|
||||||
icon?: string;
|
icon?: string;
|
||||||
layoutType: string;
|
layoutType: string;
|
||||||
}
|
}
|
||||||
@ -66,8 +64,7 @@ interface Props
|
|||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidgetCallback, entityPrimaryKey, tableName, storeDropdownSelections}: Props, ): JSX.Element
|
||||||
function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidgetCallback, entityPrimaryKey, tableName, storeDropdownSelections}: Props,): JSX.Element
|
|
||||||
{
|
{
|
||||||
const [childUrlParams, setChildUrlParams] = useState((urlParams) ? urlParams : "");
|
const [childUrlParams, setChildUrlParams] = useState((urlParams) ? urlParams : "");
|
||||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||||
@ -84,27 +81,27 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (qInstance && data && data.childWidgetNameList)
|
if(qInstance && data && data.childWidgetNameList)
|
||||||
{
|
{
|
||||||
let widgetMetaDataList = [] as QWidgetMetaData[];
|
let widgetMetaDataList = [] as QWidgetMetaData[];
|
||||||
data?.childWidgetNameList.forEach((widgetName: string) =>
|
data?.childWidgetNameList.forEach((widgetName: string) =>
|
||||||
{
|
{
|
||||||
widgetMetaDataList.push(qInstance.widgets.get(widgetName));
|
widgetMetaDataList.push(qInstance.widgets.get(widgetName));
|
||||||
});
|
})
|
||||||
setWidgets(widgetMetaDataList);
|
setWidgets(widgetMetaDataList);
|
||||||
}
|
}
|
||||||
}, [qInstance, data, childUrlParams]);
|
}, [qInstance, data, childUrlParams]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setChildUrlParams(urlParams);
|
setChildUrlParams(urlParams)
|
||||||
}, [urlParams]);
|
}, [urlParams]);
|
||||||
|
|
||||||
const parentReloadWidgetCallback = (data: string) =>
|
const parentReloadWidgetCallback = (data: string) =>
|
||||||
{
|
{
|
||||||
setChildUrlParams(data);
|
setChildUrlParams(data);
|
||||||
reloadWidgetCallback(data);
|
reloadWidgetCallback(data);
|
||||||
};
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if this parent widget is in card form, and its children are too, then we need some px //
|
// if this parent widget is in card form, and its children are too, then we need some px //
|
||||||
@ -128,7 +125,7 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
|||||||
omitPadding={omitPadding}
|
omitPadding={omitPadding}
|
||||||
>
|
>
|
||||||
<Box sx={{height: "100%", width: "100%"}} px={px}>
|
<Box sx={{height: "100%", width: "100%"}} px={px}>
|
||||||
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType?.toLowerCase() == "tabs"} />
|
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType == "TABS"}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Widget>
|
</Widget>
|
||||||
) : null
|
) : null
|
||||||
|
@ -28,14 +28,10 @@ import Icon from "@mui/material/Icon";
|
|||||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {useContext, useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {NavigateFunction, useNavigate} from "react-router-dom";
|
import {NavigateFunction, useNavigate} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
|
||||||
import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
|
import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
|
||||||
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
|
||||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
|
||||||
|
|
||||||
export interface WidgetData
|
export interface WidgetData
|
||||||
{
|
{
|
||||||
@ -113,18 +109,16 @@ export class HeaderIcon extends LabelComponent
|
|||||||
iconPath: string;
|
iconPath: string;
|
||||||
color: string;
|
color: string;
|
||||||
coloredBG: boolean;
|
coloredBG: boolean;
|
||||||
role: string;
|
|
||||||
|
|
||||||
iconColor: string;
|
iconColor: string;
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
|
|
||||||
constructor(iconName: string, iconPath: string, color: string, role?: string, coloredBG: boolean = true)
|
constructor(iconName: string, iconPath: string, color: string, coloredBG: boolean = true)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.iconName = iconName;
|
this.iconName = iconName;
|
||||||
this.iconPath = iconPath;
|
this.iconPath = iconPath;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
this.role = role;
|
|
||||||
this.coloredBG = coloredBG;
|
this.coloredBG = coloredBG;
|
||||||
|
|
||||||
this.iconColor = this.coloredBG ? "#FFFFFF" : this.color;
|
this.iconColor = this.coloredBG ? "#FFFFFF" : this.color;
|
||||||
@ -134,7 +128,7 @@ export class HeaderIcon extends LabelComponent
|
|||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
const styles: any = {
|
const styles = {
|
||||||
width: "1.75rem",
|
width: "1.75rem",
|
||||||
height: "1.75rem",
|
height: "1.75rem",
|
||||||
color: this.iconColor,
|
color: this.iconColor,
|
||||||
@ -142,12 +136,6 @@ export class HeaderIcon extends LabelComponent
|
|||||||
borderRadius: "0.25rem"
|
borderRadius: "0.25rem"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.role == "topLeftInsideCard")
|
|
||||||
{
|
|
||||||
styles["order"] = -1;
|
|
||||||
styles["marginRight"] = "0.5rem";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.iconPath)
|
if (this.iconPath)
|
||||||
{
|
{
|
||||||
return (<Box sx={{textAlign: "center", ...styles}}><img src={this.iconPath} width="16" height="16" /></Box>);
|
return (<Box sx={{textAlign: "center", ...styles}}><img src={this.iconPath} width="16" height="16" /></Box>);
|
||||||
@ -235,22 +223,22 @@ export class Dropdown extends LabelComponent
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
const localStorageOption = JSON.parse(localStorage.getItem(localStorageKey));
|
const localStorageOption = JSON.parse(localStorage.getItem(localStorageKey));
|
||||||
if (localStorageOption)
|
if(localStorageOption)
|
||||||
{
|
{
|
||||||
const id = localStorageOption.id;
|
const id = localStorageOption.id;
|
||||||
for (let i = 0; i < this.options.length; i++)
|
for (let i = 0; i < this.options.length; i++)
|
||||||
{
|
{
|
||||||
if (this.options[i].id == id)
|
if (this.options[i].id == id)
|
||||||
{
|
{
|
||||||
defaultValue = this.options[i];
|
defaultValue = this.options[i]
|
||||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e)
|
catch(e)
|
||||||
{
|
{
|
||||||
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e);
|
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +249,7 @@ export class Dropdown extends LabelComponent
|
|||||||
{
|
{
|
||||||
for (let i = 0; i < this.options.length; i++)
|
for (let i = 0; i < this.options.length; i++)
|
||||||
{
|
{
|
||||||
if (this.options[i].id == this.dropdownDefaultValue)
|
if(this.options[i].id == this.dropdownDefaultValue)
|
||||||
{
|
{
|
||||||
defaultValue = this.options[i];
|
defaultValue = this.options[i];
|
||||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||||
@ -329,13 +317,11 @@ export class ReloadControl extends LabelComponent
|
|||||||
|
|
||||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||||
{
|
{
|
||||||
return (<Typography key={1} variant="body2" py={0} px={0} display="inline" position="relative" top="-0.25rem">
|
return (
|
||||||
<Tooltip title="Refresh">
|
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.175rem">
|
||||||
<Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}>
|
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon sx={{color: colors.gray.main, fontSize: 1.125}}>refresh</Icon></Button></Tooltip>
|
||||||
<Icon sx={{color: colors.gray.main, fontSize: 1.125}}>refresh</Icon>
|
</Typography>
|
||||||
</Button>
|
);
|
||||||
</Tooltip>
|
|
||||||
</Typography>);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,31 +336,15 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
{
|
{
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [dropdownData, setDropdownData] = useState([]);
|
const [dropdownData, setDropdownData] = useState([]);
|
||||||
|
const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
|
||||||
const [reloading, setReloading] = useState(false);
|
const [reloading, setReloading] = useState(false);
|
||||||
const [dropdownDataJSON, setDropdownDataJSON] = useState("");
|
const [dropdownDataJSON, setDropdownDataJSON] = useState("");
|
||||||
const [labelComponentsLeft, setLabelComponentsLeft] = useState([] as LabelComponent[]);
|
const [labelComponentsLeft, setLabelComponentsLeft] = useState([] as LabelComponent[]);
|
||||||
const [labelComponentsRight, setLabelComponentsRight] = useState([] as LabelComponent[]);
|
const [labelComponentsRight, setLabelComponentsRight] = useState([] as LabelComponent[]);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// support for using widget (data) label as page header, w/o it disappearing if dropdowns are changed //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const [lastSeenLabel, setLastSeenLabel] = useState("");
|
|
||||||
const [usingLabelAsTitle, setUsingLabelAsTitle] = useState(false);
|
|
||||||
|
|
||||||
const {helpHelpActive} = useContext(QContext);
|
|
||||||
|
|
||||||
function renderComponent(component: LabelComponent, componentIndex: number)
|
function renderComponent(component: LabelComponent, componentIndex: number)
|
||||||
{
|
{
|
||||||
if (component && component.render)
|
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
|
||||||
{
|
|
||||||
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log("Request to render a null component or component without a render function...");
|
|
||||||
console.log(JSON.stringify(component));
|
|
||||||
return (<></>);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -401,7 +371,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
// for initial render, put right-components from props into the state variable //
|
// for initial render, put right-components from props into the state variable //
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
const stateLabelComponentsRight = [] as LabelComponent[];
|
const stateLabelComponentsRight = [] as LabelComponent[];
|
||||||
// console.log(`${props.widgetMetaData.name} initiating right-components`);
|
// console.log(`${props.widgetMetaData.name} init'ing right-components`);
|
||||||
if (props.labelAdditionalComponentsRight)
|
if (props.labelAdditionalComponentsRight)
|
||||||
{
|
{
|
||||||
props.labelAdditionalComponentsRight.map((component) => stateLabelComponentsRight.push(component));
|
props.labelAdditionalComponentsRight.map((component) => stateLabelComponentsRight.push(component));
|
||||||
@ -435,14 +405,11 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
{
|
{
|
||||||
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
|
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
|
||||||
let defaultValue = null;
|
let defaultValue = null;
|
||||||
if (props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
|
if(props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
|
||||||
{
|
{
|
||||||
defaultValue = props.widgetData.dropdownDefaultValueList[index];
|
defaultValue = props.widgetData.dropdownDefaultValueList[index];
|
||||||
}
|
}
|
||||||
if (props.widgetData?.dropdownLabelList && props.widgetData?.dropdownLabelList[index] && props.widgetMetaData?.dropdowns && props.widgetMetaData?.dropdowns[index] && props.widgetData?.dropdownNameList && props.widgetData?.dropdownNameList[index])
|
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
|
||||||
{
|
|
||||||
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
setLabelComponentsRight(updatedStateLabelComponentsRight);
|
setLabelComponentsRight(updatedStateLabelComponentsRight);
|
||||||
}
|
}
|
||||||
@ -533,35 +500,18 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExportClick = () =>
|
const toggleFullScreenWidget = () =>
|
||||||
{
|
{
|
||||||
if (props.widgetData?.csvData)
|
if (fullScreenWidgetClassName)
|
||||||
{
|
{
|
||||||
const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
|
setFullScreenWidgetClassName("");
|
||||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
|
||||||
HtmlUtils.download(fileName, csv);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
alert("There is no data available to export.");
|
setFullScreenWidgetClassName("fullScreenWidget");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// add the export button to the label's left elements, if the meta-data says to show it //
|
|
||||||
// don't do this for 2 types which themselves add the button (and have custom code to do the export) //
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
let localLabelAdditionalElementsLeft = [...props.labelAdditionalElementsLeft];
|
|
||||||
if (props.widgetMetaData?.showExportButton && props.widgetMetaData?.type !== "table" && props.widgetMetaData?.type !== "childRecordList")
|
|
||||||
{
|
|
||||||
if (!localLabelAdditionalElementsLeft)
|
|
||||||
{
|
|
||||||
localLabelAdditionalElementsLeft = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
localLabelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
||||||
|
|
||||||
const isSet = (v: any): boolean =>
|
const isSet = (v: any): boolean =>
|
||||||
@ -576,129 +526,85 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
if (hasPermission)
|
if (hasPermission)
|
||||||
{
|
{
|
||||||
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
||||||
needLabelBox ||= (localLabelAdditionalElementsLeft && localLabelAdditionalElementsLeft.length > 0);
|
needLabelBox ||= (props.labelAdditionalElementsLeft && props.labelAdditionalElementsLeft.length > 0);
|
||||||
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
||||||
needLabelBox ||= isSet(props.widgetData?.icon);
|
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
||||||
needLabelBox ||= isSet(props.widgetData?.label);
|
needLabelBox ||= isSet(props.widgetData?.label);
|
||||||
needLabelBox ||= isSet(props.widgetMetaData?.label);
|
needLabelBox ||= isSet(props.widgetMetaData?.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// first look for a label in the widget data, which would override that in the metadata //
|
// first look for a label in the widget data, which would override that in the metadata //
|
||||||
|
// note - previously this had a ?: and one was pl={2}, the other was pl={3}... //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const isParentWidget = props.widgetMetaData.type == "parentWidget"; // todo - do we need to know top-level parent, vs. a nested parent?
|
const labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
|
||||||
let labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
|
|
||||||
|
|
||||||
if (!labelToUse)
|
|
||||||
{
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// prevent the label from disappearing, especially when it's being used as the page header //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
if (lastSeenLabel && isParentWidget && usingLabelAsTitle)
|
|
||||||
{
|
|
||||||
labelToUse = lastSeenLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let labelElement = (
|
let labelElement = (
|
||||||
<Typography sx={{cursor: "default", pl: "auto", fontWeight: 600}} variant={isParentWidget && (props.widgetData.isLabelPageTitle || usingLabelAsTitle) ? "h3" : "h6"} display="inline">
|
<Typography sx={{cursor: "default", pl: "auto", pt: props.widgetMetaData.type == "parentWidget" ? "1rem" : "auto", fontWeight: 600}} variant="h6" display="inline">
|
||||||
{labelToUse}
|
{labelToUse}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
let sublabelElement = (
|
if (props.widgetMetaData.tooltip)
|
||||||
<Box height="20px">
|
|
||||||
<Typography sx={{position: "relative", top: "-18px"}} variant="caption">
|
|
||||||
{props.widgetData?.sublabel}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (labelToUse && labelToUse != lastSeenLabel)
|
|
||||||
{
|
{
|
||||||
setLastSeenLabel(labelToUse);
|
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||||
setUsingLabelAsTitle(props.widgetData.isLabelPageTitle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const helpRoles = ["ALL_SCREENS"]
|
|
||||||
const slotName = "label";
|
|
||||||
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
|
|
||||||
|
|
||||||
if(showHelp)
|
|
||||||
{
|
|
||||||
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
|
|
||||||
labelElement = <Tooltip title={formattedHelpContent} arrow={true} placement="bottom-start">{labelElement}</Tooltip>;
|
|
||||||
}
|
|
||||||
else if (props.widgetMetaData?.tooltip)
|
|
||||||
{
|
|
||||||
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={true} placement="bottom-start">{labelElement}</Tooltip>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTable = props.widgetMetaData.type == "table";
|
|
||||||
|
|
||||||
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
||||||
const widgetContent =
|
const widgetContent =
|
||||||
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||||
{
|
{
|
||||||
needLabelBox &&
|
needLabelBox &&
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"}>
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"}>
|
||||||
<Box display="flex" flexDirection="column">
|
<Box>
|
||||||
<Box display="flex" alignItems="baseline">
|
{
|
||||||
{
|
hasPermission ?
|
||||||
hasPermission ?
|
props.widgetMetaData?.icon && (
|
||||||
props.widgetMetaData?.icon && (
|
<Box ml={1} mr={2} mt={-4} sx={{
|
||||||
<Box ml={1} mr={2} mt={-4} sx={{
|
display: "flex",
|
||||||
display: "flex",
|
justifyContent: "center",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
alignItems: "center",
|
width: "64px",
|
||||||
width: "64px",
|
height: "64px",
|
||||||
height: "64px",
|
borderRadius: "8px",
|
||||||
borderRadius: "8px",
|
background: colors.info.main,
|
||||||
background: colors.info.main,
|
color: "#ffffff",
|
||||||
color: "#ffffff",
|
float: "left"
|
||||||
float: "left"
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Icon fontSize="medium" color="inherit">
|
||||||
<Icon fontSize="medium" color="inherit">
|
{props.widgetMetaData.icon}
|
||||||
{props.widgetMetaData.icon}
|
</Icon>
|
||||||
</Icon>
|
</Box>
|
||||||
</Box>
|
) :
|
||||||
) :
|
(
|
||||||
(
|
<Box ml={3} mt={-4} sx={{
|
||||||
<Box ml={3} mt={-4} sx={{
|
display: "flex",
|
||||||
display: "flex",
|
justifyContent: "center",
|
||||||
justifyContent: "center",
|
alignItems: "center",
|
||||||
alignItems: "center",
|
width: "64px",
|
||||||
width: "64px",
|
height: "64px",
|
||||||
height: "64px",
|
borderRadius: "8px",
|
||||||
borderRadius: "8px",
|
background: colors.info.main,
|
||||||
background: colors.info.main,
|
color: "#ffffff",
|
||||||
color: "#ffffff",
|
float: "left"
|
||||||
float: "left"
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Icon fontSize="medium" color="inherit">lock</Icon>
|
||||||
<Icon fontSize="medium" color="inherit">lock</Icon>
|
</Box>
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
hasPermission && labelToUse && (labelElement)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
hasPermission && (
|
|
||||||
labelComponentsLeft.map((component, i) =>
|
|
||||||
{
|
|
||||||
return (<React.Fragment key={i}>{renderComponent(component, i)}</React.Fragment>);
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{localLabelAdditionalElementsLeft}
|
{
|
||||||
</Box>
|
hasPermission && labelToUse && (labelElement)
|
||||||
<Box display="flex">
|
}
|
||||||
{
|
{
|
||||||
hasPermission && props.widgetData?.sublabel && (sublabelElement)
|
hasPermission && (
|
||||||
}
|
labelComponentsLeft.map((component, i) =>
|
||||||
</Box>
|
{
|
||||||
|
return (<span key={i}>{renderComponent(component, i)}</span>);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{props.labelAdditionalElementsLeft}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
@ -744,27 +650,17 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
!errorLoading && props?.footerHTML && (
|
!errorLoading && props?.footerHTML && (
|
||||||
<Box mt={isTable ? "36px" : 1} ml={isTable ? 0 : 3} mr={isTable ? 0 : 3} mb={isTable ? "-12px" : 2} sx={{fontWeight: 300, color: "#7b809a", display: "flex", alignContent: "flex-end", fontSize: "14px"}}>{parse(props.footerHTML)}</Box>
|
<Box mt={1} ml={3} mr={3} mb={2} sx={{fontWeight: 300, color: "#7b809a", display: "flex", alignContent: "flex-end", fontSize: "14px"}}>{parse(props.footerHTML)}</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|
||||||
const padding = props.omitPadding ? "auto" : "24px 16px";
|
const padding = props.omitPadding ? "auto" : "24px 16px";
|
||||||
|
|
||||||
///////////////////////////////////////////////////
|
|
||||||
// try to make tables fill their entire "parent" //
|
|
||||||
///////////////////////////////////////////////////
|
|
||||||
let noCardMarginBottom = "unset";
|
|
||||||
if (isTable)
|
|
||||||
{
|
|
||||||
noCardMarginBottom = "-8px";
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.widgetMetaData?.isCard
|
return props.widgetMetaData?.isCard
|
||||||
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%", p: padding}} className="widget inCard">
|
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%", p: padding}} className={fullScreenWidgetClassName}>
|
||||||
{widgetContent}
|
{widgetContent}
|
||||||
</Card>
|
</Card>
|
||||||
: <span style={{width: "100%", padding: padding, marginBottom: noCardMarginBottom}} className="widget noCard">{widgetContent}</span>;
|
: <span style={{width: "100%", padding: padding}}>{widgetContent}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Widget;
|
export default Widget;
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
|
||||||
import {Alert, Skeleton} from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
|
||||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
import DividerBlock from "qqq/components/widgets/blocks/DividerBlock";
|
|
||||||
import NumberIconBadgeBlock from "qqq/components/widgets/blocks/NumberIconBadgeBlock";
|
|
||||||
import ProgressBarBlock from "qqq/components/widgets/blocks/ProgressBarBlock";
|
|
||||||
import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRowDetailRowBlock";
|
|
||||||
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
|
||||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
|
||||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
|
||||||
|
|
||||||
|
|
||||||
interface WidgetBlockProps
|
|
||||||
{
|
|
||||||
widgetMetaData: QWidgetMetaData;
|
|
||||||
block: BlockData;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Component to render a single Block in the widget framework!
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element
|
|
||||||
{
|
|
||||||
if(!block)
|
|
||||||
{
|
|
||||||
return (<Skeleton />);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!block.values)
|
|
||||||
{
|
|
||||||
block.values = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!block.styles)
|
|
||||||
{
|
|
||||||
block.styles = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if(block.blockTypeName == "COMPOSITE")
|
|
||||||
{
|
|
||||||
// @ts-ignore - special case for composite type block...
|
|
||||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(block.blockTypeName)
|
|
||||||
{
|
|
||||||
case "TEXT":
|
|
||||||
return (<TextBlock widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
case "NUMBER_ICON_BADGE":
|
|
||||||
return (<NumberIconBadgeBlock widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
case "UP_OR_DOWN_NUMBER":
|
|
||||||
return (<UpOrDownNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
case "TABLE_SUB_ROW_DETAIL_ROW":
|
|
||||||
return (<TableSubRowDetailRowBlock widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
case "PROGRESS_BAR":
|
|
||||||
return (<ProgressBarBlock widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
case "DIVIDER":
|
|
||||||
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
case "BIG_NUMBER":
|
|
||||||
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
|
||||||
default:
|
|
||||||
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Icon from "@mui/material/Icon";
|
|
||||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import React from "react";
|
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
|
||||||
import {WidgetData} from "qqq/components/widgets/Widget";
|
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Utility class used by Widgets
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
export class WidgetUtils
|
|
||||||
{
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public static generateExportButton = (onExportClick: () => void): JSX.Element =>
|
|
||||||
{
|
|
||||||
return (<Typography key={1} variant="body2" py={0} px={0} display="inline" position="relative" top="-0.25rem">
|
|
||||||
<Tooltip title="Export">
|
|
||||||
<Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}>
|
|
||||||
<Icon sx={{color: colors.gray.main, fontSize: 1.125}}>save_alt</Icon>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public static widgetCsvDataToString = (data: WidgetData): string =>
|
|
||||||
{
|
|
||||||
function isNumeric(x: any)
|
|
||||||
{
|
|
||||||
return !isNaN(Number(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
let csv = "";
|
|
||||||
for (let i = 0; i < data.csvData.length; i++)
|
|
||||||
{
|
|
||||||
for (let j = 0; j < data.csvData[i].length; j++)
|
|
||||||
{
|
|
||||||
if (j > 0)
|
|
||||||
{
|
|
||||||
csv += ",";
|
|
||||||
}
|
|
||||||
|
|
||||||
let cell = data.csvData[i][j];
|
|
||||||
if (cell && isNumeric(String(cell)))
|
|
||||||
{
|
|
||||||
csv += cell;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
csv += `"${ValueUtils.cleanForCsv(cell)}"`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
csv += "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (csv);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public static makeExportFileName = (data: WidgetData, widgetMetaData: QWidgetMetaData): string =>
|
|
||||||
{
|
|
||||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
|
||||||
return (fileName);
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
|
||||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Block that renders ... a big number, optionally with some other stuff.
|
|
||||||
**
|
|
||||||
** ${heading}
|
|
||||||
** ${number} ${context}
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function BigNumberBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
|
||||||
{
|
|
||||||
let flexJustifyContent = "normal";
|
|
||||||
let flexAlignItems = "baseline";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{width: data.styles.width ?? "auto"}}>
|
|
||||||
|
|
||||||
<div style={{fontWeight: "700", fontSize: "0.875rem", color: "#3D3D3D", marginBottom: "-0.5rem"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="heading">
|
|
||||||
<span>{data.values.heading}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{display: "flex", alignItems: flexAlignItems, justifyContent: flexJustifyContent}}>
|
|
||||||
|
|
||||||
<div style={{display: "flex", alignItems: "baseline"}}>
|
|
||||||
<div style={{fontWeight: "700", fontSize: "2rem", marginRight: "0.25rem"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="number">
|
|
||||||
<span style={{color: data.styles.numberColor}}>{data.values.number}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
data.values.context &&
|
|
||||||
<div style={{fontWeight: "500", fontSize: "0.875rem", color: "#7b809a"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="context">
|
|
||||||
<span>{data.values.context}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {Tooltip} from "@mui/material";
|
|
||||||
import React, {ReactElement} from "react";
|
|
||||||
import {Link} from "react-router-dom";
|
|
||||||
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
|
|
||||||
interface BlockElementWrapperProps
|
|
||||||
{
|
|
||||||
data: BlockData;
|
|
||||||
slot: string
|
|
||||||
linkProps?: any;
|
|
||||||
children: ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** For Blocks - wrap their "slot" elements with an optional tooltip and/or link
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function BlockElementWrapper({data, slot, linkProps, children}: BlockElementWrapperProps): JSX.Element
|
|
||||||
{
|
|
||||||
let link: BlockLink;
|
|
||||||
let tooltip: BlockTooltip;
|
|
||||||
|
|
||||||
if(slot)
|
|
||||||
{
|
|
||||||
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
|
||||||
if(!link)
|
|
||||||
{
|
|
||||||
link = data.link;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
|
||||||
if(!tooltip)
|
|
||||||
{
|
|
||||||
tooltip = data.tooltip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
link = data.link;
|
|
||||||
tooltip = data.tooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rs = children;
|
|
||||||
|
|
||||||
if(link)
|
|
||||||
{
|
|
||||||
rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link>
|
|
||||||
}
|
|
||||||
|
|
||||||
if(tooltip)
|
|
||||||
{
|
|
||||||
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom"
|
|
||||||
|
|
||||||
// @ts-ignore - placement possible values
|
|
||||||
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (rs);
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
|
||||||
|
|
||||||
|
|
||||||
export interface BlockData
|
|
||||||
{
|
|
||||||
blockTypeName: string;
|
|
||||||
|
|
||||||
tooltip?: BlockTooltip;
|
|
||||||
link?: BlockLink;
|
|
||||||
tooltipMap?: {[slot: string]: BlockTooltip};
|
|
||||||
linkMap?: {[slot: string]: BlockLink};
|
|
||||||
|
|
||||||
values: any;
|
|
||||||
styles?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface BlockTooltip
|
|
||||||
{
|
|
||||||
title: string;
|
|
||||||
placement: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface BlockLink
|
|
||||||
{
|
|
||||||
href: string;
|
|
||||||
target: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface StandardBlockComponentProps
|
|
||||||
{
|
|
||||||
widgetMetaData: QWidgetMetaData;
|
|
||||||
data: BlockData;
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Block that renders a simple dividing line
|
|
||||||
** margins & width are set such that it covers the padding of a card.
|
|
||||||
** if we need to use it differently, a style attribute should be added to its backend data.
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function DividerBlock({}: StandardBlockComponentProps): JSX.Element
|
|
||||||
{
|
|
||||||
return (<div style={{margin: "1rem -1rem", width: "calc(100% + 2rem)", borderBottom: "1px solid #E0E0E0"}} />);
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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";
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Block that renders ... a number, and an icon, like a badge.
|
|
||||||
**
|
|
||||||
** ${number} ${icon}
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function NumberIconBadgeBlock({data}: StandardBlockComponentProps): JSX.Element
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<div style={{display: "inline-block", whiteSpace: "nowrap", color: data.styles.color}}>
|
|
||||||
{
|
|
||||||
data.values.number &&
|
|
||||||
<BlockElementWrapper data={data} slot="number">
|
|
||||||
<span style={{color: data.styles.color, fontSize: "0.875rem"}}>{data.values.number}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
data.values.iconName &&
|
|
||||||
<BlockElementWrapper data={data} slot="icon">
|
|
||||||
<Icon style={{color: data.styles.color, fontSize: "1rem", position: "relative", top: "3px"}}>{data.values.iconName}</Icon>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
}
|
|
||||||
</div>);
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 Typography from "@mui/material/Typography";
|
|
||||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
|
||||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Block that renders a progress bar!
|
|
||||||
**
|
|
||||||
** Values:
|
|
||||||
** ${heading}
|
|
||||||
** [${percent}===___] ${value ?? percent}
|
|
||||||
**
|
|
||||||
** Slots:
|
|
||||||
** ${heading}
|
|
||||||
** ${bar} ${value}
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function ProgressBarBlock({data}: StandardBlockComponentProps): JSX.Element
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<Typography component="div" variant="button" color="text" fontWeight="light" sx={{textTransform: "none"}}>
|
|
||||||
{
|
|
||||||
data.values.heading &&
|
|
||||||
<div style={{marginBottom: "0.25rem", fontWeight: 500, color: "#3D3D3D"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="heading">
|
|
||||||
<span>{data.values.heading}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div style={{display: "flex", alignItems: "center", marginBottom: "0.75rem"}}>
|
|
||||||
|
|
||||||
<BlockElementWrapper data={data} slot="bar" linkProps={{style: {width: "100%"}}}>
|
|
||||||
<div style={{background: "#E0E0E0", width: "100%", borderRadius: "0.5rem", height: "1rem"}}>
|
|
||||||
{
|
|
||||||
data.values.percent > 0 ? <div style={{background: data.styles.barColor ?? "#0062ff", minWidth: "1rem", width: `${data.values.percent}%`, borderRadius: "0.5rem", height: "1rem"}}></div> : <></>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
|
|
||||||
<div style={{width: "60px", textAlign: "right", fontWeight: 600, color: "#3D3D3D"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="value">
|
|
||||||
<span>{data.values.value ?? `${(data.values.percent as number).toFixed(1)}%`}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</Typography>);
|
|
||||||
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
|
||||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Block that renders a label & value, meant to be used as a detail-row in a
|
|
||||||
** sub-row within a table widget
|
|
||||||
**
|
|
||||||
** ${label} ${value}
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function TableSubRowDetailRowBlock({data}: StandardBlockComponentProps): JSX.Element
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<div style={{display: "flex", maxWidth: "calc(100% - 24px)", justifyContent: "space-between"}}>
|
|
||||||
|
|
||||||
{
|
|
||||||
data.values.label &&
|
|
||||||
<div style={{overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="label">
|
|
||||||
<span style={{color: data.styles.labelColor}}>{data.values.label}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
data.values.value &&
|
|
||||||
<BlockElementWrapper data={data} slot="value">
|
|
||||||
<span style={{color: data.styles.valueColor}}>{data.values.value}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
|
||||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Block that renders ... just some text.
|
|
||||||
**
|
|
||||||
** ${text}
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function TextBlock({data}: StandardBlockComponentProps): JSX.Element
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
<BlockElementWrapper data={data} slot="">
|
|
||||||
<span>{data.values.text}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 React from "react";
|
|
||||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
|
||||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Block that renders an up/down icon, a number, and some context
|
|
||||||
**
|
|
||||||
** ${icon} ${number} ${context}
|
|
||||||
*
|
|
||||||
** or, if style.isStacked:
|
|
||||||
*
|
|
||||||
** ${icon} ${number}
|
|
||||||
** ${context}
|
|
||||||
*******************************************************************************/
|
|
||||||
export default function UpOrDownNumberBlock({data}: StandardBlockComponentProps): JSX.Element
|
|
||||||
{
|
|
||||||
if (!data.styles)
|
|
||||||
{
|
|
||||||
data.styles = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.values)
|
|
||||||
{
|
|
||||||
data.values = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const UP_ICON = "arrow_drop_up";
|
|
||||||
const DOWN_ICON = "arrow_drop_down";
|
|
||||||
|
|
||||||
const defaultGreenColor = "#2BA83F";
|
|
||||||
const defaultRedColor = "#FB4141";
|
|
||||||
|
|
||||||
const goodOrBadColor = data.styles.colorOverride ?? (data.values.isGood ? defaultGreenColor : defaultRedColor);
|
|
||||||
const iconName = data.values.isUp ? UP_ICON : DOWN_ICON;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline"}}>
|
|
||||||
|
|
||||||
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="number">
|
|
||||||
<>
|
|
||||||
<Icon sx={{color: goodOrBadColor, alignSelf: "flex-end", fontSize: "2.25rem !important", lineHeight: "0.875rem", height: "1rem", width: "2rem",}}>{iconName}</Icon>
|
|
||||||
<span style={{color: goodOrBadColor}}>{data.values.number}</span>
|
|
||||||
</>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{fontWeight: 500, fontSize: "0.875rem", color: "#7b809a", marginLeft: "0.25rem"}}>
|
|
||||||
<BlockElementWrapper data={data} slot="context">
|
|
||||||
<span>{data.values.context}</span>
|
|
||||||
</BlockElementWrapper>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -39,79 +39,65 @@ ChartJS.register(
|
|||||||
Legend
|
Legend
|
||||||
);
|
);
|
||||||
|
|
||||||
export const makeOptions = (data: DefaultChartData) =>
|
export const options = {
|
||||||
{
|
maintainAspectRatio: false,
|
||||||
return({
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
animation: {
|
||||||
responsive: true,
|
duration: 0
|
||||||
animation: {
|
},
|
||||||
duration: 0
|
elements: {
|
||||||
},
|
bar: {
|
||||||
elements: {
|
borderRadius: 4
|
||||||
bar: {
|
}
|
||||||
borderRadius: 4
|
},
|
||||||
}
|
plugins: {
|
||||||
},
|
tooltip: {
|
||||||
onHover: function (event: any, elements: any[], chart: any)
|
// todo - some configs around this
|
||||||
{
|
callbacks: {
|
||||||
if(event.type == "mousemove" && elements.length > 0 && data.urls && data.urls.length > elements[0].index && data.urls[elements[0].index])
|
title: function(context: any)
|
||||||
{
|
{
|
||||||
chart.canvas.style.cursor = "pointer";
|
return ("");
|
||||||
}
|
},
|
||||||
else
|
label: function(context: any)
|
||||||
{
|
{
|
||||||
chart.canvas.style.cursor = "default";
|
if(context.dataset.label.startsWith(context.label))
|
||||||
}
|
{
|
||||||
},
|
return `${context.label}: ${context.formattedValue}`;
|
||||||
plugins: {
|
}
|
||||||
tooltip: {
|
else
|
||||||
// todo - some configs around this
|
|
||||||
callbacks: {
|
|
||||||
title: function(context: any)
|
|
||||||
{
|
{
|
||||||
return ("");
|
return ("");
|
||||||
},
|
|
||||||
label: function(context: any)
|
|
||||||
{
|
|
||||||
if(context.dataset.label.startsWith(context.label))
|
|
||||||
{
|
|
||||||
return `${context.label}: ${context.formattedValue}`;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
position: "bottom",
|
|
||||||
labels: {
|
|
||||||
usePointStyle: true,
|
|
||||||
pointStyle: "circle",
|
|
||||||
boxHeight: 6,
|
|
||||||
boxWidth: 6,
|
|
||||||
padding: 12,
|
|
||||||
font: {
|
|
||||||
size: 14
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
legend: {
|
||||||
x: {
|
position: "bottom",
|
||||||
stacked: true,
|
labels: {
|
||||||
grid: {display: false},
|
usePointStyle: true,
|
||||||
ticks: {autoSkip: false, maxRotation: 90}
|
pointStyle: "circle",
|
||||||
},
|
boxHeight: 6,
|
||||||
y: {
|
boxWidth: 6,
|
||||||
stacked: true,
|
padding: 12,
|
||||||
position: "right",
|
font: {
|
||||||
ticks: {precision: 0}
|
size: 14
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
grid: {display: false},
|
||||||
|
ticks: {autoSkip: false, maxRotation: 90}
|
||||||
},
|
},
|
||||||
});
|
y: {
|
||||||
}
|
stacked: true,
|
||||||
|
position: "right",
|
||||||
|
ticks: {precision: 0}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -165,7 +151,7 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
|||||||
<Box>
|
<Box>
|
||||||
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||||
<Box width="100%" height="300px">
|
<Box width="100%" height="300px">
|
||||||
<Bar data={data} options={makeOptions(data)} getElementsAtEvent={handleClick} />
|
<Bar data={data} options={options} getElementsAtEvent={handleClick} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;
|
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;
|
||||||
|
@ -70,7 +70,7 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
|
|||||||
chartData.dataset.backgroundColors = chartColors;
|
chartData.dataset.backgroundColors = chartColors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {}, chartData?.dataset?.urls);
|
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
@ -23,7 +23,7 @@ import colors from "qqq/assets/theme/base/colors";
|
|||||||
|
|
||||||
const {gradients, dark} = colors;
|
const {gradients, dark} = colors;
|
||||||
|
|
||||||
function configs(labels: any, datasets: any, urls: string[] | undefined)
|
function configs(labels: any, datasets: any)
|
||||||
{
|
{
|
||||||
const backgroundColors = [];
|
const backgroundColors = [];
|
||||||
|
|
||||||
@ -66,17 +66,6 @@ function configs(labels: any, datasets: any, urls: string[] | undefined)
|
|||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
onHover: function (event: any, elements: any[], chart: any)
|
|
||||||
{
|
|
||||||
if(event.type == "mousemove" && elements.length > 0 && urls && urls.length > elements[0].index && urls[elements[0].index])
|
|
||||||
{
|
|
||||||
chart.canvas.style.cursor = "pointer";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
chart.canvas.style.cursor = "default";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
@ -178,7 +178,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
|||||||
if(widgetMetaData?.showExportButton)
|
if(widgetMetaData?.showExportButton)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(
|
labelAdditionalElementsLeft.push(
|
||||||
<Typography key={1} variant="body2" px={0} display="inline" position="relative">
|
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative">
|
||||||
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.25}}>save_alt</Icon></Button></Tooltip>
|
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.25}}>save_alt</Icon></Button></Tooltip>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
|
||||||
import {tooltipClasses, TooltipProps} from "@mui/material";
|
import {tooltipClasses, TooltipProps} from "@mui/material";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
@ -30,19 +29,17 @@ import TableContainer from "@mui/material/TableContainer";
|
|||||||
import TableRow from "@mui/material/TableRow";
|
import TableRow from "@mui/material/TableRow";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {useEffect, useMemo, useState} from "react";
|
import {useEffect, useMemo, useState} from "react";
|
||||||
import {useAsyncDebounce, useExpanded, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
|
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} from "react-table";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDInput from "qqq/components/legacy/MDInput";
|
import MDInput from "qqq/components/legacy/MDInput";
|
||||||
import MDPagination from "qqq/components/legacy/MDPagination";
|
import MDPagination from "qqq/components/legacy/MDPagination";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
|
||||||
import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell";
|
import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell";
|
||||||
import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell";
|
import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell";
|
||||||
import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
|
import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
|
||||||
import ImageCell from "qqq/components/widgets/tables/cells/ImageCell";
|
import ImageCell from "qqq/components/widgets/tables/cells/ImageCell";
|
||||||
import {TableDataInput} from "qqq/components/widgets/tables/TableCard";
|
import {TableDataInput} from "qqq/components/widgets/tables/TableCard";
|
||||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -60,7 +57,6 @@ interface Props
|
|||||||
};
|
};
|
||||||
isSorted?: boolean;
|
isSorted?: boolean;
|
||||||
noEndBorder?: boolean;
|
noEndBorder?: boolean;
|
||||||
widgetMetaData: QWidgetMetaData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable.defaultProps = {
|
DataTable.defaultProps = {
|
||||||
@ -96,7 +92,6 @@ function DataTable({
|
|||||||
pagination,
|
pagination,
|
||||||
isSorted,
|
isSorted,
|
||||||
noEndBorder,
|
noEndBorder,
|
||||||
widgetMetaData
|
|
||||||
}: Props): JSX.Element
|
}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
let defaultValue: any;
|
let defaultValue: any;
|
||||||
@ -173,17 +168,6 @@ function DataTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(table.columnHeaderTooltips)
|
|
||||||
{
|
|
||||||
for (let column of columnsToMemo)
|
|
||||||
{
|
|
||||||
if(table.columnHeaderTooltips[column.accessor])
|
|
||||||
{
|
|
||||||
column.tooltip = table.columnHeaderTooltips[column.accessor];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = useMemo<any>(() => columnsToMemo, [table]);
|
const columns = useMemo<any>(() => columnsToMemo, [table]);
|
||||||
const data = useMemo<any>(() => table.rows, [table]);
|
const data = useMemo<any>(() => table.rows, [table]);
|
||||||
const gridTemplateColumns = widths.join(" ");
|
const gridTemplateColumns = widths.join(" ");
|
||||||
@ -296,36 +280,21 @@ function DataTable({
|
|||||||
entriesEnd = pageSize * (pageIndex + 1);
|
entriesEnd = pageSize * (pageIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleFooterRows = 1;
|
|
||||||
if(expanded && expanded[`${table.rows.length-1}`])
|
|
||||||
{
|
|
||||||
//////////////////////////////////////////////////
|
|
||||||
// todo - should count how many are expanded... //
|
|
||||||
//////////////////////////////////////////////////
|
|
||||||
visibleFooterRows = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTable(includeHead: boolean, rows: any, isFooter: boolean)
|
function getTable(includeHead: boolean, rows: any, isFooter: boolean)
|
||||||
{
|
{
|
||||||
let boxStyle = {};
|
let boxStyle = {};
|
||||||
if(fixedStickyLastRow)
|
if(fixedStickyLastRow)
|
||||||
{
|
{
|
||||||
boxStyle = isFooter
|
boxStyle = isFooter
|
||||||
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
|
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, overflow: "auto", scrollbarGutter: "stable"}
|
||||||
: {flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
: {height: fixedHeight ? `${fixedHeight}px` : "360px", overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
||||||
}
|
}
|
||||||
|
|
||||||
let innerBoxStyle = {};
|
return <Box sx={boxStyle}>
|
||||||
if(fixedStickyLastRow && isFooter)
|
|
||||||
{
|
|
||||||
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
|
|
||||||
<Table {...getTableProps()}>
|
<Table {...getTableProps()}>
|
||||||
{
|
{
|
||||||
includeHead && (
|
includeHead && (
|
||||||
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
|
<Box component="thead" sx={{position: "sticky", top: 0, background: "white"}}>
|
||||||
{headerGroups.map((headerGroup: any, i: number) => (
|
{headerGroups.map((headerGroup: any, i: number) => (
|
||||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", gridTemplateColumns: gridTemplateColumns}}>
|
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", gridTemplateColumns: gridTemplateColumns}}>
|
||||||
{headerGroup.headers.map((column: any) => (
|
{headerGroup.headers.map((column: any) => (
|
||||||
@ -335,7 +304,6 @@ function DataTable({
|
|||||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||||
align={column.align ? column.align : "left"}
|
align={column.align ? column.align : "left"}
|
||||||
sorted={setSortedValue(column)}
|
sorted={setSortedValue(column)}
|
||||||
tooltip={column.tooltip}
|
|
||||||
>
|
>
|
||||||
{column.render("header")}
|
{column.render("header")}
|
||||||
</DataTableHeadCell>
|
</DataTableHeadCell>
|
||||||
@ -373,23 +341,13 @@ function DataTable({
|
|||||||
overrideNoEndBorder = true;
|
overrideNoEndBorder = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let background = "initial";
|
|
||||||
if(isFooter)
|
|
||||||
{
|
|
||||||
background = "#EEEEEE";
|
|
||||||
}
|
|
||||||
else if(row.depth > 0 || row.isExpanded)
|
|
||||||
{
|
|
||||||
background = "#FAFAFA";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: background}} key={key} {...row.getRowProps()}>
|
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: (row.depth > 0 ? "#FAFAFA" : "initial")}} key={key} {...row.getRowProps()}>
|
||||||
{row.cells.map((cell: any) => (
|
{row.cells.map((cell: any) => (
|
||||||
cell.column.type !== "hidden" && (
|
cell.column.type !== "hidden" && (
|
||||||
<DataTableBodyCell
|
<DataTableBodyCell
|
||||||
key={key}
|
key={key}
|
||||||
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
noBorder={noEndBorder || overrideNoEndBorder}
|
||||||
depth={row.depth}
|
depth={row.depth}
|
||||||
align={cell.column.align ? cell.column.align : "left"}
|
align={cell.column.align ? cell.column.align : "left"}
|
||||||
{...cell.getCellProps()}
|
{...cell.getCellProps()}
|
||||||
@ -414,21 +372,7 @@ function DataTable({
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "html" && (
|
cell.column.type === "html" && (
|
||||||
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
<DefaultCell isFooter={isFooter}>{parse(cell.value)}</DefaultCell>
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
cell.column.type === "composite" && (
|
|
||||||
<DefaultCell isFooter={isFooter}>
|
|
||||||
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
|
||||||
</DefaultCell>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
cell.column.type === "block" && (
|
|
||||||
<DefaultCell isFooter={isFooter}>
|
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
|
||||||
</DefaultCell>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@ -453,11 +397,11 @@ function DataTable({
|
|||||||
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box></Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer sx={{boxShadow: "none", height: fixedHeight ? `${fixedHeight}px` : "auto"}}>
|
<TableContainer sx={{boxShadow: "none"}}>
|
||||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||||
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
||||||
@ -504,16 +448,14 @@ function DataTable({
|
|||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Box display="flex" justifyContent="space-between" flexDirection="column" height="100%">
|
{
|
||||||
{
|
fixedStickyLastRow ? (
|
||||||
fixedStickyLastRow ? (
|
<>
|
||||||
<>
|
{getTable(true, page.slice(0, page.length -1), false)}
|
||||||
{getTable(true, page.slice(0, page.length - visibleFooterRows), false)}
|
{getTable(false, page.slice(page.length-1), true)}
|
||||||
{getTable(false, page.slice(page.length - visibleFooterRows), true)}
|
</>
|
||||||
</>
|
) : getTable(true, page, false)
|
||||||
) : getTable(true, page, false)
|
}
|
||||||
}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
|
||||||
import {Skeleton} from "@mui/material";
|
import {Skeleton} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Table from "@mui/material/Table";
|
import Table from "@mui/material/Table";
|
||||||
@ -43,7 +42,6 @@ import Client from "qqq/utils/qqq/Client";
|
|||||||
export interface TableDataInput
|
export interface TableDataInput
|
||||||
{
|
{
|
||||||
columns: { [key: string]: any }[];
|
columns: { [key: string]: any }[];
|
||||||
columnHeaderTooltips?: { [columnName: string]: string | JSX.Element }
|
|
||||||
rows: { [key: string]: any }[];
|
rows: { [key: string]: any }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,11 +57,10 @@ interface Props
|
|||||||
fixedStickyLastRow?: boolean;
|
fixedStickyLastRow?: boolean;
|
||||||
fixedHeight?: number;
|
fixedHeight?: number;
|
||||||
data: TableDataInput;
|
data: TableDataInput;
|
||||||
widgetMetaData: QWidgetMetaData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight, widgetMetaData}: Props): JSX.Element
|
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||||
|
|
||||||
@ -77,7 +74,7 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="tableCard" mx={-2} mb="-28px" pt="11px" pb="0.25rem">
|
<Box py={1} mx={-2}>
|
||||||
{
|
{
|
||||||
data && data.columns && !noRowsFoundHTML ?
|
data && data.columns && !noRowsFoundHTML ?
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -88,10 +85,9 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
|||||||
fixedHeight={fixedHeight}
|
fixedHeight={fixedHeight}
|
||||||
showTotalEntries={false}
|
showTotalEntries={false}
|
||||||
isSorted={false}
|
isSorted={false}
|
||||||
widgetMetaData={widgetMetaData}
|
|
||||||
/>
|
/>
|
||||||
: noRowsFoundHTML ?
|
: noRowsFoundHTML ?
|
||||||
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
<Box p={3} pt={1} pb={1} sx={{textAlign: "center"}}>
|
||||||
<MDTypography
|
<MDTypography
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
@ -21,14 +21,16 @@
|
|||||||
|
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {htmlToText} from "html-to-text";
|
import {htmlToText} from "html-to-text";
|
||||||
import React, {useContext, useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import QContext from "QContext";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
|
||||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||||
import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
||||||
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
|
||||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
@ -48,7 +50,6 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
||||||
const [csv, setCsv] = useState(null as string);
|
const [csv, setCsv] = useState(null as string);
|
||||||
const [fileName, setFileName] = useState(null as string);
|
const [fileName, setFileName] = useState(null as string);
|
||||||
const {helpHelpActive} = useContext(QContext);
|
|
||||||
|
|
||||||
const rows = props.widgetData?.rows;
|
const rows = props.widgetData?.rows;
|
||||||
const columns = props.widgetData?.columns;
|
const columns = props.widgetData?.columns;
|
||||||
@ -104,7 +105,7 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
|
|
||||||
setCsv(csv);
|
setCsv(csv);
|
||||||
|
|
||||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||||
setFileName(fileName)
|
setFileName(fileName)
|
||||||
|
|
||||||
console.log(`useEffect, setting fileName ${fileName}`);
|
console.log(`useEffect, setting fileName ${fileName}`);
|
||||||
@ -114,13 +115,7 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
|
|
||||||
const onExportClick = () =>
|
const onExportClick = () =>
|
||||||
{
|
{
|
||||||
if(props.widgetData?.csvData)
|
if(csv)
|
||||||
{
|
|
||||||
const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
|
|
||||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
|
||||||
HtmlUtils.download(fileName, csv);
|
|
||||||
}
|
|
||||||
else if(csv)
|
|
||||||
{
|
{
|
||||||
HtmlUtils.download(fileName, csv);
|
HtmlUtils.download(fileName, csv);
|
||||||
}
|
}
|
||||||
@ -133,24 +128,11 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||||
if(props.widgetMetaData?.showExportButton)
|
if(props.widgetMetaData?.showExportButton)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
|
labelAdditionalElementsLeft.push(
|
||||||
}
|
<Typography key={1} variant="body2" py={2} px={0} display="inline" position="relative" top="-0.25rem">
|
||||||
|
<Tooltip title="Export"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}><Icon sx={{color: colors.gray.main, fontSize: 1.125}}>save_alt</Icon></Button></Tooltip>
|
||||||
//////////////////////////////////////////////////////
|
</Typography>
|
||||||
// look for column-header tooltips from helpContent //
|
);
|
||||||
//////////////////////////////////////////////////////
|
|
||||||
const columnHeaderTooltips: {[columnName: string]: JSX.Element} = {}
|
|
||||||
for (let column of props.widgetData?.columns ?? [])
|
|
||||||
{
|
|
||||||
const helpRoles = ["ALL_SCREENS"]
|
|
||||||
const slotName = `columnHeader=${column.accessor}`;
|
|
||||||
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
|
|
||||||
|
|
||||||
if(showHelp)
|
|
||||||
{
|
|
||||||
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
|
|
||||||
columnHeaderTooltips[column.accessor] = formattedHelpContent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -168,8 +150,7 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
||||||
fixedStickyLastRow={props.widgetData?.fixedStickyLastRow}
|
fixedStickyLastRow={props.widgetData?.fixedStickyLastRow}
|
||||||
fixedHeight={props.widgetData?.fixedHeight}
|
fixedHeight={props.widgetData?.fixedHeight}
|
||||||
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows, columnHeaderTooltips: columnHeaderTooltips}}
|
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
|
||||||
widgetMetaData={props.widgetMetaData}
|
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
@ -39,7 +39,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
|||||||
component="td"
|
component="td"
|
||||||
textAlign={align}
|
textAlign={align}
|
||||||
py={1.5}
|
py={1.5}
|
||||||
px={1.5}
|
px={3}
|
||||||
sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({
|
sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({
|
||||||
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {Theme} from "@mui/material/styles";
|
import {Theme} from "@mui/material/styles";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {useMaterialUIController} from "qqq/context";
|
import {useMaterialUIController} from "qqq/context";
|
||||||
@ -34,10 +33,9 @@ interface Props
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
sorted?: false | "none" | "asce" | "desc";
|
sorted?: false | "none" | "asce" | "desc";
|
||||||
align?: "left" | "right" | "center";
|
align?: "left" | "right" | "center";
|
||||||
tooltip?: string | JSX.Element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: Props): JSX.Element
|
function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [controller] = useMaterialUIController();
|
const [controller] = useMaterialUIController();
|
||||||
const {darkMode} = controller;
|
const {darkMode} = controller;
|
||||||
@ -47,7 +45,7 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
|
|||||||
component="th"
|
component="th"
|
||||||
width={width}
|
width={width}
|
||||||
py={1.5}
|
py={1.5}
|
||||||
px={1.5}
|
px={3}
|
||||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||||
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||||
"&:nth-child(1)": {
|
"&:nth-child(1)": {
|
||||||
@ -75,43 +73,39 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
|
|||||||
userSelect: sorted && "none",
|
userSelect: sorted && "none",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<>
|
{children}
|
||||||
{
|
{sorted && (
|
||||||
tooltip ? <Tooltip title={tooltip}><span style={{cursor: "default"}}>{children}</span></Tooltip> : children
|
<Box
|
||||||
}
|
position="absolute"
|
||||||
{sorted && (
|
top={0}
|
||||||
|
right={align !== "right" ? "16px" : 0}
|
||||||
|
left={align === "right" ? "-5px" : "unset"}
|
||||||
|
sx={({typography: {size}}: any) => ({
|
||||||
|
fontSize: size.lg,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
sx={{
|
||||||
top={0}
|
position: "absolute",
|
||||||
right={align !== "right" ? "16px" : 0}
|
top: -6,
|
||||||
left={align === "right" ? "-5px" : "unset"}
|
color: sorted === "asce" ? "text" : "secondary",
|
||||||
sx={({typography: {size}}: any) => ({
|
opacity: sorted === "asce" ? 1 : 0.5
|
||||||
fontSize: size.lg,
|
}}
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Icon>arrow_drop_up</Icon>
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: -6,
|
|
||||||
color: sorted === "asce" ? "text" : "secondary",
|
|
||||||
opacity: sorted === "asce" ? 1 : 0.5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon>arrow_drop_up</Icon>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
color: sorted === "desc" ? "text" : "secondary",
|
|
||||||
opacity: sorted === "desc" ? 1 : 0.5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon>arrow_drop_down</Icon>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
<Box
|
||||||
</>
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
color: sorted === "desc" ? "text" : "secondary",
|
||||||
|
opacity: sorted === "desc" ? 1 : 0.5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon>arrow_drop_down</Icon>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -75,17 +75,9 @@ function AppHome({app}: Props): JSX.Element
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const mdbMetaData = app?.supplementalAppMetaData?.get("materialDashboard");
|
|
||||||
let showAppLabelOnHomeScreen = true;
|
|
||||||
if(mdbMetaData)
|
|
||||||
{
|
|
||||||
showAppLabelOnHomeScreen = mdbMetaData.showAppLabelOnHomeScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
// setPageHeader(app.label);
|
setPageHeader(app.label);
|
||||||
setPageHeader(null);
|
|
||||||
|
|
||||||
if (!qInstance)
|
if (!qInstance)
|
||||||
{
|
{
|
||||||
@ -216,12 +208,6 @@ function AppHome({app}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
{
|
|
||||||
showAppLabelOnHomeScreen &&
|
|
||||||
<Typography textTransform="capitalize" variant="h3">
|
|
||||||
{app.label}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12} lg={12}>
|
<Grid item xs={12} lg={12}>
|
||||||
<Card sx={{overflow: "visible"}}>
|
<Card sx={{overflow: "visible"}}>
|
||||||
@ -267,19 +253,13 @@ function AppHome({app}: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
{
|
|
||||||
showAppLabelOnHomeScreen &&
|
|
||||||
<Typography textTransform="capitalize" variant="h3">
|
|
||||||
{app.label}
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
<Box>
|
<Box>
|
||||||
{app.widgets && app.widgets.length > 0 && (
|
{app.widgets && app.widgets.length > 0 && (
|
||||||
<Box pb={app.sections ? 2.375 : 4} pt={"0.5rem"}>
|
<Box pb={app.sections ? 2.375 : 0}>
|
||||||
<DashboardWidgets widgetMetaDataList={widgets} />
|
<DashboardWidgets widgetMetaDataList={widgets} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Grid container spacing={3} mt={"-1rem"}>
|
<Grid container spacing={3}>
|
||||||
{
|
{
|
||||||
app.sections ? (
|
app.sections ? (
|
||||||
<Grid item xs={12} lg={12}>
|
<Grid item xs={12} lg={12}>
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||||
@ -147,11 +146,6 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData);
|
const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData);
|
||||||
const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, null, null, "bySection");
|
const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, null, null, "bySection");
|
||||||
|
|
||||||
if(fieldMetaData.type == QFieldType.DATE_TIME)
|
|
||||||
{
|
|
||||||
columns[0].headerName = fieldMetaData.label + " (grouped by hour)"
|
|
||||||
}
|
|
||||||
|
|
||||||
columns.forEach((c) =>
|
columns.forEach((c) =>
|
||||||
{
|
{
|
||||||
c.filterable = false;
|
c.filterable = false;
|
||||||
|
@ -168,7 +168,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
// define some default values (e.g., to be used if nothing in local storage or no active view) //
|
// define some default values (e.g., to be used if nothing in local storage or no active view) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let defaultSort = [] as GridSortItem[];
|
let defaultSort = [] as GridSortItem[];
|
||||||
let defaultRowsPerPage = 50;
|
let defaultRowsPerPage = 10;
|
||||||
let defaultDensity = "standard" as GridDensity;
|
let defaultDensity = "standard" as GridDensity;
|
||||||
let defaultTableVariant: QTableVariant = null;
|
let defaultTableVariant: QTableVariant = null;
|
||||||
let defaultMode = "basic";
|
let defaultMode = "basic";
|
||||||
@ -610,14 +610,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
updateTable("'r' keyboard event");
|
updateTable("'r' keyboard event");
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
// disable until we add a ... ref down to let us programmatically open Columns button
|
|
||||||
else if (! e.metaKey && e.key === "c")
|
else if (! e.metaKey && e.key === "c")
|
||||||
{
|
{
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns)
|
gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
else if (! e.metaKey && e.key === "f")
|
else if (! e.metaKey && e.key === "f")
|
||||||
{
|
{
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -1383,18 +1380,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
if (selectFullFilterState === "filter")
|
if (selectFullFilterState === "filter")
|
||||||
{
|
{
|
||||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(queryFilter))}`;
|
||||||
filterForBackend.skip = 0;
|
|
||||||
filterForBackend.limit = null;
|
|
||||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectFullFilterState === "filterSubset")
|
if (selectFullFilterState === "filterSubset")
|
||||||
{
|
{
|
||||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(queryFilter))}`;
|
||||||
filterForBackend.skip = 0;
|
|
||||||
filterForBackend.limit = selectionSubsetSize;
|
|
||||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedIds.length > 0)
|
if (selectedIds.length > 0)
|
||||||
@ -1414,17 +1405,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
if (selectFullFilterState === "filter")
|
if (selectFullFilterState === "filter")
|
||||||
{
|
{
|
||||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
setRecordIdsForProcess(queryFilter);
|
||||||
filterForBackend.skip = 0;
|
|
||||||
filterForBackend.limit = null;
|
|
||||||
setRecordIdsForProcess(filterForBackend);
|
|
||||||
}
|
}
|
||||||
else if (selectFullFilterState === "filterSubset")
|
else if (selectFullFilterState === "filterSubset")
|
||||||
{
|
{
|
||||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
setRecordIdsForProcess(new QQueryFilter(queryFilter.criteria, queryFilter.orderBys, queryFilter.booleanOperator, 0, selectionSubsetSize));
|
||||||
filterForBackend.skip = 0;
|
|
||||||
filterForBackend.limit = selectionSubsetSize;
|
|
||||||
setRecordIdsForProcess(filterForBackend);
|
|
||||||
}
|
}
|
||||||
else if (selectedIds.length > 0)
|
else if (selectedIds.length > 0)
|
||||||
{
|
{
|
||||||
@ -2112,32 +2097,20 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
if(selectedIndex == 0)
|
if(selectedIndex == 0)
|
||||||
{
|
{
|
||||||
///////////////
|
|
||||||
// this page //
|
|
||||||
///////////////
|
|
||||||
programmaticallySelectSomeOrAllRows();
|
programmaticallySelectSomeOrAllRows();
|
||||||
setSelectFullFilterState("checked")
|
setSelectFullFilterState("checked")
|
||||||
}
|
}
|
||||||
else if(selectedIndex == 1)
|
else if(selectedIndex == 1)
|
||||||
{
|
{
|
||||||
///////////////////////
|
|
||||||
// full query result //
|
|
||||||
///////////////////////
|
|
||||||
programmaticallySelectSomeOrAllRows();
|
programmaticallySelectSomeOrAllRows();
|
||||||
setSelectFullFilterState("filter")
|
setSelectFullFilterState("filter")
|
||||||
}
|
}
|
||||||
else if(selectedIndex == 2)
|
else if(selectedIndex == 2)
|
||||||
{
|
{
|
||||||
////////////////////////////
|
|
||||||
// subset of query result //
|
|
||||||
////////////////////////////
|
|
||||||
setSelectionSubsetSizePromptOpen(true);
|
setSelectionSubsetSizePromptOpen(true);
|
||||||
}
|
}
|
||||||
else if(selectedIndex == 3)
|
else if(selectedIndex == 3)
|
||||||
{
|
{
|
||||||
/////////////////////
|
|
||||||
// clear selection //
|
|
||||||
/////////////////////
|
|
||||||
setSelectFullFilterState("n/a")
|
setSelectFullFilterState("n/a")
|
||||||
setRowSelectionModel([]);
|
setRowSelectionModel([]);
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
|
@ -64,6 +64,7 @@ class FilterUtils
|
|||||||
let rs = [];
|
let rs = [];
|
||||||
for (let i = 0; i < param.length; i++)
|
for (let i = 0; i < param.length; i++)
|
||||||
{
|
{
|
||||||
|
console.log(param[i]);
|
||||||
if (param[i] && param[i].id && param[i].label)
|
if (param[i] && param[i].id && param[i].label)
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////
|
||||||
@ -318,7 +319,7 @@ class FilterUtils
|
|||||||
** get the values associated with a criteria as a string, e.g., for showing
|
** get the values associated with a criteria as a string, e.g., for showing
|
||||||
** in a tooltip.
|
** in a tooltip.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3, andMoreFormat: "andNOther" | "+N" = "andNOther"): string
|
public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3): string
|
||||||
{
|
{
|
||||||
let valuesString = "";
|
let valuesString = "";
|
||||||
|
|
||||||
@ -339,10 +340,6 @@ class FilterUtils
|
|||||||
{
|
{
|
||||||
maxLoops = maxValuesToShow;
|
maxLoops = maxValuesToShow;
|
||||||
}
|
}
|
||||||
else if(maxValuesToShow == 1 && criteria.values.length > 1)
|
|
||||||
{
|
|
||||||
maxLoops = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < maxLoops; i++)
|
for (let i = 0; i < maxLoops; i++)
|
||||||
{
|
{
|
||||||
@ -360,12 +357,7 @@ class FilterUtils
|
|||||||
else if (value.type == "ThisOrLastPeriod")
|
else if (value.type == "ThisOrLastPeriod")
|
||||||
{
|
{
|
||||||
const expression = new ThisOrLastPeriodExpression(value);
|
const expression = new ThisOrLastPeriodExpression(value);
|
||||||
let startOfPrefix = "";
|
labels.push(expression.toString());
|
||||||
if(fieldMetaData.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
|
|
||||||
{
|
|
||||||
startOfPrefix = "start of ";
|
|
||||||
}
|
|
||||||
labels.push(`${startOfPrefix}${expression.toString()}`);
|
|
||||||
}
|
}
|
||||||
else if(fieldMetaData.type == QFieldType.BOOLEAN)
|
else if(fieldMetaData.type == QFieldType.BOOLEAN)
|
||||||
{
|
{
|
||||||
@ -391,16 +383,7 @@ class FilterUtils
|
|||||||
|
|
||||||
if (maxLoops < criteria.values.length)
|
if (maxLoops < criteria.values.length)
|
||||||
{
|
{
|
||||||
const n = criteria.values.length - maxLoops;
|
labels.push(" and " + (criteria.values.length - maxLoops) + " other values.");
|
||||||
switch (andMoreFormat)
|
|
||||||
{
|
|
||||||
case "andNOther":
|
|
||||||
labels.push(` and ${n} other value${n == 1 ? "" : "s"}.`);
|
|
||||||
break;
|
|
||||||
case "+N":
|
|
||||||
labels[labels.length-1] += ` +${n}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
valuesString = (labels.join(", "));
|
valuesString = (labels.join(", "));
|
||||||
|
@ -29,7 +29,6 @@ import java.util.Objects;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -505,33 +504,7 @@ public class QSeleniumLib
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public WebElement waitForSelectorContaining(String cssSelector, String textContains)
|
public WebElement waitForSelectorContaining(String cssSelector, String textContains)
|
||||||
{
|
{
|
||||||
return (waitForSelectorMatchingPredicate(cssSelector, "containing text [" + textContains + "]", (WebElement element) ->
|
LOG.debug("Waiting for element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||||
{
|
|
||||||
return (element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public WebElement waitForSelectorContainingTextMatchingRegex(String cssSelector, String regExp)
|
|
||||||
{
|
|
||||||
return (waitForSelectorMatchingPredicate(cssSelector, "matching regexp [" + regExp + "]", (WebElement element) ->
|
|
||||||
{
|
|
||||||
return (element.getText() != null && element.getText().matches(regExp));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private WebElement waitForSelectorMatchingPredicate(String cssSelector, String description, Function<WebElement, Boolean> predicate)
|
|
||||||
{
|
|
||||||
LOG.debug("Waiting for element matching selector [" + cssSelector + "] " + description);
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
do
|
do
|
||||||
@ -541,9 +514,9 @@ public class QSeleniumLib
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(predicate.apply(element))
|
if(element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()))
|
||||||
{
|
{
|
||||||
LOG.debug("Found element matching selector [" + cssSelector + "] " + description);
|
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||||
Actions actions = new Actions(driver);
|
Actions actions = new Actions(driver);
|
||||||
actions.moveToElement(element);
|
actions.moveToElement(element);
|
||||||
conditionallyAutoHighlight(element);
|
conditionallyAutoHighlight(element);
|
||||||
@ -564,7 +537,7 @@ public class QSeleniumLib
|
|||||||
}
|
}
|
||||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
|
|
||||||
fail("Failed to find element matching selector [" + cssSelector + "] " + description + " after [" + WAIT_SECONDS + "] seconds.");
|
fail("Failed to find element matching selector [" + cssSelector + "] containing text [" + textContains + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,9 +22,6 @@
|
|||||||
package com.kingsrook.qqq.frontend.materialdashboard.selenium.lib;
|
package com.kingsrook.qqq.frontend.materialdashboard.selenium.lib;
|
||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.Keys;
|
import org.openqa.selenium.Keys;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
@ -106,7 +103,7 @@ public class QueryScreenLib
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void clickFilterBuilderButton()
|
public void clickFilterButton()
|
||||||
{
|
{
|
||||||
qSeleniumLib.waitForSelectorContaining("BUTTON", "FILTER BUILDER").click();
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "FILTER BUILDER").click();
|
||||||
}
|
}
|
||||||
@ -184,11 +181,10 @@ public class QueryScreenLib
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void addAdvancedQueryFilterInput(int index, String fieldLabel, String operator, String value, String booleanOperator)
|
public void addAdvancedQueryFilterInput(QSeleniumLib qSeleniumLib, int index, String fieldLabel, String operator, String value, String booleanOperator)
|
||||||
{
|
{
|
||||||
if(index > 0)
|
if(index > 0)
|
||||||
{
|
{
|
||||||
@ -225,91 +221,10 @@ public class QueryScreenLib
|
|||||||
operatorInput.sendKeys("\n");
|
operatorInput.sendKeys("\n");
|
||||||
qSeleniumLib.waitForMillis(100);
|
qSeleniumLib.waitForMillis(100);
|
||||||
|
|
||||||
if(StringUtils.hasContent(value))
|
WebElement valueInput = subFormForField.findElement(By.cssSelector(".filterValuesColumn INPUT"));
|
||||||
{
|
valueInput.click();
|
||||||
WebElement valueInput = subFormForField.findElement(By.cssSelector(".filterValuesColumn INPUT"));
|
valueInput.sendKeys(value);
|
||||||
valueInput.click();
|
qSeleniumLib.waitForMillis(100);
|
||||||
valueInput.sendKeys(value);
|
|
||||||
qSeleniumLib.waitForMillis(100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void addBasicFilter(String fieldLabel)
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelectorContaining("BUTTON", "Add Filter").click();
|
|
||||||
qSeleniumLib.waitForSelectorContaining(".fieldListMenuBody-addQuickFilter LI", fieldLabel).click();
|
|
||||||
qSeleniumLib.clickBackdrop();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void setBasicFilter(String fieldLabel, String operatorLabel, String value)
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelectorContaining("BUTTON", fieldLabel).click();
|
|
||||||
qSeleniumLib.waitForMillis(250);
|
|
||||||
qSeleniumLib.waitForSelector("#criteriaOperator").click();
|
|
||||||
qSeleniumLib.waitForSelectorContaining("LI", operatorLabel).click();
|
|
||||||
|
|
||||||
if(StringUtils.hasContent(value))
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelector(".filterValuesColumn INPUT").click();
|
|
||||||
// todo - no, not in a listbox/LI here...
|
|
||||||
qSeleniumLib.waitForSelectorContaining(".MuiAutocomplete-listbox LI", value).click();
|
|
||||||
System.out.println(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
qSeleniumLib.clickBackdrop();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void setBasicFilterPossibleValues(String fieldLabel, String operatorLabel, List<String> values)
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelectorContaining("BUTTON", fieldLabel).click();
|
|
||||||
qSeleniumLib.waitForMillis(250);
|
|
||||||
qSeleniumLib.waitForSelector("#criteriaOperator").click();
|
|
||||||
qSeleniumLib.waitForSelectorContaining("LI", operatorLabel).click();
|
|
||||||
|
|
||||||
if(CollectionUtils.nullSafeHasContents(values))
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelector(".filterValuesColumn INPUT").click();
|
|
||||||
for(String value : values)
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelectorContaining(".MuiAutocomplete-listbox LI", value).click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qSeleniumLib.clickBackdrop();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void waitForAdvancedQueryStringMatchingRegex(String regEx)
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelectorContainingTextMatchingRegex(".advancedQueryString", regEx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void waitForBasicFilterButtonMatchingRegex(String regEx)
|
|
||||||
{
|
|
||||||
qSeleniumLib.waitForSelectorContainingTextMatchingRegex("BUTTON", regEx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QSeleniumLib;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QSeleniumLib;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -285,6 +284,7 @@ public class QSeleniumJavalin
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
|
// LOG.debug(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
|
||||||
for(CapturedContext context : captured)
|
for(CapturedContext context : captured)
|
||||||
{
|
{
|
||||||
if(context.getPath().equals(path))
|
if(context.getPath().equals(path))
|
||||||
@ -301,7 +301,6 @@ public class QSeleniumJavalin
|
|||||||
}
|
}
|
||||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
|
|
||||||
LOG.debug(" captured paths: \n " + captured.stream().map(cc -> cc.getPath() + "[" + cc.getBody() + "]").collect(Collectors.joining("\n ")));
|
|
||||||
fail("Failed to capture a request for path [" + path + "] with body containing [" + bodyContaining + "] after [" + WAIT_SECONDS + "] seconds.");
|
fail("Failed to capture a request for path [" + path + "] with body containing [" + bodyContaining + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.assertFilterButtonBadge(1);
|
queryScreenLib.assertFilterButtonBadge(1);
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
qSeleniumLib.waitForSelector("input[value=\"is not empty\"]");
|
qSeleniumLib.waitForSelector("input[value=\"is not empty\"]");
|
||||||
|
|
||||||
///////////////////////////////
|
///////////////////////////////
|
||||||
@ -93,7 +93,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.assertFilterButtonBadge(1);
|
queryScreenLib.assertFilterButtonBadge(1);
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
qSeleniumLib.waitForSelector("input[value=\"is between\"]");
|
qSeleniumLib.waitForSelector("input[value=\"is between\"]");
|
||||||
qSeleniumLib.waitForSelector("input[value=\"1701\"]");
|
qSeleniumLib.waitForSelector("input[value=\"1701\"]");
|
||||||
qSeleniumLib.waitForSelector("input[value=\"74656\"]");
|
qSeleniumLib.waitForSelector("input[value=\"74656\"]");
|
||||||
@ -116,7 +116,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.assertFilterButtonBadge(1);
|
queryScreenLib.assertFilterButtonBadge(1);
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
qSeleniumLib.waitForSelector("input[value=\"is any of\"]");
|
qSeleniumLib.waitForSelector("input[value=\"is any of\"]");
|
||||||
qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "St. Louis");
|
qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "St. Louis");
|
||||||
qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "Chesterfield");
|
qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "Chesterfield");
|
||||||
@ -129,7 +129,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.assertFilterButtonBadge(1);
|
queryScreenLib.assertFilterButtonBadge(1);
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
qSeleniumLib.waitForSelector("input[value=\"is after\"]");
|
qSeleniumLib.waitForSelector("input[value=\"is after\"]");
|
||||||
qSeleniumLib.waitForSelector("input[value=\"5 days ago\"]");
|
qSeleniumLib.waitForSelector("input[value=\"5 days ago\"]");
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.assertFilterButtonBadge(2);
|
queryScreenLib.assertFilterButtonBadge(2);
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
qSeleniumLib.waitForSelector("input[value=\"is at or before\"]");
|
qSeleniumLib.waitForSelector("input[value=\"is at or before\"]");
|
||||||
qSeleniumLib.waitForSelector("input[value=\"start of this year\"]");
|
qSeleniumLib.waitForSelector("input[value=\"start of this year\"]");
|
||||||
qSeleniumLib.waitForSelector("input[value=\"starts with\"]");
|
qSeleniumLib.waitForSelector("input[value=\"starts with\"]");
|
||||||
@ -165,7 +165,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.assertFilterButtonBadge(1);
|
queryScreenLib.assertFilterButtonBadge(1);
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
qSeleniumLib.waitForSelector("input[value=\"does not equal\"]");
|
qSeleniumLib.waitForSelector("input[value=\"does not equal\"]");
|
||||||
qSeleniumLib.waitForSelector("input[value=\"St. Louis\"]");
|
qSeleniumLib.waitForSelector("input[value=\"St. Louis\"]");
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
|
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
|
||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QQQMaterialDashboardSelectors;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QQQMaterialDashboardSelectors;
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QueryScreenLib;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QueryScreenLib;
|
||||||
@ -49,7 +48,6 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||||
.withRouteToFile("/data/person/query", "data/person/index.json")
|
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||||
.withRouteToFile("/data/person/variants", "data/person/variants.json")
|
.withRouteToFile("/data/person/variants", "data/person/variants.json")
|
||||||
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
|
|
||||||
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
|
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,13 +64,13 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.gotoAdvancedMode();
|
queryScreenLib.gotoAdvancedMode();
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
// open the filter window, enter a value, wait for query to re-run //
|
// open the filter window, enter a value, wait for query to re-run //
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
qSeleniumJavalin.beginCapture();
|
qSeleniumJavalin.beginCapture();
|
||||||
queryScreenLib.addAdvancedQueryFilterInput(0, "Id", "equals", "1", null);
|
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 0, "Id", "equals", "1", null);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////
|
||||||
// assert that query & count both have the expected filter value //
|
// assert that query & count both have the expected filter value //
|
||||||
@ -119,11 +117,11 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
queryScreenLib.gotoAdvancedMode();
|
queryScreenLib.gotoAdvancedMode();
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
queryScreenLib.clickFilterButton();
|
||||||
|
|
||||||
qSeleniumJavalin.beginCapture();
|
qSeleniumJavalin.beginCapture();
|
||||||
queryScreenLib.addAdvancedQueryFilterInput(0, "First Name", "contains", "Dar", "Or");
|
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 0, "First Name", "contains", "Dar", "Or");
|
||||||
queryScreenLib.addAdvancedQueryFilterInput(1, "First Name", "contains", "Jam", "Or");
|
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
|
||||||
|
|
||||||
String expectedFilterContents0 = """
|
String expectedFilterContents0 = """
|
||||||
{"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";
|
{"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";
|
||||||
@ -139,138 +137,6 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testBasicBooleanOperators()
|
|
||||||
{
|
|
||||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
|
||||||
|
|
||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
|
||||||
|
|
||||||
queryScreenLib.addBasicFilter("Is Employed");
|
|
||||||
|
|
||||||
testBasicCriteria(queryScreenLib, "Is Employed", "equals yes", null, "(?s).*Is Employed:.*yes.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"EQUALS","values":[true]}""");
|
|
||||||
|
|
||||||
testBasicCriteria(queryScreenLib, "Is Employed", "equals no", null, "(?s).*Is Employed:.*no.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"EQUALS","values":[false]}""");
|
|
||||||
|
|
||||||
testBasicCriteria(queryScreenLib, "Is Employed", "is empty", null, "(?s).*Is Employed:.*is empty.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"IS_BLANK","values":[]}""");
|
|
||||||
|
|
||||||
testBasicCriteria(queryScreenLib, "Is Employed", "is not empty", null, "(?s).*Is Employed:.*is not empty.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"IS_NOT_BLANK","values":[]}""");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testBasicPossibleValues()
|
|
||||||
{
|
|
||||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
|
||||||
|
|
||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
|
||||||
|
|
||||||
String field = "Home City";
|
|
||||||
queryScreenLib.addBasicFilter(field);
|
|
||||||
|
|
||||||
testBasicCriteriaPossibleValues(queryScreenLib, field, "is any of", List.of("St. Louis", "Chesterfield"), "(?s).*" + field + ":.*St. Louis.*\\+1.*", """
|
|
||||||
{"fieldName":"homeCityId","operator":"IN","values":[1,2]}""");
|
|
||||||
|
|
||||||
testBasicCriteriaPossibleValues(queryScreenLib, field, "equals", List.of("Chesterfield"), "(?s).*" + field + ":.*Chesterfield.*", """
|
|
||||||
{"fieldName":"homeCityId","operator":"EQUALS","values":[2]}""");
|
|
||||||
|
|
||||||
testBasicCriteriaPossibleValues(queryScreenLib, field, "is empty", null, "(?s).*" + field + ":.*is empty.*", """
|
|
||||||
{"fieldName":"homeCityId","operator":"IS_BLANK","values":[]}""");
|
|
||||||
|
|
||||||
testBasicCriteriaPossibleValues(queryScreenLib, field, "does not equal", List.of("St. Louis"), "(?s).*" + field + ":.*does not equal.*St. Louis.*", """
|
|
||||||
{"fieldName":"homeCityId","operator":"NOT_EQUALS_OR_IS_NULL","values":[1]}""");
|
|
||||||
|
|
||||||
testBasicCriteriaPossibleValues(queryScreenLib, field, "is none of", List.of("Chesterfield"), "(?s).*" + field + ":.*is none of.*St. Louis.*\\+1", """
|
|
||||||
{"fieldName":"homeCityId","operator":"NOT_IN","values":[1,2]}""");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private void testBasicCriteria(QueryScreenLib queryScreenLib, String fieldLabel, String operatorLabel, String value, String expectButtonStringRegex, String expectFilterJsonContains)
|
|
||||||
{
|
|
||||||
qSeleniumJavalin.beginCapture();
|
|
||||||
queryScreenLib.setBasicFilter(fieldLabel, operatorLabel, value);
|
|
||||||
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
|
|
||||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
|
|
||||||
qSeleniumJavalin.endCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private void testBasicCriteriaPossibleValues(QueryScreenLib queryScreenLib, String fieldLabel, String operatorLabel, List<String> values, String expectButtonStringRegex, String expectFilterJsonContains)
|
|
||||||
{
|
|
||||||
qSeleniumJavalin.beginCapture();
|
|
||||||
queryScreenLib.setBasicFilterPossibleValues(fieldLabel, operatorLabel, values);
|
|
||||||
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
|
|
||||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
|
|
||||||
qSeleniumJavalin.endCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testAdvancedBooleanOperators()
|
|
||||||
{
|
|
||||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
|
||||||
|
|
||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
|
||||||
queryScreenLib.waitForQueryToHaveRan();
|
|
||||||
|
|
||||||
queryScreenLib.gotoAdvancedMode();
|
|
||||||
|
|
||||||
testAdvancedCriteria(queryScreenLib, "Is Employed", "equals yes", null, "(?s).*Is Employed.*equals yes.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"EQUALS","values":[true]}""");
|
|
||||||
|
|
||||||
testAdvancedCriteria(queryScreenLib, "Is Employed", "equals no", null, "(?s).*Is Employed.*equals no.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"EQUALS","values":[false]}""");
|
|
||||||
|
|
||||||
testAdvancedCriteria(queryScreenLib, "Is Employed", "is empty", null, "(?s).*Is Employed.*is empty.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"IS_BLANK","values":[]}""");
|
|
||||||
|
|
||||||
testAdvancedCriteria(queryScreenLib, "Is Employed", "is not empty", null, "(?s).*Is Employed.*is not empty.*", """
|
|
||||||
{"fieldName":"isEmployed","operator":"IS_NOT_BLANK","values":[]}""");
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo - table requires variant - prompt for it, choose it, see query; change variant, change on-screen, re-query
|
// todo - table requires variant - prompt for it, choose it, see query; change variant, change on-screen, re-query
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private void testAdvancedCriteria(QueryScreenLib queryScreenLib, String fieldLabel, String operatorLabel, String value, String expectQueryStringRegex, String expectFilterJsonContains)
|
|
||||||
{
|
|
||||||
qSeleniumJavalin.beginCapture();
|
|
||||||
queryScreenLib.clickFilterBuilderButton();
|
|
||||||
queryScreenLib.addAdvancedQueryFilterInput(0, fieldLabel, operatorLabel, value, null);
|
|
||||||
qSeleniumLib.clickBackdrop();
|
|
||||||
queryScreenLib.waitForAdvancedQueryStringMatchingRegex(expectQueryStringRegex);
|
|
||||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
|
|
||||||
qSeleniumJavalin.endCapture();
|
|
||||||
queryScreenLib.clickAdvancedFilterClearIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user