Skip to content

CS-10802: Refactor SearchPanel – extract utilities and simplify args#4418

Open
FadhlanR wants to merge 5 commits intomainfrom
refactor-search-panel-args
Open

CS-10802: Refactor SearchPanel – extract utilities and simplify args#4418
FadhlanR wants to merge 5 commits intomainfrom
refactor-search-panel-args

Conversation

@FadhlanR
Copy link
Copy Markdown
Contributor

Summary

  • Simplify SearchPanel args to use domain types (URL[], ResolvedCodeRef[]) instead of internal concerns (PickerOption[], booleans)
  • Extract TypeSummariesResource from SearchPanel; introduce RealmFilter/TypeFilter interfaces
  • Move all card-search utilities to app/utils/card-search/ (7 focused files: url, query-builder, type-filter, recent-cards, sections, section-pagination, types)
  • Deduplicate URL parsing logic shared between search-sheet and search-content into isURLSearchKey()/resolveSearchKeyAsURL()
  • Slim SearchContent from 736 to ~500 lines by extracting section building, pagination, and filtering into pure helpers

Test plan

  • Existing integration tests pass (card-catalog, operator-mode-ui)
  • Card catalog modal: realm/type pickers, search, pagination, create-new all work
  • Search sheet: search, realm filtering, type filtering, recent cards, URL lookup all work
  • "Show Only" focus/unfocus on sections works
  • "Show More" pagination within sections works

Linear: https://linear.app/cardstack/issue/CS-10802

🤖 Generated with Claude Code

FadhlanR and others added 3 commits April 16, 2026 15:31
…cerns

Replace cluttered args (consumingRealm, preselectConsumingRealm,
availableRealmUrls, onFilterChange) with cleaner domain-typed
alternatives (initialSelectedRealms, initialSelectedTypes,
onRealmChange, onTypeChange) so callers deal with URL[] and
ResolvedCodeRef[] instead of PickerOption[] and booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace SearchPanel's internal-leaking args with domain types
  (URL[], ResolvedCodeRef[] instead of PickerOption[], booleans)
- Extract TypeSummariesResource from SearchPanel
- Introduce RealmFilter/TypeFilter interfaces to bundle picker args
- Move utilities to app/utils/card-search/ (7 focused files)
- Deduplicate URL parsing into shared isURLSearchKey/resolveSearchKeyAsURL
- Extract section building, pagination, type filtering, recent cards
  filtering into pure helper functions
- Slim SearchContent from 736 to ~500 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 16, 2026

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 16, 2026

Realm Server Test Results

  1 files  ±0    1 suites  ±0   13m 57s ⏱️ -17s
894 tests ±0  894 ✅ ±0  0 💤 ±0  0 ❌ ±0 
966 runs  ±0  966 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit bf6d5d0. ± Comparison against base commit e3f0866.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 16, 2026

Host Test Results

2 246 tests  ±0   2 231 ✅ ±0   2h 32m 29s ⏱️ - 1m 3s
    1 suites ±0      15 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit bf6d5d0. ± Comparison against base commit e3f0866.

♻️ This comment has been updated with latest results.

@FadhlanR FadhlanR marked this pull request as ready for review April 16, 2026 10:40
@FadhlanR FadhlanR requested a review from a team April 16, 2026 10:41
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf6d5d0ea6

ℹ️ 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".

