From 3b8b45ecea49bb103ab3338caf56d1921c6d3f16 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 9 Aug 2022 14:23:51 -0500 Subject: [PATCH] Add field sections, record labels, display values being populated --- .../core/actions/tables/QueryAction.java | 5 + .../core/actions/values/QValueFormatter.java | 105 +++++++- .../core/instances/QInstanceEnricher.java | 91 ++++++- .../core/instances/QInstanceValidator.java | 54 ++++ .../qqq/backend/core/model/data/QRecord.java | 59 +++-- .../model/metadata/fields/QFieldType.java | 4 - .../frontend/QFrontendProcessMetaData.java | 14 +- .../frontend/QFrontendTableMetaData.java | 30 ++- .../metadata/processes/QProcessMetaData.java | 37 ++- .../model/metadata/tables/QFieldSection.java | 234 ++++++++++++++++++ .../model/metadata/tables/QTableMetaData.java | 135 ++++++++++ .../core/model/metadata/tables/Tier.java | 33 +++ .../implementations/mock/MockQueryAction.java | 1 - .../actions/values/QValueFormatterTest.java | 160 ++++++++++++ .../instances/QInstanceValidatorTest.java | 125 ++++++++++ .../qqq/backend/module/rdbms/TestUtils.java | 16 ++ .../rdbms/actions/RDBMSQueryActionTest.java | 26 ++ .../sampleapp/SampleMetaDataProvider.java | 52 +++- .../test/resources/prime-test-database.sql | 15 +- 19 files changed, 1140 insertions(+), 56 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Tier.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index c929bc24..c09ce3a9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -48,6 +49,10 @@ public class QueryAction // todo pre-customization - just get to modify the request? QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); // todo post-customization - can do whatever w/ the result if you want + + QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords()); + return queryOutput; } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 107b66bd..4c20ae7f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -23,7 +23,10 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; @@ -63,7 +66,25 @@ public class QValueFormatter } catch(Exception e) { - LOG.warn("Error formatting value [" + value + "] for field [" + field.getName() + "] with format [" + field.getDisplayFormat() + "]: " + e.getMessage()); + try + { + if(e.getMessage().equals("f != java.lang.Integer")) + { + return formatValue(field, ValueUtils.getValueAsBigDecimal(value)); + } + else if(e.getMessage().equals("d != java.math.BigDecimal")) + { + return formatValue(field, ValueUtils.getValueAsInteger(value)); + } + else + { + LOG.warn("Error formatting value [" + value + "] for field [" + field.getName() + "] with format [" + field.getDisplayFormat() + "]: " + e.getMessage()); + } + } + catch(Exception e2) + { + LOG.warn("Caught secondary exception trying to convert type on field [" + field.getName() + "] for formatting", e); + } } } @@ -72,4 +93,86 @@ public class QValueFormatter //////////////////////////////////////// return (ValueUtils.getValueAsString(value)); } + + + + /******************************************************************************* + ** Make a string from a table's recordLabelFormat and fields, for a given record. + *******************************************************************************/ + public static String formatRecordLabel(QTableMetaData table, QRecord record) + { + if(!StringUtils.hasContent(table.getRecordLabelFormat())) + { + return (formatRecordLabelExceptionalCases(table, record)); + } + + /////////////////////////////////////////////////////////////////////// + // get list of values, then pass them to the string formatter method // + /////////////////////////////////////////////////////////////////////// + try + { + List values = table.getRecordLabelFields().stream() + .map(record::getValue) + .map(v -> v == null ? "" : v) + .toList(); + return (table.getRecordLabelFormat().formatted(values.toArray())); + } + catch(Exception e) + { + return (formatRecordLabelExceptionalCases(table, record)); + } + } + + + + /******************************************************************************* + ** Deal with non-happy-path cases for making a record label. + *******************************************************************************/ + private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) + { + /////////////////////////////////////////////////////////////////////////////////////// + // if there's no record label format, then just return the primary key display value // + /////////////////////////////////////////////////////////////////////////////////////// + String pkeyDisplayValue = record.getDisplayValue(table.getPrimaryKeyField()); + if(StringUtils.hasContent(pkeyDisplayValue)) + { + return (pkeyDisplayValue); + } + + String pkeyRawValue = ValueUtils.getValueAsString(record.getValue(table.getPrimaryKeyField())); + if(StringUtils.hasContent(pkeyRawValue)) + { + return (pkeyRawValue); + } + + /////////////////////////////////////////////////////////////////////////////// + // worst case scenario, return empty string, but never null from this method // + /////////////////////////////////////////////////////////////////////////////// + return (""); + } + + + + /******************************************************************************* + ** For a list of records, set their recordLabels and display values + *******************************************************************************/ + public static void setDisplayValuesInRecords(QTableMetaData table, List records) + { + if(records == null) + { + return; + } + + for(QRecord record : records) + { + for(QFieldMetaData field : table.getFields().values()) + { + String formattedValue = QValueFormatter.formatValue(field, record.getValue(field.getName())); + record.setDisplayValue(field.getName(), formattedValue); + } + + record.setRecordLabel(QValueFormatter.formatRecordLabel(table, record)); + } + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index de16582d..a7ca02a9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.instances; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -32,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; @@ -41,13 +44,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMe import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteStoreStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditReceiveValuesStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditStoreRecordsStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveFileStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertStoreRecordsStep; import com.kingsrook.qqq.backend.core.processes.implementations.general.LoadInitialRecordsStep; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -111,6 +117,11 @@ public class QInstanceEnricher { table.getFields().values().forEach(this::enrich); } + + if(CollectionUtils.nullSafeIsEmpty(table.getSections())) + { + generateTableFieldSections(table); + } } @@ -196,12 +207,19 @@ public class QInstanceEnricher *******************************************************************************/ private String nameToLabel(String name) { - if(name == null) + if(!StringUtils.hasContent(name)) { - return (null); + return (name); } - return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1")); + if(name.length() == 1) + { + return (name.substring(0, 1).toUpperCase(Locale.ROOT)); + } + else + { + return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1")); + } } @@ -422,4 +440,71 @@ public class QInstanceEnricher ))); } + + + /******************************************************************************* + ** If a table didn't have any sections, generate "sensible defaults" + *******************************************************************************/ + private void generateTableFieldSections(QTableMetaData table) + { + if(CollectionUtils.nullSafeIsEmpty(table.getFields())) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // assume this table is invalid if it has no fields, but surely it doesn't need any sections then. // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + return; + } + + ////////////////////////////////////////////////////////////////////////////// + // create an identity section for the id and any fields in the record label // + ////////////////////////////////////////////////////////////////////////////// + QFieldSection identitySection = new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, new ArrayList<>()); + + Set usedFieldNames = new HashSet<>(); + + if(StringUtils.hasContent(table.getPrimaryKeyField())) + { + identitySection.getFieldNames().add(table.getPrimaryKeyField()); + usedFieldNames.add(table.getPrimaryKeyField()); + } + + if(CollectionUtils.nullSafeHasContents(table.getRecordLabelFields())) + { + for(String fieldName : table.getRecordLabelFields()) + { + if(!usedFieldNames.contains(fieldName)) + { + identitySection.getFieldNames().add(fieldName); + usedFieldNames.add(fieldName); + } + } + } + + if(!identitySection.getFieldNames().isEmpty()) + { + table.addSection(identitySection); + } + + /////////////////////////////////////////////////////////////////////////////// + // if there are more fields, then add them in a default/Other Fields section // + /////////////////////////////////////////////////////////////////////////////// + QFieldSection otherSection = new QFieldSection("otherFields", "Other Fields", new QIcon("dataset"), Tier.T2, new ArrayList<>()); + if(CollectionUtils.nullSafeHasContents(table.getFields())) + { + for(String fieldName : table.getFields().keySet()) + { + if(!usedFieldNames.contains(fieldName)) + { + otherSection.getFieldNames().add(fieldName); + usedFieldNames.add(fieldName); + } + } + } + + if(!otherSection.getFieldNames().isEmpty()) + { + table.addSection(otherSection); + } + + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index c7665022..9ae87598 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -32,6 +32,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -160,12 +163,63 @@ public class QInstanceValidator } }); } + + ////////////////////////////////////////// + // validate field sections in the table // + ////////////////////////////////////////// + Set fieldNamesInSections = new HashSet<>(); + QFieldSection tier1Section = null; + if(table.getSections() != null) + { + for(QFieldSection section : table.getSections()) + { + validateSection(errors, table, section, fieldNamesInSections); + if(section.getTier().equals(Tier.T1)) + { + assertCondition(errors, tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1"); + tier1Section = section; + } + } + } + + if(CollectionUtils.nullSafeHasContents(table.getFields())) + { + for(String fieldName : table.getFields().keySet()) + { + assertCondition(errors, fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections."); + } + } + }); } } + /******************************************************************************* + ** + *******************************************************************************/ + private void validateSection(List errors, QTableMetaData table, QFieldSection section, Set fieldNamesInSections) + { + assertCondition(errors, StringUtils.hasContent(section.getName()), "Missing a name for field section in table " + table.getName() + "."); + assertCondition(errors, StringUtils.hasContent(section.getLabel()), "Missing a label for field section in table " + table.getLabel() + "."); + if(assertCondition(errors, CollectionUtils.nullSafeHasContents(section.getFieldNames()), "Table " + table.getName() + " section " + section.getName() + " does not have any fields.")) + { + if(table.getFields() != null) + { + for(String fieldName : section.getFieldNames()) + { + assertCondition(errors, table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table."); + assertCondition(errors, !fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections."); + + fieldNamesInSections.add(fieldName); + } + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 2ef044ff..3c30366f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -55,7 +54,9 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; *******************************************************************************/ public class QRecord implements Serializable { - private String tableName; + private String tableName; + private String recordLabel; + private Map values = new LinkedHashMap<>(); private Map displayValues = new LinkedHashMap<>(); private Map backendDetails = new LinkedHashMap<>(); @@ -90,6 +91,7 @@ public class QRecord implements Serializable public QRecord(QRecord record) { this.tableName = record.tableName; + this.recordLabel = record.recordLabel; this.values = record.values; this.displayValues = record.displayValues; this.backendDetails = record.backendDetails; @@ -139,15 +141,6 @@ public class QRecord implements Serializable - /******************************************************************************* - ** - *******************************************************************************/ - public void setDisplayValue(QFieldMetaData field, Serializable rawValue) - { - displayValues.put(field.getName(), QValueFormatter.formatValue(field, rawValue)); - } - - /******************************************************************************* ** @@ -160,17 +153,6 @@ public class QRecord implements Serializable - /******************************************************************************* - ** - *******************************************************************************/ - public QRecord withDisplayValue(QFieldMetaData field, Serializable rawValue) - { - setDisplayValue(field, rawValue); - return (this); - } - - - /******************************************************************************* ** Getter for tableName ** @@ -205,6 +187,39 @@ public class QRecord implements Serializable + /******************************************************************************* + ** Getter for recordLabel + ** + *******************************************************************************/ + public String getRecordLabel() + { + return recordLabel; + } + + + + /******************************************************************************* + ** Setter for recordLabel + ** + *******************************************************************************/ + public void setRecordLabel(String recordLabel) + { + this.recordLabel = recordLabel; + } + + + /******************************************************************************* + ** Fluent setter for recordLabel + ** + *******************************************************************************/ + public QRecord withRecordLabel(String recordLabel) + { + this.recordLabel = recordLabel; + return (this); + } + + + /******************************************************************************* ** Getter for values ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java index 79bc824a..324e8991 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java @@ -61,10 +61,6 @@ public enum QFieldType { return (INTEGER); } - if(c.equals(Boolean.class)) - { - return (BOOLEAN); - } if(c.equals(BigDecimal.class)) { return (DECIMAL); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java index c6f86289..f6cb8e0a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java @@ -30,6 +30,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -45,6 +46,8 @@ public class QFrontendProcessMetaData private String tableName; private boolean isHidden; + private String iconName; + private List frontendSteps; ////////////////////////////////////////////////////////////////////////////////// @@ -77,6 +80,11 @@ public class QFrontendProcessMetaData frontendSteps = new ArrayList<>(); } } + + if(processMetaData.getIcon() != null && StringUtils.hasContent(processMetaData.getIcon().getName())) + { + this.iconName = processMetaData.getIcon().getName(); + } } @@ -148,12 +156,12 @@ public class QFrontendProcessMetaData /******************************************************************************* - ** Setter for isHidden + ** Getter for iconName ** *******************************************************************************/ - public void setIsHidden(boolean isHidden) + public String getIconName() { - this.isHidden = isHidden; + return iconName; } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java index 0253448f..283402af 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java @@ -23,11 +23,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend; import java.util.HashMap; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -43,7 +46,10 @@ public class QFrontendTableMetaData private boolean isHidden; private String primaryKeyField; + private String iconName; + private Map fields; + private List sections; ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // @@ -68,6 +74,13 @@ public class QFrontendTableMetaData { this.fields.put(entry.getKey(), new QFrontendFieldMetaData(entry.getValue())); } + + this.sections = tableMetaData.getSections(); + } + + if(tableMetaData.getIcon() != null && StringUtils.hasContent(tableMetaData.getIcon().getName())) + { + this.iconName = tableMetaData.getIcon().getName(); } } @@ -117,6 +130,17 @@ public class QFrontendTableMetaData + /******************************************************************************* + ** Getter for sections + ** + *******************************************************************************/ + public List getSections() + { + return sections; + } + + + /******************************************************************************* ** Getter for isHidden ** @@ -129,11 +153,11 @@ public class QFrontendTableMetaData /******************************************************************************* - ** Setter for isHidden + ** Getter for iconName ** *******************************************************************************/ - public void setIsHidden(boolean isHidden) + public String getIconName() { - this.isHidden = isHidden; + return iconName; } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java index a63f66ba..07105a34 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java @@ -27,7 +27,7 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; /******************************************************************************* @@ -44,6 +44,7 @@ public class QProcessMetaData implements QAppChildMetaData private List stepList; private String parentAppName; + private QIcon icon; @@ -321,4 +322,38 @@ public class QProcessMetaData implements QAppChildMetaData this.parentAppName = parentAppName; } + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } + + + + /******************************************************************************* + ** Setter for icon + ** + *******************************************************************************/ + public void setIcon(QIcon icon) + { + this.icon = icon; + } + + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public QProcessMetaData withIcon(QIcon icon) + { + this.icon = icon; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java new file mode 100644 index 00000000..3324e581 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java @@ -0,0 +1,234 @@ +/* + * 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.backend.core.model.metadata.tables; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; + + +/******************************************************************************* + ** A section of fields - a logical grouping. + *******************************************************************************/ +public class QFieldSection +{ + private String name; + private String label; + private Tier tier; + + private List fieldNames; + private QIcon icon; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QFieldSection() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QFieldSection(String name, String label, QIcon icon, Tier tier, List fieldNames) + { + this.name = name; + this.label = label; + this.icon = icon; + this.tier = tier; + this.fieldNames = fieldNames; + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QFieldSection withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public QFieldSection withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for tier + ** + *******************************************************************************/ + public Tier getTier() + { + return tier; + } + + + + /******************************************************************************* + ** Setter for tier + ** + *******************************************************************************/ + public void setTier(Tier tier) + { + this.tier = tier; + } + + + + /******************************************************************************* + ** Fluent setter for tier + ** + *******************************************************************************/ + public QFieldSection withTier(Tier tier) + { + this.tier = tier; + return (this); + } + + + + /******************************************************************************* + ** Getter for fieldNames + ** + *******************************************************************************/ + public List getFieldNames() + { + return fieldNames; + } + + + + /******************************************************************************* + ** Setter for fieldNames + ** + *******************************************************************************/ + public void setFieldNames(List fieldNames) + { + this.fieldNames = fieldNames; + } + + + + /******************************************************************************* + ** Fluent setter for fieldNames + ** + *******************************************************************************/ + public QFieldSection withFieldNames(List fieldNames) + { + this.fieldNames = fieldNames; + return (this); + } + + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } + + + + /******************************************************************************* + ** Setter for icon + ** + *******************************************************************************/ + public void setIcon(QIcon icon) + { + this.icon = icon; + } + + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public QFieldSection withIcon(QIcon icon) + { + this.icon = icon; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index 506ca3d4..6bb2fdae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; import java.io.Serializable; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -64,6 +65,12 @@ public class QTableMetaData implements QAppChildMetaData, Serializable private String parentAppName; private QIcon icon; + private String recordLabelFormat; + private List recordLabelFields; + + private List sections; + + /******************************************************************************* ** Default constructor. @@ -496,6 +503,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable } + /******************************************************************************* ** Fluent setter for icon ** @@ -506,4 +514,131 @@ public class QTableMetaData implements QAppChildMetaData, Serializable return (this); } + + + /******************************************************************************* + ** Getter for recordLabelFormat + ** + *******************************************************************************/ + public String getRecordLabelFormat() + { + return recordLabelFormat; + } + + + + /******************************************************************************* + ** Setter for recordLabelFormat + ** + *******************************************************************************/ + public void setRecordLabelFormat(String recordLabelFormat) + { + this.recordLabelFormat = recordLabelFormat; + } + + + + /******************************************************************************* + ** Fluent setter for recordLabelFormat + ** + *******************************************************************************/ + public QTableMetaData withRecordLabelFormat(String recordLabelFormat) + { + this.recordLabelFormat = recordLabelFormat; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordLabelFields + ** + *******************************************************************************/ + public List getRecordLabelFields() + { + return recordLabelFields; + } + + + + /******************************************************************************* + ** Setter for recordLabelFields + ** + *******************************************************************************/ + public void setRecordLabelFields(List recordLabelFields) + { + this.recordLabelFields = recordLabelFields; + } + + + + /******************************************************************************* + ** Fluent setter for recordLabelFields + ** + *******************************************************************************/ + public QTableMetaData withRecordLabelFields(List recordLabelFields) + { + this.recordLabelFields = recordLabelFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for sections + ** + *******************************************************************************/ + public List getSections() + { + return sections; + } + + + + /******************************************************************************* + ** Setter for sections + ** + *******************************************************************************/ + public void setSections(List sections) + { + this.sections = sections; + } + + + + /******************************************************************************* + ** Fluent setter for sections + ** + *******************************************************************************/ + public QTableMetaData withSections(List fieldSections) + { + this.sections = fieldSections; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addSection(QFieldSection fieldSection) + { + if(this.sections == null) + { + this.sections = new ArrayList<>(); + } + this.sections.add(fieldSection); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData withSection(QFieldSection fieldSection) + { + addSection(fieldSection); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Tier.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Tier.java new file mode 100644 index 00000000..f83a7f7f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Tier.java @@ -0,0 +1,33 @@ +/* + * 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.backend.core.model.metadata.tables; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum Tier +{ + T1, + T2, + T3 +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java index ec5c3365..ff3761f7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java @@ -65,7 +65,6 @@ public class MockQueryAction implements QueryInterface { Serializable value = field.equals("id") ? (i + 1) : getValue(table, field); record.setValue(field, value); - record.setDisplayValue(table.getField(field), value); } queryOutput.addRecord(record); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java new file mode 100644 index 00000000..4be9cf52 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java @@ -0,0 +1,160 @@ +/* + * 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.backend.core.actions.values; + + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for QValueFormatter + *******************************************************************************/ +class QValueFormatterTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFormatValue() + { + assertNull(QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), null)); + + assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1)); + assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), 1000)); + assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(null), 1000)); + assertEquals("$1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.CURRENCY), 1000)); + assertEquals("1,000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2_COMMAS), 1000)); + assertEquals("1000.00", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.DECIMAL2), 1000)); + + assertEquals("1", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1"))); + assertEquals("1,000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000"))); + assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); + assertEquals("1000", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); + + ////////////////////////////////////////////////// + // this one flows through the exceptional cases // + ////////////////////////////////////////////////// + assertEquals("1000.01", QValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.COMMAS), new BigDecimal("1000.01"))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFormatRecordLabel() + { + QTableMetaData table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("firstName", "lastName")); + assertEquals("Darin Kelkhoff", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", "Kelkhoff"))); + assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin"))); + assertEquals("Darin ", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("lastName", null))); + + table = new QTableMetaData().withRecordLabelFormat("%s " + DisplayFormat.CURRENCY).withRecordLabelFields(List.of("firstName", "price")); + assertEquals("Darin $10,000.00", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("firstName", "Darin").withValue("price", new BigDecimal(10000)))); + + table = new QTableMetaData().withRecordLabelFormat(DisplayFormat.DEFAULT).withRecordLabelFields(List.of("id")); + assertEquals("123456", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", "123456"))); + + /////////////////////////////////////////////////////// + // exceptional flow: no recordLabelFormat specified // + /////////////////////////////////////////////////////// + table = new QTableMetaData().withPrimaryKeyField("id"); + assertEquals("42", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 42))); + + ///////////////////////////////////////////////// + // exceptional flow: no fields for the format // + ///////////////////////////////////////////////// + table = new QTableMetaData().withRecordLabelFormat("%s %s").withPrimaryKeyField("id"); + assertEquals("128", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("id", 128))); + + ///////////////////////////////////////////////////////// + // exceptional flow: not enough fields for the format // + ///////////////////////////////////////////////////////// + table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a")).withPrimaryKeyField("id"); + assertEquals("256", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("id", 256))); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // exceptional flow (kinda): too many fields for the format (just get the ones that are in the format) // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + table = new QTableMetaData().withRecordLabelFormat("%s %s").withRecordLabelFields(List.of("a", "b", "c")).withPrimaryKeyField("id"); + assertEquals("47 48", QValueFormatter.formatRecordLabel(table, new QRecord().withValue("a", 47).withValue("b", 48).withValue("c", 49).withValue("id", 256))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetDisplayValuesInRecords() + { + QTableMetaData table = new QTableMetaData() + .withRecordLabelFormat("%s %s") + .withRecordLabelFields(List.of("firstName", "lastName")) + .withField(new QFieldMetaData("firstName", QFieldType.STRING)) + .withField(new QFieldMetaData("lastName", QFieldType.STRING)) + .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) + .withField(new QFieldMetaData("quantity", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)); + + ///////////////////////////////////////////////////////////////// + // first, make sure it doesn't crash with null or empty inputs // + ///////////////////////////////////////////////////////////////// + QValueFormatter.setDisplayValuesInRecords(table, null); + QValueFormatter.setDisplayValuesInRecords(table, Collections.emptyList()); + + List records = List.of( + new QRecord() + .withValue("firstName", "Tim") + .withValue("lastName", "Chamberlain") + .withValue("price", new BigDecimal("3.50")) + .withValue("quantity", 1701), + new QRecord() + .withValue("firstName", "Tyler") + .withValue("lastName", "Samples") + .withValue("price", new BigDecimal("174999.99")) + .withValue("quantity", 47) + ); + + QValueFormatter.setDisplayValuesInRecords(table, records); + + assertEquals("Tim Chamberlain", records.get(0).getRecordLabel()); + assertEquals("$3.50", records.get(0).getDisplayValue("price")); + assertEquals("1,701", records.get(0).getDisplayValue("quantity")); + + assertEquals("Tyler Samples", records.get(1).getRecordLabel()); + assertEquals("$174,999.99", records.get(1).getDisplayValue("price")); + assertEquals("47", records.get(1).getDisplayValue("quantity")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 743f43ef..a5cefc2c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -24,10 +24,17 @@ package com.kingsrook.qqq.backend.core.instances; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -283,6 +290,124 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionsMissingName() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection(null, "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "Missing a name"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionsMissingLabel() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", null, new QIcon("person"), Tier.T1, List.of("id"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "Missing a label"); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionsNoFields() + { + QTableMetaData table1 = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of())) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table1), "section1 does not have any fields", "field id is not listed in any field sections"); + + QTableMetaData table2 = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, null)) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table2), "section1 does not have any fields", "field id is not listed in any field sections"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionsUnrecognizedFieldName() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id", "od"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "not a field on this table"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionsDuplicatedFieldName() + { + QTableMetaData table1 = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id", "id"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table1), "more than once"); + + QTableMetaData table2 = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("section2", "Section 2", new QIcon("person"), Tier.T2, List.of("id"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table2), "more than once"); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldNotInAnySections() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "not listed in any field sections"); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSectionsMultipleTier1() + { + QTableMetaData table = new QTableMetaData().withName("test") + .withBackendName(TestUtils.DEFAULT_BACKEND_NAME) + .withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("section2", "Section 2", new QIcon("person"), Tier.T1, List.of("name"))) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)); + assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section listed as Tier 1"); + } + + + /******************************************************************************* ** Run a little setup code on a qInstance; then validate it, and assert that it ** failed validation with reasons that match the supplied vararg-reasons (but allow diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index 5354578a..96b75742 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -22,10 +22,12 @@ package com.kingsrook.qqq.backend.module.rdbms; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; @@ -48,11 +50,25 @@ public class TestUtils QInstance qInstance = new QInstance(); qInstance.addBackend(defineBackend()); qInstance.addTable(defineTablePerson()); + qInstance.setAuthentication(defineAuthentication()); return (qInstance); } + /******************************************************************************* + ** Define the authentication used in standard tests - using 'mock' type. + ** + *******************************************************************************/ + public static QAuthenticationMetaData defineAuthentication() + { + return new QAuthenticationMetaData() + .withName("mock") + .withType(QAuthenticationType.MOCK); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 5e53c0f2..3e4c37ba 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -23,16 +23,20 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; /******************************************************************************* @@ -416,7 +420,29 @@ public class RDBMSQueryActionTest extends RDBMSActionTest QueryInput queryInput = new QueryInput(); queryInput.setInstance(TestUtils.defineInstance()); queryInput.setTableName(TestUtils.defineTablePerson().getName()); + queryInput.setSession(new QSession()); return queryInput; } + + + /******************************************************************************* + ** This doesn't really test any RDBMS code, but is a checkpoint that the core + ** module is populating displayValues when it performs the system-level query action. + *******************************************************************************/ + @Test + public void testThatDisplayValuesGetSetGoingThroughQueryAction() throws QException + { + QueryInput queryInput = initQueryRequest(); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + Assertions.assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); + + for(QRecord record : queryOutput.getRecords()) + { + assertThat(record.getValues()).isNotEmpty(); + assertThat(record.getDisplayValues()).isNotEmpty(); + assertThat(record.getErrors()).isEmpty(); + } + } + } \ No newline at end of file diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java index 87458a05..ed82ff44 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java @@ -33,16 +33,20 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.processes.implementations.general.LoadInitialRecordsStep; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; @@ -123,18 +127,25 @@ public class SampleMetaDataProvider { qInstance.addApp(new QAppMetaData() .withName(APP_NAME_GREETINGS) - .withChild(qInstance.getProcess(PROCESS_NAME_GREET)) - .withChild(qInstance.getProcess(PROCESS_NAME_GREET_INTERACTIVE))); + .withIcon(new QIcon().withName("emoji_people")) + .withChild(qInstance.getProcess(PROCESS_NAME_GREET) + .withIcon(new QIcon().withName("emoji_people"))) + .withChild(qInstance.getTable(TABLE_NAME_PERSON) + .withIcon(new QIcon().withName("person"))) + .withChild(qInstance.getTable(TABLE_NAME_CITY) + .withIcon(new QIcon().withName("location_city"))) + .withChild(qInstance.getProcess(PROCESS_NAME_GREET_INTERACTIVE)) + .withIcon(new QIcon().withName("waving_hand"))); qInstance.addApp(new QAppMetaData() .withName(APP_NAME_PEOPLE) - .withChild(qInstance.getTable(TABLE_NAME_PERSON)) - .withChild(qInstance.getTable(TABLE_NAME_CITY)) + .withIcon(new QIcon().withName("person")) .withChild(qInstance.getApp(APP_NAME_GREETINGS))); qInstance.addApp(new QAppMetaData() .withName(APP_NAME_MISCELLANEOUS) - .withChild(qInstance.getTable(TABLE_NAME_CARRIER)) + .withIcon(new QIcon().withName("stars")) + .withChild(qInstance.getTable(TABLE_NAME_CARRIER).withIcon(new QIcon("local_shipping"))) .withChild(qInstance.getProcess(PROCESS_NAME_SIMPLE_SLEEP)) .withChild(qInstance.getProcess(PROCESS_NAME_SLEEP_INTERACTIVE)) .withChild(qInstance.getProcess(PROCESS_NAME_SIMPLE_THROW))); @@ -205,19 +216,25 @@ public class SampleMetaDataProvider table.setName(TABLE_NAME_CARRIER); table.setBackendName(RDBMS_BACKEND_NAME); table.setPrimaryKeyField("id"); + table.setRecordLabelFormat("%s"); + table.setRecordLabelFields(List.of("name")); table.addField(new QFieldMetaData("id", QFieldType.INTEGER)); table.addField(new QFieldMetaData("name", QFieldType.STRING) .withIsRequired(true)); - table.addField(new QFieldMetaData("company_code", QFieldType.STRING) // todo enum + table.addField(new QFieldMetaData("company_code", QFieldType.STRING) // todo PVS .withLabel("Company") .withIsRequired(true) .withBackendName("comp_code")); - table.addField(new QFieldMetaData("service_level", QFieldType.STRING) - .withIsRequired(true)); // todo enum + table.addField(new QFieldMetaData("service_level", QFieldType.STRING) // todo PVS + .withLabel("Service Level") + .withIsRequired(true)); + + table.addSection(new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, List.of("id", "name"))); + table.addSection(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("company_code", "service_level"))); return (table); } @@ -234,13 +251,24 @@ public class SampleMetaDataProvider .withLabel("Person") .withBackendName(RDBMS_BACKEND_NAME) .withPrimaryKeyField("id") - .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date")) - .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date")) + .withRecordLabelFormat("%s %s") + .withRecordLabelFields(List.of("firstName", "lastName")) + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) .withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name").withIsRequired(true)) .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) - .withField(new QFieldMetaData("email", QFieldType.STRING)); + .withField(new QFieldMetaData("email", QFieldType.STRING)) + .withField(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary").withDisplayFormat(DisplayFormat.CURRENCY)) + .withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked").withDisplayFormat(DisplayFormat.COMMAS)) + + .withSection(new QFieldSection("identity", "Identity", new QIcon("badge"), Tier.T1, List.of("id", "firstName", "lastName"))) + .withSection(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate"))) + .withSection(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("annualSalary", "daysWorked"))) + .withSection(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))) + + .withInferredFieldBackendNames(); } diff --git a/qqq-sample-project/src/test/resources/prime-test-database.sql b/qqq-sample-project/src/test/resources/prime-test-database.sql index 07ab6ac6..ef295c31 100644 --- a/qqq-sample-project/src/test/resources/prime-test-database.sql +++ b/qqq-sample-project/src/test/resources/prime-test-database.sql @@ -29,14 +29,17 @@ CREATE TABLE person first_name VARCHAR(80) NOT NULL, last_name VARCHAR(80) NOT NULL, birth_date DATE, - email VARCHAR(250) NOT NULL + email VARCHAR(250) NOT NULL, + + annual_salary DECIMAL(12, 2), + days_worked INTEGER ); -INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com'); -INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com'); -INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com'); -INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com'); -INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 75003.50, 1001); +INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 150000, 10100); +INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 300000, 100100); +INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 950000, 75); +INSERT INTO person (id, first_name, last_name, birth_date, email, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1500000, 1); DROP TABLE IF EXISTS carrier; CREATE TABLE carrier