Skip to main content

Query Filters and Search

Learn how to implement powerful filtering capabilities in Stack9 applications. This comprehensive guide covers all filter types, search functionality, and how they integrate with different screen types.

Overview

Query filters in Stack9 enable users to refine data displayed in list views and grids. They work with both stack9_api and opensearch connector types, providing a rich filtering experience through the GridBuilder component.

Key Concepts

  • Filters: UI controls that allow users to narrow down data based on specific criteria
  • Filter Sections: Grouped collections of related filters for better organization
  • Search Fields: Text-based search across multiple fields simultaneously
  • Filter Types: Different UI components for various data types (dropdowns, date ranges, checkboxes, etc.)

How Filters Work with Screen Types

Filters are primarily used with screens that display tabular data:

Compatible Screen Types

Screen TypeFilter SupportImplementation
listView✅ Full supportVia listQuery with filters
simpleCrud✅ Full supportVia listQuery with filters
screenView✅ When using GridBuilderVia custom components
detailView❌ Not applicableSingle record view
embeddedView✅ When using GridBuilderVia custom components

The GridBuilder Component

The GridBuilder component (packages/stack9-ui/src/components/common/Grid/GridBuilder.tsx) renders filters defined in your queries. It provides:

  • Filter accordion UI on the left side
  • Search box at the top (when querySearchFields is defined)
  • Applied filter badges
  • Filter sections with collapse/expand functionality

Filter Schema

Filters are defined in the query library JSON files. Here's the complete schema:

{
"filters": [
{
"name": "Filter Display Name", // Required: Shows in UI
"key": "filter_key", // Required: Unique identifier
"typeQueryFilter": "compare", // Required: compare | between | array
"field": "database_field", // Required: Database field to filter
"typeFilter": "FilterUIType", // Required: UI component type
"useSubquery": false, // Required: For nested queries
"sequence": 1, // Required: Display order
"filterSectionKey": "section_key", // Optional: Group filters
"dataSource": { // Required for selection filters
"type": "static", // static | query | queryDistinct
"options": [] // For static type
}
}
],
"filterSections": [
{
"name": "Section Name",
"key": "section_key",
"sequence": 1,
"collapsed": true // Optional: Start collapsed
}
],
"querySearchFields": ["field1", "field2"] // Text search fields
}

Filter Types Reference

Text Filters

StringCompareValue

Text input with comparison operators (equals, contains, starts with).

{
"name": "Customer Name",
"key": "customer_name",
"typeQueryFilter": "compare",
"field": "name",
"typeFilter": "StringCompareValue",
"useSubquery": false,
"sequence": 1
}

Use cases: Names, descriptions, codes, addresses

Numeric Filters

NumericCompareValue

Number input with comparison operators (equals, greater than, less than).

{
"name": "Order ID",
"key": "order_id",
"typeQueryFilter": "compare",
"field": "id",
"typeFilter": "NumericCompareValue",
"useSubquery": false,
"sequence": 1
}

NumericRangeValue

Min/max number inputs for range filtering.

{
"name": "Price Range",
"key": "price",
"typeQueryFilter": "between",
"field": "price",
"typeFilter": "NumericRangeValue",
"useSubquery": false,
"sequence": 2
}

Use cases: IDs, quantities, prices, scores

Date Filters

DateRangeValue

Date range picker with start and end dates.

{
"name": "Created Date",
"key": "created_date",
"typeQueryFilter": "between",
"field": "created_at",
"typeFilter": "DateRangeValue",
"useSubquery": false,
"sequence": 1
}

DateCompareValue

Single date picker with comparison operators.

{
"name": "Due Date",
"key": "due_date",
"typeQueryFilter": "compare",
"field": "due_date",
"typeFilter": "DateCompareValue",
"useSubquery": false,
"sequence": 1
}

Use cases: Creation dates, transaction dates, deadlines

Selection Filters

SingleDropDown

Single selection dropdown menu.

{
"name": "Card Type",
"key": "card_type",
"typeQueryFilter": "compare",
"field": "card_type",
"typeFilter": "SingleDropDown",
"useSubquery": false,
"sequence": 1,
"dataSource": {
"type": "static",
"options": [
{ "label": "Visa", "value": "Visa" },
{ "label": "Mastercard", "value": "Mastercard" },
{ "label": "American Express", "value": "American Express" }
]
}
}

MultiDropDown

Multiple selection dropdown with checkboxes.

{
"name": "Status",
"key": "status",
"typeQueryFilter": "array",
"field": "status",
"typeFilter": "MultiDropDown",
"useSubquery": false,
"sequence": 1,
"dataSource": {
"type": "static",
"options": [
{ "label": "Active", "value": "active" },
{ "label": "Pending", "value": "pending" },
{ "label": "Inactive", "value": "inactive" }
]
}
}

Checkboxes

Checkbox group for multiple selections.

{
"name": "Payment Method",
"key": "payment_method",
"typeQueryFilter": "array",
"field": "payment_method",
"typeFilter": "Checkboxes",
"useSubquery": false,
"sequence": 1,
"dataSource": {
"type": "static",
"options": [
{ "label": "Credit Card", "value": "credit_card" },
{ "label": "PayPal", "value": "paypal" },
{ "label": "Bank Transfer", "value": "bank_transfer" }
]
}
}

RadioButtons

Radio button group for single selection.

{
"name": "Customer Type",
"key": "customer_type",
"typeQueryFilter": "compare",
"field": "type",
"typeFilter": "RadioButtons",
"useSubquery": false,
"sequence": 1,
"dataSource": {
"type": "static",
"options": [
{ "label": "Individual", "value": "individual" },
{ "label": "Business", "value": "business" }
]
}
}

Boolean Filter

BooleanValue

Yes/No/Any selector for boolean fields.

{
"name": "Is Featured",
"key": "is_featured",
"typeQueryFilter": "compare",
"field": "is_featured",
"typeFilter": "BooleanValue",
"useSubquery": false,
"sequence": 1
}

Use cases: Active/inactive flags, true/false conditions

Data Source Types

Static Data Source

Hardcoded options defined in the query.

"dataSource": {
"type": "static",
"options": [
{ "label": "Option 1", "value": "value1" },
{ "label": "Option 2", "value": "value2" }
]
}

Query Data Source

Fetches options from another query.

"dataSource": {
"type": "query",
"labelProp": "name", // Field to display
"valueProp": "id", // Field to use as value
"query": {
"name": "getCategoryList",
"queryKey": "getcategorylist"
}
}

Query Distinct Data Source

Gets distinct values from a query.

"dataSource": {
"type": "queryDistinct",
"query": {
"name": "getDistinctStatuses",
"queryKey": "getdistinctstatuses"
}
}

Text Search with querySearchFields

The querySearchFields property enables text-based search across multiple fields simultaneously.

{
"querySearchFields": [
"name",
"email",
"phone",
"address"
]
}

Search Field Weighting (OpenSearch)

For OpenSearch queries, you can add weight to fields using the ^ operator:

{
"querySearchFields": [
"name^3", // 3x weight
"email^2", // 2x weight
"description", // 1x weight (default)
"sku.keyword" // Exact match on keyword field
]
}

Complete Examples

Example 1: Customer List with Comprehensive Filters (OpenSearch)

{
"key": "getcustomerlist",
"name": "getCustomerList",
"connector": "opensearch",
"queryTemplate": {
"actionType": "search",
"indexName": "customer",
"bodyParams": "{\n \"from\": {{page}},\n \"size\": {{limit}},\n \"query\": {\n \"bool\": {\n \"filter\": [\n {\n \"term\": {\n \"is_active\": true\n }\n }\n ]\n }\n }\n}"
},
"filterSections": [
{
"name": "Customer Details",
"key": "customer_details",
"sequence": 1,
"collapsed": false
},
{
"name": "Contact Preferences",
"key": "contact_preferences",
"sequence": 2,
"collapsed": true
}
],
"filters": [
{
"name": "Customer Name",
"key": "name",
"typeQueryFilter": "compare",
"field": "all_names",
"typeFilter": "StringCompareValue",
"useSubquery": false,
"sequence": 1,
"filterSectionKey": "customer_details"
},
{
"name": "Email",
"key": "email",
"typeQueryFilter": "compare",
"field": "email_address",
"typeFilter": "StringCompareValue",
"useSubquery": false,
"sequence": 2,
"filterSectionKey": "customer_details"
},
{
"name": "Date of Birth",
"key": "dob",
"typeQueryFilter": "between",
"field": "date_of_birth",
"typeFilter": "DateRangeValue",
"useSubquery": false,
"sequence": 3,
"filterSectionKey": "customer_details"
},
{
"name": "Customer Type",
"key": "customer_type",
"typeQueryFilter": "array",
"field": "type",
"typeFilter": "MultiDropDown",
"useSubquery": false,
"sequence": 4,
"filterSectionKey": "customer_details",
"dataSource": {
"type": "static",
"options": [
{ "label": "Individual", "value": "Individual" },
{ "label": "Business", "value": "Business" }
]
}
},
{
"name": "Prefer Email",
"key": "prefer_email",
"typeQueryFilter": "compare",
"field": "preferences.email",
"typeFilter": "BooleanValue",
"useSubquery": false,
"sequence": 1,
"filterSectionKey": "contact_preferences"
}
],
"querySearchFields": [
"preferred_name^2",
"last_name^2",
"email_address",
"phone",
"address_line_1"
],
"userParams": {
"page": "0",
"limit": "50"
}
}