Comment on lines +200 to +202
if (!result) {
this._isLoading = false;
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale type filter state on failed summaries fetch

If fetchCardTypeSummaries returns undefined (network error, auth/login race, or canFetch false), this branch exits without resetting _typeSummariesData/_options/_selected. After a realm change, the picker can continue showing types from the previous realm and keep an invalid selection applied to subsequent queries, which is a behavior regression from the previous implementation that cleared state on fetch failure.

Useful? React with 👍 / 👎.

Comment on lines +93 to +97
if (realmURLsKey !== prevKey) {
this.#previousRealmURLs = realmURLs;
this._typeSearchKey = '';
this.fetchTypeSummariesTask.perform(realmURLs, '');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recompute type selection when initial type args change

modify() stores new baseFilter/initialSelectedTypes on every update, but it only triggers a recompute path when realmURLs changes. If SearchSheet calls doExternallyTriggeredSearch() while the sheet is already open (same realms, new typeRef), the new initial type is ignored and the previous type selection remains active, so the externally requested type-constrained search does not take effect.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the card search UI by simplifying SearchPanel’s public API to domain types, extracting type-summary fetching into a dedicated resource, and moving search logic into focused utilities.

Changes:

  • Replaced SearchPanel picker/boolean args with domain-level URL[] / ResolvedCodeRef[] and RealmFilter / TypeFilter interfaces.
  • Extracted type summary fetching/pagination into TypeSummariesResource, and moved search helpers (URL parsing, type filtering, section building, recents logic) into app/utils/card-search/*.
  • Updated host/operator-mode UI + Boxel Picker to display checkbox selection state and adjusted integration tests accordingly.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/host/tests/integration/components/operator-mode-ui-test.gts Updates integration assertions to use picker checkbox-selected UI + new TypeSummariesResource.PAGE_SIZE.
packages/host/app/utils/card-search/url.ts Adds shared URL/empty-search helpers for search key parsing.
packages/host/app/utils/card-search/types.ts Introduces domain types (NewCardArgs) and shared removeFileExtension.
packages/host/app/utils/card-search/type-filter.ts Extracts filter/type-ref logic (root type handling, subtype checks, type-ref filtering).
packages/host/app/utils/card-search/sections.ts Extracts section data types + builders (recents/url/query sections) and assembly ordering.
packages/host/app/utils/card-search/section-pagination.ts Extracts section focus + “show more” pagination into a tracked helper class.
packages/host/app/utils/card-search/recent-cards.ts Extracts recent-card filtering + sorting logic (including type constraints).
packages/host/app/utils/card-search/query-builder.ts Removes duplicated helpers and reuses new URL/empty helpers; keeps query construction centralized.
packages/host/app/services/realm-server.ts Adds canFetch guard used by type summaries resource to avoid early fetch attempts.
packages/host/app/services/operator-mode-state-service.ts Repoints removeFileExtension import to new utils location.
packages/host/app/resources/type-summaries.ts New resource for fetching/paginating type summaries and maintaining selected types.
packages/host/app/components/type-picker/index.gts Switches to TypeFilter interface and maps domain options/selection to Picker options.
packages/host/app/components/search-sheet/index.gts Uses shared URL helpers and new SearchPanel args/events (onRealmChange/onTypeChange).
packages/host/app/components/realm-picker/index.gts Switches to RealmFilter interface and maps selected URLs to Picker options.
packages/host/app/components/operator-mode/operator-mode-overlays.gts Repoints removeFileExtension import to new utils location.
packages/host/app/components/card-search/section-header.gts Imports RealmSectionInfo from extracted sections util types.
packages/host/app/components/card-search/search-result-section.gts Imports section + NewCardArgs types from extracted utils.
packages/host/app/components/card-search/search-result-header.gts Imports NewCardArgs from extracted utils.
packages/host/app/components/card-search/search-content.gts Large refactor: uses extracted helpers/resources; replaces internal pagination/section building.
packages/host/app/components/card-search/search-bar.gts Simplifies args to accept realmFilter / typeFilter.
packages/host/app/components/card-search/panel.gts Major simplification: delegates type summaries to resource and yields domain filter objects.
packages/host/app/components/card-search/item-button.gts Repoints removeFileExtension/NewCardArgs imports to new utils.
packages/host/app/components/card-search/constants.ts Adds clarification about dual-mode sorting + recent-card sorting helper.
packages/host/app/components/card-catalog/modal.gts Updates SearchPanel usage for new args, and derives initial selected realms.
packages/boxel-ui/test-app/tests/integration/components/picker-test.gts Adds coverage for checkbox selected-state rendering in picker dropdown.
packages/boxel-ui/addon/src/components/picker/index.gts Changes selected-state logic to use id equality instead of object identity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +53 to 61
return selected.map((url) => {
let realmURL = url.href;
let info = this.realm.info(realmURL);
let label = info?.name ?? this.realmDisplayNameFromURL(realmURL);
let icon = info?.iconURL ?? undefined;
return { id: realmURL, icon, label, type: 'option' as const };
});
}

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

pickerSelected builds new PickerOption objects instead of reusing the option instances from realmOptions. Because the underlying PowerSelect selection logic relies on object identity, this can break highlighting/toggling (e.g., clicking a selected realm may add a duplicate instead of deselecting, and selected options may not be recognized by PowerSelect). Build @selected by looking up the corresponding objects from the same @options array (and consider caching realmOptions so @options/@selected share stable references).

Suggested change
return selected.map((url) => {
let realmURL = url.href;
let info = this.realm.info(realmURL);
let label = info?.name ?? this.realmDisplayNameFromURL(realmURL);
let icon = info?.iconURL ?? undefined;
return { id: realmURL, icon, label, type: 'option' as const };
});
}
let optionsById = new Map(
this.realmOptions.map((option) => [option.id, option] as const),
);
return selected.flatMap((url) => {
let option = optionsById.get(url.href);
return option ? [option] : [];
});
}
@cached

Copilot uses AI. Check for mistakes.
Comment on lines +54 to 75
private get pickerOptions(): PickerOption[] {
const options: PickerOption[] = this.args.filter.options.map((opt) => ({
id: opt.id,
label: opt.displayName,
tooltip: opt.id,
icon: opt.icon,
type: 'option' as const,
}));
return [this.selectAllOption, ...options];
}

get selected(): PickerOption[] {
return this.args.selected.length > 0
? this.args.selected
: [this.selectAllOption];
private get pickerSelected(): PickerOption[] {
const selectedKeys = new Set(
this.args.filter.selected.map((ref) => internalKeyFor(ref, undefined)),
);
if (selectedKeys.size === 0) {
return [this.selectAllOption];
}
return this.pickerOptions.filter(
(opt) => opt.type !== 'select-all' && selectedKeys.has(opt.id),
);
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

pickerOptions and pickerSelected each compute fresh PickerOption object arrays. In the template, @options={{this.pickerOptions}} and @selected={{this.pickerSelected}} will therefore often contain different object instances for the same ids, which can break PowerSelect’s identity-based selection/toggle behavior (selected items may not deselect, duplicates can appear). Cache pickerOptions (e.g., via @cached) and derive pickerSelected by selecting references from that cached array (or otherwise ensure @selected is composed of the exact objects found in @options).

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +96

this.#baseFilter = baseFilter;
this.#initialSelectedTypes = initialSelectedTypes;

// Only re-fetch when realmURLs change (search key changes are handled by onSearchChange)
let realmURLsKey = realmURLs.join(',');
let prevKey = this.#previousRealmURLs?.join(',');

if (realmURLsKey !== prevKey) {
this.#previousRealmURLs = realmURLs;
this._typeSearchKey = '';
this.fetchTypeSummariesTask.perform(realmURLs, '');
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

modify() updates #baseFilter / #initialSelectedTypes but only triggers work when realmURLs changes. That means changes to baseFilter (and potentially initialSelectedTypes) won’t update _options / _selected until the next realm change/search fetch, which can leave the UI showing stale type options while hasNonRootBaseFilter has already flipped. Consider calling recomputeTypeFilter() when these inputs change (and/or tracking previous baseFilter/initialSelectedTypes so you can recompute without refetching).

Suggested change
this.#baseFilter = baseFilter;
this.#initialSelectedTypes = initialSelectedTypes;
// Only re-fetch when realmURLs change (search key changes are handled by onSearchChange)
let realmURLsKey = realmURLs.join(',');
let prevKey = this.#previousRealmURLs?.join(',');
if (realmURLsKey !== prevKey) {
this.#previousRealmURLs = realmURLs;
this._typeSearchKey = '';
this.fetchTypeSummariesTask.perform(realmURLs, '');
let previousBaseFilter = this.#baseFilter;
let previousInitialSelectedTypes = this.#initialSelectedTypes;
this.#baseFilter = baseFilter;
this.#initialSelectedTypes = initialSelectedTypes;
// Only re-fetch when realmURLs change (search key changes are handled by onSearchChange)
let realmURLsKey = realmURLs.join(',');
let prevKey = this.#previousRealmURLs?.join(',');
let didBaseFilterChange = previousBaseFilter !== baseFilter;
let didInitialSelectedTypesChange =
previousInitialSelectedTypes !== initialSelectedTypes;
if (realmURLsKey !== prevKey) {
this.#previousRealmURLs = realmURLs;
this._typeSearchKey = '';
this.fetchTypeSummariesTask.perform(realmURLs, '');
} else if (didBaseFilterChange || didInitialSelectedTypesChange) {
this.recomputeTypeFilter();

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@lukemelia lukemelia left a comment

Choose a reason for hiding this comment

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

Nice cleanup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants