diff --git a/packages/host/app/components/operator-mode/create-listing-modal.gts b/packages/host/app/components/operator-mode/create-listing-modal.gts index 28229318c63..d7cb37466f4 100644 --- a/packages/host/app/components/operator-mode/create-listing-modal.gts +++ b/packages/host/app/components/operator-mode/create-listing-modal.gts @@ -47,8 +47,12 @@ export default class CreateListingModal extends Component { @tracked private selectedExamples: PickerOption[] = []; - private instancesSearch = getSearch(this, getOwner(this)!, () => - this.codeRef ? { filter: { type: this.codeRef } } : undefined, + private instancesSearch = getSearch( + this, + getOwner(this)!, + () => (this.codeRef ? { filter: { type: this.codeRef } } : undefined), + () => (this.payload?.targetRealm ? [this.payload.targetRealm] : undefined), + { isLive: true }, ); private get instances(): CardDef[] { @@ -119,6 +123,10 @@ export default class CreateListingModal extends Component { : this.initialSelected; } + private get isCreateDisabled(): boolean { + return this.createListing.isRunning || this.instancesSearch.isLoading; + } + @action private onExampleChange(selected: PickerOption[]) { this.selectedExamples = selected; } @@ -208,20 +216,22 @@ export default class CreateListingModal extends Component { - {{#if this.instanceOptions.length}} - -
- -
-
- {{/if}} + {{#unless this.instancesSearch.isLoading}} + {{#if this.instanceOptions.length}} + +
+ +
+
+ {{/if}} + {{/unless}} <:footer> @@ -249,7 +259,7 @@ export default class CreateListingModal extends Component { @kind='primary' @size='tall' @loading={{this.createListing.isRunning}} - @disabled={{this.createListing.isRunning}} + @disabled={{this.isCreateDisabled}} {{on 'click' (perform this.createListing)}} {{onKeyMod 'Enter'}} data-test-create-listing-confirm-button diff --git a/packages/host/app/resources/search.ts b/packages/host/app/resources/search.ts index 4cd9bb5f208..e9c91826ed1 100644 --- a/packages/host/app/resources/search.ts +++ b/packages/host/app/resources/search.ts @@ -163,6 +163,12 @@ export class SearchResource< } if (query === undefined) { + this.#previousQueryString = undefined; + this.#previousQuery = undefined; + for (let subscription of this.subscriptions) { + subscription.unsubscribe(); + } + this.subscriptions = []; return; } diff --git a/packages/host/tests/integration/resources/search-test.ts b/packages/host/tests/integration/resources/search-test.ts index 4ed5d520057..08114017a35 100644 --- a/packages/host/tests/integration/resources/search-test.ts +++ b/packages/host/tests/integration/resources/search-test.ts @@ -591,6 +591,135 @@ module(`Integration | search resource`, function (hooks) { ); }); + test(`setting query to undefined clears cached state and re-runs search on next query`, async function (assert) { + let query: Query | undefined = { + filter: { + on: { + module: `${testRealmURL}book`, + name: 'Book', + }, + eq: { + 'author.lastName': 'Abdel-Rahman', + }, + }, + }; + let args = { + query, + realms: [testRealmURL], + isLive: false, + isAutoSaved: false, + storeService, + owner: this.owner, + } satisfies SearchResourceArgs['named']; + + let search = getSearchResourceForTest(loaderService, () => ({ + named: args, + })); + await search.loaded; + assert.strictEqual( + search.instances.length, + 2, + 'initial search returns 2 books', + ); + + // Simulate modal close: set query to undefined + args = { ...args, query: undefined as any }; + search.modify([], args); + await settled(); + + // Simulate modal reopen with same query + args = { ...args, query }; + search.modify([], args); + await search.loaded; + + assert.strictEqual( + search.instances.length, + 2, + 'search re-runs after query was set to undefined and back', + ); + }); + + test(`setting query to undefined tears down live subscriptions`, async function (assert) { + let query: Query | undefined = { + filter: { + on: { + module: `${testRealmURL}book`, + name: 'Book', + }, + eq: { + 'author.lastName': 'Abdel-Rahman', + }, + }, + }; + let args = { + query, + realms: [testRealmURL], + isLive: true, + isAutoSaved: false, + storeService, + owner: this.owner, + } satisfies SearchResourceArgs['named']; + + let search = getSearchResourceForTest(loaderService, () => ({ + named: args, + })); + await search.loaded; + assert.strictEqual( + search.instances.length, + 2, + 'initial live search returns 2 books', + ); + + // Simulate modal close: set query to undefined + args = { ...args, query: undefined as any }; + search.modify([], args); + await settled(); + + // Write a new matching card while "modal is closed" + await realm.write( + 'books/4.json', + JSON.stringify({ + data: { + type: 'card', + attributes: { + author: { + firstName: 'New', + lastName: 'Abdel-Rahman', + }, + editions: 0, + pubDate: '2024-01-01', + }, + meta: { + adoptsFrom: { + module: `${testRealmURL}book`, + name: 'Book', + }, + }, + }, + } as LooseSingleCardDocument), + ); + + await settled(); + + // Instances should NOT have updated since subscriptions were torn down + assert.strictEqual( + search.instances.length, + 2, + 'live subscription was torn down — no update while query is undefined', + ); + + // Simulate modal reopen: restore query + args = { ...args, query }; + search.modify([], args); + await search.loaded; + + assert.strictEqual( + search.instances.length, + 3, + 'fresh search on reopen picks up the new card', + ); + }); + test(`can search for file-meta instances using SearchResource`, async function (assert) { let query: Query = { filter: {