Skip to content
This repository was archived by the owner on Jun 28, 2026. It is now read-only.

feat(renderers): add SearchSelectControl with dynamic search and pagination - #601

Open
saknarajapakshe wants to merge 1 commit into
LSFLK-Archive:mainfrom
saknarajapakshe:599-add-search-select-renderer
Open

feat(renderers): add SearchSelectControl with dynamic search and pagination#601
saknarajapakshe wants to merge 1 commit into
LSFLK-Archive:mainfrom
saknarajapakshe:599-add-search-select-renderer

Conversation

@saknarajapakshe

@saknarajapakshe saknarajapakshe commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds SearchSelectControl — a JSONForms renderer for string fields that need a server-driven, searchable dropdown. Configured entirely via an x-search extension on the JSON Schema, keeping the schema self-describing.

Replaces the earlier DataSourceContext design (consumer-owned search function) with a minimal SearchContext that only provides baseUrl and getHeaders. The renderer owns all HTTP logic — URL building, fetch, response parsing, and pagination — mirroring the existing FileControl + UploadContext pattern.

Type of Change

  • New feature (non-breaking change which adds functionality)

Changes Made

  • SearchContext.tsx — new context (SearchProvider / useSearchContext) that accepts:
    • baseUrl — API origin (e.g. https://api.example.com)
    • getHeaders — async function returning auth headers (e.g. Bearer token)
  • SearchSelectControl.tsx — JSONForms renderer built on Radix UI TextField + ScrollArea:
    • Debounced text search (300 ms)
    • Three pagination modes: none (full fetch), offset, and cursor, with a "Load more" button
    • loadOnOpen flag to pre-fetch results on dropdown open
    • AbortController to cancel in-flight requests on new search or dropdown close
    • Dot-path response parsing via resolvePath for arbitrary API shapes
    • Clear button, disabled/read-only state, validation error display, schema description hint
    • useClearWhenHidden to reset value when the field is hidden by a visibility rule
    • getErrorMessage for consistent required-field error messages
    • Returns 'Search service not configured.' when no SearchProvider is mounted
  • SearchSelectControlTester.ts — ranks at priority 3; activates on string schemas carrying an x-search object
  • renderers/index.ts — registers the renderer in radixRenderers
  • src/index.ts — exports SearchProvider, useSearchContext, and associated types alongside UploadContext
  • DataSourceContext.tsx — deleted (replaced by SearchContext)

How to use in production

1. Mount SearchProvider in your app

Wrap your JSONForms tree with SearchProvider — same pattern as UploadProvider:

import { SearchProvider } from '@opennsw/jsonforms-renderers'
import { API_BASE_URL } from './constants'

function SearchWrapper({ children }: { children: ReactNode }) {
  const api = useApi()
  return (
    <SearchProvider
      baseUrl={API_BASE_URL}
      getHeaders={() => api.getAuthHeaders(false)}
    >
      {children}
    </SearchProvider>
  )
}

2. Add x-search to a schema field

{
  "type": "object",
  "properties": {
    "chaCompany": {
      "type": "string",
      "description": "Select a CHA company",
      "x-search": {
        "path": "/api/v1/companies",
        "valueKey": "id",
        "labelKey": "name",
        "pagination": "offset",
        "pageSize": 10,
        "loadOnOpen": true
      }
    }
  }
}

The renderer builds the request URL as new URL(path, baseUrl) and attaches the headers from getHeaders(). No other wiring required.

x-search options reference

Option Default Description
path — (required) API endpoint path
valueKey "id" Field used as the stored form value
labelKey "name" Field shown in the dropdown
pagination "offset" "none", "offset", or "cursor"
pageSize 5 Items per page (ignored for "none")
itemsPath "" (none) / "items" (offset, cursor) Dot-path to the array in the response body
nextCursorPath "nextCursor" Cursor pagination only
searchParam "q" Query param name for search text
limitParam "limit" Query param name for page size
offsetParam "offset" Offset pagination only
cursorParam "cursor" Cursor pagination only
loadOnOpen false Fetch immediately on dropdown open

Expected API response shapes

pagination: "none" — endpoint returns the full list; response IS the array or array at itemsPath:

[{ "id": "cha-001", "name": "Ace Customs Brokers" }]

Or with a wrapper path (itemsPath: "data"):

{ "data": [{ "id": "cha-001", "name": "Ace Customs Brokers" }] }

pagination: "offset" / "cursor" — paginated endpoint, defaults to itemsPath: "items":

{
  "items": [{ "id": "cha-001", "name": "Ace Customs Brokers" }],
  "total": 50
}

The itemsPath, valueKey, and labelKey options accept dot-notation (e.g. "data.results", "company.id") for nested response shapes.

Backend integration

Add the search endpoint to the relevant Go service and register it under app.go. The renderer calls it with the query params defined in x-search (defaults: q, limit, offset). No additional backend scaffolding is required — the renderer adapts to whatever param names and response shape the endpoint exposes via x-search config.

How to test locally

Option A — dev playground (renderer package)

cd portals/packages/jsonforms-renderers
pnpm dev

Open http://localhost:5173 and select Search Select from the sidebar. Without a SearchProvider mounted the field shows 'Search service not configured.' — wire up a provider pointing at a real or mock API to see results.

Option B — trader-app with mock backend

# Terminal 1 — mock backend (50 CHA companies on port 8080)
cd portals/apps/trader-app
pnpm mock

# Terminal 2 — trader-app
cd portals/apps/trader-app
pnpm dev

Open http://localhost:5173, log in, open a consignment — the CHA search box calls the mock backend.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • My changes generate no new warnings
  • All existing tests pass

Related Issues

Closes #599

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new 'SearchSelectControl' component for JSON Forms, which supports server-side filtering, infinite scrolling, and selection clearing, along with a 'DataSourceContext' to provide search and resolution capabilities. The code review feedback suggests storing the full selected option object in state instead of just the label to fix a hydration bug when external data changes and to prevent losing selection during filtering. Additionally, the reviewer recommends resetting the control's state when the popover is closed to ensure a clean state for subsequent uses, and simplifying the control tester by removing a redundant 'and' combinator.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread portals/packages/jsonforms-renderers/src/renderers/SearchSelectControl.tsx Outdated
Comment thread portals/packages/jsonforms-renderers/src/renderers/SearchSelectControl.tsx Outdated
Comment thread portals/packages/jsonforms-renderers/src/renderers/SearchSelectControl.tsx Outdated
Comment thread portals/packages/jsonforms-renderers/src/renderers/SearchSelectControl.tsx Outdated
Comment thread portals/packages/jsonforms-renderers/src/renderers/SearchSelectControl.tsx Outdated
@saknarajapakshe
saknarajapakshe force-pushed the 599-add-search-select-renderer branch 4 times, most recently from 05ae94d to a9dcbed Compare June 11, 2026 11:17
@sthanikan2000
sthanikan2000 marked this pull request as draft June 13, 2026 12:21
@saknarajapakshe
saknarajapakshe force-pushed the 599-add-search-select-renderer branch 4 times, most recently from 4d1df5d to 8b8143e Compare June 17, 2026 09:09
@saknarajapakshe
saknarajapakshe marked this pull request as ready for review June 17, 2026 09:10
Comment thread portals/packages/jsonforms-renderers/dev/fixtures.ts
Comment thread portals/packages/jsonforms-renderers/dev/main.tsx Outdated
@sthanikan2000

Copy link
Copy Markdown
Contributor

Heads-up: align with conventions from #604 / #605 (now merged to main)

#604 and #605 landed after this branch was cut and added two package-wide conventions to all controls that SearchSelectControl (a new file) doesn't yet follow.

Good news first: this branch merges onto main cleanly — no conflicts (dev/main.tsx auto-merges, verified with git merge-tree). But since SearchSelectControl.tsx is a new file the convention PRs never touched, git won't surface these two consistency gaps — they need to be added by hand:

1. Visibility rules aren't supported

Every control now reads visible, clears its value when hidden via the shared useClearWhenHidden hook, and returns null when hidden. SearchSelectControl doesn't destructure visible at all, so a search field won't hide or clear under a JSONForms rule while every sibling does.

import { useClearWhenHidden } from '../hooks/useClearWhenHidden'
// add to props:  visible = true,

useClearWhenHidden(visible, path, handleChange, null) // null matches onClear's handleChange(path, null)

⚠️ Placement matters. This component has the most hooks in the package (8 useState, 5 useRef, 4 useEffect, 1 useCallback). The if (visible === false) return null must go after the last hook (after the debounced-search effect, just before openDropdown) — not at the top. Adding it at the top would reintroduce the exact React Rules-of-Hooks crash that #604 had and #605 just fixed.

2. Error message uses the old pattern that #604 removed

Line 420 still uses the filter errors !== 'is a required property', which hides required-field errors — the exact behavior #604 eliminated. Switch to the shared util so required errors render as {Label} is required, consistent with every other control:

import { getErrorMessage } from '../utils/error'
...
{!isValid && (
  <Text color="red" size="1">{getErrorMessage(errors, label)}</Text>
)}

Note: the getErrorMessage import path stays ../utils/error#605 only removed it from the public src/index.ts barrel; the util file itself is unchanged.

No other files conflict with #604/#605.

@sthanikan2000

sthanikan2000 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

@saknarajapakshe There are conventions introduced in PR #604 and #605. We need to follow them. First rebase the branch. I made what changes you need to do here with the help from Claude. Can you take a look?

@saknarajapakshe
saknarajapakshe force-pushed the 599-add-search-select-renderer branch from 8b8143e to 12d9633 Compare June 18, 2026 03:28
@saknarajapakshe

Copy link
Copy Markdown
Contributor Author

@saknarajapakshe There are conventions introduced in PR #604 and #605. We need to follow them. First rebase the branch. I made what changes you need to do here with the help from Claude. Can you take a look?

Thank you for your feedback. I have rebased the branch with upstream/main and applied the conventions introduced in the last 2 PRs now. You can check it.

@saknarajapakshe
saknarajapakshe force-pushed the 599-add-search-select-renderer branch from 12d9633 to 87f8af4 Compare June 18, 2026 03:45
@saknarajapakshe
saknarajapakshe force-pushed the 599-add-search-select-renderer branch from 87f8af4 to f02e0d8 Compare June 18, 2026 10:29
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a dynamic, paginated search-select renderer to @opennsw/jsonforms-renderers

2 participants