@sapui5/sap.fe.templates 1.149.0 → 1.150.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 (23) 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 +82 -3
  5. package/src/sap/fe/templates/ListReport/ListReportController.controller.ts +74 -2
  6. package/src/sap/fe/templates/ListReport/manifest.json +1 -1
  7. package/src/sap/fe/templates/ListReport/view/fragments/Table.fragment.xml +1 -0
  8. package/src/sap/fe/templates/ListReport/view/fragments/VariantManagement.fragment.xml +1 -0
  9. package/src/sap/fe/templates/ObjectPage/ObjectPage.view.xml +4 -2
  10. package/src/sap/fe/templates/ObjectPage/ObjectPageController.controller.js +102 -73
  11. package/src/sap/fe/templates/ObjectPage/ObjectPageController.controller.ts +102 -91
  12. package/src/sap/fe/templates/ObjectPage/components/CollaborationDraft.js +19 -1
  13. package/src/sap/fe/templates/ObjectPage/components/CollaborationDraft.tsx +17 -0
  14. package/src/sap/fe/templates/ObjectPage/controls/SubSectionBlock.js +3 -2
  15. package/src/sap/fe/templates/ObjectPage/controls/SubSectionBlock.ts +4 -1
  16. package/src/sap/fe/templates/ObjectPage/manifest.json +1 -1
  17. package/src/sap/fe/templates/ObjectPage/view/fragments/EditableHeaderFacet.fragment.xml +2 -2
  18. package/src/sap/fe/templates/ObjectPage/view/fragments/ExpandedHeading.fragment.xml +1 -1
  19. package/src/sap/fe/templates/ObjectPage/view/fragments/Heading.fragment.xml +7 -3
  20. package/src/sap/fe/templates/ObjectPage/view/fragments/ObjectPageHeaderForm.fragment.xml +1 -0
  21. package/src/sap/fe/templates/ObjectPage/view/fragments/Section.fragment.xml +4 -4
  22. package/src/sap/fe/templates/ObjectPage/view/fragments/Table.fragment.xml +1 -0
  23. package/src/sap/fe/templates/library.js +1 -1
@@ -30,15 +30,17 @@ import type { InternalModelContext } from "sap/fe/core/helpers/ModelHelper";
30
30
  import ModelHelper from "sap/fe/core/helpers/ModelHelper";
31
31
  import FELibrary from "sap/fe/core/library";
32
32
  import { type WrappedCard } from "sap/fe/core/services/CollaborationManagerServiceFactory";
33
+ import type { TelemetryVariantEvent } from "sap/fe/core/services/TelemetryServiceFactory";
33
34
  import type ChartBlock from "sap/fe/macros/Chart";
34
35
  import CommonHelper from "sap/fe/macros/CommonHelper";
36
+ import type FormBlock from "sap/fe/macros/Form";
35
37
  import type MacroAPI from "sap/fe/macros/MacroAPI";
36
38
  import type MultiValueFieldBlock from "sap/fe/macros/MultiValueField";
37
39
  import type TableAPI from "sap/fe/macros/Table";
40
+ import VariantManagementBB from "sap/fe/macros/VariantManagement";
38
41
  import type EasyFillButton from "sap/fe/macros/ai/EasyFillButton";
39
42
  import type SummarizationButton from "sap/fe/macros/ai/SummarizationButton";
40
43
  import type SubSection from "sap/fe/macros/controls/section/SubSection";
41
- import type FormAPI from "sap/fe/macros/form/FormAPI";
42
44
  import type MessageButton from "sap/fe/macros/messages/MessageButton";
43
45
  import type ShareAPI from "sap/fe/macros/share/ShareAPI";
44
46
  import type TableCreationOptions from "sap/fe/macros/table/TableCreationOptions";
@@ -100,7 +102,7 @@ import MessageHandlerOverride from "./overrides/MessageHandler";
100
102
  import PaginatorOverride from "./overrides/Paginator";
101
103
  import ShareOverrides from "./overrides/Share";
102
104
  import ViewStateOverrides from "./overrides/ViewState";
103
-
105
+ const { VariantType } = VariantManagementBB;
104
106
  export type BindingParams = {
105
107
  redirectedToNonDraft?: string | undefined;
106
108
  reason?: NavigationReason;
@@ -184,6 +186,8 @@ class ObjectPageController extends PageController {
184
186
 
185
187
  private mergePatchDraft = false;
186
188
 
189
+ private fclCloseHandlerSet = false;
190
+
187
191
  private autoQueueLock: ReturnType<ODataModel["lock"]> | undefined;
188
192
 
189
193
  private messageBinding?: Binding;
@@ -353,7 +357,9 @@ class ObjectPageController extends PageController {
353
357
  */
354
358
  private checkSectionsForNonResponsiveTable(subSections: ObjectPageSubSection[]): void {
355
359
  const changeClassForTables = (event: Event, visibleSubSection: ObjectPageSubSection): void => {
356
- const blocks = [...visibleSubSection.getBlocks(), ...visibleSubSection.getMoreBlocks()];
360
+ const blocks = [...visibleSubSection.getBlocks(), ...visibleSubSection.getMoreBlocks()].filter((block) =>
361
+ block.isA("sap.fe.templates.ObjectPage.controls.SubSectionBlock")
362
+ );
357
363
  if (blocks.length === 1) {
358
364
  const table = this.searchTableInBlock(blocks[0] as SubSectionBlock);
359
365
  if (!table) {
@@ -671,7 +677,7 @@ class ObjectPageController extends PageController {
671
677
  _getFirstEmptyMandatoryFieldFromSubSection(aSubSections: ObjectPageSubSection[]): InputBase | undefined {
672
678
  if (aSubSections.length === 0) return undefined;
673
679
  for (const subSection of aSubSections) {
674
- const aBlocks = subSection.getBlocks() as (Form | FormAPI)[];
680
+ const aBlocks = subSection.getBlocks() as (Form | FormBlock)[];
675
681
 
676
682
  if (aBlocks) {
677
683
  for (const blockControl of aBlocks) {
@@ -682,8 +688,8 @@ class ObjectPageController extends PageController {
682
688
  aFormContainers = blockControl.getFormContainers();
683
689
  } else if (blockControl.getContent && blockControl.getContent()?.isA<Form>("sap.ui.layout.form.Form")) {
684
690
  aFormContainers = (blockControl.getContent() as Form).getFormContainers();
685
- } else if (blockControl?.getContent?.()?.isA<FormAPI>("sap.fe.macros.form.FormAPI")) {
686
- aFormContainers = ((blockControl.getContent() as FormAPI).getContent() as Form).getFormContainers();
691
+ } else if (blockControl?.getContent?.()?.isA<FormBlock>("sap.fe.macros.Form")) {
692
+ aFormContainers = ((blockControl.getContent() as FormBlock).getContent() as Form).getFormContainers();
687
693
  }
688
694
  if (aFormContainers) {
689
695
  for (const formContainer of aFormContainers) {
@@ -835,69 +841,6 @@ class ObjectPageController extends PageController {
835
841
  oSubSection.getActions().forEach((actions) => actions.setBindingContext(undefined));
836
842
  }
837
843
 
838
- _onBackNavigationInDraft(oContext: ODataV4Context): void {
839
- this.messageHandler.removeTransitionMessages();
840
-
841
- // Function to navigate back, or display the launchpad if we're on the first page of the history
842
- const navBack = (): void => {
843
- // Use the new Navigation API if it's available in the browser, to check if we can go back in history.
844
- const canGoBack = (window as { navigation?: { canGoBack?: boolean } }).navigation?.canGoBack;
845
- if (canGoBack === true) {
846
- history.back();
847
- } else if (canGoBack === false) {
848
- this._routing.navigateBackFromContext(oContext);
849
- } else {
850
- // Fallback in case the browser doesn't support the Navigation API
851
- const currentURL = document.URL;
852
- history.back();
853
- // In case there is no previous page in the history, history.back does nothing.
854
- // In this case, we need to use navigateBackFromContext, that will display the home page
855
- setTimeout(() => {
856
- if (document.URL === currentURL) {
857
- this._routing.navigateBackFromContext(oContext);
858
- }
859
- }, 500);
860
- }
861
- };
862
-
863
- if (this.getAppComponent().getRouterProxy().checkIfBackHasSameContext()) {
864
- // Back nav will keep the same context --> no need to display the dialog
865
- history.back();
866
- } else if (!this.getView().getBindingContext()) {
867
- // This object page doesn't have a binding context, but still handles the shell back navigation --> pform the nav back and remove the navback handler as it makes no sense any longer
868
- this.getAppComponent()
869
- .getShellServices()
870
- .setBackNavigation(undefined)
871
- .then(navBack)
872
- .catch((e) => {
873
- Log.warning("Error while setting back navigation", e);
874
- });
875
- } else {
876
- const hiddenDraftEnabled = (this.getAppComponent()?.getEnvironmentCapabilities()?.getCapabilities()?.HiddenDraft as HiddenDraft)
877
- ?.enabled;
878
- draft.processDataLossOrDraftDiscardConfirmation(
879
- () => {
880
- if (this.getAppComponent().getRouterProxy().checkIfBackIsOutOfGuard()) {
881
- this.editFlow.handleSessionOff();
882
- }
883
- navBack();
884
- },
885
- Function.prototype,
886
- oContext,
887
- this,
888
- true,
889
- draft.NavigationType.BackNavigation,
890
- undefined,
891
- hiddenDraftEnabled ? true : undefined,
892
- undefined,
893
- // `navigationType` (BackNavigation above) still drives the non-FCL paths inside
894
- // processDraftAdminData (e.g. processNoActiveEntityMode / processEditingDraftForExistingEntity).
895
- // `draftDataLossStrategy` is consumed by the FCL branch (processFclMode) only — both are needed.
896
- { withoutChanges: draft.DraftDataLossOptions.Discard }
897
- );
898
- }
899
- }
900
-
901
844
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
902
845
  _onAfterBinding(inputBindingContext: ODataV4Context | undefined, mParameters: BindingParams | undefined): void {
903
846
  const view = this.getView();
@@ -1095,6 +1038,13 @@ class ObjectPageController extends PageController {
1095
1038
  if (!targetControlId) {
1096
1039
  return;
1097
1040
  }
1041
+ // Reset stale flag so ViewState.apply() is not blocked when the OP is re-activated
1042
+ // via shell keep-alive (cross-app re-entry) without _onBeforeBinding firing.
1043
+ // Intentional ordering: reset to false here before pollForSectionStability (below) sets it back
1044
+ // to true, so any concurrent ViewState.apply() triggered on re-entry is unblocked immediately.
1045
+ if (xAppStateTargetControlId) {
1046
+ this.skipViewStateSectionRestore = false;
1047
+ }
1098
1048
 
1099
1049
  // Wait for visibility bindings (needed for lazy loading scenarios)
1100
1050
  try {
@@ -1144,22 +1094,22 @@ class ObjectPageController extends PageController {
1144
1094
  return;
1145
1095
  }
1146
1096
 
1147
- // Poll to detect UX rules reset only on new entity binding.
1148
- // _adjustSelectedSectionByUXRules() only runs when ObjectPageLayout re-initializes with a new entity.
1149
- if (this.isNewBindingContext) {
1150
- pollForSectionStability({
1151
- getSelectedSection: () => oObjectPage.getSelectedSection(),
1152
- targetSectionId,
1153
- onResetDetected: navigateToTarget
1097
+ // Poll to detect UX rules reset. The guard on isNewBindingContext was removed because
1098
+ // _adjustSelectedSectionByUXRules() can also reset the section on cross-app re-entry to
1099
+ // the same entity (shell keep-alive). Polling is lightweight (max 10 × 50 ms) and safe to
1100
+ // run unconditionally whenever we need to hold a target section.
1101
+ pollForSectionStability({
1102
+ getSelectedSection: () => oObjectPage.getSelectedSection(),
1103
+ targetSectionId,
1104
+ onResetDetected: navigateToTarget
1105
+ })
1106
+ .then(() => {
1107
+ this.skipViewStateSectionRestore = true;
1108
+ return; // Required by linter rule promise/always-return
1154
1109
  })
1155
- .then(() => {
1156
- this.skipViewStateSectionRestore = true;
1157
- return; // Required by linter rule promise/always-return
1158
- })
1159
- .catch(() => {
1160
- // Required by linter rule promise/catch-or-return - polling failures are non-critical
1161
- });
1162
- }
1110
+ .catch(() => {
1111
+ // Required by linter rule promise/catch-or-return - polling failures are non-critical
1112
+ });
1163
1113
  }
1164
1114
 
1165
1115
  /**
@@ -1177,9 +1127,48 @@ class ObjectPageController extends PageController {
1177
1127
  }
1178
1128
 
1179
1129
  /**
1180
- * Performs navigation to the target section and then updates the app state.
1181
- * @param targetControlId The local ID of the target section or subsection.
1130
+ * Tracks telemetry when a new variant is saved.
1131
+ * @param event The save event.
1132
+ */
1133
+ private async trackVariantSavedTelemetry(event: UI5Event<Record<string, unknown>>): Promise<void> {
1134
+ if (event.getParameter("overwrite")) {
1135
+ return;
1136
+ }
1137
+ if (!event.getSource().isA("sap.fe.macros.Table")) {
1138
+ return;
1139
+ }
1140
+ if (!(await this.getAppComponent()?.getTelemetryService()?.isEnabled())) {
1141
+ return;
1142
+ }
1143
+ const tableAPI = event.getSource() as unknown as TableAPI;
1144
+ this.fireTelemetry(tableAPI?.getContent?.()?.getVariant?.()?.getVariants?.()?.length ?? 0, VariantType.OBJECT_PAGE_TABLE);
1145
+ }
1146
+
1147
+ /**
1148
+ * Tracks telemetry when variants are managed (e.g. deleted).
1149
+ * @param event The manage event.
1150
+ */
1151
+ private async trackVariantManagedTelemetry(event: UI5Event<Record<string, unknown>>): Promise<void> {
1152
+ const deletedCount: number = ((event.getParameter("deleted") as string[] | undefined) ?? []).length;
1153
+ if (deletedCount === 0) {
1154
+ return;
1155
+ }
1156
+ if (!(await this.getAppComponent()?.getTelemetryService()?.isEnabled())) {
1157
+ return;
1158
+ }
1159
+ this.fireTelemetry(deletedCount, VariantType.OBJECT_PAGE_TABLE);
1160
+ }
1161
+
1162
+ /**
1163
+ * Stores a FE.Variant telemetry event.
1164
+ * @param countVariants Number of variants affected by the action.
1165
+ * @param variantType The variant type discriminator.
1182
1166
  */
1167
+ private fireTelemetry(countVariants: number, variantType: TelemetryVariantEvent["parameters"]["variantType"]): void {
1168
+ const telemetryEvent: TelemetryVariantEvent = { type: "FE.Variant", parameters: { countVariants, variantType } };
1169
+ this.getAppComponent()?.getTelemetryService()?.storeAction(telemetryEvent);
1170
+ }
1171
+
1183
1172
  private navigateWithConfirmation(targetControlId: string): void {
1184
1173
  // Set targetSubSection BEFORE navigation for app state saving.
1185
1174
  // scrollToSection() doesn't fire the navigate event, so onNavigateChange won't be called.
@@ -1335,12 +1324,13 @@ class ObjectPageController extends PageController {
1335
1324
  const oView = this.getView();
1336
1325
  const oInternalModelContext = oView.getBindingContext("internal") as InternalModelContext;
1337
1326
  const oBindingContext = oView.getBindingContext();
1338
- //Show popup while navigating back from object page in case of draft
1327
+ // Show popup while navigating back from object page in case of draft (incl. hidden draft).
1328
+ // Sticky's shell back is registered centrally from EditFlow.registerEditLifecycleCallbacks.
1339
1329
  if (oBindingContext) {
1340
- const bIsStickyMode = ModelHelper.isStickySessionSupported(oBindingContext.getModel().getMetaModel());
1341
- if (!bIsStickyMode) {
1330
+ const isDraftSupported = ModelHelper.isDraftSupported(oBindingContext.getModel().getMetaModel(), oBindingContext.getPath());
1331
+ if (isDraftSupported) {
1342
1332
  const oAppComponent = CommonUtils.getAppComponent(oView);
1343
- await oAppComponent.getShellServices().setBackNavigation(() => this._onBackNavigationInDraft(oBindingContext));
1333
+ await oAppComponent.getShellServices().setBackNavigation(() => this.editFlow.onBackNavigationInDraft(oBindingContext));
1344
1334
  }
1345
1335
  }
1346
1336
  // do not request recommendations action if we are in Display mode
@@ -1386,6 +1376,22 @@ class ObjectPageController extends PageController {
1386
1376
 
1387
1377
  this.mergePatchDraft = true;
1388
1378
  }
1379
+
1380
+ // When the user clicks the FCL Close button while a key property field has focus,
1381
+ // the field's focus-loss triggers side effects that refresh the OData binding and
1382
+ // re-render the ObjectPage header title — removing the Close button from the DOM
1383
+ // before the click event fires, so the press is silently lost.
1384
+ // Listening on mousedown (which fires before focus-loss) ensures closeColumn() is
1385
+ // always called regardless of the subsequent re-render.
1386
+ if (!this.fclCloseHandlerSet && !Device.support.touch) {
1387
+ const fclCloseButton = oView.byId("fe::FCLStandardAction::Close");
1388
+ if (fclCloseButton?.isA<Button>("sap.m.Button")) {
1389
+ fclCloseButton.attachBrowserEvent("mousedown", (): void => {
1390
+ this._routing.closeColumn();
1391
+ });
1392
+ }
1393
+ this.fclCloseHandlerSet = true;
1394
+ }
1389
1395
  }
1390
1396
 
1391
1397
  /**
@@ -1702,6 +1708,7 @@ class ObjectPageController extends PageController {
1702
1708
  if (error !== RecommendationDialogDecision.Continue) {
1703
1709
  messagePopoverErr = (error as string) ?? "";
1704
1710
  }
1711
+ throw error;
1705
1712
  }
1706
1713
  } finally {
1707
1714
  if (BusyLocker.isLocked(model)) {
@@ -2442,11 +2449,15 @@ class ObjectPageController extends PageController {
2442
2449
  onVariantSelected: function (this: ObjectPageController): void {
2443
2450
  this.getExtensionAPI().updateAppState();
2444
2451
  },
2445
- onVariantSaved: function (this: ObjectPageController): void {
2452
+ onVariantSaved: function (this: ObjectPageController, event: UI5Event<Record<string, unknown>>): void {
2446
2453
  //TODO: Should remove this setTimeOut once Variant Management provides an api to fetch the current variant key on save
2447
2454
  setTimeout(() => {
2448
2455
  this.getExtensionAPI().updateAppState();
2449
2456
  }, 2000);
2457
+ this.trackVariantSavedTelemetry(event);
2458
+ },
2459
+ onVariantManaged: function (this: ObjectPageController, event: UI5Event<Record<string, unknown>>): void {
2460
+ this.trackVariantManagedTelemetry(event);
2450
2461
  },
2451
2462
  navigateToSubSection: function (oController: ObjectPageController, vDetailConfig: string | object): void {
2452
2463
  const oDetailConfig = typeof vDetailConfig === "string" ? JSON.parse(vDetailConfig) : vDetailConfig;