diff --git a/packages/boxel-ui/addon/src/components/picker/index.gts b/packages/boxel-ui/addon/src/components/picker/index.gts index c96c488f5a..79360c1523 100644 --- a/packages/boxel-ui/addon/src/components/picker/index.gts +++ b/packages/boxel-ui/addon/src/components/picker/index.gts @@ -25,6 +25,7 @@ export interface PickerSignature { // Display label: string; matchTriggerWidth?: boolean; + maxSelectedDisplay?: number; onChange: (selected: PickerOption[]) => void; // Data @@ -44,6 +45,8 @@ export interface PickerSignature { export default class Picker extends Component { @tracked searchTerm = ''; + @tracked private pinnedOption: PickerOption | null = null; + @tracked private pinnedToSection: 'selected' | 'unselected' | null = null; constructor(owner: Owner, args: PickerSignature['Args']) { super(owner, args); @@ -107,22 +110,46 @@ export default class Picker extends Component { // Reorders the already-filtered options so that: // - "select-all" (search-all) options are always first - // - Selected regular options come next - // - Unselected regular options are listed last + // - Selected regular options come next (including pinned items that were in selected section) + // - Unselected regular options are listed last (including pinned items that were in unselected section) get sortedOptions(): PickerOption[] { const options = this.filteredOptions; - const selected = options.filter( - (o) => this.args.selected.includes(o) && o.type !== 'select-all', - ); - const unselected = options.filter( - (o) => !this.args.selected.includes(o) && o.type !== 'select-all', - ); + const { pinnedOption, pinnedToSection } = this; + + const selected = options.filter((o) => { + if (o.type === 'select-all') return false; + // If this is the pinned option, check which section it should stay in + if (o === pinnedOption) { + return pinnedToSection === 'selected'; + } + return this.args.selected.includes(o); + }); + + const unselected = options.filter((o) => { + if (o.type === 'select-all') return false; + // If this is the pinned option, check which section it should stay in + if (o === pinnedOption) { + return pinnedToSection === 'unselected'; + } + return !this.args.selected.includes(o); + }); + const selectAll = options.filter((o) => o.type === 'select-all'); return [...selectAll, ...selected, ...unselected]; } + private isVisuallyInSelectedSection(option: PickerOption): boolean { + if (option.type === 'select-all') return false; + if (option === this.pinnedOption) { + return this.pinnedToSection === 'selected'; + } + return this.args.selected.includes(option); + } + get selectedInSortedOptions(): PickerOption[] { - return this.sortedOptions.filter((o) => this.args.selected.includes(o)); + return this.sortedOptions.filter((o) => + this.isVisuallyInSelectedSection(o), + ); } get isSelected() { @@ -137,7 +164,7 @@ export default class Picker extends Component { get hasUnselected() { const unselected = this.sortedOptions.filter( - (o) => !this.args.selected.includes(o), + (o) => o.type !== 'select-all' && !this.isVisuallyInSelectedSection(o), ); return unselected.length > 0; } @@ -150,12 +177,26 @@ export default class Picker extends Component { this.searchTerm = term; }; + onOptionHover = (option: PickerOption | null) => { + if (option && option.type !== 'select-all') { + // Remember where the option was when hover started + this.pinnedOption = option; + this.pinnedToSection = this.args.selected.includes(option) + ? 'selected' + : 'unselected'; + } else { + this.pinnedOption = null; + this.pinnedToSection = null; + } + }; + get extra() { return { label: this.args.label, searchTerm: this.searchTerm, searchPlaceholder: this.args.searchPlaceholder, onSearchTermChange: this.onSearchTermChange, + maxSelectedDisplay: this.args.maxSelectedDisplay, }; } @@ -219,7 +260,7 @@ export default class Picker extends Component { @placeholder={{@placeholder}} @disabled={{@disabled}} @renderInPlace={{this.renderInPlace}} - @matchTriggerWidth={{true}} + @matchTriggerWidth={{@matchTriggerWidth}} @searchEnabled={{false}} @closeOnSelect={{false}} @eventType='click' @@ -235,6 +276,7 @@ export default class Picker extends Component { @option={{option}} @isSelected={{this.isSelected option}} @currentSelected={{@selected}} + @onHover={{this.onOptionHover}} /> {{#if (this.displayDivider option)}}
diff --git a/packages/boxel-ui/addon/src/components/picker/option-row.gts b/packages/boxel-ui/addon/src/components/picker/option-row.gts index ef6e789343..cdd6c74c62 100644 --- a/packages/boxel-ui/addon/src/components/picker/option-row.gts +++ b/packages/boxel-ui/addon/src/components/picker/option-row.gts @@ -1,4 +1,4 @@ -//import { on } from '@ember/modifier'; +import { on } from '@ember/modifier'; import { htmlSafe } from '@ember/template'; import Component from '@glimmer/component'; import type { Select } from 'ember-power-select/components/power-select'; @@ -13,6 +13,7 @@ export interface OptionRowSignature { Args: { currentSelected?: PickerOption[]; isSelected: boolean; + onHover?: (option: PickerOption | null) => void; option: PickerOption; select?: Select; }; @@ -20,6 +21,14 @@ export interface OptionRowSignature { } export default class PickerOptionRow extends Component { + handleMouseEnter = () => { + this.args.onHover?.(this.args.option); + }; + + handleMouseLeave = () => { + this.args.onHover?.(null); + }; + get icon() { return ( this.args.option.icon ?? @@ -56,6 +65,8 @@ export default class PickerOptionRow extends Component { class={{cn 'picker-option-row' picker-option-row--selected=@isSelected}} data-test-boxel-picker-option-selected={{if @isSelected 'true' 'false'}} data-test-boxel-picker-option-row={{@option.id}} + {{on 'mouseenter' this.handleMouseEnter}} + {{on 'mouseleave' this.handleMouseLeave}} >
0; + } + @action removeItem(item: any, event?: MouseEvent) { event?.stopPropagation(); @@ -51,6 +83,14 @@ export default class PickerLabeledTrigger extends Component {{/each}} + {{#if this.hasMoreItems}} +
+ +{{this.remainingCount}} + more +
+ {{/if}} {{/let}} {{/if}}
@@ -149,6 +201,26 @@ export default class PickerLabeledTrigger extends Component + +
+

Realm Picker (with max display limit of 2)

+ +
diff --git a/packages/boxel-ui/test-app/tests/integration/components/picker-test.gts b/packages/boxel-ui/test-app/tests/integration/components/picker-test.gts index 022b92c746..7d5eb5d1fc 100644 --- a/packages/boxel-ui/test-app/tests/integration/components/picker-test.gts +++ b/packages/boxel-ui/test-app/tests/integration/components/picker-test.gts @@ -1,6 +1,12 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'test-app/tests/helpers'; -import { click, render, waitFor, fillIn } from '@ember/test-helpers'; +import { + click, + render, + waitFor, + fillIn, + triggerEvent, +} from '@ember/test-helpers'; import { tracked } from '@glimmer/tracking'; import { Picker, type PickerOption } from '@cardstack/boxel-ui/components'; import Ember from 'ember'; @@ -490,4 +496,323 @@ module('Integration | Component | picker', function (hooks) { Ember.onerror = original; } }); + + test('picker limits displayed items when maxSelectedDisplay is set', async function (assert) { + const selected = [testOptions[0], testOptions[1], testOptions[2]]; + + await render( + , + ); + + // Only 2 items should be visible + assert.dom('[data-test-boxel-picker-selected-item]').exists({ count: 2 }); + }); + + test('picker shows +X more pill when items exceed maxSelectedDisplay', async function (assert) { + const selected = [ + testOptions[0], + testOptions[1], + testOptions[2], + testOptions[3], + ]; + + await render( + , + ); + + // 2 items + 1 more pill + assert.dom('[data-test-boxel-picker-selected-item]').exists({ count: 2 }); + assert.dom('[data-test-boxel-picker-more-items]').exists(); + assert.dom('[data-test-boxel-picker-more-items]').hasText('+2 more'); + }); + + test('picker does not show +X more pill when items do not exceed maxSelectedDisplay', async function (assert) { + const selected = [testOptions[0], testOptions[1]]; + + await render( + , + ); + + // Only 2 items should be visible, no more pill + assert.dom('[data-test-boxel-picker-selected-item]').exists({ count: 2 }); + assert.dom('[data-test-boxel-picker-more-items]').doesNotExist(); + }); + + test('clicking +X more pill opens the dropdown', async function (assert) { + const selected = [ + testOptions[0], + testOptions[1], + testOptions[2], + testOptions[3], + ]; + + await render( + , + ); + + assert.dom('[data-test-boxel-picker-more-items]').exists(); + + // Click the more items pill + await click('[data-test-boxel-picker-more-items]'); + await waitFor('[data-test-boxel-picker-option-row]'); + + // Dropdown should be open and show all options + assert.dom('[data-test-boxel-picker-option-row]').exists({ count: 5 }); + }); + + test('picker shows all items when maxSelectedDisplay is not set', async function (assert) { + const selected = [ + testOptions[0], + testOptions[1], + testOptions[2], + testOptions[3], + ]; + + await render( + , + ); + + // All 4 items should be visible + assert.dom('[data-test-boxel-picker-selected-item]').exists({ count: 4 }); + assert.dom('[data-test-boxel-picker-more-items]').doesNotExist(); + }); + + test('picker keeps unchecked item in selected section while hovering', async function (assert) { + class SelectionController { + @tracked selected: PickerOption[] = [testOptions[0], testOptions[1]]; // Option 1 and Option 2 selected + } + + const controller = new SelectionController(); + + const onChange = (newSelected: PickerOption[]) => { + controller.selected = newSelected; + }; + + await render( + , + ); + + await click('[data-test-boxel-picker-trigger]'); + await waitFor('[data-test-boxel-picker-option-row]'); + + // Get the option IDs before any interaction + const getOptionIds = () => + Array.from( + document.querySelectorAll('[data-test-boxel-picker-option-row]'), + ).map((el) => + (el as HTMLElement).getAttribute('data-test-boxel-picker-option-row'), + ); + + // Initial order: select-all, then selected items (1, 2), then unselected (3, 4) + assert.deepEqual( + getOptionIds(), + ['select-all', '1', '2', '3', '4'], + 'Initial order has selected items first', + ); + + // Hover over Option 1 (which is selected) + const option1Row = document.querySelector( + '[data-test-boxel-picker-option-row="1"]', + ) as HTMLElement; + await triggerEvent(option1Row, 'mouseenter'); + + // Click to uncheck Option 1 + await click(option1Row); + + // Option 1 should still be in the selected section while hovering + assert.deepEqual( + getOptionIds(), + ['select-all', '1', '2', '3', '4'], + 'Unchecked item stays in selected section while hovering', + ); + + // Verify Option 1 is actually unchecked (checkbox state) + assert + .dom('[data-test-boxel-picker-option-row="1"]') + .hasAttribute( + 'data-test-boxel-picker-option-selected', + 'false', + 'Option 1 checkbox shows unchecked', + ); + + // Mouse leave - item should now move to unselected section + await triggerEvent(option1Row, 'mouseleave'); + + assert.deepEqual( + getOptionIds(), + ['select-all', '2', '1', '3', '4'], + 'After mouse leave, unchecked item moves to unselected section', + ); + }); + + test('picker keeps checked item in unselected section while hovering', async function (assert) { + class SelectionController { + @tracked selected: PickerOption[] = [testOptions[0]]; // Only Option 1 selected + } + + const controller = new SelectionController(); + + const onChange = (newSelected: PickerOption[]) => { + controller.selected = newSelected; + }; + + await render( + , + ); + + await click('[data-test-boxel-picker-trigger]'); + await waitFor('[data-test-boxel-picker-option-row]'); + + const getOptionIds = () => + Array.from( + document.querySelectorAll('[data-test-boxel-picker-option-row]'), + ).map((el) => + (el as HTMLElement).getAttribute('data-test-boxel-picker-option-row'), + ); + + // Initial order: select-all, then selected item (1), then unselected (2, 3, 4) + assert.deepEqual( + getOptionIds(), + ['select-all', '1', '2', '3', '4'], + 'Initial order has selected items first', + ); + + // Hover over Option 2 (which is unselected) + const option2Row = document.querySelector( + '[data-test-boxel-picker-option-row="2"]', + ) as HTMLElement; + await triggerEvent(option2Row, 'mouseenter'); + + // Click to check Option 2 + await click(option2Row); + + // Option 2 should still be in the unselected section while hovering + assert.deepEqual( + getOptionIds(), + ['select-all', '1', '2', '3', '4'], + 'Checked item stays in unselected section while hovering', + ); + + // Verify Option 2 is actually checked (checkbox state) + assert + .dom('[data-test-boxel-picker-option-row="2"]') + .hasAttribute( + 'data-test-boxel-picker-option-selected', + 'true', + 'Option 2 checkbox shows checked', + ); + + // Mouse leave - item should now move to selected section + await triggerEvent(option2Row, 'mouseleave'); + + assert.deepEqual( + getOptionIds(), + ['select-all', '1', '2', '3', '4'], + 'After mouse leave, checked item moves to selected section', + ); + }); + + test('picker divider stays in correct position while item is pinned', async function (assert) { + class SelectionController { + @tracked selected: PickerOption[] = [testOptions[0], testOptions[1]]; // Option 1 and 2 selected + } + + const controller = new SelectionController(); + + const onChange = (newSelected: PickerOption[]) => { + controller.selected = newSelected; + }; + + await render( + , + ); + + await click('[data-test-boxel-picker-trigger]'); + await waitFor('[data-test-boxel-picker-option-row]'); + + // Hover over Option 2 (last selected item) + const option2Row = document.querySelector( + '[data-test-boxel-picker-option-row="2"]', + ) as HTMLElement; + await triggerEvent(option2Row, 'mouseenter'); + + // Click to uncheck Option 2 + await click(option2Row); + + // The divider should still appear after Option 2 (pinned in selected section) + // since it's visually the last item in the selected section + const divider = document.querySelector('[data-test-boxel-picker-divider]'); + assert.dom(divider).exists('Divider should exist while item is pinned'); + + // Mouse leave + await triggerEvent(option2Row, 'mouseleave'); + + // After unpin, divider should now be after Option 1 (only remaining selected) + const dividerAfterUnpin = document.querySelector( + '[data-test-boxel-picker-divider]', + ); + assert + .dom(dividerAfterUnpin) + .exists('Divider should still exist after unpin'); + }); }); diff --git a/packages/host/app/components/realm-picker/index.gts b/packages/host/app/components/realm-picker/index.gts new file mode 100644 index 0000000000..e4d87a4d1d --- /dev/null +++ b/packages/host/app/components/realm-picker/index.gts @@ -0,0 +1,101 @@ +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import { Picker, type PickerOption } from '@cardstack/boxel-ui/components'; + +import type RealmService from '@cardstack/host/services/realm'; +import type RealmServerService from '@cardstack/host/services/realm-server'; + +import WithKnownRealmsLoaded from '../with-known-realms-loaded'; + +const SELECT_ALL_OPTION: PickerOption = { + id: 'select-all', + name: 'All Realms', + type: 'select-all', +}; + +interface Signature { + Args: { + selected: PickerOption[]; + onChange: (selected: PickerOption[]) => void; + label?: string; + placeholder?: string; + }; + Blocks: {}; +} + +export default class RealmPicker extends Component { + @service declare realm: RealmService; + @service declare realmServer: RealmServerService; + + get realmOptions(): PickerOption[] { + const urls = this.realmServer.availableRealmURLs; + const options: PickerOption[] = [SELECT_ALL_OPTION]; + for (const realmURL of urls) { + const info = this.realm.info(realmURL); + const name = info?.name ?? this.realmDisplayNameFromURL(realmURL); + const icon = info?.iconURL ?? undefined; + options.push({ + id: realmURL, + icon, + name, + type: 'option', + }); + } + return options; + } + + private realmDisplayNameFromURL(realmURL: string): string { + try { + const pathname = new URL(realmURL).pathname; + const segments = pathname.split('/').filter(Boolean); + if (segments.length === 0) { + return 'Base'; + } + return segments[segments.length - 1] ?? 'Base'; + } catch { + return 'Unknown Workspace'; + } + } + + +} diff --git a/packages/host/app/components/search-sheet/card-query-results.gts b/packages/host/app/components/search-sheet/card-query-results.gts index f96d41be2b..26dcb2a250 100644 --- a/packages/host/app/components/search-sheet/card-query-results.gts +++ b/packages/host/app/components/search-sheet/card-query-results.gts @@ -22,6 +22,7 @@ interface Signature { searchKey: string; isCompact: boolean; handleCardSelect: (cardId: string) => void; + realms?: string[]; }; Blocks: {}; } @@ -30,8 +31,14 @@ export default class CardQueryResults extends Component { @service declare realmServer: RealmServerService; get realms() { - return this.realmServer.availableRealmURLs; + return this.args.realms ?? this.realmServer.availableRealmURLs; } + + /** Serialized for data-test-search-realms (testing realm filter) */ + get realmsForTest(): string { + return this.realms.join(','); + } + get query() { let { searchKey } = this.args; let type = getCodeRefFromSearchKey(searchKey); @@ -67,45 +74,47 @@ export default class CardQueryResults extends Component { } } diff --git a/packages/host/app/components/search-sheet/index.gts b/packages/host/app/components/search-sheet/index.gts index 794479d53c..6d06bd9deb 100644 --- a/packages/host/app/components/search-sheet/index.gts +++ b/packages/host/app/components/search-sheet/index.gts @@ -8,17 +8,16 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import onClickOutside from 'ember-click-outside/modifiers/on-click-outside'; -import { modifier } from 'ember-modifier'; import { consume } from 'ember-provide-consume-context'; import { trackedFunction } from 'reactiveweb/function'; import { Button, - BoxelInput, IconButton, BoxelInputBottomTreatments, } from '@cardstack/boxel-ui/components'; +import type { PickerOption } from '@cardstack/boxel-ui/components'; import { eq } from '@cardstack/boxel-ui/helpers'; import { IconSearch } from '@cardstack/boxel-ui/icons'; @@ -29,8 +28,8 @@ import type RealmServerService from '@cardstack/host/services/realm-server'; import CardQueryResults from './card-query-results'; import CardURLResults from './card-url-results'; - import RecentCardsSection from './recent-cards-section'; +import SearchBar from './search-bar'; import { getCodeRefFromSearchKey } from './utils'; import type StoreService from '../../services/store'; @@ -61,18 +60,11 @@ interface Signature { Blocks: {}; } -let elementCallback = modifier( - (element, [callback]: [((element: HTMLElement) => void) | undefined]) => { - if (callback) { - callback(element as HTMLElement); - } - }, -); - export default class SearchSheet extends Component { @consume(GetCardContextName) declare private getCard: getCard; @tracked private searchKey = ''; + @tracked selectedRealms: PickerOption[] = []; @service declare private realmServer: RealmServerService; @service declare private store: StoreService; @@ -144,6 +136,22 @@ export default class SearchSheet extends Component { private resetState() { this.searchKey = ''; + this.selectedRealms = []; + } + + private get selectedRealmURLs(): string[] { + const hasSelectAll = this.selectedRealms.some( + (opt) => opt.type === 'select-all', + ); + if (hasSelectAll || this.selectedRealms.length === 0) { + return this.realmServer.availableRealmURLs; + } + return this.selectedRealms.map((opt) => opt.id).filter(Boolean); + } + + @action + private onRealmChange(selected: PickerOption[]) { + this.selectedRealms = selected; } @action private debouncedSetSearchKey(searchKey: string) { @@ -226,7 +234,10 @@ export default class SearchSheet extends Component { id='search-sheet' class='search-sheet {{this.sheetSize}}' data-test-search-sheet={{@mode}} - {{onClickOutside @onBlur exceptSelector='.add-card-to-neighbor-stack'}} + {{onClickOutside + @onBlur + exceptSelector='.add-card-to-neighbor-stack,.boxel-picker__dropdown,.picker-before-options-with-search,.picker-option-row' + }} > {{#if (eq @mode 'closed')}} { data-test-open-search-field /> {{else}} -
{{#if this.searchKeyIsURL}} @@ -268,6 +278,7 @@ export default class SearchSheet extends Component { {{else}} @@ -326,11 +337,13 @@ export default class SearchSheet extends Component { overflow: hidden; } .search-sheet:not(.closed):deep(.input-container), - .search-sheet:not(.closed):deep(.search-sheet__search-input-group) { + .search-sheet:not(.closed):deep(.search-sheet__search-input-group), + .search-sheet:not(.closed):deep(.search-sheet__search-bar) { border-radius: inherit; } - .search-sheet__search-input-group { + .search-sheet__search-input-group, + .search-sheet__search-bar { transition: height var(--boxel-transition), width var(--boxel-transition); diff --git a/packages/host/app/components/search-sheet/search-bar.gts b/packages/host/app/components/search-sheet/search-bar.gts new file mode 100644 index 0000000000..d4663da0ad --- /dev/null +++ b/packages/host/app/components/search-sheet/search-bar.gts @@ -0,0 +1,196 @@ +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; + +import { modifier } from 'ember-modifier'; + +import { + BoxelInput, + type BoxelInputBottomTreatments, +} from '@cardstack/boxel-ui/components'; +import type { PickerOption } from '@cardstack/boxel-ui/components'; +import { IconSearch } from '@cardstack/boxel-ui/icons'; + +import RealmPicker from '@cardstack/host/components/realm-picker'; + +let elementCallback = modifier( + (element, [callback]: [((element: HTMLElement) => void) | undefined]) => { + if (callback) { + callback(element as HTMLElement); + } + }, +); + +interface Signature { + Element: HTMLElement; + Args: { + value: string; + placeholder?: string; + onInput?: (value: string) => void; + onFocus?: (ev: Event) => void; + onBlur?: (ev: Event) => void; + onKeyDown?: (ev: KeyboardEvent) => void; + onInputInsertion?: (element: HTMLElement) => void; + selectedRealms: PickerOption[]; + onRealmChange: (selected: PickerOption[]) => void; + bottomTreatment?: BoxelInputBottomTreatments; + state?: 'none' | 'valid' | 'invalid' | 'loading' | 'initial'; + id?: string; + }; + Blocks: {}; +} + +export default class SearchBar extends Component { + @action + handleKeyDown(ev: KeyboardEvent): void { + this.args.onKeyDown?.(ev); + } + + +} diff --git a/packages/host/tests/integration/components/operator-mode-ui-test.gts b/packages/host/tests/integration/components/operator-mode-ui-test.gts index 7febc126ed..613ed534b0 100644 --- a/packages/host/tests/integration/components/operator-mode-ui-test.gts +++ b/packages/host/tests/integration/components/operator-mode-ui-test.gts @@ -390,6 +390,73 @@ module('Integration | operator-mode | ui', function (hooks) { assert.dom(`[data-test-search-sheet="closed"]`).exists(); }); + test('search sheet shows realm picker when expanded and filters by selected realm', async function (assert) { + ctx.setCardInOperatorModeState(`${testRealmURL}grid`); + await renderComponent( + class TestDriver extends GlimmerComponent { + + }, + ); + await waitFor(`[data-test-stack-card="${testRealmURL}grid"]`); + await click(`[data-test-boxel-filter-list-button="All Cards"]`); + await waitFor(`[data-test-cards-grid-item]`); + + await click(`[data-test-open-search-field]`); + assert.dom(`[data-test-search-sheet="search-prompt"]`).exists(); + assert.dom('[data-test-search-sheet-search-bar]').exists(); + assert.dom('[data-test-realm-picker]').exists(); + + // Type a search term to trigger search results + await typeIn('[data-test-search-field]', 'Person'); + await click('[data-test-search-sheet] .search-sheet-content'); + await waitFor('[data-test-search-label]', { timeout: 8000 }); + await waitFor('[data-test-search-realms]', { timeout: 3000 }); + + // Helper function to get current selected realms + const getSelectedRealms = () => { + const attr = document + .querySelector('[data-test-search-realms]') + ?.getAttribute('data-test-search-realms'); + return attr?.split(',').map((r) => r.trim()) ?? []; + }; + + // Verify initial realm filtering includes test realm + let selectedRealms = getSelectedRealms(); + assert.ok( + selectedRealms.some( + (r) => r.includes('test-realm') && r.includes('/test'), + ), + 'search should initially include the test realm', + ); + + // Select only the test realm in the picker to verify filter updates + const trigger = + document.querySelector( + '[data-test-realm-picker] .ember-power-select-trigger', + ) ?? document.querySelector('[data-test-realm-picker]'); + await click(trigger as HTMLElement); + await waitFor('.ember-power-select-option', { timeout: 3000 }); + + const options = document.querySelectorAll('.ember-power-select-option'); + const testRealmOption = Array.from(options).find((el) => + el.textContent?.includes(ctx.realmName), + ); + assert.ok(testRealmOption, `option for "${ctx.realmName}" should exist`); + await click(testRealmOption as HTMLElement); + + // Verify the filter was applied + await waitUntil(() => getSelectedRealms().includes(testRealmURL), { + timeout: 5000, + }); + selectedRealms = getSelectedRealms(); + assert.ok( + selectedRealms.some( + (r) => r.includes('test-realm') && r.includes('/test'), + ), + 'search should be filtered to the test realm after selection', + ); + }); + test('displays card in interact mode when clicking `Open in Interact Mode` menu in preview panel', async function (assert) { ctx.setCardInOperatorModeState(`${testRealmURL}grid`);