This is an automated email from the ASF dual-hosted git repository.

danwatford pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 68c1084edc Improved: MacroFormRenderer refactoring of datetime fields 
(OFBIZ-12126)
68c1084edc is described below

commit 68c1084edc56872fe7621976fd5d88bc1312b180
Author: Daniel Watford <dan...@watfordconsulting.com>
AuthorDate: Tue Nov 15 10:25:56 2022 +0000

    Improved: MacroFormRenderer refactoring of datetime fields (OFBIZ-12126)
    
    Part of the OFBIZ-11456 MacroFormRenderer refactoring effort.
    
    Rather than MacroFormRenderer producing and evaulating FTL strings, it now 
uses RenderableFtlElementsBuilder to create RenderableFtlMacroCall objects for 
datetime fields which are then passed to an FtlWriter for evaluation.
    
    Added tests to document how rendering parameters are generated for datetime 
fields.
    
    Simplified generation of selectable minute options for date-time fields 
used in time-dropdown mode.
    
    Moved typing of the date-time field's step, type, mask and clock attributes 
to DateTimeField model to ensure they are checked when the model is 
instantiated. The type and clock attributes are exposed by the model as boolean 
getters to check field types and whether a 12 or 24-hour clock should be used. 
These changes remove the need for client code to decode the various string 
attributes.
    
    Removed redundant code which enabled/disabled encoding of date-time field 
output
---
 .../apache/ofbiz/widget/model/ModelFormField.java  |  82 +++---
 .../ofbiz/widget/model/XmlWidgetFieldVisitor.java  |   6 +-
 .../widget/renderer/macro/MacroFormRenderer.java   | 284 +--------------------
 .../macro/RenderableFtlFormElementsBuilder.java    | 179 +++++++++++++
 .../renderer/macro/MacroCallParameterMatcher.java  |   8 +
 ...croCallParameterStringValueEndsWithMatcher.java |  45 ++++
 ...oCallParameterStringValueStartsWithMatcher.java |  45 ++++
 .../renderer/macro/MacroFormRendererTest.java      |  17 +-
 ...nderableFtlFormElementsBuilderDatetimeTest.java | 236 +++++++++++++++++
 .../template/macro/HtmlFormMacroLibrary.ftl        |  14 +-
 .../template/macro/XlsFormMacroLibrary.ftl         |   6 +-
 11 files changed, 595 insertions(+), 327 deletions(-)

diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java
index 7e4143582a..25c7901639 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java
@@ -88,7 +88,7 @@ import org.w3c.dom.Element;
 /**
  * Models the &lt;field&gt; element.
  *
- * @see <code>widget-form.xsd</code>
+ * @see <a 
href="https://ofbiz.apache.org/dtds/widget-form.xsd";>widget-form.xsd</a>
  */
 public final class ModelFormField {
 
@@ -1272,11 +1272,11 @@ public final class ModelFormField {
      * @see <code>widget-form.xsd</code>
      */
     public static class DateTimeField extends FieldInfo {
-        private final String clock;
+        private final boolean isTwelveHour;
         private final FlexibleStringExpander defaultValue;
         private final String inputMethod;
-        private final String mask;
-        private final String step;
+        private final boolean useMask;
+        private final int step;
         private final String type;
 
         protected DateTimeField(DateTimeField original, ModelFormField 
modelFormField) {
@@ -1284,8 +1284,8 @@ public final class ModelFormField {
             this.defaultValue = original.defaultValue;
             this.type = original.type;
             this.inputMethod = original.inputMethod;
-            this.clock = original.clock;
-            this.mask = original.mask;
+            this.isTwelveHour = original.isTwelveHour;
+            this.useMask = original.useMask;
             this.step = original.step;
         }
 
@@ -1294,13 +1294,22 @@ public final class ModelFormField {
             this.defaultValue = 
FlexibleStringExpander.getInstance(element.getAttribute("default-value"));
             this.type = element.getAttribute("type");
             this.inputMethod = element.getAttribute("input-method");
-            this.clock = element.getAttribute("clock");
-            this.mask = element.getAttribute("mask");
-            String step = element.getAttribute("step");
-            if (step.isEmpty()) {
-                step = "1";
+            this.isTwelveHour = "12".equals(element.getAttribute("clock"));
+            this.useMask = "Y".equals(element.getAttribute("mask"));
+
+            final String stepAttribute = element.getAttribute("step");
+            if (stepAttribute.isEmpty()) {
+                this.step = 1;
+            } else {
+                try {
+                    this.step = Integer.parseInt(stepAttribute);
+                } catch (IllegalArgumentException e) {
+                    final String msg = "Could not read the step value of the 
datetime element: [" + stepAttribute
+                            + "]. Value must be an integer.";
+                    Debug.logError(msg, MODULE);
+                    throw new RuntimeException(msg, e);
+                }
             }
-            this.step = step;
         }
 
         public DateTimeField(int fieldSource, ModelFormField modelFormField) {
@@ -1308,9 +1317,9 @@ public final class ModelFormField {
             this.defaultValue = FlexibleStringExpander.getInstance("");
             this.type = "";
             this.inputMethod = "";
-            this.clock = "";
-            this.mask = "";
-            this.step = "1";
+            this.isTwelveHour = false;
+            this.useMask = false;
+            this.step = 1;
         }
 
         public DateTimeField(int fieldSource, String type) {
@@ -1318,9 +1327,9 @@ public final class ModelFormField {
             this.defaultValue = FlexibleStringExpander.getInstance("");
             this.type = type;
             this.inputMethod = "";
-            this.clock = "";
-            this.mask = "";
-            this.step = "1";
+            this.isTwelveHour = false;
+            this.useMask = false;
+            this.step = 1;
         }
 
         public DateTimeField(ModelFormField modelFormField) {
@@ -1328,9 +1337,9 @@ public final class ModelFormField {
             this.defaultValue = FlexibleStringExpander.getInstance("");
             this.type = "";
             this.inputMethod = "";
-            this.clock = "";
-            this.mask = "";
-            this.step = "1";
+            this.isTwelveHour = false;
+            this.useMask = false;
+            this.step = 1;
         }
 
         @Override
@@ -1344,11 +1353,10 @@ public final class ModelFormField {
         }
 
         /**
-         * Gets clock.
-         * @return the clock
+         * @return True if this field uses a 12-hour clock. If false then a 
24-hour clock should be used.
          */
-        public String getClock() {
-            return this.clock;
+        public boolean isTwelveHour() {
+            return this.isTwelveHour;
         }
 
         /**
@@ -1400,26 +1408,32 @@ public final class ModelFormField {
 
         /**
          * Gets mask.
+         *
          * @return the mask
          */
-        public String getMask() {
-            return this.mask;
+        public boolean useMask() {
+            return this.useMask;
         }
 
         /**
          * Gets step.
+         *
          * @return the step
          */
-        public String getStep() {
+        public int getStep() {
             return this.step;
         }
 
-        /**
-         * Gets type.
-         * @return the type
-         */
-        public String getType() {
-            return type;
+        public final boolean isDateType() {
+            return "date".equals(type);
+        }
+
+        public final boolean isTimeType() {
+            return "time".equals(type);
+        }
+
+        public final boolean isTimestampType() {
+            return "timestamp".equals(type);
         }
 
         @Override
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java
index 4839acd588..402e0c877b 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java
@@ -361,10 +361,10 @@ public class XmlWidgetFieldVisitor extends 
XmlAbstractWidgetVisitor implements M
 
     private void visitDateTimeFieldAttrs(DateTimeField field) throws Exception 
{
         visitAttribute("default-value", field.getDefaultValue());
-        visitAttribute("type", field.getType());
+        visitAttribute("type", field.isDateType() ? "date" : 
field.isTimeType() ? "time" : "timestamp");
         visitAttribute("input-method", field.getInputMethod());
-        visitAttribute("clock", field.getClock());
-        visitAttribute("mask", field.getMask());
+        visitAttribute("isTwelveHour", field.isTwelveHour());
+        visitAttribute("mask", field.useMask() ? "Y" : "N");
         visitAttribute("step", field.getStep());
     }
 
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
index 89d917edd9..5ac065184d 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
@@ -23,7 +23,6 @@ import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URLEncoder;
-import java.sql.Timestamp;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -80,7 +79,6 @@ import 
org.apache.ofbiz.widget.model.ModelFormField.SubmitField;
 import org.apache.ofbiz.widget.model.ModelFormField.TextField;
 import org.apache.ofbiz.widget.model.ModelFormField.TextFindField;
 import org.apache.ofbiz.widget.model.ModelFormField.TextareaField;
-import org.apache.ofbiz.widget.model.ModelFormFieldBuilder;
 import org.apache.ofbiz.widget.model.ModelScreenWidget;
 import org.apache.ofbiz.widget.model.ModelSingleForm;
 import org.apache.ofbiz.widget.model.ModelTheme;
@@ -95,8 +93,6 @@ import 
org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
 import 
org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlMacroCall;
 import org.jsoup.nodes.Element;
 
-import com.ibm.icu.util.Calendar;
-
 /**
  * Widget Library - Form Renderer implementation based on Freemarker macros
  */
@@ -259,267 +255,10 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
     }
 
     @Override
-    public void renderDateTimeField(Appendable writer, Map<String, Object> 
context, DateTimeField dateTimeField) throws IOException {
-        ModelFormField modelFormField = dateTimeField.getModelFormField();
-        String paramName = modelFormField.getParameterName(context);
-        String defaultDateTimeString = 
dateTimeField.getDefaultDateTimeString(context);
-        boolean disabled = modelFormField.getDisabled(context);
-        String className = "";
-        String alert = "false";
-        String name = "";
-        String formattedMask = "";
-        String event = modelFormField.getEvent();
-        String action = modelFormField.getAction(context);
-        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
-            className = modelFormField.getWidgetStyle();
-            if (modelFormField.shouldBeRed(context)) {
-                alert = "true";
-            }
-        }
-        boolean useTimeDropDown = 
"time-dropdown".equals(dateTimeField.getInputMethod());
-        String stepString = dateTimeField.getStep();
-        int step = 1;
-        StringBuilder timeValues = new StringBuilder();
-        if (useTimeDropDown && UtilValidate.isNotEmpty(step)) {
-            try {
-                step = Integer.parseInt(stepString);
-            } catch (IllegalArgumentException e) {
-                Debug.logWarning("Invalid value for step property for field[" 
+ paramName + "] with input-method=\"time-dropdown\" "
-                        + " Found Value [" + stepString + "]  " + 
e.getMessage(), MODULE);
-            }
-            timeValues.append("[");
-            for (int i = 0; i <= 59;) {
-                if (i != 0) {
-                    timeValues.append(", ");
-                }
-                timeValues.append(i);
-                i += step;
-            }
-            timeValues.append("]");
-        }
-        Map<String, String> uiLabelMap = 
UtilGenerics.cast(context.get("uiLabelMap"));
-        if (uiLabelMap == null) {
-            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
-        }
-        String localizedInputTitle = "";
-        String localizedIconTitle = "";
-        // whether the date field is short form, yyyy-mm-dd
-        boolean shortDateInput = ("date".equals(dateTimeField.getType()) || 
useTimeDropDown ? true : false);
-        if (useTimeDropDown) {
-            name = UtilHttp.makeCompositeParam(paramName, "date");
-        } else {
-            name = paramName;
-        }
-        // the default values for a timestamp
-        int size = 25;
-        int maxlength = 30;
-        if (shortDateInput) {
-            maxlength = 10;
-            size = maxlength;
-            if (uiLabelMap != null) {
-                localizedInputTitle = uiLabelMap.get("CommonFormatDate");
-            }
-        } else if ("time".equals(dateTimeField.getType())) {
-            maxlength = 8;
-            size = maxlength;
-            if (uiLabelMap != null) {
-                localizedInputTitle = uiLabelMap.get("CommonFormatTime");
-            }
-        } else {
-            if (uiLabelMap != null) {
-                localizedInputTitle = uiLabelMap.get("CommonFormatDateTime");
-            }
-        }
-        /*
-         * FIXME: Using a builder here is a hack. Replace the builder with 
appropriate code.
-         */
-        ModelFormFieldBuilder builder = new 
ModelFormFieldBuilder(modelFormField);
-        boolean memEncodeOutput = modelFormField.getEncodeOutput();
-        if (useTimeDropDown) {
-            // If time-dropdown deactivate encodingOutput for found hour and 
minutes
-            // FIXME: Encoding should be controlled by the renderer, not by 
the model.
-            builder.setEncodeOutput(false);
-        }
-        // FIXME: modelFormField.getEntry ignores shortDateInput when 
converting Date objects to Strings.
-        if (useTimeDropDown) {
-            builder.setEncodeOutput(memEncodeOutput);
-        }
-        modelFormField = builder.build();
-        String contextValue = modelFormField.getEntry(context, 
dateTimeField.getDefaultValue(context));
-        String value = contextValue;
-        if (UtilValidate.isNotEmpty(value)) {
-            if (value.length() > maxlength) {
-                value = value.substring(0, maxlength);
-            }
-        }
-        String id = modelFormField.getCurrentContainerId(context);
-        ModelForm modelForm = modelFormField.getModelForm();
-        String formName = FormRenderer.getCurrentFormName(modelForm, context);
-        String timeDropdown = dateTimeField.getInputMethod();
-        String timeDropdownParamName = "";
-        String classString = "";
-        boolean isTwelveHour = false;
-        String timeHourName = "";
-        int hour2 = 0;
-        int hour1 = 0;
-        int minutes = 0;
-        String timeMinutesName = "";
-        String amSelected = "";
-        String pmSelected = "";
-        String ampmName = "";
-        String compositeType = "";
-        // search for a localized label for the icon
-        if (uiLabelMap != null) {
-            localizedIconTitle = uiLabelMap.get("CommonViewCalendar");
-        }
-        if (!"time".equals(dateTimeField.getType())) {
-            String tempParamName;
-            if (useTimeDropDown) {
-                tempParamName = UtilHttp.makeCompositeParam(paramName, "date");
-            } else {
-                tempParamName = paramName;
-            }
-            timeDropdownParamName = tempParamName;
-            defaultDateTimeString = 
UtilHttp.encodeBlanks(modelFormField.getEntry(context, defaultDateTimeString));
-        }
-        // if we have an input method of time-dropdown, then render two
-        // dropdowns
-        if (useTimeDropDown) {
-            className = modelFormField.getWidgetStyle();
-            classString = (className != null ? className : "");
-            isTwelveHour = "12".equals(dateTimeField.getClock());
-            // set the Calendar to the default time of the form or now()
-            Calendar cal = null;
-            try {
-                Timestamp defaultTimestamp = Timestamp.valueOf(contextValue);
-                cal = Calendar.getInstance();
-                cal.setTime(defaultTimestamp);
-            } catch (IllegalArgumentException e) {
-                Debug.logWarning("Form widget field [" + paramName
-                        + "] with input-method=\"time-dropdown\" was not able 
to understand the default time [" + defaultDateTimeString
-                        + "]. The parsing error was: " + e.getMessage(), 
MODULE);
-            }
-            timeHourName = UtilHttp.makeCompositeParam(paramName, "hour");
-            if (cal != null) {
-                int hour = cal.get(Calendar.HOUR_OF_DAY);
-                hour2 = hour;
-                if (hour == 0) {
-                    hour = 12;
-                }
-                if (hour > 12) {
-                    hour -= 12;
-                }
-                hour1 = hour;
-                minutes = cal.get(Calendar.MINUTE);
-            }
-            timeMinutesName = UtilHttp.makeCompositeParam(paramName, 
"minutes");
-            compositeType = UtilHttp.makeCompositeParam(paramName, 
"compositeType");
-            // if 12 hour clock, write the AM/PM selector
-            if (isTwelveHour) {
-                amSelected = ((cal != null && cal.get(Calendar.AM_PM) == 
Calendar.AM) ? "selected" : "");
-                pmSelected = ((cal != null && cal.get(Calendar.AM_PM) == 
Calendar.PM) ? "selected" : "");
-                ampmName = UtilHttp.makeCompositeParam(paramName, "ampm");
-            }
-        }
-        //check for required field style on single forms
-        if (shouldApplyRequiredField(modelFormField)) {
-            String requiredStyle = modelFormField.getRequiredFieldStyle();
-            if (UtilValidate.isEmpty(requiredStyle)) {
-                requiredStyle = "required";
-            }
-            if (UtilValidate.isEmpty(className)) {
-                className = requiredStyle;
-            } else {
-                className = requiredStyle + " " + className;
-            }
-        }
-        String mask = dateTimeField.getMask();
-        if ("Y".equals(mask)) {
-            if ("date".equals(dateTimeField.getType())) {
-                formattedMask = "9999-99-99";
-            } else if ("time".equals(dateTimeField.getType())) {
-                formattedMask = "99:99:99";
-            } else if ("timestamp".equals(dateTimeField.getType())) {
-                formattedMask = "9999-99-99 99:99:99";
-            }
-        }
-        String isXMLHttpRequest = "false";
-        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
-            isXMLHttpRequest = "true";
-        }
-        String tabindex = modelFormField.getTabindex();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderDateTimeField ");
-        sr.append("name=\"");
-        sr.append(name);
-        sr.append("\" className=\"");
-        sr.append(className);
-        sr.append("\" alert=\"");
-        sr.append(alert);
-        sr.append("\" value=\"");
-        sr.append(value);
-        sr.append("\" title=\"");
-        sr.append(localizedInputTitle);
-        sr.append("\" size=\"");
-        sr.append(Integer.toString(size));
-        sr.append("\" maxlength=\"");
-        sr.append(Integer.toString(maxlength));
-        sr.append("\" step=\"");
-        sr.append(Integer.toString(step));
-        sr.append("\" timeValues=\"");
-        sr.append(timeValues.toString());
-        sr.append("\" id=\"");
-        sr.append(id);
-        sr.append("\" event=\"");
-        sr.append(event);
-        sr.append("\" action=\"");
-        sr.append(action);
-        sr.append("\" dateType=\"");
-        sr.append(dateTimeField.getType());
-        sr.append("\" shortDateInput=");
-        sr.append(Boolean.toString(shortDateInput));
-        sr.append(" timeDropdownParamName=\"");
-        sr.append(timeDropdownParamName);
-        sr.append("\" defaultDateTimeString=\"");
-        sr.append(defaultDateTimeString);
-        sr.append("\" localizedIconTitle=\"");
-        sr.append(localizedIconTitle);
-        sr.append("\" timeDropdown=\"");
-        sr.append(timeDropdown);
-        sr.append("\" timeHourName=\"");
-        sr.append(timeHourName);
-        sr.append("\" classString=\"");
-        sr.append(classString);
-        sr.append("\" hour1=");
-        sr.append(Integer.toString(hour1));
-        sr.append(" hour2=");
-        sr.append(Integer.toString(hour2));
-        sr.append(" timeMinutesName=\"");
-        sr.append(timeMinutesName);
-        sr.append("\" minutes=");
-        sr.append(Integer.toString(minutes));
-        sr.append(" isTwelveHour=");
-        sr.append(Boolean.toString(isTwelveHour));
-        sr.append(" ampmName=\"");
-        sr.append(ampmName);
-        sr.append("\" amSelected=\"");
-        sr.append(amSelected);
-        sr.append("\" pmSelected=\"");
-        sr.append(pmSelected);
-        sr.append("\" compositeType=\"");
-        sr.append(compositeType);
-        sr.append("\" formName=\"");
-        sr.append(formName);
-        sr.append("\" mask=\"");
-        sr.append(formattedMask);
-        sr.append("\" tabindex=\"");
-        sr.append(tabindex);
-        sr.append("\" isXMLHttpRequest=\"");
-        sr.append(isXMLHttpRequest);
-        sr.append("\" disabled=");
-        sr.append(Boolean.toString(disabled));
-        sr.append(" />");
-        executeMacro(writer, (Locale) context.get("locale"), sr.toString());
+    public void renderDateTimeField(Appendable writer, Map<String, Object> 
context, DateTimeField dateTimeField) {
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.dateTime(context, dateTimeField));
+
+        final ModelFormField modelFormField = 
dateTimeField.getModelFormField();
         this.addAsterisks(writer, context, modelFormField);
         this.appendTooltip(writer, context, modelFormField);
     }
@@ -1870,14 +1609,13 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         // the default values for a timestamp
         int size = 25;
         int maxlength = 30;
-        String dateType = dateFindField.getType();
-        if ("date".equals(dateType)) {
+        if (dateFindField.isDateType()) {
             maxlength = 10;
             size = maxlength;
             if (uiLabelMap != null) {
                 localizedInputTitle = uiLabelMap.get("CommonFormatDate");
             }
-        } else if ("time".equals(dateFindField.getType())) {
+        } else if (dateFindField.isTimeType()) {
             maxlength = 8;
             size = maxlength;
             if (uiLabelMap != null) {
@@ -1900,7 +1638,7 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         String defaultDateTimeString = "";
         StringBuilder imgSrc = new StringBuilder();
         // add calendar pop-up button and seed data IF this is not a "time" 
type date-find
-        if (!"time".equals(dateFindField.getType())) {
+        if (!dateFindField.isTimeType()) {
             ModelForm modelForm = modelFormField.getModelForm();
             formName = FormRenderer.getCurrentFormName(modelForm, context);
             defaultDateTimeString = 
UtilHttp.encodeBlanks(modelFormField.getEntry(context, 
dateFindField.getDefaultDateTimeString(context)));
@@ -1949,9 +1687,11 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         sr.append(Integer.toString(size));
         sr.append("\" maxlength=\"");
         sr.append(Integer.toString(maxlength));
-        sr.append("\" dateType=\"");
-        sr.append(dateType);
-        sr.append("\" formName=\"");
+        sr.append("\" isDateType=");
+        sr.append(Boolean.toString(dateFindField.isDateType()));
+        sr.append("\" isTimeType=");
+        sr.append(Boolean.toString(dateFindField.isTimeType()));
+        sr.append(" formName=\"");
         sr.append(formName);
         sr.append("\" defaultDateTimeString=\"");
         sr.append(defaultDateTimeString);
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
index 8d256c5924..db0725f96e 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
@@ -20,6 +20,7 @@ package org.apache.ofbiz.widget.renderer.macro;
 
 import java.io.StringWriter;
 import java.net.URI;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -27,11 +28,14 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
+import com.ibm.icu.util.Calendar;
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilFormatOut;
 import org.apache.ofbiz.base.util.UtilGenerics;
@@ -47,6 +51,7 @@ import 
org.apache.ofbiz.widget.model.ModelFormField.ContainerField;
 import org.apache.ofbiz.widget.model.ModelFormField.DisplayField;
 import org.apache.ofbiz.widget.model.ModelScreenWidget.Label;
 import org.apache.ofbiz.widget.model.ModelTheme;
+import org.apache.ofbiz.widget.renderer.FormRenderer;
 import org.apache.ofbiz.widget.renderer.Paginator;
 import org.apache.ofbiz.widget.renderer.VisualTheme;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
@@ -364,6 +369,180 @@ public final class RenderableFtlFormElementsBuilder {
         return builder.build();
     }
 
+    public RenderableFtl dateTime(final Map<String, Object> context, final 
ModelFormField.DateTimeField dateTimeField) {
+
+        final ModelFormField modelFormField = 
dateTimeField.getModelFormField();
+        final ModelForm modelForm = modelFormField.getModelForm();
+
+        // Determine whether separate drop down select inputs be used for the 
hour/minute/am_pm components of the date-time.
+        boolean useTimeDropDown = 
"time-dropdown".equals(dateTimeField.getInputMethod());
+
+        final String paramName = modelFormField.getParameterName(context);
+
+        final RenderableFtlMacroCallBuilder macroCallBuilder = 
RenderableFtlMacroCall.builder()
+                .name("renderDateTimeField")
+                .booleanParameter("disabled", 
modelFormField.getDisabled(context))
+                .stringParameter("name", useTimeDropDown ? 
UtilHttp.makeCompositeParam(paramName, "date") : paramName)
+                .stringParameter("id", 
modelFormField.getCurrentContainerId(context))
+                .booleanParameter("isXMLHttpRequest", 
"XMLHttpRequest".equals(request.getHeader("X-Requested-With")))
+                .stringParameter("tabindex", modelFormField.getTabindex())
+                .stringParameter("event", modelFormField.getEvent())
+                .stringParameter("formName", 
FormRenderer.getCurrentFormName(modelForm, context))
+                .booleanParameter("alert", false)
+                .stringParameter("action", modelFormField.getAction(context));
+
+        // Set names for the various input components that might be rendered 
for this date-time field.
+        macroCallBuilder.stringParameter("timeHourName", 
UtilHttp.makeCompositeParam(paramName, "hour"))
+                .stringParameter("timeMinutesName", 
UtilHttp.makeCompositeParam(paramName, "minutes"))
+                .stringParameter("compositeType", 
UtilHttp.makeCompositeParam(paramName, "compositeType"))
+                .stringParameter("ampmName", 
UtilHttp.makeCompositeParam(paramName, "ampm"));
+
+        ArrayList<String> classNames = new ArrayList<>();
+        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
+            classNames.add(modelFormField.getWidgetStyle());
+
+            if (modelFormField.shouldBeRed(context)) {
+                macroCallBuilder.booleanParameter("alert", true);
+            }
+        }
+
+        if (shouldApplyRequiredField(modelFormField)) {
+            String requiredStyle = modelFormField.getRequiredFieldStyle();
+            if (UtilValidate.isEmpty(requiredStyle)) {
+                requiredStyle = "required";
+            }
+            classNames.add(requiredStyle);
+        }
+        macroCallBuilder.stringParameter("className", String.join(" ", 
classNames));
+
+        String defaultDateTimeString = 
dateTimeField.getDefaultDateTimeString(context);
+
+        if (useTimeDropDown) {
+            final int step = dateTimeField.getStep();
+            final String timeValues = IntStream.range(0, 60)
+                    .filter(i -> i % step == 0)
+                    .mapToObj(Integer::toString)
+                    .collect(Collectors.joining(", ", "[", "]"));
+            macroCallBuilder.stringParameter("timeValues", timeValues)
+                    .intParameter("step", step);
+        }
+
+        Map<String, String> uiLabelMap = 
UtilGenerics.cast(context.get("uiLabelMap"));
+        if (uiLabelMap == null) {
+            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
+        }
+
+        // whether the date field is short form, yyyy-mm-dd
+        boolean shortDateInput = dateTimeField.isDateType() || useTimeDropDown;
+        macroCallBuilder.booleanParameter("shortDateInput", shortDateInput);
+
+        // Set render properties based on the date-time field's type.
+        final int size;
+        final int maxlength;
+        final String formattedMask;
+        final String titleLabelMapKey;
+
+        if (shortDateInput) {
+            size = 10;
+            maxlength = 10;
+            formattedMask = "9999-99-99";
+            titleLabelMapKey = "CommonFormatDate";
+        } else if (dateTimeField.isTimeType()) {
+            size = 8;
+            maxlength = 8;
+            formattedMask = "99:99:99";
+            titleLabelMapKey = "CommonFormatTime";
+
+            macroCallBuilder.booleanParameter("isTimeType", true);
+        } else {
+            size = 25;
+            maxlength = 30;
+            formattedMask = "9999-99-99 99:99:99";
+            titleLabelMapKey = "CommonFormatDateTime";
+        }
+
+        macroCallBuilder.intParameter("size", size)
+                .intParameter("maxlength", maxlength);
+
+        if (dateTimeField.useMask()) {
+            macroCallBuilder.stringParameter("mask", formattedMask);
+        }
+
+        if (uiLabelMap != null) {
+            macroCallBuilder.stringParameter("title", 
uiLabelMap.get(titleLabelMapKey))
+                    .stringParameter("localizedIconTitle", 
uiLabelMap.get("CommonViewCalendar"));
+        }
+
+        final String contextValue = modelFormField.getEntry(context, 
dateTimeField.getDefaultValue(context));
+        final String value = UtilValidate.isNotEmpty(contextValue) && 
contextValue.length() > maxlength
+                ? contextValue.substring(0, maxlength)
+                : contextValue;
+
+        String timeDropdown = dateTimeField.getInputMethod();
+        String timeDropdownParamName = "";
+
+        if (!dateTimeField.isTimeType()) {
+            String tempParamName;
+            if (useTimeDropDown) {
+                tempParamName = UtilHttp.makeCompositeParam(paramName, "date");
+            } else {
+                tempParamName = paramName;
+            }
+            timeDropdownParamName = tempParamName;
+            defaultDateTimeString = 
UtilHttp.encodeBlanks(modelFormField.getEntry(context, defaultDateTimeString));
+        }
+
+        // If we have an input method of time-dropdown, then render two 
dropdowns
+        if (useTimeDropDown) {
+            // Set the class to apply to the time input components.
+            final String widgetStyle = modelFormField.getWidgetStyle();
+            macroCallBuilder.stringParameter("classString", widgetStyle != 
null ? widgetStyle : "");
+
+            // Set the Calendar to the field's context value, or the field's 
default if no context value exists.
+            final Calendar cal = Calendar.getInstance();
+            try {
+                if (contextValue != null) {
+                    Timestamp contextValueTimestamp = 
Timestamp.valueOf(contextValue);
+                    cal.setTime(contextValueTimestamp);
+                }
+            } catch (IllegalArgumentException e) {
+                Debug.logWarning("Form widget field [" + paramName
+                        + "] with input-method=\"time-dropdown\" was not able 
to understand the time [" + contextValue
+                        + "]. The parsing error was: " + e.getMessage(), 
MODULE);
+            }
+
+            if (cal != null) {
+                int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
+                int minutesOfHour = cal.get(Calendar.MINUTE);
+
+                // Set the hour value for when in 12-hour clock mode.
+                macroCallBuilder.intParameter("hour1", hourOfDay % 12);
+
+                // Set the hour value for when in 24-hour clock mode.
+                macroCallBuilder.intParameter("hour2", hourOfDay);
+
+                macroCallBuilder.intParameter("minutes", minutesOfHour);
+            }
+
+            boolean isTwelveHourClock = dateTimeField.isTwelveHour();
+            macroCallBuilder.booleanParameter("isTwelveHour", 
isTwelveHourClock);
+
+            // if using a 12-hour clock, write the AM/PM selector
+            if (isTwelveHourClock) {
+                macroCallBuilder.booleanParameter("amSelected", 
cal.get(Calendar.AM_PM) == Calendar.AM)
+                        .booleanParameter("pmSelected", 
cal.get(Calendar.AM_PM) == Calendar.PM);
+
+            }
+        }
+
+        macroCallBuilder.stringParameter("value", value)
+                .stringParameter("timeDropdownParamName", 
timeDropdownParamName)
+                .stringParameter("defaultDateTimeString", 
defaultDateTimeString)
+                .stringParameter("timeDropdown", timeDropdown);
+
+        return macroCallBuilder.build();
+    }
+
     public RenderableFtl makeHyperlinkString(final ModelFormField.SubHyperlink 
subHyperlink,
                                              final Map<String, Object> 
context) {
         if (subHyperlink == null || !subHyperlink.shouldUse(context)) {
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java
index 30c37bee3a..c142b0733a 100644
--- 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java
@@ -88,6 +88,14 @@ public final class MacroCallParameterMatcher extends 
TypeSafeMatcher<Map.Entry<S
         return new MacroCallParameterMatcher(name, new 
MacroCallParameterStringValueMatcher(value));
     }
 
+    public static MacroCallParameterMatcher 
hasNameAndStringValueStartsWith(final String name, final String startsWith) {
+        return new MacroCallParameterMatcher(name, new 
MacroCallParameterStringValueStartsWithMatcher(startsWith));
+    }
+
+    public static MacroCallParameterMatcher 
hasNameAndStringValueEndsWith(final String name, final String endsWith) {
+        return new MacroCallParameterMatcher(name, new 
MacroCallParameterStringValueEndsWithMatcher(endsWith));
+    }
+
     public static MacroCallParameterMatcher hasNameAndIntegerValue(final 
String name, final int value) {
         return new MacroCallParameterMatcher(name, new 
MacroCallParameterIntegerValueMatcher(value));
     }
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueEndsWithMatcher.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueEndsWithMatcher.java
new file mode 100644
index 0000000000..e53817ad9f
--- /dev/null
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueEndsWithMatcher.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
*******************************************************************************/
+package org.apache.ofbiz.widget.renderer.macro;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+public final class MacroCallParameterStringValueEndsWithMatcher extends 
TypeSafeMatcher<Object> {
+    private final String endsWith;
+
+    public MacroCallParameterStringValueEndsWithMatcher(final String endsWith) 
{
+        this.endsWith = endsWith;
+    }
+
+    @Override
+    protected boolean matchesSafely(final Object item) {
+        return item != null && item instanceof String && ((String) 
item).endsWith(endsWith);
+    }
+
+    @Override
+    public void describeTo(final Description description) {
+        description.appendText("ends with '" + endsWith + "'");
+    }
+
+    @Override
+    protected void describeMismatchSafely(final Object item, final Description 
mismatchDescription) {
+        mismatchDescription.appendText("with value '" + item + "'");
+    }
+}
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueStartsWithMatcher.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueStartsWithMatcher.java
new file mode 100644
index 0000000000..e544d6b864
--- /dev/null
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueStartsWithMatcher.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
*******************************************************************************/
+package org.apache.ofbiz.widget.renderer.macro;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+public final class MacroCallParameterStringValueStartsWithMatcher extends 
TypeSafeMatcher<Object> {
+    private final String startsWith;
+
+    public MacroCallParameterStringValueStartsWithMatcher(final String 
startsWith) {
+        this.startsWith = startsWith;
+    }
+
+    @Override
+    protected boolean matchesSafely(final Object item) {
+        return item != null && item instanceof String && ((String) 
item).startsWith(startsWith);
+    }
+
+    @Override
+    public void describeTo(final Description description) {
+        description.appendText("starts with '" + startsWith + "'");
+    }
+
+    @Override
+    protected void describeMismatchSafely(final Object item, final Description 
mismatchDescription) {
+        mismatchDescription.appendText("with value '" + item + "'");
+    }
+}
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
index eb5b3e244e..0c508c2cea 100644
--- 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
@@ -222,7 +222,7 @@ public class MacroFormRendererTest {
     }
 
     @Test
-    public void textAreaMacroRendered(@Mocked ModelFormField.TextareaField 
textareaField) throws IOException {
+    public void textAreaMacroRendered(@Mocked ModelFormField.TextareaField 
textareaField) {
         new Expectations() {
             {
                 renderableFtlFormElementsBuilder.textArea(withNotNull(), 
textareaField);
@@ -239,19 +239,20 @@ public class MacroFormRendererTest {
     }
 
     @Test
-    public void dateTimeMacroRendered(@Mocked ModelFormField.DateTimeField 
dateTimeField) throws IOException {
+    public void dateTimeMacroRendered(@Mocked ModelFormField.DateTimeField 
dateTimeField) {
         new Expectations() {
             {
-                modelFormField.getEntry(withNotNull(), anyString);
-                result = "2020-01-02";
-
-                dateTimeField.getInputMethod();
-                result = "date";
+                renderableFtlFormElementsBuilder.dateTime(withNotNull(), 
dateTimeField);
+                result = genericMacroCall;
             }
         };
 
+        genericTooltipRenderedExpectation(dateTimeField);
+
         macroFormRenderer.renderDateTimeField(appendable, ImmutableMap.of(), 
dateTimeField);
-        assertAndGetMacroString("renderDateTimeField", 
ImmutableMap.of("value", "2020-01-02"));
+
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderDatetimeTest.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderDatetimeTest.java
new file mode 100644
index 0000000000..df577a1e5f
--- /dev/null
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderDatetimeTest.java
@@ -0,0 +1,236 @@
+package org.apache.ofbiz.widget.renderer.macro;
+
+import mockit.Expectations;
+import mockit.Injectable;
+import mockit.Mocked;
+import mockit.Tested;
+import org.apache.ofbiz.webapp.control.RequestHandler;
+import org.apache.ofbiz.widget.model.ModelFormField;
+import org.apache.ofbiz.widget.model.ModelTheme;
+import org.apache.ofbiz.widget.renderer.VisualTheme;
+import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.util.HashMap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class RenderableFtlFormElementsBuilderDatetimeTest {
+
+    @Injectable
+    private VisualTheme visualTheme;
+
+    @Injectable
+    private RequestHandler requestHandler;
+
+    @Injectable
+    private HttpServletRequest request;
+
+    @Injectable
+    private HttpServletResponse response;
+
+    @Mocked
+    private HttpSession httpSession;
+
+    @Mocked
+    private ModelTheme modelTheme;
+
+    @Mocked
+    private ModelFormField.ContainerField containerField;
+
+    @Mocked
+    private ModelFormField modelFormField;
+
+    @Tested
+    private RenderableFtlFormElementsBuilder renderableFtlFormElementsBuilder;
+
+    @Test
+    public void datetimeFieldSetsIdAndValue(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        final int maxLength = 22;
+        new Expectations() {
+            {
+                modelFormField.getCurrentContainerId(withNotNull());
+                result = "CurrentDatetimeId";
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("id", 
"CurrentDatetimeId"),
+                MacroCallParameterMatcher.hasNameAndStringValue("value", 
"DATETIMEVALUE")));
+    }
+
+    @Test
+    public void datetimeFieldSetsDisabledParameters(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                modelFormField.getDisabled(withNotNull());
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndBooleanValue("disabled", 
true)));
+    }
+
+    @Test
+    public void datetimeFieldSetsLengthAndMaskForDateType(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.isDateType();
+                result = true;
+
+                datetimeField.useMask();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("mask", 
"9999-99-99"),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("size", 10),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("maxlength", 
10)));
+    }
+
+    @Test
+    public void datetimeFieldSetsLengthForTimeType(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.isTimeType();
+                result = true;
+
+                datetimeField.useMask();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("mask", 
"99:99:99"),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("size", 8),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("maxlength", 
8)));
+    }
+
+    @Test
+    public void datetimeFieldSetsLengthForTimestampType(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.isTimestampType();
+                result = true;
+                minTimes = 0;
+
+                datetimeField.useMask();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("mask", 
"9999-99-99 99:99:99"),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("size", 25),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("maxlength", 
30)));
+    }
+
+    @Test
+    public void datetimeFieldSetsTimeValuesForStepSize1(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.getStep();
+                result = 1;
+
+                datetimeField.getInputMethod();
+                result = "time-dropdown";
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                
MacroCallParameterMatcher.hasNameAndStringValueStartsWith("timeValues", "[0, 1, 
2, 3,"),
+                
MacroCallParameterMatcher.hasNameAndStringValueEndsWith("timeValues", "56, 57, 
58, 59]")));
+    }
+
+    @Test
+    public void datetimeFieldSetsTimeValuesForStepSize3(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.getStep();
+                result = 3;
+
+                datetimeField.getInputMethod();
+                result = "time-dropdown";
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                
MacroCallParameterMatcher.hasNameAndStringValueStartsWith("timeValues", "[0, 3, 
6, 9,"),
+                
MacroCallParameterMatcher.hasNameAndStringValueEndsWith("timeValues", "48, 51, 
54, 57]")));
+    }
+
+    @Test
+    public void datetimeFieldSetsValuesFor12HourClock(@Mocked final 
ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.getStep();
+                result = 1;
+
+                datetimeField.getInputMethod();
+                result = "time-dropdown";
+
+                datetimeField.isTwelveHour();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "2022-05-18 16:44:57";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndIntegerValue("hour1", 4),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("hour2", 16),
+                
MacroCallParameterMatcher.hasNameAndBooleanValue("isTwelveHour", true),
+                MacroCallParameterMatcher.hasNameAndBooleanValue("pmSelected", 
true)));
+    }
+}
diff --git a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl 
b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
index 94a9ba7b57..d1d1f6be1e 100644
--- a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
+++ b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
@@ -88,13 +88,13 @@ under the License.
   </textarea><#lt/>
 </#macro>
 
-<#macro renderDateTimeField name className alert dateType 
timeDropdownParamName defaultDateTimeString localizedIconTitle timeHourName 
timeMinutesName minutes isTwelveHour ampmName amSelected pmSelected 
compositeType timeDropdown="" classString="" hour1="" hour2="" 
shortDateInput="" title="" value="" size="" maxlength="" id="" formName="" 
mask="" event="" action="" step="" timeValues="" tabindex="" disabled=false 
isXMLHttpRequest="">
+<#macro renderDateTimeField name className alert timeDropdownParamName 
defaultDateTimeString localizedIconTitle timeHourName timeMinutesName minutes 
isTwelveHour ampmName compositeType isTimeType=false isDateType=false 
amSelected=false pmSelected=false timeDropdown="" classString="" hour1="" 
hour2="" shortDateInput="" title="" value="" size="" maxlength="" id="" 
formName="" mask="" event="" action="" step="" timeValues="" tabindex="" 
disabled=false isXMLHttpRequest="">
   <span class="view-calendar">
     <#local cultureInfo = 
Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("datejs", 
.locale)/>
     <#local datePickerLang = 
Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("jquery", 
.locale)/>
     <#local timePicker = 
"/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js,/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css"/>
     <#local timePickerLang = 
Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("dateTime",
 .locale)/>
-    <#if dateType!="time" >
+    <#if !isTimeType >
       <input type="text" name="${name}_i18n" <@renderClass className alert /> 
<@renderDisabled disabled />
         <#if tabindex?has_content> tabindex="${tabindex}"</#if>
         <#if title?has_content> title="${title}"</#if>
@@ -140,8 +140,8 @@ under the License.
         <#rt/>
         <#if isTwelveHour>
           <select name="${ampmName}" <@renderDisabled disabled /> <#if 
classString?has_content>class="${classString}"</#if>><#rt/>
-            <option value="AM" <#if "selected" == 
amSelected>selected="selected"</#if> >AM</option><#rt/>
-            <option value="PM" <#if "selected" == 
pmSelected>selected="selected"</#if>>PM</option><#rt/>
+            <option value="AM" <#if 
amSelected>selected="selected"</#if>>AM</option><#rt/>
+            <option value="PM" <#if 
pmSelected>selected="selected"</#if>>PM</option><#rt/>
           </select>
         <#rt/>
       </#if>
@@ -442,14 +442,14 @@ under the License.
   </#if>
 </#macro>
 
-<#macro renderDateFindField className alert id name dateType formName value 
defaultDateTimeString imgSrc localizedIconTitle defaultOptionFrom 
defaultOptionThru opEquals opSameDay opGreaterThanFromDayStart opGreaterThan 
opGreaterThan opLessThan opUpToDay opUpThruDay opIsEmpty conditionGroup="" 
localizedInputTitle="" value2="" size="" maxlength="" titleStyle="" tabindex="" 
disabled=false>
+<#macro renderDateFindField className alert id name formName value 
defaultDateTimeString imgSrc localizedIconTitle defaultOptionFrom 
defaultOptionThru opEquals opSameDay opGreaterThanFromDayStart opGreaterThan 
opGreaterThan opLessThan opUpToDay opUpThruDay opIsEmpty isTimeType=false 
isDateType=false conditionGroup="" localizedInputTitle="" value2="" size="" 
maxlength="" titleStyle="" tabindex="" disabled=false>
   <#if conditionGroup?has_content>
     <input type="hidden" name="${name}_grp" value="${conditionGroup}" 
<@renderDisabled disabled />/>
   </#if>
-  <#if dateType != "time">
+  <#if !isTimeType>
     <#local className = className + " date-time-picker"/>
   </#if>
-  <#local shortDateInput = "date" == dateType/>
+  <#local shortDateInput = isDateType/>
   <#local cultureInfo = 
Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("datejs", 
.locale)/>
   <#local datePickerLang = 
Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("jquery", 
.locale)/>
   <#local timePicker = 
"/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js,/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css"/>
diff --git a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl 
b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl
index 3ef8b5545e..d4c564854c 100644
--- a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl
+++ b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl
@@ -34,9 +34,9 @@ under the License.
 
 <#macro renderTextareaField name className alert cols rows maxlength id 
readonly value visualEditorEnable buttons tabindex language="" 
disabled=""></#macro>
 
-<#macro renderDateTimeField name className alert title value size maxlength id 
dateType shortDateInput timeDropdownParamName defaultDateTimeString 
localizedIconTitle timeDropdown timeHourName classString hour1 hour2 
timeMinutesName minutes isTwelveHour ampmName amSelected pmSelected 
compositeType formName mask="" event="" action="" step="" timeValues="" 
tabindex="" disabled="" isXMLHttpRequest=""> 
-<#if dateType=="time" ><@renderItemField value "tf" className/>
-<#elseif dateType=="date"><@renderItemField value "dt" className/>
+<#macro renderDateTimeField name className alert title value size maxlength id 
isTimeType isDateType shortDateInput timeDropdownParamName 
defaultDateTimeString localizedIconTitle timeDropdown timeHourName classString 
hour1 hour2 timeMinutesName minutes isTwelveHour ampmName amSelected pmSelected 
compositeType formName mask="" event="" action="" step="" timeValues="" 
tabindex="" disabled="" isXMLHttpRequest="">
+<#if isTimeType ><@renderItemField value "tf" className/>
+<#elseif isDateType><@renderItemField value "dt" className/>
 <#else><@renderItemField value "dtf" className/></#if>
 </#macro>
 

Reply via email to