CS-10380: Fetch type picker options from _federated-types endpoint#4171
CS-10380: Fetch type picker options from _federated-types endpoint#4171
Conversation
Instead of deriving type picker options from search results and recent cards, fetch all available types from the _federated-types endpoint so the picker is populated with all types across selected realms regardless of search state. Types with the same display_name but different code_refs appear as one picker option, but all associated code_refs are used when filtering search results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Preview deployments |
Host Test Results 1 files ±0 1 suites ±0 2h 31m 48s ⏱️ + 1m 4s Results for commit b645670. ± Comparison against base commit 0da90e3. This pull request removes 3 and adds 4 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
- Add built-in infinite scroll and loading indicators to the boxel-ui Picker component (isLoading, isLoadingMore, hasMore, onLoadMore args) - Full overlay loading covers options area during initial/search fetch, bottom spinner shows during load-more pagination - Simplify TypePicker by removing custom afterOptionsComponent, infiniteScroll modifier, and pickerExtra - now passes args directly - Split _typeSummariesLoading into _isLoadingTypes and _isLoadingMoreTypes for distinct loading states - Update tests for pagination (waitFor specific types) and type count Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
boxel/packages/host/app/components/card-search/panel.gts
Lines 327 to 331 in b645670
When the picker is using server-side search, this branch drops any previously selected type whose label is not present in the currently returned option set, so selecting Pet and then searching for Person clears Pet as soon as the filtered response arrives. That makes multi-select across different search terms impossible and unexpectedly reverts users back toward Any Type without an explicit deselect action.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!codeRefsByDisplayName.has(name)) { | ||
| codeRefsByDisplayName.set(name, []); | ||
| } | ||
| codeRefsByDisplayName.get(name)!.push(codeRef); |
There was a problem hiding this comment.
Merge type code refs beyond the current paged slice
The typeCodeRefs map is built only from _typeSummariesData, which is fetched in 25-item pages, so a selected display name can end up mapped to only the code refs from already-loaded pages. In multi-realm or large-type sets, cards whose same display name appears on later pages get filtered out incorrectly because their code refs were never added to this map.
Useful? React with 👍 / 👎.
|
I don’t know what best practice is for this kind of thing, I assume pagination is a reason but I find it confusing that I scroll to the bottom and see “WelcomeToBoxel” as the last item, then loading, then it’s still the last item, what were the new ones? And then after enough of that, “YouTube Thumbnail Composer” becomes the last item. Is it possible to trigger the load of them all instead of needing to scroll to the bottom? There may be constraints here I’m unaware of, it just seems confusing. |
There was a problem hiding this comment.
Pull request overview
This PR updates the card search “Type” filter to fetch options from the realm server’s /_federated-types endpoint (with server-side search + pagination) instead of deriving types from search results/recent cards, and moves the loading + infinite-scroll UX into the shared boxel-ui Picker.
Changes:
- Add
isLoading/isLoadingMore/hasMore/onLoadMore+ server-search support toboxel-uiPicker, including built-in loading UI and scroll-based pagination triggers. - Implement type summaries fetching in host card-search panel via
realmServer.fetchCardTypeSummaries()with search + pagination, and filter search results by resolved type code refs. - Update operator-mode UI tests to reflect realm-derived type options and the new loading/pagination behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/boxel-ui/addon/src/components/picker/index.gts | Adds loading + infinite-scroll behavior and server-search toggles to the shared Picker. |
| packages/host/app/components/type-picker/index.gts | Wires TypePicker to Picker’s new server-search/loading/pagination args. |
| packages/host/app/components/card-search/panel.gts | Fetches and dedupes federated type summaries; manages loading states and pagination. |
| packages/host/app/components/card-search/search-content.gts | Changes type filtering to use code refs (via internalKeyFor) rather than display-name strings. |
| packages/host/app/components/card-search/search-bar.gts | Threads new type loading/search/pagination args into TypePicker. |
| packages/host/app/services/realm-server.ts | Adds fetchCardTypeSummaries() that queries /_federated-types. |
| packages/host/tests/helpers/realm-server-mock/routes.ts | Adds mock /_federated-types endpoint for host tests. |
| packages/host/tests/integration/components/operator-mode-ui-test.gts | Updates expectations and waits for realm-derived type options. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let handleScroll = () => { | ||
| if (isLoadingMore) { | ||
| return; | ||
| } | ||
| let { scrollTop, scrollHeight, clientHeight } = optionsList as Element; | ||
| if (scrollTop + clientHeight >= scrollHeight - 50) { |
| if (!optionsList) { | ||
| return; | ||
| } | ||
|
|
||
| let handleScroll = () => { | ||
| if (isLoadingMore) { | ||
| return; | ||
| } | ||
| let { scrollTop, scrollHeight, clientHeight } = optionsList as Element; | ||
| if (scrollTop + clientHeight >= scrollHeight - 50) { | ||
| onLoadMore(); | ||
| } | ||
| }; | ||
|
|
||
| optionsList.addEventListener('scroll', handleScroll); | ||
|
|
||
| // Check immediately: if the list is short enough to fit without | ||
| // scrolling, we're already at the "bottom" and should load more. | ||
| requestAnimationFrame(() => handleScroll()); | ||
|
|
||
| return () => optionsList!.removeEventListener('scroll', handleScroll); |
| {{#if this.isLoading}} | ||
| <div class='picker-full-loading-overlay' data-test-picker-loading> | ||
| <LoadingIndicator class='picker-full-loading-spinner' /> | ||
| </div> | ||
| {{else if this.hasMore}} | ||
| <div | ||
| class='picker-infinite-scroll' | ||
| {{loadMoreSentinel | ||
| this.onLoadMore | ||
| this.isLoadingMore | ||
| enabled=this.hasMore | ||
| }} | ||
| data-test-picker-infinite-scroll | ||
| > | ||
| {{#if this.isLoadingMore}} | ||
| <div class='picker-bottom-loading' data-test-picker-loading-more> | ||
| <LoadingIndicator class='picker-loading-spinner' /> | ||
| </div> | ||
| {{/if}} | ||
| </div> | ||
| {{/if}} | ||
|
|
||
| {{! template-lint-disable require-scoped-style }} | ||
| <style> | ||
| .picker-full-loading-overlay { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| background: var(--boxel-light); | ||
| } |
| private onLoadMoreTypes() { | ||
| if ( | ||
| this._isLoadingTypes || | ||
| this._isLoadingMoreTypes || | ||
| !this._hasMoreTypes | ||
| ) { | ||
| return; | ||
| } | ||
| this._typePageNumber = this._typePageNumber + 1; | ||
| this.fetchTypeSummaries.perform( | ||
| this.selectedRealmURLs, | ||
| this._typeSearchKey, | ||
| this._typePageNumber, | ||
| true, | ||
| ); | ||
| } |
| // Open type picker dropdown | ||
| await click('[data-test-type-picker] [data-test-boxel-picker-trigger]'); | ||
| await waitFor('[data-test-boxel-picker-option-row]'); | ||
| // Wait for all pages to load via infinite scroll |

Screen.Recording.2026-03-16.at.13.11.49.mov
Summary
_federated-typesendpoint instead of deriving them from search resultsTypePickerinto the boxel-uiPickercomponentChanges
packages/boxel-ui/addon/src/components/picker/index.gtsisLoading,isLoadingMore,hasMore,onLoadMoreargs to PickerPickerAfterOptionscomponent with full overlay loading for initial/search and bottom spinner for load-morerequestAnimationFramedropdownClassadds--loadingmodifier for z-index layering (search input stays above overlay)packages/host/app/components/type-picker/index.gts@isLoading,@isLoadingMore,@hasMore,@onLoadMoredirectly to Pickerpackages/host/app/components/card-search/panel.gtsfetchTypeSummariesrestartable task callingrealmServer.fetchCardTypeSummaries()_isLoadingTypes(initial/search) and_isLoadingMoreTypes(pagination)_typeCodeRefsmap tracks multiple code refs per display name for accurate search filteringpackages/host/app/components/card-search/search-bar.gtsisLoadingTypesarg, passed through to TypePickerTests
waitForfor paginated type options in operator-mode UI testsTest plan
packages/hostintegration tests for operator-mode UI🤖 Generated with Claude Code