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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,8 @@ The sample-app smoke gate diffs actual startup results against
adaptations always win. Added fill-ins are **load-gated**: each must actually
`require()` (self-referencing `abap2UI5/*` via the assembled core's own
`exports`, third-party via the cap adapter's `node_modules`); ones that fail
are dropped and reported. This is what keeps e.g. `z2ui5_cl_app_preload` —
whose deps resolve to a path that does not exist — out of the published
package.
are dropped and reported — a transpiled class whose dependencies resolve to
a path that does not exist never reaches the published package.
- **samples** (`run/output/samples` → `core/srv/app/samples`): flattened (every
class lands directly under `samples/`, keyed by bare class name) and
overwritten; the hand-written `samples/README.md` from `src/` stays.
Expand Down Expand Up @@ -216,7 +215,7 @@ lives in
[builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) — it
mirrors the published cap2UI5 app repo and only relies on the two framework
hooks `z2ui5_cl_util.register_app_class()` and
`z2ui5_cl_core_srv_draft.set_store()`.
`z2ui5_cl_ui5_srv_draft.set_store()`.

## External wiring

Expand Down
2 changes: 1 addition & 1 deletion adapters/cap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ npm start # → http://localhost:4404/z2ui5/webapp/index.html
```

Start any bundled app via URL parameter, e.g.
`?app_start=z2ui5_cl_app_hello_world` or `?app_start=z2ui5_cl_demo_app_001`.
`?app_start=z2ui5_cl_ui5_app_hi_world` or `?app_start=z2ui5_cl_smp_app_004`.

Drafts live in an in-memory `Map` — swap the `set_store` implementation for
anything durable. Samples that consume the CAP OData services (e.g.
Expand Down
2 changes: 1 addition & 1 deletion adapters/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ npm start # → http://localhost:4204/z2ui5/webapp/index.html
```

Start any bundled app via URL parameter, e.g.
`?app_start=z2ui5_cl_app_hello_world` or `?app_start=z2ui5_cl_demo_app_001`.
`?app_start=z2ui5_cl_ui5_app_hi_world` or `?app_start=z2ui5_cl_smp_app_004`.

Drafts live in an in-memory `Map` — swap the `set_store` implementation for
anything durable. Samples that consume the CAP OData services (e.g.
Expand Down
4 changes: 2 additions & 2 deletions adapters/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ npm start # → http://localhost:4104/z2ui5/webapp/index.html
Start any bundled app via URL parameter, e.g.

```
http://localhost:4104/z2ui5/webapp/index.html?app_start=z2ui5_cl_app_hello_world
http://localhost:4104/z2ui5/webapp/index.html?app_start=z2ui5_cl_demo_app_001
http://localhost:4104/z2ui5/webapp/index.html?app_start=z2ui5_cl_ui5_app_hi_world
http://localhost:4104/z2ui5/webapp/index.html?app_start=z2ui5_cl_smp_app_004
```

## What is (deliberately) different from the CAP project
Expand Down
2 changes: 1 addition & 1 deletion adapters/node/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* GET /resources/* local UI5 runtime (openui5-dist)
*
* Start: npm install && npm start → http://localhost:4104/z2ui5/webapp/index.html
* Apps: ?app_start=z2ui5_cl_app_hello_world (all bundled samples work)
* Apps: ?app_start=z2ui5_cl_ui5_app_hi_world (all bundled samples work)
*/
"use strict";

Expand Down
2 changes: 1 addition & 1 deletion adapters/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ are skipped at registry load with a console warning — everything else runs.
```bash
npm install # links ../../core + esbuild
npm run build # → dist/
npm run serve # → http://localhost:4304/index.html?app_start=z2ui5_cl_app_hello_world
npm run serve # → http://localhost:4304/index.html?app_start=z2ui5_cl_ui5_app_hi_world
```

`dist/` is a plain static site — GitHub Pages, S3 or nginx serve it as-is.
Expand Down
4 changes: 3 additions & 1 deletion adapters/web/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
const fs = require("fs");
const path = require("path");

const pkgRoot = path.dirname(require.resolve("abap2UI5/engine")).replace(/srv[\/\\]z2ui5$/, "");

Check warning on line 25 in adapters/web/build.js

View workflow job for this annotation

GitHub Actions / test

Unnecessary escape character: \/
const dist = path.join(__dirname, "dist");

// ---- 1. registry ---------------------------------------------------------
// subpath specifier per app-class location (must match the package exports map)
const SOURCES = [
{ dir: "srv/z2ui5/02", spec: (n) => `abap2UI5/${n}`, filter: (n) => /^z2ui5_cl_app_/.test(n) },
{ dir: "srv/z2ui5/01/04", spec: (n) => `abap2UI5/${n}`, filter: (n) => /^z2ui5_cl_ui5_app_/.test(n) },
{ dir: "srv/z2ui5/99/02", spec: (n) => `abap2UI5/${n}`, filter: (n) => /^z2ui5_cl_pop_/.test(n) },
{ dir: "srv/app", spec: (n) => `abap2UI5/app/${n}`, filter: (n) => /^z2ui5_c[lx]_/.test(n) },
{ dir: "srv/app/samples", spec: (n) => `abap2UI5/app/samples/${n}`, filter: (n) => /^z2ui5_c[lx]_/.test(n) },
Expand Down Expand Up @@ -78,6 +78,8 @@
"node:path": path.join(__dirname, "shims/path.js"),
crypto: path.join(__dirname, "shims/crypto.js"),
"node:crypto": path.join(__dirname, "shims/crypto.js"),
async_hooks: path.join(__dirname, "shims/async_hooks.js"),
"node:async_hooks": path.join(__dirname, "shims/async_hooks.js"),
},
external: ["@sap/cds", "openui5-dist"],
// CJS node-isms: __dirname feeds only the (shimmed, no-op) fs discovery
Expand Down
50 changes: 50 additions & 0 deletions adapters/web/shims/async_hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* async_hooks shim for the browser bundle — the framework only needs
* AsyncLocalStorage, and only to isolate the user-exit context per request
* (z2ui5_cl_ui5_user_exit._als).
*
* A page is single-threaded and answers one roundtrip at a time, so a single
* current-store slot is an exact stand-in: run( ) sets it for the duration of
* the callback (including its awaits) and restores the previous one after,
* which is the only nesting the framework produces.
*/
"use strict";

class AsyncLocalStorage {
constructor() {
this._store = undefined;
}

run(store, fn, ...args) {
const previous = this._store;
this._store = store;
try {
const out = fn(...args);
// async callback: keep the store alive until it settles
if (out && typeof out.then === "function") {
return out.finally(() => {
this._store = previous;
});
}
this._store = previous;
return out;
} catch (e) {
this._store = previous;
throw e;
}
}

getStore() {
return this._store;
}

enterWith(store) {
this._store = store;
}

exit(fn, ...args) {
return this.run(undefined, fn, ...args);
}
}

module.exports = { AsyncLocalStorage };
151 changes: 85 additions & 66 deletions core/app/z2ui5/webapp/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@ sap.ui.define(
"z2ui5/model/models",
"z2ui5/core/Server",
"sap/ui/VersionInfo",
"z2ui5/core/DeveloperTools",
"z2ui5/devtools/DevTools",
"z2ui5/core/Lib",
"z2ui5/core/AppState",
"z2ui5/Util",
"z2ui5/model/formatter",
"sap/ui/core/routing/HashChanger",
"z2ui5/core/Router",
"z2ui5/core/ScrollFocus",
],
(
UIComponent,
Models,
Server,
VersionInfo,
DeveloperTools,
DevTools,
Lib,
AppState,
DateUtil,
Formatter,
HashChanger,
Router,
ScrollFocus,
) => {
"use strict";

Expand All @@ -39,6 +41,36 @@ sap.ui.define(
// a fully initialized global from here on.
AppState.initGlobal();

// Two sibling BSPs carry frontend artefacts the framework itself does
// not ship: z2ui5_cci (abap2UI5-addons/custom-controls) and z2ui5_ccc
// (abap2UI5/customer-frontend-extension, the customer's own library).
// The two roots differ in one letter and are NOT the same BSP - each
// matches the ABAP prefix of its repository (z2ui5_cl_cci for the
// community controls, z2ui5_cl_ccc for the customer extension).
// Both are normally found through their reserved resourceRoot in
// manifest.json ("z2ui5_cci": "../z2ui5_cci/", "z2ui5_ccc":
// "../z2ui5_ccc/"), a sibling of THIS BSP. In the standalone HTTP
// service there is no BSP for them to be a sibling of, so the backend
// hands the absolute paths over on the global instead
// (z2ui5_cl_http_handler=>_http_get).
//
// They have to be applied HERE and not in the page: the manifest
// registers its own value while the component is being created, which
// is after everything the shell can run, so a registration made there
// is overwritten again. init() runs after manifest processing.
// Absent in BSP and Launchpad mode, where the manifest entries are
// right. Neither BSP is loaded from here - nothing is requested until
// a view actually names the namespace - so a system that has only one
// of them installed (or neither) never pays for the other.
const ccResourceRoot = AppState.getGlobal("ccResourceRoot");
if (ccResourceRoot) {
sap.ui.loader.config({ paths: { z2ui5_cci: ccResourceRoot } });
}
const cccResourceRoot = AppState.getGlobal("cccResourceRoot");
if (cccResourceRoot) {
sap.ui.loader.config({ paths: { z2ui5_ccc: cccResourceRoot } });
}

UIComponent.prototype.init.call(this);

AppState.getGlobal("oConfig").ComponentData = this.getComponentData();
Expand Down Expand Up @@ -82,20 +114,14 @@ sap.ui.define(
this._initVersionInfo();

this._installUnloadListener();
this._installDeveloperToolsShortcut();
// The developer tools own everything of their own: the Ctrl+F12
// shortcut, the dialog instance, the roundtrip recorder and the
// "?z2ui5-devtools=" auto open. This call and the exit() below are
// the framework's ENTIRE coupling to devtools/ - keep it that
// way (see the module header there).
DevTools.install();
this._installScrollListener();
this._installRouterListener();

// The stopped router removed with the manifest routing section used
// to initialize the HashChanger (and its underlying hasher
// singleton) as a side effect. Without that init hasher never
// learns the URL's current hash, so the app-state cleanup after
// every roundtrip (View1._updateBrowserHistory calling
// replaceHash("")) is treated as a change and rewrites the URL to
// "...#" - every app start ended with a dangling "#". Initialize it
// explicitly; inside the FLP the shell has already done this and
// init() is a guarded no-op.
HashChanger.getInstance().init();
},

// ------------------------------------------------------------------
Expand All @@ -104,57 +130,40 @@ sap.ui.define(

_installUnloadListener() {
this._boundUnload = this._onUnload.bind(this);
// Safari on iOS does not fire "beforeunload" reliably, so we use
// "pagehide" there. iPads on iPadOS 13+ report a Mac user agent
// ("desktop site" default) - the touch-point probe catches those,
// while real Macs report 0 touch points.
const isIos =
/iPad|iPhone/.test(navigator.userAgent) ||
(navigator.userAgent.includes("Mac") && navigator.maxTouchPoints > 1);
this._unloadEvent = isIos ? "pagehide" : "beforeunload";
// "pagehide", not "beforeunload": pagehide fires only after the
// navigation is committed (any "leave page?" prompt was answered),
// so tearing the app down here can neither swallow the cc/Dirty
// unsaved-changes prompt (destroying the app mid-beforeunload
// removed its window.onbeforeunload handler before the browser
// invoked it) nor kill the live session when the user chooses to
// stay. It is also the reliable event on iOS Safari, which never
// fired beforeunload dependably.
this._unloadEvent = "pagehide";
window.addEventListener(this._unloadEvent, this._boundUnload);
},

_installDeveloperToolsShortcut() {
// Ctrl + F12 opens / closes the in-app developer tools.
this._boundKeydown = (event) => {
if (event.ctrlKey && event.key === "F12") {
const state = AppState.state;
if (!state.developerTools)
state.developerTools = new DeveloperTools();
state.developerTools.toggle();
}
};
document.addEventListener("keydown", this._boundKeydown);
},

_installScrollListener() {
// Scroll events do not bubble, but they do trigger capture-phase
// listeners on ancestors - a single document-level listener observes
// every scrollable container. Server.onScrollCapture records the
// every scrollable container. ScrollFocus.onScrollCapture records the
// last scrolled element per view slot for the S_SCROLL request info.
this._boundScroll = (event) => Server.onScrollCapture(event);
this._boundScroll = (event) => ScrollFocus.onScrollCapture(event);
document.addEventListener("scroll", this._boundScroll, {
capture: true,
passive: true,
});
},

_installRouterListener() {
// Hash-based app routing (UI5 Router style). The HashChanger is the
// same engine the sap.ui.core.routing.Router sits on; listening to its
// hashChanged event makes the native browser Back/Forward buttons (and
// manual URL edits / bookmarks) drive navigation - a hash of the form
// "#/app/<CLASS>" starts that app. Only sessions that opted in via
// client->set_nav_routing( ) act on it (Server.onHashChange guards on
// AppState.navRouting), so apps that manage their own hash are
// unaffected.
this._boundHashChanged = (oEvent) =>
Server.onHashChange(oEvent.getParameter("newHash"));
HashChanger.getInstance().attachEvent(
"hashChanged",
this._boundHashChanged,
);
// Hash-based app routing (UI5 Router style), owned by core/Router.js.
// It sits on the HashChanger - the same engine
// sap.ui.core.routing.Router uses, and inside the FLP the shell's own
// one - so the native browser Back/Forward buttons and the launchpad
// back button drive navigation. Only apps that opted in via
// follow_up_action( cs_event-set_nav_routing ) act on it, so apps that manage their own
// hash are unaffected. Server does the actual restore roundtrip; it is
// injected here so the router stays free of a Server dependency.
Router.init(() => Server.restoreFromRoute());
},

// ------------------------------------------------------------------
Expand Down Expand Up @@ -246,7 +255,10 @@ sap.ui.define(
return "";
},

_onUnload() {
_onUnload(event) {
// pagehide with persisted = true means the page enters the browser's
// back/forward cache and may be shown again - keep the app alive.
if (event?.persisted) return;
// destroy() runs exit(), which removes the unload listener (and every
// other one) - no need to remove it here too.
this.destroy();
Expand All @@ -258,25 +270,32 @@ sap.ui.define(

exit() {
window.removeEventListener(this._unloadEvent, this._boundUnload);
document.removeEventListener("keydown", this._boundKeydown);
document.removeEventListener("scroll", this._boundScroll, {
capture: true,
});
HashChanger.getInstance().detachEvent(
"hashChanged",
this._boundHashChanged,
);
Router.exit();

// The developer tools control is created lazily by the Ctrl+F12
// shortcut - destroy it (which also closes its dialog) so a re-launch
// (FLP) does not leak the control instance.
if (AppState.state.developerTools) {
AppState.state.developerTools.destroy();
AppState.state.developerTools = null;
}
// Drops the shortcut, the dialog instance and the recorded history -
// all of which are module-scoped and would otherwise outlive the
// component on an FLP re-launch.
DevTools.exit();

Server.endSession();

// Global state that would outlive the component (FLP keeps the page
// alive): cancel any pending backend timer, empty the shortcut
// registry so the module-scoped keydown listener becomes a no-op,
// and detach the device model's handlers from the Device singleton.
for (const key of Object.keys(AppState.state.timers)) {
clearTimeout(AppState.state.timers[key]);
delete AppState.state.timers[key];
}
AppState.state.shortcuts = {};
if (AppState.state.oDeviceModel) {
AppState.state.oDeviceModel.destroy();
AppState.state.oDeviceModel = null;
}

// Robust launchpad teardown:
// 1. Clear the FLP dirty flag so it does not carry over into the
// next app the user opens.
Expand Down
1 change: 0 additions & 1 deletion core/app/z2ui5/webapp/cc/CameraPicture.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ sap.ui.define(
return Control.extend("z2ui5.cc.CameraPicture", {
metadata: {
properties: {
id: { type: "string" },
value: { type: "string" },
thumbnail: { type: "string" },
// Empty default leaves the trigger button auto-sized; a bare
Expand Down
6 changes: 3 additions & 3 deletions core/app/z2ui5/webapp/cc/Dirty.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ sap.ui.define(
},
},
setIsDirty(val) {
// Empty renderer -> suppress the no-op invalidation; the dirty state
// is applied explicitly below.
// Empty renderer -> suppress the no-op invalidation; the effect below
// (applying the dirty state) is what actually matters.
this.setProperty("isDirty", val, true);
if (val) {
dirtyControls.add(this);
Expand All @@ -58,7 +58,7 @@ sap.ui.define(
syncUnloadPrompt(anyDirty);
}
} catch (e) {
Lib.logError("Dirty.setIsDirty: setDirtyFlag failed", e);
Lib.logError("Dirty._applyDirtyState: setDirtyFlag failed", e);
syncUnloadPrompt(anyDirty);
}
},
Expand Down
Loading
Loading