First version of no-code dashboard widgets

This commit is contained in:
2023-02-13 10:30:51 -06:00
parent d9a17ac99b
commit ff6c2b7fa6
18 changed files with 1659 additions and 66 deletions

View File

@ -33,6 +33,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -120,7 +121,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableBulkLoad(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + tableName + ".bulkInsert");
}
@ -131,7 +132,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableBulkLoadChildren(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
@ -147,7 +148,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableCreate(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create");
}
@ -158,7 +159,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create?defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
}
@ -170,7 +171,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
public static String getCountLink(RenderWidgetInput input, String tableName, QQueryFilter filter, int count) throws QException
{
String totalString = QValueFormatter.formatValue(DisplayFormat.COMMAS, count);
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null || filter == null)
{
return (totalString);
@ -185,7 +186,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static void addTableFilterToListIfPermissed(RenderWidgetInput input, String tableName, List<String> urls, QQueryFilter filter) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return;
@ -201,7 +202,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableFilterUnencoded(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
@ -217,7 +218,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableFilter(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
@ -234,7 +235,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException
{
String displayText = QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + " " + StringUtils.plural(noOfRecords, singularLabel, pluralLabel);
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (displayText);
@ -251,7 +252,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String aHrefViewRecord(RenderWidgetInput input, String tableName, Serializable id, String linkText) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (linkText);
@ -267,7 +268,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/edit");
}
@ -278,7 +279,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkRecordView(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
@ -294,9 +295,9 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException
{
QProcessMetaData process = input.getInstance().getProcess(processName);
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
String tableName = process.getTableName();
String tablePath = input.getInstance().getTablePath(tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/" + processName);
}
@ -327,7 +328,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields) throws QException
{
String tablePath = input.getInstance().getTablePath(childTableName);
String tablePath = QContext.getQInstance().getTablePath(childTableName);
if(tablePath == null)
{
return (null);
@ -347,7 +348,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String aHrefTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields) throws QException
{
String tablePath = input.getInstance().getTablePath(childTableName);
String tablePath = QContext.getQInstance().getTablePath(childTableName);
if(tablePath == null)
{
return (null);

View File

@ -0,0 +1,123 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.actions.dashboard.widgets;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWidgetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWidgetValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.QNoCodeWidgetMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
/*******************************************************************************
**
*******************************************************************************/
public class NoCodeWidgetRenderer extends AbstractWidgetRenderer
{
private static final QLogger LOG = QLogger.getLogger(NoCodeWidgetRenderer.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
QNoCodeWidgetMetaData widgetMetaData = (QNoCodeWidgetMetaData) input.getWidgetMetaData();
////////////////////////////////////////////
// build context by evaluating all values //
////////////////////////////////////////////
Map<String, Object> context = new HashMap<>();
for(AbstractWidgetValueSource valueSource : widgetMetaData.getValues())
{
LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "...");
Object value = valueSource.evaluate(context);
LOG.trace("Computed: " + valueSource.getName() + " = " + value);
context.put(valueSource.getName(), value);
context.put(valueSource.getName() + ".source", valueSource);
}
/////////////////////////////////////////////
// set default utils object in context too //
/////////////////////////////////////////////
context.put("utils", new NoCodeWidgetVelocityUtils(context));
/////////////////////////////////////////////
// build content by evaluating all outputs //
/////////////////////////////////////////////
StringBuilder content = new StringBuilder();
for(AbstractWidgetOutput output : widgetMetaData.getOutputs())
{
boolean conditionPassed = true;
if(output.getCondition() != null)
{
conditionPassed = evaluateCondition(output.getCondition(), context);
}
if(conditionPassed)
{
String render = output.render(context);
content.append(render);
LOG.trace("Condition passed, rendered: " + render);
}
else
{
LOG.trace("Condition failed - not rendering this output.");
}
}
return (new RenderWidgetOutput(new RawHTML(widgetMetaData.getLabel(), content.toString())));
}
/*******************************************************************************
**
*******************************************************************************/
private boolean evaluateCondition(QFilterCriteria condition, Map<String, Object> context)
{
try
{
Object value = context.get(condition.getFieldName());
return (BackendQueryFilterUtils.doesCriteriaMatch(condition, condition.getFieldName(), (Serializable) value));
}
catch(Exception e)
{
LOG.warn("Error evaluating condition: " + condition, e);
return (false);
}
}
}

View File

@ -0,0 +1,113 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.actions.dashboard.widgets;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractHTMLWidgetRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetCount;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class NoCodeWidgetVelocityUtils
{
private static final QLogger LOG = QLogger.getLogger(NoCodeWidgetVelocityUtils.class);
/*******************************************************************************
**
*******************************************************************************/
private final Map<String, Object> context;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public NoCodeWidgetVelocityUtils(Map<String, Object> context)
{
this.context = context;
}
/*******************************************************************************
**
*******************************************************************************/
public final String errorIcon()
{
return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: red; position: relative; top: 6px;" aria-hidden="true">error_outline</span>
""");
}
/*******************************************************************************
**
*******************************************************************************/
public final String checkIcon()
{
return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: green; position: relative; top: 6px;" aria-hidden="true">check</span>
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String plural(Integer size, String ifOne, String ifNotOne)
{
return StringUtils.plural(size, ifOne, ifNotOne);
}
/*******************************************************************************
**
*******************************************************************************/
public String tableCountFilterLink(String countVariableName, String singular, String plural) throws QException
{
try
{
WidgetCount widgetCount = (WidgetCount) context.get(countVariableName + ".source");
Integer count = ValueUtils.getValueAsInteger(context.get(countVariableName));
QQueryFilter filter = widgetCount.getFilter();
return (AbstractHTMLWidgetRenderer.aHrefTableFilterNoOfRecords(null, widgetCount.getTableName(), filter, count, singular, plural));
}
catch(Exception e)
{
LOG.warn("Error rendering widget link", e);
return ("");
}
}
}

View File

@ -29,6 +29,7 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -72,9 +73,9 @@ public class PermissionsHelper
private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
warnAboutPermissionSubTypeForTables(permissionSubType);
QTableMetaData table = actionInput.getInstance().getTable(tableName);
QTableMetaData table = QContext.getQInstance().getTable(tableName);
commonCheckPermissionThrowing(getEffectivePermissionRules(table, actionInput.getInstance()), permissionSubType, table.getName(), actionInput);
commonCheckPermissionThrowing(getEffectivePermissionRules(table, QContext.getQInstance()), permissionSubType, table.getName(), actionInput);
}
@ -102,7 +103,7 @@ public class PermissionsHelper
*******************************************************************************/
public static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules)
{
QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, actionInput.getInstance());
QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, QContext.getQInstance());
String permissionBaseName = getEffectivePermissionBaseName(rules, metaDataWithPermissionRules.getName());
switch(rules.getLevel())
@ -167,8 +168,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
{
QProcessMetaData process = actionInput.getInstance().getProcess(processName);
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, actionInput.getInstance());
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, QContext.getQInstance());
if(effectivePermissionRules.getCustomPermissionChecker() != null)
{
@ -208,8 +209,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
{
QAppMetaData app = actionInput.getInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput);
QAppMetaData app = QContext.getQInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput);
}
@ -237,8 +238,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
{
QReportMetaData report = actionInput.getInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput);
QReportMetaData report = QContext.getQInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput);
}
@ -266,8 +267,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
{
QWidgetMetaDataInterface widget = actionInput.getInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput);
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput);
}

View File

@ -44,7 +44,7 @@ public class QFilterCriteria implements Serializable, Cloneable
private QCriteriaOperator operator;
private List<Serializable> values;
private String otherFieldName;
private String otherFieldName;
private AbstractFilterExpression<?> expression;
@ -98,6 +98,23 @@ public class QFilterCriteria implements Serializable, Cloneable
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteria(String fieldName, QCriteriaOperator operator, AbstractFilterExpression<?> expression)
{
this.fieldName = fieldName;
this.operator = operator;
this.expression = expression;
///////////////////////////////////////
// this guy doesn't like to be null? //
///////////////////////////////////////
this.values = new ArrayList<>();
}
/*******************************************************************************
**
*******************************************************************************/
@ -305,6 +322,8 @@ public class QFilterCriteria implements Serializable, Cloneable
return (rs.toString());
}
/*******************************************************************************
** Getter for expression
*******************************************************************************/
@ -334,5 +353,4 @@ public class QFilterCriteria implements Serializable, Cloneable
return (this);
}
}

View File

@ -90,6 +90,7 @@ public class QInstance
private Map<String, QQueueMetaData> queues = new LinkedHashMap<>();
private Map<String, String> environmentValues = new LinkedHashMap<>();
private String defaultTimeZoneId = "UTC";
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
@ -993,6 +994,37 @@ public class QInstance
/*******************************************************************************
** Setter for defaultTimeZoneId
*******************************************************************************/
public void setDefaultTimeZoneId(String defaultTimeZoneId)
{
this.defaultTimeZoneId = defaultTimeZoneId;
}
/*******************************************************************************
** Fluent setter for defaultTimeZoneId
*******************************************************************************/
public QInstance withDefaultTimeZoneId(String defaultTimeZoneId)
{
this.defaultTimeZoneId = defaultTimeZoneId;
return (this);
}
/*******************************************************************************
** Getter for defaultTimeZoneId
*******************************************************************************/
public String getDefaultTimeZoneId()
{
return (this.defaultTimeZoneId);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,107 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractWidgetOutput
{
protected QFilterCriteria condition;
protected String type;
/*******************************************************************************
**
*******************************************************************************/
public abstract String render(Map<String, Object> context) throws QException;
/*******************************************************************************
** Getter for condition
*******************************************************************************/
public QFilterCriteria getCondition()
{
return (this.condition);
}
/*******************************************************************************
** Setter for condition
*******************************************************************************/
public void setCondition(QFilterCriteria condition)
{
this.condition = condition;
}
/*******************************************************************************
** Fluent setter for condition
*******************************************************************************/
public AbstractWidgetOutput withCondition(QFilterCriteria condition)
{
this.condition = condition;
return (this);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public AbstractWidgetOutput withType(String type)
{
this.type = type;
return (this);
}
}

View File

@ -0,0 +1,118 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractWidgetValueSource
{
protected String name;
protected String type;
/*******************************************************************************
**
*******************************************************************************/
public abstract Object evaluate(Map<String, Object> context) throws QException;
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public AbstractWidgetValueSource withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public AbstractWidgetValueSource withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void supplementContext(Map<String, Object> context)
{
////////////////////////
// noop in base class //
////////////////////////
}
}

View File

@ -0,0 +1,73 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.Objects;
/*******************************************************************************
**
*******************************************************************************/
public class HtmlWrapper
{
private String prefix;
private String suffix;
public static final HtmlWrapper SUBHEADER = new HtmlWrapper("<h4>", "</h4>");
public static final HtmlWrapper INDENT_1 = new HtmlWrapper("<div style='padding-left: 1rem;'>", "</div>");
public static final HtmlWrapper INDENT_2 = new HtmlWrapper("<div style='padding-left: 2rem;'>", "</div>");
/*******************************************************************************
**
*******************************************************************************/
public HtmlWrapper(String prefix, String suffix)
{
this.prefix = prefix;
this.suffix = suffix;
}
/*******************************************************************************
**
*******************************************************************************/
public static HtmlWrapper paddingTop(String amount)
{
return (new HtmlWrapper("<div style='padding-top: " + amount + "'>", "</div>"));
}
/*******************************************************************************
**
*******************************************************************************/
public String wrap(String content)
{
return (Objects.requireNonNullElse(prefix, "")
+ "\n" + Objects.requireNonNullElse(content, "") + "\n"
+ Objects.requireNonNullElse(suffix, "") + "\n");
}
}

View File

@ -0,0 +1,130 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class QNoCodeWidgetMetaData extends QWidgetMetaData
{
private List<AbstractWidgetValueSource> values = new ArrayList<>();
private List<AbstractWidgetOutput> outputs = new ArrayList<>();
/*******************************************************************************
** Getter for values
*******************************************************************************/
public List<AbstractWidgetValueSource> getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(List<AbstractWidgetValueSource> values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter to add a single value
*******************************************************************************/
public QNoCodeWidgetMetaData withValue(AbstractWidgetValueSource value)
{
if(this.values == null)
{
this.values = new ArrayList<>();
}
this.values.add(value);
return (this);
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public QNoCodeWidgetMetaData withValues(List<AbstractWidgetValueSource> values)
{
this.values = values;
return (this);
}
/*******************************************************************************
** Getter for outputs
*******************************************************************************/
public List<AbstractWidgetOutput> getOutputs()
{
return (this.outputs);
}
/*******************************************************************************
** Setter for outputs
*******************************************************************************/
public void setOutputs(List<AbstractWidgetOutput> outputs)
{
this.outputs = outputs;
}
/*******************************************************************************
** Fluent setter to add a single output
*******************************************************************************/
public QNoCodeWidgetMetaData withOutput(AbstractWidgetOutput output)
{
if(this.outputs == null)
{
this.outputs = new ArrayList<>();
}
this.outputs.add(output);
return (this);
}
/*******************************************************************************
** Fluent setter for outputs
*******************************************************************************/
public QNoCodeWidgetMetaData withOutputs(List<AbstractWidgetOutput> outputs)
{
this.outputs = outputs;
return (this);
}
}

View File

@ -0,0 +1,186 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetCalculation extends AbstractWidgetValueSource
{
private Operator operator;
private List<String> values;
/*******************************************************************************
**
*******************************************************************************/
public enum Operator
{
SUM_INTEGERS((List<String> valueNames, Map<String, Object> context) ->
{
Integer sum = 0;
for(String valueName : valueNames)
{
Integer addend = ValueUtils.getValueAsInteger(context.get(valueName));
sum += addend;
}
return (sum);
}),
AGE_MINUTES((List<String> valueNames, Map<String, Object> context) ->
{
Instant now = Instant.now();
Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0)));
return (then.until(now, ChronoUnit.MINUTES));
});
private final BiFunction<List<String>, Map<String, Object>, Object> function;
/*******************************************************************************
**
*******************************************************************************/
Operator(BiFunction<List<String>, Map<String, Object>, Object> function)
{
this.function = function;
}
/*******************************************************************************
**
*******************************************************************************/
public Object execute(List<String> values, Map<String, Object> context)
{
return (function.apply(values, context));
}
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetCalculation()
{
setType(getClass().getSimpleName());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object evaluate(Map<String, Object> context) throws QException
{
return (operator.execute(values, context));
}
/*******************************************************************************
**
*******************************************************************************/
public WidgetCalculation withName(String name)
{
setName(name);
return (this);
}
/*******************************************************************************
** Getter for operator
*******************************************************************************/
public Operator getOperator()
{
return (this.operator);
}
/*******************************************************************************
** Setter for operator
*******************************************************************************/
public void setOperator(Operator operator)
{
this.operator = operator;
}
/*******************************************************************************
** Fluent setter for operator
*******************************************************************************/
public WidgetCalculation withOperator(Operator operator)
{
this.operator = operator;
return (this);
}
/*******************************************************************************
** Getter for values
*******************************************************************************/
public List<String> getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(List<String> values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public WidgetCalculation withValues(List<String> values)
{
this.values = values;
return (this);
}
}

View File

@ -0,0 +1,152 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetCount extends AbstractWidgetValueSource
{
private String tableName;
private QQueryFilter filter;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetCount()
{
setType(getClass().getSimpleName());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object evaluate(Map<String, Object> context) throws QException
{
// todo - look for params in the filter (fields or values)
// make sure to update it in supplementContext below too!!
CountInput countInput = new CountInput();
countInput.setTableName(tableName);
countInput.setFilter(filter);
CountOutput countOutput = new CountAction().execute(countInput);
return (countOutput.getCount());
}
/*******************************************************************************
**
*******************************************************************************/
public void supplementContext(Map<String, Object> context)
{
context.put(getName() + ".filter", filter);
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public WidgetCount withName(String name)
{
setName(name);
return (this);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public WidgetCount withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public WidgetCount withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
}

View File

@ -0,0 +1,170 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.templates.RenderTemplateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetHtmlLine extends AbstractWidgetOutput
{
private List<HtmlWrapper> wrappers;
private String velocityTemplate;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetHtmlLine()
{
setType(getClass().getSimpleName());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String render(Map<String, Object> context) throws QException
{
RenderTemplateInput renderTemplateInput = new RenderTemplateInput();
renderTemplateInput.setTemplateType(TemplateType.VELOCITY);
renderTemplateInput.setCode(velocityTemplate);
renderTemplateInput.setContext(context);
RenderTemplateOutput renderTemplateOutput = new RenderTemplateAction().execute(renderTemplateInput);
String content = renderTemplateOutput.getResult();
if(wrappers != null)
{
for(int i = wrappers.size() - 1; i >= 0; i--)
{
content = wrappers.get(i).wrap(content);
}
}
return (content);
}
/*******************************************************************************
** Getter for velocityTemplate
*******************************************************************************/
public String getVelocityTemplate()
{
return (this.velocityTemplate);
}
/*******************************************************************************
** Setter for velocityTemplate
*******************************************************************************/
public void setVelocityTemplate(String velocityTemplate)
{
this.velocityTemplate = velocityTemplate;
}
/*******************************************************************************
** Fluent setter for velocityTemplate
*******************************************************************************/
public WidgetHtmlLine withVelocityTemplate(String velocityTemplate)
{
this.velocityTemplate = velocityTemplate;
return (this);
}
/*******************************************************************************
** Fluent setter for condition
*******************************************************************************/
public WidgetHtmlLine withCondition(QFilterCriteria condition)
{
this.condition = condition;
return (this);
}
/*******************************************************************************
** Getter for wrappers
*******************************************************************************/
public List<HtmlWrapper> getWrappers()
{
return (this.wrappers);
}
/*******************************************************************************
** Setter for wrappers
*******************************************************************************/
public void setWrappers(List<HtmlWrapper> wrappers)
{
this.wrappers = wrappers;
}
/*******************************************************************************
** Fluent setter for wrappers
*******************************************************************************/
public WidgetHtmlLine withWrappers(List<HtmlWrapper> wrappers)
{
this.wrappers = wrappers;
return (this);
}
/*******************************************************************************
** Fluent setter to add 1 wrapper
*******************************************************************************/
public WidgetHtmlLine withWrapper(HtmlWrapper wrapper)
{
if(this.wrappers == null)
{
this.wrappers = new ArrayList<>();
}
this.wrappers.add(wrapper);
return (this);
}
}

View File

@ -0,0 +1,191 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.Map;
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.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.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetQueryField extends AbstractWidgetValueSource
{
private String tableName;
private String selectFieldName;
private QQueryFilter filter;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetQueryField()
{
setType(getClass().getSimpleName());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object evaluate(Map<String, Object> context) throws QException
{
// todo - look for params in the filter (fields or values)
// make sure to update it in supplementContext below too!!
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(filter);
queryInput.setLimit(1);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
return (queryOutput.getRecords().get(0).getValue(selectFieldName));
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public void supplementContext(Map<String, Object> context)
{
context.put(getName() + ".filter", filter);
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public WidgetQueryField withName(String name)
{
setName(name);
return (this);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public WidgetQueryField withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public WidgetQueryField withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for selectFieldName
*******************************************************************************/
public String getSelectFieldName()
{
return (this.selectFieldName);
}
/*******************************************************************************
** Setter for selectFieldName
*******************************************************************************/
public void setSelectFieldName(String selectFieldName)
{
this.selectFieldName = selectFieldName;
}
/*******************************************************************************
** Fluent setter for selectFieldName
*******************************************************************************/
public WidgetQueryField withSelectFieldName(String selectFieldName)
{
this.selectFieldName = selectFieldName;
return (this);
}
}

View File

@ -50,7 +50,6 @@ public class BackendQueryFilterUtils
/*******************************************************************************
** Test if record matches filter.
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public static boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord)
{
if(filter == null || !filter.hasAnyCriteria())
@ -72,40 +71,7 @@ public class BackendQueryFilterUtils
String fieldName = criterion.getFieldName();
Serializable value = qRecord.getValue(fieldName);
boolean criterionMatches = switch(criterion.getOperator())
{
case EQUALS -> testEquals(criterion, value);
case NOT_EQUALS -> !testEquals(criterion, value);
case IN -> testIn(criterion, value);
case NOT_IN -> !testIn(criterion, value);
case IS_BLANK -> testBlank(criterion, value);
case IS_NOT_BLANK -> !testBlank(criterion, value);
case CONTAINS -> testContains(criterion, fieldName, value);
case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
case GREATER_THAN -> testGreaterThan(criterion, value);
case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
case BETWEEN ->
{
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
criteria1.getValues().remove(0);
yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
}
case NOT_BETWEEN ->
{
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues());
criteria1.getValues().remove(0);
yield !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
}
};
boolean criterionMatches = doesCriteriaMatch(criterion, fieldName, value);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. //
@ -139,6 +105,51 @@ public class BackendQueryFilterUtils
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public static boolean doesCriteriaMatch(QFilterCriteria criterion, String fieldName, Serializable value)
{
boolean criterionMatches = switch(criterion.getOperator())
{
case EQUALS -> testEquals(criterion, value);
case NOT_EQUALS -> !testEquals(criterion, value);
case IN -> testIn(criterion, value);
case NOT_IN -> !testIn(criterion, value);
case IS_BLANK -> testBlank(criterion, value);
case IS_NOT_BLANK -> !testBlank(criterion, value);
case CONTAINS -> testContains(criterion, fieldName, value);
case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
case GREATER_THAN -> testGreaterThan(criterion, value);
case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
case BETWEEN ->
{
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
criteria1.getValues().remove(0);
yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
}
case NOT_BETWEEN ->
{
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues());
criteria1.getValues().remove(0);
yield !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
}
};
return criterionMatches;
}
/*******************************************************************************
** Based on an incoming boolean value (accumulator), a new value, and a boolean
** operator, update the accumulator, and if we can then short-circuit remaining

View File

@ -52,8 +52,6 @@ public class ValueUtils
private static final DateTimeFormatter dateTimeFormatter_MdyyyyWithSlashes = DateTimeFormatter.ofPattern("M/d/yyyy");
private static final DateTimeFormatter dateTimeFormatter_yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd");
public static final String COMPANY_TIMEZONE_ID = "America/New_York";
/*******************************************************************************

View File

@ -0,0 +1,101 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.actions.dashboard.widgets;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.context.QContext;
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.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.QNoCodeWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetCalculation;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetCount;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for NoCodeWidgetRenderer
*******************************************************************************/
class NoCodeWidgetRendererTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
TestUtils.insertDefaultShapes(QContext.getQInstance());
QNoCodeWidgetMetaData metaData = new QNoCodeWidgetMetaData();
metaData.withValue(new WidgetCount()
.withName("shapeCount")
.withTableName(TestUtils.TABLE_NAME_SHAPE));
metaData.withValue(new WidgetCalculation()
.withName("shapeCountPlusShapeCount")
.withOperator(WidgetCalculation.Operator.SUM_INTEGERS)
.withValues(List.of("shapeCount", "shapeCount")));
metaData.withOutput(new WidgetHtmlLine()
.withWrapper(HtmlWrapper.SUBHEADER)
.withVelocityTemplate("Header"));
metaData.withOutput(new WidgetHtmlLine()
.withCondition(new QFilterCriteria("shapeCount", QCriteriaOperator.GREATER_THAN_OR_EQUALS, 0))
.withWrapper(HtmlWrapper.INDENT_1)
.withVelocityTemplate("""
${utils.checkIcon()} Yes: ${shapeCount} ${utils.plural($shapeCount, "shape", "shapes")}
"""));
metaData.withOutput(new WidgetHtmlLine()
.withCondition(new QFilterCriteria("shapeCount", QCriteriaOperator.EQUALS, 0))
.withVelocityTemplate("No: ${shapeCount}"));
metaData.withOutput(new WidgetHtmlLine()
.withVelocityTemplate("Double: ${shapeCountPlusShapeCount}"));
RenderWidgetInput input = new RenderWidgetInput();
input.setWidgetMetaData(metaData);
RenderWidgetOutput output = new NoCodeWidgetRenderer().render(input);
String html = ((RawHTML) output.getWidgetData()).getHtml();
System.out.println(html);
assertTrue(html.matches("(?s).*<h4>.*Header.*</h4>.*"));
assertTrue(html.matches("(?s).*1rem.*Yes: 3 shapes.*"));
assertTrue(html.matches("(?s).*>check<.*"));
assertFalse(html.matches("(?s).*No: 3.*"));
assertTrue(html.matches("(?s).*Double: 6.*"));
}
}

View File

@ -26,15 +26,18 @@ import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.interfaces.QActionInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
@ -715,6 +718,11 @@ public abstract class AbstractRDBMSAction implements QActionInterface
clauses.add("(" + clause + ")");
if(field.getType().equals(QFieldType.DATE_TIME))
{
values = evaluateDateTimeParamValues(values);
}
params.addAll(values);
}
@ -723,6 +731,66 @@ public abstract class AbstractRDBMSAction implements QActionInterface
/*******************************************************************************
**
*******************************************************************************/
private List<Serializable> evaluateDateTimeParamValues(List<Serializable> values)
{
if(CollectionUtils.nullSafeIsEmpty(values))
{
return (values);
}
List<Serializable> rs = new ArrayList<>();
for(Serializable value : values)
{
if(value instanceof Instant)
{
rs.add(value);
}
else
{
try
{
Instant valueAsInstant = ValueUtils.getValueAsInstant(value);
rs.add(valueAsInstant);
}
catch(Exception e)
{
try
{
Optional<Instant> valueAsRelativeInstant = parseValueAsRelativeInstant(value);
rs.add(valueAsRelativeInstant.orElseThrow());
}
catch(Exception e2)
{
throw (new QValueException("Parameter value [" + value + "] could not be evaluated as an absolute or relative Instant"));
}
}
}
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private Optional<Instant> parseValueAsRelativeInstant(Serializable value)
{
String valueString = ValueUtils.getValueAsString(value);
if(valueString == null)
{
return (Optional.empty());
}
// todo - use parser!!
return Optional.of(Instant.now());
}
/*******************************************************************************
**
*******************************************************************************/