From 37dd924f83071356ed1596a50af538836ae42108 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Fri, 22 Nov 2024 13:12:13 -0500 Subject: [PATCH 01/11] Add last known good rendering of card in error state to stack item --- .../operator-mode/card-error-detail.gts | 14 ++ .../components/operator-mode/code-submode.gts | 115 ++++++------- .../operator-mode/interact-submode.gts | 6 + .../components/operator-mode/stack-item.gts | 102 ++++++++++-- packages/host/app/lib/stack-item.ts | 2 +- packages/host/app/resources/card-resource.ts | 75 +++++++-- .../host/app/resources/stack-backgrounds.ts | 3 + packages/host/package.json | 2 + .../components/operator-mode-test.gts | 152 +++++++++++++++++- pnpm-lock.yaml | 10 ++ 10 files changed, 398 insertions(+), 83 deletions(-) create mode 100644 packages/host/app/components/operator-mode/card-error-detail.gts diff --git a/packages/host/app/components/operator-mode/card-error-detail.gts b/packages/host/app/components/operator-mode/card-error-detail.gts new file mode 100644 index 0000000000..f275a13870 --- /dev/null +++ b/packages/host/app/components/operator-mode/card-error-detail.gts @@ -0,0 +1,14 @@ +import Component from '@glimmer/component'; + +import { type CardError } from '../../resources/card-resource'; + +interface Signature { + Args: { + error: CardError['errors'][0]; + }; +} +export default class CardErrorDetail extends Component { + +} diff --git a/packages/host/app/components/operator-mode/code-submode.gts b/packages/host/app/components/operator-mode/code-submode.gts index 6a3056f7b1..0871d39eb1 100644 --- a/packages/host/app/components/operator-mode/code-submode.gts +++ b/packages/host/app/components/operator-mode/code-submode.gts @@ -32,7 +32,7 @@ import { type ResolvedCodeRef, PermissionsContextName, } from '@cardstack/runtime-common'; -import { SerializedError } from '@cardstack/runtime-common/error'; +// import { SerializedError } from '@cardstack/runtime-common/error'; import { isEquivalentBodyPosition } from '@cardstack/runtime-common/schema-analysis-plugin'; import RecentFiles from '@cardstack/host/components/editor/recent-files'; @@ -311,56 +311,57 @@ export default class CodeSubmode extends Component { return null; } - private get fileErrorMessages(): string[] { - if (this.isCard) { - if (this.cardResource.cardError) { - try { - let error = this.cardResource.cardError.error; - - if (error.responseText) { - let parsedError = JSON.parse(error.responseText); - - // handle instance errors - if (parsedError.errors.find((e: any) => e.message)) { - return parsedError.errors.map((e: any) => e.message); - } - - // otherwise handle module errors - let allDetails = parsedError.errors - .concat( - ...parsedError.errors.map( - (e: SerializedError) => e.additionalErrors, - ), - ) - .map((e: SerializedError) => e.detail); - - // There’s often a pair of errors where one has an unhelpful prefix like this: - // cannot return card from index: Not Found - http://test-realm/test/non-card not found - // http://test-realm/test/non-card not found - - let detailsWithoutDuplicateSuffixes = allDetails.reduce( - (details: string[], currentDetail: string) => { - return [ - ...details.filter( - (existingDetail) => !existingDetail.endsWith(currentDetail), - ), - currentDetail, - ]; - }, - [], - ); - - return detailsWithoutDuplicateSuffixes; - } - } catch (e) { - console.log('Error extracting card preview errors', e); - return []; - } - } - } - - return []; - } + // private get fileErrorMessages(): string[] { + // if (this.isCard) { + // if (this.cardResource.cardError) { + // try { + // return this.card + // let error = this.cardResource.cardError; + + // // if (error.responseText) { + // // let parsedError = JSON.parse(error.responseText); + + // // handle instance errors + // if (parsedError.errors.find((e: any) => e.message)) { + // return parsedError.errors.map((e: any) => e.message); + // } + + // // otherwise handle module errors + // let allDetails = parsedError.errors + // .concat( + // ...parsedError.errors.map( + // (e: SerializedError) => e.additionalErrors, + // ), + // ) + // .map((e: SerializedError) => e.detail); + + // // There’s often a pair of errors where one has an unhelpful prefix like this: + // // cannot return card from index: Not Found - http://test-realm/test/non-card not found + // // http://test-realm/test/non-card not found + + // let detailsWithoutDuplicateSuffixes = allDetails.reduce( + // (details: string[], currentDetail: string) => { + // return [ + // ...details.filter( + // (existingDetail) => !existingDetail.endsWith(currentDetail), + // ), + // currentDetail, + // ]; + // }, + // [], + // ); + + // return detailsWithoutDuplicateSuffixes; + // } + // } catch (e) { + // console.log('Error extracting card preview errors', e); + // return []; + // } + // } + // } + + // return []; + // } private get currentOpenFile() { return this.operatorModeStateService.openFile.current; @@ -854,12 +855,12 @@ export default class CodeSubmode extends Component {
- {{#each this.fileErrorMessages as |error|}} -
{{error}}
- {{/each}} + {{!-- {{#each this.fileErrorMessages as |error|}} --}} +
{{this.cardResource.cardError.message}}
+ {{!-- {{/each}} --}} {{else if this.fileIncompatibilityMessage}} diff --git a/packages/host/app/components/operator-mode/interact-submode.gts b/packages/host/app/components/operator-mode/interact-submode.gts index b833617603..ef02e19f72 100644 --- a/packages/host/app/components/operator-mode/interact-submode.gts +++ b/packages/host/app/components/operator-mode/interact-submode.gts @@ -341,6 +341,12 @@ export default class InteractSubmode extends Component { } private close = task(async (item: StackItem) => { + // TODO test that a card error stack item can be closed + if (item.cardError) { + this.operatorModeStateService.trimItemsFromStack(item); + return; + } + let { card, request } = item; // close the item first so user doesn't have to wait for the save to complete this.operatorModeStateService.trimItemsFromStack(item); diff --git a/packages/host/app/components/operator-mode/stack-item.gts b/packages/host/app/components/operator-mode/stack-item.gts index de579fcce8..79544d070b 100644 --- a/packages/host/app/components/operator-mode/stack-item.gts +++ b/packages/host/app/components/operator-mode/stack-item.gts @@ -35,7 +35,7 @@ import { import { MenuItem, getContrastColor } from '@cardstack/boxel-ui/helpers'; import { cssVar, optional } from '@cardstack/boxel-ui/helpers'; -import { IconTrash, IconLink } from '@cardstack/boxel-ui/icons'; +import { IconTrash, IconLink, Warning } from '@cardstack/boxel-ui/icons'; import { type Actions, @@ -59,13 +59,17 @@ import type { FieldType, } from 'https://cardstack.com/base/card-api'; +import { htmlComponent } from '../../lib/html-component'; import ElementTracker from '../../resources/element-tracker'; import Preview from '../preview'; +import CardErrorDetail from './card-error-detail'; + import OperatorModeOverlays from './overlays'; import type CardService from '../../services/card-service'; import type EnvironmentService from '../../services/environment-service'; +import type LoaderService from '../../services/loader-service'; import type OperatorModeStateService from '../../services/operator-mode-state-service'; import type RealmService from '../../services/realm'; @@ -116,6 +120,7 @@ export default class OperatorModeStackItem extends Component { @service private declare environmentService: EnvironmentService; @service private declare operatorModeStateService: OperatorModeStateService; @service private declare realm: RealmService; + @service private declare loaderService: LoaderService; // @tracked private selectedCards = new TrackedArray([]); @tracked private selectedCards = new TrackedArray([]); @@ -319,20 +324,59 @@ export default class OperatorModeStackItem extends Component { return this.args.item.card; } + @cached + get cardError() { + return this.args.item.cardError; + } + + @cached + get lastKnownGoodHtml() { + if (this.cardError?.meta.lastKnownGoodHtml) { + this.loadScopedCSS.perform(); + return htmlComponent(this.cardError.meta.lastKnownGoodHtml); + } + return undefined; + } + + @cached + get cardErrorTitle() { + if (!this.cardError) { + return undefined; + } + return this.cardError.status === 404 && + // a missing link error looks a lot like a missing card error + this.cardError.message.includes('missing') + ? `Card Error: Link Not Found` + : `Card Error: ${this.cardError.title}`; + } + private loadCard = restartableTask(async () => { await this.args.item.ready(); }); + private loadScopedCSS = restartableTask(async () => { + if (this.cardError?.meta.scopedCssUrls) { + await Promise.all( + this.cardError.meta.scopedCssUrls.map((cssModuleUrl) => + this.loaderService.loader.import(cssModuleUrl), + ), + ); + } + }); + private subscribeToCard = task(async () => { await this.args.item.ready(); - this.subscribedCard = this.card; - let api = this.args.item.api; - registerDestructor(this, this.cleanup); - api.subscribeToChanges(this.subscribedCard, this.onCardChange); - this.refreshSaveMsg = setInterval( - () => this.calculateLastSavedMsg(), - 10 * 1000, - ) as unknown as number; + // TODO how do we make sure that this is called after the error is cleared? + if (!this.cardError) { + this.subscribedCard = this.card; + let api = this.args.item.api; + registerDestructor(this, this.cleanup); + api.subscribeToChanges(this.subscribedCard, this.onCardChange); + this.refreshSaveMsg = setInterval( + () => this.calculateLastSavedMsg(), + 10 * 1000, + ) as unknown as number; + } }); private cleanup = () => { @@ -530,6 +574,40 @@ export default class OperatorModeStackItem extends Component { Loading card... + {{else if this.cardError}} + +
+ {{! TODO show stock error message when no last known good HTML !}} + {{#if this.lastKnownGoodHtml}} + + {{/if}} + +
{{else}} {{#let (this.realm.info this.card.id) as |realmInfo|}} { justify: center; align-items: center; } + .card-error { + opacity: 0.4; + border-radius: 0; + box-shadow: none; + overflow: auto; + } } diff --git a/packages/host/app/lib/stack-item.ts b/packages/host/app/lib/stack-item.ts index fbcfacd1ce..c41e2ce09d 100644 --- a/packages/host/app/lib/stack-item.ts +++ b/packages/host/app/lib/stack-item.ts @@ -93,7 +93,7 @@ export class StackItem { } get cardError() { - return this.cardResource?.cardError?.error; + return this.cardResource?.cardError; } get isWideFormat() { diff --git a/packages/host/app/resources/card-resource.ts b/packages/host/app/resources/card-resource.ts index a5e3b2ee70..a9d9f79ad8 100644 --- a/packages/host/app/resources/card-resource.ts +++ b/packages/host/app/resources/card-resource.ts @@ -10,6 +10,8 @@ import { restartableTask } from 'ember-concurrency'; import { task } from 'ember-concurrency'; import { Resource } from 'ember-resources'; +import status from 'statuses'; + import { Loader, isSingleCardDocument, @@ -18,8 +20,6 @@ import { hasExecutableExtension, } from '@cardstack/runtime-common'; -import { ErrorDetails } from '@cardstack/runtime-common/error'; - import type MessageService from '@cardstack/host/services/message-service'; import type { CardDef } from 'https://cardstack.com/base/card-api'; @@ -29,9 +29,18 @@ import type * as CardAPI from 'https://cardstack.com/base/card-api'; import type CardService from '../services/card-service'; import type LoaderService from '../services/loader-service'; -interface CardError { - id: string; - error: ErrorDetails; +export interface CardError { + errors: { + id: string; + status: number; + title: string; + message: string; + meta: { + lastKnownGoodHtml: string | null; + scopedCssUrls: string[]; + stack: string | null; + }; + }[]; } interface Args { @@ -71,7 +80,7 @@ const realmSubscriptions: Map< export class CardResource extends Resource { url: string | undefined; @tracked loaded: Promise | undefined; - @tracked cardError: CardError | undefined; + @tracked cardError: CardError['errors'][0] | undefined; @tracked private _card: CardDef | undefined; @tracked private _api: typeof CardAPI | undefined; @tracked private staleCard: CardDef | undefined; @@ -167,10 +176,7 @@ export class CardResource extends Resource { let card = await this.getCard(url); if (!card) { if (this.cardError) { - console.warn( - `cannot load card ${this.cardError.id}`, - this.cardError.error, - ); + console.warn(`cannot load card ${this.cardError.id}`, this.cardError); } this.clearCardInstance(); return; @@ -262,14 +268,55 @@ export class CardResource extends Resource { ); return card; } catch (error: any) { - this.cardError = { - id: url.href, - error, - }; + let errorResponse: CardError; + try { + errorResponse = JSON.parse(error.responseText) as CardError; + } catch (parseError) { + switch (error.status) { + // tailor HTTP responses as necessary for better user feedback + case 404: + errorResponse = { + errors: [ + { + id: url.href, + status: 404, + title: 'Card Not Found', + message: `The card ${url.href} does not exist`, + meta: { + lastKnownGoodHtml: null, + scopedCssUrls: [], + stack: null, + }, + }, + ], + }; + break; + default: + errorResponse = { + errors: [ + { + id: url.href, + status: error.status, + title: status.message[error.status] ?? `HTTP ${error.status}`, + message: `Received HTTP ${error.status} from server ${ + error.responseText ?? '' + }`.trim(), + meta: { + lastKnownGoodHtml: null, + scopedCssUrls: [], + stack: null, + }, + }, + ], + }; + } + } + this.cardError = errorResponse.errors[0]; return; } } + // TODO deal with live update of card that goes into and out of an error state private reload = task(async (card: CardDef) => { try { await this.cardService.reloadCard(card); diff --git a/packages/host/app/resources/stack-backgrounds.ts b/packages/host/app/resources/stack-backgrounds.ts index 6dbad1eba6..a877929fa8 100644 --- a/packages/host/app/resources/stack-backgrounds.ts +++ b/packages/host/app/resources/stack-backgrounds.ts @@ -46,6 +46,9 @@ export class StackBackgroundsResource extends Resource { } let bottomMostStackItem = stack[0]; await bottomMostStackItem.ready; + if (bottomMostStackItem.cardError) { + return undefined; + } return (await this.cardService.getRealmInfo(bottomMostStackItem.card)) ?.backgroundURL; }), diff --git a/packages/host/package.json b/packages/host/package.json index d9f60793da..4a26cc9ec1 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -71,6 +71,7 @@ "@types/qs": "^6.9.17", "@types/qunit": "^2.11.3", "@types/rsvp": "^4.0.9", + "@types/statuses": "^2.0.5", "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/parser": "^7.9.0", @@ -164,6 +165,7 @@ "qunit-dom": "^2.0.0", "safe-stable-stringify": "^2.4.3", "start-server-and-test": "^1.14.0", + "statuses": "^2.0.1", "stream-browserify": "^3.0.0", "super-fast-md5": "^1.0.1", "testem": "3.10.1", diff --git a/packages/host/tests/integration/components/operator-mode-test.gts b/packages/host/tests/integration/components/operator-mode-test.gts index 015410a01a..38114b7ea2 100644 --- a/packages/host/tests/integration/components/operator-mode-test.gts +++ b/packages/host/tests/integration/components/operator-mode-test.gts @@ -17,7 +17,12 @@ import { module, test } from 'qunit'; import { FieldContainer } from '@cardstack/boxel-ui/components'; -import { baseRealm, Deferred, Realm } from '@cardstack/runtime-common'; +import { + baseRealm, + Deferred, + LooseSingleCardDocument, + Realm, +} from '@cardstack/runtime-common'; import { Loader } from '@cardstack/runtime-common/loader'; import CardPrerender from '@cardstack/host/components/card-prerender'; @@ -100,6 +105,29 @@ module('Integration | operator-mode', function (hooks) { let { CardsGrid } = cardsGrid; let { CatalogEntry } = catalogEntry; + // use string source so we can get the transpiled scoped CSS + let friendWithCSSSource = ` + import { Component, field, contains, linksTo, CardDef, StringField } from 'https://cardstack.com/base/card-api'; + export class FriendWithCSS extends CardDef { + static displayName = 'Friend'; + @field friend = linksTo(() => FriendWithCSS); + static isolated = class Isolated extends Component { + + }; + } + `; + class Pet extends CardDef { static displayName = 'Pet'; @field name = contains(StringField); @@ -353,6 +381,7 @@ module('Integration | operator-mode', function (hooks) { 'blog-post.gts': { BlogPost }, 'author.gts': { Author }, 'friend.gts': { Friend }, + 'friend-with-css.gts': friendWithCSSSource, 'publishing-packet.gts': { PublishingPacket }, 'pet-room.gts': { PetRoom }, 'Pet/mango.json': petMango, @@ -381,6 +410,59 @@ module('Integration | operator-mode', function (hooks) { name: 'Friend A', friend: friendB, }), + 'FriendWithCSS/friend-b.json': { + data: { + attributes: { + title: 'Jade', + }, + meta: { + adoptsFrom: { + module: '../friend-with-css.gts', + name: 'FriendWithCSS', + }, + }, + }, + } as LooseSingleCardDocument, + 'FriendWithCSS/friend-a.json': { + data: { + attributes: { + title: 'Hassan', + }, + relationships: { + friend: { + links: { + self: './friend-b', + }, + }, + }, + meta: { + adoptsFrom: { + module: '../friend-with-css.gts', + name: 'FriendWithCSS', + }, + }, + }, + } as LooseSingleCardDocument, + 'FriendWithCSS/missing-link.json': { + data: { + attributes: { + title: 'Boris', + }, + relationships: { + friend: { + links: { + self: './does-not-exist', + }, + }, + }, + meta: { + adoptsFrom: { + module: '../friend-with-css.gts', + name: 'FriendWithCSS', + }, + }, + }, + } as LooseSingleCardDocument, 'grid.json': new CardsGrid(), 'CatalogEntry/publishing-packet.json': new CatalogEntry({ title: 'Publishing Packet', @@ -465,6 +547,72 @@ module('Integration | operator-mode', function (hooks) { assert.dom('[data-test-stack-card-index="1"]').includesText('Mango'); }); + test('it renders a card with an error that has does not have a last known good state', async function (assert) { + await setCardInOperatorModeState( + `${testRealmURL}FriendWithCSS/missing-link`, + ); + await renderComponent( + class TestDriver extends GlimmerComponent { + + }, + ); + + assert + .dom('[data-test-boxel-card-header-title]') + .includesText('Link Not Found', 'card error title is displayed'); + }); + + test('it renders a card with an error that has a last known good state', async function (assert) { + await testRealm.write( + 'FriendWithCSS/friend-a.json', + JSON.stringify({ + data: { + type: 'card', + attributes: { + name: 'Friend A', + }, + relationships: { + friend: { + links: { + self: './does-not-exist', + }, + }, + }, + meta: { + adoptsFrom: { + module: '../friend-with-css.gts', + name: 'FriendWithCSS', + }, + }, + }, + } as LooseSingleCardDocument), + ); + await setCardInOperatorModeState(`${testRealmURL}FriendWithCSS/friend-a`); + await renderComponent( + class TestDriver extends GlimmerComponent { + + }, + ); + + assert + .dom('[data-test-boxel-card-header-title]') + .includesText('Link Not Found', 'card error title is displayed'); + assert + .dom('[data-test-card-error]') + .includesText( + 'Hassan has a friend Jade', + 'the last known good HTML is rendered', + ); + // use percy snapshot to ensure the CSS has been applied--a red color + await percySnapshot(assert); + }); + test('it auto saves the field value', async function (assert) { assert.expect(7); await setCardInOperatorModeState(`${testRealmURL}Person/fadhlan`); @@ -579,7 +727,7 @@ module('Integration | operator-mode', function (hooks) { assert.dom('[data-test-pet]').includesText('Paper Bad cat!'); }); - test('opens workspace chooser after closing the only remainingcard on the stack', async function (assert) { + test('opens workspace chooser after closing the only remaining card on the stack', async function (assert) { await setCardInOperatorModeState(`${testRealmURL}Person/fadhlan`); await renderComponent( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee7c1bb03c..1838713fc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1353,6 +1353,9 @@ importers: '@types/rsvp': specifier: ^4.0.9 version: 4.0.9 + '@types/statuses': + specifier: ^2.0.5 + version: 2.0.5 '@types/uuid': specifier: ^9.0.8 version: 9.0.8 @@ -1632,6 +1635,9 @@ importers: start-server-and-test: specifier: ^1.14.0 version: 1.14.0 + statuses: + specifier: ^2.0.1 + version: 2.0.1 stream-browserify: specifier: ^3.0.0 version: 3.0.0 @@ -7392,6 +7398,10 @@ packages: resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} dev: true + /@types/statuses@2.0.5: + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + dev: true + /@types/stream-chain@2.0.1: resolution: {integrity: sha512-D+Id9XpcBpampptkegH7WMsEk6fUdf9LlCIX7UhLydILsqDin4L0QT7ryJR0oycwC7OqohIzdfcMHVZ34ezNGg==} dependencies: From 58c88a9d0f0dac567849d3528398e6875198a8bb Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 25 Nov 2024 18:47:04 -0500 Subject: [PATCH 02/11] Add error detail component --- .../operator-mode/card-error-detail.gts | 79 +++++++++++++++++++ .../components/operator-mode/stack-item.gts | 3 +- packages/host/app/resources/card-resource.ts | 1 + 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/host/app/components/operator-mode/card-error-detail.gts b/packages/host/app/components/operator-mode/card-error-detail.gts index f275a13870..4978d6e60b 100644 --- a/packages/host/app/components/operator-mode/card-error-detail.gts +++ b/packages/host/app/components/operator-mode/card-error-detail.gts @@ -1,4 +1,9 @@ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { fn } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { Accordion, Button } from '@cardstack/boxel-ui/components'; +import TriangleAlert from '@cardstack/boxel-icons/triangle-alert'; import { type CardError } from '../../resources/card-resource'; @@ -7,8 +12,82 @@ interface Signature { error: CardError['errors'][0]; }; } + export default class CardErrorDetail extends Component { + @tracked showErrorDetail = false; + + // TODO centralize this somewhere + get errorTitle() { + return this.args.error.status === 404 && + // a missing link error looks a lot like a missing card error + this.args.error.message.includes('missing') + ? `Link Not Found` + : this.args.error.title; + } + + private toggleDetail = () => (this.showErrorDetail = !this.showErrorDetail); + + private viewInCodeMode = () => { + // TODO + }; + } diff --git a/packages/host/app/components/operator-mode/stack-item.gts b/packages/host/app/components/operator-mode/stack-item.gts index 79544d070b..8e31227118 100644 --- a/packages/host/app/components/operator-mode/stack-item.gts +++ b/packages/host/app/components/operator-mode/stack-item.gts @@ -603,11 +603,12 @@ export default class OperatorModeStackItem extends Component { />
{{! TODO show stock error message when no last known good HTML !}} + {{! TODO confirm that we render this in edit format of the stack item !}} {{#if this.lastKnownGoodHtml}} {{/if}} -
+ {{else}} {{#let (this.realm.info this.card.id) as |realmInfo|}} { return; } realmSubscribers.set(this, { + // TODO HASSAN NEXT TASK: FIGURE OUT HOW TO GO IN AN OUT OF ERRORS VIA SSE unsubscribe: this.messageService.subscribe( realmURL.href, ({ type, data: dataStr }) => { From 6e9dfca162e99848a8540f1a72042189f4cec91a Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 27 Nov 2024 13:20:12 -0500 Subject: [PATCH 03/11] made script for running stripe commands --- packages/realm-server/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index 1358b4c3f6..114a4b09aa 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -96,6 +96,7 @@ "lint:js:fix": "eslint . --fix", "lint:glint": "glint", "full-reset": "./scripts/full-reset.sh", + "stripe": "docker run --rm --net=host -it stripe/stripe-cli:latest", "sync-stripe-products": "NODE_NO_WARNINGS=1 PGDATABASE=boxel PGPORT=5435 ts-node --transpileOnly scripts/sync-stripe-products.ts" }, "volta": { From 4cc452a3809f4b321170d2de1d33248debef99d7 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 27 Nov 2024 13:20:32 -0500 Subject: [PATCH 04/11] support for generic error message when ther eis no last known good state --- packages/base/command.gts | 1 + packages/host/app/commands/switch-submode.ts | 14 ++++--- .../operator-mode/card-error-detail.gts | 41 +++++++++++-------- .../components/operator-mode/card-error.gts | 30 ++++++++++++++ .../components/operator-mode/stack-item.gts | 22 +++++++--- 5 files changed, 82 insertions(+), 26 deletions(-) create mode 100644 packages/host/app/components/operator-mode/card-error.gts diff --git a/packages/base/command.gts b/packages/base/command.gts index 5c183af9c5..645011c6d7 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -72,6 +72,7 @@ export class ShowCardInput extends CardDef { export class SwitchSubmodeInput extends CardDef { @field submode = contains(StringField); + @field codePath = contains(StringField); } export class CreateModuleInput extends CardDef { diff --git a/packages/host/app/commands/switch-submode.ts b/packages/host/app/commands/switch-submode.ts index 0a8a7800d2..c297f2ecfd 100644 --- a/packages/host/app/commands/switch-submode.ts +++ b/packages/host/app/commands/switch-submode.ts @@ -44,11 +44,15 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< this.operatorModeStateService.updateCodePath(null); break; case Submodes.Code: - this.operatorModeStateService.updateCodePath( - this.lastCardInRightMostStack - ? new URL(this.lastCardInRightMostStack.id + '.json') - : null, - ); + if (input.codePath) { + this.operatorModeStateService.updateCodePath(new URL(input.codePath)); + } else { + this.operatorModeStateService.updateCodePath( + this.lastCardInRightMostStack + ? new URL(this.lastCardInRightMostStack.id + '.json') + : null, + ); + } break; default: throw new Error(`invalid submode specified: ${input.submode}`); diff --git a/packages/host/app/components/operator-mode/card-error-detail.gts b/packages/host/app/components/operator-mode/card-error-detail.gts index 4978d6e60b..4d39f9216d 100644 --- a/packages/host/app/components/operator-mode/card-error-detail.gts +++ b/packages/host/app/components/operator-mode/card-error-detail.gts @@ -2,34 +2,41 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { fn } from '@ember/helper'; import { on } from '@ember/modifier'; +import { service } from '@ember/service'; import { Accordion, Button } from '@cardstack/boxel-ui/components'; import TriangleAlert from '@cardstack/boxel-icons/triangle-alert'; +import { dropTask } from 'ember-concurrency'; +import perform from 'ember-concurrency/helpers/perform'; + +import SwitchSubmodeCommand from '../../commands/switch-submode'; import { type CardError } from '../../resources/card-resource'; +import type CommandService from '../../services/command-service'; interface Signature { Args: { error: CardError['errors'][0]; + title?: string; }; } export default class CardErrorDetail extends Component { - @tracked showErrorDetail = false; - - // TODO centralize this somewhere - get errorTitle() { - return this.args.error.status === 404 && - // a missing link error looks a lot like a missing card error - this.args.error.message.includes('missing') - ? `Link Not Found` - : this.args.error.title; - } + @tracked private showErrorDetail = false; + @service private declare commandService: CommandService; private toggleDetail = () => (this.showErrorDetail = !this.showErrorDetail); - private viewInCodeMode = () => { - // TODO - }; + private viewInCodeMode = dropTask(async () => { + let switchSubmodeCommand = new SwitchSubmodeCommand( + this.commandService.commandContext, + ); + const InputType = await switchSubmodeCommand.getInputType(); + let input = new InputType({ + submode: 'code', + codePath: `${this.args.error.id}.json`, + }); + await switchSubmodeCommand.execute(input); + });