Compare commits

...

9 Commits

11 changed files with 374 additions and 51 deletions

View File

@ -45,7 +45,7 @@ export default function ExportMenuItem(props: QExportMenuItemProps)
return (
<MenuItem
disabled={totalRecords === 0}
disabled={!totalRecords}
onClick={() =>
{
///////////////////////////////////////////////////////////////////////////////

View File

@ -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)

View File

@ -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}&nbsp;</>);
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>&nbsp;<span style={{fontWeight: 400}}>{operatorSelectedValue.label}&nbsp;{valuesString}</span></>);
buttonContent = (<><span style={{fontWeight: 700}}>{buttonContent}:</span>&nbsp;<span style={{fontWeight: 400}}>{operatorString}{valuesString}</span></>);
}
const mouseEvents =
@ -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 &zwnj; entity as first character in label instead... //
////////////////////////////////////////////////////////////////////////////////////////////////////
renderInput={(params) => (<TextField {...params} label={<>&zwnj;Operator</>} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
options={operatorOptions}
value={operatorSelectedValue as any}
inputValue={operatorInputValue}

View File

@ -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;

View File

@ -1380,12 +1380,15 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
if (selectFullFilterState === "filter")
{
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(queryFilter))}`;
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(prepQueryFilterForBackend(queryFilter)))}`;
}
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)
@ -1405,11 +1408,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
if (selectFullFilterState === "filter")
{
setRecordIdsForProcess(queryFilter);
setRecordIdsForProcess(prepQueryFilterForBackend(queryFilter));
}
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);
}
else if (selectedIds.length > 0)
{

View File

@ -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)
{
/////////////////////////////////////////////////////////////
@ -319,7 +318,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 +339,10 @@ class FilterUtils
{
maxLoops = maxValuesToShow;
}
else if(maxValuesToShow == 1 && criteria.values.length > 1)
{
maxLoops = 1;
}
for (let i = 0; i < maxLoops; i++)
{
@ -357,7 +360,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 +391,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(", "));

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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\"]");

View File

@ -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();
}
}