CE-1115 pre-QA commit on saved report UI, including:

- adding pre-insert/update validation
- move json field value formatting from post-query customizer to a FieldDisplayBehavior instead (works for audits this way :)
This commit is contained in:
2024-04-14 20:14:32 -05:00
parent 32021fc36e
commit c89fe958c3
9 changed files with 800 additions and 42 deletions

View File

@ -303,7 +303,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
@ -329,8 +329,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(table, oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
@ -464,7 +464,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
private static String getFormattedValueForAuditDetail(QRecord record, String fieldName, QFieldMetaData field, Serializable value)
private static String getFormattedValueForAuditDetail(QTableMetaData table, QRecord record, String fieldName, QFieldMetaData field, Serializable value)
{
String formattedValue = null;
if(value != null)
@ -479,7 +479,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
formattedValue = QValueFormatter.formatValue(field, value);
QValueFormatter.setDisplayValuesInRecord(table, table.getFields(), record);
formattedValue = record.getDisplayValue(fieldName);
}
}

View File

@ -531,7 +531,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
{
if(fieldName.indexOf('.') > -1)
{
@ -1097,5 +1097,22 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
private record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {}
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}
}

View File

@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.model.savedreports;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@ -35,6 +37,7 @@ public class ReportColumns implements Serializable
private List<ReportColumn> columns;
/*******************************************************************************
** Getter for columns
*******************************************************************************/
@ -45,6 +48,23 @@ public class ReportColumns implements Serializable
/*******************************************************************************
**
*******************************************************************************/
public List<ReportColumn> extractVisibleColumns()
{
return CollectionUtils.nonNullList(getColumns()).stream()
//////////////////////////////////////////////////////
// if isVisible is missing, we assume it to be true //
//////////////////////////////////////////////////////
.filter(rc -> rc.getIsVisible() == null || rc.getIsVisible())
.filter(rc -> StringUtils.hasContent(rc.getName()))
.filter(rc -> !rc.getName().startsWith("__check"))
.toList();
}
/*******************************************************************************
** Setter for columns
*******************************************************************************/
@ -65,6 +85,7 @@ public class ReportColumns implements Serializable
}
/*******************************************************************************
** Fluent setter to add 1 column
*******************************************************************************/
@ -79,6 +100,7 @@ public class ReportColumns implements Serializable
}
/*******************************************************************************
** Fluent setter to add 1 column w/ just a name
*******************************************************************************/

View File

@ -0,0 +1,150 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.savedreports;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
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.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class SavedReportJsonFieldDisplayValueFormatter implements FieldDisplayBehavior<SavedReportJsonFieldDisplayValueFormatter>
{
private static SavedReportJsonFieldDisplayValueFormatter savedReportJsonFieldDisplayValueFormatter = null;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private SavedReportJsonFieldDisplayValueFormatter()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static SavedReportJsonFieldDisplayValueFormatter getInstance()
{
if(savedReportJsonFieldDisplayValueFormatter == null)
{
savedReportJsonFieldDisplayValueFormatter = new SavedReportJsonFieldDisplayValueFormatter();
}
return (savedReportJsonFieldDisplayValueFormatter);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public SavedReportJsonFieldDisplayValueFormatter getDefault()
{
return getInstance();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
if(field.getName().equals("queryFilterJson"))
{
String queryFilterJson = record.getValueString("queryFilterJson");
if(StringUtils.hasContent(queryFilterJson))
{
try
{
QQueryFilter qQueryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson);
int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size();
record.setDisplayValue("queryFilterJson", criteriaCount + " Filter" + StringUtils.plural(criteriaCount));
}
catch(Exception e)
{
record.setDisplayValue("queryFilterJson", "Invalid Filter...");
}
}
}
if(field.getName().equals("columnsJson"))
{
String columnsJson = record.getValueString("columnsJson");
if(StringUtils.hasContent(columnsJson))
{
try
{
ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson);
int columnCount = reportColumns.extractVisibleColumns().size();
record.setDisplayValue("columnsJson", columnCount + " Column" + StringUtils.plural(columnCount));
}
catch(Exception e)
{
record.setDisplayValue("columnsJson", "Invalid Columns...");
}
}
}
if(field.getName().equals("pivotTableJson"))
{
String pivotTableJson = record.getValueString("pivotTableJson");
if(StringUtils.hasContent(pivotTableJson))
{
try
{
PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson);
int rowCount = CollectionUtils.nonNullList(pivotTableDefinition.getRows()).size();
int columnCount = CollectionUtils.nonNullList(pivotTableDefinition.getColumns()).size();
int valueCount = CollectionUtils.nonNullList(pivotTableDefinition.getValues()).size();
record.setDisplayValue("pivotTableJson", rowCount + " Row" + StringUtils.plural(rowCount) + ", " + columnCount + " Column" + StringUtils.plural(columnCount) + ", and " + valueCount + " Value" + StringUtils.plural(valueCount));
}
catch(Exception e)
{
record.setDisplayValue("pivotTableJson", "Invalid Pivot Table...");
}
}
}
}
}
}

