@sapui5/sap.fe.controls 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.
@@ -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
 
@@ -405,6 +436,8 @@ export default class EasyFilterBarContainer extends Control {
405
436
  if (this.easyFilterInput.current?.isHistoryEnabled() ?? false) {
406
437
  this.easyFilterInput.current?.addRecentQuery(magicQuery);
407
438
  }
439
+ // Set query as successful in the input field
440
+ this.easyFilterInput.current?.setQuerySuccess(magicQuery);
408
441
  if (easyFilterResult.data.version === 1) {
409
442
  this.handleV1Success(easyFilterResult);
410
443
  } else if (easyFilterResult.data.version === 2) {
@@ -412,10 +445,12 @@ export default class EasyFilterBarContainer extends Control {
412
445
  }
413
446
  } else {
414
447
  // error
448
+ this.easyFilterInput.current?.setQueryFailure();
415
449
  this.removeNonMandatoryTokens();
416
450
  this.setMessageStrip(easyFilterResult.message);
417
451
  this.invisibleMessage.announce(this.resourceBundle.getText(easyFilterResult.message), InvisibleMessageMode.Assertive);
418
452
  this.state.messageStripType = MessageType.Warning;
453
+ this.state.groupLevels = [];
419
454
  Log.error("Error while generating filter criteria: ", easyFilterResult.message);
420
455
  }
421
456
 
@@ -498,12 +533,162 @@ export default class EasyFilterBarContainer extends Control {
498
533
  }
499
534
 
500
535
  const updatedTokens = this.verifySingleSelectTokenValues(tokens);
536
+
537
+ // Validate and store sort expressions (applied to table directly, not shown as tokens)
538
+ this.state.sorters = this.supportsSorting ? this.validateSortExpressions(easyFilterResult.data.sorters) : [];
501
539
  this.state.showResult = true;
502
540
  this.state.thumbButtonEnabled = true;
503
541
  this.state.thumbDownButtonPressed = false;
504
542
  this.state.thumbUpButtonPressed = false;
505
543
  this.state.tokens = updatedTokens;
506
544
  }
