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

nmalin 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 24fc14a6ea Implemented: Add group option on drow-down in xml screen 
form (OFBIZ-13157) (#842)
24fc14a6ea is described below

commit 24fc14a6eaadd9f97d4bd1263426ca6ef042d68b
Author: Nicolas Malin <nicolas.ma...@nereide.fr>
AuthorDate: Wed Oct 23 20:11:07 2024 +0200

    Implemented: Add group option on drow-down in xml screen form (OFBIZ-13157) 
(#842)
    
    When you define a drop-down field in xml form, add new optional level with 
group-options to organize options.
    
            <field name="group">
                <drop-down>
                    <group-options description="A">
                        <option key="A1" description="Ad1"/>
                        <option key="A2" description="Ad2"/>
                    </group-options>
                    <group-options description="B">
                        <option key="B1" description="Bd1"/>
                        <option key="B2" description="Bd2"/>
                    </group-options>
                </drop-down>
            </field>
    
    Group-options can receive all options type :
    
            <field name="roleTypeId">
                <drop-down>
                    <group-options description="Customer roles">
                        <entity-options entity-name="RoleType">
                            <entity-constraint name="parentType" 
vale="CUSTOMER"/>
                        </entity-options>
                    </group-options>
                    <group-options description="Supplier roles">
                        <entity-options entity-name="RoleType">
                            <entity-constraint name="parentType" 
vale="SUPPLIER"/>
                        </entity-options>
                    </group-options>
                    <group-options description="Other roles">
                        <list-options list-name="otherRoles"/>
                    </group-options>
                </drop-down>
            </field>
    
    At this time only the drop-down is supported, group-option can works for 
radio and check but not implement on ftl macro library.
---
 framework/widget/dtd/widget-form.xsd               |  23 +++
 .../apache/ofbiz/widget/model/ModelFormField.java  | 174 +++++++++++++++++++++
 .../macro/RenderableFtlFormElementsBuilder.java    |  53 +++++--
 .../widget/renderer/macro/model/GroupOption.java   |  30 ++++
 .../template/macro/HtmlFormMacroLibrary.ftl        |  49 +++---
 5 files changed, 301 insertions(+), 28 deletions(-)

diff --git a/framework/widget/dtd/widget-form.xsd 
b/framework/widget/dtd/widget-form.xsd
index 09b382faa4..76e9cda4f0 100644
--- a/framework/widget/dtd/widget-form.xsd
+++ b/framework/widget/dtd/widget-form.xsd
@@ -873,6 +873,7 @@ under the License.
     <xs:element name="check" substitutionGroup="AllFields">
         <xs:complexType>
             <xs:choice minOccurs="0" maxOccurs="unbounded">
+                <xs:element ref="group-options" />
                 <xs:element ref="entity-options" />
                 <xs:element ref="list-options" />
                 <xs:element ref="option" />
@@ -1100,6 +1101,7 @@ under the License.
             <xs:sequence>
                 <xs:element ref="auto-complete" minOccurs="0" maxOccurs="1" />
                 <xs:choice minOccurs="0" maxOccurs="unbounded">
+                    <xs:element ref="group-options" />
                     <xs:element ref="entity-options" />
                     <xs:element ref="list-options" />
                     <xs:element ref="option" />
@@ -1383,6 +1385,7 @@ under the License.
     <xs:element name="radio" substitutionGroup="AllFields">
         <xs:complexType>
             <xs:choice minOccurs="0" maxOccurs="unbounded">
+                <xs:element ref="group-options" />
                 <xs:element ref="entity-options" />
                 <xs:element ref="list-options" />
                 <xs:element ref="option" />
@@ -1671,6 +1674,26 @@ under the License.
             <xs:attribute type="xs:string" name="field-name" use="required" />
         </xs:complexType>
     </xs:element>
+    <xs:element name="group-options">
+        <xs:annotation>
+            <xs:documentation>group the options </xs:documentation>
+        </xs:annotation>
+        <xs:complexType>
+            <xs:choice minOccurs="0" maxOccurs="unbounded">
+                <xs:element ref="group-options" />
+                <xs:element ref="entity-options" />
+                <xs:element ref="list-options" />
+                <xs:element ref="option" />
+            </xs:choice>
+            <xs:attribute type="xs:string" name="id" />
+            <xs:attribute type="xs:string" name="widget-style" />
+            <xs:attribute type="xs:string" name="description" 
default="${description}">
+                <xs:annotation>
+                    <xs:documentation>Will be presented to the user with field 
values substituted using the ${} syntax.</xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+        </xs:complexType>
+    </xs:element>
     <xs:element name="in-place-editor">
         <xs:annotation>
             <xs:documentation>Enables in place editon for the display 
field.</xs:documentation>
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 847de50a04..fdb653f9f8 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
@@ -36,6 +36,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.TimeZone;
+import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -2183,6 +2184,160 @@ public final class ModelFormField {
         }
     }
 
+    /**
+     * Models the &lt;group-options&gt; element.
+     * @see <code>widget-form.xsd</code>
+     */
+    public static class GroupOptions {
+        private final FlexibleStringExpander description;
+        private final FlexibleStringExpander id;
+        private final FlexibleStringExpander widgetStyle;
+
+        private final List<OptionSource> optionSources;
+        private final List<GroupOptions> groupOptions;
+
+        /**
+         * Create a new groupOptions instance from xml element
+         * @param groupOptionsElement
+         * @param modelFormField
+         */
+        public GroupOptions(Element groupOptionsElement, ModelFormField 
modelFormField) {
+            super();
+            this.description = 
FlexibleStringExpander.getInstance(groupOptionsElement.getAttribute("description"));
+            this.id = 
FlexibleStringExpander.getInstance(groupOptionsElement.getAttribute("id"));
+            this.widgetStyle = 
FlexibleStringExpander.getInstance(groupOptionsElement.getAttribute("widgetStyle"));
+
+            List<? extends Element> childElements = 
UtilXml.childElementList(groupOptionsElement);
+            List<OptionSource> optionSources = new ArrayList<>();
+            List<GroupOptions> groupOptions = new ArrayList<>();
+            if (!childElements.isEmpty()) {
+                for (Element childElement : childElements) {
+                    switch (childElement.getLocalName()) {
+                    case "option":
+                        optionSources.add(new SingleOption(childElement, 
modelFormField));
+                        break;
+                    case "list-options":
+                        optionSources.add(new ListOptions(childElement, 
modelFormField));
+                        break;
+                    case "entity-options":
+                        optionSources.add(new EntityOptions(childElement, 
modelFormField));
+                        break;
+                    case "group-options":
+                        groupOptions.add(new GroupOptions(childElement, 
modelFormField));
+                        break;
+                    }
+                }
+            }
+            this.optionSources = Collections.unmodifiableList(optionSources);
+            this.groupOptions = Collections.unmodifiableList(groupOptions);
+        }
+
+        /**
+         * Copy an existing groupOptions to a new one
+         * @param original
+         * @param modelFormField
+         */
+        private GroupOptions(GroupOptions original, ModelFormField 
modelFormField) {
+            super();
+            this.description = original.description;
+            this.id = original.id;
+            this.widgetStyle = original.widgetStyle;
+            List<OptionSource> optionSources = new 
ArrayList<>(original.optionSources.size());
+            for (OptionSource source : original.optionSources) {
+                optionSources.add(source.copy(modelFormField));
+            }
+            this.optionSources = Collections.unmodifiableList(optionSources);
+            List<GroupOptions> groupOptions = new 
ArrayList<>(original.groupOptions.size());
+            for (GroupOptions group : original.groupOptions) {
+                groupOptions.add(group.copy(modelFormField));
+            }
+            this.groupOptions = Collections.unmodifiableList(groupOptions);
+        }
+
+        /**
+         * create a groupOptions from a modelFormField
+         * @param modelFormField
+         */
+        public GroupOptions(ModelFormField modelFormField) {
+            super();
+            this.description = FlexibleStringExpander.getInstance("");
+            this.id = FlexibleStringExpander.getInstance("");
+            this.widgetStyle = FlexibleStringExpander.getInstance("");
+            this.optionSources = Collections.emptyList();
+            this.groupOptions = Collections.emptyList();
+        }
+
+        /**
+         * @return description present for a groupOptions instance
+         */
+        public FlexibleStringExpander getDescription() {
+            return description;
+        }
+
+        /**
+         * @return parsed description with context for a groupOptions instance
+         */
+        public String getDescription(Map<String, Object> context) {
+            return this.description.expandString(context);
+        }
+
+        /**
+         * @return unique reference for a groupOptions instance
+         */
+        public FlexibleStringExpander getId() {
+            return id;
+        }
+
+        /**
+         * @return parsed unique reference with context for a groupOptions 
instance
+         */
+        public String getId(Map<String, Object> context) {
+            String id = this.id.expandString(context);
+            return UtilValidate.isNotEmpty(id) ? id
+                    : UUID.randomUUID().toString().replace("-", "");
+        }
+
+        /**
+         * @return widgetStyle present for a groupOptions instance
+         */
+        public FlexibleStringExpander getWidgetStyle() {
+            return widgetStyle;
+        }
+
+        /**
+         * @return parsed widgetStyle with context for a groupOptions instance
+         */
+        public String getWidgetStyle(Map<String, Object> context) {
+            return this.widgetStyle.expandString(context);
+        }
+
+        /**
+         * Compute all options define for groupOptions instance
+         * @return options list present on this groupOptions
+         */
+        public List<OptionValue> getAllOptionValues(Map<String, Object> 
context, Delegator delegator) {
+            List<OptionValue> optionValues = new LinkedList<>();
+            for (OptionSource optionSource : this.optionSources) {
+                optionSource.addOptionValues(optionValues, context, delegator);
+            }
+            return optionValues;
+        }
+        /**
+         * @return groupOptions sub list
+         */
+        public List<GroupOptions> getGroupOptions() {
+            return groupOptions;
+        }
+
+        /**
+         * Duplicate the groupOptions
+         * @return new groupOptions instance
+         */
+        public GroupOptions copy(ModelFormField modelFormField) {
+            return new GroupOptions(this, modelFormField);
+        }
+    }
+
     /**
      * Models the &lt;entity-options&gt; element.
      * @see <code>widget-form.xsd</code>
@@ -2409,12 +2564,14 @@ public final class ModelFormField {
 
         private final FlexibleStringExpander noCurrentSelectedKey;
         private final List<OptionSource> optionSources;
+        private final List<GroupOptions> groupOptions;
 
         public FieldInfoWithOptions(Element element, ModelFormField 
modelFormField) {
             super(element, modelFormField);
             this.noCurrentSelectedKey = 
FlexibleStringExpander.getInstance(element.getAttribute("no-current-selected-key"));
             // read all option and entity-options sub-elements, maintaining 
order
             ArrayList<OptionSource> optionSources = new ArrayList<>();
+            ArrayList<GroupOptions> groupSources = new ArrayList<>();
             List<? extends Element> childElements = 
UtilXml.childElementList(element);
             if (!childElements.isEmpty()) {
                 for (Element childElement : childElements) {
@@ -2425,6 +2582,8 @@ public final class ModelFormField {
                         optionSources.add(new ListOptions(childElement, 
modelFormField));
                     } else if ("entity-options".equals(childName)) {
                         optionSources.add(new EntityOptions(childElement, 
modelFormField));
+                    } else if ("group-options".equals(childName)) {
+                        groupSources.add(new GroupOptions(childElement, 
modelFormField));
                     }
                 }
             } else {
@@ -2433,6 +2592,7 @@ public final class ModelFormField {
             }
             optionSources.trimToSize();
             this.optionSources = Collections.unmodifiableList(optionSources);
+            this.groupOptions = Collections.unmodifiableList(groupSources);
         }
 
         // Copy constructor.
@@ -2448,18 +2608,25 @@ public final class ModelFormField {
                 }
                 this.optionSources = 
Collections.unmodifiableList(optionSources);
             }
+            List<GroupOptions> groupOptions = new 
ArrayList<>(original.groupOptions.size());
+            for (GroupOptions group: original.groupOptions) {
+                groupOptions.add(group.copy(modelFormField));
+            }
+            this.groupOptions = groupOptions;
         }
 
         protected FieldInfoWithOptions(int fieldSource, int fieldType, 
List<OptionSource> optionSources) {
             super(fieldSource, fieldType, null);
             this.noCurrentSelectedKey = FlexibleStringExpander.getInstance("");
             this.optionSources = Collections.unmodifiableList(new 
ArrayList<>(optionSources));
+            this.groupOptions = Collections.emptyList();
         }
 
         public FieldInfoWithOptions(int fieldSource, int fieldType, 
ModelFormField modelFormField) {
             super(fieldSource, fieldType, modelFormField);
             this.noCurrentSelectedKey = FlexibleStringExpander.getInstance("");
             this.optionSources = Collections.emptyList();
+            this.groupOptions = Collections.emptyList();
         }
 
         /**
@@ -2500,6 +2667,13 @@ public final class ModelFormField {
         public List<OptionSource> getOptionSources() {
             return optionSources;
         }
+        /**
+         * Gets group options.
+         * @return the group options
+         */
+        public List<GroupOptions> getGroupOptions() {
+            return groupOptions;
+        }
     }
 
     /**
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 79af56caee..beee77fa04 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
@@ -61,6 +61,7 @@ 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.model.GroupOption;
 import org.apache.ofbiz.widget.renderer.macro.model.Option;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
 import 
org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlMacroCall;
@@ -928,6 +929,7 @@ public final class RenderableFtlFormElementsBuilder {
         }
 
         final var allOptionValues = dropDownField.getAllOptionValues(context, 
WidgetWorker.getDelegator(context));
+        final var allGroupValues = dropDownField.getGroupOptions();
         final var explicitDescription =
                 // Populate explicitDescription with the description from the 
option associated with the current value.
                 allOptionValues.stream()
@@ -957,15 +959,11 @@ public final class RenderableFtlFormElementsBuilder {
                             : UtilMisc.toList(currentValue))
                         : Collections.emptyList();
 
-        var optionsList = allOptionValues.stream()
-                .map(optionValue -> {
-                    var encodedKey = encode(optionValue.getKey(), 
modelFormField, context);
-                    var truncatedDescription = 
truncate(optionValue.getDescription(), textSizeOptional);
-                    var selected = 
currentValuesList.contains(optionValue.getKey());
-
-                    return new Option(encodedKey, truncatedDescription, 
selected);
-                })
-                .collect(Collectors.toList());
+        var optionsList = new ArrayList<>();
+        if (UtilValidate.isNotEmpty(allGroupValues)) {
+            optionsList.addAll(populateGroupAndOptions(context, 
allGroupValues, modelFormField, textSizeOptional, currentValuesList));
+        }
+        optionsList.addAll(populateOptions(context, allOptionValues, 
modelFormField, textSizeOptional, currentValuesList));
 
         builder.objectParameter("options", optionsList);
 
@@ -989,6 +987,43 @@ public final class RenderableFtlFormElementsBuilder {
         return builder.build();
     }
 
+    private List<Object> populateGroupAndOptions(Map<String, Object> context, 
List<ModelFormField.GroupOptions> allGroupOptions,
+                                         ModelFormField modelFormField, 
Optional<Integer> textSizeOptional, List<String> currentValuesList) {
+        if (UtilValidate.isEmpty(allGroupOptions)) {
+            return new ArrayList<>();
+        }
+        return UtilGenerics.cast(allGroupOptions.stream()
+                .map(groupOptions -> {
+                    var groupOptionId = groupOptions.getId(context);
+                    var truncatedDescription = 
truncate(groupOptions.getDescription(context), textSizeOptional);
+                    var widgetStyle = groupOptions.getWidgetStyle(context);
+                    List<Object> optionsInGroupList = new ArrayList<>();
+                    optionsInGroupList.addAll(populateGroupAndOptions(context,
+                            groupOptions.getGroupOptions(),
+                            modelFormField, textSizeOptional, 
currentValuesList));
+                    optionsInGroupList.addAll(populateOptions(context,
+                            groupOptions.getAllOptionValues(context, 
WidgetWorker.getDelegator(context)),
+                            modelFormField, textSizeOptional, 
currentValuesList));
+                    return new GroupOption(groupOptionId, 
truncatedDescription, widgetStyle, optionsInGroupList);
+                })
+        .toList());
+    }
+    private List<Object> populateOptions(Map<String, Object> context, 
List<ModelFormField.OptionValue> allOptionValues,
+                                         ModelFormField modelFormField, 
Optional<Integer> textSizeOptional, List<String> currentValuesList) {
+        if (UtilValidate.isEmpty(allOptionValues)) {
+            return new ArrayList<>();
+        }
+        return UtilGenerics.cast(allOptionValues.stream()
+                .map(optionValue -> {
+                    var encodedKey = encode(optionValue.getKey(), 
modelFormField, context);
+                    var truncatedDescription = 
truncate(optionValue.getDescription(), textSizeOptional);
+                    var selected = 
currentValuesList.contains(optionValue.getKey());
+
+                    return new Option(encodedKey, truncatedDescription, 
selected);
+                })
+        .toList());
+    }
+
     /**
      * Create an ajaxXxxx JavaScript CSV string from a list of UpdateArea 
objects. See
      * <code>OfbizUtil.js</code>.
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/model/GroupOption.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/model/GroupOption.java
new file mode 100644
index 0000000000..e9f2a3ba23
--- /dev/null
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/model/GroupOption.java
@@ -0,0 +1,30 @@
+/*
+ * 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.model;
+
+import java.util.List;
+
+/**
+ * Record representing a group option in a drop-down (select) list.
+ */
+public record GroupOption(String id,
+                          String description,
+                          String widgetStyle,
+                          List<Object> options) {
+}
diff --git a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl 
b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
index 154f67c3f2..8ce8fd76f8 100644
--- a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
+++ b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
@@ -156,6 +156,35 @@ under the License.
   </span>
 </#macro>
 
