Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/base/command.gts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/base/menu-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -155,7 +155,7 @@ export function getDefaultCardMenuItems(
});
menuItems = [...menuItems, ...getSampleDataMenuItems(card, params)];
menuItems.push({
label: `Create Listing with AI`,
label: `Create Listing`,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

action: async () => {
const codeRef = resolveAdoptsFrom(card);
if (!codeRef) {
Expand All @@ -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,
});
},
Expand Down
3 changes: 2 additions & 1 deletion packages/catalog-realm/catalog-app/listing/listing.gts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ class EmbeddedTemplate extends Component<typeof Listing> {
export class Listing extends CardDef {
static displayName = 'Listing';
static headerColor = '#6638ff';
static isListingDef = true;

@field name = contains(StringField);
@field summary = contains(MarkdownField);
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions packages/host/app/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -404,6 +409,7 @@ export const HostCommandClasses: (typeof HostBaseCommand<any, any>)[] = [
ListingUseCommandModule.default,
OneShotLlmRequestCommandModule.default,
OpenAiAssistantRoomCommandModule.default,
OpenCreateListingModalCommandModule.default,
OpenInInteractModeModule.default,
OpenWorkspaceCommandModule.default,
GenerateThemeExampleCommandModule.default,
Expand Down
103 changes: 53 additions & 50 deletions packages/host/app/commands/listing-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ export default class ListingCreateCommand extends HostBaseCommand<
protected async run(
input: BaseCommandModule.ListingCreateInput,
): Promise<BaseCommandModule.ListingCreateResult> {
const cardAPI = await this.loadCardAPI();
let { openCardId, codeRef, targetRealm } = input;
let { openCardIds, codeRef, targetRealm } = input;

if (!codeRef) {
throw new Error('codeRef is required');
Expand All @@ -127,14 +126,17 @@ export default class ListingCreateCommand extends HostBaseCommand<

let listingType = await this.guessListingType(codeRef);

let relationships: Record<string, { links: { self: string } }> = {};
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`,
Expand All @@ -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(
Expand Down Expand Up @@ -254,7 +253,9 @@ export default class ListingCreateCommand extends HostBaseCommand<
targetRealm: string,
resourceUrl: string, // can be module or card instance id
): Promise<Spec[]> {
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 },
});
Expand Down Expand Up @@ -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[])
Expand All @@ -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<CardAPI.CardDef>(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<CardAPI.CardDef>(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,
Expand All @@ -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) {
Expand All @@ -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());
Expand All @@ -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,
});
}
Expand Down
33 changes: 33 additions & 0 deletions packages/host/app/commands/open-create-listing-modal.ts
Original file line number Diff line number Diff line change
@@ -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<undefined> {
this.operatorModeStateService.showCreateListingModal({
codeRef: input.codeRef,
targetRealm: input.targetRealm,
openCardIds: input.openCardIds,
});
}
}
2 changes: 2 additions & 0 deletions packages/host/app/components/operator-mode/container.gts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -143,6 +144,7 @@ export default class OperatorModeContainer extends Component<Signature> {
<template>
<div class='operator-mode'>
<ChooseFileModal />
<CreateListingModal />
<CardCatalogModal />
<FromElseWhere @name='modal-elsewhere' />

Expand Down
Loading
Loading