Add search form discovery for form-lists
- FTL: Detect header-field search widgets (text-find, date-find, etc.) - FTL: Add 'searchable' flag and search-specific types to field metadata - ARIA: Add role='search' landmark before grids with searchable fields - Compact: Add searchParams array listing filterable parameters - Filter out 'get*' transitions (autocomplete helpers) from transitions list This helps LLM clients understand how to filter form-list results by passing parameters, rather than trying to call autocomplete transitions.
Showing
2 changed files
with
63 additions
and
10 deletions
| ... | @@ -277,18 +277,33 @@ | ... | @@ -277,18 +277,33 @@ |
| 277 | <#assign columnNames = columnNames + [fieldNode["@name"]!""]> | 277 | <#assign columnNames = columnNames + [fieldNode["@name"]!""]> |
| 278 | </#list> | 278 | </#list> |
| 279 | 279 | ||
| 280 | <#-- Extract Field Metadata for form-list (header fields usually) --> | 280 | <#-- Extract Field Metadata for form-list - distinguish header (search) from display fields --> |
| 281 | <#assign fieldMetaList = []> | 281 | <#assign fieldMetaList = []> |
| 282 | <#list formListColumnList as columnFieldList> | 282 | <#list formListColumnList as columnFieldList> |
| 283 | <#assign fieldNode = columnFieldList[0]> | 283 | <#assign fieldNode = columnFieldList[0]> |
| 284 | <#assign fieldSubNode = fieldNode["header-field"][0]!fieldNode["default-field"][0]!fieldNode["conditional-field"][0]!> | 284 | <#-- Check if this field has a header-field with search widgets --> |
| 285 | <#assign headerFieldNode = fieldNode["header-field"][0]!> | ||
| 286 | <#assign hasSearchWidget = false> | ||
| 287 | <#if headerFieldNode?has_content> | ||
| 288 | <#-- Check for search-capable widgets in header-field --> | ||
| 289 | <#if headerFieldNode["text-find"]?has_content || headerFieldNode["text-line"]?has_content || | ||
| 290 | headerFieldNode["drop-down"]?has_content || headerFieldNode["date-find"]?has_content || | ||
| 291 | headerFieldNode["date-period"]?has_content || headerFieldNode["range-find"]?has_content> | ||
| 292 | <#assign hasSearchWidget = true> | ||
| 293 | </#if> | ||
| 294 | </#if> | ||
| 295 | <#assign fieldSubNode = headerFieldNode!fieldNode["default-field"][0]!fieldNode["conditional-field"][0]!> | ||
| 285 | 296 | ||
| 286 | <#if fieldSubNode?has_content && !fieldSubNode["ignored"]?has_content && !fieldSubNode["hidden"]?has_content> | 297 | <#if fieldSubNode?has_content && !fieldSubNode["ignored"]?has_content && !fieldSubNode["hidden"]?has_content> |
| 287 | <#assign title><@fieldTitle fieldSubNode/></#assign> | 298 | <#assign title><@fieldTitle fieldSubNode/></#assign> |
| 288 | <#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true")}> | 299 | <#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true"), "searchable": hasSearchWidget}> |
| 289 | 300 | ||
| 290 | <#if fieldSubNode["text-line"]?has_content><#assign fieldMeta = fieldMeta + {"type": "text"}></#if> | 301 | <#if fieldSubNode["text-line"]?has_content><#assign fieldMeta = fieldMeta + {"type": "text"}></#if> |
| 302 | <#if fieldSubNode["text-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "text-search"}></#if> | ||
| 291 | <#if fieldSubNode["text-area"]?has_content><#assign fieldMeta = fieldMeta + {"type": "textarea"}></#if> | 303 | <#if fieldSubNode["text-area"]?has_content><#assign fieldMeta = fieldMeta + {"type": "textarea"}></#if> |
| 304 | <#if fieldSubNode["date-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "date-search"}></#if> | ||
| 305 | <#if fieldSubNode["date-period"]?has_content><#assign fieldMeta = fieldMeta + {"type": "date-period"}></#if> | ||
| 306 | <#if fieldSubNode["range-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "range-search"}></#if> | ||
| 292 | <#if fieldSubNode["drop-down"]?has_content> | 307 | <#if fieldSubNode["drop-down"]?has_content> |
| 293 | <#-- Evaluate any 'set' nodes from widget-template-include before getting options --> | 308 | <#-- Evaluate any 'set' nodes from widget-template-include before getting options --> |
| 294 | <#list fieldSubNode["set"]! as setNode> | 309 | <#list fieldSubNode["set"]! as setNode> | ... | ... |
| ... | @@ -912,7 +912,7 @@ def convertToAriaTree = { semanticState, targetScreenPath -> | ... | @@ -912,7 +912,7 @@ def convertToAriaTree = { semanticState, targetScreenPath -> |
| 912 | if (formNode.children) children << formNode | 912 | if (formNode.children) children << formNode |
| 913 | } | 913 | } |
| 914 | 914 | ||
| 915 | // Process form-lists (become grids) | 915 | // Process form-lists (become grids with optional search section) |
| 916 | formMetadata.findAll { k, v -> v.type == "form-list" }.each { formName, formData -> | 916 | formMetadata.findAll { k, v -> v.type == "form-list" }.each { formName, formData -> |
| 917 | def listData = data[formName] | 917 | def listData = data[formName] |
| 918 | def itemCount = 0 | 918 | def itemCount = 0 |
| ... | @@ -922,6 +922,28 @@ def convertToAriaTree = { semanticState, targetScreenPath -> | ... | @@ -922,6 +922,28 @@ def convertToAriaTree = { semanticState, targetScreenPath -> |
| 922 | itemCount = listData._totalCount | 922 | itemCount = listData._totalCount |
| 923 | } | 923 | } |
| 924 | 924 | ||
| 925 | // Extract searchable fields from header-fields and create a search landmark | ||
| 926 | def searchableFields = formData.fields?.findAll { it.searchable == true } | ||
| 927 | if (searchableFields && searchableFields.size() > 0) { | ||
| 928 | def searchNode = [ | ||
| 929 | role: "search", | ||
| 930 | name: "Filter ${formData.name ?: formName}", | ||
| 931 | description: "Pass these as parameters to filter results", | ||
| 932 | children: searchableFields.collect { field -> | ||
| 933 | def searchField = [ | ||
| 934 | role: (field.type == "dropdown" || field.dynamicOptions) ? "combobox" : "searchbox", | ||
| 935 | name: field.title ?: field.name, | ||
| 936 | ref: field.name | ||
| 937 | ] | ||
| 938 | if (field.type == "dropdown" || field.dynamicOptions) { | ||
| 939 | searchField.description = "dropdown - use moqui_get_screen_details for options" | ||
| 940 | } | ||
| 941 | searchField | ||
| 942 | } | ||
| 943 | ] | ||
| 944 | children << searchNode | ||
| 945 | } | ||
| 946 | |||
| 925 | def gridNode = [ | 947 | def gridNode = [ |
| 926 | role: "grid", | 948 | role: "grid", |
| 927 | name: formData.name ?: formName, | 949 | name: formData.name ?: formName, |
| ... | @@ -929,9 +951,9 @@ def convertToAriaTree = { semanticState, targetScreenPath -> | ... | @@ -929,9 +951,9 @@ def convertToAriaTree = { semanticState, targetScreenPath -> |
| 929 | rowcount: itemCount | 951 | rowcount: itemCount |
| 930 | ] | 952 | ] |
| 931 | 953 | ||
| 932 | // Add column info | 954 | // Add column info (only non-searchable display columns) |
| 933 | def columns = formData.fields?.collect { it.title ?: it.name } | 955 | def displayColumns = formData.fields?.findAll { !it.searchable }?.collect { it.title ?: it.name } |
| 934 | if (columns) gridNode.columns = columns | 956 | if (displayColumns) gridNode.columns = displayColumns |
| 935 | 957 | ||
| 936 | // Add all rows with detail (no truncation - model needs complete data) | 958 | // Add all rows with detail (no truncation - model needs complete data) |
| 937 | if (listData instanceof List && listData.size() > 0) { | 959 | if (listData instanceof List && listData.size() > 0) { |
| ... | @@ -1143,14 +1165,25 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> | ... | @@ -1143,14 +1165,25 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> |
| 1143 | } | 1165 | } |
| 1144 | if (forms) result.forms = forms | 1166 | if (forms) result.forms = forms |
| 1145 | 1167 | ||
| 1146 | // Process grids (form-lists) | 1168 | // Process grids (form-lists) - include searchable parameters |
| 1147 | def grids = [:] | 1169 | def grids = [:] |
| 1170 | def searchParams = [] // Collect all searchable params across grids | ||
| 1148 | formMetadata.findAll { k, v -> v.type == "form-list" }.each { formName, formData -> | 1171 | formMetadata.findAll { k, v -> v.type == "form-list" }.each { formName, formData -> |
| 1149 | def listData = data[formName] | 1172 | def listData = data[formName] |
| 1150 | def gridInfo = [:] | 1173 | def gridInfo = [:] |
| 1151 | 1174 | ||
| 1152 | // Column names | 1175 | // Extract searchable fields and add to searchParams |
| 1153 | def columns = formData.fields?.findAll { it.type != "hidden" }?.collect { it.title ?: it.name } | 1176 | def searchableFields = formData.fields?.findAll { it.searchable == true } |
| 1177 | searchableFields?.each { field -> | ||
| 1178 | def paramInfo = [name: field.name] | ||
| 1179 | if (field.title && field.title != field.name) paramInfo.label = field.title | ||
| 1180 | if (field.type) paramInfo.type = field.type | ||
| 1181 | if (field.dynamicOptions) paramInfo.autocomplete = true | ||
| 1182 | searchParams << paramInfo | ||
| 1183 | } | ||
| 1184 | |||
| 1185 | // Column names (display columns only, not search fields) | ||
| 1186 | def columns = formData.fields?.findAll { it.type != "hidden" && !it.searchable }?.collect { it.title ?: it.name } | ||
| 1154 | if (columns) gridInfo.columns = columns | 1187 | if (columns) gridInfo.columns = columns |
| 1155 | 1188 | ||
| 1156 | // Rows with key data and links | 1189 | // Rows with key data and links |
| ... | @@ -1266,11 +1299,16 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> | ... | @@ -1266,11 +1299,16 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> |
| 1266 | } | 1299 | } |
| 1267 | if (actionMap) result.actions = actionMap | 1300 | if (actionMap) result.actions = actionMap |
| 1268 | 1301 | ||
| 1302 | // Add searchParams if any were collected from form-lists | ||
| 1303 | if (searchParams) result.searchParams = searchParams | ||
| 1304 | |||
| 1269 | // Transitions - screen navigation actions (toolbar buttons, row actions) | 1305 | // Transitions - screen navigation actions (toolbar buttons, row actions) |
| 1306 | // Filter out autocomplete/helper transitions that aren't user actions | ||
| 1270 | def transitionList = actions.findAll { | 1307 | def transitionList = actions.findAll { |
| 1271 | it.type == "screen-transition" && | 1308 | it.type == "screen-transition" && |
| 1272 | it.name && | 1309 | it.name && |
| 1273 | !it.name.startsWith("form") && // Skip formSelectColumns, formSaveFind | 1310 | !it.name.startsWith("form") && // Skip formSelectColumns, formSaveFind |
| 1311 | !it.name.startsWith("get") && // Skip getProductList, getFeatureList etc (autocomplete helpers) | ||
| 1274 | it.name != "actions" && // Skip generic 'actions' | 1312 | it.name != "actions" && // Skip generic 'actions' |
| 1275 | it.name != "screenDoc" // Skip documentation link | 1313 | it.name != "screenDoc" // Skip documentation link |
| 1276 | }.collect { it.name } | 1314 | }.collect { it.name } | ... | ... |
-
Please register or sign in to post a comment