Skip to content
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
96 changes: 80 additions & 16 deletions src/preview/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,31 +113,78 @@ sap.ui.define([
window.addEventListener('scroll', function () { if (tools) placeTools(); }, true);
window.addEventListener('resize', function () { if (tools) placeTools(); });

// Every render ends - in a view on the canvas, or in an error the
// designer shows - and releases the next one. Whatever throws on the way
// (the view's creation, a model, the placing: a renderer that fails on a
// value) is reported, never swallowed: a render that got stuck used to
// leave the canvas blank and every later edit unseen.
var RENDER_TIMEOUT = 20000;
var renderSeq = 0;
function render(msg) {
if (rendering) { pending = msg; return; }
if (msg.xml === lastXml && view) {
view.getModel().setData(msg.model);
highlight(false);
try {
view.getModel().setData(msg.model);
highlight(false);
showError('');
post({ type: 'rendered' }); // the view on the canvas is the design's again
} catch (err) {
fail('The model does not apply', err);
}
return;
}
rendering = true;
viewSeq += 1;
XMLView.create({ id: 'dsg' + (viewSeq % 2), definition: msg.xml, controller: new PreviewController() }).then(function (v) {
if (view) view.destroy();
view = v;
lastXml = msg.xml;
v.setModel(new JSONModel(msg.model));
v.placeAt('content');
showError('');
post({ type: 'rendered' });
setTimeout(function () { highlight(false); }, 50);
}, function (err) {
showError('The view does not render: ' + (err && err.message ? err.message : err));
post({ type: 'error', message: String(err && err.message ? err.message : err) });
}).then(function () {
var seq = ++renderSeq;
var done = false;
function finish() {
if (done) return;
done = true;
rendering = false;
if (pending) { var next = pending; pending = null; render(next); }
});
}
function fail(what, err) {
var text = what + ': ' + (err && err.message ? err.message : err);
showError(text);
post({ type: 'error', message: text });
}
// UI5 that never answers (a control module the CDN does not deliver)
// would otherwise hold the canvas for ever
setTimeout(function () {
if (seq === renderSeq && !done) {
fail('The view is still loading after ' + (RENDER_TIMEOUT / 1000) + ' seconds', 'a control may not be loading - check the network, or reload the canvas');
finish();
}
}, RENDER_TIMEOUT);
var promise;
try {
promise = XMLView.create({ id: 'dsg' + (viewSeq % 2), definition: msg.xml, controller: new PreviewController() });
} catch (err) {
fail('The view does not render', err);
finish();
return;
}
promise.then(function (v) {
if (done) { v.destroy(); return; } // timed out meanwhile: the next render owns the canvas
try {
v.setModel(new JSONModel(msg.model));
if (view) view.destroy();
view = v;
lastXml = msg.xml;
v.placeAt('content');
showError('');
post({ type: 'rendered' });
setTimeout(function () { highlight(false); }, 50);
} catch (err) {
// the old view is gone or half replaced: say so, the next edit
// renders anew
view = null;
lastXml = null;
fail('The view does not render', err);
}
}, function (err) {
fail('The view does not render', err);
}).then(finish, function (err) { fail('The view does not render', err); finish(); });
}

// the metadata of a control class, the shape tools/ui5-metadata.mjs
Expand Down Expand Up @@ -249,6 +296,23 @@ sap.ui.define([
post({ type: 'key', key: e.key, ctrl: e.ctrlKey || e.metaKey, shift: e.shiftKey, alt: e.altKey });
});

// what UI5 logs as an error (an invalid value for a property, a binding
// it cannot parse, a renderer that gave up) reaches the designer too -
// the canvas may look fine or empty and nobody knows why
sap.ui.require(['sap/base/Log'], function (Log) {
var seen = {};
Log.addLogListener({
onLogEntry: function (entry) {
if (entry.level > Log.Level.ERROR) return;
var text = String(entry.message || '');
if (/Parameters: The following parameters|could not be loaded from|sap-ui-version\.json/.test(text)) return; // local trees, not the view
if (seen[text]) return;
seen[text] = true;
post({ type: 'ui5log', message: text.slice(0, 300) });
},
});
});

// an error after the view was created - in a renderer, a formatter, an
// event - would otherwise end in the console only
window.addEventListener('error', function (e) {
Expand Down
16 changes: 11 additions & 5 deletions src/ui/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { previewModel, findNode, shiftNode, duplicateNode, removeNode, walk, typ
import { toXml, namespaceUri } from '../xml.js';
import { getControl, registerControl, runtimeDef, parseClassName, splitType } from '../catalog.js';
import { state, select, commit, labelOfSelection, emitChange } from './store.js';
import { $, toast } from './dom.js';
import { $, h, toast } from './dom.js';
import { handleDrop } from './edit.js';
import { onKey } from './keys.js';

Expand All @@ -24,10 +24,13 @@ export function loadPreview() {
$('#preview').src = previewUrl();
}

// the line over the canvas: what UI5 is doing, or what went wrong
function previewStatus(text, level) {
// the line over the canvas: what UI5 is doing, or what went wrong -
// `reload` adds the way out of a canvas that is stuck
function previewStatus(text, level, reload) {
const el = $('#previewStatus');
el.textContent = text || '';
el.replaceChildren(text || '');
if (text && reload) el.append(h('button', { class: 'link', title: 'Reload the canvas', onclick: () => loadPreview() }, 'Reload canvas'));
el.title = text || '';
el.className = `preview-status${text ? '' : ' hidden'}${level ? ` ${level}` : ''}`;
}

Expand Down Expand Up @@ -116,8 +119,11 @@ window.addEventListener('message', (e) => {
onKey({ key: msg.key, ctrlKey: msg.ctrl, metaKey: false, shiftKey: msg.shift, altKey: msg.alt, preventDefault() {}, target: document.body });
} else if (msg.type === 'error') {
document.body.dataset.previewError = msg.message;
previewStatus(msg.boot ? 'UI5 did not load - see the canvas' : `The view ${msg.runtime ? 'failed at runtime' : 'does not render'}: ${msg.message}`, 'error');
previewStatus(msg.boot ? 'UI5 did not load - see the canvas' : msg.runtime ? `The view failed at runtime: ${msg.message}` : msg.message, 'error', true);
count();
} else if (msg.type === 'ui5log') {
// UI5's own error: the view may show, but not as meant
if (!document.body.dataset.previewError) previewStatus(`UI5 reports: ${msg.message}`, 'warn', true);
} else if (msg.type === 'rendered') {
delete document.body.dataset.previewError;
previewStatus('');
Expand Down
2 changes: 2 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ button.cmd { display: flex; gap: 10px; align-items: center; min-width: 210px; ju
.canvas-pane { display: flex; justify-content: center; padding: 14px; min-height: 0; min-width: 0; background: var(--panel-2); position: relative; }
.preview-status { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 3; max-width: calc(100% - 40px); padding: 5px 12px; border-radius: 8px; font-size: 12px; background: var(--panel); color: var(--muted); border: 1px solid var(--line); box-shadow: 0 2px 8px rgba(0,0,0,.12); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.preview-status.error { color: var(--error); border-color: var(--error); }
.preview-status.warn { color: var(--warn); border-color: var(--warn); }
.preview-status button.link { margin-left: 10px; font-size: 12px; }
.device-frame { width: 100%; height: 100%; max-width: 100%; transition: width .15s; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 0 0.125rem 0 rgba(34,53,72,.15), 0 0.5rem 1rem 0 rgba(34,53,72,.15); }
.device-frame iframe { width: 100%; height: 100%; border: 0; display: block; }
.code-pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; border-left: 1px solid var(--line); background: var(--panel); }
Expand Down
15 changes: 15 additions & 0 deletions test/e2e/designer.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,18 @@ test('a list inside a bound list repeats a table field of the row', async ({ des
expect(abap).toMatch(/n = `items` +v = `\{NODES\}`/);
await expect(preview(page).getByText('Second child')).toBeVisible();
});

test('a view UI5 cannot render says why on the canvas, and the next edit renders again', async ({ designer: page }) => {
const before = await page.evaluate(() => Number(document.body.dataset.renders || 0));
await page.evaluate(() => window.designer.commit((d) => {
d.root.aggs.content[0].aggs.content.push({ id: 'nbroken1', type: 'NoSuchControl', props: {}, events: {}, aggs: {} });
}));
await expect(page.locator('#previewStatus')).toContainText('does not render');
await expect(page.locator('#previewStatus')).toContainText('NoSuchControl');
await expect(page.locator('#previewStatus')).toHaveClass(/error/);
// the way back: undo, and the canvas renders again - no render stays stuck
await page.locator('#btnUndo').click();
await expect.poll(() => page.evaluate(() => Number(document.body.dataset.renders || 0))).toBeGreaterThan(before + 1);
await expect(page.locator('#previewStatus')).toBeHidden();
await expect(preview(page).getByText('Customer data')).toBeVisible();
});
Loading