mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 07:08:44 +00:00
Compare commits
25 Commits
snapshot-i
...
wip/bugfix
Author | SHA1 | Date | |
---|---|---|---|
b1eba925fa | |||
e7b5821fbd | |||
aed1c9d4d0 | |||
88a4c17bbc | |||
2900cd8593 | |||
8ab0f5f549 | |||
8cffbbcac4 | |||
37eb280d79 | |||
948aee70fd | |||
f0c1af18d0 | |||
fa65d6c0ad | |||
d6c9bf79b1 | |||
677b93a09f | |||
314bf0fd67 | |||
76642f13e9 | |||
0eaf171523 | |||
b137b3346d | |||
63479ba282 | |||
967c557a58 | |||
fc45b5bed8 | |||
b6e204aa7e | |||
a5569900b4 | |||
0ca6f36bc2 | |||
7a32d20acb | |||
886eea8e88 |
@ -2,12 +2,12 @@ version: 2.1
|
||||
|
||||
orbs:
|
||||
node: circleci/node@5.1.0
|
||||
browser-tools: circleci/browser-tools@1.4.6
|
||||
browser-tools: circleci/browser-tools@1.4.7
|
||||
|
||||
executors:
|
||||
java17:
|
||||
docker:
|
||||
- image: 'cimg/openjdk:17.0'
|
||||
- image: 'cimg/openjdk:17.0.9'
|
||||
|
||||
commands:
|
||||
install_java17:
|
||||
|
@ -33,6 +33,7 @@
|
||||
"html-react-parser": "1.4.8",
|
||||
"html-to-text": "^9.0.5",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"idb": "8.0.0",
|
||||
"jwt-decode": "3.1.2",
|
||||
"rapidoc": "9.3.4",
|
||||
"react": "18.0.0",
|
||||
|
@ -354,8 +354,7 @@ const CommandMenu = ({metaData}: Props) =>
|
||||
<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}>r</span>Refresh the Query</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 item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Builder (Advanced mode only)</Grid>
|
||||
</Grid>
|
||||
|
||||
<Typography variant="h6" pt={3}>Record View</Typography>
|
||||
|
@ -398,7 +398,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
criteria.values = [];
|
||||
}
|
||||
|
||||
if(newValue.valueMode)
|
||||
if(newValue.valueMode && !newValue.implicitValues)
|
||||
{
|
||||
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
||||
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
||||
|
@ -94,6 +94,24 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
document.getElementById(`${idPrefix}${criteria.id}`).focus();
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Event handler for key-down events - specifically added here, to stop pressing
|
||||
** 'tab' in a date or date-time from closing the quick-filter...
|
||||
*******************************************************************************/
|
||||
const handleKeyDown = (e: any) =>
|
||||
{
|
||||
if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME)
|
||||
{
|
||||
if(e.code == "Tab")
|
||||
{
|
||||
console.log("Tab on date or date-time - don't close me, just move to the next sub-field!...");
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const inputProps: any = {};
|
||||
inputProps.endAdornment = (
|
||||
<InputAdornment position="end">
|
||||
@ -110,6 +128,7 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
autoComplete="off"
|
||||
type={type}
|
||||
onChange={(event) => valueChangeHandler(event, valueIndex)}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={value}
|
||||
InputLabelProps={inputLabelProps}
|
||||
InputProps={inputProps}
|
||||
|
@ -30,7 +30,7 @@ import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {SyntheticEvent, useContext, useState} from "react";
|
||||
import React, {SyntheticEvent, useContext, useReducer, useState} from "react";
|
||||
import QContext from "QContext";
|
||||
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||
import {getDefaultCriteriaValue, getOperatorOptions, getValueModeRequiredCount, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
||||
@ -148,7 +148,10 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
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 [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
||||
@ -158,6 +161,11 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
//////////////////////
|
||||
// ole' faithful... //
|
||||
//////////////////////
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -182,15 +190,30 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (criteriaParamIsCriteria(criteriaParam) && JSON.stringify(criteriaParam) !== JSON.stringify(criteria))
|
||||
{
|
||||
const newCriteria = criteriaParam as QFilterCriteriaWithId;
|
||||
setCriteria(newCriteria);
|
||||
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
||||
setOperatorSelectedValue(operatorOption);
|
||||
setOperatorInputValue(operatorOption.label);
|
||||
if(isOpen)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// this was firing too-often for case where: there was a criteria originally //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
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
|
||||
** 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 =>
|
||||
{
|
||||
@ -199,6 +222,16 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -207,7 +240,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Construct a new criteria object - resetting the values tied to the oprator
|
||||
** Construct a new criteria object - resetting the values tied to the operator
|
||||
** autocomplete at the same time.
|
||||
*******************************************************************************/
|
||||
const makeNewCriteria = (): QFilterCriteria =>
|
||||
@ -241,6 +274,11 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
*******************************************************************************/
|
||||
const closeMenu = () =>
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// when closing the menu, that's when we'll update the criteria from the caller //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
updateCriteria(criteria, false, false);
|
||||
|
||||
setIsOpen(false);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
@ -271,7 +309,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
criteria.values = [];
|
||||
}
|
||||
|
||||
if(newValue.valueMode)
|
||||
if(newValue.valueMode && !newValue.implicitValues)
|
||||
{
|
||||
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
||||
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
||||
@ -286,7 +324,8 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
setOperatorInputValue("");
|
||||
}
|
||||
|
||||
updateCriteria(criteria, false, false);
|
||||
setCriteria(criteria);
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
@ -320,7 +359,8 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
criteria.values[valueIndex] = value;
|
||||
}
|
||||
|
||||
updateCriteria(criteria, true, false);
|
||||
setCriteria(criteria);
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
@ -400,16 +440,18 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
buttonAdditionalStyles.color = "white !important";
|
||||
buttonClassName = "filterActive";
|
||||
|
||||
let valuesString = FilterUtils.getValuesString(fieldMetaData, criteria);
|
||||
if(fieldMetaData.type == QFieldType.BOOLEAN)
|
||||
let valuesString = FilterUtils.getValuesString(fieldMetaData, criteria, 1, "+N");
|
||||
|
||||
///////////////////////////////////////////
|
||||
// don't show the Equals or In operators //
|
||||
///////////////////////////////////////////
|
||||
let operatorString = (<>{operatorSelectedValue.label} </>);
|
||||
if(operatorSelectedValue.value == QCriteriaOperator.EQUALS || operatorSelectedValue.value == QCriteriaOperator.IN)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for booleans, in here, the operator-label is "equals yes" or "equals no", so we don't want the values string //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
valuesString = "";
|
||||
operatorString = (<></>)
|
||||
}
|
||||
|
||||
buttonContent = (<><span style={{fontWeight: 700}}>{buttonContent}:</span> <span style={{fontWeight: 400}}>{operatorSelectedValue.label} {valuesString}</span></>);
|
||||
buttonContent = (<><span style={{fontWeight: 700}}>{buttonContent}:</span> <span style={{fontWeight: 400}}>{operatorString}{valuesString}</span></>);
|
||||
}
|
||||
|
||||
const mouseEvents =
|
||||
@ -462,7 +504,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
//////////////////////////////
|
||||
// return the button & menu //
|
||||
//////////////////////////////
|
||||
const widthAndMaxWidth = 250
|
||||
const widthAndMaxWidth = fieldMetaData?.type == QFieldType.DATE_TIME ? 275 : 250
|
||||
return (
|
||||
<>
|
||||
{button}
|
||||
@ -478,7 +520,12 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
|
||||
<Autocomplete
|
||||
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}
|
||||
value={operatorSelectedValue as any}
|
||||
inputValue={operatorInputValue}
|
||||
|
@ -39,65 +39,79 @@ ChartJS.register(
|
||||
Legend
|
||||
);
|
||||
|
||||
export const options = {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
bar: {
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
// todo - some configs around this
|
||||
callbacks: {
|
||||
title: function(context: any)
|
||||
{
|
||||
return ("");
|
||||
},
|
||||
label: function(context: any)
|
||||
{
|
||||
if(context.dataset.label.startsWith(context.label))
|
||||
{
|
||||
return `${context.label}: ${context.formattedValue}`;
|
||||
}
|
||||
else
|
||||
export const makeOptions = (data: DefaultChartData) =>
|
||||
{
|
||||
return({
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
bar: {
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
onHover: function (event: any, elements: any[], chart: any)
|
||||
{
|
||||
if(event.type == "mousemove" && elements.length > 0 && data.urls && data.urls.length > elements[0].index && data.urls[elements[0].index])
|
||||
{
|
||||
chart.canvas.style.cursor = "pointer";
|
||||
}
|
||||
else
|
||||
{
|
||||
chart.canvas.style.cursor = "default";
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
// todo - some configs around this
|
||||
callbacks: {
|
||||
title: function(context: any)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: "circle",
|
||||
boxHeight: 6,
|
||||
boxWidth: 6,
|
||||
padding: 12,
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {display: false},
|
||||
ticks: {autoSkip: false, maxRotation: 90}
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {display: false},
|
||||
ticks: {autoSkip: false, maxRotation: 90}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
position: "right",
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
position: "right",
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -151,7 +165,7 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
<Box>
|
||||
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||
<Box width="100%" height="300px">
|
||||
<Bar data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
<Bar data={data} options={makeOptions(data)} getElementsAtEvent={handleClick} />
|
||||
</Box>
|
||||
</Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;
|
||||
|
@ -70,7 +70,7 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
|
||||
chartData.dataset.backgroundColors = chartColors;
|
||||
}
|
||||
}
|
||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {}, chartData?.dataset?.urls);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
@ -23,7 +23,7 @@ import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
const {gradients, dark} = colors;
|
||||
|
||||
function configs(labels: any, datasets: any)
|
||||
function configs(labels: any, datasets: any, urls: string[] | undefined)
|
||||
{
|
||||
const backgroundColors = [];
|
||||
|
||||
@ -66,6 +66,17 @@ function configs(labels: any, datasets: any)
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
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: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
|
@ -1089,19 +1089,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const formData = new FormData();
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
let queryStringPairsForInit = [];
|
||||
if (urlSearchParams.get("recordIds"))
|
||||
{
|
||||
const recordIdsFromQueryString = urlSearchParams.get("recordIds").split(",");
|
||||
const encodedRecordIds = recordIdsFromQueryString.map(r => encodeURIComponent(r)).join(",");
|
||||
queryStringPairsForInit.push("recordsParam=recordIds");
|
||||
queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
|
||||
formData.append("recordsParam", "recordIds")
|
||||
formData.append("recordIds", urlSearchParams.get("recordIds"))
|
||||
}
|
||||
else if (urlSearchParams.get("filterJSON"))
|
||||
{
|
||||
queryStringPairsForInit.push("recordsParam=filterJSON");
|
||||
queryStringPairsForInit.push(`filterJSON=${encodeURIComponent(urlSearchParams.get("filterJSON"))}`);
|
||||
formData.append("recordsParam", "filterJSON")
|
||||
formData.append("filterJSON", urlSearchParams.get("filterJSON"));
|
||||
}
|
||||
// todo once saved filters exist
|
||||
//else if(urlSearchParams.get("filterId")) {
|
||||
@ -1110,23 +1108,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
// }
|
||||
else if (recordIds)
|
||||
{
|
||||
if (recordIds instanceof QQueryFilter)
|
||||
// @ts-ignore - we're checking to see if recordIds is a QQueryFilter-looking object here.
|
||||
if (recordIds instanceof QQueryFilter || (typeof recordIds === "object" && recordIds.criteria))
|
||||
{
|
||||
queryStringPairsForInit.push("recordsParam=filterJSON");
|
||||
queryStringPairsForInit.push(`filterJSON=${encodeURIComponent(JSON.stringify(recordIds))}`);
|
||||
formData.append("recordsParam", "filterJSON")
|
||||
formData.append("filterJSON", JSON.stringify(recordIds));
|
||||
}
|
||||
else if (typeof recordIds === "object" && recordIds.length)
|
||||
{
|
||||
const encodedRecordIds = recordIds.map(r => encodeURIComponent(r)).join(",");
|
||||
queryStringPairsForInit.push("recordsParam=recordIds");
|
||||
queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
|
||||
formData.append("recordsParam", "recordIds")
|
||||
formData.append("recordIds", recordIds.join(","))
|
||||
}
|
||||
}
|
||||
|
||||
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||
queryStringPairsForInit.push(`tableVariant=${encodeURIComponent(JSON.stringify(tableVariant))}`);
|
||||
formData.append("tableVariant", JSON.stringify(tableVariant));
|
||||
}
|
||||
|
||||
try
|
||||
@ -1170,18 +1168,18 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
for (let key in defaultProcessValues)
|
||||
{
|
||||
queryStringPairsForInit.push(`${key}=${encodeURIComponent(defaultProcessValues[key])}`);
|
||||
formData.append(key, defaultProcessValues[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (tableMetaData)
|
||||
{
|
||||
queryStringPairsForInit.push(`tableName=${encodeURIComponent(tableMetaData.name)}`);
|
||||
formData.append("tableName", tableMetaData.name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const processResponse = await Client.getInstance().processInit(processName, queryStringPairsForInit.join("&"));
|
||||
const processResponse = await Client.getInstance().processInit(processName, formData);
|
||||
setProcessUUID(processResponse.processUUID);
|
||||
setLastProcessResponse(processResponse);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||
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 {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||
@ -146,6 +147,11 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
||||
const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData);
|
||||
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) =>
|
||||
{
|
||||
c.filterable = false;
|
||||
|
@ -44,9 +44,10 @@ import LinearProgress from "@mui/material/LinearProgress";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {ColumnHeaderFilterIconButtonProps, DataGridPro, GridCallbackDetails, GridColDef, GridColumnHeaderParams, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnResizeParams, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridFilterMenuItem, GridPinnedColumns, gridPreferencePanelStateSelector, GridPreferencePanelsValue, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridApiRef, useGridSelector} from "@mui/x-data-grid-pro";
|
||||
import {ColumnHeaderFilterIconButtonProps, DataGridPro, GridCallbackDetails, GridColDef, GridColumnHeaderParams, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnResizeParams, GridColumnVisibilityModel, GridDensity, GridEventListener, GridPinnedColumns, gridPreferencePanelStateSelector, GridPreferencePanelsValue, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarContainer, GridToolbarDensitySelector, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridApiRef, useGridSelector} from "@mui/x-data-grid-pro";
|
||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||
import FormData from "form-data";
|
||||
import {IDBPDatabase, openDB} from "idb";
|
||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
@ -101,6 +102,13 @@ RecordQuery.defaultProps = {
|
||||
///////////////////////////////////////////////////////
|
||||
type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready";
|
||||
|
||||
//////////////////////////////////////////
|
||||
// define IndexedDB store & field names //
|
||||
//////////////////////////////////////////
|
||||
const recordIdsForProcessesDBName = "qqq.recordIdsForProcesses";
|
||||
const recordIdsForProcessesStoreName = "recordIdsForProcesses";
|
||||
const timestampIndexAndFieldName = "timestamp";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
/*******************************************************************************
|
||||
@ -114,7 +122,6 @@ const getLoadingScreen = () =>
|
||||
</BaseLayout>);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QQQ Record Query Screen component.
|
||||
**
|
||||
@ -168,7 +175,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) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let defaultSort = [] as GridSortItem[];
|
||||
let defaultRowsPerPage = 10;
|
||||
let defaultRowsPerPage = 50;
|
||||
let defaultDensity = "standard" as GridDensity;
|
||||
let defaultTableVariant: QTableVariant = null;
|
||||
let defaultMode = "basic";
|
||||
@ -610,11 +617,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
e.preventDefault()
|
||||
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")
|
||||
{
|
||||
e.preventDefault()
|
||||
gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns)
|
||||
}
|
||||
*/
|
||||
else if (! e.metaKey && e.key === "f")
|
||||
{
|
||||
e.preventDefault()
|
||||
@ -1380,12 +1390,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(queryFilter))}`;
|
||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = null;
|
||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
||||
}
|
||||
|
||||
if (selectFullFilterState === "filterSubset")
|
||||
{
|
||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(queryFilter))}`;
|
||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = selectionSubsetSize;
|
||||
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
||||
}
|
||||
|
||||
if (selectedIds.length > 0)
|
||||
@ -1398,36 +1414,142 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** launch/open a modal process. Ends up navigating to the process's path w/
|
||||
** records selected via query string.
|
||||
** For the various functions that work with the recordIdsForProcess indexedDB,
|
||||
** open that database (creating if needed).
|
||||
*******************************************************************************/
|
||||
const openModalProcess = (process: QProcessMetaData = null) =>
|
||||
const openRecordIdsForProcessIndexedDB = async () =>
|
||||
{
|
||||
return await openDB(recordIdsForProcessesDBName, 1, {
|
||||
upgrade(db)
|
||||
{
|
||||
const store = db.createObjectStore(recordIdsForProcessesStoreName, {
|
||||
keyPath: "uuid"
|
||||
});
|
||||
store.createIndex(timestampIndexAndFieldName, timestampIndexAndFieldName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** clean up old indexedDB records that were created to launch processes in the past.
|
||||
*******************************************************************************/
|
||||
const manageRecordIdsForProcessIndexedDB = async (db: IDBPDatabase) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const now = new Date().getTime();
|
||||
const limit = now - (1000 * 60 * 60 * 24 * 7); // now minus 1 week
|
||||
|
||||
const expiredKeys = await db.getAllKeysFromIndex(recordIdsForProcessesStoreName, timestampIndexAndFieldName, IDBKeyRange.upperBound(limit, true));
|
||||
for (let expiredKey of expiredKeys)
|
||||
{
|
||||
db.delete(recordIdsForProcessesStoreName, expiredKey);
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log("Error managing recordIdsForProcess in indexeddb: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** we used to pass recordIds (that is, either an array of ids (from checkboxes)
|
||||
** or a json-query-filter) on the query string... but that gets too big, so,
|
||||
** instead, write it ... not to local storage (5MB limit, and we want to keep
|
||||
** them around for ... some period of time, for reloading processes), so,
|
||||
** write it to indexedDB instead.
|
||||
*******************************************************************************/
|
||||
const storeRecordIdsForProcessInIndexedDB = async (recordIds: string[] | QQueryFilter): Promise<string> =>
|
||||
{
|
||||
const uuid = crypto.randomUUID()
|
||||
|
||||
let db = await openRecordIdsForProcessIndexedDB();
|
||||
await db.add(recordIdsForProcessesStoreName, {
|
||||
uuid: uuid,
|
||||
json: JSON.stringify(recordIds),
|
||||
timestamp: new Date().getTime()
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we shouldn't need to await this function - it's just good to run the cleanup "sometimes" //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
manageRecordIdsForProcessIndexedDB(db);
|
||||
|
||||
return (uuid);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** when launching a process, if we're to use recordIds(/filter) from indexedDB,
|
||||
** then do that read (async), and open the modal process after it completes.
|
||||
*******************************************************************************/
|
||||
const launchModalProcessUsingRecordIdsFromIndexedDB = async (uuid: string, processMetaData: QProcessMetaData): Promise<void> =>
|
||||
{
|
||||
let db = await openRecordIdsForProcessIndexedDB();
|
||||
const recordIds = await db.get(recordIdsForProcessesStoreName, uuid)
|
||||
|
||||
if(recordIds)
|
||||
{
|
||||
const recordIdsObject = JSON.parse(recordIds.json);
|
||||
setRecordIdsForProcess(recordIdsObject);
|
||||
setActiveModalProcess(processMetaData);
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// closing the process will do a navigate - so we can't just set an alert, we need it to be passed in the navigation call as state - //
|
||||
// so pass it as a param to closeModalProcess, which will set it in the navigation state. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
closeModalProcess({}, "failed to start", "Could not find query filter to start this process. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** launch/open a modal process. Writes the records (ids or filter) to indexedDB,
|
||||
** identified by a UUID. Then navigates to the process's path w/
|
||||
** that UUID in the query string.
|
||||
*******************************************************************************/
|
||||
const openModalProcess = async (process: QProcessMetaData = null) =>
|
||||
{
|
||||
let uuid = "";
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
setRecordIdsForProcess(queryFilter);
|
||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = null;
|
||||
setRecordIdsForProcess(filterForBackend);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB(filterForBackend);
|
||||
}
|
||||
else if (selectFullFilterState === "filterSubset")
|
||||
{
|
||||
setRecordIdsForProcess(new QQueryFilter(queryFilter.criteria, queryFilter.orderBys, queryFilter.booleanOperator, 0, selectionSubsetSize));
|
||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = selectionSubsetSize;
|
||||
setRecordIdsForProcess(filterForBackend);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB(filterForBackend);
|
||||
}
|
||||
else if (selectedIds.length > 0)
|
||||
{
|
||||
setRecordIdsForProcess(selectedIds);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB(selectedIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
setRecordIdsForProcess([]);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB([]);
|
||||
}
|
||||
|
||||
navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}${getRecordsQueryString()}`);
|
||||
navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}?recordsParam=recordsKey&recordsKey=${uuid}`);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** close callback for modal processes
|
||||
*******************************************************************************/
|
||||
const closeModalProcess = (event: object, reason: string) =>
|
||||
const closeModalProcess = (event: object, reason: string, warning?: string) =>
|
||||
{
|
||||
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
||||
{
|
||||
@ -1439,7 +1561,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
const newPath = location.pathname.split("/");
|
||||
newPath.pop();
|
||||
navigate(newPath.join("/"));
|
||||
navigate(newPath.join("/"), warning ? {state: {warning: warning}} : undefined);
|
||||
|
||||
updateTable("close modal process");
|
||||
};
|
||||
@ -2097,20 +2219,32 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
if(selectedIndex == 0)
|
||||
{
|
||||
///////////////
|
||||
// this page //
|
||||
///////////////
|
||||
programmaticallySelectSomeOrAllRows();
|
||||
setSelectFullFilterState("checked")
|
||||
}
|
||||
else if(selectedIndex == 1)
|
||||
{
|
||||
///////////////////////
|
||||
// full query result //
|
||||
///////////////////////
|
||||
programmaticallySelectSomeOrAllRows();
|
||||
setSelectFullFilterState("filter")
|
||||
}
|
||||
else if(selectedIndex == 2)
|
||||
{
|
||||
////////////////////////////
|
||||
// subset of query result //
|
||||
////////////////////////////
|
||||
setSelectionSubsetSizePromptOpen(true);
|
||||
}
|
||||
else if(selectedIndex == 3)
|
||||
{
|
||||
/////////////////////
|
||||
// clear selection //
|
||||
/////////////////////
|
||||
setSelectFullFilterState("n/a")
|
||||
setRowSelectionModel([]);
|
||||
setSelectedIds([]);
|
||||
@ -2306,22 +2440,38 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
/////////////////////////////////////////////////////////////////
|
||||
if (pathParts[pathParts.length - 2] === tableName)
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// try to find a process by this name //
|
||||
////////////////////////////////////////
|
||||
const processName = pathParts[pathParts.length - 1];
|
||||
const processList = allTableProcesses.filter(p => p.name == processName);
|
||||
if (processList.length > 0)
|
||||
let process = processList.length > 0 ? processList[0] : null;
|
||||
if(!process)
|
||||
{
|
||||
setActiveModalProcess(processList[0]);
|
||||
process = metaData?.processes.get(processName)
|
||||
}
|
||||
else if (metaData?.processes.has(processName))
|
||||
|
||||
if(process)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// check for generic processes - should this be a specific attribute on the process? //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
setActiveModalProcess(metaData?.processes.get(processName));
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// check if a recordsKey UUID (e.g., from indexedDB) is in the query string //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const uuid = urlSearchParams.get("recordsKey")
|
||||
|
||||
if(uuid)
|
||||
{
|
||||
await launchModalProcessUsingRecordIdsFromIndexedDB(uuid, processList[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
setActiveModalProcess(processList[0]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`Couldn't find process named ${processName}`);
|
||||
setWarningAlert(`Couldn't find process named ${processName}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,6 @@ class FilterUtils
|
||||
let rs = [];
|
||||
for (let i = 0; i < param.length; i++)
|
||||
{
|
||||
console.log(param[i]);
|
||||
if (param[i] && param[i].id && param[i].label)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////
|
||||
@ -115,8 +114,11 @@ class FilterUtils
|
||||
// e.g., ...values=[1]... //
|
||||
// but we need them to be possibleValue objects (w/ id & label) so the label //
|
||||
// can be shown in the filter dropdown. So, make backend call to look them up. //
|
||||
// also, there are cases where we can get a null or "" as the only value in the //
|
||||
// values array - avoid sending that to the backend, as it comes back w/ all //
|
||||
// possible values, and a general "bad time" //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if (values && values.length > 0)
|
||||
if (values && values.length > 0 && values[0] !== null && values[0] !== undefined && values[0] !== "")
|
||||
{
|
||||
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values);
|
||||
}
|
||||
@ -319,7 +321,7 @@ class FilterUtils
|
||||
** get the values associated with a criteria as a string, e.g., for showing
|
||||
** in a tooltip.
|
||||
*******************************************************************************/
|
||||
public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3): string
|
||||
public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3, andMoreFormat: "andNOther" | "+N" = "andNOther"): string
|
||||
{
|
||||
let valuesString = "";
|
||||
|
||||
@ -340,6 +342,10 @@ class FilterUtils
|
||||
{
|
||||
maxLoops = maxValuesToShow;
|
||||
}
|
||||
else if(maxValuesToShow == 1 && criteria.values.length > 1)
|
||||
{
|
||||
maxLoops = 1;
|
||||
}
|
||||
|
||||
for (let i = 0; i < maxLoops; i++)
|
||||
{
|
||||
@ -357,7 +363,12 @@ class FilterUtils
|
||||
else if (value.type == "ThisOrLastPeriod")
|
||||
{
|
||||
const expression = new ThisOrLastPeriodExpression(value);
|
||||
labels.push(expression.toString());
|
||||
let startOfPrefix = "";
|
||||
if(fieldMetaData.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
|
||||
{
|
||||
startOfPrefix = "start of ";
|
||||
}
|
||||
labels.push(`${startOfPrefix}${expression.toString()}`);
|
||||
}
|
||||
else if(fieldMetaData.type == QFieldType.BOOLEAN)
|
||||
{
|
||||
@ -383,7 +394,16 @@ class FilterUtils
|
||||
|
||||
if (maxLoops < criteria.values.length)
|
||||
{
|
||||
labels.push(" and " + (criteria.values.length - maxLoops) + " other values.");
|
||||
const n = criteria.values.length - maxLoops;
|
||||
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(", "));
|
||||
|
@ -29,6 +29,7 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -504,7 +505,33 @@ public class QSeleniumLib
|
||||
*******************************************************************************/
|
||||
public WebElement waitForSelectorContaining(String cssSelector, String textContains)
|
||||
{
|
||||
LOG.debug("Waiting for element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||
return (waitForSelectorMatchingPredicate(cssSelector, "containing text [" + textContains + "]", (WebElement element) ->
|
||||
{
|
||||
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();
|
||||
|
||||
do
|
||||
@ -514,9 +541,9 @@ public class QSeleniumLib
|
||||
{
|
||||
try
|
||||
{
|
||||
if(element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()))
|
||||
if(predicate.apply(element))
|
||||
{
|
||||
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||
LOG.debug("Found element matching selector [" + cssSelector + "] " + description);
|
||||
Actions actions = new Actions(driver);
|
||||
actions.moveToElement(element);
|
||||
conditionallyAutoHighlight(element);
|
||||
@ -537,7 +564,7 @@ public class QSeleniumLib
|
||||
}
|
||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||
|
||||
fail("Failed to find element matching selector [" + cssSelector + "] containing text [" + textContains + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||
fail("Failed to find element matching selector [" + cssSelector + "] " + description + " after [" + WAIT_SECONDS + "] seconds.");
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,9 @@
|
||||
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.Keys;
|
||||
import org.openqa.selenium.WebElement;
|
||||
@ -103,7 +106,7 @@ public class QueryScreenLib
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void clickFilterButton()
|
||||
public void clickFilterBuilderButton()
|
||||
{
|
||||
qSeleniumLib.waitForSelectorContaining("BUTTON", "FILTER BUILDER").click();
|
||||
}
|
||||
@ -181,10 +184,11 @@ public class QueryScreenLib
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addAdvancedQueryFilterInput(QSeleniumLib qSeleniumLib, int index, String fieldLabel, String operator, String value, String booleanOperator)
|
||||
public void addAdvancedQueryFilterInput(int index, String fieldLabel, String operator, String value, String booleanOperator)
|
||||
{
|
||||
if(index > 0)
|
||||
{
|
||||
@ -221,10 +225,91 @@ public class QueryScreenLib
|
||||
operatorInput.sendKeys("\n");
|
||||
qSeleniumLib.waitForMillis(100);
|
||||
|
||||
WebElement valueInput = subFormForField.findElement(By.cssSelector(".filterValuesColumn INPUT"));
|
||||
valueInput.click();
|
||||
valueInput.sendKeys(value);
|
||||
qSeleniumLib.waitForMillis(100);
|
||||
if(StringUtils.hasContent(value))
|
||||
{
|
||||
WebElement valueInput = subFormForField.findElement(By.cssSelector(".filterValuesColumn INPUT"));
|
||||
valueInput.click();
|
||||
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,6 +27,7 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QSeleniumLib;
|
||||
import io.javalin.Javalin;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -284,7 +285,6 @@ public class QSeleniumJavalin
|
||||
|
||||
do
|
||||
{
|
||||
// LOG.debug(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
|
||||
for(CapturedContext context : captured)
|
||||
{
|
||||
if(context.getPath().equals(path))
|
||||
@ -301,6 +301,7 @@ public class QSeleniumJavalin
|
||||
}
|
||||
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.");
|
||||
return (null);
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.assertFilterButtonBadge(1);
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
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");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.assertFilterButtonBadge(1);
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
qSeleniumLib.waitForSelector("input[value=\"is between\"]");
|
||||
qSeleniumLib.waitForSelector("input[value=\"1701\"]");
|
||||
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");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.assertFilterButtonBadge(1);
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
qSeleniumLib.waitForSelector("input[value=\"is any of\"]");
|
||||
qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "St. Louis");
|
||||
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");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.assertFilterButtonBadge(1);
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
qSeleniumLib.waitForSelector("input[value=\"is after\"]");
|
||||
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");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.assertFilterButtonBadge(2);
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
qSeleniumLib.waitForSelector("input[value=\"is at or before\"]");
|
||||
qSeleniumLib.waitForSelector("input[value=\"start of this year\"]");
|
||||
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");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.assertFilterButtonBadge(1);
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
qSeleniumLib.waitForSelector("input[value=\"does not equal\"]");
|
||||
qSeleniumLib.waitForSelector("input[value=\"St. Louis\"]");
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
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.QQQMaterialDashboardSelectors;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QueryScreenLib;
|
||||
@ -48,6 +49,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||
.withRouteToFile("/data/person/query", "data/person/index.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");
|
||||
}
|
||||
|
||||
@ -64,13 +66,13 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.gotoAdvancedMode();
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// open the filter window, enter a value, wait for query to re-run //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
qSeleniumJavalin.beginCapture();
|
||||
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 0, "Id", "equals", "1", null);
|
||||
queryScreenLib.addAdvancedQueryFilterInput(0, "Id", "equals", "1", null);
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// assert that query & count both have the expected filter value //
|
||||
@ -117,11 +119,11 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||
queryScreenLib.waitForQueryToHaveRan();
|
||||
queryScreenLib.gotoAdvancedMode();
|
||||
queryScreenLib.clickFilterButton();
|
||||
queryScreenLib.clickFilterBuilderButton();
|
||||
|
||||
qSeleniumJavalin.beginCapture();
|
||||
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 0, "First Name", "contains", "Dar", "Or");
|
||||
queryScreenLib.addAdvancedQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
|
||||
queryScreenLib.addAdvancedQueryFilterInput(0, "First Name", "contains", "Dar", "Or");
|
||||
queryScreenLib.addAdvancedQueryFilterInput(1, "First Name", "contains", "Jam", "Or");
|
||||
|
||||
String expectedFilterContents0 = """
|
||||
{"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";
|
||||
@ -137,6 +139,138 @@ 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
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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