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 java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; 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.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.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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 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"); return (tablePath + "/" + tableName + ".bulkInsert");
} }
@ -131,7 +132,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/ *******************************************************************************/
public static String linkTableBulkLoadChildren(RenderWidgetInput input, String tableName) throws QException 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) if(tablePath == null)
{ {
return (null); return (null);
@ -147,7 +148,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/ *******************************************************************************/
public static String linkTableCreate(RenderWidgetInput input, String tableName) throws QException 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"); 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 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())); 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 public static String getCountLink(RenderWidgetInput input, String tableName, QQueryFilter filter, int count) throws QException
{ {
String totalString = QValueFormatter.formatValue(DisplayFormat.COMMAS, count); String totalString = QValueFormatter.formatValue(DisplayFormat.COMMAS, count);
String tablePath = input.getInstance().getTablePath(tableName); String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null || filter == null) if(tablePath == null || filter == null)
{ {
return (totalString); 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 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) if(tablePath == null)
{ {
return; return;
@ -201,7 +202,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/ *******************************************************************************/
public static String linkTableFilterUnencoded(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException 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) if(tablePath == null)
{ {
return (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 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) if(tablePath == null)
{ {
return (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 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 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) if(tablePath == null)
{ {
return (displayText); 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 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) if(tablePath == null)
{ {
return (linkText); return (linkText);
@ -267,7 +268,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/ *******************************************************************************/
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException 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"); 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 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) if(tablePath == null)
{ {
return (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 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 tableName = process.getTableName();
String tablePath = input.getInstance().getTablePath(tableName); String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/" + processName); 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 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) if(tablePath == null)
{ {
return (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 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) if(tablePath == null)
{ {
return (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.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; 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.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; 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 private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{ {
warnAboutPermissionSubTypeForTables(permissionSubType); 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) 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()); String permissionBaseName = getEffectivePermissionBaseName(rules, metaDataWithPermissionRules.getName());
switch(rules.getLevel()) switch(rules.getLevel())
@ -167,8 +168,8 @@ public class PermissionsHelper
*******************************************************************************/ *******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
{ {
QProcessMetaData process = actionInput.getInstance().getProcess(processName); QProcessMetaData process = QContext.getQInstance().getProcess(processName);
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, actionInput.getInstance()); QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, QContext.getQInstance());
if(effectivePermissionRules.getCustomPermissionChecker() != null) if(effectivePermissionRules.getCustomPermissionChecker() != null)
{ {
@ -208,8 +209,8 @@ public class PermissionsHelper
*******************************************************************************/ *******************************************************************************/
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
{ {
QAppMetaData app = actionInput.getInstance().getApp(appName); QAppMetaData app = QContext.getQInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput); 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 public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
{ {
QReportMetaData report = actionInput.getInstance().getReport(reportName); QReportMetaData report = QContext.getQInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput); 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 public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
{ {
QWidgetMetaDataInterface widget = actionInput.getInstance().getWidget(widgetName); QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput); 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 QCriteriaOperator operator;
private List<Serializable> values; private List<Serializable> values;
private String otherFieldName; private String otherFieldName;
private AbstractFilterExpression<?> expression; 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()); return (rs.toString());
} }
/******************************************************************************* /*******************************************************************************
** Getter for expression ** Getter for expression
*******************************************************************************/ *******************************************************************************/
@ -334,5 +353,4 @@ public class QFilterCriteria implements Serializable, Cloneable
return (this); return (this);
} }
} }

View File

@ -90,6 +90,7 @@ public class QInstance
private Map<String, QQueueMetaData> queues = new LinkedHashMap<>(); private Map<String, QQueueMetaData> queues = new LinkedHashMap<>();
private Map<String, String> environmentValues = new LinkedHashMap<>(); private Map<String, String> environmentValues = new LinkedHashMap<>();
private String defaultTimeZoneId = "UTC";
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance(); 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. ** Test if record matches filter.
*******************************************************************************/ *******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public static boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord) public static boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord)
{ {
if(filter == null || !filter.hasAnyCriteria()) if(filter == null || !filter.hasAnyCriteria())
@ -72,40 +71,7 @@ public class BackendQueryFilterUtils
String fieldName = criterion.getFieldName(); String fieldName = criterion.getFieldName();
Serializable value = qRecord.getValue(fieldName); Serializable value = qRecord.getValue(fieldName);
boolean criterionMatches = switch(criterion.getOperator()) boolean criterionMatches = doesCriteriaMatch(criterion, fieldName, value);
{
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));
}
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // // 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 ** 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 ** 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_MdyyyyWithSlashes = DateTimeFormatter.ofPattern("M/d/yyyy");
private static final DateTimeFormatter dateTimeFormatter_yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd"); 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.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.interfaces.QActionInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.QActionInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
@ -715,6 +718,11 @@ public abstract class AbstractRDBMSAction implements QActionInterface
clauses.add("(" + clause + ")"); clauses.add("(" + clause + ")");
if(field.getType().equals(QFieldType.DATE_TIME))
{
values = evaluateDateTimeParamValues(values);
}
params.addAll(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());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/