Example 2: Product List with Filters (Stack9 API)

{
"key": "getproductlist",
"name": "getProductList",
"connector": "stack9_api",
"queryTemplate": {
"method": "post",
"path": "/product/search",
"bodyParams": {
"$select": ["id", "name", "sku", "price", "category_id", "status"],
"$where": { "_is_deleted": false },
"$sort": { "created_at": "desc" },
"$withRelated": ["category", "brand"]
},
"queryParams": {
"page": "{{page}}",
"limit": "{{limit}}"
}
},
"filterSections": [
{
"name": "Product Information",
"key": "product_info",
"sequence": 1
},
{
"name": "Pricing & Stock",
"key": "pricing_stock",
"sequence": 2
}
],
"filters": [
{
"name": "Category",
"key": "category",
"typeQueryFilter": "array",
"field": "category_id",
"typeFilter": "MultiDropDown",
"useSubquery": false,
"sequence": 1,
"filterSectionKey": "product_info",
"dataSource": {
"type": "query",
"labelProp": "name",
"valueProp": "id",
"query": {
"name": "getCategoryList",
"queryKey": "getcategorylist"
}
}
},
{
"name": "Status",
"key": "status",
"typeQueryFilter": "array",
"field": "status",
"typeFilter": "Checkboxes",
"useSubquery": false,
"sequence": 2,
"filterSectionKey": "product_info",
"dataSource": {
"type": "static",
"options": [
{ "label": "Active", "value": "active" },
{ "label": "Discontinued", "value": "discontinued" },
{ "label": "Out of Stock", "value": "out_of_stock" }
]
}
},
{
"name": "Price Range",
"key": "price",
"typeQueryFilter": "between",
"field": "price",
"typeFilter": "NumericRangeValue",
"useSubquery": false,
"sequence": 1,
"filterSectionKey": "pricing_stock"
},
{
"name": "In Stock",
"key": "in_stock",
"typeQueryFilter": "compare",
"field": "stock_quantity",
"typeFilter": "BooleanValue",
"useSubquery": false,
"sequence": 2,
"filterSectionKey": "pricing_stock"
}
],
"querySearchFields": ["name", "sku", "description"],
"userParams": {
"page": "0",
"limit": "20"
}
}

Example 3: Sales Order List with Complex Filters

{
"key": "getsalesorderlist",
"name": "getSalesOrderList",
"connector": "opensearch",
"queryTemplate": {
"actionType": "search",
"indexName": "sales_order",
"bodyParams": "{\n \"from\": {{page}},\n \"size\": {{limit}},\n \"sort\": [{\"transaction_dt\": {\"order\": \"desc\"}}]\n}"
},
"filterSections": [
{
"name": "Order Details",
"key": "order_details",
"sequence": 1
},
{
"name": "Customer Information",
"key": "customer_info",
"sequence": 2,
"collapsed": true
},
{
"name": "Payment Information",
"key": "payment_info",
"sequence": 3,
"collapsed": true
}
],
"filters": [
{
"name": "Order Number",
"key": "order_number",
"typeQueryFilter": "compare",
"field": "sales_order_number",
"typeFilter": "StringCompareValue",
"useSubquery": false,
"sequence": 1,
"filterSectionKey": "order_details"
},
{
"name": "Transaction Date",
"key": "transaction_date",
"typeQueryFilter": "between",
"field": "transaction_dt",
"typeFilter": "DateRangeValue",
"useSubquery": false,
"sequence": 2,
"filterSectionKey": "order_details"
},
{
"name": "Order Type",
"key": "order_type",
"typeQueryFilter": "array",
"field": "order_type",
"typeFilter": "MultiDropDown",
"useSubquery": false,
"sequence": 3,
"filterSectionKey": "order_details",
"dataSource": {
"type": "static",
"options": [
{ "label": "Single Sale", "value": "single_sale" },
{ "label": "Subscription", "value": "subscription" },
{ "label": "Credit Note", "value": "credit_note" }
]
}
},
{
"name": "Total Amount",
"key": "total_amount",
"typeQueryFilter": "between",
"field": "total_payable_amount",
"typeFilter": "NumericRangeValue",
"useSubquery": false,
"sequence": 4,
"filterSectionKey": "order_details"
},
{
"name": "Customer Name",
"key": "customer_name",
"typeQueryFilter": "compare",
"field": "customer.name",
"typeFilter": "StringCompareValue",
"useSubquery": false,
"sequence": 1,
"filterSectionKey": "customer_info"
},
{
"name": "Customer Email",
"key": "customer_email",
"typeQueryFilter": "compare",
"field": "customer.email",
"typeFilter": "StringCompareValue",
"useSubquery": false,
"sequence": 2,
"filterSectionKey": "customer_info"
},
{
"name": "Payment Method",
"key": "payment_method",
"typeQueryFilter": "array",
"field": "payment_method",
"typeFilter": "Checkboxes",
"useSubquery": false,
"sequence": 1,
"filterSectionKey": "payment_info",
"dataSource": {
"type": "static",
"options": [
{ "label": "Credit Card", "value": "credit_card" },
{ "label": "Direct Debit", "value": "direct_debit" },
{ "label": "Cash", "value": "cash" },
{ "label": "Bank Deposit", "value": "bank_deposit" },
{ "label": "PayPal", "value": "paypal" }
]
}
},
{
"name": "Payment Status",
"key": "payment_status",
"typeQueryFilter": "array",
"field": "payment_status",
"typeFilter": "MultiDropDown",
"useSubquery": false,
"sequence": 2,
"filterSectionKey": "payment_info",
"dataSource": {
"type": "static",
"options": [
{ "label": "Paid", "value": "paid" },
{ "label": "Unpaid", "value": "unpaid" },
{ "label": "Partial", "value": "partial" },
{ "label": "Failed", "value": "failed" }
]
}
}
],
"querySearchFields": [
"sales_order_number.keyword",
"customer_name",
"customer_email",
"receipt_number",
"payment_reference"
],
"userParams": {
"page": "0",
"limit": "25"
}
}

Integrating Filters with Screens

Using Filters in listView Screen

{
"head": {
"key": "customer_list",
"title": "Customers",
"app": "crm",
"route": "customers"
},
"screenType": "listView",
"listQuery": "getcustomerlist",
"querySearchFields": ["name", "email", "phone"],
"columnsConfiguration": [
// Column definitions
]
}

The screen will automatically:

  1. Render filters from the query definition
  2. Show search box if querySearchFields is defined
  3. Apply filters to the query when users make selections
  4. Update results in real-time

Using Filters in simpleCrud Screen

{
"head": {
"key": "product_management",
"title": "Product Management",
"app": "inventory",
"route": "products"
},
"screenType": "simpleCrud",
"entityKey": "product",
"listQuery": "getproductlist",
"detailQuery": "getproductbyid",
"columnsConfiguration": [
// Column definitions
],
"formFieldset": {
// Form configuration
}
}

The simpleCrud screen:

  1. Shows filters in the list view mode
  2. Allows filtering before selecting records to edit
  3. Maintains filter state when switching between list and form views

Best Practices

1. Filter Organization

Group related filters into sections for better UX:

