Skip to content

Commit

Permalink
Centralize error message and make it display it more prominently
Browse files Browse the repository at this point in the history
- Create a central EditorError component and use in graphical and form
- Remove message coming from GLSP server and replace with UI extension
- Ensure tool palette minimize button is shown depending on state

Fixes #65
  • Loading branch information
martin-fleck-at committed Oct 18, 2024
1 parent 5bd00ad commit 0b6b02a
Show file tree
Hide file tree
Showing 20 changed files with 485 additions and 41 deletions.
1 change: 1 addition & 0 deletions e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"ui-test": "yarn build && yarn playwright test"
},
"dependencies": {
"@eclipse-glsp/glsp-playwright": "2.2.1",
"@playwright/test": "^1.37.1",
"@theia/playwright": "1.49.1"
},
Expand Down
156 changes: 156 additions & 0 deletions e2e-tests/src/page-objects/crossmodel-composite-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import { ElementHandle, Page } from '@playwright/test';
import { isElementVisible, normalizeId, OSUtil, TheiaApp, TheiaEditor, TheiaTextEditor, urlEncodePath } from '@theia/playwright';
import { TheiaMonacoEditor } from '@theia/playwright/lib/theia-monaco-editor';
import { join } from 'path';

export type CompositeEditorName = 'Code Editor' | 'Form Editor' | 'System Diagram' | 'Mapping Diagram';

export class CrossModelCompositeEditor extends TheiaEditor {
constructor(
protected filePath: string,
app: TheiaApp
) {
// shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
// code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
super(
{
tabSelector: normalizeId(
`#shell-tab-cm-composite-editor-handler:file://${urlEncodePath(
join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath)
)}`
),
viewSelector: normalizeId(
`#cm-composite-editor-handler:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
);
}

protected editorTabSelector(editor: CompositeEditorName): string {
return this.viewSelector + ` div.p-TabBar-tabLabel:has-text("${editor}")`;
}

protected isEditorTabVisible(editor: CompositeEditorName): Promise<boolean> {
return isElementVisible(this.editorTabElement(editor));
}

protected editorTabElement(editor: CompositeEditorName): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
return this.page.$(this.editorTabSelector(editor));
}

async switchToEditor(editor: CompositeEditorName): Promise<ElementHandle<SVGElement | HTMLElement>> {
const selector = this.editorTabSelector(editor);
const tab = await this.page.waitForSelector(selector, { state: 'visible' });
await tab?.click();
return tab;
}

async switchToCodeEditor(): Promise<IntegratedCodeEditor> {
await this.switchToEditor('Code Editor');
const textEditor = new IntegratedCodeEditor(this.filePath, this.app, this.editorTabSelector('Code Editor'));
await textEditor.waitForVisible();
return textEditor;
}

async switchToFormEditor(): Promise<IntegratedFormEditor> {
await this.switchToEditor('Form Editor');
const formEditor = new IntegratedFormEditor(this.filePath, this.app, this.editorTabSelector('Form Editor'));
await formEditor.waitForVisible();
return formEditor;
}

async switchToSystemDiagram(): Promise<IntegratedSystemDiagramEditor> {
await this.switchToEditor('System Diagram');
const diagramEditor = new IntegratedSystemDiagramEditor(this.filePath, this.app, this.editorTabSelector('System Diagram'));
await diagramEditor.waitForVisible();
return diagramEditor;
}

async switchToMappingDiagram(): Promise<IntegratedMappingDiagramEditor> {
await this.switchToEditor('Mapping Diagram');
const diagramEditor = new IntegratedMappingDiagramEditor(this.filePath, this.app, this.editorTabSelector('Mapping Diagram'));
await diagramEditor.waitForVisible();
return diagramEditor;
}
}

export class IntegratedCodeEditor extends TheiaTextEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
// shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
// code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
super(filePath, app);
this.data.viewSelector = normalizeId(
`#code-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
);
this.data.tabSelector = tabSelector;
this.monacoEditor = new TheiaMonacoEditor(this.viewSelector, app);
}
}

export class IntegratedFormEditor extends TheiaEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#form-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
);
}

async hasError(errorMessage: string): Promise<boolean> {
return hasViewError(this.page, this.viewSelector, errorMessage);
}
}

export class IntegratedSystemDiagramEditor extends TheiaEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#system-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
);
}

async hasError(errorMessage: string): Promise<boolean> {
return hasViewError(this.page, this.viewSelector, errorMessage);
}
}

export class IntegratedMappingDiagramEditor extends TheiaEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#mapping-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
);
}

async hasError(errorMessage: string): Promise<boolean> {
return hasViewError(this.page, this.viewSelector, errorMessage);
}
}

export async function hasViewError(page: Page, viewSelector: string, message: string): Promise<boolean> {
const visible = await isElementVisible(page.$(viewSelector));
if (!visible) {
return false;
}
await page.waitForSelector(viewSelector + ' .editor-diagnostics-error-message:has-text("' + message + '")');
return true;
}
42 changes: 42 additions & 0 deletions e2e-tests/src/tests/crossmodel-error-view.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { expect } from '@playwright/test';
import test, { app } from '../fixtures/crossmodel-fixture';
import { CrossModelCompositeEditor } from '../page-objects/crossmodel-composite-editor';

test.describe('CrossModel Error Views', () => {
test('Form Editor should show error if model code is broken', async () => {
const editor = await app.openEditor('example-entity.entity.cm', CrossModelCompositeEditor);
expect(editor).toBeDefined();

const codeEditor = await editor.switchToCodeEditor();
expect(codeEditor).toBeDefined();
await codeEditor.addTextToNewLineAfterLineByLineNumber(2, 'break-model');

const formEditor = await editor.switchToFormEditor();
expect(
await formEditor.hasError(
// eslint-disable-next-line max-len
"The file contains one or more errors. Please fix the error(s) using the 'Code Editor'. This perspective will be read-only until the errors are resolved."
)
).toBeTruthy();
});

test('System Diagram Editor should show error if model code is broken', async () => {
const editor = await app.openEditor('example-diagram.diagram.cm', CrossModelCompositeEditor);
expect(editor).toBeDefined();

const codeEditor = await editor.switchToCodeEditor();
expect(codeEditor).toBeDefined();
await codeEditor.addTextToNewLineAfterLineByLineNumber(2, 'break-model');

const diagramEditor = await editor.switchToSystemDiagram();
expect(
await diagramEditor.hasError(
// eslint-disable-next-line max-len
"The file contains one or more errors. Please fix the error(s) using the 'Code Editor'. This perspective will be read-only until the errors are resolved."
)
).toBeTruthy();
});
});
4 changes: 2 additions & 2 deletions e2e-tests/src/tests/crossmodel-explorer-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ test.describe('CrossModel Explorer View', () => {
});

