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 <group-options> 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 <entity-options> 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=""> </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>