diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java index 35786724..0e67461e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java @@ -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 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 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 defaultValues, Set 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 defaultValues, Set disabledFields) throws QException { - String tablePath = input.getInstance().getTablePath(childTableName); + String tablePath = QContext.getQInstance().getTablePath(childTableName); if(tablePath == null) { return (null); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRenderer.java new file mode 100644 index 00000000..c6b45597 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRenderer.java @@ -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 . + */ + +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 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 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); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtils.java new file mode 100644 index 00000000..db29e00b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetVelocityUtils.java @@ -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 . + */ + +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 context; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public NoCodeWidgetVelocityUtils(Map context) + { + this.context = context; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public final String errorIcon() + { + return (""" + + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public final String checkIcon() + { + return (""" + + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + 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 (""); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java index c6b9a3e3..3f79d968 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java @@ -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 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); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java index 5865fd17..7e394a0f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java @@ -44,7 +44,7 @@ public class QFilterCriteria implements Serializable, Cloneable private QCriteriaOperator operator; private List 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); } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 10ae4c5e..b35b7940 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -90,6 +90,7 @@ public class QInstance private Map queues = new LinkedHashMap<>(); private Map 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); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetOutput.java new file mode 100644 index 00000000..f75ee128 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetOutput.java @@ -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 . + */ + +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 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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java new file mode 100644 index 00000000..570b3890 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/AbstractWidgetValueSource.java @@ -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 . + */ + +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 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 context) + { + //////////////////////// + // noop in base class // + //////////////////////// + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/HtmlWrapper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/HtmlWrapper.java new file mode 100644 index 00000000..d1eaa507 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/HtmlWrapper.java @@ -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 . + */ + +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("

", "

"); + public static final HtmlWrapper INDENT_1 = new HtmlWrapper("
", "
"); + public static final HtmlWrapper INDENT_2 = new HtmlWrapper("
", "
"); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public HtmlWrapper(String prefix, String suffix) + { + this.prefix = prefix; + this.suffix = suffix; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static HtmlWrapper paddingTop(String amount) + { + return (new HtmlWrapper("
", "
")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String wrap(String content) + { + return (Objects.requireNonNullElse(prefix, "") + + "\n" + Objects.requireNonNullElse(content, "") + "\n" + + Objects.requireNonNullElse(suffix, "") + "\n"); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/QNoCodeWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/QNoCodeWidgetMetaData.java new file mode 100644 index 00000000..6397a333 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/QNoCodeWidgetMetaData.java @@ -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 . + */ + +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 values = new ArrayList<>(); + private List outputs = new ArrayList<>(); + + + + /******************************************************************************* + ** Getter for values + *******************************************************************************/ + public List getValues() + { + return (this.values); + } + + + + /******************************************************************************* + ** Setter for values + *******************************************************************************/ + public void setValues(List 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 values) + { + this.values = values; + return (this); + } + + + + /******************************************************************************* + ** Getter for outputs + *******************************************************************************/ + public List getOutputs() + { + return (this.outputs); + } + + + + /******************************************************************************* + ** Setter for outputs + *******************************************************************************/ + public void setOutputs(List 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 outputs) + { + this.outputs = outputs; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java new file mode 100644 index 00000000..89e943df --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCalculation.java @@ -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 . + */ + +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 values; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public enum Operator + { + SUM_INTEGERS((List valueNames, Map context) -> + { + Integer sum = 0; + for(String valueName : valueNames) + { + Integer addend = ValueUtils.getValueAsInteger(context.get(valueName)); + sum += addend; + } + return (sum); + }), + + AGE_MINUTES((List valueNames, Map context) -> + { + Instant now = Instant.now(); + Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); + return (then.until(now, ChronoUnit.MINUTES)); + }); + + + private final BiFunction, Map, Object> function; + + + + /******************************************************************************* + ** + *******************************************************************************/ + Operator(BiFunction, Map, Object> function) + { + this.function = function; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Object execute(List values, Map context) + { + return (function.apply(values, context)); + } + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetCalculation() + { + setType(getClass().getSimpleName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object evaluate(Map 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 getValues() + { + return (this.values); + } + + + + /******************************************************************************* + ** Setter for values + *******************************************************************************/ + public void setValues(List values) + { + this.values = values; + } + + + + /******************************************************************************* + ** Fluent setter for values + *******************************************************************************/ + public WidgetCalculation withValues(List values) + { + this.values = values; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCount.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCount.java new file mode 100644 index 00000000..3d672e0d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetCount.java @@ -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 . + */ + +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 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 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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetHtmlLine.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetHtmlLine.java new file mode 100644 index 00000000..8da31154 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetHtmlLine.java @@ -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 . + */ + +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 wrappers; + private String velocityTemplate; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public WidgetHtmlLine() + { + setType(getClass().getSimpleName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String render(Map 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 getWrappers() + { + return (this.wrappers); + } + + + + /******************************************************************************* + ** Setter for wrappers + *******************************************************************************/ + public void setWrappers(List wrappers) + { + this.wrappers = wrappers; + } + + + + /******************************************************************************* + ** Fluent setter for wrappers + *******************************************************************************/ + public WidgetHtmlLine withWrappers(List 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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java new file mode 100644 index 00000000..c1da7795 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java @@ -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 . + */ + +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 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 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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java index a6fd9422..87cab255 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java @@ -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 diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index ef670d59..a870540b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -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"; - /******************************************************************************* diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRendererTest.java new file mode 100644 index 00000000..1a4960e4 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/NoCodeWidgetRendererTest.java @@ -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 . + */ + +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).*

.*Header.*

.*")); + 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.*")); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 05f31d8a..476b29cc 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -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 evaluateDateTimeParamValues(List values) + { + if(CollectionUtils.nullSafeIsEmpty(values)) + { + return (values); + } + + List 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 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 parseValueAsRelativeInstant(Serializable value) + { + String valueString = ValueUtils.getValueAsString(value); + if(valueString == null) + { + return (Optional.empty()); + } + + // todo - use parser!! + return Optional.of(Instant.now()); + } + + + /******************************************************************************* ** *******************************************************************************/