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
2 changes: 1 addition & 1 deletion UPSTREAM_HEAD
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8f1413b1873ed74df61610fa00159123fc8d1743
0f49fdc4d60f5f4c116e8b4e7b1eb99b841f0631
2 changes: 1 addition & 1 deletion run/input/UPSTREAM_COMMIT
Original file line number Diff line number Diff line change
@@ -1 +1 @@
23deacb95561364c3fe598ef9e7362acd9cdd803
0f49fdc4d60f5f4c116e8b4e7b1eb99b841f0631
45 changes: 32 additions & 13 deletions run/input/core/app/z2ui5/webapp/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sap.ui.define(
"z2ui5/model/models",
"z2ui5/core/Server",
"sap/ui/VersionInfo",
"z2ui5/core/DebugTool",
"z2ui5/core/DeveloperTools",
"z2ui5/core/Lib",
"z2ui5/core/AppState",
"z2ui5/Util",
Expand All @@ -16,7 +16,7 @@ sap.ui.define(
Models,
Server,
VersionInfo,
DebugTool,
DeveloperTools,
Lib,
AppState,
DateUtil,
Expand Down Expand Up @@ -60,11 +60,29 @@ sap.ui.define(
AppState.state.oDeviceModel = Models.createDeviceModel();
this.setModel(AppState.state.oDeviceModel, "device");

// Warm-load the messaging module so Lib.getMessaging's synchronous
// sap.ui.require resolves it before the first view is displayed.
// On UI5 2.x sap/ui/core/Messaging is the only messaging API (the
// sap.ui.getCore().getMessageManager() fallback is gone), and
// nothing else pulls the module into the graph - without this the
// message> model and validation collection would silently no-op.
// Only attempt it where the module exists (1.118+): on older releases
// (e.g. 1.71) the require would 404 and make the ui5loader retry
// loudly via synchronous XHR; there Lib.getMessaging falls back to
// sap.ui.getCore().getMessageManager() instead.
if (Lib.hasMessagingModule()) {
sap.ui.require(
["sap/ui/core/Messaging"],
() => {},
() => {},
);
}

this._initLaunchpad();
this._initVersionInfo();

this._installUnloadListener();
this._installDebugToolShortcut();
this._installDeveloperToolsShortcut();
this._installScrollListener();

// The stopped router removed with the manifest routing section used
Expand Down Expand Up @@ -96,13 +114,14 @@ sap.ui.define(
window.addEventListener(this._unloadEvent, this._boundUnload);
},

_installDebugToolShortcut() {
// Ctrl + F12 opens / closes the in-app debug tool.
_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.debugTool) state.debugTool = new DebugTool();
state.debugTool.toggle();
if (!state.developerTools)
state.developerTools = new DeveloperTools();
state.developerTools.toggle();
}
};
document.addEventListener("keydown", this._boundKeydown);
Expand Down Expand Up @@ -225,12 +244,12 @@ sap.ui.define(
capture: true,
});

// The debug tool 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.debugTool) {
AppState.state.debugTool.destroy();
AppState.state.debugTool = null;
// 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;
}

Server.endSession();
Expand Down
53 changes: 34 additions & 19 deletions run/input/core/app/z2ui5/webapp/cc/Dirty.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ sap.ui.define(
["sap/ui/core/Control", "z2ui5/core/Lib", "z2ui5/core/AppState"],
(Control, Lib, AppState) => {
"use strict";

// Every live Dirty instance that is currently dirty. The FLP dirty flag
// and the browser's onbeforeunload are single global slots, so the guard
// must reflect whether ANY instance is dirty - one instance clearing its
// own flag (or being destroyed) must not wipe another instance's unsaved
// guard (e.g. a main-view form plus a form in a dialog).
const dirtyControls = new Set();

function syncUnloadPrompt(anyDirty) {
window.onbeforeunload = anyDirty
? (e) => {
e.preventDefault();
e.returnValue = "";
}
: null;
}

return Control.extend("z2ui5.cc.Dirty", {
metadata: {
properties: {
Expand All @@ -16,38 +33,36 @@ sap.ui.define(
},
setIsDirty(val) {
this.setProperty("isDirty", val);
if (val) {
dirtyControls.add(this);
} else {
dirtyControls.delete(this);
}
this._applyDirtyState();
},

// Fallback for non-launchpad scenarios: ask the browser to confirm
// before leaving the page when something is unsaved.
const fallback = () => {
if (val) {
window.onbeforeunload = (e) => {
e.preventDefault();
e.returnValue = "";
};
} else {
window.onbeforeunload = null;
}
};

// Use the FLP dirty flag when running inside the Launchpad (SAPUI5
// only); otherwise fall back to the browser unload prompt.
// Apply the AGGREGATE dirty state (any instance dirty) to whichever
// mechanism is active: the FLP dirty flag inside the Launchpad (SAPUI5
// only), else the browser unload prompt.
_applyDirtyState() {
const anyDirty = dirtyControls.size > 0;
try {
const launchpad = AppState.state.oLaunchpad;
const hasFlpDirtyFlag =
launchpad?.Container?.setDirtyFlag && launchpad.ShellUIService;
if (hasFlpDirtyFlag) {
launchpad.Container.setDirtyFlag(val);
launchpad.Container.setDirtyFlag(anyDirty);
} else {
fallback();
syncUnloadPrompt(anyDirty);
}
} catch (e) {
Lib.logError("Dirty.setIsDirty: setDirtyFlag failed", e);
fallback();
syncUnloadPrompt(anyDirty);
}
},
exit() {
window.onbeforeunload = null;
dirtyControls.delete(this);
this._applyDirtyState();
},
renderer: { apiVersion: 2, render() {} },
});
Expand Down
7 changes: 5 additions & 2 deletions run/input/core/app/z2ui5/webapp/cc/Focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ sap.ui.define(
setFocusId(val) {
try {
this.setProperty("focusId", val);
const oElement = ViewSlots.byId("MAIN", val);
const oElement = ViewSlots.byIdOfOwner(this, val);
if (oElement) oElement.applyFocusInfo(oElement.getFocusInfo());
} catch (e) {
Lib.logError("Focus.setFocusId failed", e);
Expand All @@ -37,7 +37,10 @@ sap.ui.define(
onAfterRendering() {
if (!this._pendingFocus) return;
this._pendingFocus = false;
const oElement = ViewSlots.byId("MAIN", this.getProperty("focusId"));
const oElement = ViewSlots.byIdOfOwner(
this,
this.getProperty("focusId"),
);
if (!oElement) return;
try {
// Merge the additional selection info into the existing focus info,
Expand Down
5 changes: 4 additions & 1 deletion run/input/core/app/z2ui5/webapp/cc/MultiInputExt.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ sap.ui.define(
},
renderer: { apiVersion: 2, render() {} },
setControl() {
const input = ViewSlots.byId("MAIN", this.getProperty("MultiInputId"));
const input = ViewSlots.byIdOfOwner(
this,
this.getProperty("MultiInputId"),
);
if (!input || this.getProperty("checkInit")) return;
this.setProperty("checkInit", true);
try {
Expand Down
8 changes: 4 additions & 4 deletions run/input/core/app/z2ui5/webapp/cc/Scrolling.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ sap.ui.define(
},

_getDomInnerElement(id) {
const control = ViewSlots.byId("MAIN", id);
const control = ViewSlots.byIdOfOwner(this, id);
if (!control) return null;
return document.getElementById(`${control.getId()}-inner`);
},

_getScrollTop(item) {
try {
const control = ViewSlots.byId("MAIN", item.N);
const control = ViewSlots.byIdOfOwner(this, item.N);
// Some controls expose a scroll delegate; prefer it when available.
const delegate = control?.getScrollDelegate?.();
if (delegate) return delegate.getScrollTop();
Expand Down Expand Up @@ -78,7 +78,7 @@ sap.ui.define(

_restoreScrollPosition(item) {
try {
const control = ViewSlots.byId("MAIN", item.N);
const control = ViewSlots.byIdOfOwner(this, item.N);
if (control?.scrollTo) {
control.scrollTo(item.V);
return;
Expand All @@ -99,7 +99,7 @@ sap.ui.define(

try {
for (const item of items) {
const control = ViewSlots.byId("MAIN", item.N);
const control = ViewSlots.byIdOfOwner(this, item.N);
if (!control) continue;

// Restore immediately when rendered, otherwise once it is.
Expand Down
12 changes: 11 additions & 1 deletion run/input/core/app/z2ui5/webapp/cc/SmartMultiInputExt.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ sap.ui.define(

// Mirror each range entry with the visible token text + long key
// so the backend has enough info to re-render the input later.
// NOTE: this pairs the i-th range entry with the i-th token by
// position, which assumes getRangeData() and getTokens() are index
// aligned. That holds only while every token is a range token; a
// mix of plain value tokens and range tokens would misalign the
// captions. The wrapped control is the SAPUI5-only sap.ui.comp
// SmartMultiInput, so a token-identity pairing needs verification
// against that control before it can replace the index pairing.
const source = oEvent.getSource();
const tokens = source.getTokens();
const rangeData = source.getRangeData() || [];
Expand Down Expand Up @@ -104,7 +111,10 @@ sap.ui.define(
},
renderer: { apiVersion: 2, render() {} },
setControl() {
const input = ViewSlots.byId("MAIN", this.getProperty("multiInputId"));
const input = ViewSlots.byIdOfOwner(
this,
this.getProperty("multiInputId"),
);
if (!input || this.getProperty("checkInit")) return;
this.setProperty("checkInit", true);
try {
Expand Down
54 changes: 45 additions & 9 deletions run/input/core/app/z2ui5/webapp/cc/Tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,31 @@ sap.ui.define(
},

_getTreeBinding() {
const treeControl = ViewSlots.byId("MAIN", this.getProperty("tree_id"));
// Resolve the tree in the companion's OWN slot (main, popup,
// popover, nested): byIdOfOwner works next to a tree in a dialog,
// and unlike resolveById it never picks a same-id tree from another
// open slot.
const treeControl = ViewSlots.byIdOfOwner(
this,
this.getProperty("tree_id"),
);
return treeControl?.getBinding("items");
},

// Snapshots are keyed by tree_id so several trees on one page (e.g. a
// main-view tree plus a tree in a popup) keep independent state - a
// single shared slot would let one companion overwrite another's.
setBackend() {
try {
const binding = this._getTreeBinding();
AppState.state.treeState = binding
? binding.getCurrentTreeState()
: undefined;
const id = this.getProperty("tree_id");
if (!id) return;
// Only overwrite the snapshot when the binding is actually
// resolvable - a momentarily missing binding must not wipe a
// still-valid snapshot for this id.
if (binding) {
AppState.state.treeStates[id] = binding.getCurrentTreeState();
}
} catch (e) {
Lib.logError("Tree.setBackend: failed", e);
}
Expand All @@ -46,11 +61,34 @@ sap.ui.define(
},

onAfterRendering() {
if (!this._pendingTreeState) return;
this._pendingTreeState = false;
try {
const id = this.getProperty("tree_id");
const snapshot = id && AppState.state.treeStates[id];
if (!snapshot) return;
const binding = this._getTreeBinding();
if (binding) binding.setTreeState(AppState.state.treeState);
if (!binding) return;
// Restore exactly once per (snapshot, binding) pair. setBackend
// creates a fresh snapshot object each roundtrip, so a new
// snapshot or a rebuilt binding is what triggers a restore -
// NOT an incidental re-render (theme/density/parent
// invalidation), which would otherwise force a spurious
// refresh(true) and collapse the user's expansions back to the
// last roundtrip snapshot.
if (
this._appliedSnapshot === snapshot &&
this._appliedBinding === binding
) {
return;
}
this._appliedSnapshot = snapshot;
this._appliedBinding = binding;
// setTreeState only stores an INITIAL tree state - the binding
// adapter consumes it while creating its nodes, which already
// happened during this rendering cycle. Force a rebuild so the
// snapshot actually gets applied (headless-verified: without the
// refresh the tree stays collapsed).
binding.setTreeState(snapshot);
binding.refresh(true);
} catch (e) {
Lib.logError("Tree.onAfterRendering: setTreeState failed", e);
}
Expand All @@ -63,8 +101,6 @@ sap.ui.define(
oRm.style("display", "none");
oRm.openEnd();
oRm.close("span");
if (!AppState.state.treeState) return;
oControl._pendingTreeState = true;
},
},
});
Expand Down
2 changes: 1 addition & 1 deletion run/input/core/app/z2ui5/webapp/cc/UITableExt.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ sap.ui.define(
},

_getTable() {
return ViewSlots.byId("MAIN", this.getProperty("tableId"));
return ViewSlots.byIdOfOwner(this, this.getProperty("tableId"));
},

readFilter() {
Expand Down
4 changes: 2 additions & 2 deletions run/input/core/app/z2ui5/webapp/cc/UploadSetExt.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ sap.ui.define(

renderer: { apiVersion: 2, render() {} },
setControl() {
const uploadSet = ViewSlots.byId(
"MAIN",
const uploadSet = ViewSlots.byIdOfOwner(
this,
this.getProperty("uploadSetId"),
);
if (!uploadSet || this.getProperty("checkInit")) return;
Expand Down
Loading