0153ca18 by Ean Schuessler

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.
1 parent 98e52cc1
...@@ -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 }
......