Skip to content

Commit

Permalink
Merge pull request #1834 from cardstack/cs-7526-update-the-isolated-c…
Browse files Browse the repository at this point in the history
…ard-rendering-for-stack-items-in-an

Add last known good rendering of card in error state to stack item
  • Loading branch information
habdelra authored Dec 2, 2024
2 parents 7df6ffe + af119a7 commit fca3143
Show file tree
Hide file tree
Showing 16 changed files with 576 additions and 96 deletions.
1 change: 1 addition & 0 deletions packages/base/command.gts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 9 additions & 5 deletions packages/host/app/commands/switch-submode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
113 changes: 113 additions & 0 deletions packages/host/app/components/operator-mode/card-error-detail.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { fn } from '@ember/helper';
import { on } from '@ember/modifier';
import { service } from '@ember/service';

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import TriangleAlert from '@cardstack/boxel-icons/triangle-alert';

import { dropTask } from 'ember-concurrency';
import perform from 'ember-concurrency/helpers/perform';

import { Accordion, Button } from '@cardstack/boxel-ui/components';

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<Signature> {
@tracked private showErrorDetail = false;
@service private declare commandService: CommandService;

private toggleDetail = () => (this.showErrorDetail = !this.showErrorDetail);

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

<template>
<Accordion as |A|>
<A.Item
data-test-error-detail-toggle
@onClick={{fn this.toggleDetail 'schema'}}
@isOpen={{this.showErrorDetail}}
>
<:title>
<TriangleAlert />
An error was encountered on this card:
<span class='error-detail' data-test-error-title>{{@title}}</span>
</:title>
<:content>
<div class='actions'>
<Button
data-test-view-in-code-mode-button
@kind='primary'
{{on 'click' (perform this.viewInCodeMode)}}
>View in Code Mode</Button>
</div>
<div class='detail'>
<div class='detail-item'>
<div class='detail-title'>Details:</div>
<div
class='detail-contents'
data-test-error-detail
>{{@error.message}}</div>
</div>
{{#if @error.meta.stack}}
<div class='detail-item'>
<div class='detail-title'>Stack trace:</div>
<pre
data-test-error-stack
>
{{@error.meta.stack}}
</pre>
</div>
{{/if}}
</div>
</:content>
</A.Item>
</Accordion>

<style scoped>
.actions {
display: flex;
justify-content: center;
margin-top: var(--boxel-sp-lg);
}
.detail {
padding: var(--boxel-sp);
}
.detail-item {
margin-top: var(--boxel-sp);
}
.detail-title {
font: 600 var(--boxel-font);
}
.detail-contents {
font: var(--boxel-font);
}
pre {
margin-top: 0;
white-space: pre-wrap;
word-break: break-all;
}
</style>
</template>
}
31 changes: 31 additions & 0 deletions packages/host/app/components/operator-mode/card-error.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { TemplateOnlyComponent } from '@ember/component/template-only';

import FileAlert from '@cardstack/boxel-icons/file-alert';

const CardErrorComponent: TemplateOnlyComponent = <template>
<div class='card-error'>
<FileAlert class='icon' />
<div class='message'>This card contains an error.</div>
</div>

<style scoped>
.icon {
height: 100px;
width: 100px;
}
.card-error {
display: flex;
height: 100%;
align-content: center;
justify-content: center;
flex-wrap: wrap;
}
.message {
width: 100%;
text-align: center;
font: 600 var(--boxel-font);
}
</style>
</template>;

export default CardErrorComponent;
62 changes: 4 additions & 58 deletions packages/host/app/components/operator-mode/code-submode.gts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
type ResolvedCodeRef,
PermissionsContextName,
} from '@cardstack/runtime-common';
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';
Expand Down Expand Up @@ -311,57 +310,6 @@ export default class CodeSubmode extends Component<Signature> {
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 currentOpenFile() {
return this.operatorModeStateService.openFile.current;
}
Expand Down Expand Up @@ -854,12 +802,10 @@ export default class CodeSubmode extends Component<Signature> {

<hr class='preview-error' />

{{#each this.fileErrorMessages as |error|}}
<pre
class='preview-error'
data-test-card-preview-error
>{{error}}</pre>
{{/each}}
<pre
class='preview-error'
data-test-card-preview-error
>{{this.cardResource.cardError.message}}</pre>
</div>
</div>
{{else if this.fileIncompatibilityMessage}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,13 @@ export default class InteractSubmode extends Component<Signature> {
}

private close = task(async (item: StackItem) => {
let { card, request } = item;
// close the item first so user doesn't have to wait for the save to complete
this.operatorModeStateService.trimItemsFromStack(item);
if (item.cardError) {
return;
}

let { card, request } = item;

// only save when closing a stack item in edit mode. there should be no unsaved
// changes in isolated mode because they were saved when user toggled between
Expand Down
Loading

0 comments on commit fca3143

Please sign in to comment.