@sapui5/sap.fe.templates 1.147.0 → 1.148.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/sap/fe/templates/.library +1 -1
  3. package/src/sap/fe/templates/AnalyticalListPage/manifest.json +1 -1
  4. package/src/sap/fe/templates/ListReport/ListReportController.controller.js +4 -14
  5. package/src/sap/fe/templates/ListReport/ListReportController.controller.ts +2 -12
  6. package/src/sap/fe/templates/ListReport/controls/MultipleModeControl.js +4 -1
  7. package/src/sap/fe/templates/ListReport/controls/MultipleModeControl.ts +3 -0
  8. package/src/sap/fe/templates/ListReport/manifest.json +1 -1
  9. package/src/sap/fe/templates/ObjectPage/ExtensionAPI.js +134 -24
  10. package/src/sap/fe/templates/ObjectPage/ExtensionAPI.ts +132 -19
  11. package/src/sap/fe/templates/ObjectPage/ObjectPageController.controller.js +169 -17
  12. package/src/sap/fe/templates/ObjectPage/ObjectPageController.controller.ts +195 -19
  13. package/src/sap/fe/templates/ObjectPage/ObjectPageTemplating.js +13 -8
  14. package/src/sap/fe/templates/ObjectPage/ObjectPageTemplating.ts +17 -9
  15. package/src/sap/fe/templates/ObjectPage/components/CollaborationDiscardDialog.js +4 -1
  16. package/src/sap/fe/templates/ObjectPage/components/CollaborationDiscardDialog.tsx +1 -0
  17. package/src/sap/fe/templates/ObjectPage/components/CollaborationDraft.js +15 -4
  18. package/src/sap/fe/templates/ObjectPage/components/CollaborationDraft.tsx +11 -2
  19. package/src/sap/fe/templates/ObjectPage/controls/StashableHBox.js +28 -1
  20. package/src/sap/fe/templates/ObjectPage/controls/StashableHBox.ts +31 -0
  21. package/src/sap/fe/templates/ObjectPage/helpers/SectionNavigationHelper.js +72 -0
  22. package/src/sap/fe/templates/ObjectPage/helpers/SectionNavigationHelper.ts +75 -0
  23. package/src/sap/fe/templates/ObjectPage/manifest.json +1 -15
  24. package/src/sap/fe/templates/ObjectPage/overrides/CollaborationManager.js +29 -20
  25. package/src/sap/fe/templates/ObjectPage/overrides/CollaborationManager.ts +28 -21
  26. package/src/sap/fe/templates/ObjectPage/overrides/IntentBasedNavigation.js +4 -3
  27. package/src/sap/fe/templates/ObjectPage/overrides/IntentBasedNavigation.ts +4 -4
  28. package/src/sap/fe/templates/ObjectPage/overrides/ViewState.js +93 -17
  29. package/src/sap/fe/templates/ObjectPage/overrides/ViewState.ts +108 -22
  30. package/src/sap/fe/templates/ObjectPage/view/fragments/EmphasizedFirstHeaderAction.fragment.xml +1 -0
  31. package/src/sap/fe/templates/ObjectPage/view/fragments/ExpandedHeading.fragment.xml +4 -4
  32. package/src/sap/fe/templates/ObjectPage/view/fragments/Heading.fragment.xml +4 -4
  33. package/src/sap/fe/templates/ObjectPage/view/fragments/ObjectPageHeaderAddress.fragment.xml +1 -1
  34. package/src/sap/fe/templates/ObjectPage/view/fragments/ObjectPageHeaderContact.fragment.xml +1 -1
  35. package/src/sap/fe/templates/ObjectPage/view/fragments/ObjectPageHeaderForm.fragment.xml +1 -0
  36. package/src/sap/fe/templates/library.js +1 -1
  37. package/src/sap/fe/templates/messagebundle_no.properties +1 -1
  38. package/src/sap/fe/templates/messagebundle_uk.properties +1 -1
@@ -48,6 +48,7 @@ import type { default as ObjectPageExtensionAPI } from "sap/fe/templates/ObjectP
48
48
  import { default as ExtensionAPI } from "sap/fe/templates/ObjectPage/ExtensionAPI";
49
49
  import CollaborationDiscard from "sap/fe/templates/ObjectPage/components/CollaborationDiscardDialog";
50
50
  import type SubSectionBlock from "sap/fe/templates/ObjectPage/controls/SubSectionBlock";
51
+ import { pollForSectionStability } from "sap/fe/templates/ObjectPage/helpers/SectionNavigationHelper";
51
52
  import TableScroller from "sap/fe/templates/TableScroller";
52
53
  import type Button from "sap/m/Button";
53
54
  import type InputBase from "sap/m/InputBase";
@@ -55,6 +56,7 @@ import InstanceManager from "sap/m/InstanceManager";
55
56
  import type MenuButton from "sap/m/MenuButton";
56
57
  import type NavContainer from "sap/m/NavContainer";
57
58
  import type Popover from "sap/m/Popover";
59
+ import type SearchField from "sap/m/SearchField";
58
60
  import type ToolbarSpacer from "sap/m/ToolbarSpacer";
59
61
  import Device from "sap/ui/Device";
60
62
  import type UI5Event from "sap/ui/base/Event";
@@ -104,6 +106,8 @@ export type BindingParams = {
104
106
  bPersistOPScroll?: boolean;
105
107
  listBinding?: ODataListBinding;
106
108
  showPlaceholder?: boolean;
109
+ requestOnBinding?: string[];
110
+ targetControlId?: string;
107
111
  };
108
112
 
109
113
  const ProgrammingModel = FELibrary.ProgrammingModel;
@@ -189,6 +193,13 @@ class ObjectPageController extends PageController {
189
193
 
190
194
  private previousBindingContextPath?: string;
191
195
 
196
+ private pendingTargetControlId?: string;
197
+
198
+ private targetSubSection?: ObjectPageSubSection;
199
+
200
+ // Tracks if initial ObjectPage load has completed
201
+ private hasInitialLoadCompleted = false;
202
+
192
203
  /**
193
204
  * Returns controls (Button / MenuButton) that are marked as "primary action".
194
205
  * The marker is set by the converters when an action is emphasized (annotation-based) or
@@ -429,6 +440,9 @@ class ObjectPageController extends PageController {
429
440
  }
430
441
 
431
442
  _onBeforeBinding(oContext: ODataV4Context, mParameters: BindingParams = {}): void {
443
+ // Store targetControlId for scrolling after binding completes
444
+ this.pendingTargetControlId = mParameters.targetControlId;
445
+
432
446
  // TODO: we should check how this comes together with the transaction helper, same to the change in the afterBinding
433
447
  this.previousBindingContextPath = this.getView().getBindingContext()?.getPath();
434
448
  const aTables = this._findTables(),
@@ -690,6 +704,7 @@ class ObjectPageController extends PageController {
690
704
  /**
691
705
  * Set the initial focus in edit mode.
692
706
  * @param aSubSections Object page sub sections
707
+ * @param fromTabNavigation Indicates if the focus is set after a tab navigation
693
708
  */
694
709
  _updateFocusInEditMode(aSubSections: ObjectPageSubSection[], fromTabNavigation = false): void {
695
710
  setTimeout(
@@ -697,7 +712,7 @@ class ObjectPageController extends PageController {
697
712
  // We set the focus in a timeeout, otherwise the focus sometimes goes to the TabBar
698
713
  const oObjectPage = this._getObjectPageLayoutControl();
699
714
  const oMandatoryField = this._getFirstEmptyMandatoryFieldFromSubSection(aSubSections);
700
- let oFieldToFocus;
715
+ let oFieldToFocus: UI5Element | undefined;
701
716
  if (oMandatoryField) {
702
717
  if (oMandatoryField.isA("sap.fe.macros.MultiValueField")) {
703
718
  oFieldToFocus = (oMandatoryField as unknown as MultiValueFieldBlock).getMultiValueField();
@@ -713,9 +728,22 @@ class ObjectPageController extends PageController {
713
728
  const focusInfo = oFieldToFocus.getFocusInfo() as { targetInfo: object };
714
729
  focusInfo.targetInfo = { silent: true };
715
730
  if (oFieldToFocus.isA("sap.ui.mdc.field.FieldInput")) {
716
- oFieldToFocus = oFieldToFocus.getParent();
731
+ oFieldToFocus = oFieldToFocus.getParent() as UI5Element | undefined;
732
+ }
733
+
734
+ if (
735
+ oFieldToFocus?.isA<SearchField>("sap.m.SearchField") &&
736
+ oFieldToFocus.getEnableSuggestions() &&
737
+ !oFieldToFocus.isBound("enableSuggestions")
738
+ ) {
739
+ // In case the focus is set on a search field with suggestions, we temporarily disable the suggestions to avoid triggering them on focus
740
+ // unless this property is bound (to avoid modifying something in a model)
741
+ oFieldToFocus.setEnableSuggestions(false);
742
+ oFieldToFocus.focus(focusInfo);
743
+ oFieldToFocus.setEnableSuggestions(true);
744
+ } else {
745
+ oFieldToFocus?.focus(focusInfo);
717
746
  }
718
- oFieldToFocus.focus(focusInfo);
719
747
  }
720
748
  }.bind(this),
721
749
  fromTabNavigation ? 300 : 0
@@ -777,15 +805,24 @@ class ObjectPageController extends PageController {
777
805
 
778
806
  // Function to navigate back, or display the launchpad if we're on the first page of the history
779
807
  const navBack = (): void => {
780
- const currentURL = document.URL;
781
- history.back();
782
- // In case there is no previous page in the history, history.back does nothing.
783
- // In this case, we need to use navigateBackFromContext, that will display the home page
784
- setTimeout(() => {
785
- if (document.URL === currentURL) {
786
- this._routing.navigateBackFromContext(oContext);
787
- }
788
- }, 500);
808
+ // Use the new Navigation API if it's available in the browser, to check if we can go back in history.
809
+ const canGoBack = (window as { navigation?: { canGoBack?: boolean } }).navigation?.canGoBack;
810
+ if (canGoBack === true) {
811
+ history.back();
812
+ } else if (canGoBack === false) {
813
+ this._routing.navigateBackFromContext(oContext);
814
+ } else {
815
+ // Fallback in case the browser doesn't support the Navigation API
816
+ const currentURL = document.URL;
817
+ history.back();
818
+ // In case there is no previous page in the history, history.back does nothing.
819
+ // In this case, we need to use navigateBackFromContext, that will display the home page
820
+ setTimeout(() => {
821
+ if (document.URL === currentURL) {
822
+ this._routing.navigateBackFromContext(oContext);
823
+ }
824
+ }, 500);
825
+ }
789
826
  };
790
827
 
791
828
  if (this.getAppComponent().getRouterProxy().checkIfBackHasSameContext()) {
@@ -826,7 +863,7 @@ class ObjectPageController extends PageController {
826
863
  this.editFlow.storeSiblingContextData(inputBindingContext);
827
864
  }
828
865
 
829
- this._sideEffects.clearFieldGroupsValidity();
866
+ this.sideEffects.clearFieldGroupsValidity();
830
867
 
831
868
  // TODO: this is only a temp solution as long as the model fix the cache issue and we use this additional
832
869
  // binding with ownRequest
@@ -984,6 +1021,143 @@ class ObjectPageController extends PageController {
984
1021
  applyAppState,
985
1022
  this.previousBindingContextPath !== this.getView().getBindingContext()?.getPath()
986
1023
  );
1024
+
1025
+ // Capture synchronously to avoid race with subsequent navigations
1026
+ const pendingTargetControlId = this.pendingTargetControlId;
1027
+ this.pendingTargetControlId = undefined;
1028
+
1029
+ this.handlePendingTargetControlIdNavigation(applyAppState, oObjectPage, pendingTargetControlId);
1030
+ }
1031
+
1032
+ /**
1033
+ * Handles navigation to a target section or subsection after app state is applied.
1034
+ * This coordinates targetControlId navigation from two sources:
1035
+ * - In-app navigation: pendingTargetControlId captured before applyAppState.
1036
+ * - Cross-app navigation: xAppState targetControlId stored by AppStateHandler during LR to OP transition.
1037
+ * @param applyAppState Promise that resolves when app state has been applied.
1038
+ * @param oObjectPage The ObjectPageLayout control.
1039
+ * @param pendingTargetControlId The targetControlId captured before applyAppState, if any.
1040
+ */
1041
+ private handlePendingTargetControlIdNavigation(
1042
+ applyAppState: Promise<void | object>,
1043
+ oObjectPage: ObjectPageLayout,
1044
+ pendingTargetControlId: string | undefined
1045
+ ): void {
1046
+ applyAppState
1047
+ .then(async () => {
1048
+ const xAppStateTargetControlId = this.getAppComponent().getAppStateHandler()?.consumePendingTargetControlId();
1049
+ const targetControlId = pendingTargetControlId ?? xAppStateTargetControlId;
1050
+ if (!targetControlId) {
1051
+ return;
1052
+ }
1053
+
1054
+ // Wait for visibility bindings (needed for lazy loading scenarios)
1055
+ try {
1056
+ await Promise.all(this.waitForVisibilityBindings);
1057
+ } catch {
1058
+ // Some visibility bindings failed to resolve, proceeding anyway
1059
+ }
1060
+
1061
+ // Wait for pageReady to ensure ObjectPage is fully initialized
1062
+ await this.pageReady.waitPageReady();
1063
+
1064
+ // Navigate to target section
1065
+ this.navigateToTargetSection(oObjectPage, targetControlId);
1066
+ return; // Required by linter rule promise/always-return
1067
+ })
1068
+ .catch(() => {
1069
+ // applyAppState failed - skip targetControlId navigation
1070
+ });
1071
+ }
1072
+
1073
+ /**
1074
+ * Navigates to a target section with automatic re-navigation if UX rules reset the selection.
1075
+ * Polls the section state since setSelectedSection() doesn't fire the navigate event.
1076
+ * @param oObjectPage The ObjectPageLayout control.
1077
+ * @param targetControlId The local ID of the target section or subsection.
1078
+ */
1079
+ private navigateToTargetSection(oObjectPage: ObjectPageLayout, targetControlId: string): void {
1080
+ // Determine target section ID for comparison
1081
+ const targetControl = this.getView().byId(targetControlId);
1082
+ const isSection = targetControl?.isA("sap.uxap.ObjectPageSection");
1083
+ const targetSectionId = isSection ? targetControl?.getId() : (targetControl?.getParent() as ObjectPageSection)?.getId();
1084
+
1085
+ // Navigate function with guard to prevent duplicate calls when already on target
1086
+ const navigateToTarget = (): void => {
1087
+ if (oObjectPage.getSelectedSection() === targetSectionId) {
1088
+ return; // Already on target section
1089
+ }
1090
+ this.navigateWithConfirmation(targetControlId);
1091
+ };
1092
+
1093
+ // Navigate immediately
1094
+ navigateToTarget();
1095
+
1096
+ // Skip polling if already on target section (navigation was no-op or succeeded immediately)
1097
+ if (oObjectPage.getSelectedSection() === targetSectionId) {
1098
+ this.hasInitialLoadCompleted = true;
1099
+ return;
1100
+ }
1101
+
1102
+ // Skip polling on subsequent navigations
1103
+ if (this.hasInitialLoadCompleted) {
1104
+ return;
1105
+ }
1106
+
1107
+ // Poll to detect UX rules reset during initial load
1108
+ // Uses shared utility from helpers/SectionNavigationHelper.ts
1109
+ pollForSectionStability({
1110
+ getSelectedSection: () => oObjectPage.getSelectedSection(),
1111
+ targetSectionId,
1112
+ onResetDetected: navigateToTarget
1113
+ })
1114
+ .then(() => {
1115
+ this.hasInitialLoadCompleted = true;
1116
+ return; // Required by linter rule promise/always-return
1117
+ })
1118
+ .catch(() => {
1119
+ // Required by linter rule promise/catch-or-return - polling failures are non-critical
1120
+ });
1121
+ }
1122
+
1123
+ /**
1124
+ * Sets targetSubSection based on the target control ID for app state persistence.
1125
+ * @param targetControlId The local ID of the target section or subsection.
1126
+ */
1127
+ private setTargetSubSection(targetControlId: string): void {
1128
+ const targetControl = this.getView().byId(targetControlId);
1129
+ if (targetControl?.isA<ObjectPageSubSection>("sap.uxap.ObjectPageSubSection")) {
1130
+ this.targetSubSection = targetControl;
1131
+ } else if (targetControl?.isA<ObjectPageSection>("sap.uxap.ObjectPageSection")) {
1132
+ const subSections = targetControl.getSubSections();
1133
+ this.targetSubSection = subSections[0];
1134
+ }
1135
+ }
1136
+
1137
+ /**
1138
+ * Performs navigation to the target section and then updates the app state.
1139
+ * @param targetControlId The local ID of the target section or subsection.
1140
+ */
1141
+ private navigateWithConfirmation(targetControlId: string): void {
1142
+ // Set targetSubSection BEFORE navigation for app state saving.
1143
+ // scrollToSection() doesn't fire the navigate event, so onNavigateChange won't be called.
1144
+ this.setTargetSubSection(targetControlId);
1145
+
1146
+ this.getExtensionAPI().navigateToSubSection(targetControlId);
1147
+
1148
+ // Wait for ExtensionAPI polling to complete (~100ms) plus scroll to finish
1149
+ // before updating app state and setting focus.
1150
+ setTimeout(() => {
1151
+ this.viewState.updateAppStateDebounced();
1152
+
1153
+ // Set focus if in edit mode (for in-app navigation to sub-object page in edit mode)
1154
+ // This is needed because scrollToSection() doesn't fire onNavigateChange,
1155
+ // so we need to manually trigger focus setting here
1156
+ const isInEditMode = CommonUtils.getIsEditable(this.getView());
1157
+ if (isInEditMode && this.targetSubSection) {
1158
+ this._updateFocusInEditMode([this.targetSubSection], false);
1159
+ }
1160
+ }, 150);
987
1161
  }
988
1162
 
989
1163
  /**
@@ -1384,9 +1558,7 @@ class ObjectPageController extends PageController {
1384
1558
  // If there is at least one global SideEffects for the related entity, execute it/them
1385
1559
  if (globalSideEffects.length) {
1386
1560
  await this.editFlow.syncTask();
1387
- return Promise.all(
1388
- globalSideEffects.map(async (sideEffects) => this._sideEffects.requestSideEffects(sideEffects, context))
1389
- );
1561
+ return Promise.all(globalSideEffects.map(async (sideEffects) => this.sideEffects.requestSideEffects(sideEffects, context)));
1390
1562
  }
1391
1563
  }
1392
1564
 
@@ -2204,17 +2376,21 @@ class ObjectPageController extends PageController {
2204
2376
 
2205
2377
  onNavigateChange(this: ObjectPageController, oEvent: UI5Event<{ subSection: ObjectPageSubSection }>): void {
2206
2378
  //will be called always when we click on a section tab
2207
- this.getExtensionAPI().updateAppState();
2208
2379
  this.bSectionNavigated = true;
2209
2380
 
2381
+ const subSection = oEvent.getParameter("subSection");
2382
+ this.targetSubSection = subSection;
2383
+
2384
+ this.getExtensionAPI().updateAppState();
2385
+
2210
2386
  const oInternalModelContext = this.getView().getBindingContext("internal") as InternalModelContext;
2387
+
2211
2388
  if (
2212
2389
  CommonUtils.getIsEditable(this.getView()) &&
2213
2390
  this.getView().getViewData().sectionLayout === "Tabs" &&
2214
2391
  oInternalModelContext.getProperty("errorNavigationSectionFlag") === false
2215
2392
  ) {
2216
- const oSubSection = oEvent.getParameter("subSection");
2217
- this._updateFocusInEditMode([oSubSection], true);
2393
+ this._updateFocusInEditMode([subSection], true);
2218
2394
  }
2219
2395
  },
2220
2396
  onVariantSelected: function (this: ObjectPageController): void {