Skip to content

Commit

Permalink
Introduce CopyCard command
Browse files Browse the repository at this point in the history
  • Loading branch information
lukemelia committed Jan 2, 2025
1 parent 4c500e5 commit 504856a
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 67 deletions.
14 changes: 11 additions & 3 deletions packages/base/command.gts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down
71 changes: 71 additions & 0 deletions packages/host/app/commands/copy-card.ts
Original file line number Diff line number Diff line change
@@ -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<BaseCommandModule.CopyCardResult> {
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;
}
}
5 changes: 5 additions & 0 deletions packages/host/app/commands/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down
24 changes: 12 additions & 12 deletions packages/host/app/components/matrix/room-message-command.gts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -156,18 +158,15 @@ export default class RoomMessageCommand extends Component<Signature> {
}

@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,
});
}

<template>
Expand Down Expand Up @@ -232,6 +231,7 @@ export default class RoomMessageCommand extends Component<Signature> {
<CardContainer
@displayBoundaries={{false}}
class='command-result-card-preview'
data-test-command-result-container
>
<CardHeader
@cardTypeDisplayName={{this.headerTitle}}
Expand Down
5 changes: 4 additions & 1 deletion packages/host/app/components/matrix/room-message.gts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@ export default class RoomMessage extends Component<Signature> {
}

run = task(async () => {
if (!this.args.message.command) {
throw new Error('No command to run');
}
return this.commandService.run
.unlinked()
.perform(this.args.message.command, this.args.roomId);
.perform(this.args.message.command);
});

<template>
Expand Down
39 changes: 18 additions & 21 deletions packages/host/app/components/operator-mode/interact-submode.gts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type LooseSingleCardDocument,
} from '@cardstack/runtime-common';

import CopyCardCommand from '@cardstack/host/commands/copy-card';
import config from '@cardstack/host/config/environment';
import { StackItem, isIndexCard } from '@cardstack/host/lib/stack-item';

Expand Down Expand Up @@ -420,28 +421,19 @@ export default class InteractSubmode extends Component<Signature> {
}

private _copyCard = dropTask(
async (card: CardDef, stackIndex: number, done: Deferred<CardDef>) => {
async (
sourceCard: CardDef,
stackIndex: number,
done: Deferred<CardDef>,
) => {
let newCard: CardDef | undefined;
try {
// use existing card in stack to determine realm url,
// otherwise use user's first writable realm
let topCard =
this.operatorModeStateService.topMostStackItems()[stackIndex]?.card;
let realmURL: URL | undefined;
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)) {
realmURL = url;
}
}
if (!realmURL) {
if (!this.realm.defaultWritableRealm) {
throw new Error('Could not find a writable realm');
}
realmURL = new URL(this.realm.defaultWritableRealm.path);
}
newCard = await this.cardService.copyCard(card, realmURL);
let { commandContext } = this.commandService;
const result = await new CopyCardCommand(commandContext).execute({
sourceCard,
targetStackIndex: stackIndex,
});
newCard = result.newCard;
} catch (e) {
done.reject(e);
} finally {
Expand Down Expand Up @@ -475,7 +467,12 @@ export default class InteractSubmode extends Component<Signature> {
sources.sort((a, b) => a.title.localeCompare(b.title));
let scrollToCard: CardDef | undefined;
for (let [index, card] of sources.entries()) {
let newCard = await this.cardService.copyCard(card, realmURL);
let { newCard } = await new CopyCardCommand(
this.commandService.commandContext,
).execute({
sourceCard: card,
targetRealmUrl: realmURL.href,
});
if (index === 0) {
scrollToCard = newCard; // we scroll to the first card lexically by title
}
Expand Down
6 changes: 3 additions & 3 deletions packages/host/app/services/command-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default class CommandService extends Service {
}

//TODO: Convert to non-EC async method after fixing CS-6987
run = task(async (command: MessageCommand, roomId: string) => {
run = task(async (command: MessageCommand) => {
let { payload, eventId } = command;
let resultCard: CardDef | undefined;
try {
Expand Down Expand Up @@ -177,7 +177,7 @@ export default class CommandService extends Service {
let { SearchCardsResult } = commandModule;
resultCard = new SearchCardsResult({
cardIds: instances.map((c) => c.id),
description: `Query: ${JSON.stringify(query)}`,
description: `Query: ${JSON.stringify(query.filter, null, 2)}`,
});
} else if (command.name === 'generateAppModule') {
let realmURL = this.operatorModeStateService.realmURL;
Expand Down Expand Up @@ -219,7 +219,7 @@ export default class CommandService extends Service {
);
}
await this.matrixService.sendCommandResultEvent(
roomId,
command.message.roomId,
eventId,
resultCard,
);
Expand Down
Loading

0 comments on commit 504856a

Please sign in to comment.