diff --git a/packages/base/command.gts b/packages/base/command.gts index 397591c36b..09c6e089ea 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -1,18 +1,16 @@ import { CardDef, Component, - FieldDef, StringField, contains, containsMany, field, linksTo, linksToMany, - primitive, - queryableValue, } from './card-api'; import CodeRefField from './code-ref'; import BooleanField from './boolean'; +import NumberField from './number'; import { SkillCard } from './skill-card'; import { JsonField } from './command-result'; import { SearchCardsResult } from './command-result'; @@ -24,6 +22,16 @@ export class SaveCardInput extends CardDef { @field card = linksTo(CardDef); } +export class CopyCardInput extends CardDef { + @field sourceCard = linksTo(CardDef); + @field targetRealmUrl = contains(StringField); + @field targetStackIndex = contains(NumberField); +} + +export class CopyCardResult extends CardDef { + @field newCard = linksTo(CardDef); +} + export class PatchCardInput extends CardDef { @field cardId = contains(StringField); @field patch = contains(JsonField); diff --git a/packages/host/app/commands/copy-card.ts b/packages/host/app/commands/copy-card.ts new file mode 100644 index 0000000000..3d6028487d --- /dev/null +++ b/packages/host/app/commands/copy-card.ts @@ -0,0 +1,71 @@ +import { service } from '@ember/service'; + +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import HostBaseCommand from '../lib/host-base-command'; + +import type CardService from '../services/card-service'; +import type OperatorModeStateService from '../services/operator-mode-state-service'; +import type RealmService from '../services/realm'; + +export default class CopyCardCommand extends HostBaseCommand< + BaseCommandModule.CopyCardInput, + BaseCommandModule.CopyCardResult +> { + @service private declare cardService: CardService; + @service private declare operatorModeStateService: OperatorModeStateService; + @service private declare realm: RealmService; + + description = 'Copy a card to a realm'; + + async getInputType() { + let commandModule = await this.loadCommandModule(); + const { CopyCardInput } = commandModule; + return CopyCardInput; + } + + protected async run( + input: BaseCommandModule.CopyCardInput, + ): Promise { + const realmUrl = await this.determineTargetRealmUrl(input); + const newCard = await this.cardService.copyCard( + input.sourceCard, + new URL(realmUrl), + ); + let commandModule = await this.loadCommandModule(); + const { CopyCardResult } = commandModule; + return new CopyCardResult({ newCard }); + } + + private async determineTargetRealmUrl({ + targetStackIndex, + targetRealmUrl, + }: BaseCommandModule.CopyCardInput) { + if (targetRealmUrl !== undefined && targetStackIndex !== undefined) { + console.warn( + 'Both targetStackIndex and targetRealmUrl are set; only one should be set; using targetRealmUrl', + ); + } + let realmUrl = targetRealmUrl; + if (realmUrl) { + return realmUrl; + } + if (targetStackIndex !== undefined) { + // use existing card in stack to determine realm url, + let topCard = + this.operatorModeStateService.topMostStackItems()[targetStackIndex] + ?.card; + if (topCard) { + let url = await this.cardService.getRealmURL(topCard); + // open card might be from a realm in which we don't have write permissions + if (url && this.realm.canWrite(url.href)) { + return url.href; + } + } + } + if (!this.realm.defaultWritableRealm) { + throw new Error('Could not find a writable realm'); + } + return this.realm.defaultWritableRealm.path; + } +} diff --git a/packages/host/app/commands/index.ts b/packages/host/app/commands/index.ts index b06b3f8b69..f7c0bfa554 100644 --- a/packages/host/app/commands/index.ts +++ b/packages/host/app/commands/index.ts @@ -1,6 +1,7 @@ import { VirtualNetwork } from '@cardstack/runtime-common'; import * as AddSkillsToRoomCommandModule from './add-skills-to-room'; +import * as CopyCardCommandModule from './copy-card'; import * as CreateAIAssistantRoomCommandModule from './create-ai-assistant-room'; import * as OpenAiAssistantRoomCommandModule from './open-ai-assistant-room'; import * as PatchCardCommandModule from './patch-card'; @@ -17,6 +18,10 @@ export function shimHostCommands(virtualNetwork: VirtualNetwork) { '@cardstack/boxel-host/commands/add-skills-to-room', AddSkillsToRoomCommandModule, ); + virtualNetwork.shimModule( + '@cardstack/boxel-host/commands/copy-card', + CopyCardCommandModule, + ); virtualNetwork.shimModule( '@cardstack/boxel-host/commands/create-ai-assistant-room', CreateAIAssistantRoomCommandModule, diff --git a/packages/host/app/components/matrix/room-message-command.gts b/packages/host/app/components/matrix/room-message-command.gts index 325b3a10c7..cad424810d 100644 --- a/packages/host/app/components/matrix/room-message-command.gts +++ b/packages/host/app/components/matrix/room-message-command.gts @@ -25,6 +25,8 @@ import { ArrowLeft, Copy as CopyIcon } from '@cardstack/boxel-ui/icons'; import { cardTypeDisplayName, cardTypeIcon } from '@cardstack/runtime-common'; +import CopyCardCommand from '@cardstack/host/commands/copy-card'; +import ShowCardCommand from '@cardstack/host/commands/show-card'; import MessageCommand from '@cardstack/host/lib/matrix-classes/message-command'; import type { MonacoEditorOptions } from '@cardstack/host/modifiers/monaco'; import monacoModifier from '@cardstack/host/modifiers/monaco'; @@ -156,18 +158,15 @@ export default class RoomMessageCommand extends Component { } @action async copyToWorkspace() { - debugger; - //TODO: refactor to a command - // let newCard = await this.args.context?.actions?.copyCard?.( - // this.commandResultCard.card as CardDef, - // ); - // if (!newCard) { - // console.error('Could not copy card to workspace.'); - // return; - // } - // this.args.context?.actions?.viewCard(newCard, 'isolated', { - // openCardInRightMostStack: true, - // }); + let { commandContext } = this.commandService; + const { newCard } = await new CopyCardCommand(commandContext).execute({ + sourceCard: this.commandResultCard.card as CardDef, + }); + + let showCardCommand = new ShowCardCommand(commandContext); + await showCardCommand.execute({ + cardToShow: newCard, + }); }