"filterSections": [
{
"name": "Basic Filters",
"key": "basic",
"sequence": 1,
"collapsed": false // Keep important filters visible
},
{
"name": "Advanced Filters",
"key": "advanced",
"sequence": 2,
"collapsed": true // Collapse less-used filters
}
]

2. Filter Naming

Use clear, consistent naming:

  • ✅ "Transaction Date" (clear)
  • ❌ "Trans Dt" (abbreviated)
  • ✅ "Customer Name" (specific)
  • ❌ "Name" (ambiguous)

3. Default Values

Set sensible defaults in userParams:

"userParams": {
"page": "0",
"limit": "25",
"status": "active", // Default to active records
"date_from": "{{last30days}}" // Default date range
}

4. Performance Optimization

For large datasets:

  • Index filtered fields in the database
  • Use useSubquery: true for related entity filters
  • Limit dropdown options with intelligent queries
  • Consider using OpenSearch for complex filtering

5. Query Data Sources

Cache frequently-used filter options:

// Create a dedicated query for filter options
{
"key": "getcategoryoptions",
"name": "getCategoryOptions",
"connector": "stack9_api",
"queryTemplate": {
"method": "post",
"path": "/category/search",
"bodyParams": {
"$select": ["id", "name"],
"$where": { "is_active": true },
"$sort": { "name": "asc" },
"$limit": 100
}
}
}

Troubleshooting

Filters Not Appearing

Problem: Filters defined but not showing in UI

Solutions:

  1. Verify listQuery in screen has filters defined
  2. Check filter sequence values are unique
  3. Ensure filterSectionKey matches defined sections
  4. Verify GridBuilder component is being used

Filter Not Working

Problem: Filter appears but doesn't filter data

Solutions:

  1. Check field matches database column name
  2. Verify typeQueryFilter matches filter logic (compare/between/array)
  3. For OpenSearch, ensure field is indexed
  4. Check useSubquery setting for nested fields

Problem: Dropdown filter shows no options

Solutions:

  1. For query data source, verify query returns data
  2. Check labelProp and valueProp match query fields
  3. Ensure query has proper permissions
  4. For static options, verify JSON syntax

Search Not Working

Problem: Search box doesn't filter results

Solutions:

  1. Verify querySearchFields is defined
  2. Check fields exist in the entity/index
  3. For OpenSearch, verify fields are searchable
  4. Test with exact field values first

Advanced Topics

Custom Filter Components

For complex filtering needs, create custom filter components:

{
"name": "Custom Date Filter",
"key": "custom_date",
"typeQueryFilter": "compare",
"field": "date_field",
"typeFilter": "CustomComponentFilter",
"useSubquery": false,
"sequence": 1,
"options": {
"componentName": "CustomDateFilter",
"props": {
"format": "YYYY-MM-DD",
"showTime": true
}
}
}

Dynamic Filter Options

Load filter options based on other filter selections:

{
"name": "Subcategory",
"key": "subcategory",
"typeQueryFilter": "array",
"field": "subcategory_id",
"typeFilter": "MultiDropDown",
"useSubquery": false,
"sequence": 2,
"dataSource": {
"type": "query",
"labelProp": "name",
"valueProp": "id",
"query": {
"name": "getSubcategories",
"queryKey": "getsubcategories",
"userParams": {
"category_id": "{{filters.category}}" // Reference other filter
}
}
}
}

Filter Persistence

Filters can be persisted in URL parameters for bookmarking:

// URL structure with filters
/customers?filters[status]=active&filters[type]=business&search=acme

Integration with Pagination

Filters work seamlessly with pagination. Always include pagination parameters:

"queryParams": {
"page": "{{page}}", // Current page (0-indexed)
"limit": "{{limit}}" // Items per page
}

The framework automatically:

  1. Resets to page 0 when filters change
  2. Maintains filter state during pagination
  3. Updates total count based on filtered results

Summary

Query filters in Stack9 provide a powerful way to refine data in list views. Key takeaways:

  • Multiple filter types support different data types and use cases
  • Filter sections organize filters for better UX
  • Text search complements filters for quick searching
  • Data sources can be static, from queries, or distinct values
  • Integration is automatic with listView and simpleCrud screens
  • Performance can be optimized with proper indexing and query design

By combining different filter types, search fields, and proper organization, you can create intuitive and performant data filtering experiences for your users.

See Also