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 <[email protected]>
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>