Add UI narrative for LLM action guidance and unify render modes
- Create UiNarrativeBuilder class for structured, story-like UI descriptions - screen: 50-80 words describing current state - actions: 80-120 words with exact tool invocation examples - navigation: 30-40 words for navigating to related screens - notes: 30-50 words for truncation, permissions, constraints - Enhance mcp.ftl macros to capture semantic metadata - form-single: Track field names, types, validation rules - form-list: Capture totalItems, displayedItems, truncated flags - Store metadata in mcpSemanticData for narrative generation - Deprecate DefaultScreenMacros.json.ftl - Update MoquiConf.xml to map both mcp and json to mcp.ftl - Remove redundant 218-line template with no semantic capture - Integrate uiNarrative into BrowseScreens service - Generate narrative using UiNarrativeBuilder - Include uiNarrative in result map - Provide screenDef for context - Remove redundant semantic state extraction - Delete fallback logic that extracted forms/lists from screen definition - Rely exclusively on mcpSemanticData captured by macros - Improve smart truncation in serializeMoquiObject - terse mode: 10 items, 200 char strings - non-terse mode: 50 items, no string truncation (fixed bug) - Add _hasMore flag to truncated data metadata - Fix CustomScreenTestImpl postRenderContext capture - Capture context before pop to preserve mcpSemanticData
Showing
9 changed files
with
76 additions
and
224 deletions
No preview for this file type
| ... | @@ -2,7 +2,7 @@ arguments=--init-script /home/ean/.local/share/opencode/bin/jdtls/config_linux/o | ... | @@ -2,7 +2,7 @@ arguments=--init-script /home/ean/.local/share/opencode/bin/jdtls/config_linux/o |
| 2 | auto.sync=false | 2 | auto.sync=false |
| 3 | build.scans.enabled=false | 3 | build.scans.enabled=false |
| 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9)) | 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9)) |
| 5 | connection.project.dir= | 5 | connection.project.dir=../../../framework |
| 6 | eclipse.preferences.version=1 | 6 | eclipse.preferences.version=1 |
| 7 | gradle.user.home= | 7 | gradle.user.home= |
| 8 | java.home=/usr/lib/jvm/java-21-openjdk-amd64 | 8 | java.home=/usr/lib/jvm/java-21-openjdk-amd64 | ... | ... |
| ... | @@ -28,10 +28,11 @@ | ... | @@ -28,10 +28,11 @@ |
| 28 | </webapp-list> | 28 | </webapp-list> |
| 29 | 29 | ||
| 30 | <screen-facade> | 30 | <screen-facade> |
| 31 | <!-- DEPRECATED: json mode now uses mcp template for unified semantic capture --> | ||
| 31 | <screen-text-output type="mcp" mime-type="text/markdown" always-standalone="true" | 32 | <screen-text-output type="mcp" mime-type="text/markdown" always-standalone="true" |
| 32 | macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.mcp.ftl"/> | 33 | macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.mcp.ftl"/> |
| 33 | <screen-text-output type="json" mime-type="application/json" always-standalone="true" | 34 | <screen-text-output type="json" mime-type="application/json" always-standalone="true" |
| 34 | macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.json.ftl"/> | 35 | macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.mcp.ftl"/> |
| 35 | <widget-render-mode type="mcp" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/> | 36 | <widget-render-mode type="mcp" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/> |
| 36 | <widget-render-mode type="json" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/> | 37 | <widget-render-mode type="json" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/> |
| 37 | </screen-facade> | 38 | </screen-facade> | ... | ... |
| 1 | <#-- | ||
| 2 | Moqui MCP JSON Macros | ||
| 3 | Renders screens in structured JSON format for LLM consumption. | ||
| 4 | --> | ||
| 5 | |||
| 6 | <#include "DefaultScreenMacros.any.ftl"/> | ||
| 7 | |||
| 8 | <#macro @element></#macro> | ||
| 9 | |||
| 10 | <#macro screen>{"screen": {<#recurse>}}</#macro> | ||
| 11 | |||
| 12 | <#macro widgets> | ||
| 13 | "widgets": [<#recurse>] | ||
| 14 | </#macro> | ||
| 15 | |||
| 16 | <#macro "fail-widgets"><#recurse></#macro> | ||
| 17 | |||
| 18 | <#-- ================ Subscreens ================ --> | ||
| 19 | <#macro "subscreens-menu"></#macro> | ||
| 20 | <#macro "subscreens-active">{"type": "subscreens-active", "content": ${sri.renderSubscreen()}}</#macro> | ||
| 21 | <#macro "subscreens-panel">{"type": "subscreens-panel", "content": ${sri.renderSubscreen()}}</#macro> | ||
| 22 | |||
| 23 | <#-- ================ Section ================ --> | ||
| 24 | <#macro section>{"type": "section", "name": ${(.node["@name"]!"")?json_string}, "content": ${sri.renderSection(.node["@name"])}}</#macro> | ||
| 25 | <#macro "section-iterate">${sri.renderSection(.node["@name"])}</#macro> | ||
| 26 | <#macro "section-include">${sri.renderSectionInclude(.node)}</#macro> | ||
| 27 | |||
| 28 | <#-- ================ Containers ================ --> | ||
| 29 | <#macro container> | ||
| 30 | <#assign children = []> | ||
| 31 | <#list .node?children as child> | ||
| 32 | <#assign rendered><#recurse child></#assign> | ||
| 33 | <#if rendered?has_content && !(rendered?starts_with("{\"widgets\""))> | ||
| 34 | <#assign children = children + [rendered]> | ||
| 35 | </#if> | ||
| 36 | </#list> | ||
| 37 | {"type": "container", "children": [${children?join(",")}]} | ||
| 38 | </#macro> | ||
| 39 | |||
| 40 | <#macro "container-box"> | ||
| 41 | {"type": "container-box"<#if .node["box-header"]?has_content>, "header": ${.node["box-header"][0]["@label"]!?json_string}</#if><#if .node["box-body"]?has_content>, "body": ${.node["box-body"][0]["@label"]!?json_string}</#if>} | ||
| 42 | </#macro> | ||
| 43 | |||
| 44 | <#macro "container-row"><#list .node["row-col"] as rowColNode><#recurse rowColNode></#list></#macro> | ||
| 45 | |||
| 46 | <#macro "container-panel"> | ||
| 47 | {"type": "container-panel"<#if .node["panel-header"]?has_content>, "header": ${.node["panel-header"][0]["@label"]!?json_string}</#if>} | ||
| 48 | </#macro> | ||
| 49 | |||
| 50 | <#macro "container-dialog"> | ||
| 51 | {"type": "container-dialog", "buttonText": ${ec.resource.expand(.node["@button-text"], "")?json_string}} | ||
| 52 | </#macro> | ||
| 53 | |||
| 54 | <#-- ================== Standalone Fields ==================== --> | ||
| 55 | <#macro link> | ||
| 56 | <#assign linkNode = .node> | ||
| 57 | <#if linkNode["@condition"]?has_content><#assign conditionResult = ec.getResource().condition(linkNode["@condition"], "")><#else><#assign conditionResult = true></#if> | ||
| 58 | <#if conditionResult> | ||
| 59 | <#assign urlInstance = sri.makeUrlByType(linkNode["@url"]!"", linkNode["@url-type"]!"transition", linkNode, "true")> | ||
| 60 | <#assign linkText = ""> | ||
| 61 | <#if linkNode["@text"]?has_content> | ||
| 62 | <#assign linkText = ec.getResource().expand(linkNode["@text"], "")> | ||
| 63 | <#elseif linkNode["@entity-name"]?has_content> | ||
| 64 | <#assign linkText = sri.getFieldEntityValue(linkNode)!""?string> | ||
| 65 | </#if> | ||
| 66 | <#if !(linkText?has_content) && .node?parent?node_name?ends_with("-field")> | ||
| 67 | <#assign linkText = sri.getFieldValueString(.node?parent?parent)!> | ||
| 68 | </#if> | ||
| 69 | |||
| 70 | <#-- Convert path to dot notation for moqui_render_screen --> | ||
| 71 | <#assign fullPath = urlInstance.sui.fullPathNameList![]> | ||
| 72 | <#assign dotPath = ""> | ||
| 73 | <#list fullPath as pathPart><#assign dotPath = dotPath + (dotPath?has_content)?then(".", "") + pathPart></#list> | ||
| 74 | |||
| 75 | <#assign paramStr = urlInstance.getParameterString()!""> | ||
| 76 | <#if paramStr?has_content><#assign dotPath = dotPath + "?" + paramStr></#if> | ||
| 77 | |||
| 78 | {"type": "link", "text": ${linkText?json_string}, "path": ${dotPath?json_string}} | ||
| 79 | </#if> | ||
| 80 | </#macro> | ||
| 81 | |||
| 82 | <#macro image>{"type": "image", "alt": ${(.node["@alt"]!"")?json_string}, "url": ${(.node["@url"]!"")?json_string}}</#macro> | ||
| 83 | |||
| 84 | <#macro label> | ||
| 85 | <#assign text = ec.resource.expand(.node["@text"], "")> | ||
| 86 | <#assign type = .node["@type"]!"span"> | ||
| 87 | {"type": "label", "text": ${text?json_string}, "labelType": ${type?json_string}} | ||
| 88 | </#macro> | ||
| 89 | |||
| 90 | <#-- ======================= Form ========================= --> | ||
| 91 | <#macro "form-single"> | ||
| 92 | <#assign formNode = sri.getFormNode(.node["@name"])> | ||
| 93 | <#assign mapName = formNode["@map"]!"fieldValues"> | ||
| 94 | |||
| 95 | <#assign fields = []> | ||
| 96 | <#t>${sri.pushSingleFormMapContext(mapName)} | ||
| 97 | <#list formNode["field"] as fieldNode> | ||
| 98 | <#assign fieldSubNode = ""> | ||
| 99 | <#list fieldNode["conditional-field"] as csf><#if ec.resource.condition(csf["@condition"], "")><#assign fieldSubNode = csf><#break></#if></#list> | ||
| 100 | <#if !(fieldSubNode?has_content)><#assign fieldSubNode = fieldNode["default-field"][0]!></#if> | ||
| 101 | <#if fieldSubNode?has_content && !(fieldSubNode["ignored"]?has_content) && !(fieldSubNode["hidden"]?has_content) && !(fieldSubNode["submit"]?has_content) && fieldSubNode?parent["@hide"]! != "true"> | ||
| 102 | <#assign fieldValue = ec.context.get(fieldSubNode?parent["@name"])!""> | ||
| 103 | <#if fieldValue?has_content> | ||
| 104 | <#assign fieldInfo = {"name": (fieldSubNode?parent["@name"]!"")?json_string, "value": (fieldValue!?json_string)}> | ||
| 105 | <#assign fields = fields + [fieldInfo]> | ||
| 106 | </#if> | ||
| 107 | </#if> | ||
| 108 | </#list> | ||
| 109 | <#t>${sri.popContext()} | ||
| 110 | {"type": "form-single", "name": ${formNode["@name"]?json_string}, "map": ${mapName?json_string}, "fields": [${fields?join(",")}]} | ||
| 111 | </#macro> | ||
| 112 | |||
| 113 | <#macro "form-list"> | ||
| 114 | <#assign formInstance = sri.getFormInstance(.node["@name"])> | ||
| 115 | <#assign formListInfo = formInstance.makeFormListRenderInfo()> | ||
| 116 | <#assign formNode = formListInfo.getFormNode()> | ||
| 117 | <#assign formListColumnList = formListInfo.getAllColInfo()> | ||
| 118 | <#assign listObject = formListInfo.getListObject(false)!> | ||
| 119 | |||
| 120 | {"type": "form-list", "name": ${.node["@name"]?json_string}} | ||
| 121 | </#macro> | ||
| 122 | |||
| 123 | <#macro formListSubField fieldNode> | ||
| 124 | <#list fieldNode["conditional-field"] as fieldSubNode> | ||
| 125 | <#if ec.resource.condition(fieldSubNode["@condition"], "")> | ||
| 126 | {"type": "field", "name": ${fieldSubNode["@name"]?json_string}} | ||
| 127 | <#return> | ||
| 128 | </#if> | ||
| 129 | </#list> | ||
| 130 | </#macro> | ||
| 131 | |||
| 132 | <#macro formListWidget fieldSubNode> | ||
| 133 | <#if fieldSubNode["ignored"]?has_content || fieldSubNode["hidden"]?has_content || fieldSubNode?parent["@hide"]! == "true"><#return></#if> | ||
| 134 | <#if fieldSubNode["submit"]?has_content> | ||
| 135 | <#assign submitText = sri.getFieldValueString(fieldSubNode)!""?json_string> | ||
| 136 | <#assign screenName = sri.getEffectiveScreen().name!""?string> | ||
| 137 | <#assign formNodeObj = sri.getFormNode(.node["@name"])!""> | ||
| 138 | <#assign formName = formNodeObj["@name"]!?string> | ||
| 139 | <#assign fieldName = fieldSubNode["@name"]!""?string> | ||
| 140 | {"type": "submit", "text": ${submitText}, "action": "${screenName}.${formName}.${fieldName}"} | ||
| 141 | </#if> | ||
| 142 | <#recurse fieldSubNode> | ||
| 143 | </#macro> | ||
| 144 | |||
| 145 | <#macro fieldTitle fieldSubNode> | ||
| 146 | <#assign titleValue><#if fieldSubNode["@title"]?has_content>${fieldSubNode["@title"]}<#else><#list fieldSubNode?parent["@name"]?split("(?=[A-Z])", "r") as nameWord>${nameWord?cap_first?replace("Id", "ID")}<#if nameWord_has_next> </#if></#list></#if></#assign> | ||
| 147 | ${ec.l10n.localize(titleValue)?json_string} | ||
| 148 | </#macro> | ||
| 149 | |||
| 150 | <#-- ================== Form Field Widgets ==================== --> | ||
| 151 | <#macro "check"> | ||
| 152 | <#assign options = sri.getFieldOptions(.node)!> | ||
| 153 | <#assign currentValue = sri.getFieldValueString(.node)!""> | ||
| 154 | {"type": "check", "value": ${(options.get(currentValue)!currentValue)?json_string}} | ||
| 155 | </#macro> | ||
| 156 | |||
| 157 | <#macro "date-find"></#macro> | ||
| 158 | |||
| 159 | <#macro "date-time"> | ||
| 160 | <#assign javaFormat = .node["@format"]!""> | ||
| 161 | <#if !(javaFormat?has_content)> | ||
| 162 | <#if .node["@type"]! == "time"><#assign javaFormat="HH:mm"> | ||
| 163 | <#elseif .node["@type"]! == "date"><#assign javaFormat="yyyy-MM-dd"> | ||
| 164 | <#else><#assign javaFormat="yyyy-MM-dd HH:mm"></#if> | ||
| 165 | </#if> | ||
| 166 | <#assign fieldValue = sri.getFieldValueString(.node?parent?parent, .node["@default-value"]!"", javaFormat)!""> | ||
| 167 | {"type": "date-time", "name": ${(.node["@name"]!"")?json_string}, "format": ${javaFormat?json_string}, "value": ${fieldValue?json_string!"null"}} | ||
| 168 | </#macro> | ||
| 169 | |||
| 170 | <#macro "display"> | ||
| 171 | <#assign fieldValue = ""> | ||
| 172 | <#assign dispFieldNode = .node?parent?parent> | ||
| 173 | <#if .node["@text"]?has_content> | ||
| 174 | <#assign textMap = {}> | ||
| 175 | <#if .node["@text-map"]?has_content><#assign textMap = ec.getResource().expression(.node["@text-map"], {})!></#if> | ||
| 176 | <#assign fieldValue = ec.getResource().expand(.node["@text"], "", textMap, false)!> | ||
| 177 | <#if .node["@currency-unit-field"]?has_content> | ||
| 178 | <#assign fieldValue = ec.getL10n().formatCurrency(fieldValue, ec.getResource().expression(.node["@currency-unit-field"], ""))!""> | ||
| 179 | </#if> | ||
| 180 | <#else> | ||
| 181 | <#assign fieldValue = sri.getFieldValueString(.node)!""> | ||
| 182 | </#if> | ||
| 183 | {"type": "display", "value": ${fieldValue?json_string}} | ||
| 184 | </#macro> | ||
| 185 | |||
| 186 | <#macro "display-entity"> | ||
| 187 | <#assign entityValue = sri.getFieldEntityValue(.node)!""> | ||
| 188 | {"type": "display-entity", "value": ${entityValue?json_string}} | ||
| 189 | </#macro> | ||
| 190 | |||
| 191 | <#macro "drop-down"> | ||
| 192 | <#assign options = sri.getFieldOptions(.node)!> | ||
| 193 | <#assign currentValue = sri.getFieldValueString(.node)!""> | ||
| 194 | {"type": "drop-down", "value": ${(options.get(currentValue)!currentValue)?json_string}} | ||
| 195 | </#macro> | ||
| 196 | |||
| 197 | <#macro "text-area"> | ||
| 198 | <#assign fieldValue = sri.getFieldValueString(.node)!""> | ||
| 199 | {"type": "text-area", "value": ${fieldValue?json_string}} | ||
| 200 | </#macro> | ||
| 201 | |||
| 202 | <#macro "text-line"> | ||
| 203 | <#assign fieldValue = sri.getFieldValueString(.node)!""> | ||
| 204 | {"type": "text-line", "value": ${fieldValue?json_string}} | ||
| 205 | </#macro> | ||
| 206 | |||
| 207 | <#macro "text-find"> | ||
| 208 | <#assign fieldValue = sri.getFieldValueString(.node)!""> | ||
| 209 | {"type": "text-find", "value": ${fieldValue?json_string}} | ||
| 210 | </#macro> | ||
| 211 | |||
| 212 | <#macro "submit"> | ||
| 213 | <#assign text = ec.resource.expand(.node["@text"], "")!""> | ||
| 214 | {"type": "submit", "text": ${text?json_string}} | ||
| 215 | </#macro> | ||
| 216 | |||
| 217 | <#macro "password"></#macro> | ||
| 218 | <#macro "hidden"></#macro> |
| ... | @@ -75,6 +75,11 @@ | ... | @@ -75,6 +75,11 @@ |
| 75 | <#if paramStr?has_content><#assign dotPath = dotPath + "?" + paramStr></#if> | 75 | <#if paramStr?has_content><#assign dotPath = dotPath + "?" + paramStr></#if> |
| 76 | 76 | ||
| 77 | [${linkText}](${dotPath})<#t> | 77 | [${linkText}](${dotPath})<#t> |
| 78 | <#if mcpSemanticData??> | ||
| 79 | <#if !mcpSemanticData.links??><#assign dummy = mcpSemanticData.put("links", [])></#if> | ||
| 80 | <#assign linkInfo = {"text": linkText, "path": dotPath, "type": "navigation"}> | ||
| 81 | <#assign dummy = mcpSemanticData.links.add(linkInfo)> | ||
| 82 | </#if> | ||
| 78 | </#if> | 83 | </#if> |
| 79 | </#macro> | 84 | </#macro> |
| 80 | 85 | ||
| ... | @@ -94,6 +99,19 @@ | ... | @@ -94,6 +99,19 @@ |
| 94 | <#macro "form-single"> | 99 | <#macro "form-single"> |
| 95 | <#assign formNode = sri.getFormNode(.node["@name"])> | 100 | <#assign formNode = sri.getFormNode(.node["@name"])> |
| 96 | <#assign mapName = formNode["@map"]!"fieldValues"> | 101 | <#assign mapName = formNode["@map"]!"fieldValues"> |
| 102 | <#assign formMap = ec.resource.expression(mapName, "")!> | ||
| 103 | |||
| 104 | <#if mcpSemanticData??> | ||
| 105 | <#if !mcpSemanticData.formMetadata??><#assign dummy = mcpSemanticData.put("formMetadata", {})</#if> | ||
| 106 | |||
| 107 | <#assign formMeta = {}> | ||
| 108 | <#assign formMeta = formMeta + {"name": .node["@name"]!"", "map": mapName}> | ||
| 109 | <#assign fieldMetaList = []> | ||
| 110 | |||
| 111 | <#assign dummy = mcpSemanticData.formMeta.put(.node["@name"], formMeta)> | ||
| 112 | </#if> | ||
| 113 | |||
| 114 | <#if mcpSemanticData?? && formMap?has_content><#assign dummy = mcpSemanticData.put(.node["@name"], formMap)></#if> | ||
| 97 | <#t>${sri.pushSingleFormMapContext(mapName)} | 115 | <#t>${sri.pushSingleFormMapContext(mapName)} |
| 98 | <#list formNode["field"] as fieldNode> | 116 | <#list formNode["field"] as fieldNode> |
| 99 | <#assign fieldSubNode = ""> | 117 | <#assign fieldSubNode = ""> |
| ... | @@ -101,9 +119,28 @@ | ... | @@ -101,9 +119,28 @@ |
| 101 | <#if !fieldSubNode?has_content><#assign fieldSubNode = fieldNode["default-field"][0]!></#if> | 119 | <#if !fieldSubNode?has_content><#assign fieldSubNode = fieldNode["default-field"][0]!></#if> |
| 102 | <#if fieldSubNode?has_content && !fieldSubNode["ignored"]?has_content && !fieldSubNode["hidden"]?has_content && !fieldSubNode["submit"]?has_content && fieldSubNode?parent["@hide"]! != "true"> | 120 | <#if fieldSubNode?has_content && !fieldSubNode["ignored"]?has_content && !fieldSubNode["hidden"]?has_content && !fieldSubNode["submit"]?has_content && fieldSubNode?parent["@hide"]! != "true"> |
| 103 | <#assign title><@fieldTitle fieldSubNode/></#assign> | 121 | <#assign title><@fieldTitle fieldSubNode/></#assign> |
| 122 | |||
| 123 | <#if mcpSemanticData??> | ||
| 124 | <#assign fieldMeta = {}> | ||
| 125 | <#assign fieldMeta = fieldMeta + {"name": fieldNode["@name"]!"", "title": title!"", "required": (fieldNode["@required"]! == "true")}> | ||
| 126 | |||
| 127 | <#if fieldSubNode["text-line"]?has_content><#assign dummy = fieldMeta.put("type", "text")></#if> | ||
| 128 | <#if fieldSubNode["text-area"]?has_content><#assign dummy = fieldMeta.put("type", "textarea")></#if> | ||
| 129 | <#if fieldSubNode["drop-down"]?has_content><#assign dummy = fieldMeta.put("type", "dropdown")></#if> | ||
| 130 | <#if fieldSubNode["check"]?has_content><#assign dummy = fieldMeta.put("type", "checkbox")></#if> | ||
| 131 | <#if fieldSubNode["date-find"]?has_content><#assign dummy = fieldMeta.put("type", "date")></#if> | ||
| 132 | |||
| 133 | <#assign dummy = fieldMetaList.add(fieldMeta)> | ||
| 134 | </#if> | ||
| 135 | |||
| 104 | * **${title}**: <#recurse fieldSubNode> | 136 | * **${title}**: <#recurse fieldSubNode> |
| 105 | </#if> | 137 | </#if> |
| 106 | </#list> | 138 | </#list> |
| 139 | |||
| 140 | <#if mcpSemanticData?? && fieldMetaList?has_content> | ||
| 141 | <#assign dummy = mcpSemanticData.formMeta[.node["@name"]!].put("fields", fieldMetaList)> | ||
| 142 | </#if> | ||
| 143 | |||
| 107 | <#t>${sri.popContext()} | 144 | <#t>${sri.popContext()} |
| 108 | </#macro> | 145 | </#macro> |
| 109 | 146 | ||
| ... | @@ -113,6 +150,31 @@ | ... | @@ -113,6 +150,31 @@ |
| 113 | <#assign formNode = formListInfo.getFormNode()> | 150 | <#assign formNode = formListInfo.getFormNode()> |
| 114 | <#assign formListColumnList = formListInfo.getAllColInfo()> | 151 | <#assign formListColumnList = formListInfo.getAllColInfo()> |
| 115 | <#assign listObject = formListInfo.getListObject(false)!> | 152 | <#assign listObject = formListInfo.getListObject(false)!> |
| 153 | <#assign totalItems = listObject?size> | ||
| 154 | |||
| 155 | <#if mcpSemanticData?? && listObject?has_content> | ||
| 156 | <#assign truncatedList = listObject> | ||
| 157 | <#if listObject?size > 50> | ||
| 158 | <#assign truncatedList = listObject?take(50)> | ||
| 159 | </#if> | ||
| 160 | <#assign dummy = mcpSemanticData.put(.node["@name"], truncatedList)> | ||
| 161 | |||
| 162 | <#if !mcpSemanticData.listMetadata??><#assign dummy = mcpSemanticData.put("listMetadata", {})</#if> | ||
| 163 | |||
| 164 | <#assign columnNames = []> | ||
| 165 | <#list formListColumnList as columnFieldList> | ||
| 166 | <#assign fieldNode = columnFieldList[0]> | ||
| 167 | <#assign dummy = columnNames.add(fieldNode["@name"]!"")> | ||
| 168 | </#list> | ||
| 169 | |||
| 170 | <#assign dummy = mcpSemanticData.listMeta.put(.node["@name"]!"", { | ||
| 171 | "name": .node["@name"]!"", | ||
| 172 | "totalItems": totalItems, | ||
| 173 | "displayedItems": truncatedList?size, | ||
| 174 | "truncated": (listObject?size > 50), | ||
| 175 | "columns": columnNames | ||
| 176 | })> | ||
| 177 | </#if> | ||
| 116 | 178 | ||
| 117 | <#-- Header Row --> | 179 | <#-- Header Row --> |
| 118 | <#list formListColumnList as columnFieldList> | 180 | <#list formListColumnList as columnFieldList> |
| ... | @@ -123,7 +185,7 @@ | ... | @@ -123,7 +185,7 @@ |
| 123 | | | 185 | | |
| 124 | <#list formListColumnList as columnFieldList>| --- </#list>| | 186 | <#list formListColumnList as columnFieldList>| --- </#list>| |
| 125 | <#-- Data Rows --> | 187 | <#-- Data Rows --> |
| 126 | <#list listObject as listEntry> | 188 | <#list (truncatedList?? && truncatedList?size > 0)!listObject as listEntry> |
| 127 | <#t>${sri.startFormListRow(formListInfo, listEntry, listEntry_index, listEntry_has_next)} | 189 | <#t>${sri.startFormListRow(formListInfo, listEntry, listEntry_index, listEntry_has_next)} |
| 128 | <#list formListColumnList as columnFieldList> | 190 | <#list formListColumnList as columnFieldList> |
| 129 | <#t>| <#list columnFieldList as fieldNode><@formListSubField fieldNode/><#if fieldNode_has_next> </#if></#list><#t> | 191 | <#t>| <#list columnFieldList as fieldNode><@formListSubField fieldNode/><#if fieldNode_has_next> </#if></#list><#t> | ... | ... |
This diff is collapsed.
Click to expand it.
| ... | @@ -291,6 +291,11 @@ class CustomScreenTestImpl implements McpScreenTest { | ... | @@ -291,6 +291,11 @@ class CustomScreenTestImpl implements McpScreenTest { |
| 291 | // push the context | 291 | // push the context |
| 292 | ContextStack cs = eci.getContext() | 292 | ContextStack cs = eci.getContext() |
| 293 | cs.push() | 293 | cs.push() |
| 294 | |||
| 295 | // Create a persistent map for semantic data that survives nested pops | ||
| 296 | Map<String, Object> mcpSemanticData = new HashMap<>() | ||
| 297 | cs.put("mcpSemanticData", mcpSemanticData) | ||
| 298 | |||
| 294 | // create the WebFacadeStub using our custom method | 299 | // create the WebFacadeStub using our custom method |
| 295 | org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod, stri.screenPath) | 300 | org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod, stri.screenPath) |
| 296 | // set stub on eci, will also put parameters in the context | 301 | // set stub on eci, will also put parameters in the context |
| ... | @@ -336,8 +341,10 @@ class CustomScreenTestImpl implements McpScreenTest { | ... | @@ -336,8 +341,10 @@ class CustomScreenTestImpl implements McpScreenTest { |
| 336 | // calc renderTime | 341 | // calc renderTime |
| 337 | stri.renderTime = System.currentTimeMillis() - startTime | 342 | stri.renderTime = System.currentTimeMillis() - startTime |
| 338 | 343 | ||
| 344 | // capture everything currently in the context stack before popping | ||
| 345 | stri.postRenderContext = new HashMap<>(cs) | ||
| 339 | // pop the context stack, get rid of var space | 346 | // pop the context stack, get rid of var space |
| 340 | stri.postRenderContext = cs.pop() | 347 | cs.pop() |
| 341 | 348 | ||
| 342 | // check, pass through, error messages | 349 | // check, pass through, error messages |
| 343 | if (eci.message.hasError()) { | 350 | if (eci.message.hasError()) { | ... | ... |
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment