87e0b6a4 by Ean Schuessler

Refactor MCP services and adopt slash-based screen paths

Extract core MCP logic into modular services (ResolveScreenPath, RenderScreenNarrative, ExecuteScreenAction) and update screen path conventions to use slash notation (e.g., /PopCommerce/Product) instead of dot notation. This aligns MCP navigation with browser URLs and improves path resolution reliability.

- Split McpServices.xml into specialized services for better maintainability
- Update DefaultScreenMacros.mcp.ftl to generate slash-based links
- Update prompts and documentation to reflect new path convention
- Enhance CustomScreenTestImpl to support slash path parsing
- Add AGENTS.md documenting self-guided narrative screens architecture
1 parent 09883cfe
This diff is collapsed. Click to expand it.
......@@ -67,21 +67,21 @@ For ${focus?.capitalize() ?: 'General'} operations:
${focus == 'catalog' ? '''
## Catalog Operations
- **FindProduct**: Use `PopCommerce.PopCommerceAdmin.Catalog.Product.FindProduct` for product catalog
- **FindFeature**: Use `PopCommerce.PopCommerceAdmin.Catalog.Feature.FindFeature` to search by features like color or size
- **EditPrices**: Use `PopCommerce.PopCommerceAdmin.Catalog.Product.EditPrices` to check prices
- **FindProduct**: Use `/PopCommerce/PopCommerceAdmin.Catalog.Product.FindProduct` for product catalog
- **FindFeature**: Use `/PopCommerce/PopCommerceAdmin.Catalog.Feature.FindFeature` to search by features like color or size
- **EditPrices**: Use `/PopCommerce/PopCommerceAdmin.Catalog.Product.EditPrices` to check prices
''' : ''}
${focus == 'orders' ? '''
## Order Operations
- **FindOrder**: Use `PopCommerce.PopCommerceAdmin.Order.FindOrder` for order status and lookup
- **QuickSearch**: Use `PopCommerce.PopCommerceAdmin.QuickSearch` for general order searches
- **FindOrder**: Use `/PopCommerce/PopCommerceAdmin.Order.FindOrder` for order status and lookup
- **QuickSearch**: Use `/PopCommerce/PopCommerceAdmin.QuickSearch` for general order searches
''' : ''}
${focus == 'customers' ? '''
## Customer Operations
- **CustomerRoot**: Use `PopCommerce.PopCommerceRoot.Customer` for customer management
- **QuickSearch**: Use `PopCommerce.PopCommerceAdmin.QuickSearch` for customer lookup
- **CustomerRoot**: Use `/PopCommerce/PopCommerceRoot.Customer` for customer management
- **QuickSearch**: Use `/PopCommerce/PopCommerceAdmin.QuickSearch` for customer lookup
''' : ''}
${detailLevel == 'advanced' ? '''
......
......@@ -45,23 +45,23 @@ Use the following discovery tools to explore available functionality:
## Common Screen Paths
### Catalog Operations
- `PopCommerce.PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products
- `PopCommerce.PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size
- `PopCommerce.PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices
- `/PopCommerce/PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products
- `/PopCommerce/PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size
- `/PopCommerce/PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices
### Order Management
- `PopCommerce.PopCommerceAdmin.Order.FindOrder`: Lookup order status and details
- `PopCommerce.PopCommerceAdmin.QuickSearch`: General order and customer search
- `/PopCommerce/PopCommerceAdmin.Order.FindOrder`: Lookup order status and details
- `/PopCommerce/PopCommerceAdmin.QuickSearch`: General order and customer search
### Customer Management
- `PopCommerce.PopCommerceRoot.Customer`: Manage customer accounts
- `PopCommerce.PopCommerceAdmin.QuickSearch`: Customer lookup
- `/PopCommerce/PopCommerceRoot.Customer`: Manage customer accounts
- `/PopCommerce/PopCommerceAdmin.QuickSearch`: Customer lookup
## Tips for LLM Clients
- All screens support parameterized queries for filtering results
- Use `moqui_render_screen` with screen path to execute screens
- Screen paths use dot notation (e.g., `PopCommerce.Catalog.Product`)
- Screen paths use dot notation (e.g., `/PopCommerce/Catalog.Product`)
- Check `moqui_get_screen_details` for required parameters before rendering
- Use `renderMode: "mcp"` for structured JSON output or `"text"` for human-readable format]]></fileData>
</moqui.resource.DbResourceFile>
......@@ -107,23 +107,23 @@ Use the following discovery tools to explore available functionality:
## Common Screen Paths
### Catalog Operations
- `PopCommerce.PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products
- `PopCommerce.PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size
- `PopCommerce.PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices
- `/PopCommerce/PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products
- `/PopCommerce/PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size
- `/PopCommerce/PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices
### Order Management
- `PopCommerce.PopCommerceAdmin.Order.FindOrder`: Lookup order status and details
- `PopCommerce.PopCommerceAdmin.QuickSearch`: General order and customer search
- `/PopCommerce/PopCommerceAdmin.Order.FindOrder`: Lookup order status and details
- `/PopCommerce/PopCommerceAdmin.QuickSearch`: General order and customer search
### Customer Management
- `PopCommerce.PopCommerceRoot.Customer`: Manage customer accounts
- `PopCommerce.PopCommerceAdmin.QuickSearch`: Customer lookup
- `/PopCommerce/PopCommerceRoot.Customer`: Manage customer accounts
- `/PopCommerce/PopCommerceAdmin.QuickSearch`: Customer lookup
## Tips for LLM Clients
- All screens support parameterized queries for filtering results
- Use `moqui_render_screen` with screen path to execute screens
- Screen paths use dot notation (e.g., `PopCommerce.Catalog.Product`)
- Screen paths use dot notation (e.g., `/PopCommerce/Catalog.Product`)
- Check `moqui_get_screen_details` for required parameters before rendering
- Use `renderMode: "mcp"` for structured JSON output or `"text"` for human-readable format]]></fileData>
</moqui.resource.DbResourceFile>
......@@ -132,7 +132,7 @@ Use the following discovery tools to explore available functionality:
<moqui.resource.DbResource
resourceId="WIKI_MCP_DOCS_POPCOMM_ROOT"
parentResourceId="WIKI_MCP_SCREEN_DOCS"
filename="PopCommerce.PopCommerceRoot.md"
filename="PopCommerce/PopCommerceRoot.md"
isFile="Y"/>
<moqui.resource.DbResourceFile
resourceId="WIKI_MCP_DOCS_POPCOMM_ROOT"
......@@ -158,7 +158,7 @@ Use browse tools to explore the full catalog of PopCommerce screens starting fro
<moqui.resource.wiki.WikiPage
wikiPageId="MCP_SCREEN_DOCS/PopCommerceRoot"
wikiSpaceId="MCP_SCREEN_DOCS"
pagePath="PopCommerce.PopCommerceRoot"
pagePath="PopCommerce/PopCommerceRoot"
publishedVersionName="v1"
restrictView="N">
</moqui.resource.wiki.WikiPage>
......
......@@ -7,7 +7,9 @@
<#macro @element></#macro>
<#macro screen><#recurse></#macro>
<#macro screen>
<#recurse>
</#macro>
<#macro widgets>
<#recurse>
......@@ -48,6 +50,7 @@
<#macro "container-dialog">
[Button: ${ec.resource.expand(.node["@button-text"], "")}]
<#recurse>
</#macro>
<#-- ================== Standalone Fields ==================== -->
......@@ -66,18 +69,18 @@
<#assign linkText = sri.getFieldValueString(.node?parent?parent)>
</#if>
<#-- Convert path to dot notation for moqui_render_screen -->
<#-- Convert path to slash notation for moqui_render_screen (matches browser URLs) -->
<#assign fullPath = urlInstance.sui.fullPathNameList![]>
<#assign dotPath = "">
<#list fullPath as pathPart><#assign dotPath = dotPath + (dotPath?has_content)?then(".", "") + pathPart></#list>
<#assign slashPath = "">
<#list fullPath as pathPart><#assign slashPath = slashPath + (slashPath?has_content)?then("/", "") + pathPart></#list>
<#assign paramStr = urlInstance.getParameterString()>
<#if paramStr?has_content><#assign dotPath = dotPath + "?" + paramStr></#if>
<#if paramStr?has_content><#assign slashPath = slashPath + "?" + paramStr></#if>
[${linkText}](${dotPath})<#t>
[${linkText}](${slashPath})<#t>
<#if mcpSemanticData??>
<#if !mcpSemanticData.links??><#assign dummy = mcpSemanticData.put("links", [])></#if>
<#assign linkInfo = {"text": linkText, "path": dotPath, "type": "navigation"}>
<#assign linkInfo = {"text": linkText, "path": slashPath, "type": "navigation"}>
<#assign dummy = mcpSemanticData.links.add(linkInfo)>
</#if>
</#if>
......@@ -98,17 +101,16 @@
<#-- ======================= Form ========================= -->
<#macro "form-single">
<#assign formNode = sri.getFormNode(.node["@name"])>
<#assign mapName = formNode["@map"]!"fieldValues">
<#assign mapName = (formNode["@map"]!"fieldValues")>
<#assign formMap = ec.resource.expression(mapName, "")!>
<#if mcpSemanticData??>
<#if !mcpSemanticData.formMetadata??><#assign dummy = mcpSemanticData.put("formMetadata", {})</#if>
<#if !mcpSemanticData.formMetadata??><#assign dummy = mcpSemanticData.put("formMetadata", {})></#if>
<#assign formMeta = {}>
<#assign formMeta = formMeta + {"name": .node["@name"]!"", "map": mapName}>
<#assign formMeta = {"name": (.node["@name"]!""), "map": mapName}>
<#assign fieldMetaList = []>
<#assign dummy = mcpSemanticData.formMeta.put(.node["@name"], formMeta)>
<#assign dummy = mcpSemanticData.formMetadata.put(.node["@name"], formMeta)>
</#if>
<#if mcpSemanticData?? && formMap?has_content><#assign dummy = mcpSemanticData.put(.node["@name"], formMap)></#if>
......@@ -121,8 +123,7 @@
<#assign title><@fieldTitle fieldSubNode/></#assign>
<#if mcpSemanticData??>
<#assign fieldMeta = {}>
<#assign fieldMeta = fieldMeta + {"name": fieldNode["@name"]!"", "title": title!"", "required": (fieldNode["@required"]! == "true")}>
<#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true")}>
<#if fieldSubNode["text-line"]?has_content><#assign dummy = fieldMeta.put("type", "text")></#if>
<#if fieldSubNode["text-area"]?has_content><#assign dummy = fieldMeta.put("type", "textarea")></#if>
......@@ -138,7 +139,7 @@
</#list>
<#if mcpSemanticData?? && fieldMetaList?has_content>
<#assign dummy = mcpSemanticData.formMeta[.node["@name"]!].put("fields", fieldMetaList)>
<#assign dummy = mcpSemanticData.formMetadata[.node["@name"]!].put("fields", fieldMetaList)>
</#if>
<#t>${sri.popContext()}
......@@ -154,12 +155,9 @@
<#if mcpSemanticData?? && listObject?has_content>
<#assign truncatedList = listObject>
<#if listObject?size > 50>
<#assign truncatedList = listObject?take(50)>
</#if>
<#assign dummy = mcpSemanticData.put(.node["@name"], truncatedList)>
<#if !mcpSemanticData.listMetadata??><#assign dummy = mcpSemanticData.put("listMetadata", {})</#if>
<#if !mcpSemanticData.listMetadata??><#assign dummy = mcpSemanticData.put("listMetadata", {})></#if>
<#assign columnNames = []>
<#list formListColumnList as columnFieldList>
......@@ -167,11 +165,11 @@
<#assign dummy = columnNames.add(fieldNode["@name"]!"")>
</#list>
<#assign dummy = mcpSemanticData.listMeta.put(.node["@name"]!"", {
<#assign dummy = mcpSemanticData.listMetadata.put(.node["@name"]!"", {
"name": .node["@name"]!"",
"totalItems": totalItems,
"displayedItems": truncatedList?size,
"truncated": (listObject?size > 50),
"displayedItems": (totalItems > 50)?then(50, totalItems),
"truncated": (totalItems > 50),
"columns": columnNames
})>
</#if>
......@@ -185,7 +183,8 @@
|
<#list formListColumnList as columnFieldList>| --- </#list>|
<#-- Data Rows -->
<#list (truncatedList?? && truncatedList?size > 0)!listObject as listEntry>
<#list listObject as listEntry>
<#if (listEntry_index >= 50)><#break></#if>
<#t>${sri.startFormListRow(formListInfo, listEntry, listEntry_index, listEntry_has_next)}
<#list formListColumnList as columnFieldList>
<#t>| <#list columnFieldList as fieldNode><@formListSubField fieldNode/><#if fieldNode_has_next> </#if></#list><#t>
......
<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, author(s) have dedicated all
copyright and related and neighboring rights to this software to the
public domain worldwide. This software is distributed without any
warranty.
-->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-3.xsd">
<service verb="mcp" noun="ExecuteScreenAction" authenticate="true" allow-remote="true" transaction-timeout="60">
<description>Execute a screen action (transition or CRUD convention) with parameters.</description>
<in-parameters>
<parameter name="path" required="true"/>
<parameter name="action" required="true"/>
<parameter name="parameters" type="Map"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
ExecutionContext ec = context.ec
def actionResult = null
def actionError = null
def actionParams = parameters ?: [:]
try {
// First, resolve the screen path
def resolveResult = ec.service.sync().name("McpServices.mcp#ResolveScreenPath")
.parameter("path", path).call()
def screenPath = resolveResult.screenPath
def screenDef = resolveResult.screenDef
if (!screenDef) {
actionError = "Could not resolve screen for path '${path}'"
result = [actionResult: actionResult, actionError: actionError]
return
}
if (action == "submit") {
// Special handling for form submit - just acknowledge receipt
actionResult = [
action: "submit",
status: "success",
message: "Form parameters submitted",
parametersProcessed: actionParams.keySet()
]
} else {
// Check if action matches CRUD convention for ProductPrice
def actionPrefix = action?.take(6)
if (actionPrefix && actionPrefix in ['update', 'create', 'delete']) {
// Convention: updateProductPrice -> update#mantle.product.ProductPrice
def serviceName = "${actionPrefix}#mantle.product.ProductPrice"
ec.logger.info("ExecuteScreenAction: Calling service by convention: ${serviceName}")
try {
def serviceCallResult = ec.service.sync().name(serviceName).parameters(actionParams).call()
actionResult = [
action: action,
status: "executed",
message: "Executed service ${serviceName}",
result: serviceCallResult
]
} catch (Exception e) {
actionError = "Service call failed: ${e.message}"
}
} else {
// Look for matching transition on screen
def foundTransition = null
def allTransitions = screenDef.getAllTransitions()
if (allTransitions) {
for (def transition : allTransitions) {
if (transition.getName() == action) {
foundTransition = transition
break
}
}
}
if (foundTransition) {
// Try to find and execute the transition's service
def serviceName = null
if (foundTransition.xmlTransition) {
def serviceCallNode = foundTransition.xmlTransition.first("service-call")
if (serviceCallNode) serviceName = serviceCallNode.attribute("name")
}
if (serviceName) {
ec.logger.info("ExecuteScreenAction: Executing transition '${action}' service: ${serviceName}")
try {
def serviceCallResult = ec.service.sync().name(serviceName).parameters(actionParams).call()
actionResult = [
action: action,
status: "executed",
message: "Executed service ${serviceName}",
result: serviceCallResult
]
} catch (Exception e) {
actionError = "Service call failed: ${e.message}"
}
} else {
actionResult = [
action: action,
status: "success",
message: "Transition '${action}' ready (no direct service)"
]
}
} else {
actionError = "Transition '${action}' not found on screen"
}
}
}
} catch (Exception e) {
actionError = "Action execution failed: ${e.message}"
ec.logger.warn("ExecuteScreenAction error: ${e.message}")
}
result = [actionResult: actionResult, actionError: actionError]
]]></script>
</actions>
</service>
</services>
<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, author(s) have dedicated all
copyright and related and neighboring rights to this software to the
public domain worldwide. This software is distributed without any
warranty.
-->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-3.xsd">
<service verb="mcp" noun="RenderScreenNarrative" authenticate="true" allow-remote="true" transaction-timeout="120">
<description>Render a screen with semantic state extraction and UI narrative generation.</description>
<in-parameters>
<parameter name="path" required="true"/>
<parameter name="parameters" type="Map"/>
<parameter name="renderMode" default="mcp"/>
<parameter name="sessionId"/>
<parameter name="terse" type="Boolean" default="false"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
ExecutionContext ec = context.ec
def renderedContent = null
def semanticState = null
def uiNarrative = null
// Resolve screen path
def resolveResult = ec.service.sync().name("McpServices.mcp#ResolveScreenPath")
.parameter("path", path).call()
def screenPath = resolveResult.screenPath
def subscreenName = resolveResult.subscreenName
if (!screenPath) {
result = [renderedContent: null, semanticState: null, uiNarrative: null]
return
}
// Build render parameters
def screenCallParams = [
path: screenPath,
parameters: parameters ?: [:],
renderMode: renderMode ?: "mcp",
sessionId: sessionId,
terse: terse == true
]
if (subscreenName) screenCallParams.subscreenName = subscreenName
// Render screen using ScreenAsMcpTool
try {
def serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
.parameters(screenCallParams).call()
// Extract semantic state from rendered result
if (serviceResult) {
def resultObj = null
if (serviceResult.containsKey('content') && serviceResult.content && serviceResult.content.size() > 0) {
def rawText = serviceResult.content[0].text
if (rawText && rawText.startsWith("{")) {
try { resultObj = new groovy.json.JsonSlurper().parseText(rawText) } catch(e) {}
}
renderedContent = rawText
} else if (serviceResult.containsKey('result') && serviceResult.result && serviceResult.result.content && serviceResult.result.content.size() > 0) {
def rawText = serviceResult.result.content[0].text
if (rawText && rawText.startsWith("{")) {
try { resultObj = new groovy.json.JsonSlurper().parseText(rawText) } catch(e) {}
}
renderedContent = rawText
}
// Generate UI narrative if we have semantic state
if (resultObj && resultObj.semanticState) {
semanticState = resultObj.semanticState
try {
def narrativeBuilder = new org.moqui.mcp.UiNarrativeBuilder()
def screenDefForNarrative = ec.screen.getScreenDefinition(screenPath)
uiNarrative = narrativeBuilder.buildNarrative(
screenDefForNarrative,
semanticState,
path,
terse == true
)
ec.logger.info("RenderScreenNarrative: Generated UI narrative for ${path}")
} catch (Exception e) {
ec.logger.warn("RenderScreenNarrative: Failed to generate UI narrative: ${e.message}")
}
// Truncate content if we have UI narrative to save tokens
if (renderedContent && renderedContent.length() > 500) {
renderedContent = renderedContent.take(500) + "... (truncated, see uiNarrative for actions)"
}
}
}
} catch (Exception e) {
ec.logger.warn("RenderScreenNarrative: Error rendering screen ${path}: ${e.message}")
renderedContent = "RENDER_ERROR: ${e.message}"
}
result = [
renderedContent: renderedContent,
semanticState: semanticState,
uiNarrative: uiNarrative
]
]]></script>
</actions>
</service>
</services>
<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, author(s) have dedicated all
copyright and related and neighboring rights to this software to the
public domain worldwide. This software is distributed without any
warranty.
-->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-3.xsd">
<service verb="mcp" noun="ResolveScreenPath" authenticate="false" allow-remote="true" transaction-timeout="30">
<description>Resolve a simple screen path to component path, subscreen name, and screen definition.</description>
<in-parameters>
<parameter name="path" required="true"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
ExecutionContext ec = context.ec
def resolvedPath = null
def subscreenName = null
def screenDef = null
if (!path || path == "root") {
return [
screenPath: null,
subscreenName: null,
screenDef: null
]
}
// Support both dot and slash notation
def pathParts = path.contains('/')
? path.split('/')
: path.split('\\.')
def componentName = pathParts[0]
// Use longest prefix match to find actual screen file
for (int i = pathParts.size(); i >= 1; i--) {
def subPath = i > 1 ? pathParts[0] + "/" + (pathParts[1..<i].join('/')) : pathParts[0]
def currentTry = "component://${componentName}/screen/${subPath}.xml"
if (ec.resource.getLocationReference(currentTry).getExists()) {
resolvedPath = currentTry
// If we found a screen matching the full path, we're already at the target
if (i < pathParts.size()) {
def remainingParts = pathParts[i..-1]
subscreenName = remainingParts.size() > 1 ? remainingParts.join('_') : remainingParts[0]
}
break
}
}
// Fallback to component root screen if no match found
if (!resolvedPath) {
resolvedPath = "component://${componentName}/screen/${componentName}.xml"
if (pathParts.size() > 1) {
subscreenName = pathParts[1..-1].join('_')
}
}
// Navigate to subscreen if needed
if (resolvedPath) {
try {
screenDef = ec.screen.getScreenDefinition(resolvedPath)
if (subscreenName && screenDef) {
for (subName in subscreenName.split('_')) {
def subItem = screenDef?.getSubscreensItem(subName)
if (subItem && subItem.getLocation()) {
screenDef = ec.screen.getScreenDefinition(subItem.getLocation())
} else {
break
}
}
}
} catch (Exception e) {
ec.logger.warn("ResolveScreenPath: Error loading screen definition: ${e.message}")
}
}
result = [
screenPath: resolvedPath,
subscreenName: subscreenName,
screenDef: screenDef
]
]]></script>
</actions>
</service>
</services>
......@@ -93,13 +93,26 @@ class CustomScreenTestImpl implements McpScreenTest {
return this
}
protected static List<String> pathToList(String path) {
List<String> pathList = new ArrayList<>()
if (path && path.contains('/')) {
String[] pathSegments = path.split('/')
for (String segment in pathSegments) {
if (segment && segment.trim().length() > 0) {
pathList.add(segment)
}
}
}
return pathList
}
@Override
McpScreenTest baseScreenPath(String screenPath) {
if (!rootScreenLocation) throw new BaseArtifactException("No rootScreen specified")
baseScreenPath = screenPath
if (baseScreenPath.endsWith("/")) baseScreenPath = baseScreenPath.substring(0, baseScreenPath.length() - 1)
if (baseScreenPath) {
baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, [], baseScreenPath, null, sfi)
baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, pathToList(baseScreenPath), baseScreenPath, [:], sfi)
if (baseScreenPathList == null) throw new BaseArtifactException("Error in baseScreenPath, could find not base screen path ${baseScreenPath} under ${rootScreenDef.location}")
for (String screenName in baseScreenPathList) {
ScreenDefinition.SubscreensItem ssi = baseScreenDef.getSubscreensItem(screenName)
......@@ -282,10 +295,24 @@ class CustomScreenTestImpl implements McpScreenTest {
}
}
logger.info("Custom screen path parsing for non-webroot root: ${screenPathList}")
} else {
screenPathList = ScreenUrlInfo.parseSubScreenPath(csti.rootScreenDef, csti.baseScreenDef,
csti.baseScreenPathList, stri.screenPath, stri.parameters, csti.sfi)
}
} else {
// For webroot or other cases, use ScreenUrlInfo.parseSubScreenPath for resolution
// Convert screenPath to list for parseSubScreenPath
List<String> inputPathList = new ArrayList<>()
if (stri.screenPath && stri.screenPath.contains('/')) {
String[] pathSegments = stri.screenPath.split('/')
for (String segment in pathSegments) {
if (segment && segment.trim().length() > 0) {
inputPathList.add(segment)
}
}
}
// Use Moqui's parseSubScreenPath to resolve actual screen path
// Note: pass null for fromPathList since stri.screenPath is already relative to root or from screen
screenPathList = ScreenUrlInfo.parseSubScreenPath(csti.rootScreenDef, csti.baseScreenDef,
null, stri.screenPath, stri.parameters, csti.sfi)
}
if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${csti.baseScreenDef.location}")
// push the context
......