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 @@
<#assign columnNames = columnNames + [fieldNode["@name"]!""]>
</#list>
<#-- Extract Field Metadata for form-list (header fields usually) -->
<#-- Extract Field Metadata for form-list - distinguish header (search) from display fields -->
<#assign fieldMetaList = []>
<#list formListColumnList as columnFieldList>
<#assign fieldNode = columnFieldList[0]>
<#assign fieldSubNode = fieldNode["header-field"][0]!fieldNode["default-field"][0]!fieldNode["conditional-field"][0]!>
<#-- Check if this field has a header-field with search widgets -->
<#assign headerFieldNode = fieldNode["header-field"][0]!>
<#assign hasSearchWidget = false>
<#if headerFieldNode?has_content>
<#-- Check for search-capable widgets in header-field -->
<#if headerFieldNode["text-find"]?has_content || headerFieldNode["text-line"]?has_content ||
headerFieldNode["drop-down"]?has_content || headerFieldNode["date-find"]?has_content ||
headerFieldNode["date-period"]?has_content || headerFieldNode["range-find"]?has_content>
<#assign hasSearchWidget = true>
</#if>
</#if>
<#assign fieldSubNode = headerFieldNode!fieldNode["default-field"][0]!fieldNode["conditional-field"][0]!>
<#if fieldSubNode?has_content && !fieldSubNode["ignored"]?has_content && !fieldSubNode["hidden"]?has_content>
<#assign title><@fieldTitle fieldSubNode/></#assign>
<#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true")}>
<#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true"), "searchable": hasSearchWidget}>
<#if fieldSubNode["text-line"]?has_content><#assign fieldMeta = fieldMeta + {"type": "text"}></#if>
<#if fieldSubNode["text-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "text-search"}></#if>
<#if fieldSubNode["text-area"]?has_content><#assign fieldMeta = fieldMeta + {"type": "textarea"}></#if>
<#if fieldSubNode["date-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "date-search"}></#if>
<#if fieldSubNode["date-period"]?has_content><#assign fieldMeta = fieldMeta + {"type": "date-period"}></#if>
<#if fieldSubNode["range-find"]?has_content><#assign fieldMeta = fieldMeta + {"type": "range-search"}></#if>
<#if fieldSubNode["drop-down"]?has_content>
<#-- Evaluate any 'set' nodes from widget-template-include before getting options -->
<#list fieldSubNode["set"]! as setNode>
......
......@@ -912,7 +912,7 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
if (formNode.children) children << formNode
}
// Process form-lists (become grids)
// Process form-lists (become grids with optional search section)
formMetadata.findAll { k, v -> v.type == "form-list" }.each { formName, formData ->
def listData = data[formName]
def itemCount = 0
......@@ -922,6 +922,28 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
itemCount = listData._totalCount
}
// Extract searchable fields from header-fields and create a search landmark
def searchableFields = formData.fields?.findAll { it.searchable == true }
if (searchableFields && searchableFields.size() > 0) {
def searchNode = [
role: "search",
name: "Filter ${formData.name ?: formName}",
description: "Pass these as parameters to filter results",
children: searchableFields.collect { field ->
def searchField = [
role: (field.type == "dropdown" || field.dynamicOptions) ? "combobox" : "searchbox",
name: field.title ?: field.name,
ref: field.name
]
if (field.type == "dropdown" || field.dynamicOptions) {
searchField.description = "dropdown - use moqui_get_screen_details for options"
}
searchField
}
]
children << searchNode
}
def gridNode = [
role: "grid",
name: formData.name ?: formName,
......@@ -929,9 +951,9 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
rowcount: itemCount
]
// Add column info
def columns = formData.fields?.collect { it.title ?: it.name }
if (columns) gridNode.columns = columns
// Add column info (only non-searchable display columns)
def displayColumns = formData.fields?.findAll { !it.searchable }?.collect { it.title ?: it.name }
if (displayColumns) gridNode.columns = displayColumns
// Add all rows with detail (no truncation - model needs complete data)
if (listData instanceof List && listData.size() > 0) {
......@@ -1143,14 +1165,25 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
}
if (forms) result.forms = forms
// Process grids (form-lists)
// Process grids (form-lists) - include searchable parameters
def grids = [:]
def searchParams = [] // Collect all searchable params across grids
formMetadata.findAll { k, v -> v.type == "form-list" }.each { formName, formData ->
def listData = data[formName]
def gridInfo = [:]
// Column names
def columns = formData.fields?.findAll { it.type != "hidden" }?.collect { it.title ?: it.name }
// Extract searchable fields and add to searchParams
def searchableFields = formData.fields?.findAll { it.searchable == true }
searchableFields?.each { field ->
def paramInfo = [name: field.name]
if (field.title && field.title != field.name) paramInfo.label = field.title
if (field.type) paramInfo.type = field.type
if (field.dynamicOptions) paramInfo.autocomplete = true
searchParams << paramInfo
}
// Column names (display columns only, not search fields)
def columns = formData.fields?.findAll { it.type != "hidden" && !it.searchable }?.collect { it.title ?: it.name }
if (columns) gridInfo.columns = columns
// Rows with key data and links
......@@ -1266,11 +1299,16 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
}
if (actionMap) result.actions = actionMap
// Add searchParams if any were collected from form-lists
if (searchParams) result.searchParams = searchParams
// Transitions - screen navigation actions (toolbar buttons, row actions)
// Filter out autocomplete/helper transitions that aren't user actions
def transitionList = actions.findAll {
it.type == "screen-transition" &&
it.name &&
!it.name.startsWith("form") && // Skip formSelectColumns, formSaveFind
!it.name.startsWith("get") && // Skip getProductList, getFeatureList etc (autocomplete helpers)
it.name != "actions" && // Skip generic 'actions'
it.name != "screenDoc" // Skip documentation link
}.collect { it.name }
......