From 7cba7e173855b762459ce0effdb6f301e2931a9f Mon Sep 17 00:00:00 2001 From: Diana Petcheva Date: Sun, 20 Jul 2025 22:24:21 -0400 Subject: [PATCH 1/6] feat(view-settings-dialog): add group option --- .../cypress/specs/ViewSettingsDialog.cy.tsx | 120 ++++++++++- packages/fiori/src/GroupItem.ts | 43 ++++ packages/fiori/src/ViewSettingsDialog.ts | 186 +++++++++++++++- .../fiori/src/ViewSettingsDialogTemplate.tsx | 202 +++++++++++------- packages/fiori/src/bundle.esm.ts | 1 + .../fiori/src/i18n/messagebundle.properties | 11 +- .../fiori/src/types/ViewSettingsDialogMode.ts | 9 +- .../fiori/test/pages/ViewSettingsDialog.html | 27 +++ packages/fiori/test/ssr/component-imports.js | 1 + 9 files changed, 512 insertions(+), 88 deletions(-) create mode 100644 packages/fiori/src/GroupItem.ts diff --git a/packages/fiori/cypress/specs/ViewSettingsDialog.cy.tsx b/packages/fiori/cypress/specs/ViewSettingsDialog.cy.tsx index 8878f18671e2..c19924bf120f 100644 --- a/packages/fiori/cypress/specs/ViewSettingsDialog.cy.tsx +++ b/packages/fiori/cypress/specs/ViewSettingsDialog.cy.tsx @@ -1,10 +1,11 @@ import ViewSettingsDialog from "../../src/ViewSettingsDialog.js"; +import GroupItem from "../../src/GroupItem.js"; import SortItem from "../../src/SortItem.js"; import FilterItem from "../../src/FilterItem.js"; import FilterItemOption from "../../src/FilterItemOption.js"; -describe("View settings dialog - selection", () => { - it("tests clicking on sort items (both on the text and radio button)", () => { +describe("View settings dialog - confirm event", () => { + it("should throw confirm event after selecting sort options and confirm button", () => { cy.mount( @@ -86,7 +87,7 @@ describe("View settings dialog - selection", () => { .should("equal", "Name"); }); - it("tests clicking on filter items, and filter item options (both on the text and checkbox)", () => { + it("should throw confirm event after selecting filter options and confirm button", () => { cy.mount( @@ -167,6 +168,61 @@ describe("View settings dialog - selection", () => { .find("[ui5-dialog]") .should("not.be.visible"); }); + + it("should throw confirm event after selecting group options and confirm button", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-view-settings-dialog]") + .as("vsd") + .invoke("prop", "open", true); + + cy.get("@vsd") + .shadow() + .find("[group-order] ui5-li") + .as("groupOrderItems"); + + cy.get("@groupOrderItems") + .should("have.length", 2); + + // Select the group order (Descending) + cy.get("@groupOrderItems") + .eq(1) + .realClick(); + + cy.get("@vsd") + .shadow() + .find("[group-by] ui5-li") + .as("groupByItems"); + + cy.get("@groupByItems") + .should("have.length", 3); + + // Select the group by (No Group) + cy.get("@groupByItems") + .eq(2) + .realClick(); + + // Confirm the selection + cy.get("@vsd") + .shadow() + .find(".ui5-vsd-footer ui5-button") + .realClick(); + + // Check the confirm event for groupOrder and groupBy + cy.get("@confirm") + .should("be.calledWithMatch", { + detail: { + groupOrder: "Descending", + groupBy: "(No Group)", + }, + }); + }); }); describe("ViewSettingsDialog Tests", () => { @@ -370,4 +426,62 @@ describe("ViewSettingsDialog Tests", () => { cy.get("@vsd") .invoke("prop", "open", false); }); + + it("should handle group-only mode", () => { + cy.mount( + + + + + + + ); + + cy.get("#vsdGroup") + .as("vsd"); + + cy.get("@vsd") + .invoke("prop", "open", true); + + cy.get("@vsd") + .shadow() + .find("[ui5-segmented-button]") + .should("not.exist"); + + cy.get("@vsd") + .invoke("prop", "open", false); + }); + + it("should show a split button with all loaded VSD options", () => { + cy.mount( + + + + + + + + + + ); + + cy.get("[ui5-view-settings-dialog]") + .as("vsd") + .invoke("prop", "open", true); + + cy.get("@vsd") + .shadow() + .find("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .should("be.visible"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .as("items"); + + cy.get("@items") + .should("have.length", 3); + }); }); diff --git a/packages/fiori/src/GroupItem.ts b/packages/fiori/src/GroupItem.ts new file mode 100644 index 000000000000..fb8a238706a2 --- /dev/null +++ b/packages/fiori/src/GroupItem.ts @@ -0,0 +1,43 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; + +/** + * @class + * + * ### Overview + * + * ### Usage + * + * For the `ui5-group-item` + * ### ES6 Module Import + * + * `import @ui5/webcomponents-fiori/dist/GroupItem.js";` + * @constructor + * @extends UI5Element + * @abstract + * @since 2.13.0 + * @public + */ +@customElement("ui5-group-item") +class GroupItem extends UI5Element { + /** + * Defines the text of the component. + * @default undefined + * @public + */ + @property() + text?: string; + + /** + * Defines if the component is selected. + * @default false + * @public + */ + @property({ type: Boolean }) + selected = false; +} + +GroupItem.define(); + +export default GroupItem; diff --git a/packages/fiori/src/ViewSettingsDialog.ts b/packages/fiori/src/ViewSettingsDialog.ts index a95b34fc896d..3bcbf65cd7ea 100644 --- a/packages/fiori/src/ViewSettingsDialog.ts +++ b/packages/fiori/src/ViewSettingsDialog.ts @@ -17,9 +17,11 @@ import InvisibleMessageMode from "@ui5/webcomponents-base/dist/types/InvisibleMe import ViewSettingsDialogMode from "./types/ViewSettingsDialogMode.js"; import "@ui5/webcomponents-icons/dist/sort.js"; import "@ui5/webcomponents-icons/dist/filter.js"; +import "@ui5/webcomponents-icons/dist/group-2.js"; import "@ui5/webcomponents-icons/dist/nav-back.js"; import type SortItem from "./SortItem.js"; import type FilterItem from "./FilterItem.js"; +import type GroupItem from "./GroupItem.js"; import { VSD_DIALOG_TITLE_SORT, @@ -28,11 +30,14 @@ import { VSD_RESET_BUTTON, VSD_SORT_ORDER, VSD_SORT_BY, + VSD_GROUP_ORDER, + VSD_GROUP_BY, VSD_ORDER_ASCENDING, VSD_ORDER_DESCENDING, VSD_FILTER_BY, VSD_SORT_TOOLTIP, VSD_FILTER_TOOLTIP, + VSD_GROUP_TOOLTIP, VSD_RESET_BUTTON_ACTION, VSD_FILTER_ITEM_LABEL_TEXT, } from "./generated/i18n/i18n-defaults.js"; @@ -51,17 +56,23 @@ type VSDSettings = { sortOrder: string, sortBy: string, filters: VSDFilters, + groupOrder: string, + groupBy: string, } // Events' detail type ViewSettingsDialogConfirmEventDetail = VSDSettings & { sortByItem: SortItem, sortDescending: boolean, + groupByItem: GroupItem, + groupDescending: boolean, } type ViewSettingsDialogCancelEventDetail = VSDSettings & { sortByItem: SortItem, sortDescending: boolean, + groupByItem: GroupItem, + groupDescending: boolean, } // Common properties for several VSDInternalSettings fields @@ -72,6 +83,8 @@ type VSDInternalSettings = { sortOrder: Array, sortBy: Array, filters: Array}>, + groupOrder: Array, + groupBy: Array, } /** @@ -114,6 +127,10 @@ type VSDInternalSettings = { * @param {String} sortBy The currently selected `ui5-sort-item` text attribute. * @param {HTMLElement} sortByItem The currently selected `ui5-sort-item`. * @param {Boolean} sortDescending The selected sort order (true = descending, false = ascending). + * @param {String} groupOrder The current group order selected. + * @param {String} groupBy The currently selected `ui5-group-item` text attribute. + * @param {HTMLElement} groupByItem The currently selected `ui5-group-item`. + * @param {Boolean} groupDescending The selected group order (true = descending, false = ascending). * @param {Array} filters The selected filters items. * @public */ @@ -127,6 +144,10 @@ type VSDInternalSettings = { * @param {String} sortBy The currently selected `ui5-sort-item` text attribute. * @param {HTMLElement} sortByItem The currently selected `ui5-sort-item`. * @param {Boolean} sortDescending The selected sort order (true = descending, false = ascending). + * @param {String} groupOrder The current group order selected. + * @param {String} groupBy The currently selected `ui5-group-item` text attribute. + * @param {HTMLElement} groupByItem The currently selected `ui5-group-item`. + * @param {Boolean} groupDescending The selected group order (true = descending, false = ascending). * @param {Array} filters The selected filters items. * @public */ @@ -173,6 +194,15 @@ class ViewSettingsDialog extends UI5Element { @property({ type: Boolean }) sortDescending = false; + /** + * Defines the initial group order. + * @default false + * @since 2.13.0 + * @public + */ + @property({ type: Boolean }) + groupDescending = false; + /** * Indicates if the dialog is open. * @public @@ -198,6 +228,8 @@ class ViewSettingsDialog extends UI5Element { sortOrder: [], sortBy: [], filters: [], + groupOrder: [], + groupBy: [], }; /** @@ -237,7 +269,7 @@ class ViewSettingsDialog extends UI5Element { * @public */ @slot() - sortItems!: Array + sortItems!: Array; /** * Defines the `filterItems` list. @@ -248,9 +280,20 @@ class ViewSettingsDialog extends UI5Element { @slot() filterItems!: Array; + /** + * Defines the list of items against which the user could group data. + * + * **Note:** If you want to use this slot, you need to import used item: `import "@ui5/webcomponents-fiori/dist/GroupItem.js";` + * @public + */ + @slot() + groupItems!: Array; + _dialog?: Dialog; _sortOrder?: List; _sortBy?: List; + _groupOrder?: List; + _groupBy?: List; @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; @@ -260,8 +303,18 @@ class ViewSettingsDialog extends UI5Element { this._setAdditionalTexts(); } - if (!this.shouldBuildSort && this.shouldBuildFilter) { + if (this.shouldBuildSort) { + return; + } + + if (this.shouldBuildFilter) { this._currentMode = ViewSettingsDialogMode.Filter; + return; + } + + if (this.shouldBuildGroup) { + this._currentMode = ViewSettingsDialogMode.Group; + return; } } @@ -307,8 +360,13 @@ class ViewSettingsDialog extends UI5Element { return !!this.filterItems.length; } + get shouldBuildGroup() { + return !!this.groupItems.length; + } + get hasPagination() { - return this.shouldBuildSort && this.shouldBuildFilter; + const buildConditions = [this.shouldBuildSort, this.shouldBuildFilter, this.shouldBuildGroup]; + return buildConditions.filter(condition => condition).length > 1; } get _filterByTitle() { @@ -344,6 +402,10 @@ class ViewSettingsDialog extends UI5Element { return ViewSettingsDialog.i18nBundle.getText(VSD_SORT_ORDER); } + get _groupOrderLabel() { + return ViewSettingsDialog.i18nBundle.getText(VSD_GROUP_ORDER); + } + get _filterByLabel() { return ViewSettingsDialog.i18nBundle.getText(VSD_FILTER_BY); } @@ -352,6 +414,10 @@ class ViewSettingsDialog extends UI5Element { return ViewSettingsDialog.i18nBundle.getText(VSD_SORT_BY); } + get _groupByLabel() { + return ViewSettingsDialog.i18nBundle.getText(VSD_GROUP_BY); + } + get _sortButtonTooltip() { return ViewSettingsDialog.i18nBundle.getText(VSD_SORT_TOOLTIP); } @@ -360,6 +426,10 @@ class ViewSettingsDialog extends UI5Element { return ViewSettingsDialog.i18nBundle.getText(VSD_FILTER_TOOLTIP); } + get _groupButtonTooltip() { + return ViewSettingsDialog.i18nBundle.getText(VSD_GROUP_TOOLTIP); + } + get _resetButtonAction() { return ViewSettingsDialog.i18nBundle.getText(VSD_RESET_BUTTON_ACTION); } @@ -382,14 +452,14 @@ class ViewSettingsDialog extends UI5Element { * Determines disabled state of the `Reset` button. */ get _disableResetButton() { - return this._dialog && this._sortSetttingsAreInitial && this._filteresAreInitial; + return this._dialog && this._setttingsAreInitial && this._filteresAreInitial; } - get _sortSetttingsAreInitial() { + get _setttingsAreInitial() { let settingsAreInitial = true; - ["sortBy", "sortOrder"].forEach(sortList => { - this._currentSettings[sortList as keyof VSDInternalSettings].forEach((item, index) => { - if (item.selected !== this._initialSettings[sortList as keyof VSDInternalSettings][index].selected) { + ["sortBy", "sortOrder", "groupBy", "groupOrder"].forEach(settingsList => { + this._currentSettings[settingsList as keyof VSDInternalSettings].forEach((item, index) => { + if (item.selected !== this._initialSettings[settingsList as keyof VSDInternalSettings][index].selected) { settingsAreInitial = false; } }); @@ -418,6 +488,8 @@ class ViewSettingsDialog extends UI5Element { return { sortOrder: JSON.parse(JSON.stringify(this.initSortOrderItems)), sortBy: JSON.parse(JSON.stringify(this.initSortByItems)), + groupOrder: JSON.parse(JSON.stringify(this.initGroupOrderItems)), + groupBy: JSON.parse(JSON.stringify(this.initGroupByItems)), filters: this.filterItems.map(item => { return { text: item.text || "", @@ -443,6 +515,16 @@ class ViewSettingsDialog extends UI5Element { }); } + get initGroupByItems() { + return this.groupItems.map((item, index) => { + return { + text: item.text, + selected: item.selected, + index, + }; + }); + } + get initSortOrderItems() { return [ { @@ -456,6 +538,19 @@ class ViewSettingsDialog extends UI5Element { ]; } + get initGroupOrderItems() { + return [ + { + text: this._ascendingLabel, + selected: !this.groupDescending, + }, + { + text: this._descendingLabel, + selected: this.groupDescending, + }, + ]; + } + get expandContent() { return this._filterStepTwo || !this.hasPagination; } @@ -468,6 +563,10 @@ class ViewSettingsDialog extends UI5Element { return this._currentMode === ViewSettingsDialogMode.Filter; } + get isModeGroup() { + return this._currentMode === ViewSettingsDialogMode.Group; + } + get showBackButton() { return this.isModeFilter && this._filterStepTwo; } @@ -480,6 +579,14 @@ class ViewSettingsDialog extends UI5Element { return this.shadowRoot!.querySelector("[ui5-list][sort-by]")!; } + get _groupOrderListDomRef() { + return this.shadowRoot!.querySelector("[ui5-list][group-order]")!; + } + + get _groupByList() { + return this.shadowRoot!.querySelector("[ui5-list][group-by]")!; + } + get _dialogDomRef() { return this.shadowRoot!.querySelector("[ui5-dialog]")!; } @@ -492,6 +599,9 @@ class ViewSettingsDialog extends UI5Element { this._sortOrder = this._sortOrderListDomRef; this._sortBy = this._sortByList; + this._groupOrder = this._groupOrderListDomRef; + this._groupBy = this._groupByList; + // Sorting this._initialSettings = this._settings; this._currentSettings = this._settings; @@ -605,17 +715,28 @@ class ViewSettingsDialog extends UI5Element { get eventsParams() { const _currentSortOrderSelected = this._currentSettings.sortOrder.filter(item => item.selected)[0], _currentSortBySelected = this._currentSettings.sortBy.filter(item => item.selected)[0], + _currentGroupOrderSelected = this._currentSettings.groupOrder.filter(item => item.selected)[0], + _currentGroupBySelected = this._currentSettings.groupBy.filter(item => item.selected)[0], sortOrder = _currentSortOrderSelected && (_currentSortOrderSelected.text || ""), sortDescending = !this._currentSettings.sortOrder[0].selected, sortBy = _currentSortBySelected && (_currentSortBySelected.text || ""), sortByElementIndex = _currentSortBySelected && _currentSortBySelected.index, sortByItem = this.sortItems[sortByElementIndex], + groupOrder = _currentGroupOrderSelected && (_currentGroupOrderSelected.text || ""), + groupDescending = !this._currentSettings.groupOrder[0].selected, + groupBy = _currentGroupBySelected && (_currentGroupBySelected.text || ""), + groupByElementIndex = _currentGroupBySelected && _currentGroupBySelected.index, + groupByItem = this.groupItems[groupByElementIndex], selectedFilterItems = this.filterItems.filter(filterItem => filterItem.values.some(item => item.selected)); return { sortOrder, sortDescending, sortBy, sortByItem, + groupOrder, + groupDescending, + groupBy, + groupByItem, filters: this.selectedFilters, filterItems: selectedFilterItems, }; @@ -702,6 +823,35 @@ class ViewSettingsDialog extends UI5Element { this._currentSettings = JSON.parse(JSON.stringify(this._currentSettings)); } + /** + * Stores `Group Order` list as recently used control and its selected item in current state. + */ + _onGroupOrderChange(e: CustomEvent) { + this._recentlyFocused = this._groupOrder!; + this._currentSettings.groupOrder = this.initGroupOrderItems.map(item => { + item.selected = item.text === e.detail.targetItem.innerText; + return item; + }); + + // Invalidate + this._currentSettings = JSON.parse(JSON.stringify(this._currentSettings)); + } + + /** + * Stores `Group By` list as recently used control and its selected item in current state. + */ + _onGroupByChange(e: CustomEvent) { + const selectedItemIndex = Number(e.detail.targetItem.getAttribute("data-ui5-external-action-item-index")); + this._recentlyFocused = this._groupBy!; + this._currentSettings.groupBy = this.initGroupByItems.map((item, index) => { + item.selected = index === selectedItemIndex; + return item; + }); + + // Invalidate + this._currentSettings = JSON.parse(JSON.stringify(this._currentSettings)); + } + /** * Sets a JavaScript object, as settings to the `ui5-view-settings-dialog`. * This method can be used after the dialog is initially open, as the dialog needs @@ -735,6 +885,26 @@ class ViewSettingsDialog extends UI5Element { } } + if (settings.groupOrder) { + for (let i = 0; i < tempSettings.groupOrder.length; i++) { + if (tempSettings.groupOrder[i].text === settings.groupOrder) { + tempSettings.groupOrder[i].selected = true; + } else { + tempSettings.groupOrder[i].selected = false; + } + } + } + + if (settings.groupBy) { + for (let i = 0; i < tempSettings.groupBy.length; i++) { + if (tempSettings.groupBy[i].text === settings.groupBy) { + tempSettings.groupBy[i].selected = true; + } else { + tempSettings.groupBy[i].selected = false; + } + } + } + if (settings.filters) { const inputFilters: VSDFilter = {}; for (let i = 0; i < settings.filters.length; i++) { diff --git a/packages/fiori/src/ViewSettingsDialogTemplate.tsx b/packages/fiori/src/ViewSettingsDialogTemplate.tsx index 31534bc6a7b1..c78fea2555cb 100644 --- a/packages/fiori/src/ViewSettingsDialogTemplate.tsx +++ b/packages/fiori/src/ViewSettingsDialogTemplate.tsx @@ -9,6 +9,7 @@ import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import navBackIcon from "@ui5/webcomponents-icons/dist/nav-back.js"; import sortIcon from "@ui5/webcomponents-icons/dist/sort.js"; import filterIcon from "@ui5/webcomponents-icons/dist/filter.js"; +import groupIcon from "@ui5/webcomponents-icons/dist/group-2.js"; import type ViewSettingsDialog from "./ViewSettingsDialog.js"; @@ -46,18 +47,7 @@ function ViewSettingsDialogTemplateHeader(this: ViewSettingsDialog) { - - + {_getSplitButtonItems.call(this)} @@ -66,6 +56,45 @@ function ViewSettingsDialogTemplateHeader(this: ViewSettingsDialog) { ); } +function _getSplitButtonItems(this: ViewSettingsDialog) { + const buttonItems = []; + + if (this.shouldBuildSort) { + buttonItems.push( + + ); + } + + if (this.shouldBuildFilter) { + buttonItems.push( + + ); + } + + if (this.shouldBuildGroup) { + buttonItems.push( + + ); + } + + return buttonItems; +} + function ViewSettingsDialogTemplateContent(this: ViewSettingsDialog) { return (
{this.shouldBuildSort && this.isModeSort && ( -
- - - {this._currentSettings.sortOrder.map(item => ( - {item.text} - ))} - - - - - {this._currentSettings.sortBy.map((item, index) => ( - {item.text} - ))} - - -
+ ViewSettingsDialogSortAndGroupTemplate.call(this, true) + )} + + {this.shouldBuildFilter && this.isModeFilter && ( + ViewSettingsDialogFilterTemplate.call(this) )} - {this.shouldBuildFilter && this.isModeFilter && (<> - {this._filterStepTwo ? ( - - {this._currentSettings.filters.filter(item => item.selected).map(item => (<> - {item.filterOptions.map(option => ( - {option.text} - ))} - ))} - - ) : ( // else - - - {this.filterItems.map(item => ( - {item.text} - ))} - - - )} - )} + + {this.shouldBuildGroup && this.isModeGroup && ( + ViewSettingsDialogSortAndGroupTemplate.call(this, false) + )} +
); } +function ViewSettingsDialogSortAndGroupTemplate(this: ViewSettingsDialog, sortMode: boolean) { + const currentSettingsOrder = sortMode ? this._currentSettings.sortOrder : this._currentSettings.groupOrder; + const currentSettingsBy = sortMode ? this._currentSettings.sortBy : this._currentSettings.groupBy; + + return ( +
+ + + {currentSettingsOrder.map(item => ( + {item.text} + ))} + + + + + {currentSettingsBy.map((item, index) => ( + {item.text} + ))} + + +
+ ); +} + +function ViewSettingsDialogFilterTemplate(this: ViewSettingsDialog) { + return ( + <> + {this._filterStepTwo ? ( + + {this._currentSettings.filters.filter(item => item.selected).map(item => (<> + {item.filterOptions.map(option => ( + {option.text} + ))} + ))} + + ) : ( // else + + + {this.filterItems.map(item => ( + {item.text} + ))} + + + )} + + ); +} + function ViewSettingsDialogTemplateFooter(this: ViewSettingsDialog) { return (