72108ddb by Ean Schuessler

Fix validation error reporting and clarify feature service docs

Critical fix: Errors now correctly captured and reported
- Capture errors immediately after render() in CustomScreenTestImpl
- Added saveMessagesToSession() to WebFacadeStub for framework compatibility
- Actions that fail validation now report status:'error' instead of false success

Documentation fix: Clarify two different feature services
- applyProductFeatures: Apply EXISTING features to products (most common)
- createProductFeature: Create NEW feature definitions (rare)
- Updated EditProduct and ProductFeature wiki docs with correct examples
1 parent 6259fd93
......@@ -762,13 +762,16 @@ Manage product details, features, prices, categories, and associations.
Features are managed directly on the EditProduct screen (not a separate EditFeatures subscreen).
**IMPORTANT:** Use `applyProductFeatures` to add existing features (like ColorGreen).
Use `createProductFeature` only to create NEW feature definitions.
### Add Selectable Feature (Virtual Products)
```
path: PopCommerce/PopCommerceAdmin/Catalog/Product/EditProduct
action: createProductFeature
action: applyProductFeatures
parameters: {
productId: "DEMO_VAR",
productFeatureId: "ColorRed",
productFeatureIdList: ["ColorGreen"],
applTypeEnumId: "PfatSelectable"
}
```
......@@ -776,10 +779,10 @@ parameters: {
### Add Distinguishing Feature (Variant Products)
```
path: PopCommerce/PopCommerceAdmin/Catalog/Product/EditProduct
action: createProductFeature
action: applyProductFeatures
parameters: {
productId: "DEMO_VAR_RED",
productFeatureId: "ColorRed",
productId: "DEMO_VAR_GR_SM",
productFeatureIdList: ["ColorGreen", "SizeSmall"],
applTypeEnumId: "PfatDistinguishing"
}
```
......@@ -944,14 +947,31 @@ parameters: {
versionName="v1">
<fileData><![CDATA[# ProductFeature Service
Apply features (like color, size) to products.
Manage product features (color, size, etc.) - two different operations:
## mantle.product.ProductServices.create#ProductFeature
## IMPORTANT: Two Different Services
1. **applyProductFeatures** - Apply EXISTING features to a product (most common)
2. **createProductFeature** - Create a NEW feature definition (rare)
Most of the time you want `applyProductFeatures` to link existing features like ColorGreen or SizeLarge to products.
---
## mantle.product.ProductServices.apply#ProductFeatures
**Use this to apply existing features to products.**
**Action:** `applyProductFeatures`
**Required:**
- `productId`: Product to add feature to
- `productFeatureId`: Feature ID (e.g., ColorRed, SizeMedium)
- `applTypeEnumId`: How feature applies to product
- `productId`: Product to add features to
- `productFeatureIdList`: List of feature IDs (e.g., ["ColorGreen", "SizeMedium"])
**Optional:**
- `applTypeEnumId`: How feature applies (default: PfatStandard)
- `fromDate`: When feature becomes active (default: now)
- `sequenceNum`: Display order
**Application Types (applTypeEnumId):**
- `PfatSelectable`: Customer can choose (use on virtual parent)
......@@ -959,34 +979,61 @@ Apply features (like color, size) to products.
- `PfatStandard`: Always included
- `PfatRequired`: Must be selected at purchase
**Optional:**
- `fromDate`: When feature becomes active (default: now)
- `sequenceNum`: Display order
**Example - Virtual Parent:**
**Example - Add Selectable Feature to Virtual Parent:**
```
action: createProductFeature
action: applyProductFeatures
parameters: {
productId: "DEMO_VAR",
productFeatureId: "ColorGreen",
productFeatureIdList: ["ColorGreen"],
applTypeEnumId: "PfatSelectable"
}
```
**Example - Variant:**
**Example - Add Distinguishing Feature to Variant:**
```
action: createProductFeature
action: applyProductFeatures
parameters: {
productId: "DEMO_VAR_GR_SM",
productFeatureId: "ColorGreen",
productFeatureIdList: ["ColorGreen", "SizeSmall"],
applTypeEnumId: "PfatDistinguishing"
}
```
## Common Feature IDs
---
## mantle.product.ProductServices.create#ProductFeature
**Use this ONLY to create a NEW feature that doesn't exist yet.**
**Action:** `createProductFeature`
**Required:**
- `productFeatureTypeEnumId`: Type of feature (PftColor, PftSize, etc.)
- `description`: Feature name/description (e.g., "Green", "Extra Small")
**Optional:**
- `productId`: If provided, also applies the new feature to this product
- `applTypeEnumId`: How to apply (if productId provided)
- `abbrev`: Short abbreviation (e.g., "GR", "XS")
**Example - Create a new color feature:**
```
action: createProductFeature
parameters: {
productFeatureTypeEnumId: "PftColor",
description: "Teal",
abbrev: "TL",
productId: "DEMO_VAR",
applTypeEnumId: "PfatSelectable"
}
```
---
## Common Feature IDs (already exist - use with applyProductFeatures)
Colors: ColorRed, ColorBlue, ColorGreen, ColorBlack, ColorWhite
Sizes: SizeSmall, SizeMedium, SizeLarge, SizeXL]]></fileData>
**Colors:** ColorRed, ColorBlue, ColorGreen, ColorBlack, ColorWhite
**Sizes:** SizeSmall, SizeMedium, SizeLarge, SizeXL]]></fileData>
</moqui.resource.DbResourceFile>
<moqui.resource.wiki.WikiPage
......
......@@ -397,6 +397,25 @@ class CustomScreenTestImpl implements McpScreenTest {
try {
logger.info("Starting render for ${screenPathList} with root ${csti.rootScreenLocation}")
screenRender.render(wfs.getRequest(), wfs.getResponse())
// IMMEDIATELY capture errors after render - before anything can clear them
// This is critical for transition validation errors which get cleared during redirect handling
if (eci.message.hasError()) {
List<String> immediateErrors = new ArrayList<>(eci.message.getErrors())
if (immediateErrors.size() > 0) {
stri.errorMessages.addAll(immediateErrors)
logger.warn("Captured ${immediateErrors.size()} errors immediately after render: ${immediateErrors}")
}
}
List<org.moqui.context.ValidationError> immediateValErrors = eci.message.getValidationErrors()
if (immediateValErrors != null && immediateValErrors.size() > 0) {
for (org.moqui.context.ValidationError ve : immediateValErrors) {
String errMsg = ve.getMessage() ?: "${ve.getField()}: Validation error"
stri.errorMessages.add(errMsg)
}
logger.warn("Captured ${immediateValErrors.size()} validation errors immediately after render")
}
// get the response text from the WebFacadeStub
stri.outputString = wfs.getResponseText()
stri.jsonObj = wfs.getResponseJsonObj()
......@@ -416,7 +435,7 @@ class CustomScreenTestImpl implements McpScreenTest {
// pop the context stack, get rid of var space
cs.pop()
// check, pass through, error messages
// check, pass through, error messages from ExecutionContext
if (eci.message.hasError()) {
stri.errorMessages.addAll(eci.message.getErrors())
eci.message.clearErrors()
......@@ -426,6 +445,29 @@ class CustomScreenTestImpl implements McpScreenTest {
csti.errorCount += stri.errorMessages.size()
}
// Also check errors saved to session by saveMessagesToSession() during transition redirects
// This captures validation errors that occur during service calls but get cleared before we check
if (wfs instanceof WebFacadeStub) {
def savedErrors = wfs.getSavedErrors()
if (savedErrors && savedErrors.size() > 0) {
stri.errorMessages.addAll(savedErrors)
StringBuilder sb = new StringBuilder("Saved error messages from ${stri.screenPath}: ")
for (String errorMessage in savedErrors) sb.append("\n").append(errorMessage)
logger.warn(sb.toString())
csti.errorCount += savedErrors.size()
}
def savedValidationErrors = wfs.getSavedValidationErrors()
if (savedValidationErrors && savedValidationErrors.size() > 0) {
// Convert ValidationError objects to string messages
for (def ve in savedValidationErrors) {
def errMsg = ve.message ?: "${ve.field}: Validation error"
stri.errorMessages.add(errMsg)
csti.errorCount++
}
logger.warn("Saved validation errors from ${stri.screenPath}: ${savedValidationErrors.size()} errors")
}
}
// check for error strings in output
if (stri.outputString != null) for (String errorStr in csti.errorStrings) if (stri.outputString.contains(errorStr)) {
String errMsg = "Found error [${errorStr}] in output from ${stri.screenPath}"
......
......@@ -294,6 +294,46 @@ class WebFacadeStub implements WebFacade {
this.responseText = "System message not implemented in stub"
}
// Save methods - capture errors/messages before redirect so they're available after render
void saveMessagesToSession() {
// Capture current errors and validation errors from ExecutionContext
if (ecfi instanceof org.moqui.impl.context.ExecutionContextFactoryImpl) {
org.moqui.impl.context.ExecutionContextFactoryImpl ecfiImpl = (org.moqui.impl.context.ExecutionContextFactoryImpl) ecfi
org.moqui.context.ExecutionContext eci = ecfiImpl.getEci()
if (eci != null) {
MessageFacade mf = eci.getMessage()
if (mf != null) {
List<String> errors = mf.getErrors()
if (errors != null && errors.size() > 0) {
savedErrors.addAll(errors)
logger.info("WebFacadeStub.saveMessagesToSession: Captured ${errors.size()} errors: ${errors}")
}
List<ValidationError> valErrors = mf.getValidationErrors()
if (valErrors != null && valErrors.size() > 0) {
savedValidationErrors.addAll(valErrors)
logger.info("WebFacadeStub.saveMessagesToSession: Captured ${valErrors.size()} validation errors")
}
}
}
}
}
void saveRequestParametersToSession() {
// Store parameters for potential re-display
sessionAttributes.put("moqui.saved.parameters", new HashMap(parameters))
}
void saveErrorParametersToSession() {
// Store error parameters
errorParameters.putAll(parameters)
}
void saveParametersToSession(Map parameters) {
if (parameters) {
sessionAttributes.put("moqui.saved.parameters", new HashMap(parameters))
}
}
// Helper methods for ScreenTestImpl
String getResponseText() {
if (responseText != null) {
......