View File

@ -22,17 +22,26 @@
package com.kingsrook.qqq.backend.core.model.savedreports;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
@ -45,63 +54,229 @@ public class SavedReportTableCustomizer implements TableCustomizerInterface
**
*******************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
return (preInsertOrUpdate(records));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
return (preInsertOrUpdate(records));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> preInsertOrUpdate(List<QRecord> records)
{
for(QRecord record : CollectionUtils.nonNullList(records))
{
preValidateRecord(record);
}
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
void preValidateRecord(QRecord record)
{
try
{
String tableName = record.getValueString("tableName");
String queryFilterJson = record.getValueString("queryFilterJson");
String columnsJson = record.getValueString("columnsJson");
String pivotTableJson = record.getValueString("pivotTableJson");
Set<String> usedColumns = new HashSet<>();
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
record.addError(new BadInputStatusMessage("Unrecognized table name: " + tableName));
}
if(StringUtils.hasContent(queryFilterJson))
{
try
{
QQueryFilter qQueryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson);
int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size();
record.setDisplayValue("queryFilterJson", criteriaCount + " Filter" + StringUtils.plural(criteriaCount));
////////////////////////////////////////////////////////////////
// nothing to validate on filter, other than, we can parse it //
////////////////////////////////////////////////////////////////
SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson);
}
catch(Exception e)
catch(IOException e)
{
record.setDisplayValue("queryFilterJson", "Invalid Filter...");
record.addError(new BadInputStatusMessage("Unable to parse queryFilterJson: " + e.getMessage()));
}
}
boolean hadColumnParseError = false;
if(StringUtils.hasContent(columnsJson))
{
try
{
/////////////////////////////////////////////////////////////////////////
// make sure we can parse columns, and that we have at least 1 visible //
/////////////////////////////////////////////////////////////////////////
ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson);
long columnCount = CollectionUtils.nonNullList(reportColumns.getColumns())
.stream().filter(rc -> BooleanUtils.isTrue(rc.getIsVisible()))
.count();
record.setDisplayValue("columnsJson", columnCount + " Column" + StringUtils.plural((int) columnCount));
for(ReportColumn column : reportColumns.extractVisibleColumns())
{
usedColumns.add(column.getName());
}
}
catch(Exception e)
catch(IOException e)
{
record.setDisplayValue("columnsJson", "Invalid Columns...");
record.addError(new BadInputStatusMessage("Unable to parse columnsJson: " + e.getMessage()));
hadColumnParseError = true;
}
}
if(usedColumns.isEmpty() && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A Report must contain at least 1 column"));
}
if(StringUtils.hasContent(pivotTableJson))
{
try
{
PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson);
int rowCount = CollectionUtils.nonNullList(pivotTableDefinition.getRows()).size();
int columnCount = CollectionUtils.nonNullList(pivotTableDefinition.getColumns()).size();
int valueCount = CollectionUtils.nonNullList(pivotTableDefinition.getValues()).size();
record.setDisplayValue("pivotTableJson", rowCount + " Row" + StringUtils.plural(rowCount) + ", " + columnCount + " Column" + StringUtils.plural(columnCount) + ", and " + valueCount + " Value" + StringUtils.plural(valueCount));
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make sure we can parse pivot table, and we have ... at least 1 ... row? maybe that's all that's needed //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson);
boolean anyRows = false;
boolean missingAnyFieldNamesInRows = false;
boolean missingAnyFieldNamesInColumns = false;
boolean missingAnyFieldNamesInValues = false;
boolean missingAnyFunctionsInValues = false;
//////////////////
// look at rows //
//////////////////
for(PivotTableGroupBy row : CollectionUtils.nonNullList(pivotTableDefinition.getRows()))
{
anyRows = true;
if(StringUtils.hasContent(row.getFieldName()))
{
if(!usedColumns.contains(row.getFieldName()) && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A pivot table row is using field (" + getFieldLabelElseName(table, row.getFieldName()) + ") which is not an active column on this report."));
}
}
else
{
missingAnyFieldNamesInRows = true;
}
}
if(!anyRows)
{
record.addError(new BadInputStatusMessage("A Pivot Table must contain at least 1 row"));
}
/////////////////////
// look at columns //
/////////////////////
for(PivotTableGroupBy column : CollectionUtils.nonNullList(pivotTableDefinition.getColumns()))
{
if(StringUtils.hasContent(column.getFieldName()))
{
if(!usedColumns.contains(column.getFieldName()) && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A pivot table column is using field (" + getFieldLabelElseName(table, column.getFieldName()) + ") which is not an active column on this report."));
}
}
else
{
missingAnyFieldNamesInColumns = true;
}
}
////////////////////
// look at values //
////////////////////
for(PivotTableValue value : CollectionUtils.nonNullList(pivotTableDefinition.getValues()))
{
if(StringUtils.hasContent(value.getFieldName()))
{
if(!usedColumns.contains(value.getFieldName()) && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A pivot table value is using field (" + getFieldLabelElseName(table, value.getFieldName()) + ") which is not an active column on this report."));
}
}
else
{
missingAnyFieldNamesInValues = true;
}
if(value.getFunction() == null)
{
missingAnyFunctionsInValues = true;
}
}
////////////////////////////////////////////////
// errors based on missing things found above //
////////////////////////////////////////////////
if(missingAnyFieldNamesInRows)
{
record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table row."));
}
if(missingAnyFieldNamesInColumns)
{
record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table column."));
}
if(missingAnyFieldNamesInValues)
{
record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table value."));
}
if(missingAnyFunctionsInValues)
{
record.addError(new BadInputStatusMessage("Missing function for at least one pivot table value."));
}
}
catch(Exception e)
catch(IOException e)
{
record.setDisplayValue("pivotTableJson", "Invalid Pivot Table...");
record.addError(new BadInputStatusMessage("Unable to parse pivotTableJson: " + e.getMessage()));
}
}
}
catch(Exception e)
{
LOG.warn("Error validating a savedReport");
}
}
return (records);
/*******************************************************************************
**
*******************************************************************************/
private String getFieldLabelElseName(QTableMetaData table, String fieldName)
{
try
{
GenerateReportAction.FieldAndJoinTable fieldAndJoinTable = GenerateReportAction.getFieldAndJoinTable(table, fieldName);
return (fieldAndJoinTable.getLabel(table));
}
catch(Exception e)
{
return (fieldName);
}
}
}

View File

@ -30,6 +30,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
@ -147,6 +149,7 @@ public class SavedReportsMetaDataProvider
.withBackendName(backendName)
.withPrimaryKeyField("id")
.withFieldsFromEntity(SavedReport.class)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName")))
.withSection(new QFieldSection("filtersAndColumns", new QIcon().withName("table_chart"), Tier.T2).withLabel("Filters and Columns").withWidgetName("reportSetupWidget"))
.withSection(new QFieldSection("pivotTable", new QIcon().withName("pivot_table_chart"), Tier.T2).withLabel("Pivot Table").withWidgetName("pivotTableSetupWidget"))
@ -154,7 +157,12 @@ public class SavedReportsMetaDataProvider
.withSection(new QFieldSection("hidden", new QIcon().withName("text_snippet"), Tier.T2, List.of("inputFieldsJson", "userId")).withIsHidden(true))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
table.getField("queryFilterJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
table.getField("columnsJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
table.getField("pivotTableJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
table.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
if(backendDetailEnricher != null)
{

View File

@ -57,7 +57,6 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -122,16 +121,8 @@ public class SavedReportToReportMetaDataAdapter
Set<String> neededJoinTables = new HashSet<>();
for(ReportColumn column : columnsObject.getColumns())
for(ReportColumn column : columnsObject.extractVisibleColumns())
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if isVisible is missing, we assume it to be true - so only if it isFalse do we skip the column //
////////////////////////////////////////////////////////////////////////////////////////////////////
if(BooleanUtils.isFalse(column.getIsVisible()))
{
continue;
}
////////////////////////////////////////////////////
// figure out the field being named by the column //
////////////////////////////////////////////////////