Skip to content

Commit 2c6314d

Browse files
Centralize error message and make it display it more prominently (#71)
- 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
1 parent 7b69226 commit 2c6314d

File tree

20 files changed

+485
-41
lines changed

20 files changed

+485
-41
lines changed

e2e-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"ui-test": "yarn build && yarn playwright test"
1717
},
1818
"dependencies": {
19+
"@eclipse-glsp/glsp-playwright": "2.2.1",
1920
"@playwright/test": "^1.37.1",
2021
"@theia/playwright": "1.49.1"
2122
},
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/********************************************************************************
2+
* Copyright (c) 2024 CrossBreeze.
3+
********************************************************************************/
4+
5+
import { ElementHandle, Page } from '@playwright/test';
6+
import { isElementVisible, normalizeId, OSUtil, TheiaApp, TheiaEditor, TheiaTextEditor, urlEncodePath } from '@theia/playwright';
7+
import { TheiaMonacoEditor } from '@theia/playwright/lib/theia-monaco-editor';
8+
import { join } from 'path';
9+
10+
export type CompositeEditorName = 'Code Editor' | 'Form Editor' | 'System Diagram' | 'Mapping Diagram';
11+
12+
export class CrossModelCompositeEditor extends TheiaEditor {
13+
constructor(
14+
protected filePath: string,
15+
app: TheiaApp
16+
) {
17+
// shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
18+
// code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
19+
super(
20+
{
21+
tabSelector: normalizeId(
22+
`#shell-tab-cm-composite-editor-handler:file://${urlEncodePath(
23+
join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath)
24+
)}`
25+
),
26+
viewSelector: normalizeId(
27+
`#cm-composite-editor-handler:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
28+
)
29+
},
30+
app
31+
);
32+
}
33+
34+
protected editorTabSelector(editor: CompositeEditorName): string {
35+
return this.viewSelector + ` div.p-TabBar-tabLabel:has-text("${editor}")`;
36+
}
37+
38+
protected isEditorTabVisible(editor: CompositeEditorName): Promise<boolean> {
39+
return isElementVisible(this.editorTabElement(editor));
40+
}
41+
42+
protected editorTabElement(editor: CompositeEditorName): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
43+
return this.page.$(this.editorTabSelector(editor));
44+
}
45+
46+
async switchToEditor(editor: CompositeEditorName): Promise<ElementHandle<SVGElement | HTMLElement>> {
47+
const selector = this.editorTabSelector(editor);
48+
const tab = await this.page.waitForSelector(selector, { state: 'visible' });
49+
await tab?.click();
50+
return tab;
51+
}
52+
53+
async switchToCodeEditor(): Promise<IntegratedCodeEditor> {
54+
await this.switchToEditor('Code Editor');
55+
const textEditor = new IntegratedCodeEditor(this.filePath, this.app, this.editorTabSelector('Code Editor'));
56+
await textEditor.waitForVisible();
57+
return textEditor;
58+
}
59+
60+
async switchToFormEditor(): Promise<IntegratedFormEditor> {
61+
await this.switchToEditor('Form Editor');
62+
const formEditor = new IntegratedFormEditor(this.filePath, this.app, this.editorTabSelector('Form Editor'));
63+
await formEditor.waitForVisible();
64+
return formEditor;
65+
}
66+
67+
async switchToSystemDiagram(): Promise<IntegratedSystemDiagramEditor> {
68+
await this.switchToEditor('System Diagram');
69+
const diagramEditor = new IntegratedSystemDiagramEditor(this.filePath, this.app, this.editorTabSelector('System Diagram'));
70+
await diagramEditor.waitForVisible();
71+
return diagramEditor;
72+
}
73+
74+
async switchToMappingDiagram(): Promise<IntegratedMappingDiagramEditor> {
75+
await this.switchToEditor('Mapping Diagram');
76+
const diagramEditor = new IntegratedMappingDiagramEditor(this.filePath, this.app, this.editorTabSelector('Mapping Diagram'));
77+
await diagramEditor.waitForVisible();
78+
return diagramEditor;
79+
}
80+
}
81+
82+
export class IntegratedCodeEditor extends TheiaTextEditor {
83+
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
84+
// shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
85+
// code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
86+
super(filePath, app);
87+
this.data.viewSelector = normalizeId(
88+
`#code-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
89+
);
90+
this.data.tabSelector = tabSelector;
91+
this.monacoEditor = new TheiaMonacoEditor(this.viewSelector, app);
92+
}
93+
}
94+
95+
export class IntegratedFormEditor extends TheiaEditor {
96+
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
97+
super(
98+
{
99+
tabSelector,
100+
viewSelector: normalizeId(
101+
`#form-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
102+
)
103+
},
104+
app
105+
);
106+
}
107+
108+
async hasError(errorMessage: string): Promise<boolean> {
109+
return hasViewError(this.page, this.viewSelector, errorMessage);
110+
}
111+
}
112+
113+
export class IntegratedSystemDiagramEditor extends TheiaEditor {
114+
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
115+
super(
116+
{
117+
tabSelector,
118+
viewSelector: normalizeId(
119+
`#system-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
120+
)
121+
},
122+
app
123+
);
124+
}
125+
126+
async hasError(errorMessage: string): Promise<boolean> {
127+
return hasViewError(this.page, this.viewSelector, errorMessage);
128+
}
129+
}
130+
131+
export class IntegratedMappingDiagramEditor extends TheiaEditor {
132+
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
133+
super(
134+
{
135+
tabSelector,
136+
viewSelector: normalizeId(
137+
`#mapping-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
138+
)
139+
},
140+
app
141+
);
142+
}
143+
144+
async hasError(errorMessage: string): Promise<boolean> {
145+
return hasViewError(this.page, this.viewSelector, errorMessage);
146+
}
147+
}
148+
149+
export async function hasViewError(page: Page, viewSelector: string, message: string): Promise<boolean> {
150+
const visible = await isElementVisible(page.$(viewSelector));
151+
if (!visible) {
152+
return false;
153+
}
154+
await page.waitForSelector(viewSelector + ' .editor-diagnostics-error-message:has-text("' + message + '")');
155+
return true;
156+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/********************************************************************************
2+
* Copyright (c) 2023 CrossBreeze.
3+
********************************************************************************/
4+
import { expect } from '@playwright/test';
5+
import test, { app } from '../fixtures/crossmodel-fixture';
6+
import { CrossModelCompositeEditor } from '../page-objects/crossmodel-composite-editor';
7+
8+
test.describe('CrossModel Error Views', () => {
9+
test('Form Editor should show error if model code is broken', async () => {
10+
const editor = await app.openEditor('example-entity.entity.cm', CrossModelCompositeEditor);
11+
expect(editor).toBeDefined();
12+
13+
const codeEditor = await editor.switchToCodeEditor();
14+
expect(codeEditor).toBeDefined();
15+
await codeEditor.addTextToNewLineAfterLineByLineNumber(2, 'break-model');
16+
17+
const formEditor = await editor.switchToFormEditor();
18+
expect(
19+
await formEditor.hasError(
20+
// eslint-disable-next-line max-len
21+
"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."
22+
)
23+
).toBeTruthy();
24+
});
25+
26+
test('System Diagram Editor should show error if model code is broken', async () => {
27+
const editor = await app.openEditor('example-diagram.diagram.cm', CrossModelCompositeEditor);
28+
expect(editor).toBeDefined();
29+
30+
const codeEditor = await editor.switchToCodeEditor();
31+
expect(codeEditor).toBeDefined();
32+
await codeEditor.addTextToNewLineAfterLineByLineNumber(2, 'break-model');
33+
34+
const diagramEditor = await editor.switchToSystemDiagram();
35+
expect(
36+
await diagramEditor.hasError(
37+
// eslint-disable-next-line max-len
38+
"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."
39+
)
40+
).toBeTruthy();
41+
});
42+
});

e2e-tests/src/tests/crossmodel-explorer-view.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ test.describe('CrossModel Explorer View', () => {
3030
});
3131

3232
test('code and form editor options available in the context menu on an entity', async () => {
33-
const file = await explorer.getFileStatNodeByLabel('example-entity.cm');
33+
const file = await explorer.getFileStatNodeByLabel('example-entity.entity.cm');
3434
expect(file).toBeDefined();
35-
expect(await file.label()).toBe('example-entity.cm');
35+
expect(await file.label()).toBe('example-entity.entity.cm');
3636
const menu = await file.openContextMenu();
3737
expect(await menu.isOpen()).toBe(true);
3838
// Expect the Code and Form editor to be in the Open With menu option.

extensions/crossmodel-lang/src/glsp-server/common/cross-model-storage.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* Copyright (c) 2023 CrossBreeze.
33
********************************************************************************/
44

5-
import { ERRONEOUS_MODEL } from '@crossbreeze/protocol';
65
import {
76
Action,
87
ActionDispatcher,
@@ -19,8 +18,7 @@ import {
1918
SOURCE_URI_ARG,
2019
SaveModelAction,
2120
SetEditModeAction,
22-
SourceModelStorage,
23-
StatusAction
21+
SourceModelStorage
2422
} from '@eclipse-glsp/server';
2523
import { inject, injectable, postConstruct } from 'inversify';
2624
import { AstUtils } from 'langium';
@@ -89,9 +87,9 @@ export class CrossModelStorage implements SourceModelStorage, ClientSessionListe
8987
this.state.editMode = document.diagnostics.length > 0 ? EditMode.READONLY : EditMode.EDITABLE;
9088
if (prevEditMode !== this.state.editMode) {
9189
if (this.state.isReadonly) {
92-
actions.push(SetEditModeAction.create(EditMode.READONLY), StatusAction.create(ERRONEOUS_MODEL, { severity: 'ERROR' }));
90+
actions.push(SetEditModeAction.create(EditMode.READONLY));
9391
} else {
94-
actions.push(SetEditModeAction.create(EditMode.EDITABLE), StatusAction.create('', { severity: 'NONE' }));
92+
actions.push(SetEditModeAction.create(EditMode.EDITABLE));
9593
}
9694
}
9795
return actions;

packages/composite-editor/src/browser/composite-editor-frontend-module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (c) 2024 CrossBreeze.
33
********************************************************************************/
44
import { CrossModelWidgetOptions } from '@crossbreeze/core/lib/browser';
5-
import { OpenHandler, WidgetFactory } from '@theia/core/lib/browser';
5+
import { FrontendApplicationContribution, OpenHandler, WidgetFactory } from '@theia/core/lib/browser';
66
import { ContainerModule } from '@theia/core/shared/inversify';
77
import { EditorPreviewManager } from '@theia/editor-preview/lib/browser/editor-preview-manager';
88
import { CompositeEditor } from './composite-editor';
@@ -15,6 +15,7 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
1515

1616
bind(CompositeEditorOpenHandler).toSelf().inSingletonScope();
1717
bind(OpenHandler).toService(CompositeEditorOpenHandler);
18+
bind(FrontendApplicationContribution).toService(CompositeEditorOpenHandler);
1819
bind<WidgetFactory>(WidgetFactory).toDynamicValue(context => ({
1920
id: CompositeEditorOpenHandler.ID, // must match the id in the open handler
2021
createWidget: (options: CompositeEditorOptions) => {

packages/composite-editor/src/browser/composite-editor-open-handler.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
import { ModelFileExtensions, ModelFileType } from '@crossbreeze/protocol';
66
import { RecursivePartial, URI } from '@theia/core';
7-
import { NavigatableWidgetOpenHandler, NavigatableWidgetOptions, OpenWithHandler, OpenWithService } from '@theia/core/lib/browser';
7+
import {
8+
FrontendApplicationContribution,
9+
NavigatableWidgetOpenHandler,
10+
NavigatableWidgetOptions,
11+
OpenWithHandler,
12+
OpenWithService
13+
} from '@theia/core/lib/browser';
814
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
915
import { Range } from '@theia/core/shared/vscode-languageserver-types';
1016
import { EditorOpenerOptions } from '@theia/editor/lib/browser';
@@ -17,7 +23,10 @@ export interface CompositeEditorOptions extends NavigatableWidgetOptions {
1723
}
1824

1925
@injectable()
20-
export class CompositeEditorOpenHandler extends NavigatableWidgetOpenHandler<CompositeEditor> implements OpenWithHandler {
26+
export class CompositeEditorOpenHandler
27+
extends NavigatableWidgetOpenHandler<CompositeEditor>
28+
implements OpenWithHandler, FrontendApplicationContribution
29+
{
2130
static readonly ID = 'cm-composite-editor-handler';
2231
static readonly PRIORITY = 2000;
2332

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

45+
initialize(): void {
46+
// ensure this class is instantiated early
47+
}
48+
3649
protected override createWidgetOptions(resourceUri: URI, options?: EditorOpenerOptions): CompositeEditorOptions {
3750
const { kind, uri } = super.createWidgetOptions(resourceUri, options);
3851
const widgetId = createCompositeEditorId(uri);

packages/composite-editor/src/browser/composite-editor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export class CompositeEditor extends BaseWidget implements Saveable, Navigatable
188188
const options: NavigatableWidgetOptions = { kind, uri, counter };
189189
const codeWidget = await this.widgetManager.getOrCreateWidget(EditorPreviewWidgetFactory.ID, options);
190190
codeWidget.title.label = 'Code Editor';
191+
codeWidget.title.iconClass = codiconCSSString('code');
191192
codeWidget.title.closable = false;
192193
return codeWidget;
193194
}
@@ -196,8 +197,9 @@ export class CompositeEditor extends BaseWidget implements Saveable, Navigatable
196197
const { kind, uri, counter } = this.options;
197198
const options: NavigatableWidgetOptions = { kind, uri, counter };
198199
const formEditor = await this.widgetManager.getOrCreateWidget<FormEditorWidget>(FormEditorOpenHandler.ID, options);
199-
formEditor.title.closable = false;
200200
formEditor.title.label = 'Form Editor';
201+
formEditor.title.iconClass = codiconCSSString('symbol-keyword');
202+
formEditor.title.closable = false;
201203
return formEditor;
202204
}
203205

packages/core/src/browser/model-widget.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,11 @@ export class CrossModelWidget extends ReactWidget implements Saveable {
8585
await this.closeModel(this.document.uri.toString());
8686
}
8787
this.document = uri ? await this.openModel(uri) : undefined;
88-
this.updateTitle(new URI(this.document?.uri));
8988
this.setDirty(false);
9089
this.update();
9190
this.focusInput();
9291
}
9392

94-
private updateTitle(uri?: URI): void {
95-
if (uri) {
96-
this.title.label = this.labelProvider.getName(uri);
97-
this.title.iconClass = this.labelProvider.getIcon(uri);
98-
} else {
99-
this.title.label = 'Model Widget';
100-
this.title.iconClass = 'no-icon';
101-
}
102-
}
103-
10493
protected async closeModel(uri: string): Promise<void> {
10594
this.document = undefined;
10695
await this.modelService.close({ clientId: this.options.clientId, uri });

packages/core/style/index.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,23 @@ input[type='number'] {
3535
:root {
3636
--theia-private-file-dialog-input-height: 24px; /* from: 21px - better aligns with the file name panel size */
3737
}
38+
39+
.editor-diagnostics-error {
40+
background-color: var(--theia-errorBackground);
41+
color: var(--theia-problemsErrorIcon-foreground);
42+
font-family: var(--theia-ui-font-family);
43+
font-size: 1.2em;
44+
padding: 0 1em;
45+
display: flex;
46+
align-items: center;
47+
gap: 1em;
48+
min-height: 4em;
49+
}
50+
51+
.editor-diagnostics-error-icon {
52+
font-size: 1.2em !important;
53+
}
54+
55+
.editor-diagnostics-error-message {
56+
font-weight: 500;
57+
}

packages/glsp-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"dependencies": {
3333
"@crossbreeze/core": "0.0.0",
3434
"@crossbreeze/protocol": "0.0.0",
35+
"@crossbreeze/react-model-ui": "0.0.0",
3536
"@eclipse-glsp/client": "2.2.1",
3637
"@eclipse-glsp/theia-integration": "2.2.1",
3738
"@theia/core": "1.49.1",

0 commit comments

Comments
 (0)