Improve ARIA grid rendering and action result feedback
- Fix column extraction to include fields with both search and display widgets - Add all ID fields to row ref as pipe-delimited key=value pairs for unambiguous LLM parsing - Show NULL values as '(none)' in grid cells for consistency - Add ARIA-compliant action result feedback (role=alert for errors, role=status for success) - Pass actionResult through BrowseScreens ARIA mode - Hide batch_operations tool until fixed - Default renderMode to 'aria' instead of 'compact'
Showing
3 changed files
with
28 additions
and
46 deletions
| ... | @@ -154,6 +154,7 @@ | ... | @@ -154,6 +154,7 @@ |
| 154 | 154 | ||
| 155 | <#-- Evaluate any 'set' nodes from widget-template-include before getting options --> | 155 | <#-- Evaluate any 'set' nodes from widget-template-include before getting options --> |
| 156 | <#-- These set variables like enumTypeId needed by entity-options --> | 156 | <#-- These set variables like enumTypeId needed by entity-options --> |
| 157 | <#-- Note: set nodes are appended to fieldSubNode after template expansion --> | ||
| 157 | <#assign setNodes = fieldSubNode["set"]!> | 158 | <#assign setNodes = fieldSubNode["set"]!> |
| 158 | <#list setNodes as setNode> | 159 | <#list setNodes as setNode> |
| 159 | <#if setNode["@field"]?has_content> | 160 | <#if setNode["@field"]?has_content> |
| ... | @@ -162,18 +163,18 @@ | ... | @@ -162,18 +163,18 @@ |
| 162 | </#list> | 163 | </#list> |
| 163 | <#-- Get dropdown options - pass the drop-down node, not fieldSubNode --> | 164 | <#-- Get dropdown options - pass the drop-down node, not fieldSubNode --> |
| 164 | <#assign dropdownOptions = sri.getFieldOptions(dropdownNode)!> | 165 | <#assign dropdownOptions = sri.getFieldOptions(dropdownNode)!> |
| 166 | <#assign skipTruncation = (ec.context.mcpFullOptions!false) == true> | ||
| 165 | <#if (dropdownOptions?size!0) gt 0> | 167 | <#if (dropdownOptions?size!0) gt 0> |
| 166 | <#-- Build options list from the LinkedHashMap --> | 168 | <#-- Build options list from the LinkedHashMap --> |
| 167 | <#-- Truncate if > 10 unless mcpFullOptions is set (for get_screen_details) --> | 169 | <#-- Truncate if > 10 unless mcpFullOptions is set (for get_screen_details) --> |
| 168 | <#assign optionsList = []> | 170 | <#assign optionsList = []> |
| 169 | <#assign totalOptions = dropdownOptions?size> | 171 | <#assign totalOptions = dropdownOptions?size> |
| 170 | <#assign skipTruncation = (ec.context.mcpFullOptions!false) == true> | ||
| 171 | <#assign optionLimit = skipTruncation?then(999999, 10)> | 172 | <#assign optionLimit = skipTruncation?then(999999, 10)> |
| 172 | <#assign optionCount = 0> | 173 | <#assign optionCount = 0> |
| 173 | <#list (dropdownOptions.keySet())! as optKey> | 174 | <#-- Use entrySet() to iterate Java LinkedHashMap - avoids FreeMarker exposing method names as keys --> |
| 175 | <#list dropdownOptions.entrySet() as entry> | ||
| 174 | <#if optionCount lt optionLimit> | 176 | <#if optionCount lt optionLimit> |
| 175 | <#assign optLabel = (dropdownOptions.get(optKey))!optKey> | 177 | <#assign optionsList = optionsList + [{"value": entry.getKey(), "label": entry.getValue()}]> |
| 176 | <#assign optionsList = optionsList + [{"value": optKey, "label": optLabel}]> | ||
| 177 | </#if> | 178 | </#if> |
| 178 | <#assign optionCount = optionCount + 1> | 179 | <#assign optionCount = optionCount + 1> |
| 179 | </#list> | 180 | </#list> | ... | ... |
This diff is collapsed.
Click to expand it.
| ... | @@ -17,9 +17,7 @@ import groovy.json.JsonSlurper | ... | @@ -17,9 +17,7 @@ import groovy.json.JsonSlurper |
| 17 | class McpFieldOptionsService { | 17 | class McpFieldOptionsService { |
| 18 | 18 | ||
| 19 | static service(String path, String fieldName, Map parameters, ExecutionContext ec) { | 19 | static service(String path, String fieldName, Map parameters, ExecutionContext ec) { |
| 20 | ec.logger.info("======== MCP GetScreenDetails CALLED - CODE VERSION 3 (ScreenTest) =======") | ||
| 21 | if (!path) throw new IllegalArgumentException("path is required") | 20 | if (!path) throw new IllegalArgumentException("path is required") |
| 22 | ec.logger.info("MCP GetScreenDetails: screen ${path}, field ${fieldName ?: 'all'}") | ||
| 23 | 21 | ||
| 24 | def result = [screenPath: path, fields: [:]] | 22 | def result = [screenPath: path, fields: [:]] |
| 25 | try { | 23 | try { |
| ... | @@ -30,15 +28,13 @@ class McpFieldOptionsService { | ... | @@ -30,15 +28,13 @@ class McpFieldOptionsService { |
| 30 | .parameters([path: path, parameters: mergedParams, renderMode: "mcp", sessionId: null]) | 28 | .parameters([path: path, parameters: mergedParams, renderMode: "mcp", sessionId: null]) |
| 31 | .call() | 29 | .call() |
| 32 | 30 | ||
| 33 | ec.logger.info("=== browseResult: ${browseResult != null}, result exists: ${browseResult?.result != null} ===") | ||
| 34 | |||
| 35 | if (!browseResult?.result?.content) { | 31 | if (!browseResult?.result?.content) { |
| 36 | ec.logger.warn("No content from ScreenAsMcpTool") | 32 | ec.logger.warn("GetScreenDetails: No content from ScreenAsMcpTool for path ${path}") |
| 37 | return result + [error: "No content from ScreenAsMcpTool"] | 33 | return result + [error: "No content from ScreenAsMcpTool"] |
| 38 | } | 34 | } |
| 39 | def rawText = browseResult.result.content[0].text | 35 | def rawText = browseResult.result.content[0].text |
| 40 | if (!rawText || !rawText.startsWith("{")) { | 36 | if (!rawText || !rawText.startsWith("{")) { |
| 41 | ec.logger.warn("Invalid JSON from ScreenAsMcpTool") | 37 | ec.logger.warn("GetScreenDetails: Invalid JSON from ScreenAsMcpTool for path ${path}") |
| 42 | return result + [error: "Invalid JSON from ScreenAsMcpTool"] | 38 | return result + [error: "Invalid JSON from ScreenAsMcpTool"] |
| 43 | } | 39 | } |
| 44 | 40 | ||
| ... | @@ -47,14 +43,13 @@ class McpFieldOptionsService { | ... | @@ -47,14 +43,13 @@ class McpFieldOptionsService { |
| 47 | def formMetadata = semanticState?.data?.formMetadata | 43 | def formMetadata = semanticState?.data?.formMetadata |
| 48 | 44 | ||
| 49 | if (!(formMetadata instanceof Map)) { | 45 | if (!(formMetadata instanceof Map)) { |
| 50 | ec.logger.warn("formMetadata is not a Map: ${formMetadata?.class}") | 46 | ec.logger.warn("GetScreenDetails: formMetadata is not a Map for path ${path}") |
| 51 | return result + [error: "No form metadata found"] | 47 | return result + [error: "No form metadata found"] |
| 52 | } | 48 | } |
| 53 | 49 | ||
| 54 | def allFields = [:] | 50 | def allFields = [:] |
| 55 | ec.logger.info("=== Processing formMetadata with ${formMetadata.size()} forms ===") | 51 | |
| 56 | formMetadata.each { formName, formItem -> | 52 | formMetadata.each { formName, formItem -> |
| 57 | ec.logger.info("=== Processing form: ${formName}, hasFields: ${formItem?.fields != null} ===") | ||
| 58 | if (!(formItem instanceof Map) || !formItem.fields) return | 53 | if (!(formItem instanceof Map) || !formItem.fields) return |
| 59 | formItem.fields.each { field -> | 54 | formItem.fields.each { field -> |
| 60 | if (!(field instanceof Map) || !field.name) return | 55 | if (!(field instanceof Map) || !field.name) return |
| ... | @@ -70,14 +65,27 @@ class McpFieldOptionsService { | ... | @@ -70,14 +65,27 @@ class McpFieldOptionsService { |
| 70 | def dynamicOptions = field.dynamicOptions | 65 | def dynamicOptions = field.dynamicOptions |
| 71 | if (dynamicOptions instanceof Map) { | 66 | if (dynamicOptions instanceof Map) { |
| 72 | fieldInfo.dynamicOptions = dynamicOptions | 67 | fieldInfo.dynamicOptions = dynamicOptions |
| 73 | ec.logger.info("Found dynamicOptions for field ${field.name}: ${dynamicOptions}") | ||
| 74 | try { | 68 | try { |
| 75 | fetchOptions(fieldInfo, path, parameters, dynamicOptions, ec) | 69 | fetchOptions(fieldInfo, path, parameters, dynamicOptions, ec) |
| 76 | } catch (Exception e) { | 70 | } catch (Exception e) { |
| 77 | ec.logger.warn("Failed to fetch options for ${field.name}: ${e.message}", e) | 71 | ec.logger.warn("GetScreenDetails: Failed to fetch options for ${field.name}: ${e.message}") |
| 78 | fieldInfo.optionsError = e.message | 72 | fieldInfo.optionsError = e.message |
| 79 | } | 73 | } |
| 80 | } | 74 | } |
| 75 | |||
| 76 | // Merge fields with same name - prefer version with options | ||
| 77 | // This handles cases where a field appears in both search and edit forms | ||
| 78 | def existingField = allFields[field.name] | ||
| 79 | if (existingField) { | ||
| 80 | // Keep existing options if new field has none | ||
| 81 | if (existingField.options && !fieldInfo.options) { | ||
| 82 | fieldInfo.options = existingField.options | ||
| 83 | } | ||
| 84 | // Merge dynamicOptions if existing has them | ||
| 85 | if (existingField.dynamicOptions && !fieldInfo.dynamicOptions) { | ||
| 86 | fieldInfo.dynamicOptions = existingField.dynamicOptions | ||
| 87 | } | ||
| 88 | } | ||
| 81 | allFields[field.name] = fieldInfo | 89 | allFields[field.name] = fieldInfo |
| 82 | } | 90 | } |
| 83 | } | 91 | } |
| ... | @@ -102,12 +110,8 @@ class McpFieldOptionsService { | ... | @@ -102,12 +110,8 @@ class McpFieldOptionsService { |
| 102 | * and capture the raw JSON response - exactly how ScreenRenderImpl.getFieldOptions() works. | 110 | * and capture the raw JSON response - exactly how ScreenRenderImpl.getFieldOptions() works. |
| 103 | */ | 111 | */ |
| 104 | private static void fetchOptions(Map fieldInfo, String path, Map parameters, Map dynamicOptions, ExecutionContext ec) { | 112 | private static void fetchOptions(Map fieldInfo, String path, Map parameters, Map dynamicOptions, ExecutionContext ec) { |
| 105 | ec.logger.info("=== fetchOptions START: ${fieldInfo.name} ===") | ||
| 106 | def transitionName = dynamicOptions.transition | 113 | def transitionName = dynamicOptions.transition |
| 107 | if (!transitionName) { | 114 | if (!transitionName) return |
| 108 | ec.logger.info("No transition specified for dynamic options") | ||
| 109 | return | ||
| 110 | } | ||
| 111 | 115 | ||
| 112 | def optionParams = [:] | 116 | def optionParams = [:] |
| 113 | 117 | ||
| ... | @@ -135,24 +139,17 @@ class McpFieldOptionsService { | ... | @@ -135,24 +139,17 @@ class McpFieldOptionsService { |
| 135 | } | 139 | } |
| 136 | } | 140 | } |
| 137 | 141 | ||
| 138 | // 2. Handle serverSearch fields | 142 | // 2. Handle serverSearch fields - skip if no search term provided (matches framework behavior) |
| 139 | // If serverSearch is true AND no term is provided, skip fetching (matches framework behavior) | ||
| 140 | // The framework's getFieldOptions() skips server-search fields entirely for initial load | ||
| 141 | def isServerSearch = dynamicOptions.serverSearch == true || dynamicOptions.serverSearch == "true" | 143 | def isServerSearch = dynamicOptions.serverSearch == true || dynamicOptions.serverSearch == "true" |
| 142 | if (isServerSearch) { | 144 | if (isServerSearch) { |
| 143 | if (parameters?.term != null && parameters.term.toString().length() > 0) { | 145 | if (parameters?.term != null && parameters.term.toString().length() > 0) { |
| 144 | optionParams.term = parameters.term | 146 | optionParams.term = parameters.term |
| 145 | } else { | 147 | } else { |
| 146 | // Skip fetching options for server-search fields without a term | 148 | return // Skip server-search fields without a term |
| 147 | ec.logger.info("Skipping server-search field ${fieldInfo.name} - no term provided") | ||
| 148 | return | ||
| 149 | } | 149 | } |
| 150 | } | 150 | } |
| 151 | 151 | ||
| 152 | // 3. Use CustomScreenTestImpl with skipJsonSerialize to call the transition | 152 | // 3. Use CustomScreenTestImpl with skipJsonSerialize to call the transition |
| 153 | // This is exactly how ScreenRenderImpl.getFieldOptions() works in the framework | ||
| 154 | ec.logger.info("Calling transition ${transitionName} via CustomScreenTestImpl with skipJsonSerialize=true, params: ${optionParams}") | ||
| 155 | |||
| 156 | try { | 153 | try { |
| 157 | def ecfi = (ExecutionContextFactoryImpl) ec.factory | 154 | def ecfi = (ExecutionContextFactoryImpl) ec.factory |
| 158 | 155 | ||
| ... | @@ -166,9 +163,6 @@ class McpFieldOptionsService { | ... | @@ -166,9 +163,6 @@ class McpFieldOptionsService { |
| 166 | fullPath.split('/').each { if (it && it.trim()) pathSegments.add(it) } | 163 | fullPath.split('/').each { if (it && it.trim()) pathSegments.add(it) } |
| 167 | 164 | ||
| 168 | // Component-based resolution (same as ScreenAsMcpTool) | 165 | // Component-based resolution (same as ScreenAsMcpTool) |
| 169 | // Path like "PopCommerce/PopCommerceAdmin/Party/FindParty/transition" becomes: | ||
| 170 | // - rootScreen: component://PopCommerce/screen/PopCommerceAdmin.xml | ||
| 171 | // - testScreenPath: Party/FindParty/transition | ||
| 172 | def rootScreen = "component://webroot/screen/webroot.xml" | 166 | def rootScreen = "component://webroot/screen/webroot.xml" |
| 173 | def testScreenPath = fullPath | 167 | def testScreenPath = fullPath |
| 174 | 168 | ||
| ... | @@ -178,7 +172,6 @@ class McpFieldOptionsService { | ... | @@ -178,7 +172,6 @@ class McpFieldOptionsService { |
| 178 | def compRootLoc = "component://${componentName}/screen/${rootScreenName}.xml" | 172 | def compRootLoc = "component://${componentName}/screen/${rootScreenName}.xml" |
| 179 | 173 | ||
| 180 | if (ec.resource.getLocationReference(compRootLoc).exists) { | 174 | if (ec.resource.getLocationReference(compRootLoc).exists) { |
| 181 | ec.logger.info("fetchOptions: Using component root: ${compRootLoc}") | ||
| 182 | rootScreen = compRootLoc | 175 | rootScreen = compRootLoc |
| 183 | testScreenPath = pathSegments.size() > 2 ? pathSegments[2..-1].join('/') : "" | 176 | testScreenPath = pathSegments.size() > 2 ? pathSegments[2..-1].join('/') : "" |
| 184 | } | 177 | } |
| ... | @@ -190,12 +183,10 @@ class McpFieldOptionsService { | ... | @@ -190,12 +183,10 @@ class McpFieldOptionsService { |
| 190 | .skipJsonSerialize(true) | 183 | .skipJsonSerialize(true) |
| 191 | .auth(ec.user.username) | 184 | .auth(ec.user.username) |
| 192 | 185 | ||
| 193 | ec.logger.info("Rendering transition path: ${testScreenPath} (from root: ${rootScreen})") | ||
| 194 | def str = screenTest.render(testScreenPath, optionParams, "GET") | 186 | def str = screenTest.render(testScreenPath, optionParams, "GET") |
| 195 | 187 | ||
| 196 | // Get JSON object directly (like web UI does) | 188 | // Get JSON object directly (like web UI does) |
| 197 | def jsonObj = str.getJsonObject() | 189 | def jsonObj = str.getJsonObject() |
| 198 | ec.logger.info("Transition returned jsonObj: ${jsonObj?.getClass()?.simpleName}, size: ${jsonObj instanceof Collection ? jsonObj.size() : 'N/A'}") | ||
| 199 | 190 | ||
| 200 | // Extract value-field and label-field from dynamic-options config | 191 | // Extract value-field and label-field from dynamic-options config |
| 201 | def valueField = dynamicOptions.valueField ?: dynamicOptions.'value-field' ?: 'value' | 192 | def valueField = dynamicOptions.valueField ?: dynamicOptions.'value-field' ?: 'value' |
| ... | @@ -239,20 +230,10 @@ class McpFieldOptionsService { | ... | @@ -239,20 +230,10 @@ class McpFieldOptionsService { |
| 239 | [value: entryObj, label: entryObj?.toString()] | 230 | [value: entryObj, label: entryObj?.toString()] |
| 240 | } | 231 | } |
| 241 | }.findAll { it.value != null } | 232 | }.findAll { it.value != null } |
| 242 | |||
| 243 | ec.logger.info("Successfully extracted ${fieldInfo.options.size()} autocomplete options via ScreenTest") | ||
| 244 | } else { | ||
| 245 | ec.logger.info("No options found in transition response") | ||
| 246 | |||
| 247 | // Check if there was output but no JSON (might be an error) | ||
| 248 | def output = str.getOutput() | ||
| 249 | if (output && output.length() > 0 && output.length() < 500) { | ||
| 250 | ec.logger.warn("Transition output (no JSON): ${output}") | ||
| 251 | } | ||
| 252 | } | 233 | } |
| 253 | 234 | ||
| 254 | } catch (Exception e) { | 235 | } catch (Exception e) { |
| 255 | ec.logger.warn("Error calling transition ${transitionName}: ${e.message}", e) | 236 | ec.logger.warn("GetScreenDetails: Error calling transition ${transitionName}: ${e.message}") |
| 256 | fieldInfo.optionsError = "Transition call failed: ${e.message}" | 237 | fieldInfo.optionsError = "Transition call failed: ${e.message}" |
| 257 | } | 238 | } |
| 258 | } | 239 | } | ... | ... |
-
Please register or sign in to post a comment