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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ Also in the editor:
a `.clas.abap` file, or link it with `?src=<raw URL>` (the playground's
parameter; raw.githubusercontent.com, a github.com file page, or a gist)
or `?class=<name>&repo=<owner/repo>[&ref=<branch>][&path=<folder>]`.
The playground's **Open in Layout Editor** hands its app class over as
`#playground=<its share fragment>`, the format **Open in Playground**
writes, read the other way; the first file is the class.
The `z2ui5_cl_ui5_view_builder` chain becomes the design, the public
attributes the data, `client->_event( )` the event names. Export writes
the chain back into that class and leaves every other line as it was;
Expand Down
6 changes: 5 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { renderCode, highlightCode, setCode, setView, copyText, codeText } from
import { loadPreview, postPreview, sendPreview, renderEventLog } from './ui/preview.js';
import { handleDrop, copySelection, pasteSelection, addControlByName, addControl } from './ui/edit.js';
import { openPalette } from './ui/palette.js';
import { newDialog, importDialog, openClassDialog, openClassFromUrl, recentDialog, shareLink, openFile, saveDesign, saveToBrowser, exportZip, openInPlayground, openInVscode } from './ui/files.js';
import { newDialog, importDialog, openClassDialog, openClassFromUrl, openClassFromPlayground, recentDialog, shareLink, openFile, saveDesign, saveToBrowser, exportZip, openInPlayground, openInVscode } from './ui/files.js';
import { onKey } from './ui/keys.js';


Expand Down Expand Up @@ -298,5 +298,9 @@ function openLinkedDesign() {
openLinkedDesign();
window.addEventListener('hashchange', openLinkedDesign);

// a class the playground handed over: #playground=<its share fragment>
openClassFromPlayground();
window.addEventListener('hashchange', openClassFromPlayground);

// for the browser tests and the curious: the state and the generators
window.designer = { state, generateClass, generateClassSource, toXml, validate, select, commit, handleDrop, openPalette, copySelection, pasteSelection, addControlByName, addControl };
21 changes: 21 additions & 0 deletions src/handoff.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ export async function deflate(text) {
return new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream('deflate-raw'))).arrayBuffer());
}

export function fromBase64Url(text) {
const bin = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
}

export async function inflate(bytes) {
return new TextDecoder().decode(await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer());
}

// The way back: the playground's "Open in Layout Editor" hands its app class
// over as #playground=<the share fragment>, the format below read the other
// way. Refuses what is not that format rather than guessing at it.
export async function filesFromPlaygroundFragment(fragment) {
if (!fragment || fragment[0] !== '2') throw new Error('not a playground link of a version this editor reads');
const files = JSON.parse(await inflate(fromBase64Url(fragment.slice(1))));
if (!Array.isArray(files) || !files.length || files.some((f) => typeof f?.name !== 'string' || typeof f?.source !== 'string')) {
throw new Error('the link holds no files');
}
return files;
}

// the playground's fragment for one or more files: version 2
export async function playgroundFragment(files) {
const payload = JSON.stringify(files.map(({ name, source }) => ({ name, source })));
Expand Down
17 changes: 16 additions & 1 deletion src/ui/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { clone, walk, normalizeDesign, validate, checkAttributeName } from '../m
import { copyText } from './code.js';
import { fileName } from '../abap.js';
import { fromClass, classReport, classFiles, generateClassSource, sourceUrl, repoClassUrl } from '../clas.js';
import { playgroundLink, vscodeLink, PLAYGROUND_URL } from '../handoff.js';
import { playgroundLink, vscodeLink, filesFromPlaygroundFragment, PLAYGROUND_URL } from '../handoff.js';
import { zip } from '../zip.js';
import { state, commit, replaceDesign, showPanel, recentDesigns, designToLink, libraryDesigns, saveToLibrary, removeFromLibrary } from './store.js';
import { $, h, toast, toastError, download, dialog, choose } from './dom.js';
Expand Down Expand Up @@ -220,6 +220,21 @@ export function openClassFromUrl(params) {
fetchClass(url);
}

// #playground=<fragment>: a class the playground handed over (its "Open in
// Layout Editor"). The first file is the app; it replaces the design the way
// Open ABAP class does, and the address is cleaned either way.
export async function openClassFromPlayground() {
if (!location.hash.startsWith('#playground=')) return;
const fragment = location.hash.slice('#playground='.length);
history.replaceState(null, '', location.pathname + location.search);
try {
const [app] = await filesFromPlaygroundFragment(fragment);
importClassText(app.source);
} catch (err) {
toast(`Cannot open the class from the playground: ${err.message}`);
}
}

async function fetchClass(raw) {
try {
const href = sourceUrl(raw, location.href);
Expand Down
11 changes: 10 additions & 1 deletion test/handoff.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { allDesigns } from './fixtures.mjs';
import { generateClass } from '../src/abap.js';
import { playgroundLink, playgroundFragment, vscodeLink, PLAYGROUND_URL } from '../src/handoff.js';
import { playgroundLink, playgroundFragment, filesFromPlaygroundFragment, vscodeLink, PLAYGROUND_URL } from '../src/handoff.js';

const fromBase64Url = (text) => Uint8Array.from(atob(text.replaceAll('-', '+').replaceAll('_', '/')), (c) => c.charCodeAt(0));
const inflate = async (bytes) => new TextDecoder().decode(await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer());
Expand Down Expand Up @@ -39,3 +39,12 @@ test('the VS Code link carries the class for the extension\'s URI handler', asyn
assert.equal(params.get('name'), 'zcl_my_app.clas.abap');
assert.equal(await inflate(fromBase64Url(params.get('code'))), source);
});

test('a class the playground hands over (#playground=) reads back as the files it wrote', async () => {
const files = [{ name: 'zcl_my_app.clas.abap', source }, { name: 'zcl_helper.clas.abap', source: 'CLASS zcl_helper DEFINITION.' }];
assert.deepEqual(await filesFromPlaygroundFragment(await playgroundFragment(files)), files);
await assert.rejects(filesFromPlaygroundFragment('1abc'), /version/);
await assert.rejects(filesFromPlaygroundFragment(''), /version/);
const empty = await playgroundFragment([]);
await assert.rejects(filesFromPlaygroundFragment(empty), /no files/);
});
Loading