test('code and form editor options available in the context menu on an entity', async () => {
const file = await explorer.getFileStatNodeByLabel('example-entity.cm');
const file = await explorer.getFileStatNodeByLabel('example-entity.entity.cm');
expect(file).toBeDefined();
expect(await file.label()).toBe('example-entity.cm');
expect(await file.label()).toBe('example-entity.entity.cm');
const menu = await file.openContextMenu();
expect(await menu.isOpen()).toBe(true);
// Expect the Code and Form editor to be in the Open With menu option.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { ERRONEOUS_MODEL } from '@crossbreeze/protocol';
import {
Action,
ActionDispatcher,
Expand All @@ -19,8 +18,7 @@ import {
SOURCE_URI_ARG,
SaveModelAction,
SetEditModeAction,
SourceModelStorage,
StatusAction
SourceModelStorage
} from '@eclipse-glsp/server';
import { inject, injectable, postConstruct } from 'inversify';
import { AstUtils } from 'langium';
Expand Down Expand Up @@ -89,9 +87,9 @@ export class CrossModelStorage implements SourceModelStorage, ClientSessionListe
this.state.editMode = document.diagnostics.length > 0 ? EditMode.READONLY : EditMode.EDITABLE;
if (prevEditMode !== this.state.editMode) {
if (this.state.isReadonly) {
actions.push(SetEditModeAction.create(EditMode.READONLY), StatusAction.create(ERRONEOUS_MODEL, { severity: 'ERROR' }));
actions.push(SetEditModeAction.create(EditMode.READONLY));
} else {
actions.push(SetEditModeAction.create(EditMode.EDITABLE), StatusAction.create('', { severity: 'NONE' }));
actions.push(SetEditModeAction.create(EditMode.EDITABLE));
}
}
return actions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/
import { CrossModelWidgetOptions } from '@crossbreeze/core/lib/browser';
import { OpenHandler, WidgetFactory } from '@theia/core/lib/browser';
import { FrontendApplicationContribution, OpenHandler, WidgetFactory } from '@theia/core/lib/browser';
import { ContainerModule } from '@theia/core/shared/inversify';
import { EditorPreviewManager } from '@theia/editor-preview/lib/browser/editor-preview-manager';
import { CompositeEditor } from './composite-editor';
Expand All @@ -15,6 +15,7 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {

bind(CompositeEditorOpenHandler).toSelf().inSingletonScope();
bind(OpenHandler).toService(CompositeEditorOpenHandler);
bind(FrontendApplicationContribution).toService(CompositeEditorOpenHandler);
bind<WidgetFactory>(WidgetFactory).toDynamicValue(context => ({
id: CompositeEditorOpenHandler.ID, // must match the id in the open handler
createWidget: (options: CompositeEditorOptions) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

import { ModelFileExtensions, ModelFileType } from '@crossbreeze/protocol';
import { RecursivePartial, URI } from '@theia/core';
import { NavigatableWidgetOpenHandler, NavigatableWidgetOptions, OpenWithHandler, OpenWithService } from '@theia/core/lib/browser';
import {
FrontendApplicationContribution,
NavigatableWidgetOpenHandler,
NavigatableWidgetOptions,
OpenWithHandler,
OpenWithService
} from '@theia/core/lib/browser';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { Range } from '@theia/core/shared/vscode-languageserver-types';
import { EditorOpenerOptions } from '@theia/editor/lib/browser';
Expand All @@ -17,7 +23,10 @@ export interface CompositeEditorOptions extends NavigatableWidgetOptions {
}

@injectable()
export class CompositeEditorOpenHandler extends NavigatableWidgetOpenHandler<CompositeEditor> implements OpenWithHandler {
export class CompositeEditorOpenHandler
extends NavigatableWidgetOpenHandler<CompositeEditor>
implements OpenWithHandler, FrontendApplicationContribution
{
static readonly ID = 'cm-composite-editor-handler';
static readonly PRIORITY = 2000;

Expand All @@ -33,6 +42,10 @@ export class CompositeEditorOpenHandler extends NavigatableWidgetOpenHandler<Com
this.openWithService.registerHandler(this);
}

initialize(): void {
// ensure this class is instantiated early
}

protected override createWidgetOptions(resourceUri: URI, options?: EditorOpenerOptions): CompositeEditorOptions {
const { kind, uri } = super.createWidgetOptions(resourceUri, options);
const widgetId = createCompositeEditorId(uri);
Expand Down
4 changes: 3 additions & 1 deletion packages/composite-editor/src/browser/composite-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export class CompositeEditor extends BaseWidget implements Saveable, Navigatable
const options: NavigatableWidgetOptions = { kind, uri, counter };
const codeWidget = await this.widgetManager.getOrCreateWidget(EditorPreviewWidgetFactory.ID, options);
codeWidget.title.label = 'Code Editor';
codeWidget.title.iconClass = codiconCSSString('code');
codeWidget.title.closable = false;
return codeWidget;
}
Expand All @@ -196,8 +197,9 @@ export class CompositeEditor extends BaseWidget implements Saveable, Navigatable
const { kind, uri, counter } = this.options;
const options: NavigatableWidgetOptions = { kind, uri, counter };
const formEditor = await this.widgetManager.getOrCreateWidget<FormEditorWidget>(FormEditorOpenHandler.ID, options);
formEditor.title.closable = false;
formEditor.title.label = 'Form Editor';
formEditor.title.iconClass = codiconCSSString('symbol-keyword');
formEditor.title.closable = false;
return formEditor;
}

Expand Down
11 changes: 0 additions & 11 deletions packages/core/src/browser/model-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,11 @@ export class CrossModelWidget extends ReactWidget implements Saveable {
await this.closeModel(this.document.uri.toString());
}
this.document = uri ? await this.openModel(uri) : undefined;
this.updateTitle(new URI(this.document?.uri));
this.setDirty(false);
this.update();
this.focusInput();
}

private updateTitle(uri?: URI): void {
if (uri) {
this.title.label = this.labelProvider.getName(uri);
this.title.iconClass = this.labelProvider.getIcon(uri);
} else {
this.title.label = 'Model Widget';
this.title.iconClass = 'no-icon';
}
}

protected async closeModel(uri: string): Promise<void> {
this.document = undefined;
await this.modelService.close({ clientId: this.options.clientId, uri });
Expand Down
20 changes: 20 additions & 0 deletions packages/core/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,23 @@ input[type='number'] {
:root {
--theia-private-file-dialog-input-height: 24px; /* from: 21px - better aligns with the file name panel size */
}

.editor-diagnostics-error {
background-color: var(--theia-errorBackground);
color: var(--theia-problemsErrorIcon-foreground);
font-family: var(--theia-ui-font-family);
font-size: 1.2em;
padding: 0 1em;
display: flex;
align-items: center;
gap: 1em;
min-height: 4em;
}

.editor-diagnostics-error-icon {
font-size: 1.2em !important;
}

.editor-diagnostics-error-message {
font-weight: 500;
}
1 change: 1 addition & 0 deletions packages/glsp-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"dependencies": {
"@crossbreeze/core": "0.0.0",
"@crossbreeze/protocol": "0.0.0",
"@crossbreeze/react-model-ui": "0.0.0",
"@eclipse-glsp/client": "2.2.1",
"@eclipse-glsp/theia-integration": "2.2.1",
"@theia/core": "1.49.1",
Expand Down
Loading

0 comments on commit 0b6b02a

Please sign in to comment.