As of Broadleaf 1.5, a new domain DataDrivenEnum was introduced to dynamically manage enums via the admin. This is particularly useful for fields that only allow certain values while having the ability to be modified without any re-deployments.
For example, if the Product domain had a field to represent its material, DataDrivenEnums can be directly mapped to this field based on its type instead of relying on metadata-based enums, so that the enums can be managed dynamically without needing a re-deployment.
DataDrivenEnums can be managed in the admin under the Catalog navigation menu:
You can see all the defined enums in the same grid:
You can create enums with different types, values, and display values:
The display value of the DataDrivenEnum is also translatable, so that if there’s any need to be used in commerce, it would be correctly translated based on current locale. For example, faceting products based on a DataDrivenEnum field such as material.
From out-of-box, the Product domain has three DataDrivenEnum fields, brand, merchandising type, and target demographic, and these fields can be used to further categorize products.
Important
|
The metadata for these fields are not added by default. To utilize these fields, you will need to manually add their metadata in your project by following the instructions in the next section. |
To add the metadata for the out-of-box DataDrivenEnum fields, we need to create some configuration classes in the metadata services (i.e. MyProject/services/metadata
).
First, a my-catalog-messages.properties
messages file needs to be created for the metadata field labels:
# Fields
## Product Fields
product.fields.brand=Brand
product.fields.merchandisingType=Merchandising Type
product.fields.targetDemographic=Target Demographic
Then, that messages file needs to be registered:
public class MyCustomMetadataMessages implements MetadataMessagesBasename {
@Override
@NonNull
public String getBasename() {
return StringUtils.join(Arrays.asList("messages/my-catalog-messages"), ",");
}
}
Once the labels are ready, a metadata autoconfiguration class needs to be created:
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(CatalogMetadataProperties.class)
@AutoConfigureAfter(CatalogServicesMetadataAutoConfiguration.class)
public class MyCatalogMetadataAutoConfiguration {
private final CatalogMetadataProperties properties; // (1)
@UtilityClass
public class DemoProductProps {
public final String BRAND = "brand";
public final String MERCHANDISING_TYPE = "merchandisingType";
public final String TARGET_DEMOGRAPHIC = "targetDemographic";
public final String BRAND_ID = BRAND + "Id";
public final String MERCHANDISING_TYPE_ID = MERCHANDISING_TYPE + "Id";
public final String TARGET_DEMOGRAPHIC_ID = TARGET_DEMOGRAPHIC + "Id";
}
@Bean // (2)
public ComponentSource addProductDataDrivenEnumFields() {
return registry -> {
for (DefaultProductType type : getAvailableProductTypes()) {
UpdateEntityView<?> productUpdate = (UpdateEntityView<?>) registry
.get(String.format(ProductIds.UPDATE, type.name()));
Group<?> merchandisingGroup = productUpdate.getForm(ProductForm.STORE_FRONT)
.getGroup(ProductGroups.MERCHANDISING);
merchandisingGroup
.addField(DemoProductProps.BRAND,
createDataDrivenEnumLookup(DataDrivenEnumTypes.BRAND, // (3)
"product.fields.brand") // (4)
.order(1100))
.addField(DemoProductProps.MERCHANDISING_TYPE,
createDataDrivenEnumLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
"product.fields.merchandisingType")
.order(1200))
.addField(DemoProductProps.TARGET_DEMOGRAPHIC,
createDataDrivenEnumLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
"product.fields.targetDemographic")
.order(1300));
}
};
}
/**
* Retrieve the list of active product types.
*
* @return a list of active product types
*/
protected List<DefaultProductType> getAvailableProductTypes() { // (5)
return Arrays.stream(values())
.filter(t -> properties.getActiveProductTypes().contains(t.name()))
.collect(Collectors.toList());
}
}
Metadata properties for the CatalogServices. This is needed in our configuration class to retrieve all the active product types, in order to add the DataDrivenEnum fields metadata to the forms across all types.
This bean adds the DataDrivenEnum fields metadata to the product’s update form in the "Merchandising" group, using DataDrivenEnumLookupHelpers
to lookup DataDrivenEnums by id.
Both createDataDrivenEnumLookup
and DataDrivenEnumTypes
are defined in DataDrivenEnumLookupHelpers
.
Labels defined earlier in the my-catalog-messages.properties
messages file.
Method to retrieve all the active product types based on CatalogMetadataProperties
.
Lastly, a META-INF/spring.factories
file needs to be created for the messages and our MetadataAutoConfiguration:
com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
com.myproject.metadata.i18n.MyCustomMetadataMessages
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.myproject.metadata.catalog.MyCatalogMetadataAutoConfiguration
For rule-based categories, you can define product membership rules to include products in the category based on certain Product fields (i.e. Product’s SKU, name, UPC, etc). If you want to add the out-of-box data driven enum fields to the product membership rule builder, you can simply add the following bean to the MyCatalogMetadataAutoConfiguration
that we defined earlier.
@Bean
public ComponentSource addDataDrivenEnumFieldsToCategoryProductMembershipRule() {
return registry -> {
TreeView<?> categoryTree = (TreeView<?>) registry.get(CategoryIds.TREE);
FormView<?> categoryProductUpdate =
categoryTree.getUpdateForm(CategoryIds.Forms.PRODUCTS);
Group<?> productMembershipRuleGroup = categoryProductUpdate
.getGroup(CategoryGroups.PRODUCT_MEMBERSHIP_RULE);
QueryBuilderField<?> ruleBuilder = (QueryBuilderField<?>) productMembershipRuleGroup
.getField(CategoryProps.PRODUCT_MEMBERSHIP_RULE);
ruleBuilder
.addField(DemoProductProps.BRAND_ID,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.BRAND,
"product.fields.brand")
.order(7000))
.addField(DemoProductProps.MERCHANDISING_TYPE_ID,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
"product.fields.merchandisingType")
.order(8000))
.addField(DemoProductProps.TARGET_DEMOGRAPHIC_ID,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
"product.fields.targetDemographic")
.order(9000));
};
}
Similarly, there are also rule builders that you can modify to target the out-of-box data driven enums in the item qualifier and target item criteria in Offer. To do so, we can follow the similar steps above in the metadata services (i.e. MyProject/services/metadata
), but this time, instead of making the changes in com.myproject.metadata.catalog
, we will create a new package com.myproject.metadata.offer
.
Note
|
If unfamiliar with Offers, you can learn more by referencing OfferServices Documentation |
First, a my-offer-messages.properties
messages file needs to be created for the metadata field labels:
# Fields
## Offer Fields
offer.fields.item-criteria.brand=Item Brand
offer.fields.item-criteria.merchandising-type=Item Merchandising Type
offer.fields.item-criteria.target-demographic=Item Target Demographic
Then, that messages file needs to be registered in the existing MetadataMessagesBasename
implementation that we created earlier:
public class MyCustomMetadataMessages implements MetadataMessagesBasename {
@Override
@NonNull
public String getBasename() {
return StringUtils.join(Arrays.asList("messages/my-catalog-messages", "messages/my-offer-messages"), ",");
}
}
Once the labels are ready, a metadata autoconfiguration class needs to be created:
@Configuration
@RequiredArgsConstructor
@AutoConfigureAfter(OfferServicesMetadataAutoConfiguration.class)
public class MyOfferMetadataAutoConfiguration {
@UtilityClass
public class DemoOfferProps { // (1)
public final String ITEM_BRAND = "attributes[brandId]";
public final String ITEM_MERCHANDISING_TYPE = "attributes[merchandisingTypeId]";
public final String ITEM_TARGET_DEMOGRAPHIC = "attributes[targetDemographicId]";
}
@Bean
public ComponentSource addDataDrivenEnumFieldsToOfferCriteriaRuleBuilder() {
return registry -> {
List<FieldArrayBlockField<?>> criteriaFields = getOfferCriteriaFields(
(EntityView<?>) registry.get(OfferIds.CREATE),
(EntityView<?>) registry.get(OfferIds.UPDATE));
List<RuleBuilderField<?>> ruleBuilders = getRuleBuilderFields(criteriaFields);
ruleBuilders.forEach(ruleBuilder -> ruleBuilder
.addField(DemoOfferProps.ITEM_BRAND,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.BRAND, // (2)
"offer.fields.item-criteria.brand")
.order(7100))
.addField(DemoOfferProps.ITEM_MERCHANDISING_TYPE,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
"offer.fields.item-criteria.merchandising-type")
.order(7200))
.addField(DemoOfferProps.ITEM_TARGET_DEMOGRAPHIC,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
"offer.fields.item-criteria.target-demographic")
.order(7300)));
};
}
protected List<FieldArrayBlockField<?>> getOfferCriteriaFields(EntityView<?>... entityViews) {
List<FieldArrayBlockField<?>> criteriaFields = new ArrayList<>();
for (EntityView<?> view : entityViews) {
FormView<?> generalForm = view.getGeneralForm();
Group<?> itemQualifierGroup =
generalForm.getGroup(OfferGroupIds.OFFER_ITEM_QUALIFIERS_GROUP);
Group<?> targetItemGroup =
generalForm.getGroup(OfferGroupIds.OFFER_TARGET_ITEMS_FIELD_GROUP);
// (3)
FieldArrayBlockField<?> itemQualifierCriteria =
(FieldArrayBlockField<?>) itemQualifierGroup
.getField(OfferProps.ITEM_QUALIFIER_CRITERIA);
FieldArrayBlockField<?> targetItemCriteria = (FieldArrayBlockField<?>) targetItemGroup
.getField(OfferProps.TARGET_ITEM_CRITERIA);
criteriaFields.add(itemQualifierCriteria);
criteriaFields.add(targetItemCriteria);
}
return criteriaFields;
}
protected List<RuleBuilderField<?>> getRuleBuilderFields(
List<FieldArrayBlockField<?>> criteriaFields) {
List<RuleBuilderField<?>> ruleBuilders = new ArrayList<>();
criteriaFields.forEach(criteriaField -> {
RuleBuilderField<?> fulfillmentGroup =
(RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
.getFieldKey(OfferProps.ItemCriteriaRule.FULFILLMENT_GROUP_RULES));
RuleBuilderField<?> fulfillmentItem =
(RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
.getFieldKey(OfferProps.ItemCriteriaRule.FULFILLMENT_ITEM_RULES));
RuleBuilderField<?> order =
(RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
.getFieldKey(OfferProps.ItemCriteriaRule.ORDER_RULES));
RuleBuilderField<?> orderItem =
(RuleBuilderField<?>) criteriaField.getComponent(FieldArrayField.Keys
.getFieldKey(OfferProps.ItemCriteriaRule.ORDER_ITEM_RULES));
// (4)
ruleBuilders.add(fulfillmentGroup);
ruleBuilders.add(fulfillmentItem);
ruleBuilders.add(order);
ruleBuilders.add(orderItem);
});
return ruleBuilders;
}
}
Defines where those data driven enum IDs are located in OrderLineItemDto
, they are added in OrderLineItemDto#attributes
by default.
Both createDataDrivenEnumLookup
and DataDrivenEnumTypes
are defined in DataDrivenEnumLookupHelpers
.
Gets the criteria fields from both item qualifier and target item criteria groups.
Since we use the same fields in the rule builders for fulfillment group, fulfillment item, order, and order item, we need to make sure to add the out-of-box data driven enum fields to all the rule builders. This is not necessary if not needed for your use cases, however.
Lastly, we need to add our MetadataAutoConfiguration to META-INF/spring.factories
that we created earlier:
com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
com.myproject.metadata.i18n.MyCustomMetadataMessages
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.myproject.metadata.catalog.MyCatalogMetadataAutoConfiguration,\
com.myproject.metadata.offer.MyOfferMetadataAutoConfiguration
Then you’re done! You can now build rules with those fields:
In PriceList, there is also a rule builder in the price modifier, which only has target type and id by default (referring to PriceableTarget’s type and id). To target the out-of-box data driven enums in the price modifier in PriceList, we will create a new package `com.myproject.metadata.pricing
.
Note
|
If unfamiliar with PriceLists, you can learn more by referencing PricingServices Documentation |
First, a my-pricing-messages.properties
messages file needs to be created for the metadata field labels:
# Fields
## Price List Fields
price-list.fields.item-criteria.brand=Item Brand
price-list.fields.item-criteria.merchandising-type=Item Merchandising Type
price-list.fields.item-criteria.target-demographic=Item Target Demographic
Note
|
You can also create a common messages file and define common labels there |
Then, that messages file needs to be registered in the existing MetadataMessagesBasename
implementation that we created earlier:
public class MyCustomMetadataMessages implements MetadataMessagesBasename {
@Override
@NonNull
public String getBasename() {
return StringUtils.join(Arrays.asList("messages/my-catalog-messages", "messages/my-offer-messages", "messages/my-pricing-messages"), ",");
}
}
Once the labels are ready, a metadata autoconfiguration class needs to be created:
@Configuration
@RequiredArgsConstructor
@AutoConfigureAfter(PricingServicesMetadataAutoConfiguration.class)
public class MyPricingMetadataAutoConfiguration {
@UtilityClass
public class DemoPriceListProps { // (1)
public final String ITEM_BRAND = "attributes[skuRef]?.brand?.id";
public final String ITEM_MERCHANDISING_TYPE = "attributes[skuRef]?.merchandisingType?.id";
public final String ITEM_TARGET_DEMOGRAPHIC = "attributes[skuRef]?.targetDemographic?.id";
}
@Bean
public ComponentSource addDataDrivenEnumFieldsToPriceListPriceModifierRuleBuilder() {
return registry -> {
List<EntityView<?>> entityViews = new ArrayList<>(); // (2)
entityViews.add((EntityView<?>) registry.get(PricingIds.CREATE));
entityViews.add((EntityView<?>) registry.get(PricingIds.UPDATE));
entityViews.add((EntityView<?>) registry.get(PricingIds.SALE_CREATE));
entityViews.add((EntityView<?>) registry.get(PricingIds.SALE_UPDATE));
entityViews.add((EntityView<?>) registry.get(PricingIds.CONTRACT_CREATE));
entityViews.add((EntityView<?>) registry.get(PricingIds.CONTRACT_UPDATE));
entityViews.forEach(view -> {
FormView<?> generalForm = view.getGeneralForm();
Group<?> priceModifierGroup = generalForm.getGroup(PricingGroups.PRICE_MODIFIER);
Group<?> priceModifierSubGroup =
priceModifierGroup.getGroup(PricingGroups.PRICE_MODIFIER_SUB);
RuleBuilderField<?> priceModifierCriteriaRuleBuilder =
(RuleBuilderField<?>) priceModifierSubGroup
.getField(PricingProps.PriceModifier.PRICEMODIFIER_CRITERIA);
priceModifierCriteriaRuleBuilder
.addField(DemoPriceListProps.ITEM_BRAND,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.BRAND, // (3)
"price-list.fields.item-criteria.brand")
.order(3000))
.addField(DemoPriceListProps.ITEM_MERCHANDISING_TYPE,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.MERCHANDISING_TYPE,
"price-list.fields.item-criteria.merchandising-type")
.order(4000))
.addField(DemoPriceListProps.ITEM_TARGET_DEMOGRAPHIC,
createDataDrivenEnumIdLookup(DataDrivenEnumTypes.TARGET_DEMOGRAPHIC,
"price-list.fields.item-criteria.target-demographic")
.order(5000));
});
};
}
}
Defines where those data driven enum IDs are located in PriceableTarget
, they are added in skuRef
in PriceableTarget#attributes
by default.
Since there are different types of price lists (normal price lists, sales, and contracts) and the price modifier is in both create and update forms, we need to make sure we modify the metadata for all those EntityViews
.
Both createDataDrivenEnumLookup
and DataDrivenEnumTypes
are defined in DataDrivenEnumLookupHelpers
.
Lastly, we need to add our MetadataAutoConfiguration to META-INF/spring.factories
that we created earlier:
com.broadleafcommerce.metadata.i18n.MetadataMessagesBasename=\
com.myproject.metadata.i18n.MyCustomMetadataMessages
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.myproject.metadata.catalog.MyCatalogMetadataAutoConfiguration,\
com.myproject.metadata.offer.MyOfferMetadataAutoConfiguration,\
com.myproject.metadata.offer.MyPricingMetadataAutoConfiguration
Then you’re done! You can now build rules with those fields: