Skip to content

Commit 626b891

Browse files
committed
Introduce CopyCard command
1 parent 4c500e5 commit 626b891

File tree

8 files changed

+162
-64
lines changed

8 files changed

+162
-64
lines changed

packages/base/command.gts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import {
22
CardDef,
33
Component,
4-
FieldDef,
54
StringField,
65
contains,
76
containsMany,
87
field,
98
linksTo,
109
linksToMany,
11-
primitive,
12-
queryableValue,
1310
} from './card-api';
1411
import CodeRefField from './code-ref';
1512
import BooleanField from './boolean';
13+
import NumberField from './number';
1614
import { SkillCard } from './skill-card';
1715
import { JsonField } from './command-result';
1816
import { SearchCardsResult } from './command-result';
@@ -24,6 +22,16 @@ export class SaveCardInput extends CardDef {
2422
@field card = linksTo(CardDef);
2523
}
2624

25+
export class CopyCardInput extends CardDef {
26+
@field sourceCard = linksTo(CardDef);
27+
@field targetRealmUrl = contains(StringField);
28+
@field targetStackIndex = contains(NumberField);
29+
}
30+
31+
export class CopyCardResult extends CardDef {
32+
@field newCard = linksTo(CardDef);
33+
}
34+
2735
export class PatchCardInput extends CardDef {
2836
@field cardId = contains(StringField);
2937
@field patch = contains(JsonField);
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { service } from '@ember/service';
2+
3+
import type * as BaseCommandModule from 'https://cardstack.com/base/command';
4+
5+
import HostBaseCommand from '../lib/host-base-command';
6+
7+
import type CardService from '../services/card-service';
8+
import type OperatorModeStateService from '../services/operator-mode-state-service';
9+
import type RealmService from '../services/realm';
10+
11+
export default class CopyCardCommand extends HostBaseCommand<
12+
BaseCommandModule.CopyCardInput,
13+
BaseCommandModule.CopyCardResult
14+
> {
15+
@service private declare cardService: CardService;
16+
@service private declare operatorModeStateService: OperatorModeStateService;
17+
@service private declare realm: RealmService;
18+
19+
description = 'Copy a card to a realm';
20+
21+
async getInputType() {
22+
let commandModule = await this.loadCommandModule();
23+
const { CopyCardInput } = commandModule;
24+
return CopyCardInput;
25+
}
26+
27+
protected async run(
28+
input: BaseCommandModule.CopyCardInput,
29+
): Promise<BaseCommandModule.CopyCardResult> {
30+
const realmUrl = await this.determineTargetRealmUrl(input);
31+
const newCard = await this.cardService.copyCard(
32+
input.sourceCard,
33+
new URL(realmUrl),
34+
);
35+
let commandModule = await this.loadCommandModule();
36+
const { CopyCardResult } = commandModule;
37+
return new CopyCardResult({ newCard });
38+
}
39+
40+
private async determineTargetRealmUrl({
41+
targetStackIndex,
42+
targetRealmUrl,
43+
}: BaseCommandModule.CopyCardInput) {
44+
if (targetRealmUrl !== undefined && targetStackIndex !== undefined) {
45+
console.warn(
46+
'Both targetStackIndex and targetRealmUrl are set; only one should be set; using targetRealmUrl',
47+
);
48+
}
49+
let realmUrl = targetRealmUrl;
50+
if (realmUrl) {
51+
return realmUrl;
52+
}
53+
if (targetStackIndex !== undefined) {
54+
// use existing card in stack to determine realm url,
55+
let topCard =
56+
this.operatorModeStateService.topMostStackItems()[targetStackIndex]
57+
?.card;
58+
if (topCard) {
59+
let url = await this.cardService.getRealmURL(topCard);
60+
// open card might be from a realm in which we don't have write permissions
61+
if (url && this.realm.canWrite(url.href)) {
62+
return url.href;
63+
}
64+
}
65+
}
66+
if (!this.realm.defaultWritableRealm) {
67+
throw new Error('Could not find a writable realm');
68+
}
69+
return this.realm.defaultWritableRealm.path;
70+
}
71+
}

packages/host/app/commands/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { VirtualNetwork } from '@cardstack/runtime-common';
22

33
import * as AddSkillsToRoomCommandModule from './add-skills-to-room';
4+
import * as CopyCardCommandModule from './copy-card';
45
import * as CreateAIAssistantRoomCommandModule from './create-ai-assistant-room';
56
import * as OpenAiAssistantRoomCommandModule from './open-ai-assistant-room';
67
import * as PatchCardCommandModule from './patch-card';
@@ -17,6 +18,10 @@ export function shimHostCommands(virtualNetwork: VirtualNetwork) {
1718
'@cardstack/boxel-host/commands/add-skills-to-room',
1819
AddSkillsToRoomCommandModule,
1920
);
21+
virtualNetwork.shimModule(
22+
'@cardstack/boxel-host/commands/copy-card',
23+
CopyCardCommandModule,
24+
);
2025
virtualNetwork.shimModule(
2126
'@cardstack/boxel-host/commands/create-ai-assistant-room',
2227
CreateAIAssistantRoomCommandModule,

packages/host/app/components/matrix/room-message-command.gts

+12-12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { ArrowLeft, Copy as CopyIcon } from '@cardstack/boxel-ui/icons';
2525

2626
import { cardTypeDisplayName, cardTypeIcon } from '@cardstack/runtime-common';
2727

28+
import CopyCardCommand from '@cardstack/host/commands/copy-card';
29+
import ShowCardCommand from '@cardstack/host/commands/show-card';
2830
import MessageCommand from '@cardstack/host/lib/matrix-classes/message-command';
2931
import type { MonacoEditorOptions } from '@cardstack/host/modifiers/monaco';
3032
import monacoModifier from '@cardstack/host/modifiers/monaco';
@@ -156,18 +158,15 @@ export default class RoomMessageCommand extends Component<Signature> {
156158
}
157159

158160
@action async copyToWorkspace() {
159-
debugger;
160-
//TODO: refactor to a command
161-
// let newCard = await this.args.context?.actions?.copyCard?.(
162-
// this.commandResultCard.card as CardDef,
163-
// );
164-
// if (!newCard) {
165-
// console.error('Could not copy card to workspace.');
166-
// return;
167-
// }
168-
// this.args.context?.actions?.viewCard(newCard, 'isolated', {
169-
// openCardInRightMostStack: true,
170-
// });
161+
let { commandContext } = this.commandService;
162+
const { newCard } = await new CopyCardCommand(commandContext).execute({
163+
sourceCard: this.commandResultCard.card as CardDef,
164+
});
165+
166+
let showCardCommand = new ShowCardCommand(commandContext);
167+
await showCardCommand.execute({
168+
cardToShow: newCard,
169+
});
171170
}
172171

173172
<template>
@@ -232,6 +231,7 @@ export default class RoomMessageCommand extends Component<Signature> {
232231
<CardContainer
233232
@displayBoundaries={{false}}
234233
class='command-result-card-preview'
234+
data-test-command-result-container
235235
>
236236
<CardHeader
237237
@cardTypeDisplayName={{this.headerTitle}}

packages/host/app/components/matrix/room-message.gts

+4-1
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,12 @@ export default class RoomMessage extends Component<Signature> {
8080
}
8181

8282
run = task(async () => {
83+
if (!this.args.message.command) {
84+
throw new Error('No command to run');
85+
}
8386
return this.commandService.run
8487
.unlinked()
85-
.perform(this.args.message.command, this.args.roomId);
88+
.perform(this.args.message.command);
8689
});
8790

8891
<template>

packages/host/app/components/operator-mode/interact-submode.gts

+18-21
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
type LooseSingleCardDocument,
3333
} from '@cardstack/runtime-common';
3434

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

@@ -420,28 +421,19 @@ export default class InteractSubmode extends Component<Signature> {
420421
}
421422

422423
private _copyCard = dropTask(
423-
async (card: CardDef, stackIndex: number, done: Deferred<CardDef>) => {
424+
async (
425+
sourceCard: CardDef,
426+
stackIndex: number,
427+
done: Deferred<CardDef>,
428+
) => {
424429
let newCard: CardDef | undefined;
425430
try {
426-
// use existing card in stack to determine realm url,
427-
// otherwise use user's first writable realm
428-
let topCard =
429-
this.operatorModeStateService.topMostStackItems()[stackIndex]?.card;
430-
let realmURL: URL | undefined;
431-
if (topCard) {
432-
let url = await this.cardService.getRealmURL(topCard);
433-
// open card might be from a realm in which we don't have write permissions
434-
if (url && this.realm.canWrite(url.href)) {
435-
realmURL = url;
436-
}
437-
}
438-
if (!realmURL) {
439-
if (!this.realm.defaultWritableRealm) {
440-
throw new Error('Could not find a writable realm');
441-
}
442-
realmURL = new URL(this.realm.defaultWritableRealm.path);
443-
}
444-
newCard = await this.cardService.copyCard(card, realmURL);
431+
let { commandContext } = this.commandService;
432+
const result = await new CopyCardCommand(commandContext).execute({
433+
sourceCard,
434+
targetStackIndex: stackIndex,
435+
});
436+
newCard = result.newCard;
445437
} catch (e) {
446438
done.reject(e);
447439
} finally {
@@ -475,7 +467,12 @@ export default class InteractSubmode extends Component<Signature> {
475467
sources.sort((a, b) => a.title.localeCompare(b.title));
476468
let scrollToCard: CardDef | undefined;
477469
for (let [index, card] of sources.entries()) {
478-
let newCard = await this.cardService.copyCard(card, realmURL);
470+
let { newCard } = await new CopyCardCommand(
471+
this.commandService.commandContext,
472+
).execute({
473+
sourceCard: card,
474+
targetRealmUrl: realmURL.href,
475+
});
479476
if (index === 0) {
480477
scrollToCard = newCard; // we scroll to the first card lexically by title
481478
}

packages/host/app/services/command-service.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export default class CommandService extends Service {
114114
}
115115

116116
//TODO: Convert to non-EC async method after fixing CS-6987
117-
run = task(async (command: MessageCommand, roomId: string) => {
117+
run = task(async (command: MessageCommand) => {
118118
let { payload, eventId } = command;
119119
let resultCard: CardDef | undefined;
120120
try {
@@ -177,7 +177,7 @@ export default class CommandService extends Service {
177177
let { SearchCardsResult } = commandModule;
178178
resultCard = new SearchCardsResult({
179179
cardIds: instances.map((c) => c.id),
180-
description: `Query: ${JSON.stringify(query)}`,
180+
description: `Query: ${JSON.stringify(query.filter, null, 2)}`,
181181
});
182182
} else if (command.name === 'generateAppModule') {
183183
let realmURL = this.operatorModeStateService.realmURL;
@@ -219,7 +219,7 @@ export default class CommandService extends Service {
219219
);
220220
}
221221
await this.matrixService.sendCommandResultEvent(
222-
roomId,
222+
command.message.roomId,
223223
eventId,
224224
resultCard,
225225
);

0 commit comments

Comments
 (0)