@sapui5/sap.fe.controls 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.
@@ -1,4 +1,5 @@
1
1
  import Log from "sap/base/Log";
2
+ import encodeXML from "sap/base/security/encodeXML";
2
3
  import type { EnhanceWithUI5, StateOf } from "sap/fe/base/ClassSupport";
3
4
  import {
4
5
  aggregation,
@@ -50,6 +51,7 @@ import type EasyFilter from "ux/eng/fioriai/reuse/easyfilter/EasyFilter";
50
51
  import type { EasyFilterMetadata, EasyFilterResult, PropertyMetadata } from "ux/eng/fioriai/reuse/easyfilter/EasyFilter";
51
52
  import type { Success } from "ux/eng/fioriai/reuse/shared";
52
53
  import type { EventHandler } from "../../../../../../../types/extension_types";
54
+ import ErrorMessageHandler from "./ErrorMessageHandler";
53
55
  import { triggerPXIntegration } from "./PXFeedback";
54
56
  import EasyFilterUtils from "./utils";
55
57
 
@@ -111,8 +113,17 @@ export type NonValueHelpTokenDefinition = {
111
113
 
112
114
  export type TokenDefinition = ValueHelpTokenDefinition | NonValueHelpTokenDefinition;
113
115
 
116
+ export type SortDefinition = {
117
+ key: string;
118
+ descending: boolean;
119
+ };
120
+
114
121
  export type TokenSetters = "setSelectedValues";
115
122
 
123
+ export type GroupLevelDefinition = {
124
+ key: string;
125
+ };
126
+
116
127
  export type EasyFilterPropertyMetadata = PropertyMetadata &
117
128
  (
118
129
  | {
@@ -184,12 +195,25 @@ export default class EasyFilterBarContainer extends Control {
184
195
  @event()
185
196
  tokensChanged?: EventHandler<UI5Event<{ tokens: TokenDefinition[]; reset: boolean }, EasyFilterBarContainer>>;
186
197
 
198
+ @event()
199
+ sortersChanged?: EventHandler<UI5Event<{ sorters: SortDefinition[] }, EasyFilterBarContainer>>;
200
+
201
+ @event()
202
+ groupLevelsChanged?: EventHandler<UI5Event<{ groupLevels: GroupLevelDefinition[] }, EasyFilterBarContainer>>;
203
+
187
204
  @event()
188
205
  queryChanged?: EventHandler<UI5Event<{ query: string }, EasyFilterBarContainer>>;
189
206
 
190
207
  @event()
191
208
  showValueHelp?: EventHandler<EasyFilterBarContainer$ShowValueHelpEvent>;
192
209
 
210
+ @event()
211
+ liveChange?: EventHandler<UI5Event<{ query: string }, EasyFilterBarContainer>>;
212
+
213
+ /** Fires when the user manually removes or edits a filter token. Does not fire when tokens are set by the AI search result. */
214
+ @event()
215
+ tokensChangedByUser?: EventHandler<UI5Event<{ tokens: TokenDefinition[] }, EasyFilterBarContainer>>;
216
+
193
217
  @event()
194
218
  clearFilters?: EventHandler;
195
219
 
@@ -205,6 +229,8 @@ export default class EasyFilterBarContainer extends Control {
205
229
  protected state: StateOf<{
206
230
  showResult: boolean;
207
231
  tokens: TokenDefinition[];
232
+ sorters: SortDefinition[];
233
+ groupLevels: GroupLevelDefinition[];
208
234
  tokenizerEditable: boolean;
209
235
  showSingleValueMessageStrip?: boolean;
210
236
  singleValueMessageStripText?: string;
@@ -217,6 +243,8 @@ export default class EasyFilterBarContainer extends Control {
217
243
  }> = {
218
244
  showResult: false,
219
245
  tokens: [] as TokenDefinition[],
246
+ sorters: [] as SortDefinition[],
247
+ groupLevels: [] as GroupLevelDefinition[],
220
248
  tokenizerEditable: true,
221
249
  showSingleValueMessageStrip: false,
222
250
  singleValueMessageStripText: "",
@@ -257,6 +285,10 @@ export default class EasyFilterBarContainer extends Control {
257
285
 
258
286
  private shouldMessageStripRemove = true;
259
287
 
288
+ private supportsSorting = true;
289
+
290
+ private supportsGrouping = true;
291
+
260
292
  constructor(properties: string | ($ControlSettings & PropertiesOf<EasyFilterBarContainer>), others?: $ControlSettings) {
261
293
  super(properties as string, others);
262
294
  this.initialize();
@@ -275,7 +307,6 @@ export default class EasyFilterBarContainer extends Control {
275
307
  this.setProperty("appId", appId, true);
276
308
  await this.inputFieldReady;
277
309
  this.easyFilterInput.current?.setProperty("appId", appId, true);
278
- await this.easyFilterInput.current?.initShellHistoryProvider();
279
310
  }
280
311
  }
281
312
 
@@ -416,6 +447,7 @@ export default class EasyFilterBarContainer extends Control {
416
447
  this.setMessageStrip(easyFilterResult.message);
417
448
  this.invisibleMessage.announce(this.resourceBundle.getText(easyFilterResult.message), InvisibleMessageMode.Assertive);
418
449
  this.state.messageStripType = MessageType.Warning;
450
+ this.state.groupLevels = [];
419
451
  Log.error("Error while generating filter criteria: ", easyFilterResult.message);
420
452
  }
421
453
 
@@ -498,12 +530,162 @@ export default class EasyFilterBarContainer extends Control {
498
530
  }
499
531
 
500
532
  const updatedTokens = this.verifySingleSelectTokenValues(tokens);
533
+
534
+ // Validate and store sort expressions (applied to table directly, not shown as tokens)
535
+ this.state.sorters = this.supportsSorting ? this.validateSortExpressions(easyFilterResult.data.sorters) : [];
501
536
  this.state.showResult = true;
502
537
  this.state.thumbButtonEnabled = true;
503
538
  this.state.thumbDownButtonPressed = false;
504
539
  this.state.thumbUpButtonPressed = false;
505
540
  this.state.tokens = updatedTokens;
506
541
  }
542
+
543
+ this.state.groupLevels = this.supportsGrouping ? this.applyGroupLevels(easyFilterResult.data.groupLevels) : [];
544
+
545
+ this.showUnsupportedCapabilitiesWarning(easyFilterResult.data.sorters, easyFilterResult.data.groupLevels);
546
+ }
547
+
548
+ /**
549
+ * Shows a warning when the app does not support the LLM's sort/group expressions.
550
+ * @param sorters Sort expressions from the LLM.
551
+ * @param groupLevels Group levels from the LLM.
552
+ */
553
+ private showUnsupportedCapabilitiesWarning(
554
+ sorters: EasyFilter.EasyFilterSortExpression[] | undefined,
555
+ groupLevels: EasyFilter.EasyFilterGroupLevelDefinition[] | undefined
556
+ ): void {
557
+ const hasSortersFromLLM = sorters && sorters.length > 0;
558
+ const hasGroupLevelsFromLLM = groupLevels && groupLevels.length > 0;
559
+ const sortingUnsupported = !this.supportsSorting && hasSortersFromLLM;
560
+ const groupingUnsupported = !this.supportsGrouping && hasGroupLevelsFromLLM;
561
+
562
+ if (sortingUnsupported && groupingUnsupported) {
563
+ this.appendMessageStrip(this.resourceBundle.getText("M_EASY_FILTER_SORTING_AND_GROUPING_NOT_SUPPORTED"), MessageType.Warning);
564
+ } else if (sortingUnsupported) {
565
+ this.appendMessageStrip(this.resourceBundle.getText("M_EASY_FILTER_SORTING_NOT_SUPPORTED"), MessageType.Warning);
566
+ } else if (groupingUnsupported) {
567
+ this.appendMessageStrip(this.resourceBundle.getText("M_EASY_FILTER_GROUPING_NOT_SUPPORTED"), MessageType.Warning);
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Updates whether the app supports sorting and grouping based on table personalization settings.
573
+ * @param sorting Whether the app supports sorting.
574
+ * @param grouping Whether the app supports grouping.
575
+ */
576
+ public setPersonalizationSupport(sorting: boolean, grouping: boolean): void {
577
+ this.supportsSorting = sorting;
578
+ this.supportsGrouping = grouping;
579
+ }
580
+
581
+ /**
582
+ * Validates and triggers the groupLevelsChanged function, which warns about non-groupable fields.
583
+ * @param groupLevels The group levels to validate and apply
584
+ * @returns GroupLevelDefinition[] An array of valid group level definitions to be applied
585
+ */
586
+ private applyGroupLevels(groupLevels: GroupLevelDefinition[] | undefined): GroupLevelDefinition[] {
587
+ const metadata = this.filterBarMetadata as EasyFilterPropertyMetadata[];
588
+
589
+ if (!metadata || !groupLevels || groupLevels.length === 0) {
590
+ return [];
591
+ }
592
+
593
+ const resolveField = (groupDef: GroupLevelDefinition): EasyFilterPropertyMetadata | undefined => {
594
+ const propertyName = EasyFilterUtils.resolvePropertyName(groupDef.key);
595
+ const field = metadata.find((f) => f.name === groupDef.key || f.name.endsWith(`/${propertyName}`));
596
+ if (!field) {
597
+ Log.warning(`EasyFilterBarContainer: group key "${groupDef.key}" not found in filter bar metadata, skipping`);
598
+ }
599
+ return field;
600
+ };
601
+
602
+ if (groupLevels.length === 1) {
603
+ const errorHandler = new ErrorMessageHandler();
604
+ const field = resolveField(groupLevels[0]);
605
+ if (!field) return [];
606
+ const isGroupable = errorHandler.isGroupableProperty(field);
607
+ if (errorHandler.hasErrors()) {
608
+ this.appendMessageStrip(encodeXML(errorHandler.getErrorMessage()), MessageType.Warning);
609
+ }
610
+ return isGroupable ? [groupLevels[0]] : [];
611
+ }
612
+
613
+ // Multiple group levels: find the first valid groupable field
614
+ const errorHandler = new ErrorMessageHandler();
615
+ let firstGroupableDef: GroupLevelDefinition | undefined;
616
+ let firstGroupableLabel: string | undefined;
617
+
618
+ for (const groupDef of groupLevels) {
619
+ const field = resolveField(groupDef);
620
+ if (!field) continue;
621
+ if (errorHandler.isGroupableProperty(field)) {
622
+ if (!firstGroupableDef) {
623
+ firstGroupableDef = groupDef;
624
+ firstGroupableLabel = field.label ?? field.name;
625
+ }
626
+ }
627
+ }
628
+
629
+ if (firstGroupableDef) {
630
+ const warning = this.resourceBundle.getText("M_EASY_FILTER_MULTIPLE_GROUP_WARNING", [firstGroupableLabel]);
631
+ this.appendMessageStrip(encodeXML(warning), MessageType.Warning);
632
+ return [firstGroupableDef];
633
+ }
634
+
635
+ // All fields are non-groupable
636
+ if (errorHandler.hasErrors()) {
637
+ this.appendMessageStrip(encodeXML(errorHandler.getErrorMessage()), MessageType.Warning);
638
+ }
639
+ return [];
640
+ }
641
+
642
+ /**
643
+ * Validates sort expressions from the AI result and returns valid sort definitions.
644
+ * @param sortExpressions The sort expressions from the ABAP service.
645
+ * @returns Array of validated sort definitions.
646
+ * @private
647
+ */
648
+ private validateSortExpressions(sortExpressions?: EasyFilter.EasyFilterSortExpression[]): SortDefinition[] {
649
+ if (!sortExpressions || sortExpressions.length === 0) {
650
+ return [];
651
+ }
652
+
653
+ const nonSortableFields: string[] = [];
654
+
655
+ const validSorters = sortExpressions
656
+ .filter((sortExpression) => {
657
+ // Validate that the field exists in metadata and is sortable
658
+ const fieldMetadata = this.filterBarMetadata?.find((field) => {
659
+ const fieldName = field.name.split("/").pop();
660
+ return fieldName === sortExpression.key || field.name === sortExpression.key;
661
+ });
662
+
663
+ if (!fieldMetadata) {
664
+ return false;
665
+ }
666
+
667
+ if (fieldMetadata.sortable === false) {
668
+ nonSortableFields.push(fieldMetadata.label ?? sortExpression.key);
669
+ return false;
670
+ }
671
+
672
+ return true;
673
+ })
674
+ .map((sortExpression) => {
675
+ const propertyName = sortExpression.key.split("/").pop() ?? sortExpression.key;
676
+ return {
677
+ key: propertyName,
678
+ descending: sortExpression.descending
679
+ };
680
+ });
681
+
682
+ // Show warning message for non-sortable fields
683
+ if (nonSortableFields.length > 0) {
684
+ const message = this.resourceBundle.getText("M_EASY_FILTER_NON_SORTABLE", [nonSortableFields.join(", ")]);
685
+ this.appendMessageStrip(message, MessageType.Warning);
686
+ }
687
+
688
+ return validSorters;
507
689
  }
508
690
 
509
691
  //every single select menu type should have only one value, else splice to one value and update the message strip
@@ -547,6 +729,21 @@ export default class EasyFilterBarContainer extends Control {
547
729
  this.state.showMessageStrip = true;
548
730
  }
549
731
 
732
+ setMessageStripWithType(text: string, type: MessageType): void {
733
+ this.setMessageStrip(text);
734
+ this.state.messageStripType = type;
735
+ }
736
+
737
+ /**
738
+ * Appends a message to the existing message strip instead of overwriting it.
739
+ * @param text The message text to append.
740
+ * @param type The message type for this message.
741
+ */
742
+ appendMessageStrip(text: string, type: MessageType): void {
743
+ const combined = this.state.showMessageStrip && this.state.messageStripText ? `${this.state.messageStripText}<br>${text}` : text;
744
+ this.setMessageStripWithType(combined, type);
745
+ }
746
+
550
747
  createPopoverForInnerControls(): Popover {
551
748
  if (!this.innerControlPopover) {
552
749
  this.innerControlPopover = (
@@ -637,14 +834,26 @@ export default class EasyFilterBarContainer extends Control {
637
834
  /**
638
835
  * The user has acknowledged the message strip and understood the issue.
639
836
  * When the tokens are updated next time, displaying the message strip again serves no purpose.
640
- * Additionally, once the tokens are updated, the input field text will be removed
641
- * rendering the message strip irrelevant. However, the message strip will be displayed again the next time the "GO" button is pressed.
837
+ * Additionally, once the tokens are updated, the input field text is removed
838
+ * rendering the message strip irrelevant. However, the message strip is displayed again the next time the "Go" button is pressed.
642
839
  */
643
840
  this.clearMessageStrip();
644
841
  this.clearMessageStripForSingleValue();
645
842
  }
646
843
  this.shouldMessageStripRemove = true;
647
844
  }
845
+
846
+ if (changedProps.includes("sorters")) {
847
+ this.fireEvent("sortersChanged", {
848
+ sorters: this.state.sorters
849
+ });
850
+ }
851
+
852
+ if (changedProps.includes("groupLevels")) {
853
+ this.fireEvent("groupLevelsChanged", {
854
+ groupLevels: this.state.groupLevels
855
+ });
856
+ }
648
857
  //Resetting to default values
649
858
  this.isMandatoryCheckRequired = false;
650
859
  this.shouldTokenChangeEventFired = true;
@@ -752,6 +961,7 @@ export default class EasyFilterBarContainer extends Control {
752
961
  }}
753
962
  liveChange={(): void => {
754
963
  this.state.tokenizerEditable = false;
964
+ this.fireEvent("liveChange", { query: this.getQuery() });
755
965
  }}
756
966
  />
757
967
 
@@ -797,6 +1007,7 @@ export default class EasyFilterBarContainer extends Control {
797
1007
  newTokens.splice(tokenIndex, 1);
798
1008
  this.state.tokens = newTokens;
799
1009
  this.updateFilterInput("tokenUpdated");
1010
+ this.fireEvent("tokensChangedByUser", { tokens: [...this.state.tokens] });
800
1011
  }}
801
1012
  >
802
1013
  {{
@@ -829,7 +1040,7 @@ export default class EasyFilterBarContainer extends Control {
829
1040
  />
830
1041
  </FlexBox>
831
1042
  </FlexBox>
832
- <FlexBox renderType="Bare">
1043
+ <VBox renderType="Bare" class={"sapUiTinyMarginTop sapFeControlsGap8px"}>
833
1044
  <MessageStrip
834
1045
  text={bindState(this.state, "singleValueMessageStripText")}
835
1046
  showIcon={true}
@@ -840,23 +1051,23 @@ export default class EasyFilterBarContainer extends Control {
840
1051
  this.clearMessageStripForSingleValue();
841
1052
  }}
842
1053
  ></MessageStrip>
843
- </FlexBox>
844
- <FlexBox renderType="Bare" class={"sapFeControlsGap8px"}>
845
- <MessageStrip
846
- type={bindState(this.state, "messageStripType")}
847
- text={bindState(this.state, "messageStripText")}
848
- showIcon={true}
849
- enableFormattedText={true}
850
- showCloseButton={true}
851
- close={(): void => {
852
- this.clearMessageStrip();
853
- }}
854
- visible={bindState(this.state, "showMessageStrip")}
855
- ></MessageStrip>
856
- <HBox visible={and(bindState(this.state, "showMessageStrip"), not(bindState(this.state, "showResult")))}>
857
- {thumbUpButtonMessageStripe}, {thumbDownButtonMessageStripe}
858
- </HBox>
859
- </FlexBox>
1054
+ <FlexBox renderType="Bare" class={"sapFeControlsGap8px"}>
1055
+ <MessageStrip
1056
+ type={bindState(this.state, "messageStripType")}
1057
+ text={bindState(this.state, "messageStripText")}
1058
+ showIcon={true}
1059
+ enableFormattedText={true}
1060
+ showCloseButton={true}
1061
+ close={(): void => {
1062
+ this.clearMessageStrip();
1063
+ }}
1064
+ visible={bindState(this.state, "showMessageStrip")}
1065
+ ></MessageStrip>
1066
+ <HBox visible={and(bindState(this.state, "showMessageStrip"), not(bindState(this.state, "showResult")))}>
1067
+ {thumbUpButtonMessageStripe}, {thumbDownButtonMessageStripe}
1068
+ </HBox>
1069
+ </FlexBox>
1070
+ </VBox>
860
1071
  </VBox>
861
1072
  ) as VBox;
862
1073
  FESRHelper.setSemanticStepname($topGoBtn.current!, "press", "fe:ai:search");
@@ -891,10 +1102,12 @@ export default class EasyFilterBarContainer extends Control {
891
1102
  resetState(clearAllFilters = true, isResettingToDefaultTokens = false): void {
892
1103
  if (clearAllFilters) {
893
1104
  this.fireEvent("clearFilters");
1105
+ this.state.groupLevels = [];
894
1106
  this.isResettingToDefaultTokens = isResettingToDefaultTokens;
895
1107
  } else {
896
1108
  this.shouldTokenChangeEventFired = false;
897
1109
  }
1110
+ this.state.sorters = [];
898
1111
  this.state.tokens = this.getDefaultTokens();
899
1112
  this.updateFilterInput("tokenReset");
900
1113
  this.state.tokenizerEditable = true;
@@ -905,6 +1118,12 @@ export default class EasyFilterBarContainer extends Control {
905
1118
  this.clearMessageStrip(); //clear the message strip for validated filters
906
1119
  }
907
1120
 
1121
+ clearFiltersAndTokens(): void {
1122
+ this.fireEvent("clearFilters");
1123
+ this.shouldTokenChangeEventFired = false;
1124
+ this.state.tokens = [];
1125
+ }
1126
+
908
1127
  //The below code updates the existing state by fetching the key and selectedValues
909
1128
  updateTokenArray(
910
1129
  changeType: TokenSetters,
@@ -932,6 +1151,7 @@ export default class EasyFilterBarContainer extends Control {
932
1151
  this.state.tokens = newTokens as TokenDefinition[];
933
1152
  this.state.showResult = newTokens.length > 0;
934
1153
  this.updateFilterInput("tokenUpdated");
1154
+ this.fireEvent("tokensChangedByUser", { tokens: [...this.state.tokens] });
935
1155
  }
936
1156
  }
937
1157
 
@@ -958,6 +1178,14 @@ export default class EasyFilterBarContainer extends Control {
958
1178
  return this.mandatoryKeyMap[key] ? true : false;
959
1179
  }
960
1180
 
1181
+ /**
1182
+ * Checks whether the current state has sort definitions from the easy filter.
1183
+ * @returns True if the current state contains one or more sorters.
1184
+ */
1185
+ private hasSorters(): boolean {
1186
+ return this.state.sorters.length > 0;
1187
+ }
1188
+
961
1189
  removeNonMandatoryTokens(): this {
962
1190
  const newToken: TokenDefinition[] = this.state.tokens.filter((token) => {
963
1191
  return this.isKeyMandatory(token.key);
@@ -966,6 +1194,33 @@ export default class EasyFilterBarContainer extends Control {
966
1194
  return this;
967
1195
  }
968
1196
 
1197
+ hasGroupLevels(): boolean {
1198
+ return this.state.groupLevels.length > 0;
1199
+ }
1200
+
1201
+ /**
1202
+ * Notifies the container that an external table state change (sort or group) has been detected.
1203
+ * If the container currently has active sorters or group levels, the filter input is marked dirty
1204
+ * because the external change makes the easy filter result inconsistent.
1205
+ */
1206
+ notifyExternalTableStateChange(): void {
1207
+ if (this.hasSorters() || this.hasGroupLevels()) {
1208
+ this.updateFilterInput("tokenUpdated");
1209
+ }
1210
+ }
1211
+
1212
+ getQuery(): string {
1213
+ return this.easyFilterInput.current?.getValue() ?? "";
1214
+ }
1215
+
1216
+ getTokens(): TokenDefinition[] {
1217
+ return [...this.state.tokens];
1218
+ }
1219
+
1220
+ setQuery(query: string): void {
1221
+ this.easyFilterInput.current?.setValue(query);
1222
+ }
1223
+
969
1224
  updateFilterInput(value: string): void {
970
1225
  const currValue: string = this.easyFilterInput.current?.getValue() || "";
971
1226
  if (value === "tokenUpdated" && currValue !== "") {