Last active
May 18, 2025 13:51
-
-
Save Maverick-I0/906390477578849b8148edaaac6cafc7 to your computer and use it in GitHub Desktop.
A value help that loads all the data on call, with a basic search functionality, all on the TableSelectDialog from SAP UI5 framework
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| sap.ui.define( | |
| [ | |
| "sap/m/MessageBox", | |
| "sap/m/MessageToast", | |
| "sap/m/VBox", | |
| "sap/m/Text", | |
| "sap/m/Button", | |
| "sap/m/Dialog", | |
| "sap/m/ScrollContainer", | |
| "sap/m/FormattedText", | |
| ], | |
| function (MessageBox, MessageToast, VBox, Text, Button, Dialog, ScrollContainer, FormattedText) { | |
| /** | |
| * | |
| * @param {*} title | |
| * @param {*} sMessage | |
| * @param {*} sDetails | |
| * @param {*} onOkPress | |
| */ | |
| const standardMessageBox = function (title, icon, state, sMessage, sDetails, onOkPress) { | |
| // Helper function to check if a string is HTML | |
| const isHTMLString = function (str) { | |
| return /<\/?[a-z][\s\S]*>/i.test(str); // Simple regex to detect HTML-like strings | |
| }; | |
| // Determine the type of text element to create | |
| let textElement; | |
| if (typeof sMessage === "string") { | |
| if (isHTMLString(sMessage)) { | |
| textElement = new FormattedText({ htmlText: sMessage }); // Render as FormattedText if it's HTML | |
| } else { | |
| textElement = new Text({ text: sMessage, wrapping: true, renderWhitespace: true }); // Render as plain Text | |
| } | |
| } else if (sMessage instanceof Text || sMessage instanceof FormattedText) { | |
| textElement = sMessage; // Use the passed UI5 control directly | |
| } else { | |
| throw new Error("sMessage must be a string, a Text, or a FormattedText element."); | |
| } | |
| /// Scrollabble extrea container | |
| const oScrollContainer = new ScrollContainer({ | |
| horizontal: false, | |
| vertical: true, | |
| content: [ | |
| isHTMLString(sDetails) | |
| ? new FormattedText({ htmlText: sDetails }) | |
| : new Text({ | |
| text: sDetails, | |
| wrapping: true, | |
| renderWhitespace: true, | |
| }), | |
| ], | |
| }); | |
| const oVBox = new VBox({ | |
| items: [ | |
| textElement, | |
| sDetails | |
| ? new Button({ | |
| icon: "sap-icon://message-information", | |
| text: "Show More Information", | |
| press: function () { | |
| if (sDetails) { | |
| oVBox.addItem(oScrollContainer); | |
| } else { | |
| sap.m.MessageToast.show("No additional Details available!"); | |
| } | |
| }, | |
| }) | |
| : null, | |
| ].filter(Boolean), | |
| }); | |
| // Create the dialog with the VBox content | |
| var oDialog = new Dialog({ | |
| title: title, | |
| state: state, | |
| type: sap.m.DialogType.Message, | |
| content: oVBox, | |
| icon: icon, | |
| beginButton: new Button({ | |
| type: "Emphasized", | |
| text: "OK", | |
| press: function () { | |
| // Callback function for OK button | |
| oDialog.close(); | |
| onOkPress(); | |
| }, | |
| }), | |
| afterClose: function () { | |
| // Clean up dialog after close | |
| oDialog.destroy(); | |
| }, | |
| }); | |
| // Open the dialog | |
| oDialog.open(); | |
| }; | |
| return { | |
| toast: function (sMessage) { | |
| if (!sMessage) { | |
| return; | |
| } | |
| MessageToast.show(sMessage); | |
| return; | |
| }, | |
| messageBox: { | |
| info: function (sMessage, sdetails, fnCallback = function () {}) { | |
| standardMessageBox("Information", "sap-icon://information", sap.ui.core.ValueState.Information, sMessage, sdetails, fnCallback); | |
| }, | |
| error: function (sMessage, sdetails, fnCallback = function () {}) { | |
| standardMessageBox("Error", "sap-icon://error", sap.ui.core.ValueState.Error, sMessage, sdetails, fnCallback); | |
| }, | |
| warning: function (sMessage, sdetails, fnCallback = function () {}) { | |
| standardMessageBox("Warning", "sap-icon://warning", sap.ui.core.ValueState.Warning, sMessage, sdetails, fnCallback); | |
| }, | |
| alert: function (sMessage, sdetails, fnCallback = function () {}) { | |
| standardMessageBox("Information", "sap-icon://alert", sap.ui.core.ValueState.Warning, sMessage, sdetails, fnCallback); | |
| }, | |
| show: function ( | |
| sMessage, | |
| sdetails, | |
| fnCallback = function () {}, | |
| title = "", | |
| icon = "sap-icon://information", | |
| state = sap.ui.core.ValueState.Information | |
| ) { | |
| standardMessageBox(title, icon, state, sMessage, sdetails, fnCallback); | |
| }, | |
| }, | |
| }; | |
| } | |
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| sap.ui.define([], function () { | |
| return { | |
| /** | |
| * Represents the criteria for ordering a collection of items. | |
| * | |
| * @typedef {object} orderBy | |
| * @property {string} field - The name of the property (field) by which the items will be sorted. This string corresponds to a key present in the objects being sorted. | |
| * @property {'asc'|'desc'} direction - The direction of the sorting operation. | |
| * - `'asc'` indicates ascending order, where items with smaller values in the specified `field` appear earlier in the sorted collection. | |
| * - `'desc'` indicates descending order, where items with larger values in the specified `field` appear earlier in the sorted collection. | |
| */ | |
| /** | |
| * Reads all entries from a specified entity set of an OData V4 model. | |
| * Displays a busy indicator during the data retrieval process. | |
| * | |
| * @param {sap.ui.model.odata.v4.ODataModel} oModel - The OData V4 model instance to read data from. | |
| * @param {string} entitySet - The name of the entity set within the OData service from which data will be fetched. | |
| * @param {string[]} [selectFields] - An optional array of strings specifying the properties to be retrieved in the OData query. If not provided, all properties will be selected. | |
| * @param {Array.<orderBy>} [aOrderBy] - An optional array of objects defining the sorting order of the retrieved data. Each object in the array should conform to the {@link orderBy} structure. | |
| * @param {sap.ui.model.Filter|sap.ui.model.Filter[]} [aFilter] - An optional single `sap.ui.model.Filter` object or an array of `sap.ui.model.Filter` objects to apply to the OData query for filtering the results. | |
| * @returns {Promise<object[]>} A Promise that resolves with an array of JavaScript objects, where each object represents a data entry fetched from the OData service. The structure of these objects corresponds to the properties selected. | |
| * @throws {Error} If the provided `oModel` is not an instance of `sap.ui.model.odata.v4.ODataModel` or if the `entitySet` is not a non-empty string. | |
| */ | |
| v4ReadAllListItems: async function (oModel, entitySet, selectFields, aOrderBy, aFilter) { | |
| // Show busy indicator with a custom message | |
| sap.ui.core.BusyIndicator.show(0, { text: "A moment please, Fetching all data..." }); | |
| const sOrderBy = aOrderBy | |
| ?.map((config) => { | |
| if (!config?.field || !config.direction) { | |
| throw new Error(`A fied with its direction must be provided for ordering the data.`); | |
| } | |
| return `${config.field} ${config.direction}`; | |
| }) | |
| .join(","); | |
| return new Promise((resolve, reject) => { | |
| try { | |
| // Validate input parameters | |
| if (!oModel) { | |
| throw new Error("odata v4 Model is required."); | |
| } | |
| if (!entitySet) { | |
| throw new Error("Model Entity is required."); | |
| } | |
| // Construct the entity link URL | |
| const entityLink = `/${entitySet}`; | |
| const params = { | |
| $count: true, | |
| $select: selectFields ?? undefined, | |
| $orderby: sOrderBy, | |
| }; | |
| const mParams = Object.entries(params).reduce((acc, [key, value]) => { | |
| if (value !== null && value !== undefined) { | |
| acc[key] = value; | |
| } | |
| return acc; | |
| }, {}); | |
| // Bind the list and request count | |
| const dataListBindingForCount = oModel.bindList(entityLink, null, null, aFilter ?? [], mParams); | |
| // Request contexts and handle the promise chain | |
| dataListBindingForCount | |
| .requestContexts(0, 1) | |
| .then(() => dataListBindingForCount.getHeaderContext().requestProperty("$count")) | |
| .then((count) => dataListBindingForCount.requestContexts(0, count)) | |
| .then((_allContexts) => { | |
| // Hide busy indicator and resolve the promise with the fetched data | |
| sap.ui.core.BusyIndicator.hide(); | |
| resolve(_allContexts); | |
| }) | |
| .catch((err) => { | |
| // Hide busy indicator and reject the promise in case of an error | |
| sap.ui.core.BusyIndicator.hide(); | |
| reject(err); | |
| }); | |
| } catch (err) { | |
| // Hide busy indicator and reject the promise in case of an error | |
| sap.ui.core.BusyIndicator.hide(); | |
| reject(err); | |
| } | |
| }); | |
| }, | |
| }; | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * A callback type for Array.prototype.filter. | |
| * | |
| * @callback FilterCallback | |
| * @param {*} element - The current element being processed in the array. | |
| * @param {number} index - The index of the current element in the array. | |
| * @param {Array} array - The array filter was called on. | |
| * @returns {boolean} - Whether the element should be included in the new array. | |
| */ | |
| sap.ui.define( | |
| [ | |
| "sap/ui/core/Fragment", | |
| "sap/ui/model/Filter", | |
| "sap/ui/model/FilterOperator", | |
| "./connectivity", | |
| "./commonElements", | |
| ], | |
| function (Fragment, Filter, FilterOperator, Connectivity, CommonElements) { | |
| "use-strict"; | |
| /**the view dialog id */ | |
| const sId = "ValueHelpTableWithSearch-dialog"; | |
| /** Path where the filtered data is stored on a search/live change */ | |
| const sValueHelpTableDisplayDataModel = "valueHelpTableDispData"; | |
| /**orginal data with preprocess searchable string */ | |
| const cached = []; | |
| /**separator for the search metadata */ | |
| const sSeperator = "⣷"; // Unique separator character | |
| /**The Dialog config. */ | |
| const config = { | |
| /**If the current select table allows multiselection */ | |
| isMultiSelect: false, | |
| /**The entityset from which the data has to be fetched from the model */ | |
| entityset: null, | |
| /**How the data has to be ordered in the table */ | |
| orderBy: [], | |
| /**The column config for the table select, that has `show` flag set to `true` */ | |
| columns: [], | |
| /**The oModel from which the data has to be fetched from. */ | |
| dataModel: null, | |
| /**The parent controller instance */ | |
| parent: null, | |
| /**Tile of the select dialog. */ | |
| title: "Select Data", | |
| /**The current dialog */ | |
| dialog: undefined, | |
| /**@type {Array.<sap.ui.model.Filter>} Filter list of type `sap.ui.model.Filter` which will be added `Items` aggregation.*/ | |
| filters: [], | |
| }; | |
| return { | |
| _tagEventListners: function (oDialog, _parent) { | |
| if (!oDialog) { | |
| throw new Error( | |
| "The dialog fragment instance is required to attach the listeners. Please ensure you are calling this function with a valid dialog instance. If you are calling this function outside of its intended context, please refrain from doing so." | |
| ); | |
| } | |
| // attach cancel handler | |
| oDialog.attachCancel({}, (oEvent) => this.onCancel(oEvent)); | |
| // attach confirm/select handler | |
| oDialog.attachConfirm({}, (oEvent) => this.onOk(oEvent, oDialog)); | |
| // attach search live change handler | |
| oDialog.attachLiveChange({}, (oEvent) => this.handleLiveChange(oEvent)); | |
| // attach when search is clicked | |
| oDialog.attachSearch({}, (oEvent) => this.onSearch(oEvent)); | |
| }, | |
| /** | |
| * Adds columns to the value help dialog dynamically. | |
| * @param {sap.ui.core.Control | sap.ui.core.Control} oDialog - The fragment dialog after loaded. | |
| */ | |
| _addColumns: function (oDialog) { | |
| const cells = []; | |
| oDialog.removeAllColumns(); | |
| config.columns.forEach(function (column) { | |
| if (!column.field) { | |
| throw new Error("Please provide correct column config, found empty"); | |
| } | |
| // add column. | |
| oDialog.addColumn( | |
| new sap.m.Column({ | |
| header: new sap.m.Label({ text: column?.label || column.field }), | |
| }) | |
| ); | |
| // add to cell | |
| cells.push(new sap.m.Text({ text: `{${sValueHelpTableDisplayDataModel}>${column.field}}` })); | |
| }); | |
| // template for the items aggregation | |
| const oTemplate = new sap.m.ColumnListItem({ | |
| cells: cells, | |
| selected: "{selected}", | |
| }); | |
| //Items aggregation binding. | |
| oDialog.bindAggregation("items", { | |
| path: `${sValueHelpTableDisplayDataModel}>/data`, | |
| filters: config?.filters ?? [], | |
| template: oTemplate, | |
| }); | |
| }, | |
| /** | |
| * Asynchronously reads data from a specified OData V4 entity set. | |
| * Before fetching, it clears a local cache (`cached`) to prevent data duplication, | |
| * | |
| * @async | |
| * @param {sap.ui.model.odata.v4.ODataModel} oModel - The OData V4 model instance to read data from. | |
| * @param {string} sEntitySet - The name of the entity set within the OData service to fetch data from. | |
| * @param {Array<{ field: string }>} aColumns - An array of column definition objects. Each object must have a `field` property, which specifies the field to be selected from the OData entity and used for generating searchable content. | |
| * @param {object} [oOrderBy] - An optional object specifying the sorting criteria. The structure of this object is typically `{ path: string, descending: boolean }` or similar, as expected by OData V4 read operations. | |
| * @param {FilterCallback} [filterFn] - A filter callback funciton to proccess data after being requested from list binding. | |
| * @returns {Promise<object[]>} A Promise that resolves with an array of plain JavaScript objects representing the fetched data. Each object contains the properties specified in `aColumns`. Returns an empty array if no data is retrieved. | |
| * @throws {Error} If any error occurs during the data fetching process. | |
| */ _loadData: async function (oModel, sEntitySet, aColumns, oOrderBy, filterFn) { | |
| try { | |
| /// the current dialog not being destroyed can cause duplication of data, hence adding a clear cache. | |
| if (cached.length > 0) { | |
| cached.splice(0, cached?.length ?? null); | |
| } | |
| const aFields = aColumns.map((column) => column.field); | |
| const aData = await Connectivity.v4ReadAllListItems(oModel, sEntitySet, aFields, oOrderBy, config?.filters); | |
| if (!aData || aData.length === 0) { | |
| return []; | |
| } | |
| const data = aData.map((context) => context.getObject()).filter((v, i, a) => (filterFn ? filterFn(v, i, a) : true)); | |
| // create the search data | |
| cached.push( | |
| ...data.map((item) => { | |
| return { ...item, search: aColumns.map((column) => String(item[column.field]).toLowerCase()).join(sSeperator) }; | |
| }) | |
| ); | |
| return data; | |
| } catch (error) { | |
| throw error; | |
| } | |
| }, | |
| /** | |
| * Update the model data with the serach result for the input search string. | |
| * @param {String} sSearchString - The value to be searched. | |
| */ | |
| _updateOnSearch: function (sSearchString) { | |
| try { | |
| const valueHelpModel = config.parent.getView().getModel(sValueHelpTableDisplayDataModel); | |
| if (sSearchString !== "") { | |
| const similarFields = cached.filter((data) => data.search.includes(sSearchString.toLowerCase())); | |
| // update the model | |
| valueHelpModel.setData({ data: similarFields }); | |
| } else { | |
| valueHelpModel.setData({ data: cached }); | |
| } | |
| } catch (error) { | |
| CommonElements.messageBox.error("Unable to search the requested value", error, () => { | |
| config.dialog.fireCancel(); | |
| }); | |
| } | |
| }, | |
| /** | |
| * Handles the live change event in the search field. | |
| * @param {sap.ui.base.Event} oEvent - The live change event object. | |
| */ | |
| handleLiveChange: function (oEvent) { | |
| try { | |
| const sQuery = oEvent.getParameter("value"); | |
| // Debounce mechanism | |
| if (this._debounceTimer) { | |
| clearTimeout(this._debounceTimer); | |
| } | |
| // 300ms debounce time | |
| this._debounceTimer = setTimeout( | |
| function () { | |
| // Reload data with search query | |
| this._updateOnSearch(sQuery); | |
| }.bind(this), | |
| 300 | |
| ); | |
| } catch (error) { | |
| CommonElements.messageBox.error("Unable to search the requested value", error, () => { | |
| config.dialog.fireCancel(); | |
| }); | |
| } | |
| }, | |
| /** | |
| * Handles the search event. | |
| * @param {sap.ui.base.Event} oEvent - The search event object. | |
| */ | |
| onSearch: function (oEvent) { | |
| try { | |
| const sQuery = oEvent.getParameter("value"); | |
| if (!sQuery) { | |
| CommonElements.toast("Please enter a search term to perform a search."); | |
| return; | |
| } | |
| this._updateOnSearch(sQuery); | |
| } catch (error) { | |
| CommonElements.messageBox.error("Unable to search the requested value", error, () => { | |
| config.dialog.fireCancel(); | |
| }); | |
| } | |
| }, | |
| /** | |
| * Handles the cancel event. | |
| * @param {sap.ui.base.Event} oEvent - The cancel event object. | |
| */ | |
| onCancel: function (oEvent) { | |
| // remove search metadata | |
| cached.splice(0, cached?.length ?? null); | |
| // destroy its existence 😈 | |
| config.dialog.destroy(); | |
| // remove config | |
| Object.keys(config).forEach((key) => delete config[key]); | |
| }, | |
| /** | |
| * Handles the OK button press event. | |
| * @param {sap.ui.base.Event} oEvent - The OK button press event object. | |
| */ | |
| onOk: function (oEvent, oDialog) { | |
| const selectedItems = oEvent.getParameter("selectedItems"); | |
| // map, as per the aColumn config, we are ignoring the multiselect. | |
| const keys = config.columns.map((column) => column?.field).filter((field) => Boolean(field)); | |
| const result = selectedItems.map((item) => { | |
| const cells = item?.getCells(); | |
| if (cells?.length === keys.length) { | |
| return cells.reduce((acc, cell, index) => { | |
| acc[keys[index]] = cell.getText(); | |
| return acc; | |
| }, {}); | |
| } else { | |
| oDialog.destroy(); | |
| this._reject("The column and cells count dont match unable to map the data."); | |
| } | |
| }); | |
| oDialog.destroy(); | |
| this._resolve(result); | |
| }, | |
| /** | |
| * Opens the value help dialog. | |
| * @param {Object} parentViewController - The parent view controller. | |
| * @param {Object} oModel - The data model. | |
| * @param {string} sEntitySet - The entity set. | |
| * @param {string} sTitle - The title of the dialog. | |
| * @param {Object} oConfig - The additional config for the dialog | |
| * @param {Array.<{field:String,label:String, show:Boolean}} oConfig.aColumns - Array of columns. | |
| * @param {string} oConfig.sOrderBy - Order by fields. | |
| * @param {boolean} oConfig.bMultiSelect - Flag to enable multi select. | |
| * @param {Array.<sap.ui.model.Filter>} oConfig.aFilter - Array of filters that needs to be passed. The field must be part of {@link aColumns} array. | |
| * @param {FilterCallback} oConfig.postFilterFn - A function that will be used to again filter data after being loaded from the entity. | |
| * @returns {Promise} - Resolves with the selected data. | |
| */ | |
| openValueHelpDialog: function (parentViewController, oModel, sEntitySet, sTitle, oConfig) { | |
| let _pDialog; | |
| config.isMultiSelect = oConfig.bMultiSelect; | |
| config.title = sTitle; | |
| config.parent = parentViewController; | |
| config.dataModel = oModel; | |
| config.columns = oConfig.aColumns.filter((column) => column?.show); | |
| config.entityset = sEntitySet; | |
| config.orderBy = oConfig.sOrderBy; | |
| config.filters = oConfig.aFilter; | |
| // Promisified open method | |
| return new Promise( | |
| function (resolve, reject) { | |
| this._resolve = resolve; | |
| this._reject = reject; | |
| // start busy Indicator: | |
| sap.ui.core.BusyIndicator.show(0); | |
| // check for null values | |
| if (!oModel) { | |
| throw new Error("oModel is necessary for using the value help."); | |
| } | |
| if (!sEntitySet) { | |
| throw new Error("Entity is necessary for using the value help."); | |
| } | |
| // create dialog. | |
| if (!_pDialog) { | |
| _pDialog = Fragment.load({ | |
| id: sId, | |
| name: "gateentry.view.fragment.ValueHelpTableWithSearch", | |
| containingView: "gateentry.view.fragment.ValueHelpTableWithSearch", | |
| }).then( | |
| function (oDialog) { | |
| // dialog settings | |
| oDialog.setMultiSelect(config.isMultiSelect); | |
| oDialog.setTitle(config.title); | |
| oDialog.setRememberSelections(true); | |
| // add the dialog to config | |
| config.dialog = oDialog; | |
| // loading data into existing json model. | |
| this._loadData(oModel, sEntitySet, oConfig.aColumns, config.orderBy, oConfig.postFilterFn) | |
| .then((data) => { | |
| const oModelForValueHelp = new sap.ui.model.json.JSONModel(); | |
| // set the default oModel for | |
| oModelForValueHelp.setData({ data: data }); | |
| config.parent.getView().setModel(oModelForValueHelp, sValueHelpTableDisplayDataModel); | |
| // update oDialoag config | |
| if (data.length >= 20) { | |
| oDialog.setGrowing(true); | |
| oDialog.setGrowingThreshold(20); | |
| } | |
| // Add columns dynamically | |
| this._addColumns(oDialog, oConfig.aColumns); | |
| // attach listners | |
| this._tagEventListners(oDialog, config.parent); | |
| // add the current fragmane to the view. | |
| config.parent.getView().addDependent(oDialog); | |
| oDialog.open(); | |
| sap.ui.core.BusyIndicator.hide(); | |
| }) | |
| .catch((err) => { | |
| console.error(err); | |
| CommonElements.messageBox.error("Unable to load data for value help.", err, () => { | |
| config.dialog.fireCancel(); | |
| }); | |
| sap.ui.core.BusyIndicator.hide(); | |
| }); | |
| return oDialog; | |
| }.bind(this) | |
| ); | |
| } else { | |
| _pDialog.open(); | |
| sap.ui.core.BusyIndicator.hide(); | |
| } | |
| }.bind(this) | |
| ); | |
| }, | |
| }; | |
| } | |
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <core:FragmentDefinition | |
| xmlns="sap.m" | |
| xmlns:core="sap.ui.core" | |
| > | |
| <TableSelectDialog | |
| id="ValueHelpTableWithSearch-dialog" | |
| noDataText="No items found. You also may close and reopen the dialog to refresh the data." | |
| showClearButton="true" | |
| class="sapUiResponsivePadding--header sapUiResponsivePadding--subHeader sapUiResponsivePadding--content sapUiResponsivePadding--footer" | |
| busyIndicatorSize="Auto" | |
| titleAlignment="Start" | |
| resizable="true" | |
| draggable="true" | |
| confirmButtonText="Select" | |
| > | |
| <columns> | |
| <!-- Columns will be dynamically added here --> | |
| </columns> | |
| </TableSelectDialog> | |
| </core:FragmentDefinition> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment