@sapui5/sap.fe.templates 1.147.0 → 1.148.1

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 (41) 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/ListReport.view.xml +2 -2
  5. package/src/sap/fe/templates/ListReport/ListReportController.controller.js +10 -16
  6. package/src/sap/fe/templates/ListReport/ListReportController.controller.ts +10 -17
  7. package/src/sap/fe/templates/ListReport/controls/MultipleModeControl.js +4 -1
  8. package/src/sap/fe/templates/ListReport/controls/MultipleModeControl.ts +3 -0
  9. package/src/sap/fe/templates/ListReport/manifest.json +1 -1
  10. package/src/sap/fe/templates/ObjectPage/ExtensionAPI.js +134 -24
  11. package/src/sap/fe/templates/ObjectPage/ExtensionAPI.ts +132 -19
  12. package/src/sap/fe/templates/ObjectPage/ObjectPageController.controller.js +195 -20
  13. package/src/sap/fe/templates/ObjectPage/ObjectPageController.controller.ts +226 -22
  14. package/src/sap/fe/templates/ObjectPage/ObjectPageTemplating.js +16 -8
  15. package/src/sap/fe/templates/ObjectPage/ObjectPageTemplating.ts +20 -9
  16. package/src/sap/fe/templates/ObjectPage/components/CollaborationDiscardDialog.js +4 -1
  17. package/src/sap/fe/templates/ObjectPage/components/CollaborationDiscardDialog.tsx +1 -0
  18. package/src/sap/fe/templates/ObjectPage/components/CollaborationDraft.js +15 -4
  19. package/src/sap/fe/templates/ObjectPage/components/CollaborationDraft.tsx +11 -2
  20. package/src/sap/fe/templates/ObjectPage/controls/StashableHBox.js +28 -1
  21. package/src/sap/fe/templates/ObjectPage/controls/StashableHBox.ts +31 -0
  22. package/src/sap/fe/templates/ObjectPage/controls/SubSectionBlock.js +9 -3
  23. package/src/sap/fe/templates/ObjectPage/controls/SubSectionBlock.ts +9 -1
  24. package/src/sap/fe/templates/ObjectPage/helpers/SectionNavigationHelper.js +72 -0
  25. package/src/sap/fe/templates/ObjectPage/helpers/SectionNavigationHelper.ts +75 -0
  26. package/src/sap/fe/templates/ObjectPage/manifest.json +1 -15
  27. package/src/sap/fe/templates/ObjectPage/overrides/CollaborationManager.js +29 -20
  28. package/src/sap/fe/templates/ObjectPage/overrides/CollaborationManager.ts +28 -21
  29. package/src/sap/fe/templates/ObjectPage/overrides/IntentBasedNavigation.js +4 -3
  30. package/src/sap/fe/templates/ObjectPage/overrides/IntentBasedNavigation.ts +4 -4
  31. package/src/sap/fe/templates/ObjectPage/overrides/ViewState.js +93 -17
  32. package/src/sap/fe/templates/ObjectPage/overrides/ViewState.ts +108 -22
  33. package/src/sap/fe/templates/ObjectPage/view/fragments/EmphasizedFirstHeaderAction.fragment.xml +1 -0
  34. package/src/sap/fe/templates/ObjectPage/view/fragments/ExpandedHeading.fragment.xml +4 -4
  35. package/src/sap/fe/templates/ObjectPage/view/fragments/Heading.fragment.xml +4 -4
  36. package/src/sap/fe/templates/ObjectPage/view/fragments/ObjectPageHeaderAddress.fragment.xml +1 -1
  37. package/src/sap/fe/templates/ObjectPage/view/fragments/ObjectPageHeaderContact.fragment.xml +1 -1
  38. package/src/sap/fe/templates/ObjectPage/view/fragments/ObjectPageHeaderForm.fragment.xml +1 -0
  39. package/src/sap/fe/templates/library.js +1 -1
  40. package/src/sap/fe/templates/messagebundle_no.properties +1 -1
  41. package/src/sap/fe/templates/messagebundle_uk.properties +1 -1
@@ -25,6 +25,7 @@ import NavigationReason from "sap/fe/core/controllerextensions/routing/Navigatio
25
25
  import type CommandExecution from "sap/fe/core/controls/CommandExecution";
26
26
  import { RecommendationDialogDecision } from "sap/fe/core/controls/Recommendations/ConfirmRecommendationDialog";
27
27
  import type { HiddenDraft, MicroChartManifestConfiguration } from "sap/fe/core/converters/ManifestSettings";
28
+ import { CreationMode } from "sap/fe/core/converters/ManifestSettings";
28
29
  import type { InternalModelContext } from "sap/fe/core/helpers/ModelHelper";
29
30
  import ModelHelper from "sap/fe/core/helpers/ModelHelper";
30
31
  import FELibrary from "sap/fe/core/library";
@@ -48,6 +49,7 @@ import type { default as ObjectPageExtensionAPI } from "sap/fe/templates/ObjectP
48
49
  import { default as ExtensionAPI } from "sap/fe/templates/ObjectPage/ExtensionAPI";
49
50
  import CollaborationDiscard from "sap/fe/templates/ObjectPage/components/CollaborationDiscardDialog";
50
51
  import type SubSectionBlock from "sap/fe/templates/ObjectPage/controls/SubSectionBlock";
52
+ import { pollForSectionStability } from "sap/fe/templates/ObjectPage/helpers/SectionNavigationHelper";
51
53
  import TableScroller from "sap/fe/templates/TableScroller";
52
54
  import type Button from "sap/m/Button";
53
55
  import type InputBase from "sap/m/InputBase";
@@ -55,6 +57,7 @@ import InstanceManager from "sap/m/InstanceManager";
55
57
  import type MenuButton from "sap/m/MenuButton";
56
58
  import type NavContainer from "sap/m/NavContainer";
57
59
  import type Popover from "sap/m/Popover";
60
+ import type SearchField from "sap/m/SearchField";
58
61
  import type ToolbarSpacer from "sap/m/ToolbarSpacer";
59
62
  import Device from "sap/ui/Device";
60
63
  import type UI5Event from "sap/ui/base/Event";
@@ -104,6 +107,8 @@ export type BindingParams = {
104
107
  bPersistOPScroll?: boolean;
105
108
  listBinding?: ODataListBinding;
106
109
  showPlaceholder?: boolean;
110
+ requestOnBinding?: string[];
111
+ targetControlId?: string;
107
112
  };
108
113
 
109
114
  const ProgrammingModel = FELibrary.ProgrammingModel;
@@ -189,6 +194,16 @@ class ObjectPageController extends PageController {
189
194
 
190
195
  private previousBindingContextPath?: string;
191
196
 
197
+ private pendingTargetControlId?: string;
198
+
199
+ private targetSubSection?: ObjectPageSubSection;
200
+
201
+ // Flag to prevent ViewState.apply() from overriding section selection after user interaction or targetControlId navigation
202
+ private skipViewStateSectionRestore = false;
203
+
204
+ // Tracks if the binding context path changed (new entity vs same entity re-binding)
205
+ private isNewBindingContext = false;
206
+
192
207
  /**
193
208
  * Returns controls (Button / MenuButton) that are marked as "primary action".
194
209
  * The marker is set by the converters when an action is emphasized (annotation-based) or
@@ -346,10 +361,14 @@ class ObjectPageController extends PageController {
346
361
  }
347
362
  const tableType = table.isA<Table>("sap.ui.mdc.Table") && (table.getType() as TableTypeBase);
348
363
  const tableAPI = table.getParent()?.isA<TableAPI>("sap.fe.macros.Table") ? (table.getParent() as TableAPI) : undefined;
364
+ const tableControl = tableAPI?.getTableDefinition().control;
365
+
349
366
  if (
350
367
  tableType &&
351
368
  (tableType?.isA("sap.ui.mdc.table.GridTableType") || tableType?.isA("sap.ui.mdc.table.TreeTableType")) &&
352
- tableAPI?.getTableDefinition().control.rowCountMode === "Auto"
369
+ tableControl?.rowCountMode === "Auto" &&
370
+ // Exclude CreationRow mode unless sectionLayout is Tabs: the fit-container style creates whitespace in the subsection in edit mode when CreationRow is used in TableCreationOptions
371
+ (tableControl?.creationMode !== CreationMode.CreationRow || this.getView().getViewData().sectionLayout === "Tabs")
353
372
  ) {
354
373
  //In case there is only a single table in a subSection we fit that to the whole page so that the scrollbar comes only on table and not on page
355
374
  visibleSubSection.addStyleClass("sapUxAPObjectPageSubSectionFitContainer");
@@ -429,8 +448,30 @@ class ObjectPageController extends PageController {
429
448
  }
430
449
 
431
450
  _onBeforeBinding(oContext: ODataV4Context, mParameters: BindingParams = {}): void {
432
- // TODO: we should check how this comes together with the transaction helper, same to the change in the afterBinding
433
- this.previousBindingContextPath = this.getView().getBindingContext()?.getPath();
451
+ // Reset flag to allow section restoration on this new binding context.
452
+ // This ensures ViewState.apply() can properly restore sections after discard or re-navigation.
453
+ this.skipViewStateSectionRestore = false;
454
+
455
+ // Store targetControlId for scrolling after binding completes
456
+ this.pendingTargetControlId = mParameters.targetControlId;
457
+
458
+ // Apply targetControlId only on initial navigation - don't apply during edit/save/discard transitions
459
+ // to preserve user's section selection. Note: getView().getBindingContext() returns the OLD context
460
+ // therefore the oContext parameter is used.
461
+ const currentContextPath = oContext?.getPath();
462
+ const getEntityIdentity = (path: string | undefined): string | undefined => {
463
+ if (!path) return undefined;
464
+ // Remove IsActiveEntity from the path to get entity identity
465
+ // e.g., "/Products(ID=1,IsActiveEntity=true)" -> "/Products(ID=1)"
466
+ return path
467
+ .replace(/,?IsActiveEntity=(true|false),?/, "")
468
+ .replace(/\(\s*,/, "(")
469
+ .replace(/,\s*\)/, ")");
470
+ };
471
+ const currentEntityId = getEntityIdentity(currentContextPath);
472
+ const previousEntityId = getEntityIdentity(this.previousBindingContextPath);
473
+ this.isNewBindingContext = previousEntityId !== currentEntityId;
474
+ this.previousBindingContextPath = currentContextPath;
434
475
  const aTables = this._findTables(),
435
476
  oObjectPage = this._getObjectPageLayoutControl(),
436
477
  oInternalModelContext = this.getView().getBindingContext("internal") as InternalModelContext,
@@ -690,6 +731,7 @@ class ObjectPageController extends PageController {
690
731
  /**
691
732
  * Set the initial focus in edit mode.
692
733
  * @param aSubSections Object page sub sections
734
+ * @param fromTabNavigation Indicates if the focus is set after a tab navigation
693
735
  */
694
736
  _updateFocusInEditMode(aSubSections: ObjectPageSubSection[], fromTabNavigation = false): void {
695
737
  setTimeout(
@@ -697,7 +739,7 @@ class ObjectPageController extends PageController {
697
739
  // We set the focus in a timeeout, otherwise the focus sometimes goes to the TabBar
698
740
  const oObjectPage = this._getObjectPageLayoutControl();
699
741
  const oMandatoryField = this._getFirstEmptyMandatoryFieldFromSubSection(aSubSections);
700
- let oFieldToFocus;
742
+ let oFieldToFocus: UI5Element | undefined;
701
743
  if (oMandatoryField) {
702
744
  if (oMandatoryField.isA("sap.fe.macros.MultiValueField")) {
703
745
  oFieldToFocus = (oMandatoryField as unknown as MultiValueFieldBlock).getMultiValueField();
@@ -713,9 +755,22 @@ class ObjectPageController extends PageController {
713
755
  const focusInfo = oFieldToFocus.getFocusInfo() as { targetInfo: object };
714
756
  focusInfo.targetInfo = { silent: true };
715
757
  if (oFieldToFocus.isA("sap.ui.mdc.field.FieldInput")) {
716
- oFieldToFocus = oFieldToFocus.getParent();
758
+ oFieldToFocus = oFieldToFocus.getParent() as UI5Element | undefined;
759
+ }
760
+
761
+ if (
762
+ oFieldToFocus?.isA<SearchField>("sap.m.SearchField") &&
763
+ oFieldToFocus.getEnableSuggestions() &&
764
+ !oFieldToFocus.isBound("enableSuggestions")
765
+ ) {
766
+ // In case the focus is set on a search field with suggestions, we temporarily disable the suggestions to avoid triggering them on focus
767
+ // unless this property is bound (to avoid modifying something in a model)
768
+ oFieldToFocus.setEnableSuggestions(false);
769
+ oFieldToFocus.focus(focusInfo);
770
+ oFieldToFocus.setEnableSuggestions(true);
771
+ } else {
772
+ oFieldToFocus?.focus(focusInfo);
717
773
  }
718
- oFieldToFocus.focus(focusInfo);
719
774
  }
720
775
  }.bind(this),
721
776
  fromTabNavigation ? 300 : 0
@@ -777,15 +832,24 @@ class ObjectPageController extends PageController {
777
832
 
778
833
  // Function to navigate back, or display the launchpad if we're on the first page of the history
779
834
  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);
835
+ // Use the new Navigation API if it's available in the browser, to check if we can go back in history.
836
+ const canGoBack = (window as { navigation?: { canGoBack?: boolean } }).navigation?.canGoBack;
837
+ if (canGoBack === true) {
838
+ history.back();
839
+ } else if (canGoBack === false) {
840
+ this._routing.navigateBackFromContext(oContext);
841
+ } else {
842
+ // Fallback in case the browser doesn't support the Navigation API
843
+ const currentURL = document.URL;
844
+ history.back();
845
+ // In case there is no previous page in the history, history.back does nothing.
846
+ // In this case, we need to use navigateBackFromContext, that will display the home page
847
+ setTimeout(() => {
848
+ if (document.URL === currentURL) {
849
+ this._routing.navigateBackFromContext(oContext);
850
+ }
851
+ }, 500);
852
+ }
789
853
  };
790
854
 
791
855
  if (this.getAppComponent().getRouterProxy().checkIfBackHasSameContext()) {
@@ -826,7 +890,7 @@ class ObjectPageController extends PageController {
826
890
  this.editFlow.storeSiblingContextData(inputBindingContext);
827
891
  }
828
892
 
829
- this._sideEffects.clearFieldGroupsValidity();
893
+ this.sideEffects.clearFieldGroupsValidity();
830
894
 
831
895
  // TODO: this is only a temp solution as long as the model fix the cache issue and we use this additional
832
896
  // binding with ownRequest
@@ -984,6 +1048,140 @@ class ObjectPageController extends PageController {
984
1048
  applyAppState,
985
1049
  this.previousBindingContextPath !== this.getView().getBindingContext()?.getPath()
986
1050
  );
1051
+
1052
+ // Capture synchronously to avoid race with subsequent navigations
1053
+ const pendingTargetControlId = this.pendingTargetControlId;
1054
+ this.pendingTargetControlId = undefined;
1055
+
1056
+ this.handlePendingTargetControlIdNavigation(applyAppState, oObjectPage, pendingTargetControlId);
1057
+ }
1058
+
1059
+ /**
1060
+ * Handles navigation to a target section or subsection after app state is applied.
1061
+ * This coordinates targetControlId navigation from two sources:
1062
+ * - In-app navigation: pendingTargetControlId captured before applyAppState.
1063
+ * - Cross-app navigation: xAppState targetControlId stored by AppStateHandler during LR to OP transition.
1064
+ * @param applyAppState Promise that resolves when app state has been applied.
1065
+ * @param oObjectPage The ObjectPageLayout control.
1066
+ * @param pendingTargetControlId The targetControlId captured before applyAppState, if any.
1067
+ */
1068
+ private handlePendingTargetControlIdNavigation(
1069
+ applyAppState: Promise<void | object>,
1070
+ oObjectPage: ObjectPageLayout,
1071
+ pendingTargetControlId: string | undefined
1072
+ ): void {
1073
+ applyAppState
1074
+ .then(async () => {
1075
+ const xAppStateTargetControlId = this.getAppComponent().getAppStateHandler()?.consumePendingTargetControlId();
1076
+ const targetControlId = pendingTargetControlId ?? xAppStateTargetControlId;
1077
+ if (!targetControlId) {
1078
+ return;
1079
+ }
1080
+
1081
+ // Wait for visibility bindings (needed for lazy loading scenarios)
1082
+ try {
1083
+ await Promise.all(this.waitForVisibilityBindings);
1084
+ } catch {
1085
+ // Some visibility bindings failed to resolve, proceeding anyway
1086
+ }
1087
+
1088
+ // Wait for pageReady to ensure ObjectPage is fully initialized
1089
+ await this.pageReady.waitPageReady();
1090
+
1091
+ // Navigate to target section
1092
+ this.navigateToTargetSection(oObjectPage, targetControlId);
1093
+ return; // Required by linter rule promise/always-return
1094
+ })
1095
+ .catch(() => {
1096
+ // applyAppState failed - skip targetControlId navigation
1097
+ });
1098
+ }
1099
+
1100
+ /**
1101
+ * Navigates to a target section with automatic re-navigation if UX rules reset the selection.
1102
+ * Polls the section state since setSelectedSection() doesn't fire the navigate event.
1103
+ * @param oObjectPage The ObjectPageLayout control.
1104
+ * @param targetControlId The local ID of the target section or subsection.
1105
+ */
1106
+ private navigateToTargetSection(oObjectPage: ObjectPageLayout, targetControlId: string): void {
1107
+ // Determine target section ID for comparison
1108
+ const targetControl = this.getView().byId(targetControlId);
1109
+ const isSection = targetControl?.isA("sap.uxap.ObjectPageSection");
1110
+ const targetSectionId = isSection ? targetControl?.getId() : (targetControl?.getParent() as ObjectPageSection)?.getId();
1111
+
1112
+ // Navigate function with guard to prevent duplicate calls when already on target
1113
+ const navigateToTarget = (): void => {
1114
+ if (oObjectPage.getSelectedSection() === targetSectionId) {
1115
+ return; // Already on target section
1116
+ }
1117
+ this.navigateWithConfirmation(targetControlId);
1118
+ };
1119
+
1120
+ // Navigate immediately
1121
+ navigateToTarget();
1122
+
1123
+ // Skip polling if already on target section (navigation was no-op or succeeded immediately)
1124
+ if (oObjectPage.getSelectedSection() === targetSectionId) {
1125
+ this.skipViewStateSectionRestore = true;
1126
+ return;
1127
+ }
1128
+
1129
+ // Poll to detect UX rules reset only on new entity binding.
1130
+ // _adjustSelectedSectionByUXRules() only runs when ObjectPageLayout re-initializes with a new entity.
1131
+ if (this.isNewBindingContext) {
1132
+ pollForSectionStability({
1133
+ getSelectedSection: () => oObjectPage.getSelectedSection(),
1134
+ targetSectionId,
1135
+ onResetDetected: navigateToTarget
1136
+ })
1137
+ .then(() => {
1138
+ this.skipViewStateSectionRestore = true;
1139
+ return; // Required by linter rule promise/always-return
1140
+ })
1141
+ .catch(() => {
1142
+ // Required by linter rule promise/catch-or-return - polling failures are non-critical
1143
+ });
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * Sets targetSubSection based on the target control ID for app state persistence.
1149
+ * @param targetControlId The local ID of the target section or subsection.
1150
+ */
1151
+ private setTargetSubSection(targetControlId: string): void {
1152
+ const targetControl = this.getView().byId(targetControlId);
1153
+ if (targetControl?.isA<ObjectPageSubSection>("sap.uxap.ObjectPageSubSection")) {
1154
+ this.targetSubSection = targetControl;
1155
+ } else if (targetControl?.isA<ObjectPageSection>("sap.uxap.ObjectPageSection")) {
1156
+ const subSections = targetControl.getSubSections();
1157
+ this.targetSubSection = subSections[0];
1158
+ }
1159
+ }
1160
+
1161
+ /**
1162
+ * Performs navigation to the target section and then updates the app state.
1163
+ * @param targetControlId The local ID of the target section or subsection.
1164
+ */
1165
+ private navigateWithConfirmation(targetControlId: string): void {
1166
+ // Set targetSubSection BEFORE navigation for app state saving.
1167
+ // scrollToSection() doesn't fire the navigate event, so onNavigateChange won't be called.
1168
+ this.setTargetSubSection(targetControlId);
1169
+
1170
+ this.getExtensionAPI().navigateToSubSection(targetControlId);
1171
+
1172
+ // Wait for ExtensionAPI polling to complete (~100ms) plus scroll to finish
1173
+ // before updating app state and setting focus.
1174
+ setTimeout(() => {
1175
+ this.viewState.updateAppStateDebounced();
1176
+
1177
+ // Set focus if in edit mode (for in-app navigation to sub-object page in edit mode)
1178
+ // This is needed because scrollToSection() doesn't fire onNavigateChange,
1179
+ // so we need to manually trigger focus setting here
1180
+ const isInEditMode = CommonUtils.getIsEditable(this.getView());
1181
+ if (isInEditMode && this.targetSubSection) {
1182
+ this._updateFocusInEditMode([this.targetSubSection], false);
1183
+ }
1184
+ }, 150);
987
1185
  }
988
1186
 
989
1187
  /**
@@ -1384,9 +1582,7 @@ class ObjectPageController extends PageController {
1384
1582
  // If there is at least one global SideEffects for the related entity, execute it/them
1385
1583
  if (globalSideEffects.length) {
1386
1584
  await this.editFlow.syncTask();
1387
- return Promise.all(
1388
- globalSideEffects.map(async (sideEffects) => this._sideEffects.requestSideEffects(sideEffects, context))
1389
- );
1585
+ return Promise.all(globalSideEffects.map(async (sideEffects) => this.sideEffects.requestSideEffects(sideEffects, context)));
1390
1586
  }
1391
1587
  }
1392
1588
 
@@ -2204,17 +2400,25 @@ class ObjectPageController extends PageController {
2204
2400
 
2205
2401
  onNavigateChange(this: ObjectPageController, oEvent: UI5Event<{ subSection: ObjectPageSubSection }>): void {
2206
2402
  //will be called always when we click on a section tab
2207
- this.getExtensionAPI().updateAppState();
2208
2403
  this.bSectionNavigated = true;
2209
2404
 
2405
+ // Prevent ViewState from overriding the user's manual tab selection.
2406
+ // This is important after discard when ViewState.apply might try to restore the previous section.
2407
+ this.skipViewStateSectionRestore = true;
2408
+
2409
+ const subSection = oEvent.getParameter("subSection");
2410
+ this.targetSubSection = subSection;
2411
+
2412
+ this.getExtensionAPI().updateAppState();
2413
+
2210
2414
  const oInternalModelContext = this.getView().getBindingContext("internal") as InternalModelContext;
2415
+
2211
2416
  if (
2212
2417
  CommonUtils.getIsEditable(this.getView()) &&
2213
2418
  this.getView().getViewData().sectionLayout === "Tabs" &&
2214
2419
  oInternalModelContext.getProperty("errorNavigationSectionFlag") === false
2215
2420
  ) {
2216
- const oSubSection = oEvent.getParameter("subSection");
2217
- this._updateFocusInEditMode([oSubSection], true);
2421
+ this._updateFocusInEditMode([subSection], true);
2218
2422
  }
2219
2423
  },
2220
2424
  onVariantSelected: function (this: ObjectPageController): void {