+<#macro renderDropDownOptionList items currentValue multiple dDFCurrent 
noCurrentSelectedKey>
+  <#list items as item>
+    <#if item.options?has_content>
+      <#if groupOpen??></optgroup></#if>
+      <optgroup label="${item.description}"<#if item.id??> 
id="${item.id}"</#if><#if item.widgetStyle??> 
class="${item.widgetStyle}"</#if>/>
+      <@renderDropDownOptionList item.options currentValue multiple dDFCurrent 
noCurrentSelectedKey/>
+      <#assign groupOpen = true/>
+    <#else>
+      <#if multiple>
+        <option
+        <#if currentValue?has_content && item.selected()>
+          selected
+        <#elseif !currentValue?has_content && noCurrentSelectedKey?has_content 
&& noCurrentSelectedKey == item.key()>
+          selected
+        </#if>
+        value="${item.key()}">${item.description()}</option><#rt/>
+      <#else>
+        <option
+        <#if currentValue?has_content && currentValue == item.key() && 
dDFCurrent?has_content && "selected" == dDFCurrent>
+          selected
+        <#elseif !currentValue?has_content && noCurrentSelectedKey?has_content 
&& noCurrentSelectedKey == item.key()>
+          selected
+        </#if>
+        value="${item.key()}">${item.description()}</option><#rt/>
+      </#if>
+    </#if>
+  </#list>
+  <#if groupOpen??></optgroup></#if>
+</#macro>
 <#macro renderDropDownField name className id formName explicitDescription 
