Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add last known good rendering of card in error state to stack item #1834

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading