From 2aa6fd2020fc06295cf5baf6d50eddee6f5bad40 Mon Sep 17 00:00:00 2001 From: Burcu Noyan Date: Thu, 1 Feb 2024 11:11:38 -0500 Subject: [PATCH] Display attached card pills in sent message (#1005) * allow messages to scroll into view * use Message component to display cards and card errors * revert some changes in base room card * add snapshot for message component with error state * update usage component * distinguish footer area from sent message background * lint fix --- packages/base/room.gts | 58 +++++---- .../ai-assistant/card-picker/index.gts | 84 ++++++++----- .../components/ai-assistant/message/index.gts | 43 ++++++- .../components/ai-assistant/message/usage.gts | 4 + .../app/components/matrix/room-message.gts | 114 ++++++++++++++++++ packages/host/app/components/matrix/room.gts | 83 ++----------- packages/host/app/components/pill.gts | 2 +- .../components/operator-mode-test.gts | 1 + packages/matrix/helpers/index.ts | 12 +- packages/matrix/tests/messages.spec.ts | 6 +- 10 files changed, 262 insertions(+), 145 deletions(-) create mode 100644 packages/host/app/components/matrix/room-message.gts diff --git a/packages/base/room.gts b/packages/base/room.gts index c968a14b87..3962713569 100644 --- a/packages/base/room.gts +++ b/packages/base/room.gts @@ -19,6 +19,8 @@ import { type LooseSingleCardDocument, type CodeRef, } from '@cardstack/runtime-common'; +//@ts-expect-error cached type not available yet +import { cached } from '@glimmer/tracking'; // this is so we can have triple equals equivalent room member cards function upsertRoomMember({ @@ -172,7 +174,11 @@ class EmbeddedMessageField extends Component { data-test-message-idx={{@model.index}} data-test-message-cards > - {{#each this.attachedResources as |cardResource|}} +
+ {{@fields.message}} +
+ + {{#each @model.attachedResources as |cardResource|}} {{#if cardResource.cardError}}
Error: cannot render card @@ -197,28 +203,6 @@ class EmbeddedMessageField extends Component { - attachedCardResources: string[] | undefined = this.args.model.attachedCardIds; - - get attachedResources(): AttachedCardResource[] | undefined { - if (!this.attachedCardResources?.length) { - return undefined; - } - let cards = this.attachedCardResources.map((id) => { - let card = getCard(new URL(id)); - if (!card) { - return { - card: undefined, - cardError: { - id, - error: new Error(`cannot find card for id "${id}"`), - }, - }; - } - return card; - }); - return cards; - } - get timestamp() { if (!this.args.model.created) { throw new Error(`message created time is undefined`); @@ -231,7 +215,7 @@ type JSONValue = string | number | boolean | null | JSONObject | [JSONValue]; type JSONObject = { [x: string]: JSONValue }; -export type PatchObject = { patch: { attributes: JSONObject }; id: string }; +type PatchObject = { patch: { attributes: JSONObject }; id: string }; class PatchObjectField extends FieldDef { static [primitive]: PatchObject; @@ -260,6 +244,27 @@ export class MessageField extends FieldDef { static embedded = EmbeddedMessageField; // The edit template is meant to be read-only, this field card is not mutable static edit = class Edit extends JSONView {}; + + @cached + get attachedResources(): AttachedCardResource[] | undefined { + if (!this.attachedCardIds?.length) { + return undefined; + } + let cards = this.attachedCardIds.map((id) => { + let card = getCard(new URL(id)); + if (!card) { + return { + card: undefined, + cardError: { + id, + error: new Error(`cannot find card for id "${id}"`), + }, + }; + } + return card; + }); + return cards; + } } interface RoomState { @@ -467,7 +472,10 @@ export class RoomField extends FieldDef { if (attachedCardIds.length === 0) { throw new Error(`cannot handle cards in room without an ID`); } - messageField = new MessageField({ ...cardArgs, attachedCardIds }); + messageField = new MessageField({ + ...cardArgs, + attachedCardIds, + }); } else if (event.content.msgtype === 'org.boxel.command') { // We only handle patches for now let command = event.content.data.command; diff --git a/packages/host/app/components/ai-assistant/card-picker/index.gts b/packages/host/app/components/ai-assistant/card-picker/index.gts index 724497a26b..d2cda3f9d8 100644 --- a/packages/host/app/components/ai-assistant/card-picker/index.gts +++ b/packages/host/app/components/ai-assistant/card-picker/index.gts @@ -11,10 +11,13 @@ import { IconX } from '@cardstack/boxel-ui/icons'; import { chooseCard, baseCardRef } from '@cardstack/runtime-common'; -import Pill from '@cardstack/host/components/pill'; +import RealmInfoProvider from '@cardstack/host/components/operator-mode/realm-info-provider'; import { type CardDef } from 'https://cardstack.com/base/card-api'; +import RealmIcon from '../../operator-mode/realm-icon'; +import Pill from '../../pill'; + interface Signature { Element: HTMLDivElement; Args: { @@ -30,23 +33,41 @@ export default class AiAssistantCardPicker extends Component { @@ -186,7 +142,6 @@ export default class Room extends Component { @service private declare cardService: CardService; @service private declare matrixService: MatrixService; - @service private declare operatorModeStateService: OperatorModeStateService; @tracked private isAllowedToSetObjective: boolean | undefined; @@ -228,28 +183,6 @@ export default class Room extends Component { return undefined; } - private get messageCardComponents() { - return this.room - ? this.room.messages.map((messageCard) => { - return { - component: messageCard.constructor.getComponent( - messageCard, - 'embedded', - ), - card: messageCard, - }; - }) - : []; - } - - private patchCard = (cardId: string, attributes: any) => { - if (this.operatorModeStateService.patchCard.isRunning) { - return; - } - - this.operatorModeStateService.patchCard.perform(cardId, attributes); - }; - private doWhenRoomChanges = restartableTask(async () => { await all([this.cardService.cardsSettled(), timeout(500)]); }); diff --git a/packages/host/app/components/pill.gts b/packages/host/app/components/pill.gts index f50c199750..b236c812f7 100644 --- a/packages/host/app/components/pill.gts +++ b/packages/host/app/components/pill.gts @@ -55,7 +55,7 @@ export default class Pill extends Component { } .icon > :deep(*) { - height: 20px; + height: var(--pill-icon-size, 1.25rem); } diff --git a/packages/host/tests/integration/components/operator-mode-test.gts b/packages/host/tests/integration/components/operator-mode-test.gts index 52c7524267..f17961b475 100644 --- a/packages/host/tests/integration/components/operator-mode-test.gts +++ b/packages/host/tests/integration/components/operator-mode-test.gts @@ -916,6 +916,7 @@ module('Integration | operator-mode', function (hooks) { .containsText( 'Error: cannot render card http://this-is-not-a-real-card.com/: status: 500 - Failed to fetch.', ); + await percySnapshot(assert); }); test('it can handle an error in a room objective card', async function (assert) { diff --git a/packages/matrix/helpers/index.ts b/packages/matrix/helpers/index.ts index d887b3747a..b6fa5b5a0f 100644 --- a/packages/matrix/helpers/index.ts +++ b/packages/matrix/helpers/index.ts @@ -305,24 +305,24 @@ export async function assertMessages( cards?: { id: string; title?: string }[]; }[], ) { - await expect(page.locator('[data-test-message-index]')).toHaveCount( + await expect(page.locator('[data-test-message-idx]')).toHaveCount( messages.length, ); for (let [index, { from, message, cards }] of messages.entries()) { await expect( page.locator( - `[data-test-message-index="${index}"][data-test-boxel-message-from="${from}"]`, + `[data-test-message-idx="${index}"][data-test-boxel-message-from="${from}"]`, ), ).toHaveCount(1); if (message != null) { await expect( - page.locator(`[data-test-message-index="${index}"] .content`), + page.locator(`[data-test-message-idx="${index}"] .content`), ).toContainText(message); } if (cards?.length) { await expect( page.locator( - `[data-test-message-idx="${index}"][data-test-message-cards]`, + `[data-test-message-idx="${index}"] [data-test-message-cards]`, ), ).toHaveCount(1); await expect( @@ -340,7 +340,7 @@ export async function assertMessages( // note: attached cards are in atom format (which display the title by default) await expect( page.locator( - `[data-test-message-idx="${index}"] [data-test-message-card="${card.id}"] [data-test-card-format="atom"]`, + `[data-test-message-idx="${index}"] [data-test-message-card="${card.id}"]`, ), ).toContainText(card.title); } @@ -348,7 +348,7 @@ export async function assertMessages( } else { await expect( page.locator( - `[data-test-message-idx="${index}"][data-test-message-cards]`, + `[data-test-message-idx="${index}"] [data-test-message-cards]`, ), ).toHaveCount(0); } diff --git a/packages/matrix/tests/messages.spec.ts b/packages/matrix/tests/messages.spec.ts index bf18637867..7c831d305c 100644 --- a/packages/matrix/tests/messages.spec.ts +++ b/packages/matrix/tests/messages.spec.ts @@ -83,7 +83,7 @@ test.describe('Room messages', () => { await login(page, 'user1', 'pass'); await openRoom(page, room1); - await expect(page.locator('[data-test-message-index]')).toHaveCount( + await expect(page.locator('[data-test-message-idx]')).toHaveCount( totalMessageCount, ); }); @@ -99,7 +99,7 @@ test.describe('Room messages', () => { }, ]); await expect( - page.locator(`[data-test-message-index="0"] .content em`), + page.locator(`[data-test-message-idx="0"] .content em`), ).toContainText('style'); }); @@ -149,7 +149,7 @@ test.describe('Room messages', () => { }, ]); await expect( - page.locator(`[data-test-message-index="0"] .content em`), + page.locator(`[data-test-message-idx="0"] .content em`), ).toContainText('my'); });