diff --git a/packages/base/command.gts b/packages/base/command.gts index 4d206d63cb2..8df32a10cf3 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -389,7 +389,7 @@ export class CreateListingPRRequestInput extends CardDef { } export class ListingCreateInput extends CardDef { - @field openCardId = contains(StringField); + @field openCardIds = containsMany(StringField); @field codeRef = contains(CodeRefField); @field targetRealm = contains(RealmField); } diff --git a/packages/base/menu-items.ts b/packages/base/menu-items.ts index d62e9819ece..efe10ce45d9 100644 --- a/packages/base/menu-items.ts +++ b/packages/base/menu-items.ts @@ -6,7 +6,7 @@ import { import CopyCardCommand from '@cardstack/boxel-host/commands/copy-card'; import GenerateExampleCardsCommand from '@cardstack/boxel-host/commands/generate-example-cards'; -import ListingCreateCommand from '@cardstack/boxel-host/commands/listing-create'; +import OpenCreateListingModalCommand from '@cardstack/boxel-host/commands/open-create-listing-modal'; import OpenInInteractModeCommand from '@cardstack/boxel-host/commands/open-in-interact-mode'; import PopulateWithSampleDataCommand from '@cardstack/boxel-host/commands/populate-with-sample-data'; import ShowCardCommand from '@cardstack/boxel-host/commands/show-card'; @@ -155,7 +155,7 @@ export function getDefaultCardMenuItems( }); menuItems = [...menuItems, ...getSampleDataMenuItems(card, params)]; menuItems.push({ - label: `Create Listing with AI`, + label: `Create Listing`, action: async () => { const codeRef = resolveAdoptsFrom(card); if (!codeRef) { @@ -165,9 +165,9 @@ export function getDefaultCardMenuItems( if (!targetRealm) { throw new Error('Unable to determine target realm from card'); } - await new ListingCreateCommand(params.commandContext).execute({ - openCardId: cardId, + await new OpenCreateListingModalCommand(params.commandContext).execute({ codeRef, + openCardIds: [cardId], targetRealm, }); }, diff --git a/packages/catalog-realm/catalog-app/listing/listing.gts b/packages/catalog-realm/catalog-app/listing/listing.gts index 90d0b733264..58f0e0f43cb 100644 --- a/packages/catalog-realm/catalog-app/listing/listing.gts +++ b/packages/catalog-realm/catalog-app/listing/listing.gts @@ -564,6 +564,7 @@ class EmbeddedTemplate extends Component { export class Listing extends CardDef { static displayName = 'Listing'; static headerColor = '#6638ff'; + static isListingDef = true; @field name = contains(StringField); @field summary = contains(MarkdownField); @@ -641,7 +642,7 @@ export class Listing extends CardDef { [getMenuItems](params: GetMenuItemParams): MenuItemOptions[] { let menuItems = super [getMenuItems](params) - .filter((item) => item.label?.toLowerCase() !== 'create listing with ai'); + .filter((item) => item.label?.toLowerCase() !== 'create listing'); if (params.menuContext === 'interact') { const extra = this.getGenerateExampleMenuItem(params); if (extra) { diff --git a/packages/host/app/commands/index.ts b/packages/host/app/commands/index.ts index 4fd424e63da..d125fdf950d 100644 --- a/packages/host/app/commands/index.ts +++ b/packages/host/app/commands/index.ts @@ -37,6 +37,7 @@ import * as ListingUpdateSpecsCommandModule from './listing-update-specs'; import * as ListingUseCommandModule from './listing-use'; import * as OneShotLlmRequestCommandModule from './one-shot-llm-request'; import * as OpenAiAssistantRoomCommandModule from './open-ai-assistant-room'; +import * as OpenCreateListingModalCommandModule from './open-create-listing-modal'; import * as OpenInInteractModeModule from './open-in-interact-mode'; import * as OpenWorkspaceCommandModule from './open-workspace'; import * as PatchCardInstanceCommandModule from './patch-card-instance'; @@ -256,6 +257,10 @@ export function shimHostCommands(virtualNetwork: VirtualNetwork) { '@cardstack/boxel-host/commands/open-ai-assistant-room', OpenAiAssistantRoomCommandModule, ); + virtualNetwork.shimModule( + '@cardstack/boxel-host/commands/open-create-listing-modal', + OpenCreateListingModalCommandModule, + ); virtualNetwork.shimModule( '@cardstack/boxel-host/commands/open-workspace', OpenWorkspaceCommandModule, @@ -404,6 +409,7 @@ export const HostCommandClasses: (typeof HostBaseCommand)[] = [ ListingUseCommandModule.default, OneShotLlmRequestCommandModule.default, OpenAiAssistantRoomCommandModule.default, + OpenCreateListingModalCommandModule.default, OpenInInteractModeModule.default, OpenWorkspaceCommandModule.default, GenerateThemeExampleCommandModule.default, diff --git a/packages/host/app/commands/listing-create.ts b/packages/host/app/commands/listing-create.ts index ce94c134ece..4b3eb2d4daa 100644 --- a/packages/host/app/commands/listing-create.ts +++ b/packages/host/app/commands/listing-create.ts @@ -112,8 +112,7 @@ export default class ListingCreateCommand extends HostBaseCommand< protected async run( input: BaseCommandModule.ListingCreateInput, ): Promise { - const cardAPI = await this.loadCardAPI(); - let { openCardId, codeRef, targetRealm } = input; + let { openCardIds, codeRef, targetRealm } = input; if (!codeRef) { throw new Error('codeRef is required'); @@ -127,14 +126,17 @@ export default class ListingCreateCommand extends HostBaseCommand< let listingType = await this.guessListingType(codeRef); + let relationships: Record = {}; + if (openCardIds && openCardIds.length > 0) { + openCardIds.forEach((id, index) => { + relationships[`examples.${index}`] = { links: { self: id } }; + }); + } + const listingDoc: LooseSingleCardDocument = { data: { type: 'card', - relationships: openCardId - ? { - 'examples.0': { links: { self: openCardId } }, - } - : {}, + relationships, meta: { adoptsFrom: { module: `${this.catalogRealm}catalog-app/listing/listing`, @@ -144,34 +146,31 @@ export default class ListingCreateCommand extends HostBaseCommand< }, }; const listing = await this.store.add(listingDoc, { realm: targetRealm }); - // Always use the transient symbol-based localId; ignore any persisted id at this stage - const listingId = (listing as any)[(cardAPI as any).localId]; - if (!listingId) { - throw new Error('Failed to create listing card (no localId)'); - } - await this.operatorModeStateService.openCardInInteractMode(listingId); const commandModule = await this.loadCommandModule(); - const listingCard = listing as CardAPI.CardDef; // ensure correct type - const specsPromise = this.linkSpecs( - listingCard, - targetRealm, - openCardId ?? codeRef?.module, - ); + const listingCard = listing as CardAPI.CardDef; + const firstOpenCardId = openCardIds?.[0]; - const promises = [ + const backgroundWork = Promise.all([ this.autoPatchName(listingCard, codeRef), this.autoPatchSummary(listingCard, codeRef), this.autoLinkTag(listingCard), this.autoLinkCategory(listingCard), this.autoLinkLicense(listingCard), - this.autoLinkExample(listingCard, codeRef, openCardId), - specsPromise, - ]; + this.autoLinkExample(listingCard, codeRef, openCardIds), + this.linkSpecs( + listingCard, + targetRealm, + firstOpenCardId ?? codeRef?.module, + ), + ]).catch((error) => { + console.warn('Background autopatch failed:', error); + }); - await Promise.all(promises); const { ListingCreateResult } = commandModule; - return new ListingCreateResult({ listing }); + const result = new ListingCreateResult({ listing }); + (result as any).backgroundWork = backgroundWork; + return result; } private async guessListingType( @@ -254,7 +253,9 @@ export default class ListingCreateCommand extends HostBaseCommand< targetRealm: string, resourceUrl: string, // can be module or card instance id ): Promise { - const url = `${targetRealm}_dependencies?url=${encodeURIComponent(resourceUrl)}`; + const resourceRealm = + this.realm.realmOfURL(new URL(resourceUrl))?.href ?? targetRealm; + const url = `${resourceRealm}_dependencies?url=${encodeURIComponent(resourceUrl)}`; const response = await this.network.authedFetch(url, { headers: { Accept: SupportedMimeType.JSONAPI }, }); @@ -376,7 +377,7 @@ export default class ListingCreateCommand extends HostBaseCommand< private async autoLinkExample( listing: CardAPI.CardDef, codeRef: ResolvedCodeRef, - openCardId?: string, + openCardIds?: string[], ) { const existingExamples = Array.isArray((listing as any).examples) ? ((listing as any).examples as CardAPI.CardDef[]) @@ -396,25 +397,29 @@ export default class ListingCreateCommand extends HostBaseCommand< addCard(existing); } - let exampleCard: CardAPI.CardDef | undefined; - if (openCardId) { - try { - const instance = await this.store.get(openCardId); - if (isCardInstance(instance)) { - exampleCard = instance as CardAPI.CardDef; - } else { - console.warn('autoLinkExample: openCardId is not a card instance', { - openCardId, - }); - } - } catch (error) { - console.warn('autoLinkExample: failed to load openCardId', { - openCardId, - error, - }); - } + if (openCardIds && openCardIds.length > 0) { + await Promise.all( + openCardIds.map(async (openCardId) => { + try { + const instance = await this.store.get(openCardId); + if (isCardInstance(instance)) { + addCard(instance as CardAPI.CardDef); + } else { + console.warn( + 'autoLinkExample: openCardId is not a card instance', + { openCardId }, + ); + } + } catch (error) { + console.warn('autoLinkExample: failed to load openCardId', { + openCardId, + error, + }); + } + }), + ); } else { - // If no openCardId was provided, attempt to find any existing instance of this type. + // If no openCardIds were provided, attempt to find any existing instance of this type. try { const search = new SearchCardsByTypeAndTitleCommand( this.commandContext, @@ -426,7 +431,7 @@ export default class ListingCreateCommand extends HostBaseCommand< (c: any) => c && typeof c.id === 'string' && isCardInstance(c), ); if (first) { - exampleCard = first as CardAPI.CardDef; + addCard(first as CardAPI.CardDef); } } } catch (error) { @@ -437,10 +442,8 @@ export default class ListingCreateCommand extends HostBaseCommand< } } - addCard(exampleCard); - const MAX_EXAMPLES = 4; - if (codeRef && exampleCard && uniqueById.size < MAX_EXAMPLES) { + if (codeRef && uniqueById.size > 0 && uniqueById.size < MAX_EXAMPLES) { try { const searchAndChoose = new SearchAndChooseCommand(this.commandContext); const existingIds = Array.from(uniqueById.keys()); @@ -462,7 +465,7 @@ export default class ListingCreateCommand extends HostBaseCommand< } } catch (error) { console.warn('Failed to auto-link additional examples', { - sourceCardId: exampleCard.id, + codeRef, error, }); } diff --git a/packages/host/app/commands/open-create-listing-modal.ts b/packages/host/app/commands/open-create-listing-modal.ts new file mode 100644 index 00000000000..ef1f4b0d1ca --- /dev/null +++ b/packages/host/app/commands/open-create-listing-modal.ts @@ -0,0 +1,33 @@ +import { service } from '@ember/service'; + +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import HostBaseCommand from '../lib/host-base-command'; + +import type OperatorModeStateService from '../services/operator-mode-state-service'; + +export default class OpenCreateListingModalCommand extends HostBaseCommand< + typeof BaseCommandModule.ListingCreateInput +> { + @service declare private operatorModeStateService: OperatorModeStateService; + + description = 'Open create listing confirmation modal'; + + async getInputType() { + let commandModule = await this.loadCommandModule(); + const { ListingCreateInput } = commandModule; + return ListingCreateInput; + } + + requireInputFields = ['codeRef', 'targetRealm']; + + protected async run( + input: BaseCommandModule.ListingCreateInput, + ): Promise { + this.operatorModeStateService.showCreateListingModal({ + codeRef: input.codeRef, + targetRealm: input.targetRealm, + openCardIds: input.openCardIds, + }); + } +} diff --git a/packages/host/app/components/operator-mode/container.gts b/packages/host/app/components/operator-mode/container.gts index 45653d4104c..b514090ff56 100644 --- a/packages/host/app/components/operator-mode/container.gts +++ b/packages/host/app/components/operator-mode/container.gts @@ -41,6 +41,7 @@ import PrerenderedCardSearch from '../prerendered-card-search'; import { Submodes } from '../submode-switcher'; import ChooseFileModal from './choose-file-modal'; +import CreateListingModal from './create-listing-modal'; import type CardService from '../../services/card-service'; import type CommandService from '../../services/command-service'; @@ -143,6 +144,7 @@ export default class OperatorModeContainer extends Component {