diff --git a/pom.xml b/pom.xml
index 8c6ec4c..83de67e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,7 +66,7 @@
com.kingsrook.qqq
qqq-backend-core
- feature-CTLE-503-optimization-weather-api-data-20230701.011918-3
+ 0.17.0-SNAPSHOT
org.slf4j
diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx
index 63743a2..ca7eb4c 100644
--- a/src/qqq/pages/records/query/ColumnStats.tsx
+++ b/src/qqq/pages/records/query/ColumnStats.tsx
@@ -88,7 +88,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
}
const processResult = await qController.processRun("columnStats", formData);
- setStatusString(null)
+ setStatusString(null);
if (processResult instanceof QJobError)
{
const jobError = processResult as QJobError;
@@ -107,7 +107,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
const newStatsFields = [] as QFieldMetaData[];
for(let i = 0; i();
fakeTableMetaData.fields.set(fieldMetaData.name, fieldMetaData);
fakeTableMetaData.fields.set("count", new QFieldMetaData({name: "count", label: "Count", type: "INTEGER"}));
+ fakeTableMetaData.fields.set("percent", new QFieldMetaData({name: "percent", label: "Percent", type: "DECIMAL"}));
fakeTableMetaData.sections = [] as QTableSection[];
- fakeTableMetaData.sections.push(new QTableSection({fieldNames: [fieldMetaData.name, "count"]}));
+ fakeTableMetaData.sections.push(new QTableSection({fieldNames: [fieldMetaData.name, "count", "percent"]}));
const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData);
const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, null, null, "bySection");
columns.forEach((c) =>
{
- c.width = 200;
c.filterable = false;
c.hideable = false;
})
@@ -162,7 +162,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
function CustomPagination()
{
return (
-
+
{rows && rows.length && countDistinct && rows.length < countDistinct ? Showing the first {rows.length.toLocaleString()} of {countDistinct.toLocaleString()} values : <>>}
{rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length == 1 ? Showing the only value : <>>}
{rows && rows.length && countDistinct && rows.length >= countDistinct && rows.length > 1 ? Showing all {rows.length.toLocaleString()} values : <>>}
@@ -172,9 +172,9 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
const refresh = () =>
{
- setLoading(true)
- setStatusString("Refreshing...")
- }
+ setLoading(true);
+ setStatusString("Refreshing...");
+ };
const doExport = () =>
{
@@ -188,7 +188,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
HtmlUtils.download(fileName, csv);
- }
+ };
function Loading()
{
diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx
index 0aa5626..7a1c0c2 100644
--- a/src/qqq/utils/DataGridUtils.tsx
+++ b/src/qqq/utils/DataGridUtils.tsx
@@ -259,7 +259,6 @@ export default class DataGridUtils
public static makeColumnFromField = (field: QFieldMetaData, tableMetaData: QTableMetaData, namePrefix?: string, labelPrefix?: string): GridColDef =>
{
let columnType = "string";
- let columnWidth = 200;
let filterOperators: GridFilterOperator[] = QGridStringOperators;
if (field.possibleValueSourceName)
@@ -273,28 +272,18 @@ export default class DataGridUtils
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
columnType = "number";
- columnWidth = 100;
-
- if (field.name === tableMetaData.primaryKeyField && field.label.length < 3)
- {
- columnWidth = 75;
- }
-
filterOperators = QGridNumericOperators;
break;
case QFieldType.DATE:
columnType = "date";
- columnWidth = 100;
filterOperators = QGridDateOperators;
break;
case QFieldType.DATE_TIME:
columnType = "dateTime";
- columnWidth = 200;
filterOperators = QGridDateTimeOperators;
break;
case QFieldType.BOOLEAN:
columnType = "string"; // using boolean gives an odd 'no' for nulls.
- columnWidth = 75;
filterOperators = QGridBooleanOperators;
break;
case QFieldType.BLOB:
@@ -305,6 +294,31 @@ export default class DataGridUtils
}
}
+ let headerName = labelPrefix ? labelPrefix + field.label : field.label;
+ let fieldName = namePrefix ? namePrefix + field.name : field.name;
+
+ const column: GridColDef = {
+ field: fieldName,
+ type: columnType,
+ headerName: headerName,
+ width: DataGridUtils.getColumnWidthForField(field, tableMetaData),
+ renderCell: null as any,
+ filterOperators: filterOperators,
+ };
+
+ column.renderCell = (cellValues: any) => (
+ (cellValues.value)
+ );
+
+ return (column);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static getColumnWidthForField = (field: QFieldMetaData, table?: QTableMetaData): number =>
+ {
if (field.hasAdornment(AdornmentType.SIZE))
{
const sizeAdornment = field.getAdornment(AdornmentType.SIZE);
@@ -318,7 +332,7 @@ export default class DataGridUtils
]);
if (widths.has(width))
{
- columnWidth = widths.get(width);
+ return widths.get(width);
}
else
{
@@ -326,23 +340,31 @@ export default class DataGridUtils
}
}
- let headerName = labelPrefix ? labelPrefix + field.label : field.label;
- let fieldName = namePrefix ? namePrefix + field.name : field.name;
+ if(field.possibleValueSourceName)
+ {
+ return (200);
+ }
- const column: GridColDef = {
- field: fieldName,
- type: columnType,
- headerName: headerName,
- width: columnWidth,
- renderCell: null as any,
- filterOperators: filterOperators,
- };
+ switch (field.type)
+ {
+ case QFieldType.DECIMAL:
+ case QFieldType.INTEGER:
- column.renderCell = (cellValues: any) => (
- (cellValues.value)
- );
+ if (table && field.name === table.primaryKeyField && field.label.length < 3)
+ {
+ return (75);
+ }
- return (column);
+ return (100);
+ case QFieldType.DATE:
+ return (100);
+ case QFieldType.DATE_TIME:
+ return (200);
+ case QFieldType.BOOLEAN:
+ return (75);
+ }
+
+ return (200);
}
}
diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts
index 8ce5706..eeb86b9 100644
--- a/src/qqq/utils/qqq/FilterUtils.ts
+++ b/src/qqq/utils/qqq/FilterUtils.ts
@@ -449,12 +449,15 @@ class FilterUtils
//////////////////////////////////////////////////////////////////////////
// replace objects that look like expressions with expression instances //
//////////////////////////////////////////////////////////////////////////
- for(let i = 0; i < values.length; i++)
+ if(values && values.length)
{
- const expression = this.gridCriteriaValueToExpression(values[i])
- if(expression)
+ for (let i = 0; i < values.length; i++)
{
- values[i] = expression;
+ const expression = this.gridCriteriaValueToExpression(values[i])
+ if (expression)
+ {
+ values[i] = expression;
+ }
}
}
diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/QueryScreenFilterInUrlTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/QueryScreenFilterInUrlTest.java
new file mode 100755
index 0000000..77e8b0e
--- /dev/null
+++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/QueryScreenFilterInUrlTest.java
@@ -0,0 +1,185 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.materialdashboard.tests;
+
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.temporal.ChronoUnit;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.ThisOrLastPeriod;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
+import com.kingsrook.qqq.materialdashboard.lib.QQQMaterialDashboardSelectors;
+import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.WebElement;
+
+
+/*******************************************************************************
+ ** Test for the record query screen when a filter is given in the URL
+ *******************************************************************************/
+public class QueryScreenFilterInUrlTest extends QBaseSeleniumTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
+ {
+ super.addJavalinRoutes(qSeleniumJavalin);
+ qSeleniumJavalin
+ .withRouteToFile("/data/person/count", "data/person/count.json")
+ .withRouteToFile("/data/person/query", "data/person/index.json")
+ .withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
+ .withRouteToFile("/data/person/variants", "data/person/variants.json")
+ .withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testUrlWithFilter()
+ {
+ ////////////////////////////////////////
+ // not-blank -- criteria w/ no values //
+ ////////////////////////////////////////
+ String filterJSON = JsonUtils.toJson(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("annualSalary", QCriteriaOperator.IS_NOT_BLANK)));
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
+ waitForQueryToHaveRan();
+ assertFilterButtonBadge(1);
+ clickFilterButton();
+ qSeleniumLib.waitForSelector("input[value=\"is not empty\"]");
+
+ ///////////////////////////////
+ // between on a number field //
+ ///////////////////////////////
+ filterJSON = JsonUtils.toJson(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("annualSalary", QCriteriaOperator.BETWEEN, 1701, 74656)));
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
+ waitForQueryToHaveRan();
+ assertFilterButtonBadge(1);
+ clickFilterButton();
+ qSeleniumLib.waitForSelector("input[value=\"is between\"]");
+ qSeleniumLib.waitForSelector("input[value=\"1701\"]");
+ qSeleniumLib.waitForSelector("input[value=\"74656\"]");
+
+ //////////////////////////////////////////
+ // not-equals on a possible-value field //
+ //////////////////////////////////////////
+ filterJSON = JsonUtils.toJson(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("homeCityId", QCriteriaOperator.NOT_EQUALS, 1)));
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
+ waitForQueryToHaveRan();
+ assertFilterButtonBadge(1);
+ clickFilterButton();
+ qSeleniumLib.waitForSelector("input[value=\"does not equal\"]");
+ qSeleniumLib.waitForSelector("input[value=\"St. Louis\"]");
+
+ //////////////////////////////////////
+ // an IN for a possible-value field //
+ //////////////////////////////////////
+ filterJSON = JsonUtils.toJson(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("homeCityId", QCriteriaOperator.IN, 1, 2)));
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
+ waitForQueryToHaveRan();
+ assertFilterButtonBadge(1);
+ clickFilterButton();
+ qSeleniumLib.waitForSelector("input[value=\"is any of\"]");
+ qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "St. Louis");
+ qSeleniumLib.waitForSelectorContaining(".MuiChip-label", "Chesterfield");
+
+ /////////////////////////////////////////
+ // greater than a date-time expression //
+ /////////////////////////////////////////
+ filterJSON = JsonUtils.toJson(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("createDate", QCriteriaOperator.GREATER_THAN, NowWithOffset.minus(5, ChronoUnit.DAYS))));
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
+ waitForQueryToHaveRan();
+ assertFilterButtonBadge(1);
+ clickFilterButton();
+ qSeleniumLib.waitForSelector("input[value=\"is after\"]");
+ qSeleniumLib.waitForSelector("input[value=\"5 days ago\"]");
+
+ ///////////////////////
+ // multiple criteria //
+ ///////////////////////
+ filterJSON = JsonUtils.toJson(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.STARTS_WITH, "Dar"))
+ .withCriteria(new QFilterCriteria("createDate", QCriteriaOperator.LESS_THAN_OR_EQUALS, ThisOrLastPeriod.this_(ChronoUnit.YEARS))));
+ qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filterJSON, StandardCharsets.UTF_8), "Person");
+ waitForQueryToHaveRan();
+ assertFilterButtonBadge(2);
+ clickFilterButton();
+ qSeleniumLib.waitForSelector("input[value=\"is at or before\"]");
+ qSeleniumLib.waitForSelector("input[value=\"start of this year\"]");
+ qSeleniumLib.waitForSelector("input[value=\"starts with\"]");
+ qSeleniumLib.waitForSelector("input[value=\"Dar\"]");
+
+ ////////////////
+ // remove one //
+ ////////////////
+ qSeleniumLib.waitForSelectorContaining(".MuiIcon-root", "close").click();
+ assertFilterButtonBadge(1);
+
+ qSeleniumLib.waitForever();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private WebElement assertFilterButtonBadge(int valueInBadge)
+ {
+ return qSeleniumLib.waitForSelectorContaining(".MuiBadge-root", String.valueOf(valueInBadge));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private WebElement waitForQueryToHaveRan()
+ {
+ return qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void clickFilterButton()
+ {
+ qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filter").click();
+ }
+
+}
diff --git a/src/test/resources/fixtures/data/person/possibleValues/homeCityId.json b/src/test/resources/fixtures/data/person/possibleValues/homeCityId.json
new file mode 100644
index 0000000..380b357
--- /dev/null
+++ b/src/test/resources/fixtures/data/person/possibleValues/homeCityId.json
@@ -0,0 +1,12 @@
+{
+ "options": [
+ {
+ "id": 1,
+ "label": "St. Louis"
+ },
+ {
+ "id": 2,
+ "label": "Chesterfield"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/fixtures/metaData/table/person.json b/src/test/resources/fixtures/metaData/table/person.json
index b72f440..3a3e00c 100644
--- a/src/test/resources/fixtures/metaData/table/person.json
+++ b/src/test/resources/fixtures/metaData/table/person.json
@@ -74,6 +74,15 @@
"isEditable": true,
"displayFormat": "%s"
},
+ "homeCityId": {
+ "name": "homeCityId",
+ "label": "Home City",
+ "type": "INTEGER",
+ "possibleValueSourceName": "city",
+ "isRequired": false,
+ "isEditable": true,
+ "displayFormat": "%s"
+ },
"email": {
"name": "email",
"label": "Email",