options ajaxEnabled
         otherFieldName="" otherValue="" otherFieldSize=""
         alert="" conditionGroup="" tabindex="" multiple=false event="" size="" 
placeCurrentValueAsFirstOption=false
@@ -184,25 +213,7 @@ under the License.
       <#elseif !options?has_content>
           <option value="">&nbsp;</option>
       </#if>
-      <#list options as item>
-        <#if multiple>
-          <option
-            <#if currentValue?has_content && item.selected()>
-              selected
-            <#elseif !currentValue?has_content && 
noCurrentSelectedKey?has_content && noCurrentSelectedKey == item.key()>
-              selected
-            </#if>
-            value="${item.key()}">${item.description()}</option><#rt/>
-        <#else>
-          <option
-            <#if currentValue?has_content && currentValue == item.key() && 
dDFCurrent?has_content && "selected" == dDFCurrent>
-              selected
-            <#elseif !currentValue?has_content && 
noCurrentSelectedKey?has_content && noCurrentSelectedKey == item.key()>
-              selected
-            </#if>
-            value="${item.key()}">${item.description()}</option><#rt/>
-        </#if>
-      </#list>
+      <@renderDropDownOptionList options currentValue multiple dDFCurrent 
noCurrentSelectedKey/>
     </select>
   </span>
   <#if otherFieldName?has_content>

Reply via email to