282f9ceb by Ean Schuessler

Fix FTL macro errors and stabilize semantic state extraction.

- Resolve NonHashException in macros by avoiding .add() on sequences.
- Fix 'Maps with null keys' JSON error by ensuring string keys.
- Stabilize form macros with null-safe checks.
- Update McpServices and CustomScreenTestImpl for better semantic data handling.
1 parent 87e0b6a4
...@@ -79,9 +79,7 @@ ...@@ -79,9 +79,7 @@
79 79
80 [${linkText}](${slashPath})<#t> 80 [${linkText}](${slashPath})<#t>
81 <#if mcpSemanticData??> 81 <#if mcpSemanticData??>
82 <#if !mcpSemanticData.links??><#assign dummy = mcpSemanticData.put("links", [])></#if> 82 <#assign dummy = ec.resource.expression("mcpSemanticData.links.add([text: '" + (linkText!"")?js_string + "', path: '" + (slashPath!"")?js_string + "', type: 'navigation'])", "")!>
83 <#assign linkInfo = {"text": linkText, "path": slashPath, "type": "navigation"}>
84 <#assign dummy = mcpSemanticData.links.add(linkInfo)>
85 </#if> 83 </#if>
86 </#if> 84 </#if>
87 </#macro> 85 </#macro>
...@@ -105,15 +103,10 @@ ...@@ -105,15 +103,10 @@
105 <#assign formMap = ec.resource.expression(mapName, "")!> 103 <#assign formMap = ec.resource.expression(mapName, "")!>
106 104
107 <#if mcpSemanticData??> 105 <#if mcpSemanticData??>
108 <#if !mcpSemanticData.formMetadata??><#assign dummy = mcpSemanticData.put("formMetadata", {})></#if> 106 <#assign formName = (.node["@name"]!"")?string>
109
110 <#assign formMeta = {"name": (.node["@name"]!""), "map": mapName}>
111 <#assign fieldMetaList = []> 107 <#assign fieldMetaList = []>
112 108 <#assign dummy = ec.resource.expression("if (mcpSemanticData.formMetadata == null) mcpSemanticData.formMetadata = [:]; mcpSemanticData.formMetadata.put('" + formName?js_string + "', [name: '" + formName?js_string + "', map: '" + (mapName!"")?js_string + "'])", "")!>
113 <#assign dummy = mcpSemanticData.formMetadata.put(.node["@name"], formMeta)>
114 </#if> 109 </#if>
115
116 <#if mcpSemanticData?? && formMap?has_content><#assign dummy = mcpSemanticData.put(.node["@name"], formMap)></#if>
117 <#t>${sri.pushSingleFormMapContext(mapName)} 110 <#t>${sri.pushSingleFormMapContext(mapName)}
118 <#list formNode["field"] as fieldNode> 111 <#list formNode["field"] as fieldNode>
119 <#assign fieldSubNode = ""> 112 <#assign fieldSubNode = "">
...@@ -125,13 +118,13 @@ ...@@ -125,13 +118,13 @@
125 <#if mcpSemanticData??> 118 <#if mcpSemanticData??>
126 <#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true")}> 119 <#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true")}>
127 120
128 <#if fieldSubNode["text-line"]?has_content><#assign dummy = fieldMeta.put("type", "text")></#if> 121 <#if fieldSubNode["text-line"]?has_content><#assign fieldMeta = fieldMeta + {"type": "text"}></#if>
129 <#if fieldSubNode["text-area"]?has_content><#assign dummy = fieldMeta.put("type", "textarea")></#if> 122 <#if fieldSubNode["text-area"]?has_content><#assign fieldMeta = fieldMeta + {"type": "textarea"}></#if>
130 <#if fieldSubNode["drop-down"]?has_content><#assign dummy = fieldMeta.put("type", "dropdown")></#if> 123 <#if fieldSubNode["drop-down"]?has_content><#assign fieldMeta = fieldMeta + {"type": "dropdown"}></#if>
131 <#if fieldSubNode["check"]?has_content><#assign dummy = fieldMeta.put("type", "checkbox")></#if> 124 <#if fieldSubNode["check"]?has_content><#assign fieldMeta = fieldMeta + {"type": "checkbox"}></#if>
132 <#if fieldSubNode["date-find"]?has_content><#assign dummy = fieldMeta.put("type", "date")></#if> 125 <#if fieldSubNode["date-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "date"}></#if>
133 126
134 <#assign dummy = fieldMetaList.add(fieldMeta)> 127 <#assign fieldMetaList = fieldMetaList + [fieldMeta]>
135 </#if> 128 </#if>
136 129
137 * **${title}**: <#recurse fieldSubNode> 130 * **${title}**: <#recurse fieldSubNode>
...@@ -139,7 +132,9 @@ ...@@ -139,7 +132,9 @@
139 </#list> 132 </#list>
140 133
141 <#if mcpSemanticData?? && fieldMetaList?has_content> 134 <#if mcpSemanticData?? && fieldMetaList?has_content>
142 <#assign dummy = mcpSemanticData.formMetadata[.node["@name"]!].put("fields", fieldMetaList)> 135 <#assign formName = (.node["@name"]!"")?string>
136 <#assign dummy = ec.context.put("tempFieldMetaList", fieldMetaList)!>
137 <#assign dummy = ec.resource.expression("def formMeta = mcpSemanticData.formMetadata?.get('" + formName?js_string + "'); if (formMeta != null) formMeta.put('fields', tempFieldMetaList)", "")!>
143 </#if> 138 </#if>
144 139
145 <#t>${sri.popContext()} 140 <#t>${sri.popContext()}
...@@ -154,24 +149,17 @@ ...@@ -154,24 +149,17 @@
154 <#assign totalItems = listObject?size> 149 <#assign totalItems = listObject?size>
155 150
156 <#if mcpSemanticData?? && listObject?has_content> 151 <#if mcpSemanticData?? && listObject?has_content>
157 <#assign truncatedList = listObject> 152 <#assign formName = (.node["@name"]!"")?string>
158 <#assign dummy = mcpSemanticData.put(.node["@name"], truncatedList)> 153 <#assign displayedItems = (totalItems > 50)?then(50, totalItems)>
159 154 <#assign isTruncated = (totalItems > 50)>
160 <#if !mcpSemanticData.listMetadata??><#assign dummy = mcpSemanticData.put("listMetadata", {})></#if>
161
162 <#assign columnNames = []> 155 <#assign columnNames = []>
163 <#list formListColumnList as columnFieldList> 156 <#list formListColumnList as columnFieldList>
164 <#assign fieldNode = columnFieldList[0]> 157 <#assign fieldNode = columnFieldList[0]>
165 <#assign dummy = columnNames.add(fieldNode["@name"]!"")> 158 <#assign columnNames = columnNames + [fieldNode["@name"]!""]>
166 </#list> 159 </#list>
167 160 <#assign dummy = ec.context.put("tempListObject", listObject)!>
168 <#assign dummy = mcpSemanticData.listMetadata.put(.node["@name"]!"", { 161 <#assign dummy = ec.context.put("tempColumnNames", columnNames)!>
169 "name": .node["@name"]!"", 162 <#assign dummy = ec.resource.expression("mcpSemanticData.put('" + formName?js_string + "', tempListObject); if (mcpSemanticData.listMetadata == null) mcpSemanticData.listMetadata = [:]; mcpSemanticData.listMetadata.put('" + formName?js_string + "', [name: '" + formName?js_string + "', totalItems: " + totalItems + ", displayedItems: " + displayedItems + ", truncated: " + isTruncated?string + ", columns: tempColumnNames])", "")!>
170 "totalItems": totalItems,
171 "displayedItems": (totalItems > 50)?then(50, totalItems),
172 "truncated": (totalItems > 50),
173 "columns": columnNames
174 })>
175 </#if> 163 </#if>
176 164
177 <#-- Header Row --> 165 <#-- Header Row -->
...@@ -227,8 +215,8 @@ ...@@ -227,8 +215,8 @@
227 <#-- ================== Form Field Widgets ==================== --> 215 <#-- ================== Form Field Widgets ==================== -->
228 <#macro "check"> 216 <#macro "check">
229 <#assign options = sri.getFieldOptions(.node)!> 217 <#assign options = sri.getFieldOptions(.node)!>
230 <#assign currentValue = sri.getFieldValueString(.node)> 218 <#assign currentValue = sri.getFieldValueString(.node)!>
231 <#t>${(options.get(currentValue))!(currentValue)} 219 <#t>${(options[currentValue])!currentValue}
232 </#macro> 220 </#macro>
233 221
234 <#macro "date-find"></#macro> 222 <#macro "date-find"></#macro>
...@@ -264,9 +252,9 @@ ...@@ -264,9 +252,9 @@
264 </#macro> 252 </#macro>
265 253
266 <#macro "drop-down"> 254 <#macro "drop-down">
267 <#assign options = sri.getFieldOptions(.node)> 255 <#assign options = sri.getFieldOptions(.node)!>
268 <#assign currentValue = sri.getFieldValueString(.node)> 256 <#assign currentValue = sri.getFieldValueString(.node)!>
269 <#t>${(options.get(currentValue))!(currentValue)} 257 <#t>${(options[currentValue])!currentValue}
270 </#macro> 258 </#macro>
271 259
272 <#macro "text-area"><#t>${sri.getFieldValueString(.node)}</#macro> 260 <#macro "text-area"><#t>${sri.getFieldValueString(.node)}</#macro>
......
...@@ -321,6 +321,9 @@ class CustomScreenTestImpl implements McpScreenTest { ...@@ -321,6 +321,9 @@ class CustomScreenTestImpl implements McpScreenTest {
321 321
322 // Create a persistent map for semantic data that survives nested pops 322 // Create a persistent map for semantic data that survives nested pops
323 Map<String, Object> mcpSemanticData = new HashMap<>() 323 Map<String, Object> mcpSemanticData = new HashMap<>()
324 mcpSemanticData.put("links", new ArrayList<>())
325 mcpSemanticData.put("formMetadata", new HashMap<>())
326 mcpSemanticData.put("listMetadata", new HashMap<>())
324 cs.put("mcpSemanticData", mcpSemanticData) 327 cs.put("mcpSemanticData", mcpSemanticData)
325 328
326 // create the WebFacadeStub using our custom method 329 // create the WebFacadeStub using our custom method
......
...@@ -80,9 +80,11 @@ class UiNarrativeBuilder { ...@@ -80,9 +80,11 @@ class UiNarrativeBuilder {
80 80
81 def forms = semanticState?.data 81 def forms = semanticState?.data
82 if (forms) { 82 if (forms) {
83 def formNames = forms.keySet().findAll { k -> k.contains('Form') || k.contains('form') }[0..2] 83 def maxForms = isTerse ? 2 : 10
84 def formNames = forms.keySet().findAll { k -> k.contains('Form') || k.contains('form') }
84 if (formNames) { 85 if (formNames) {
85 def fields = getFormFieldNames(forms, formNames[0]) 86 def formNamesToDescribe = formNames.take(maxForms + 1)
87 def fields = getFormFieldNames(forms, formNamesToDescribe[0])
86 if (fields) { 88 if (fields) {
87 sb.append("Form contains: ${fields.join(', ')}. ") 89 sb.append("Form contains: ${fields.join(', ')}. ")
88 } 90 }
...@@ -93,7 +95,8 @@ class UiNarrativeBuilder { ...@@ -93,7 +95,8 @@ class UiNarrativeBuilder {
93 if (links && links.size() > 0) { 95 if (links && links.size() > 0) {
94 def linkTypes = links.collect { l -> l.type?.toString() ?: 'navigation' }.unique() 96 def linkTypes = links.collect { l -> l.type?.toString() ?: 'navigation' }.unique()
95 if (linkTypes) { 97 if (linkTypes) {
96 sb.append("Available links: ${linkTypes.take(3).join(', ')}. ") 98 def maxTypes = isTerse ? 3 : 15
99 sb.append("Available links: ${linkTypes.take(maxTypes).join(', ')}. ")
97 } 100 }
98 } 101 }
99 102
...@@ -141,7 +144,8 @@ class UiNarrativeBuilder { ...@@ -141,7 +144,8 @@ class UiNarrativeBuilder {
141 if (links && links.size() > 0) { 144 if (links && links.size() > 0) {
142 def sortedLinks = links.sort { a, b -> (a.text <=> b.text) } 145 def sortedLinks = links.sort { a, b -> (a.text <=> b.text) }
143 146
144 sortedLinks.take(5).each { link -> 147 def linksToTake = isTerse ? 5 : 50
148 sortedLinks.take(linksToTake).each { link ->
145 def linkText = link.text?.toString() 149 def linkText = link.text?.toString()
146 def linkPath = link.path?.toString() 150 def linkPath = link.path?.toString()
147 def linkType = link.type?.toString() ?: 'navigation' 151 def linkType = link.type?.toString() ?: 'navigation'
......