545
+
546
+ this.state.groupLevels = this.supportsGrouping ? this.applyGroupLevels(easyFilterResult.data.groupLevels) : [];
547
+
548
+ this.showUnsupportedCapabilitiesWarning(easyFilterResult.data.sorters, easyFilterResult.data.groupLevels);
549
+ }
550
+
551
+ /**
552
+ * Shows a warning when the app does not support the LLM's sort/group expressions.
553
+ * @param sorters Sort expressions from the LLM.
554
+ * @param groupLevels Group levels from the LLM.
555
+ */
556
+ private showUnsupportedCapabilitiesWarning(
557
+ sorters: EasyFilter.EasyFilterSortExpression[] | undefined,
558
+ groupLevels: EasyFilter.EasyFilterGroupLevelDefinition[] | undefined
559
+ ): void {
560
+ const hasSortersFromLLM = sorters && sorters.length > 0;
561
+ const hasGroupLevelsFromLLM = groupLevels && groupLevels.length > 0;
562
+ const sortingUnsupported = !this.supportsSorting && hasSortersFromLLM;
563
+ const groupingUnsupported = !this.supportsGrouping && hasGroupLevelsFromLLM;
564
+
565
+ if (sortingUnsupported && groupingUnsupported) {
566
+ this.appendMessageStrip(this.resourceBundle.getText("M_EASY_FILTER_SORTING_AND_GROUPING_NOT_SUPPORTED"), MessageType.Warning);
567
+ } else if (sortingUnsupported) {
568
+ this.appendMessageStrip(this.resourceBundle.getText("M_EASY_FILTER_SORTING_NOT_SUPPORTED"), MessageType.Warning);
569
+ } else if (groupingUnsupported) {
570
+ this.appendMessageStrip(this.resourceBundle.getText("M_EASY_FILTER_GROUPING_NOT_SUPPORTED"), MessageType.Warning);
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Updates whether the app supports sorting and grouping based on table personalization settings.
576
+ * @param sorting Whether the app supports sorting.
577
+ * @param grouping Whether the app supports grouping.
578
+ */
579
+ public setPersonalizationSupport(sorting: boolean, grouping: boolean): void {
580
+ this.supportsSorting = sorting;
581
+ this.supportsGrouping = grouping;
582
+ }
583
+
584
+ /**
585
+ * Validates and triggers the groupLevelsChanged function, which warns about non-groupable fields.
586
+ * @param groupLevels The group levels to validate and apply
587
+ * @returns GroupLevelDefinition[] An array of valid group level definitions to be applied
588
+ */
589
+ private applyGroupLevels(groupLevels: GroupLevelDefinition[] | undefined): GroupLevelDefinition[] {
590
+ const metadata = this.filterBarMetadata as EasyFilterPropertyMetadata[];
591
+
592
+ if (!metadata || !groupLevels || groupLevels.length === 0) {
593
+ return [];
594
+ }
595
+
596
+ const resolveField = (groupDef: GroupLevelDefinition): EasyFilterPropertyMetadata | undefined => {
597
+ const propertyName = EasyFilterUtils.resolvePropertyName(groupDef.key);
598
+ const field = metadata.find((f) => f.name === groupDef.key || f.name.endsWith(`/${propertyName}`));
599
+ if (!field) {
600
+ Log.warning(`EasyFilterBarContainer: group key "${groupDef.key}" not found in filter bar metadata, skipping`);
601
+ }
602
+ return field;
603
+ };
604
+
605
+ if (groupLevels.length === 1) {
606
+ const errorHandler = new ErrorMessageHandler();
607
+ const field = resolveField(groupLevels[0]);
608
+ if (!field) return [];
609
+ const isGroupable = errorHandler.isGroupableProperty(field);
610
+ if (errorHandler.hasErrors()) {
611
+ this.appendMessageStrip(encodeXML(errorHandler.getErrorMessage()), MessageType.Warning);
612
+ }
613
+ return isGroupable ? [groupLevels[0]] : [];
614
+ }
615
+
616
+ // Multiple group levels: find the first valid groupable field
617
+ const errorHandler = new ErrorMessageHandler();
618
+ let firstGroupableDef: GroupLevelDefinition | undefined;
619
+ let firstGroupableLabel: string | undefined;
620
+
621
+ for (const groupDef of groupLevels) {
622
+ const field = resolveField(groupDef);
623
+ if (!field) continue;
624
+ if (errorHandler.isGroupableProperty(field)) {
625
+ if (!firstGroupableDef) {
626
+ firstGroupableDef = groupDef;
627
+ firstGroupableLabel = field.label ?? field.name;
628
+ }
629
+ }
630
+ }
631
+
632
+ if (firstGroupableDef) {
633
+ const warning = this.resourceBundle.getText("M_EASY_FILTER_MULTIPLE_GROUP_WARNING", [firstGroupableLabel]);
634
+ this.appendMessageStrip(encodeXML(warning), MessageType.Warning);
635
+ return [firstGroupableDef];
636
+ }
637
+
638
+ // All fields are non-groupable
639
+ if (errorHandler.hasErrors()) {
640
+ this.appendMessageStrip(encodeXML(errorHandler.getErrorMessage()), MessageType.Warning);
641
+ }
642
+ return [];
643
+ }
644
+
645
+ /**
646
+ * Validates sort expressions from the AI result and returns valid sort definitions.
647
+ * @param sortExpressions The sort expressions from the ABAP service.
648
+ * @returns Array of validated sort definitions.
649
+ * @private
650
+ */
651
+ private validateSortExpressions(sortExpressions?: EasyFilter.EasyFilterSortExpression[]): SortDefinition[] {
652
+ if (!sortExpressions || sortExpressions.length === 0) {
653
+ return [];
654
+ }
655
+
656
+ const nonSortableFields: string[] = [];
657
+
658
+ const validSorters = sortExpressions
659
+ .filter((sortExpression) => {
660
+ // Validate that the field exists in metadata and is sortable
661
+ const fieldMetadata = this.filterBarMetadata?.find((field) => {
662
+ const fieldName = field.name.split("/").pop();
663
+ return fieldName === sortExpression.key || field.name === sortExpression.key;
664
+ });
665
+
666
+ if (!fieldMetadata) {
667
+ return false;
668
+ }
669
+
670
+ if (fieldMetadata.sortable === false) {
671
+ nonSortableFields.push(fieldMetadata.label ?? sortExpression.key);
672
+ return false;
673
+ }
674
+
675
+ return true;
676
+ })
677
+ .map((sortExpression) => {
678
+ const propertyName = sortExpression.key.split("/").pop() ?? sortExpression.key;
679
+ return {
680
+ key: propertyName,
681
+ descending: sortExpression.descending
682
+ };
683
+ });
684
+
685
+ // Show warning message for non-sortable fields
686
+ if (nonSortableFields.length > 0) {
687
+ const message = this.resourceBundle.getText("M_EASY_FILTER_NON_SORTABLE", [nonSortableFields.join(", ")]);
688
+ this.appendMessageStrip(message, MessageType.Warning);
689
+ }
690
+
691
+ return validSorters;
507
692
  }
508
693
 
509
694
  //every single select menu type should have only one value, else splice to one value and update the message strip
@@ -547,6 +732,21 @@ export default class EasyFilterBarContainer extends Control {
547
732
  this.state.showMessageStrip = true;
548
733
  }
549
734
 
735
+ setMessageStripWithType(text: string, type: MessageType): void {
736
+ this.setMessageStrip(text);
737
+ this.state.messageStripType = type;
738
+ }
739
+
740
+ /**
741
+ * Appends a message to the existing message strip instead of overwriting it.
742
+ * @param text The message text to append.
743
+ * @param type The message type for this message.
744
+ */
745
+ appendMessageStrip(text: string, type: MessageType): void {
746
+ const combined = this.state.showMessageStrip && this.state.messageStripText ? `${this.state.messageStripText}<br>${text}` : text;
747
+ this.setMessageStripWithType(combined, type);
748
+ }
749
+
550
750
  createPopoverForInnerControls(): Popover {
551
751
  if (!this.innerControlPopover) {
552
752
  this.innerControlPopover = (
@@ -637,14 +837,26 @@ export default class EasyFilterBarContainer extends Control {
637
837
  /**
638
838
  * The user has acknowledged the message strip and understood the issue.
639
839
  * 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.
840
+ * Additionally, once the tokens are updated, the input field text is removed
841
+ * rendering the message strip irrelevant. However, the message strip is displayed again the next time the "Go" button is pressed.
642
842
  */
643
843
  this.clearMessageStrip();
644
844
  this.clearMessageStripForSingleValue();
645
845
  }
646
846
  this.shouldMessageStripRemove = true;
647
847
  }
848
+
849
+ if (changedProps.includes("sorters")) {
850
+ this.fireEvent("sortersChanged", {
851
+ sorters: this.state.sorters
852
+ });
853
+ }
854
+
855
+ if (changedProps.includes("groupLevels")) {
856
+ this.fireEvent("groupLevelsChanged", {
857
+ groupLevels: this.state.groupLevels
858
+ });
859
+ }
648
860
  //Resetting to default values
649
861
  this.isMandatoryCheckRequired = false;
650
862
  this.shouldTokenChangeEventFired = true;
@@ -752,6 +964,7 @@ export default class EasyFilterBarContainer extends Control {
752
964
  }}
753
965
  liveChange={(): void => {
754
966
  this.state.tokenizerEditable = false;
967
+ this.fireEvent("liveChange", { query: this.getQuery() });
755
968
  }}
756
969
  />
757
970
 
@@ -797,6 +1010,7 @@ export default class EasyFilterBarContainer extends Control {
797
1010
  newTokens.splice(tokenIndex, 1);
798
1011
  this.state.tokens = newTokens;
799
1012
  this.updateFilterInput("tokenUpdated");
1013
+ this.fireEvent("tokensChangedByUser", { tokens: [...this.state.tokens] });
800
1014
  }}
801
1015
  >
802
1016
  {{
@@ -829,7 +1043,7 @@ export default class EasyFilterBarContainer extends Control {
829
1043
  />
830
1044
  </FlexBox>
831
1045
  </FlexBox>
832
- <FlexBox renderType="Bare">
1046
+ <VBox renderType="Bare" class={"sapUiTinyMarginTop sapFeControlsGap8px"}>
833
1047
  <MessageStrip
834
1048
  text={bindState(this.state, "singleValueMessageStripText")}
835
1049
  showIcon={true}
@@ -840,26 +1054,29 @@ export default class EasyFilterBarContainer extends Control {
840
1054
  this.clearMessageStripForSingleValue();
841
1055
  }}
842
1056
  ></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>
1057
+ <FlexBox renderType="Bare" class={"sapFeControlsGap8px"}>
1058
+ <MessageStrip
1059
+ type={bindState(this.state, "messageStripType")}
1060
+ text={bindState(this.state, "messageStripText")}
1061
+ showIcon={true}
1062
+ enableFormattedText={true}
1063
+ showCloseButton={true}
1064
+ close={(): void => {
1065
+ this.clearMessageStrip();
1066
+ }}
1067
+ visible={bindState(this.state, "showMessageStrip")}
1068
+ ></MessageStrip>
1069
+ <HBox visible={and(bindState(this.state, "showMessageStrip"), not(bindState(this.state, "showResult")))}>
1070
+ {thumbUpButtonMessageStripe}, {thumbDownButtonMessageStripe}
1071
+ </HBox>
1072
+ </FlexBox>
1073
+ </VBox>
860
1074
  </VBox>
861
1075
  ) as VBox;
862
1076
  FESRHelper.setSemanticStepname($topGoBtn.current!, "press", "fe:ai:search");
1077
+ if (this.easyFilterInput.current) {
1078
+ FESRHelper.setSemanticStepname(this.easyFilterInput.current, "enterPressed", "fe:ai:search");
1079
+ }
863
1080
  return outVBox;
864
1081
  }
865
1082
 
@@ -891,10 +1108,12 @@ export default class EasyFilterBarContainer extends Control {
891
1108
  resetState(clearAllFilters = true, isResettingToDefaultTokens = false): void {
892
1109
  if (clearAllFilters) {
893
1110
  this.fireEvent("clearFilters");
1111
+ this.state.groupLevels = [];
894
1112
  this.isResettingToDefaultTokens = isResettingToDefaultTokens;
895
1113
  } else {
896
1114
  this.shouldTokenChangeEventFired = false;
897
1115
  }
1116
+ this.state.sorters = [];
898
1117
  this.state.tokens = this.getDefaultTokens();
899
1118
  this.updateFilterInput("tokenReset");
900
1119
  this.state.tokenizerEditable = true;
@@ -903,6 +1122,14 @@ export default class EasyFilterBarContainer extends Control {
903
1122
  this.tokenizer.current?.getTokens().forEach((token) => token.setProperty("valueState", ValueState.None));
904
1123
  this.tokenizer.current?.getTokens().forEach((token) => token.setProperty("valueStateText", null));
905
1124
  this.clearMessageStrip(); //clear the message strip for validated filters
1125
+ // Clear query success status
1126
+ this.easyFilterInput.current?.clearQueryStatus();
1127
+ }
1128
+
1129
+ clearFiltersAndTokens(): void {
1130
+ this.fireEvent("clearFilters");
1131
+ this.shouldTokenChangeEventFired = false;
1132
+ this.state.tokens = [];
906
1133
  }
907
1134
 
908
1135
  //The below code updates the existing state by fetching the key and selectedValues
@@ -932,6 +1159,7 @@ export default class EasyFilterBarContainer extends Control {
932
1159
  this.state.tokens = newTokens as TokenDefinition[];
933
1160
  this.state.showResult = newTokens.length > 0;
934
1161
  this.updateFilterInput("tokenUpdated");
1162
+ this.fireEvent("tokensChangedByUser", { tokens: [...this.state.tokens] });
935
1163
  }
936
1164
  }
937
1165
 
@@ -958,6 +1186,14 @@ export default class EasyFilterBarContainer extends Control {
958
1186
  return this.mandatoryKeyMap[key] ? true : false;
959
1187
  }
960
1188
 
1189
+ /**
1190
+ * Checks whether the current state has sort definitions from the easy filter.
1191
+ * @returns True if the current state contains one or more sorters.
1192
+ */
1193
+ private hasSorters(): boolean {
1194
+ return this.state.sorters.length > 0;
1195
+ }
1196
+
961
1197
  removeNonMandatoryTokens(): this {
962
1198
  const newToken: TokenDefinition[] = this.state.tokens.filter((token) => {
963
1199
  return this.isKeyMandatory(token.key);
@@ -966,6 +1202,33 @@ export default class EasyFilterBarContainer extends Control {
966
1202
  return this;
967
1203
  }
968
1204
 
1205
+ hasGroupLevels(): boolean {
1206
+ return this.state.groupLevels.length > 0;
1207
+ }
1208
+
1209
+ /**
1210
+ * Notifies the container that an external table state change (sort or group) has been detected.
1211
+ * If the container currently has active sorters or group levels, the filter input is marked dirty
1212
+ * because the external change makes the easy filter result inconsistent.
1213
+ */
1214
+ notifyExternalTableStateChange(): void {
1215
+ if (this.hasSorters() || this.hasGroupLevels()) {
1216
+ this.updateFilterInput("tokenUpdated");
1217
+ }
1218
+ }
1219
+
1220
+ getQuery(): string {
1221
+ return this.easyFilterInput.current?.getValue() ?? "";
1222
+ }
1223
+
1224
+ getTokens(): TokenDefinition[] {
1225
+ return [...this.state.tokens];
1226
+ }
1227
+
1228
+ setQuery(query: string): void {
1229
+ this.easyFilterInput.current?.setValue(query);
1230
+ }
1231
+
969
1232
  updateFilterInput(value: string): void {
970
1233
  const currValue: string = this.easyFilterInput.current?.getValue() || "";
971
1234
  if (value === "tokenUpdated" && currValue !== "") {