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",