Created
February 24, 2025 20:45
-
-
Save marianfoo/ab3720c444076721d20232096f57fbb9 to your computer and use it in GitHub Desktop.
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
| ├── .github | |
| └── livedemodown-template.md | |
| ├── README.md | |
| ├── docs | |
| ├── index.md | |
| └── pages | |
| │ ├── APIReference.md | |
| │ ├── Button.md | |
| │ ├── CHANGELOGSPREADSHEETIMPORTER.md | |
| │ ├── CentralDeployment.md | |
| │ ├── Checks.md | |
| │ ├── Configuration.md | |
| │ ├── Development | |
| │ ├── Docs.md | |
| │ ├── GettingStarted.md | |
| │ ├── GitHubActions.md | |
| │ ├── SampleApps.md | |
| │ ├── SpreadsheetDownload.md | |
| │ ├── Update.md | |
| │ ├── opa5.md | |
| │ └── wdi5.md | |
| │ ├── Events.md | |
| │ ├── GettingStarted.md | |
| │ ├── HowItWorks.md | |
| │ ├── Pro | |
| │ ├── deepcreate.md | |
| │ └── install.md | |
| │ ├── SupportVersions.md | |
| │ ├── TableSelector.md | |
| │ ├── Troubleshooting.md | |
| │ ├── Typescript.md | |
| │ ├── Update.md | |
| │ ├── UseCases.md | |
| │ ├── UserDocumentation.md | |
| │ └── spreadsheetdownload.md | |
| ├── examples | |
| ├── README.md | |
| └── packages | |
| │ ├── anyupload | |
| │ ├── README.md | |
| │ └── webapp | |
| │ │ ├── Component.ts | |
| │ │ └── ext | |
| │ │ └── main | |
| │ │ └── Main.controller.ts | |
| │ ├── ordersv2fe | |
| │ └── README.md | |
| │ ├── ordersv2fenondraft | |
| │ └── README.md | |
| │ ├── ordersv2freestylenondraft | |
| │ └── README.md | |
| │ ├── ordersv2freestylenondraft2 | |
| │ └── README.md | |
| │ ├── ordersv2freestylenondraftopenui5 | |
| │ └── README.md | |
| │ ├── ordersv4fe | |
| │ └── README.md | |
| │ ├── ordersv4fets | |
| │ ├── README.md | |
| │ └── webapp | |
| │ │ ├── Component.ts | |
| │ │ └── ext | |
| │ │ ├── ListReportExtController.ts | |
| │ │ └── ObjectPageExtController.ts | |
| │ ├── ordersv4fpm | |
| │ └── README.md | |
| │ └── ordersv4freestyle | |
| │ └── README.md | |
| └── packages | |
| └── ui5-cc-spreadsheetimporter | |
| ├── CHANGELOG.md | |
| ├── README.md | |
| └── src | |
| ├── Component.gen.d.ts | |
| ├── Component.ts | |
| ├── control | |
| ├── SpreadsheetDialog.gen.d.ts | |
| ├── SpreadsheetDialog.ts | |
| └── SpreadsheetDialogRenderer.ts | |
| ├── controller | |
| ├── Logger.ts | |
| ├── MessageHandler.ts | |
| ├── Parser.ts | |
| ├── Preview.ts | |
| ├── SheetHandler.ts | |
| ├── SpreadsheetUpload.ts | |
| ├── TableSelector.ts | |
| ├── Util.ts | |
| ├── dialog | |
| │ ├── ODataMessageHandler.ts | |
| │ ├── OptionsDialog.ts | |
| │ └── SpreadsheetUploadDialog.ts | |
| ├── download | |
| │ ├── DataAssigner.ts | |
| │ ├── SpreadsheetDownload.ts | |
| │ ├── SpreadsheetDownloadDialog.ts | |
| │ └── SpreadsheetGenerator.ts | |
| └── odata | |
| │ ├── MetadataHandler.ts | |
| │ ├── MetadataHandlerV2.ts | |
| │ ├── MetadataHandlerV4.ts | |
| │ ├── OData.ts | |
| │ ├── ODataV2.ts | |
| │ ├── ODataV4.ts | |
| │ └── ODataV4RequestObjects.ts | |
| ├── enums.ts | |
| └── types.d.ts | |
| /.github/livedemodown-template.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | **Describe the issue:** | |
| 2 | The website (https://livedemo.spreadsheet-importer.com) is down! | |
| 3 | | |
| 4 | **Current Status:** | |
| 5 | * HTTP Status Code: [HTTP_STATUS_CODE] | |
| 6 | * Checked at: [TIMESTAMP] | |
| -------------------------------------------------------------------------------- | |
| /README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # UI5 `ui5-cc-spreadsheetimporter` | |
| 2 | | |
| 3 | This monorepo houses the UI5 Component `ui5-cc-spreadsheetimporter`. | |
| 4 | | |
| 5 | `ui5-cc-spreadsheetimporter` is a UI5 Component designed for the integration of Spreadsheet Upload functionality into Fiori Elements and other UI5 Apps. | |
| 6 | It enables the bulk upload of data, independent of the backend, OData version, and Fiori scenario, by extracting data from an Spreadsheet file and leveraging standard APIs. | |
| 7 | Rather than submitting the file, the control only submits the extracted data. | |
| 8 | The control's integration aims for simplicity, requiring minimal to no configuration in the ideal scenario. | |
| 9 | | |
| 10 | Our goal is to support as many Fiori Scenarios and UI5 Versions as possible. You can see the currently [supported versions here](https://docs.spreadsheet-importer.com/pages/SupportVersions/). | |
| 11 | | |
| 12 |  | |
| 13 | | |
| 14 | ## Getting Started | |
| 15 | | |
| 16 | For documentation, please visit: | |
| 17 | | |
| 18 | https://docs.spreadsheet-importer.com/ | |
| 19 | | |
| 20 | ## Live Demo | |
| 21 | | |
| 22 | Give this a try directly at: | |
| 23 | https://livedemo.spreadsheet-importer.com/ | |
| 24 | | |
| 25 | The demo app uses OData V4 and UI5 version 1.120 with a CAP backend. The data resets every hour on the hour. | |
| 26 | | |
| 27 | # **Support** | |
| 28 | | |
| 29 | If you encounter implementation issues or bugs, you can open an issue [here](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/issues/new/choose). | |
| 30 | | |
| 31 | # Development | |
| 32 | | |
| 33 | The development documentation can be found here: | |
| 34 | | |
| 35 | https://docs.spreadsheet-importer.com/pages/Development/GettingStarted/ | |
| 36 | | |
| 37 | ## Quickstart | |
| 38 | | |
| 39 | 1. Clone the repository `git clone https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter` | |
| 40 | 2. Run `pnpm install` | |
| 41 | 3. Run server with `pnpm start:server` | |
| 42 | 4. Start Demo App for example a Fiori Elements App with OData V4 and UI5 1.120 with `pnpm --filter ordersv4fe120 start` | |
| 43 | | |
| 44 | # Changelogs | |
| 45 | | |
| 46 | ## Changelog `ui5-cc-spreadsheetimporter` | |
| 47 | | |
| 48 | See the [CHANGELOG.md](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/packages/ui5-cc-spreadsheetimporter/CHANGELOG.md) | |
| 49 | | |
| 50 | # Open in GitHub Codespaces | |
| 51 | | |
| 52 | The postCreateCommand will automatically install all dependencies, which might take a few minutes. | |
| 53 | | |
| 54 | [](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=569313224&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope) | |
| 55 | | |
| -------------------------------------------------------------------------------- | |
| /docs/index.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # UI5 Spreadsheet Upload Component | |
| 2 | | |
| 3 | This component provides functionality for the bulk upload of data and the quick creation of records, with support for standard identification. Its use is independent of the backend, OData version, and Fiori scenario. Importing data is achieved by reading Spreadsheet files and utilizing standard digital APIs. The component does not submit the file itself but instead submits the data extracted from the Spreadsheet files. Its integration is designed to be as simple as possible, ideally requiring no configuration. | |
| 4 | | |
| 5 | [**Get Started**](./pages/GettingStarted.md){: .md-button .md-button--primary .sap-icon-initiative } | |
| 6 | [**Live Demo**](https://livedemo.spreadsheet-importer.com/){: .md-button .md-button--secondary .sap-icon-initiative } | |
| 7 | | |
| 8 | For information about the currently supported OData and UI5 Versions, click [here](./pages/SupportVersions.md). | |
| 9 | A quick integration tutorial for this component is available on YouTube [here](https://www.youtube.com/watch?v=dODt9ZWmi4A). | |
| 10 | | |
| 11 | ## Feature Overview | |
| 12 | | |
| 13 | - Supports upload from List Report/Object Page in Fiori Elements with or without draft | |
| 14 | - Usable across all Fiori scenarios (Fiori Elements, Freestyle, OpenUI5, V2/V4) | |
| 15 | - Includes several frontend checks | |
| 16 | - Capability to download a pre-generated Spreadsheet template | |
| 17 | - Extension Points for uploading to App or sending to the backend | |
| 18 | - Supports multiversion namespace in Fiori Launchpad | |
| 19 | - Provides multilanguage support (DE, EN, ES, FR, HI, IT, JA, ZH) | |
| 20 | - Option to send to Backend in batch or single requests (batch size configurable) | |
| 21 | - Standalone Mode (upload to app without sending to backend) | |
| 22 | - Functionality to preview uploaded data | |
| 23 | - Automatic draft activation | |
| 24 | - Button control for simplified integration | |
| 25 | | |
| 26 | ## **Support** | |
| 27 | | |
| 28 | For discussions about the suitability of the component for your use case, you can schedule an appointment [here](https://outlook.office365.com/owa/calendar/[email protected]/bookings/) free of charge. | |
| 29 | | |
| 30 | If you encounter implementation issues or bugs, you can open an issue [here](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/issues/new/choose). | |
| 31 | For urgent assistance or special requirements, please schedule an appointment [here](https://outlook.office365.com/owa/calendar/[email protected]/bookings/) at a fixed rate. | |
| 32 | | |
| 33 | ## Live Demo | |
| 34 | | |
| 35 | You can try this component live at: | |
| 36 | <https://livedemo.spreadsheet-importer.com/> | |
| 37 | | |
| 38 | The demo app uses OData V4, UI5 version 1.108, and a CAP backend. | |
| 39 | Data is reset every hour on the hour. | |
| 40 | | |
| 41 | ## Blogs | |
| 42 | | |
| 43 | Find a selection of blog posts about this control: | |
| 44 | | |
| 45 | - [Simplifying Spreadsheet Upload in Fiori Elements: The Open Source and Easy-to-Use UI5 Custom Control](https://blogs.sap.com/2023/02/17/simplifying-excel-upload-in-fiori-elements-the-open-source-and-easy-to-use-ui5-custom-control/) | |
| 46 | - [Create a UI5 Custom Library with Versioning Using a Multi-Version Namespace](https://blogs.sap.com/2023/03/12/create-a-ui5-custom-library-with-versioning-using-a-multi-version-namespace/) | |
| 47 | - [Automating UI5 Testing with GitHub Actions and wdi5 in Multiple Scenarios](https://blogs.sap.com/2023/04/05/automating-ui5-testing-with-github-actions-and-wdi5-in-multiple-scenarios/) | |
| 48 | - [Load Data from a Spreadsheet File in UI5 and Display the Data in a Table with this Open Source Component](https://blogs.sap.com/2023/04/13/load-data-from-an-excel-file-in-ui5-and-display-the-data-in-a-table-with-this-open-source-component/) | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/APIReference.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | | |
| 2 | | |
| 3 | ## class spreadsheetimporter Component | |
| 4 | | |
| 5 | ### Overview | |
| 6 | | |
| 7 | The `spreadsheetimporter` component provides a way to import data from a spreadsheet into a table in the UI Builder application. | |
| 8 | | |
| 9 | ### Constructor | |
| 10 | | |
| 11 | ```javascript | |
| 12 | createComponent({ | |
| 13 | usage: "spreadsheetImporter", | |
| 14 | async: true, | |
| 15 | componentData: { | |
| 16 | context: this, | |
| 17 | tableId?, | |
| 18 | ...OTHER_OPTIONS | |
| 19 | }, | |
| 20 | }); | |
| 21 | ``` | |
| 22 | | |
| 23 | ### Properties | |
| 24 | | |
| 25 | more info see [Configuration](./Configuration.md) | |
| 26 | | |
| 27 | ### Events | |
| 28 | | |
| 29 | more info see [Events](./Events.md) | |
| 30 | | |
| 31 | | Event | Description | | |
| 32 | | -------------------- | -------------------------------------------------------------------------------------------------- | | |
| 33 | | `preFileProcessing` | Execute custom logic before processing the spreadsheet file starts | | |
| 34 | | `checkBeforeRead` | Check data before it is uploaded to the UI5 | | |
| 35 | | `changeBeforeCreate` | Change data before it is sent to the backend | | |
| 36 | | `requestCompleted` | Event when the request is completed | | |
| 37 | | `uploadButtonPress` | Fired when the `Upload` button is pressed, possible to prevent data from being sent to the backend | | |
| 38 | | |
| 39 | ### Methods | |
| 40 | | |
| 41 | #### openSpreadsheetUploadDialog | |
| 42 | | |
| 43 | Opens the spreadsheet upload dialog. | |
| 44 | | |
| 45 | ##### Usage | |
| 46 | | |
| 47 | You can use this method to open the spreadsheet upload dialog after creating the component. | |
| 48 | Optionally, you can pass the `options` object to customize the dialog. | |
| 49 | | |
| 50 | ##### Sample Code | |
| 51 | | |
| 52 | ```javascript | |
| 53 | const options = { | |
| 54 | context: this, | |
| 55 | tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Shipping::LineItem-innerTable" | |
| 56 | } | |
| 57 | this.spreadsheetUploadTableShipping = await this.editFlow.getView() | |
| 58 | .getController() | |
| 59 | .getAppComponent() | |
| 60 | .createComponent({ | |
| 61 | usage: "spreadsheetImporter", | |
| 62 | async: true | |
| 63 | }); | |
| 64 | this.spreadsheetUploadTableShipping.setBatchSize(500) | |
| 65 | this.spreadsheetUploadTableShipping.openSpreadsheetUploadDialog(options); | |
| 66 | ``` | |
| 67 | | |
| 68 | #### addArrayToMessages | |
| 69 | | |
| 70 | Adds an array of messages inside a event. | |
| 71 | | |
| 72 | ##### Usage | |
| 73 | | |
| 74 | You can use this method to add an array of messages to the event. The messages will be displayed in the error dialog after the execution of the event. | |
| 75 | | |
| 76 | ##### Sample Code | |
| 77 | | |
| 78 | ```javascript | |
| 79 | this.spreadsheetUpload.attachCheckBeforeRead(function(event) { | |
| 80 | // example | |
| 81 | const sheetdata = event.getParameter("sheetData"); | |
| 82 | let errorArray = []; | |
| 83 | for (const [index, row] of sheetData.entries()) { | |
| 84 | // Check for invalid price | |
| 85 | for (const key in row) { | |
| 86 | if (key.endsWith("[price]") && row[key].rawValue > 100) { | |
| 87 | const error = { | |
| 88 | title: "Price too high (max 100)", | |
| 89 | row: index + 2, | |
| 90 | group: true, | |
| 91 | rawValue: row[key].rawValue, | |
| 92 | ui5type: "Error" | |
| 93 | }; | |
| 94 | errorArray.push(error); | |
| 95 | } | |
| 96 | } | |
| 97 | } | |
| 98 | event.getSource().addArrayToMessages(errorArray); | |
| 99 | }, this); | |
| 100 | ``` | |
| 101 | | |
| 102 | #### getMessages | |
| 103 | | |
| 104 | Returns the messages array. | |
| 105 | | |
| 106 | ##### Usage | |
| 107 | | |
| 108 | You can use this method to get the messages array from the message handler. | |
| 109 | | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Button.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Button in Component | |
| 2 | | |
| 3 | The usage of the [UIComponent](https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.core.UIComponent) enables the possibility to return a button with the usage of a [ComponentContainer](https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.core.ComponentContainer). | |
| 4 | This has the big advantage that no separate dependency has to be installed, and a button for spreadsheet upload can be integrated very easily. | |
| 5 | | |
| 6 | You can also use the button in **Fiori Elements** applications within a section of an object page (see [Including Reuse Components on an Object Page](#including-reuse-components-on-an-object-page)). | |
| 7 | | |
| 8 | ### Requirements | |
| 9 | | |
| 10 | - Node.js Version v16.18.0, v18.12.0, or higher | |
| 11 | - npm Version v8.0.0 or higher | |
| 12 | - UI5 CLI v3.0.0 or higher | |
| 13 | | |
| 14 | ### Getting started | |
| 15 | | |
| 16 | 1\. Install from npm | |
| 17 | | |
| 18 | ```sh | |
| 19 | npm install ui5-cc-spreadsheetimporter | |
| 20 | ``` | |
| 21 | | |
| 22 | 2\. Add `resourceRoots` to your `manifest.json` under `sap.ui5` | |
| 23 | | |
| 24 | ⚠️ You must always keep your `ui5-cc-spreadsheetimporter` and button version up to date here when updating the module. | |
| 25 | | |
| 26 | !!! warning "" | |
| 27 | ⚠️ The `resourceRoots` path "./thirdparty/customcontrol/spreadsheetimporter/v1_7_3" changed from version 0.34.0 to lowercase. Please make sure to use the correct path. | |
| 28 | | |
| 29 | | |
| 30 | ```json | |
| 31 | "resourceRoots": { | |
| 32 | "cc.spreadsheetimporter.v1_7_3": "./thirdparty/customcontrol/spreadsheetimporter/v1_7_3" | |
| 33 | } | |
| 34 | ``` | |
| 35 | | |
| 36 | 3\. Add `components` to your `manifest.json` under `sap.ui5.dependencies` | |
| 37 | | |
| 38 | This is optional and preloads the component on startup of the application. | |
| 39 | | |
| 40 | ⚠️ You must always keep your `ui5-cc-spreadsheetimporter` version up to date here when updating the module. | |
| 41 | | |
| 42 | ```json | |
| 43 | "dependencies": { | |
| 44 | "minUI5Version": "1.108.30", | |
| 45 | "libs": { | |
| 46 | "sap.m": {}, | |
| 47 | "sap.ui.core": {}, | |
| 48 | "sap.f": {}, | |
| 49 | "sap.ui.table": {} | |
| 50 | }, | |
| 51 | "components": { | |
| 52 | "cc.spreadsheetimporter.v1_7_3": {} | |
| 53 | } | |
| 54 | } | |
| 55 | ``` | |
| 56 | | |
| 57 | 4\. Add `componentUsages` to your `manifest.json` under `sap.ui5` | |
| 58 | | |
| 59 | ⚠️ You must always keep your `ui5-cc-spreadsheetimporter` version up to date here when updating the module. | |
| 60 | | |
| 61 | ```json | |
| 62 | "componentUsages": { | |
| 63 | "spreadsheetImporter": { | |
| 64 | "name": "cc.spreadsheetimporter.v1_7_3" | |
| 65 | } | |
| 66 | } | |
| 67 | ``` | |
| 68 | | |
| 69 | 5\. Add the namespace `core` to your XML View | |
| 70 | | |
| 71 | ```xml | |
| 72 | <mvc:View controllerName="ui.v2.ordersv2freestylenondraft.controller.UploadToTable" | |
| 73 | xmlns="sap.m" | |
| 74 | xmlns:semantic="sap.f.semantic" | |
| 75 | xmlns:mvc="sap.ui.core.mvc" | |
| 76 | xmlns:core="sap.ui.core"> | |
| 77 | ... | |
| 78 | </mvc:View> | |
| 79 | ``` | |
| 80 | | |
| 81 | 6\. Add the `core:ComponentContainer` control to your view. | |
| 82 | | |
| 83 | ```xml | |
| 84 | <core:ComponentContainer width="100%" | |
| 85 | usage="spreadsheetImporter" propagateModel="true" | |
| 86 | async="true"/> | |
| 87 | ``` | |
| 88 | | |
| 89 | ### Define Configuration Options | |
| 90 | | |
| 91 | You can set configuration options for the spreadsheet importer in the `settings` property of the `core:ComponentContainer` control. | |
| 92 | For special configuration options for the `ComponantContainer`, see [Configuration](Configuration.md#componentcontainerdata). | |
| 93 | | |
| 94 | ```xml | |
| 95 | <core:ComponentContainer width="100%" | |
| 96 | usage="spreadsheetImporter" propagateModel="true" async="true" | |
| 97 | settings="{ | |
| 98 | standalone:true, | |
| 99 | columns: ['product_ID', 'username'], | |
| 100 | componentContainerData:{ | |
| 101 | uploadButtonPress:'uploadButtonPress', | |
| 102 | buttonText:'Excel Upload' | |
| 103 | } | |
| 104 | }" /> | |
| 105 | ``` | |
| 106 | | |
| 107 | ### Example App | |
| 108 | | |
| 109 | #### Freestyle OData V2 | |
| 110 | | |
| 111 | XML View: [Detail.view.xml](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2freestylenondraft/webapp/view/Detail.view.xml) | |
| 112 | Controller: [Detail.controller.js](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2freestylenondraft/webapp/controller/Detail.controller.js) | |
| 113 | | |
| 114 | #### Freestyle OData V2 Standalone | |
| 115 | | |
| 116 | XML View: [UploadToTable.view.xml](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2freestylenondraft/webapp/view/UploadToTable.view.xml) | |
| 117 | Controller: [UploadToTable.controller.js](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2freestylenondraft/webapp/controller/UploadToTable.controller.js) | |
| 118 | | |
| 119 | ## Including Reuse Components on an Object Page | |
| 120 | | |
| 121 | You can also use the button in **Fiori Elements** applications within a section of an object page. | |
| 122 | You can define this in the `manifest.json` under `sap.ui.generic.app` in the `pages` property. | |
| 123 | | |
| 124 | The configuration is documented in the [UI5 documentation](https://sapui5.hana.ondemand.com/sdk/#/topic/d869d7ab3caa48b2a20dc20dfa248380). | |
| 125 | | |
| 126 | A sample configuration can be found in the manifest.json of the [OData V4 Fiori Elements app](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv4fe/webapp/manifest.json) | |
| 127 | | |
| 128 | ```json | |
| 129 | "body": { | |
| 130 | "sections": { | |
| 131 | "customSectionReuse": { | |
| 132 | "title": "Spreadsheet Upload", | |
| 133 | "embeddedComponent": { | |
| 134 | "name": "cc.spreadsheetimporter.v1_7_3", | |
| 135 | "settings": { | |
| 136 | "tableId": "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable" | |
| 137 | } | |
| 138 | } | |
| 139 | } | |
| 140 | } | |
| 141 | } | |
| 142 | ``` | |
| 143 | | |
| 144 | ### Screenshot | |
| 145 | | |
| 146 |  | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/CentralDeployment.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Central Deployment of UI5 Spreadsheet Importer | |
| 2 | | |
| 3 | Deploying the UI5 Spreadsheet Importer centrally is the recommended approach. Central deployment simplifies usage for individual developers, as the component becomes readily available across the system. | |
| 4 | | |
| 5 | | |
| 6 | ## Deployment to ABAP System | |
| 7 | | |
| 8 | You can deploy the UI5 Spreadsheet Importer to an ABAP system using one of the following methods: | |
| 9 | | |
| 10 | ### Method 1: Using UI5 Carrier App | |
| 11 | | |
| 12 | The **recommended method** is to use a UI5 carrier app. This app contains all required versions of the Spreadsheet Importer component. By deploying this carrier app, all versions become available in the system with a single deployment. The components are registered in the app index through the deployment. | |
| 13 | | |
| 14 | #### Steps: | |
| 15 | | |
| 16 | 1\. **Clone the Sample Carrier App Repository** | |
| 17 | | |
| 18 | A sample carrier app is available [here](https://github.com/spreadsheetimporter/packed-deployment-abap). Clone this repository to your local machine: | |
| 19 | | |
| 20 | ```sh | |
| 21 | git clone https://github.com/spreadsheetimporter/packed-deployment-abap | |
| 22 | ``` | |
| 23 | | |
| 24 | 2\. **Follow the Instructions** | |
| 25 | | |
| 26 | Navigate to the repository's `README.md` file and follow the detailed deployment instructions provided there. | |
| 27 | | |
| 28 | 3\. **Use the Component in Your Fiori App** | |
| 29 | | |
| 30 | After successful deployment, the Spreadsheet Importer component is available for use in your Fiori applications. | |
| 31 | | |
| 32 | #### S/4HANA Public Cloud Considerations | |
| 33 | | |
| 34 | For deployment to **S/4HANA Public Cloud**, you need to make the component available in the SAP Fiori Launchpad (FLP) and assign it to users. | |
| 35 | | |
| 36 | - The `crossNavigation` configuration is already defined in the [carrier app's manifest.json](https://github.com/spreadsheetimporter/packed-deployment-abap/blob/main/webapp/manifest.json#L12-L25). | |
| 37 | - Upon deployment, an FLP app descriptor is created. | |
| 38 | - You need to create or update an IAM app, add the component to a catalog, and assign it to a role, as explained in this [SAP tutorial](https://developers.sap.com/tutorials/abap-environment-shell-plugin.html#bb4645a0-87b0-4eba-ba11-dd9321a8f781). | |
| 39 | - **Important**: Ensure that users have the necessary role assigned; otherwise, they may encounter an error like `"Blocked by UCON"`. | |
| 40 | | |
| 41 | ### Method 2: Manual Deployment | |
| 42 | | |
| 43 | If you prefer manual deployment, follow these steps: | |
| 44 | | |
| 45 | 1\. **Clone the Repository** | |
| 46 | | |
| 47 | ```sh | |
| 48 | git clone https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter | |
| 49 | ``` | |
| 50 | | |
| 51 | 2\. **Install Dependencies** | |
| 52 | | |
| 53 | Navigate to the cloned directory and install the necessary dependencies: | |
| 54 | | |
| 55 | ```sh | |
| 56 | pnpm install | |
| 57 | ``` | |
| 58 | | |
| 59 | *or* | |
| 60 | | |
| 61 | ```sh | |
| 62 | npm install | |
| 63 | ``` | |
| 64 | | |
| 65 | 3\. **Configure `ui5-deploy.yaml`** | |
| 66 | | |
| 67 | Update the default [`ui5-deploy.yaml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/packages/ui5-cc-spreadsheetimporter/ui5-deploy.yaml) file with your deployment settings. | |
| 68 | | |
| 69 | - **Version Management**: When deploying a new version, use a unique app name to avoid overwriting existing versions. This is automatically handled by the variable `XXXnamespaceShortXXX` in the `ui5-deploy-publish.yaml` file. | |
| 70 | - **Custom App Name**: If you choose to use your own app name, ensure you change it for newer versions to prevent overwriting. | |
| 71 | | |
| 72 | 4\. **Set Up Environment Variables** | |
| 73 | | |
| 74 | - **For Deployment from VS Code**: Rename `.envTEMPLATE` to `.env` and enter your ABAP system username and password. | |
| 75 | - **For Deployment from SAP Business Application Studio (BAS)**: Remove the `credentials` section from the `ui5-deploy.yaml` file. | |
| 76 | | |
| 77 | 5\. **Run Deployment** | |
| 78 | | |
| 79 | Navigate to the package directory and execute the deployment script: | |
| 80 | | |
| 81 | ```sh | |
| 82 | cd packages/ui5-cc-spreadsheetimporter | |
| 83 | npm run deploy | |
| 84 | ``` | |
| 85 | | |
| 86 | ## Deployment to HTML5 Repository on BTP | |
| 87 | | |
| 88 | To deploy the UI5 Spreadsheet Importer to the HTML5 Repository on SAP BTP, follow these steps: | |
| 89 | | |
| 90 | 1\. **Clone the Repository** | |
| 91 | | |
| 92 | ```sh | |
| 93 | git clone https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter | |
| 94 | ``` | |
| 95 | | |
| 96 | 2\. **Install Dependencies** | |
| 97 | | |
| 98 | ```sh | |
| 99 | pnpm install | |
| 100 | ``` | |
| 101 | | |
| 102 | 3\. **Optional: Modify `mta.yaml`** | |
| 103 | | |
| 104 | The `mta.yaml` file is used for deployment to the HTML5 Repository on BTP. If you need to customize deployment settings, you can modify this file accordingly. | |
| 105 | | |
| 106 | 4\. **Build the MTA Archive** | |
| 107 | | |
| 108 | ```sh | |
| 109 | pnpm build:mta | |
| 110 | ``` | |
| 111 | | |
| 112 | 5\. **Deploy to Cloud Foundry** | |
| 113 | | |
| 114 | ```sh | |
| 115 | pnpm deploy:cf | |
| 116 | ``` | |
| 117 | | |
| 118 | ## Consuming the Component in a UI5 App | |
| 119 | | |
| 120 | After deploying the component centrally, you can consume it in your UI5 application without installing it via npm or modifying the `resourceRoots` in `manifest.json`. | |
| 121 | | |
| 122 | - Follow the same steps outlined in the [Getting Started](./../pages/GettingStarted.md) guide, omitting the npm installation and `resourceRoots` configuration. | |
| 123 | | |
| 124 | For consuming apps on SAP BTP, you can refer to this [sample app](https://github.com/spreadsheetimporter/sample-full-btp), which demonstrates deployment to the HTML5 Repository. | |
| 125 | | |
| 126 | ### Additional Resources | |
| 127 | | |
| 128 | For more detailed information and guidance, consider reading the following blog posts by [Wouter Lemaire](https://community.sap.com/members/wouter.lemaire): | |
| 129 | | |
| 130 | - [Connecting UI5 Components in BTP Cloud Foundry in the Same Space](https://blogs.sap.com/2023/11/09/connecting-ui5-components-in-btp-cloudfoundry-in-the-same-space/) | |
| 131 | - [Connecting UI5 Components in BTP Cloud Foundry Across Spaces](https://blogs.sap.com/2023/11/09/connecting-ui5-components-in-btp-cloudfoundry-across-spaces/) | |
| 132 | | |
| 133 | ## Running the App Locally | |
| 134 | | |
| 135 | When the component is deployed centrally, you can still run your app locally. This can be achieved by either installing the component as a development dependency or by consuming the centrally deployed component from the ABAP system. | |
| 136 | | |
| 137 | ### Consuming the Component as a Dev Dependency Locally | |
| 138 | | |
| 139 | Simulate the centrally deployed component in your local environment by following these steps: | |
| 140 | | |
| 141 | 1\. **Install `cds-plugin-ui5` (If Using CAP Projects)** | |
| 142 | | |
| 143 | ```sh | |
| 144 | npm install cds-plugin-ui5 --save-dev | |
| 145 | ``` | |
| 146 | | |
| 147 | 2\. **Install the Component as a Dev Dependency** | |
| 148 | | |
| 149 | Install the specific version of the component that your app uses: | |
| 150 | | |
| 151 | ```sh | |
| 152 | npm install ui5-cc-spreadsheetimporter --save-dev | |
| 153 | ``` | |
| 154 | | |
| 155 | 3\. **Install `ui5-middleware-servestatic`** | |
| 156 | | |
| 157 | ```sh | |
| 158 | npm install ui5-middleware-servestatic --save-dev | |
| 159 | ``` | |
| 160 | | |
| 161 | 4\. **Configure `ui5.yaml`** | |
| 162 | | |
| 163 | Add the following configuration to your `ui5.yaml` file (adjust the version number as needed): | |
| 164 | | |
| 165 | ```yaml | |
| 166 | server: | |
| 167 | customMiddleware: | |
| 168 | - name: ui5-middleware-servestatic | |
| 169 | afterMiddleware: compression | |
| 170 | mountPath: /resources/cc/spreadsheetimporter/v1_7_3/ | |
| 171 | configuration: | |
| 172 | rootPath: "node_modules/ui5-cc-spreadsheetimporter/dist" | |
| 173 | ``` | |
| 174 | | |
| 175 | ### Consuming the Centrally Deployed Component from ABAP System | |
| 176 | | |
| 177 | To consume the centrally deployed component while developing locally in VS Code or BAS: | |
| 178 | | |
| 179 | 1\. **Determine the Component URL** | |
| 180 | | |
| 181 | Access the App Index to find the URL of the component: | |
| 182 | | |
| 183 | ``` | |
| 184 | <SAP_SYSTEM_URL>/sap/bc/ui2/app_index/ui5_app_info?id=cc.spreadsheetimporter.v1_7_3 | |
| 185 | ``` | |
| 186 | | |
| 187 | 2\. **Configure Proxy Middleware** | |
| 188 | | |
| 189 | Depending on your setup, use either `fiori-tools-proxy` or `ui5-middleware-simpleproxy` in your `ui5.yaml` file. | |
| 190 | | |
| 191 | **Using `fiori-tools-proxy`:** | |
| 192 | | |
| 193 | ```yaml | |
| 194 | server: | |
| 195 | customMiddleware: | |
| 196 | - name: fiori-tools-proxy | |
| 197 | afterMiddleware: compression | |
| 198 | configuration: | |
| 199 | backend: | |
| 200 | - path: /sap | |
| 201 | url: <Cloud Connector or local URL> | |
| 202 | destination: <System Destination Name if in BAS> | |
| 203 | - path: /resources/cc/spreadsheetimporter/v1_7_3 | |
| 204 | destination: <System Destination Name if in BAS> | |
| 205 | pathPrefix: /sap/bc/ui5_ui5/sap/<BSP_NAME>/thirdparty/customcontrol/spreadsheetimporter/v1_7_3/ | |
| 206 | url: <Cloud Connector or local URL> | |
| 207 | ``` | |
| 208 | | |
| 209 | **Using `ui5-middleware-simpleproxy`:** | |
| 210 | | |
| 211 | ```yaml | |
| 212 | server: | |
| 213 | customMiddleware: | |
| 214 | - name: ui5-middleware-simpleproxy | |
| 215 | afterMiddleware: compression | |
| 216 | mountPath: /resources/cc/spreadsheetimporter/v1_7_3/ | |
| 217 | configuration: | |
| 218 | baseUri: "<SAP_SYSTEM_URL>/sap/bc/ui5_ui5/sap/<BSP_NAME>/thirdparty/customcontrol/spreadsheetimporter/v1_7_3/" | |
| 219 | username: <SAP_USERNAME> | |
| 220 | password: <SAP_PASSWORD> | |
| 221 | query: | |
| 222 | sap-client: '100' | |
| 223 | ``` | |
| 224 | | |
| 225 | **Note**: Replace placeholders like `<SAP_SYSTEM_URL>`, `<BSP_NAME>`, `<SAP_USERNAME>`, and `<SAP_PASSWORD>` with your actual system details. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Checks.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Error Types | |
| 2 | | |
| 3 | The following types of errors are handled by the UI5 Spreadsheet Upload Control: | |
| 4 | | |
| 5 | - **Mandatory Fields**: The control ensures that all mandatory fields are filled in before submitting data. If a mandatory field is left blank, an error message is displayed. | |
| 6 | | |
| 7 | - **Mandatory Fields Metadata**: The control parses the metadata of the entity set and checks if all mandatory fields are filled in before submitting data. If a mandatory field is left blank, an error message is displayed ([CAP Annotation](https://cap.cloud.sap/docs/guides/providing-services#mandatory)). | |
| 8 | | |
| 9 | - **Column Names Mismatch**: The control checks if the column names in the uploaded file match the expected column names. If there is a mismatch, such as an additional column that shouldn't be there, an error message is displayed. | |
| 10 | | |
| 11 | - **Data Type Mismatch**: The control checks if the data types in the uploaded file match the expected data types. | |
| 12 | | |
| 13 | - **Custom Errors**: The control allows you to add custom errors to the error dialog. You can add errors to the `messages` property of the `SpreadsheetUpload` control. After the event, the upload is canceled and the errors are displayed in the Error Dialog (see [Events](./Events.md) for more information). | |
| 14 | | |
| 15 | - **Backend Errors**: If the backend service returns an error, it is displayed. In the case of checks during saving (e.g. RAP or CAP), no error is displayed in the draft scenario in Fiori Element Apps as Fiori Element catches these errors. | |
| 16 | | |
| 17 | - **Duplicate Columns**: The control checks if the uploaded file contains duplicate columns. If there is a duplicate column, an error message is displayed. | |
| 18 | | |
| 19 | - **Max Length**: The control checks if the length of the data in the uploaded file does not exceed the maximum length of the corresponding field. If the length exceeds the maximum length, an error message is displayed. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/Docs.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Static Site Generator | |
| 2 | | |
| 3 | The documentation is set up with [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). | |
| 4 | | |
| 5 | ## Usage | |
| 6 | | |
| 7 | ### Configuration | |
| 8 | | |
| 9 | The configuration is in the [`mkdocs.yml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/mkdocs.yml) file. | |
| 10 | The pages and images are stored in the [`docs`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/docs) folder. | |
| 11 | | |
| 12 | ### Local Setup | |
| 13 | | |
| 14 | To run the docs locally, you can use Docker. | |
| 15 | To build the Docker image, run: | |
| 16 | ```sh | |
| 17 | docker build . -t mkdocs | |
| 18 | ``` | |
| 19 | | |
| 20 | To run the container: | |
| 21 | | |
| 22 | ```sh | |
| 23 | docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material | |
| 24 | ``` | |
| 25 | | |
| 26 | or | |
| 27 | | |
| 28 | ```sh | |
| 29 | pnpm runDocs | |
| 30 | ``` | |
| 31 | | |
| 32 | and then open http://localhost:8000. | |
| 33 | | |
| 34 | ### Prod Build | |
| 35 | | |
| 36 | The Documentation is hosted on GitHub Pages and is rebuilt on every push to the `main` branch using the GitHub Action [`pushDocs.yml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/.github/workflows/pushDocs.yml) and forcefully pushed to the `gh-deploy` branch. | |
| 37 | The current URL is: https://docs.spreadsheet-importer.com/ | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/GettingStarted.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Quick Setup with GitHub Codespaces | |
| 2 | | |
| 3 | The `postCreateCommand` will automatically install all dependencies. | |
| 4 | This will take a few minutes. | |
| 5 | | |
| 6 | [](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=569313224&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope) | |
| 7 | | |
| 8 | ## Local Setup | |
| 9 | | |
| 10 | ### Requirements | |
| 11 | | |
| 12 | - Node.js Versions 16.18.0, 18.12.0, or later | |
| 13 | | |
| 14 | ### Install required NPM Packages | |
| 15 | | |
| 16 | ```sh | |
| 17 | # Install pnpm if needed | |
| 18 | npm i -g pnpm | |
| 19 | | |
| 20 | # Install @sap/cds-dk if needed | |
| 21 | npm i -g @sap/cds-dk | |
| 22 | ``` | |
| 23 | | |
| 24 | ### Quick start | |
| 25 | | |
| 26 | To quickly start the test environment, see here. See detailed information below. | |
| 27 | | |
| 28 | ```sh | |
| 29 | git clone https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter | |
| 30 | pnpm i | |
| 31 | # will run `build` and start CAP Server and FE Apps V4 1.108 | |
| 32 | npm start | |
| 33 | ``` | |
| 34 | | |
| 35 | ### Setup `ui5-cc-spreadsheetimporter` | |
| 36 | | |
| 37 | This is the basic setting-up to continue with the next steps. | |
| 38 | | |
| 39 | ```sh | |
| 40 | # Clone GitHub Repo | |
| 41 | git clone https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter | |
| 42 | | |
| 43 | # Install all packages including for test environment | |
| 44 | pnpm i | |
| 45 | ``` | |
| 46 | | |
| 47 | ## Start Developing | |
| 48 | | |
| 49 | ### Start CAP server | |
| 50 | | |
| 51 | The CAP Server is currently very basic and provides an Order Entity with OrderItems. All the apps will consume from this server. | |
| 52 | | |
| 53 | ```sh | |
| 54 | # Start CAP Server (serves data for all Test Apps) | |
| 55 | npm run start:server | |
| 56 | ``` | |
| 57 | | |
| 58 | ### Build Step | |
| 59 | | |
| 60 | The apps get the Spreadsheet Importer Component with the middleware `ui5-middleware-ui5`. With this, no build step is necessary. | |
| 61 | To make this work, in the `ui5-cc-spreadsheetimporter` folder, the dist folder should be empty with only the `.gitkeep` file. If a build step was executed and the dist folder is not empty, the app will only load the built version. | |
| 62 | | |
| 63 | ### Start UI5 Apps | |
| 64 | | |
| 65 | Under the folder `./examples/packages` are all the UI5 Apps that are set up for the Consumption of the Custom Control. | |
| 66 | There are five different apps for different scenarios (OData V2 Fiori Elements, V2 Freestyle, V2 FE Non Draft, V4 FE, V4 FPM). | |
| 67 | There are currently only with version `1.120`. For testing, these apps are copied and tested with other maintenance versions. | |
| 68 | If you want to test with lower maintenance versions, just run this command: | |
| 69 | `npm run copyTestApps` | |
| 70 | This will copy the apps according to this [json file](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/dev/testapps.json). | |
| 71 | | |
| 72 | ```sh | |
| 73 | # Start Test Apps | |
| 74 | npm run start:v4fe:108 | |
| 75 | npm run start:v2fe:108 | |
| 76 | | |
| 77 | # Alternative with pnpm | |
| 78 | pnpm --filter ordersv2fe108 start | |
| 79 | pnpm --filter ordersv4fe108 start | |
| 80 | | |
| 81 | # Run other apps after copying | |
| 82 | pnpm --filter ordersv2fe96 start | |
| 83 | pnpm --filter ordersv2fe84 start | |
| 84 | pnpm --filter ordersv2fe71 start | |
| 85 | pnpm --filter ordersv4fe96 start | |
| 86 | ... | |
| 87 | ``` | |
| 88 | | |
| 89 | ### Run wdi5 Tests | |
| 90 | | |
| 91 | To run the wdi5 tests, the CAP server and the corresponding app must already be running. | |
| 92 | You can run the test for the OData V4 UI5 Version 108 with this command: | |
| 93 | | |
| 94 | ```sh | |
| 95 | npm run test:v4fe:108 | |
| 96 | ``` | |
| 97 | | |
| 98 | More Info on the [wdi5 Tests](./wdi5.md) site. | |
| 99 | | |
| 100 | ### Commit Message | |
| 101 | | |
| 102 | To create an automatic changelog, we use the [angular commit message guidelines](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#commit). | |
| 103 | | |
| 104 | The commit starts with the `type` and an optional `scope` like `feat(api)`. Possible types are listed [here](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type). Scopes can be freely named or omitted. | |
| 105 | | |
| 106 | A few examples: | |
| 107 | | |
| 108 | - `feat(api): add new create api for customer` | |
| 109 | - `fix(api): edge case when customer is from EU` | |
| 110 | - `chore(workflow): changed commiting username` | |
| 111 | - `docs: typo in readme` | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/GitHubActions.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Docs | |
| 2 | | |
| 3 | The GitHub Actions `docs` is defined in the [`pushDocs.yml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/.github/workflows/pushDocs.yml) file. | |
| 4 | The content of the docs is in the [`docs`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/docs) folder and the config file is [`mkdocs.yml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/mkdocs.yml). | |
| 5 | | |
| 6 | In an Ubuntu environment, the workflow will set up Python and install `mkdocs-material` and `mkdocs-minify-plugin`. | |
| 7 | With `mkdocs gh-deploy --force`, the docs site will be pushed to the `gh-docs` branch and then published with GitHub Pages to https://docs.spreadsheet-importer.com/. | |
| 8 | | |
| 9 | ## wdi5 | |
| 10 | | |
| 11 | With wdi5, it is possible to test an E2E Scenaro with UI5 automatically in GitHub Actions. | |
| 12 | In order to cover as many scenarios as possible, this workflow will cover as many scenarios as possible. | |
| 13 | | |
| 14 | To avoid writing a separate configuration and workflow for each scenario, we use the [matrix](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs) function of GitHub Actions. This means that we only have to define the workflow once, and it is executed as often as necessary. | |
| 15 | | |
| 16 | The workflow is defined in the [`wdi5-test.yml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/.github/workflows/wdi5-test.yml) file. | |
| 17 | The following steps are currently executed in a Ubuntu environment with matrix: | |
| 18 | | |
| 19 | 1. Update Chrome in Ubuntu to the latest version. | |
| 20 | 2. Checkout the `ui5-cc-spreadsheetimporter` repo. | |
| 21 | 3. Install `pnpm`. | |
| 22 | 4. Install Node 16. | |
| 23 | 5. Get the port of the current scenario App (i.e., for `ordersv4fe`, the port is 8080). | |
| 24 | 1. With the port, we can check if the app is running. | |
| 25 | 6. Run `pnpm i`. | |
| 26 | 7. Build `ui5-cc-spreadsheetimporter`. | |
| 27 | 8. Start CAP Server (for all scenarios the same). | |
| 28 | 9. Start the Scenario App | |
| 29 | 1. For example, the matrix variables in `start:silent&` are used like: | |
| 30 | | |
| 31 | `pnpm --filter ${{ matrix.scenario }}${{ matrix.ui5version }} start:silent&` | |
| 32 | which can be: | |
| 33 | `pnpm --filter ordersv4fe108 start` | |
| 34 | 10. Start wdi5 Tests | |
| 35 | | |
| 36 | a. First check if the server and app are running. | |
| 37 | | |
| 38 | b. Start wdi5 test `headless` for the current scenario. | |
| 39 | | |
| 40 | c. So `pnpm --filter ui5-cc-spreadsheetimporter-sample test -- -- --headless ${{ matrix.scenario }} ${{ matrix.ui5version }}` | |
| 41 | will be | |
| 42 | `pnpm --filter ui5-cc-spreadsheetimporter-sample test -- -- ordersv4fe 108` | |
| 43 | | |
| 44 | ### Start wdi5 Tests | |
| 45 | | |
| 46 | Because we use only one [`wdio-base.conf.js`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/test/wdio-base.conf.js), we must and can only test this one scenario with the names of the scenario. | |
| 47 | We can pass parameters there when we call the test with `"test": "wdio run ./test/wdio-base.conf.js"`. | |
| 48 | So in GitHub, it will be called with `pnpm --filter ui5-cc-spreadsheetimporter-sample test -- -- ordersv4fe 108`. | |
| 49 | With these parameters, we can assign the appropriate port and spec files in the `wdio-base.conf.js` and read them from [`testapps.json`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/dev/testapps.json). | |
| 50 | We try to ensure that all spec files apply to all scenarios, but certain ones can only be tested with OData V4, for example. | |
| 51 | | |
| 52 | ## Release Please Action | |
| 53 | | |
| 54 | For automatic versioning and changelog generation, we use [release-please-action](https://github.com/google-github-actions/release-please-action), which allows everything to be done with GitHub Actions. | |
| 55 | This workflow is defined in [release-please.yml](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/.github/workflows/release-please.yml). | |
| 56 | | |
| 57 | This workflow will create a Pull Request if a `fix:` or `feat:` commit is pushed to the `main` branch. | |
| 58 | This Pull Request contains all changes, like the updated version and Changelog. | |
| 59 | In addition, scripts run to change the version in other files. | |
| 60 | In this [commit](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/commit/4bf424914ca6c66c52cb17852f36ddbd520af07e), you can see which files are updated with these scripts. | |
| 61 | For example, in `ui5.yaml` and the sample apps. | |
| 62 | | |
| 63 | After this Pull Request is merged, the `ui5-cc-spreadsheetimporter` will be built and published to npm automatically. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/SampleApps.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | On the Page [Supported Versions](./../SupportVersions.md) you can see which versions are supported. To make sure all those versions are supported, tests are run with the sample apps. | |
| 2 | I also make these sample apps available to you on the [Live Demo](https://livedemo.spreadsheet-importer.com/launchpad.html#Shell-home). | |
| 3 | | |
| 4 | ## Docker | |
| 5 | | |
| 6 | To make the deployment of the sample apps to the server providing the Live Demo easier, I use Docker. | |
| 7 | The Dockerfile is available in the examples folder ([see Dockerfile](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/examples/Dockerfile)). | |
| 8 | The Dockerfile is created on every push to the main branch ([see GitHub Workflow](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/.github/workflows/dockerfile-examples-deploy.yml)), pushed to [the Docker Hub](https://hub.docker.com/r/greatoceandrive/ui5-spreadsheet-importer-examples), and then deployed to the server. | |
| 9 | The Docker image is build for AMD64 and ARM64 platforms. | |
| 10 | | |
| 11 | ### How to use the Dockerfile | |
| 12 | | |
| 13 | #### Pull the Docker Image | |
| 14 | | |
| 15 | ```sh | |
| 16 | docker pull greatoceandrive/ui5-spreadsheet-importer-examples | |
| 17 | ``` | |
| 18 | | |
| 19 | #### Run the Docker Image | |
| 20 | | |
| 21 | ```sh | |
| 22 | docker run -p 4004:4004 greatoceandrive/ui5-spreadsheet-importer-examples | |
| 23 | ``` | |
| 24 | | |
| 25 | | |
| 26 | #### Build the Docker Image yourself (optional) | |
| 27 | | |
| 28 | ```sh | |
| 29 | git clone https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter | |
| 30 | cd ui5-cc-spreadsheetimporter | |
| 31 | docker build -t ui5-spreadsheet-importer-examples -f examples/Dockerfile . | |
| 32 | docker run -p 4004:4004 ui5-spreadsheet-importer-examples | |
| 33 | ``` | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/SpreadsheetDownload.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Deep Download Feature | |
| 2 | | |
| 3 | This feature is experimental and only available with an OData V4 backend. It allows downloading data from OData V4 backends with support for nested entities and relationships, handling complex data structures efficiently. | |
| 4 | | |
| 5 | ## Triggering the Download | |
| 6 | | |
| 7 | The spreadsheet download is triggered by a button in the spreadsheet upload dialog. This process is managed by the `SpreadsheetUploadDialog.ts` file. | |
| 8 | | |
| 9 | - **onInitDownloadSpreadsheetProcess**: This method initializes the download process. If `config.showOptions` is true, it opens the `SpreadsheetDownloadDialog` to allow users to set options. Otherwise, it triggers the download directly with default settings. | |
| 10 | | |
| 11 | - **onSave**: This method in `SpreadsheetDownloadDialog.ts` sets the `deepDownloadConfig` and calls `onDownloadDataSpreadsheet`. | |
| 12 | | |
| 13 | ## Method onDownloadDataSpreadsheet | |
| 14 | | |
| 15 | This method checks for errors before proceeding with the download. Errors can occur in the `setContext` method of `SpreadsheetUpload.ts`. | |
| 16 | | |
| 17 | ## MetadataHandlerV4 | |
| 18 | | |
| 19 | The `MetadataHandlerV4` class builds the metadata model, which is a tree of all entities and their properties. The `entityName` should be the root entity of the download, and `deepLevel` should be the maximum depth of the download. | |
| 20 | | |
| 21 | - **_findEntitiesByNavigationProperty**: This method traverses the metadata model and collects all entities related to the root entity by navigation properties. If the `$kind` and `$Partner` properties are present and the `$ReferentialConstraint` is not, the full entity is added to the parent entity as `$XYZEntity`, making it explicitly fetchable with `$XYZFetchableEntity`. | |
| 22 | | |
| 23 | ### JSON Sample | |
| 24 | | |
| 25 | This is the root entity without some properties for brevity before the navigation property was added: | |
| 26 | | |
| 27 | ```json | |
| 28 | { | |
| 29 | "$kind": "EntityType", | |
| 30 | "$Key": [ | |
| 31 | "ID", | |
| 32 | "IsActiveEntity" | |
| 33 | ], | |
| 34 | "OrderNo": { | |
| 35 | "$kind": "Property", | |
| 36 | "$Type": "Edm.String", | |
| 37 | "$MaxLength": 22 | |
| 38 | }, | |
| 39 | "Items": { | |
| 40 | "$kind": "NavigationProperty", | |
| 41 | "$isCollection": true, | |
| 42 | "$Type": "OrdersService.OrderItems", | |
| 43 | "$Partner": "order", | |
| 44 | "$OnDelete": "Cascade" | |
| 45 | }, | |
| 46 | "Shipping": { | |
| 47 | "$kind": "NavigationProperty", | |
| 48 | "$isCollection": true, | |
| 49 | "$Type": "OrdersService.ShippingDetails", | |
| 50 | "$Partner": "order", | |
| 51 | "$OnDelete": "Cascade" | |
| 52 | }, | |
| 53 | "buyer": { | |
| 54 | "$kind": "Property", | |
| 55 | "$Type": "Edm.String", | |
| 56 | "$MaxLength": 255 | |
| 57 | } | |
| 58 | } | |
| 59 | ``` | |
| 60 | | |
| 61 | This is the root entity after navigation properties for `Items` (here `Infos` under `Items`) and `Shipping` were added: | |
| 62 | | |
| 63 | ```json | |
| 64 | { | |
| 65 | "$kind": "EntityType", | |
| 66 | "$Key": [ | |
| 67 | "ID", | |
| 68 | "IsActiveEntity" | |
| 69 | ], | |
| 70 | "ID": { | |
| 71 | "$kind": "Property", | |
| 72 | "$Type": "Edm.Guid", | |
| 73 | "$Nullable": false | |
| 74 | }, | |
| 75 | "OrderNo": { | |
| 76 | "$kind": "Property", | |
| 77 | "$Type": "Edm.String", | |
| 78 | "$MaxLength": 22 | |
| 79 | }, | |
| 80 | "Items": { | |
| 81 | "$kind": "NavigationProperty", | |
| 82 | "$isCollection": true, | |
| 83 | "$Type": "OrdersService.OrderItems", | |
| 84 | "$Partner": "order", | |
| 85 | "$OnDelete": "Cascade", | |
| 86 | "$XYZEntity": { | |
| 87 | "$kind": "EntityType", | |
| 88 | "$Key": [ | |
| 89 | "ID", | |
| 90 | "IsActiveEntity" | |
| 91 | ], | |
| 92 | "ID": { | |
| 93 | "$kind": "Property", | |
| 94 | "$Type": "Edm.Guid", | |
| 95 | "$Nullable": false | |
| 96 | }, | |
| 97 | "order": { | |
| 98 | "$kind": "NavigationProperty", | |
| 99 | "$Type": "OrdersService.Orders", | |
| 100 | "$Partner": "Items", | |
| 101 | "$ReferentialConstraint": { | |
| 102 | "order_ID": "ID" | |
| 103 | } | |
| 104 | }, | |
| 105 | "order_ID": { | |
| 106 | "$kind": "Property", | |
| 107 | "$Type": "Edm.Guid" | |
| 108 | }, | |
| 109 | "product_ID": { | |
| 110 | "$kind": "Property", | |
| 111 | "$Type": "Edm.String" | |
| 112 | }, | |
| 113 | "Infos": { | |
| 114 | "$kind": "NavigationProperty", | |
| 115 | "$isCollection": true, | |
| 116 | "$Type": "OrdersService.OrderItemsInfo", | |
| 117 | "$Partner": "orderItem", | |
| 118 | "$OnDelete": "Cascade", | |
| 119 | "$XYZEntity": { | |
| 120 | "$kind": "EntityType", | |
| 121 | "$Key": [ | |
| 122 | "ID", | |
| 123 | "IsActiveEntity" | |
| 124 | ], | |
| 125 | "ID": { | |
| 126 | "$kind": "Property", | |
| 127 | "$Type": "Edm.Guid", | |
| 128 | "$Nullable": false | |
| 129 | }, | |
| 130 | "orderItem": { | |
| 131 | "$kind": "NavigationProperty", | |
| 132 | "$Type": "OrdersService.OrderItems", | |
| 133 | "$Partner": "Infos", | |
| 134 | "$ReferentialConstraint": { | |
| 135 | "orderItem_ID": "ID" | |
| 136 | } | |
| 137 | }, | |
| 138 | "orderItem_ID": { | |
| 139 | "$kind": "Property", | |
| 140 | "$Type": "Edm.Guid" | |
| 141 | }, | |
| 142 | "comment": { | |
| 143 | "$kind": "Property", | |
| 144 | "$Type": "Edm.String" | |
| 145 | } | |
| 146 | }, | |
| 147 | "$XYZFetchableEntity": true | |
| 148 | }, | |
| 149 | "quantity": { | |
| 150 | "$kind": "Property", | |
| 151 | "$Type": "Edm.Int32" | |
| 152 | } | |
| 153 | }, | |
| 154 | "$XYZFetchableEntity": true | |
| 155 | }, | |
| 156 | "Shipping": { | |
| 157 | "$kind": "NavigationProperty", | |
| 158 | "$isCollection": true, | |
| 159 | "$Type": "OrdersService.ShippingDetails", | |
| 160 | "$Partner": "order", | |
| 161 | "$OnDelete": "Cascade", | |
| 162 | "$XYZEntity": { | |
| 163 | "$kind": "EntityType", | |
| 164 | "$Key": [ | |
| 165 | "ID", | |
| 166 | "IsActiveEntity" | |
| 167 | ], | |
| 168 | "ID": { | |
| 169 | "$kind": "Property", | |
| 170 | "$Type": "Edm.Guid", | |
| 171 | "$Nullable": false | |
| 172 | }, | |
| 173 | "order": { | |
| 174 | "$kind": "NavigationProperty", | |
| 175 | "$Type": "OrdersService.Orders", | |
| 176 | "$Partner": "Shipping", | |
| 177 | "$ReferentialConstraint": { | |
| 178 | "order_ID": "ID" | |
| 179 | } | |
| 180 | }, | |
| 181 | "order_ID": { | |
| 182 | "$kind": "Property", | |
| 183 | "$Type": "Edm.Guid" | |
| 184 | } | |
| 185 | }, | |
| 186 | "$XYZFetchableEntity": true | |
| 187 | }, | |
| 188 | "buyer": { | |
| 189 | "$kind": "Property", | |
| 190 | "$Type": "Edm.String", | |
| 191 | "$MaxLength": 255 | |
| 192 | } | |
| 193 | } | |
| 194 | ``` | |
| 195 | | |
| 196 | With this metadata model, it is much easier to fetch data and have an overview of all the entities related to the root entity. | |
| 197 | | |
| 198 | ### Expands | |
| 199 | | |
| 200 | The method `_getExpandsRecursive` is called to traverse the metadata model and create a list of expands for the OData binding, which is created in `getBindingFromBinding` in `ODataV4.ts`. The expands are used to fetch related entities in one request. | |
| 201 | | |
| 202 | ### Binding | |
| 203 | | |
| 204 | With the binding from the root entity, a custom binding is created with the expands in `getBindingFromBinding` in `ODataV4.ts` to fetch the data. | |
| 205 | | |
| 206 | ### fetchBatch | |
| 207 | | |
| 208 | The method `fetchBatch` in `ODataV4.ts` is used to fetch the data in batches of 1000. This is done to prevent the OData V4 backend from timing out. The data is requested with `requestContexts` from the binding, and the results are of type `sap.ui.model.odata.v4.Context[]` and returned in the variable `totalResults`. | |
| 209 | | |
| 210 | ### extractObjects Method | |
| 211 | | |
| 212 | The `extractObjects` method is used to process the fetched data and prepare it for spreadsheet generation. | |
| 213 | | |
| 214 | ## Spreadsheet Generation | |
| 215 | | |
| 216 | The `SpreadsheetGenerator` class handles the creation of the spreadsheet file. It uses the fetched data to generate sheets for each entity and its related entities, ensuring that all necessary data is included in the final output. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/Update.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | | |
| 2 | Only V4 is supported for now. | |
| 3 | | |
| 4 | ## OData V4 | |
| 5 | | |
| 6 | The problem with Draft is that when updating lot of objects, the update will fail if one of the objects is not found because of the draft status. | |
| 7 | Draft status will determined with `IsActiveEntity` property. | |
| 8 | To make it as seamless as possible, the process will try to find every object with `IsActiveEntity=true`. This does not find objects that dont have a active entity (draft but not yet created). | |
| 9 | The finding of the object result in a get request to the OData service for each row. | |
| 10 | After that the process knows the state of the object and can update it. | |
| 11 | So if on the object `HasDraftEntity` is true or `IsActiveEntity` is false, the process will create a new context with `IsActiveEntity=false` and use the draft entity automatically to update the object. | |
| 12 | | |
| 13 | | |
| 14 | ## Technical Details | |
| 15 | | |
| 16 | To get the all the objects that are imported from the spreadsheet, the process will create a new empty list binding with a filter of all the keys from the spreadsheet. | |
| 17 | Technically is has to query for `IsActiveEntity=true` and `IsActiveEntity=false` and combine the results. | |
| 18 | This will result in two get requests to the OData service for each row combined in two batch request for each batch. | |
| 19 | If a row is not found it is just not included in the List Binding. | |
| 20 | So the process will not fail if a row is not found and can match which objects are not found from the List Binding. | |
| 21 | If a object was not found the user can then decide to continue with the found objects or to cancel the process. | |
| 22 | Each context will then be used to update the object with `setProperty`. | |
| 23 | | |
| 24 | ### Different States in Export | |
| 25 | | |
| 26 | For the export the process determines the state of the object by checking the `IsActiveEntity` and `HasDraftEntity` properties. | |
| 27 | | |
| 28 | | |
| 29 | #### List Report | |
| 30 | | |
| 31 | - `IsActiveEntity=true` and `HasDraftEntity=false` -> `IsActiveEntity` column is set to true | |
| 32 | - `IsActiveEntity=true` and `HasDraftEntity=true` -> `IsActiveEntity` column is set to false | |
| 33 | | |
| 34 | #### Object Page | |
| 35 | | |
| 36 | - `IsActiveEntity=true` and `HasDraftEntity=false` -> `IsActiveEntity` column is set to true | |
| 37 | - `IsActiveEntity=true` and `HasDraftEntity=true` -> `IsActiveEntity` column is set to false | |
| 38 | | |
| 39 | ### Different States in Upload | |
| 40 | | |
| 41 | #### List Report | |
| 42 | | |
| 43 | - `IsActiveEntity=true` and `HasDraftEntity=false` -> update the object (expecting `IsActiveEntity=true` in the spreadsheet import) | |
| 44 | - `IsActiveEntity=true` and `HasDraftEntity=true` -> create a new context with `IsActiveEntity=false` and use the draft entity automatically to update the object (expecting `IsActiveEntity=false` in the spreadsheet import) | |
| 45 | | |
| 46 | #### Table in Object Page | |
| 47 | | |
| 48 | ##### Not in Edit Mode | |
| 49 | | |
| 50 | - `IsActiveEntity=true` and `HasDraftEntity=false` and `HasActiveEntity=false` -> update the object (expecting `IsActiveEntity=true` in the spreadsheet import) | |
| 51 | | |
| 52 | ##### In Edit Mode | |
| 53 | | |
| 54 | - `IsActiveEntity=false` and `HasDraftEntity=false` and `HasActiveEntity=true` -> update the object (expecting `IsActiveEntity=false` in the spreadsheet import) | |
| 55 | | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/opa5.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | OPA5 Tests are used here to check the more unit-like functionality of the app. | |
| 2 | | |
| 3 | ## Setup | |
| 4 | | |
| 5 | The [ui5-test-runner](https://github.com/ArnaudBuchholz/ui5-test-runner) by Arnaud Buchholz is installed and run from the `examples` folder. | |
| 6 | Tests are currently only in OData V4 FE Example. | |
| 7 | | |
| 8 | ### Configuration | |
| 9 | | |
| 10 | There is no config file. With the ui5-test-runner, only the `opaTests.qunit.html` is called. | |
| 11 | | |
| 12 | ## Run tests | |
| 13 | | |
| 14 | You can run the tests for OData V4 and UI5 Version 108 in the root folder with: | |
| 15 | ```sh | |
| 16 | npm run start:v4fe:108 | |
| 17 | npm run test:opa:v4fe:108 | |
| 18 | ``` | |
| 19 | | |
| 20 | which will run `pnpm --filter ui5-cc-spreadsheetimporter-sample ui5-test-runner --url http://localhost:8080/test/integration/opaTests.qunit.html`. | |
| 21 | All information about the run is in the folder `./examples/report`. | |
| 22 | So you can run all the other versions like 96 and 84 with the right port. | |
| 23 | | |
| 24 | | |
| 25 | ## GitHub Actions | |
| 26 | | |
| 27 | The GitHub Action Workflow will run on every Pull Request push, testing the V4 Versions and is written down in [`opa5-test.yml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/.github/workflows/opa5-test.yml). | |
| 28 | | |
| 29 | More info at [GitHub Actions](./../Development/GitHubActions.md) | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Development/wdi5.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | The UI5 Custom Control Spreadsheet Upload is used in many different scenarios. To ensure that changes do not affect the function, the basic function and a few other additional functions are tested with wdi5 tests. | |
| 2 | | |
| 3 | The overview of which scenarios are covered by wdi5 tests can be found here: [wdi5 tests](../SupportVersions.md#wdi5-tests) | |
| 4 | | |
| 5 | ## Setup | |
| 6 | | |
| 7 | wdi5 is used in the test setup in the [`examples`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/examples) folder in the [`test`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/examples/test) folder. | |
| 8 | As pnpm is used, with `pnpm i`, all the packages, including `wdio-ui5-service`, are installed. | |
| 9 | | |
| 10 | ### Configuration | |
| 11 | | |
| 12 | The basic config file is the `wdio-base.conf.js` file. | |
| 13 | To avoid having to create a separate configuration file for each scenario, logic is integrated into the file so that the appropriate variables are automatically drawn, for example the port or the appropriate spec files. | |
| 14 | The data for this is stored in the [`testapps.json`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/dev/testapps.json) file. | |
| 15 | | |
| 16 | ## Run tests | |
| 17 | | |
| 18 | You can run the tests for OData V2 and V4 UI5 Version 108 in the root folder with: | |
| 19 | ```sh | |
| 20 | npm run test:v4fe:108 | |
| 21 | npm run test:v2fe:108 | |
| 22 | ``` | |
| 23 | | |
| 24 | which will run `pnpm --filter ui5-cc-spreadsheetimporter-sample test -- -- ordersv4fe 108`. | |
| 25 | So, you can run all the other apps like | |
| 26 | | |
| 27 | ```sh | |
| 28 | pnpm --filter ui5-cc-spreadsheetimporter-sample test -- -- ordersv4fe 84 | |
| 29 | ``` | |
| 30 | | |
| 31 | ### Run single spec | |
| 32 | | |
| 33 | You can also run single test specs. You need to go to the `examples` folder for this. | |
| 34 | For example, you can run the test spec `OpenSpreadsheetUploadDialog` with OData V2 FE UI5 Version 96 with: | |
| 35 | | |
| 36 | ```sh | |
| 37 | npm run test -- ordersv2fe 96 --spec OpenSpreadsheetUploadDialog | |
| 38 | ``` | |
| 39 | | |
| 40 | ### Run headless | |
| 41 | | |
| 42 | The wdi5 tests in GitHub Actions must run headless, which is also possible to call locally with: | |
| 43 | | |
| 44 | ```sh | |
| 45 | pnpm --filter ui5-cc-spreadsheetimporter-sample test -- -- --headless ordersv4fe 84 | |
| 46 | ``` | |
| 47 | | |
| 48 | ## GitHub Actions | |
| 49 | | |
| 50 | As specified in the [`testapps.json`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/dev/testapps.json) file, the GitHub Action Workflow will run on every Pull Request push, testing scenarios with all current UI5 Maintenance Versions and is written in [`wdi5-test.yml`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/.github/workflows/wdi5-test.yml). | |
| 51 | | |
| 52 | More info at [GitHub Actions](./../Development/GitHubActions.md) | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Events.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | The following events can be used as extension points to intervene and manipulate data: | |
| 2 | | |
| 3 | | Event | Description | | |
| 4 | | -------------------- | -------------------------------------------------------------------------------------------------- | | |
| 5 | | `preFileProcessing` | Execute custom logic before processing the spreadsheet file starts | | |
| 6 | | `checkBeforeRead` | Check data before it is uploaded to the UI5 | | |
| 7 | | `changeBeforeCreate` | Change data before it is sent to the backend | | |
| 8 | | `requestCompleted` | Event when the request is completed | | |
| 9 | | `uploadButtonPress` | Fired when the `Upload` button is pressed, possible to prevent data from being sent to the backend | | |
| 10 | | `beforeDownloadFileProcessing` | Fired before the data is converted to a spreadsheet file | | |
| 11 | | `beforeDownloadFileExport` | Fired just before the file is downloaded | | |
| 12 | | |
| 13 | | |
| 14 | You can attach async functions to the events by wrapping the function in a `Promise`. See [Attach async functions to events](#attach-async-functions-to-events) for more information. | |
| 15 | | |
| 16 | ## Event `preFileProcessing` | |
| 17 | | |
| 18 | When the file is uploaded to the app, the `preFileProcessing` event is fired. Use this event to execute custom logic before processing the spreadsheet file starts. | |
| 19 | The `file` is available in the event and can be manipulated. If you want to prevent the processing of the file, call the `preventDefault` method of the event. If you want to change the file that will be processed, return the new file. | |
| 20 | | |
| 21 | ### Example | |
| 22 | | |
| 23 | ```javascript | |
| 24 | this.spreadsheetUpload.attachPreFileProcessing(function (event) { | |
| 25 | // example | |
| 26 | let file = event.getParameter("file"); | |
| 27 | if (file.name.endsWith(".txt")) { | |
| 28 | // prevent processing of file | |
| 29 | event.preventDefault(); | |
| 30 | // show custom ui5 error message | |
| 31 | new MessageToast.show("File with .txt extension is not allowed"); | |
| 32 | // change the file that will be processed | |
| 33 | // Create a Blob with some text content | |
| 34 | const blob = new Blob(["This is some dummy text content"], { type: "text/plain" }); | |
| 35 | // Create a File object from the Blob | |
| 36 | const file2 = new File([blob], "TEXT.txt", { type: "text/plain" }); | |
| 37 | return file2; | |
| 38 | } | |
| 39 | }); | |
| 40 | ``` | |
| 41 | | |
| 42 | ## Event `checkBeforeRead` | |
| 43 | | |
| 44 | When the file is uploaded to the app, the `checkBeforeRead` event is fired. | |
| 45 | | |
| 46 | ### Example | |
| 47 | | |
| 48 | This sample is from the [sample app](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/47d22cdc42aa1cacfd797bdc0e025b830330dc5e/examples/packages/ordersv4fe/webapp/ext/ObjectPageExtController.js#L24-L42). It checks whether the price is over 100. | |
| 49 | | |
| 50 | ```javascript | |
| 51 | this.spreadsheetUpload.attachCheckBeforeRead(function(event) { | |
| 52 | // example | |
| 53 | const sheetdata = event.getParameter("sheetData"); | |
| 54 | let errorArray = []; | |
| 55 | for (const [index, row] of sheetData.entries()) { | |
| 56 | // Check for invalid price | |
| 57 | for (const key in row) { | |
| 58 | if (key.endsWith("[price]") && row[key].rawValue > 100) { | |
| 59 | const error = { | |
| 60 | title: "Price too high (max 100)", | |
| 61 | row: index + 2, | |
| 62 | group: true, | |
| 63 | rawValue: row[key].rawValue, | |
| 64 | ui5type: "Error" | |
| 65 | }; | |
| 66 | errorArray.push(error); | |
| 67 | } | |
| 68 | } | |
| 69 | } | |
| 70 | event.getSource().addArrayToMessages(errorArray); | |
| 71 | }, this); | |
| 72 | ``` | |
| 73 | | |
| 74 | You can add errors to the `messages` property of the `SpreadsheetUpload` control. After the event, the upload is canceled and the errors are displayed in the error dialog. Use the `addArrayToMessages` method to add errors to the `messages` property. It expects an array of objects with the following properties: | |
| 75 | | |
| 76 | - `title` - the title of the error | |
| 77 | - `row` - the row number of the error | |
| 78 | - `group` - set to `true` or `false` to group the errors by title | |
| 79 | - `rawValue` - the raw value of the data from the spreadsheet | |
| 80 | - `ui5type` - the type of the error, can be `Error`, `Warning`, `Success`, `Information` or `None` from the [`MessageType](https://ui5.sap.com/#/api/sap.ui.core.MessageType) enum | |
| 81 | | |
| 82 | Errors with the same title will be grouped. | |
| 83 | | |
| 84 | { loading=lazy } | |
| 85 | | |
| 86 | ## Event `changeBeforeCreate` | |
| 87 | | |
| 88 | When the `Upload` button is pressed, the `changeBeforeCreate` event is fired. Use this event to manipulate the data before it is sent to the backend. The event expects a payload object to be returned. | |
| 89 | Make sure only one handler is attached to this event. If multiple handlers are attached, only the first payload will be used. | |
| 90 | | |
| 91 | ### Example | |
| 92 | | |
| 93 | This sample is from the [sample app](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/47d22cdc42aa1cacfd797bdc0e025b830330dc5e/examples/packages/ordersv4fe/webapp/ext/ObjectPageExtController.js#L45-L52). It overwrites the payload. | |
| 94 | | |
| 95 | ```javascript | |
| 96 | this.spreadsheetUpload.attachChangeBeforeCreate(function (event) { | |
| 97 | let payload = event.getParameter("payload"); | |
| 98 | // round number from 12,56 to 12,6 | |
| 99 | if (payload.price) { | |
| 100 | payload.price = Number(payload.price.toFixed(1)); | |
| 101 | } | |
| 102 | return payload; | |
| 103 | }, this); | |
| 104 | ``` | |
| 105 | | |
| 106 | ## Event `requestCompleted` | |
| 107 | | |
| 108 | When the request is completed, the `requestCompleted` event is fired. Use the `success` parameter to check if the request was successful. | |
| 109 | | |
| 110 | ### Example | |
| 111 | | |
| 112 | ```javascript | |
| 113 | this.spreadsheetUpload.attachRequestCompleted(function (event) { | |
| 114 | const success = event.getParameter("success"); | |
| 115 | if (success) { | |
| 116 | console.log("Request Completed"); | |
| 117 | } else { | |
| 118 | console.log("Request Failed"); | |
| 119 | } | |
| 120 | }, this); | |
| 121 | ``` | |
| 122 | | |
| 123 | ## Event `uploadButtonPress` | |
| 124 | | |
| 125 | When the `Upload` button is pressed, the `uploadButtonPress` event is fired. The event is fired before the `changeBeforeCreate` event. Prevent the data from being sent to the backend by calling the `preventDefault` method of the event. | |
| 126 | | |
| 127 | ### Example 1 | |
| 128 | | |
| 129 | ```javascript | |
| 130 | this.spreadsheetUpload.attachUploadButtonPress(function (event) { | |
| 131 | // Prevent data from being sent to the backend | |
| 132 | event.preventDefault(); | |
| 133 | // Get payload | |
| 134 | const payload = event.getParameter("payload"); | |
| 135 | }, this); | |
| 136 | ``` | |
| 137 | | |
| 138 | ### Example 2 | |
| 139 | | |
| 140 | You can also use this event to sent the data to the backend and add possible errors to the component. Use the `addArrayToMessages` method to add errors. It will display the errors in the error dialog after the execution of the event. | |
| 141 | | |
| 142 | ```javascript | |
| 143 | this.spreadsheetUpload.attachUploadButtonPress(async function (event) { | |
| 144 | event.preventDefault(); | |
| 145 | | |
| 146 | event.getSource().addArrayToMessages([{ | |
| 147 | title: "Error on creating", | |
| 148 | group: false, | |
| 149 | ui5type: "Error" | |
| 150 | }]); | |
| 151 | | |
| 152 | // simulate async call | |
| 153 | await new Promise((resolve) => { | |
| 154 | // Wait for 2 seconds | |
| 155 | setTimeout(() => { | |
| 156 | resolve(); | |
| 157 | }, 2000); | |
| 158 | }); | |
| 159 | | |
| 160 | // Code here will execute after the 2-second wait | |
| 161 | }, this); | |
| 162 | ``` | |
| 163 | | |
| 164 | ## Event `beforeDownloadFileProcessing` | |
| 165 | | |
| 166 | Parameters: | |
| 167 | | |
| 168 | - `data`- the data that will be converted to a spreadsheet file, the data is always the `$XYZData` property of the data object | |
| 169 | | |
| 170 | This event is fired before the data is converted to a spreadsheet file. Use this event to manipulate the data before it is converted. | |
| 171 | You can change directly the data parameter of the event as this is a reference to the data. | |
| 172 | | |
| 173 | ### Example | |
| 174 | | |
| 175 | ```javascript | |
| 176 | onDownload: async function () { | |
| 177 | // init your spreadsheet upload component | |
| 178 | this.spreadsheetUpload.attachBeforeDownloadFileProcessing(this.onBeforeDownloadFileProcessing, this); | |
| 179 | this.spreadsheetUpload.triggerDownloadSpreadsheet(); | |
| 180 | }, | |
| 181 | | |
| 182 | onBeforeDownloadFileProcessing: function (event) { | |
| 183 | const data = event.getParameters().data; | |
| 184 | // change buyer of first row of the root entity | |
| 185 | data.$XYZData[0].buyer = "Customer 123"; | |
| 186 | // change quantity of first row of the Items entity | |
| 187 | data.Items.$XYZData[0].quantity = 4 | |
| 188 | } | |
| 189 | ``` | |
| 190 | | |
| 191 | ## Event `beforeDownloadFileExport` | |
| 192 | | |
| 193 | Parameters: | |
| 194 | | |
| 195 | - `workbook` - the SheetJS [workbook object](https://docs.sheetjs.com/docs/csf/book) | |
| 196 | - `filename` - the filename of the file that will be downloaded | |
| 197 | | |
| 198 | This event is fired just before the file is downloaded. Use this event to manipulate the filename or other parameters before the file is downloaded. | |
| 199 | | |
| 200 | | |
| 201 | | |
| 202 | ### Example | |
| 203 | | |
| 204 | ```javascript | |
| 205 | onDownload: async function () { | |
| 206 | // init your spreadsheet upload component | |
| 207 | this.spreadsheetUpload.attachBeforeDownloadFileExport(this.onBeforeDownloadFileExport, this); | |
| 208 | this.spreadsheetUpload.triggerDownloadSpreadsheet(); | |
| 209 | }, | |
| 210 | | |
| 211 | onBeforeDownloadFileExport: function (event) { | |
| 212 | | |
| 213 | const workbook = event.getParameters().workbook; | |
| 214 | event.getParameters().filename = filename + "_modified"; | |
| 215 | } | |
| 216 | ``` | |
| 217 | | |
| 218 | | |
| 219 | ## Attach async functions to events | |
| 220 | | |
| 221 | You can attach async functions to the events by wrapping the function in a `Promise`. This allows you to send a request to the backend for checks that are not possible in the frontend, for example with a function import. | |
| 222 | | |
| 223 | ```javascript | |
| 224 | // Init spreadsheet upload | |
| 225 | this.spreadsheetUpload = await this.editFlow.getView() | |
| 226 | .getController() | |
| 227 | .getAppComponent() | |
| 228 | .createComponent({ | |
| 229 | usage: "spreadsheetImporter", | |
| 230 | async: true, | |
| 231 | componentData: { | |
| 232 | context: this, | |
| 233 | activateDraft: true | |
| 234 | } | |
| 235 | }); | |
| 236 | | |
| 237 | // Event to check before uploading to app | |
| 238 | this.spreadsheetUpload.attachCheckBeforeRead(async function (event) { | |
| 239 | return new Promise(async (resolve, reject) => { | |
| 240 | // Example | |
| 241 | console.log("Start async wait"); | |
| 242 | await new Promise((resolve) => setTimeout(resolve, 5000)); | |
| 243 | console.log("End async wait"); | |
| 244 | // Don't forget to resolve the promise | |
| 245 | resolve(); | |
| 246 | }); | |
| 247 | }, this); | |
| 248 | ``` | |
| 249 | | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/HowItWorks.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # How it Works | |
| 2 | | |
| 3 | The major advantage of this UI5 component is its universal usability with minimal configuration, and it is independent of the backend implementation. | |
| 4 | This is achieved by reading the files that are already present in the frontend and utilizing the standard UI5 APIs. | |
| 5 | | |
| 6 | ## Technical Background | |
| 7 | | |
| 8 | The UI5 SpreadsheetUpload is built on a reuse component, which requires the definition of componentUsages in the manifest and the usage of createComponent in the code. | |
| 9 | This allows for the use of i18n and a component-preload, which enhances the loading time performance. | |
| 10 | When the component is centrally deployed on an ABAP server, the setup is straightforward. | |
| 11 | | |
| 12 | ## Integration into UI5 | |
| 13 | | |
| 14 | Integrating the component is straightforward as long as the component has access to the context or the view, as without this access, it won't function. | |
| 15 | Upon creation of the component, it searches for a table in the view to utilize the binding for the upload. Other necessary details, such as metadata and draft activation actions, are also derived from the table. If no table or more than two tables are found, the table must be defined in the options. | |
| 16 | | |
| 17 | ## Creating the Template File | |
| 18 | | |
| 19 | By utilizing the metadata, the component can identify the entity of the binding and generate a template file with labels. | |
| 20 | | |
| 21 | ## Extracting the Spreadsheet Files | |
| 22 | | |
| 23 | To avoid sending the entire Spreadsheet file as binary data to the backend, the component utilizes the open-source library [SheetJS](https://sheetjs.com/) to read data from the file. Additionally, Spreadsheet formats are converted to OData formats. With the raw data at hand, the component can utilize the ODataListBinding and [`create`](https://ui5.sap.com/#/api/sap.ui.model.odata.v4.ODataListBinding%23methods/create) to send the data to the backend. | |
| 24 | Since the standard interfaces are used, the key advantage is the independence from the backend scenario, such as CAP or RAP. | |
| 25 | | |
| 26 | The data is sent as a batch, and to prevent a batch from becoming too large, the data is sent in batches of 1,000 by default. | |
| 27 | The size of the batches can be adjusted in the options. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Pro/deepcreate.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | With this feature, you are able to upload a spreadsheet to create multiple entities and their relations. | |
| 2 | | |
| 3 | ## Configuration | |
| 4 | | |
| 5 | | Option | Description | Details | | |
| 6 | | ------ | --- | --- | | |
| 7 | | `operation` | Choose which method to use for uploading data | string | | |
| 8 | | `flatSheet` | Upload data in one sheet or multiple sheets | object | | |
| 9 | | `columns` | Choose which data to upload | object | | |
| 10 | | |
| 11 | ### Sample Usage | |
| 12 | | |
| 13 | ```json | |
| 14 | componentData: { | |
| 15 | context: this, | |
| 16 | pro: { | |
| 17 | operation: "deepCreate", | |
| 18 | deepCreateConfig:{ | |
| 19 | flatSheet:false, | |
| 20 | columns : { | |
| 21 | "OrderNo":{ | |
| 22 | "order": 1, | |
| 23 | "data": "" | |
| 24 | }, | |
| 25 | "buyer": { | |
| 26 | "order": 3, | |
| 27 | "data": "" | |
| 28 | }, | |
| 29 | "Items": { | |
| 30 | "quantity" : { | |
| 31 | "order": 2, | |
| 32 | "data": "" | |
| 33 | }, | |
| 34 | "title": { | |
| 35 | "order": 4, | |
| 36 | "data": "" | |
| 37 | } | |
| 38 | }, | |
| 39 | "Shipping": { | |
| 40 | "address" : { | |
| 41 | "order": 5, | |
| 42 | "data": "" | |
| 43 | }, | |
| 44 | } | |
| 45 | } | |
| 46 | } | |
| 47 | } | |
| 48 | } | |
| 49 | ``` | |
| 50 | | |
| 51 | ### operation | |
| 52 | | |
| 53 | **default:** `create` | |
| 54 | | |
| 55 | Currently available options: `create`, `deepCreate` | |
| 56 | | |
| 57 | This option defines the method to use for uploading data. | |
| 58 | | |
| 59 | ### flatSheet | |
| 60 | | |
| 61 | **default:** `false` | |
| 62 | | |
| 63 | This option determines whether you want to upload data in one sheet or multiple sheets. | |
| 64 | By default, every entity is in a separate sheet. If you want to upload data in one sheet, set this option to `true`. | |
| 65 | | |
| 66 | ### columns | |
| 67 | | |
| 68 | This option determines which data to upload. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Pro/install.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Installing npm Package from GitHub | |
| 2 | | |
| 3 | To install the `@spreadsheetimporter/ui5-cc-spreadsheetimporter-pro` package directly from a GitHub repository, you'll need a personal access token from GitHub and configure npm to use it. Below are the steps: | |
| 4 | | |
| 5 | ## 1. Be a member of the GitHub organization [``](https://github.com/) | |
| 6 | | |
| 7 | 1. After purchasing the Spreadsheet Importer package, you'll receive an invitation to join the GitHub organization. | |
| 8 | 2. If you did not receive an invitation, please contact us at [[email protected]](mailto:[email protected]). | |
| 9 | | |
| 10 | ## 2. Generating a Personal Access Token on GitHub | |
| 11 | | |
| 12 | 1. Navigate to **GitHub** and log in. | |
| 13 | 2. Click on your profile picture (top right) and choose **Settings**. | |
| 14 | 3. In the left sidebar, click on **Developer settings**. | |
| 15 | 4. Choose **Personal access tokens** from the left sidebar. | |
| 16 | 5. Click **Generate new token**. | |
| 17 | 6. Provide a descriptive name for the token in the **Note** field. | |
| 18 | 7. Under **scopes**, select the `repo` and `read:packages` checkboxes to allow access to private repositories and packages. | |
| 19 | 8. Click **Generate token**. | |
| 20 | 9. Copy the generated token. **Note**: This is your only chance to copy the token. If lost, you'll have to create a new one. | |
| 21 | | |
| 22 | ## 3. Configuring npm with `.npmrc` | |
| 23 | | |
| 24 | More Information: [Configuring npm for use with GitHub Packages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry) | |
| 25 | | |
| 26 | You can either set up a project-specific `.npmrc` file or edit the global `~/.npmrc`. | |
| 27 | | |
| 28 | ### For Project-specific Configuration | |
| 29 | | |
| 30 | 1. Go to your project directory. | |
| 31 | 2. Create or open the `.npmrc` file. | |
| 32 | | |
| 33 | ``` | |
| 34 | @spreadsheetimporter:registry=https://npm.pkg.github.com | |
| 35 | //npm.pkg.github.com/:_authToken=YOUR_PERSONAL_ACCESS_TOKEN | |
| 36 | ``` | |
| 37 | | |
| 38 | Replace YOUR_PERSONAL_ACCESS_TOKEN with the token you generated in step 2. | |
| 39 | | |
| 40 | Now you can install the package: | |
| 41 | | |
| 42 | ```bash | |
| 43 | npm install @spreadsheetimporter/ui5-cc-spreadsheetimporter-pro | |
| 44 | ``` | |
| 45 | | |
| 46 | ### For Global Configuration | |
| 47 | | |
| 48 | 1. Open the `~/.npmrc` file. | |
| 49 | 2. Add the following line: | |
| 50 | | |
| 51 | ``` | |
| 52 | @spreadsheetimporter:registry=https://npm.pkg.github.com | |
| 53 | ``` | |
| 54 | | |
| 55 | 3. Save the file. | |
| 56 | | |
| 57 | Now you can install the package: | |
| 58 | | |
| 59 | ```bash | |
| 60 | npm install @spreadsheetimporter/ui5-cc-spreadsheetimporter-pro | |
| 61 | ``` | |
| 62 | | |
| 63 | ## 4. Using the Package | |
| 64 | | |
| 65 | Now you can start using the package in your project. | |
| 66 | For more information, please refer to the [Getting Started Page](./../GettingStarted.md). | |
| 67 | | |
| 68 | The documentation for pro features is here: | |
| 69 | | |
| 70 | - [Spreadsheet Download](./spreadsheetdownload.md) | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/SupportVersions.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Supported Versions | |
| 2 | | |
| 3 | The goal is to test as many versions and environments as possible, especially all versions in long-term maintenance. | |
| 4 | Even though the tests are currently only in CAP, every OData Service, including the `metadata.xml`, is supported. That includes OData Services created with CAP, RAP, and SEGW. That means as long as you are using UI5 version `1.71`, you can also use this in ECC or S/4HANA. | |
| 5 | | |
| 6 | Here is an overview of the apps that were created and passed the smoke test: | |
| 7 | | |
| 8 | Another overview will show you which apps are already tested automatically with wdi5 and are therefore tested on a constant basis. | |
| 9 | | |
| 10 | All the example apps can be found in the [`examples`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/examples/packages) folder. The test scripts can be found in the [`test`](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/examples/test) folder. | |
| 11 | More info at [wdi5 tests](./Development/wdi5.md) | |
| 12 | | |
| 13 | !!! success | |
| 14 | **To summarize: All stable UI5 versions are currently supported (from 1.71). Also, 2.0 is already supported but is still experimental, as version 2.0 may still change. Also every OData Service, including SEGW, RAP, and CAP, is supported.** | |
| 15 | | |
| 16 | ## Apps used for the tests | |
| 17 | | |
| 18 | Here is an overview of the apps that were created and used for the tests. | |
| 19 | | |
| 20 | ### CAP V2 | |
| 21 | | |
| 22 | | [List Report Draft](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2fe/webapp/ext/controller/ListReportExt.controller.js) | [List Report Non Draft](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2fenondraft/webapp/ext/controller/ListReportExt.controller.js) | [Object Page Draft](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2fe/webapp/ext/controller/ObjectPageExt.controller.js) | [Object Page Non Draft](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2fenondraft/webapp/ext/controller/ObjectPageExt.controller.js) | [Freestyle](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv2freestylenondraft/webapp/controller/List.controller.js) | | |
| 23 | |---|---|---|---|---| | |
| 24 | | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| 25 | | |
| 26 | ### CAP V4 | |
| 27 | | |
| 28 | | [List Report Draft](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv4fe/webapp/ext/ListReportExtController.js) | [Object Page Draft](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv4fe/webapp/ext/ObjectPageExtController.js) | Freestyle | [Flexible Programming Model](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv4fpm/webapp/ext/main/Main.controller.js) | [Typescript](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/blob/main/examples/packages/ordersv4fets/webapp/ext/ListReportExtController.ts) | CDS Plugin | | |
| 29 | |---|---|---|---|---|---| | |
| 30 | | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | :white_check_mark: | | :white_check_mark: | | |
| 31 | | |
| 32 | ## wdi5 Tests | |
| 33 | | |
| 34 | ### CAP V2 | |
| 35 | | |
| 36 | | UI5 Version | List Report Draft | List Report Non Draft | Object Page Draft | Object Page Non Draft | Freestyle | OpenUI5 Freestyle | | |
| 37 | |---|---|---|---|---|---|---| | |
| 38 | | 2.0 | | | | | | | | |
| 39 | | 1.120 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| 40 | | 1.108 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| 41 | | 1.96 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| 42 | | 1.84 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| 43 | | 1.71 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| 44 | | |
| 45 | !!! warning | |
| 46 | **OpenUI5**: Draft Activation for V2 in OpenUI5 is not supported. | |
| 47 | | |
| 48 | ### CAP V4 | |
| 49 | | |
| 50 | | UI5 Version | List Report Draft | Object Page Draft | Freestyle | Flexible Programming Model | Typescript | CDS Plugin| | |
| 51 | |---|---|---|---|---|---|---| | |
| 52 | | 2.0 | | | | | | | | |
| 53 | | 1.120 | :white_check_mark: | :white_check_mark: | | |:white_check_mark: |:white_check_mark: | | |
| 54 | | 1.108 | :white_check_mark: | :white_check_mark: | | |:white_check_mark: |:white_check_mark: | | |
| 55 | | 1.96 | :white_check_mark: | :white_check_mark: | | | | | | |
| 56 | | 1.84 | :white_check_mark: | :white_check_mark: | | | | | | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/TableSelector.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Table Selector Implementation Documentation | |
| 2 | | |
| 3 | The `TableSelector` is designed to help users select a table from a list of available tables in a SAP UI5 application. The tables are aggregated and presented in a dialog, from which a user can select a table. | |
| 4 | | |
| 5 | ## Usage | |
| 6 | | |
| 7 | `TableSelector` is triggered when the option `useTableSelector` is set to `true` in the component options and multiple tables are in the view. It will open when you execute `openSpreadsheetUploadDialog`. | |
| 8 | | |
| 9 | ```js | |
| 10 | this.spreadsheetUpload = await this.editFlow.getView().getController().getAppComponent().createComponent({ | |
| 11 | usage: "spreadsheetImporter", | |
| 12 | async: true, | |
| 13 | componentData: { | |
| 14 | context: this, | |
| 15 | useTableSelector: true | |
| 16 | } | |
| 17 | }); | |
| 18 | this.spreadsheetUpload.openSpreadsheetUploadDialog(); | |
| 19 | ``` | |
| 20 | | |
| 21 | ## Custom Options for each Table | |
| 22 | | |
| 23 | If you want to set custom options for each table, you have to trigger the Table Selector before opening the dialog to get the table id with `triggerInitContext()`. | |
| 24 | | |
| 25 | ```js | |
| 26 | this.spreadsheetUpload = await this.editFlow.getView().getController().getAppComponent().createComponent({ | |
| 27 | usage: "spreadsheetImporter", | |
| 28 | async: true, | |
| 29 | componentData: { | |
| 30 | context: this, | |
| 31 | useTableSelector: true | |
| 32 | } | |
| 33 | }); | |
| 34 | // necessary to trigger Table Selector and get tableId | |
| 35 | await this.spreadsheetUpload.triggerInitContext(); | |
| 36 | const selectedTable = this.spreadsheetUpload.getTableId(); | |
| 37 | ``` | |
| 38 | | |
| 39 | The method `getTableId` returns the table id of the selected table. With the id, you can set custom options for each table. | |
| 40 | | |
| 41 | ### Full Example | |
| 42 | | |
| 43 | Make sure you check if the user selected a table with `if (selectedTable)`. | |
| 44 | | |
| 45 | ```js | |
| 46 | openSpreadsheetUploadDialog: async function (event) { | |
| 47 | let spreadsheetImporterOptions; | |
| 48 | this.editFlow.getView().setBusyIndicatorDelay(0); | |
| 49 | this.editFlow.getView().setBusy(true); | |
| 50 | // prettier-ignore | |
| 51 | this.spreadsheetUpload = await this.editFlow.getView() | |
| 52 | .getController() | |
| 53 | .getAppComponent() | |
| 54 | .createComponent({ | |
| 55 | usage: "spreadsheetImporter", | |
| 56 | async: true, | |
| 57 | componentData: { | |
| 58 | context: this, | |
| 59 | useTableSelector: true | |
| 60 | } | |
| 61 | }); | |
| 62 | // necessary to trigger Table Selector and get tableId | |
| 63 | await this.spreadsheetUpload.triggerInitContext(); | |
| 64 | const selectedTable = this.spreadsheetUpload.getTableId(); | |
| 65 | if (selectedTable) { | |
| 66 | // not necessary to have specific options for each table, but possible to set options for specific tables | |
| 67 | // check if selectedTable is available, if not, the user clicked on cancel | |
| 68 | if (selectedTable === "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable") { | |
| 69 | spreadsheetImporterOptions = { | |
| 70 | context: this, | |
| 71 | tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable", | |
| 72 | columns: ["product_ID", "quantity", "title", "price", "validFrom", "timestamp", "date", "time", "boolean", "decimal"], | |
| 73 | mandatoryFields: ["product_ID", "quantity"], | |
| 74 | spreadsheetFileName: "Test.xlsx", | |
| 75 | hidePreview: true, | |
| 76 | sampleData: [{ | |
| 77 | product_ID: "HT-1000", | |
| 78 | quantity: 1, | |
| 79 | title: "Notebook Basic 15", | |
| 80 | price: 956, | |
| 81 | validFrom: new Date(), | |
| 82 | timestamp: new Date(), | |
| 83 | date: new Date(), | |
| 84 | time: new Date(), | |
| 85 | boolean: true, | |
| 86 | decimal: 1.1 | |
| 87 | }] | |
| 88 | }; | |
| 89 | } | |
| 90 | if (selectedTable === "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Shipping::LineItem-innerTable") { | |
| 91 | spreadsheetImporterOptions = { | |
| 92 | context: this, | |
| 93 | tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Shipping::LineItem-innerTable" | |
| 94 | }; | |
| 95 | } | |
| 96 | | |
| 97 | // possible to open dialog with options, option not necessary | |
| 98 | this.spreadsheetUpload.openSpreadsheetUploadDialog(spreadsheetImporterOptions); | |
| 99 | } | |
| 100 | this.editFlow.getView().setBusy(false); | |
| 101 | }, | |
| 102 | ``` | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Troubleshooting.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Issues | |
| 2 | | |
| 3 | If you encounter any issues that are not covered in this documentation, have suggestions, or ideas for improvements, please create an issue in the GitHub repository: | |
| 4 | [https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/issues](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/issues) | |
| 5 | | |
| 6 | ## Activate Debug Mode and Copy Error Messages | |
| 7 | | |
| 8 | ### Activate Debug Mode | |
| 9 | | |
| 10 | To activate the debug mode, you need to add the following parameter to the URL: | |
| 11 | `?sap-ui-debug=true` | |
| 12 | | |
| 13 | Alternatively, you can set the [`debug`](Configuration.md#debug) parameter to `true` during the initialization of the Spreadsheet Upload component. | |
| 14 | However, this can only be done if the component can be opened, so it's preferable to use the URL parameter if possible. | |
| 15 | | |
| 16 | ```js | |
| 17 | this.spreadsheetUpload = await this.getView().getController().getAppComponent().createComponent({ | |
| 18 | usage: "spreadsheetImporter", | |
| 19 | async: true, | |
| 20 | componentData: { | |
| 21 | context: this, | |
| 22 | debug: true | |
| 23 | } | |
| 24 | }); | |
| 25 | ``` | |
| 26 | | |
| 27 | ### Copy Console Messages | |
| 28 | | |
| 29 | After activating the debug mode, you can copy the console messages from the browser console. | |
| 30 | Make sure to do this after reproducing the error. | |
| 31 | | |
| 32 | 1. Open the browser console | |
| 33 | 2. Right-click on a message and select "Save as" | |
| 34 | | |
| 35 | Save the messages to a file and use it to report an issue. | |
| 36 | Alternatively, you can try selecting all the messages and copying them to a text file. | |
| 37 | | |
| 38 | ## Error: `script load error` | |
| 39 | | |
| 40 | If you receive an error similar to the following: | |
| 41 | | |
| 42 | ``` | |
| 43 | ui5loader-dbg.js:1042 Uncaught (in promise) ModuleError: failed to load 'cc/spreadsheetimporter/v1_7_3/Component.js' from resources/cc/spreadsheetimporter/v1_7_3/Component.js: script load error | |
| 44 | ``` | |
| 45 | | |
| 46 | Since the component is designed to always use a specific version, you must ensure that the correct version is used after an update. | |
| 47 | In this example, it appears that the installed version does not match the version defined in the manifest file. | |
| 48 | The application is trying to load version "0.16.3", but the installed version is "0.16.4". | |
| 49 | See the configurations for this version below: | |
| 50 | | |
| 51 | ### package.json | |
| 52 | | |
| 53 | ```json | |
| 54 | "dependencies": { | |
| 55 | "ui5-cc-spreadsheetimporter": "0.16.4" | |
| 56 | } | |
| 57 | ``` | |
| 58 | | |
| 59 | ### manifest.json | |
| 60 | | |
| 61 | !!! warning "" | |
| 62 | ⚠️ The `resourceRoots` path "./thirdparty/customcontrol/spreadsheetimporter/v1_7_3" changed from version 0.34.0 to lowercase. Please make sure to use the correct path. | |
| 63 | | |
| 64 | | |
| 65 | ```json | |
| 66 | "componentUsages": { | |
| 67 | "spreadsheetImporter": { | |
| 68 | "name": "cc.spreadsheetimporter.v1_7_3" | |
| 69 | } | |
| 70 | }, | |
| 71 | "resourceRoots": { | |
| 72 | "cc.spreadsheetimporter.v1_7_3": "./thirdparty/customcontrol/spreadsheetimporter/v1_7_3" | |
| 73 | } | |
| 74 | ``` | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Typescript.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | Since the component is written in TypeScript, we can also provide the generated types. | |
| 2 | | |
| 3 | The GitHub repository contains a sample TypeScript application created with the Fiori Generator. You can find the example app in the [example folder](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/examples/packages/ordersv4fets). | |
| 4 | | |
| 5 | ## Setup | |
| 6 | | |
| 7 | Generate an app with the Fiori Tools Generator in TypeScript or use the [Easy UI5 TS Generator](https://github.com/ui5-community/generator-ui5-ts-app). | |
| 8 | | |
| 9 | ### ts-config.json | |
| 10 | | |
| 11 | You can consume the types from the `@sapui5/ts-types-esm` and the `ui5-cc-spreadsheetimporter` package. | |
| 12 | | |
| 13 | ```json | |
| 14 | "types": ["@sapui5/ts-types-esm", "ui5-cc-spreadsheetimporter"], | |
| 15 | "typeRoots": ["./node_modules"] | |
| 16 | ``` | |
| 17 | | |
| 18 | ### manifest.json | |
| 19 | | |
| 20 | Add the component usage and the resource roots to the manifest.json as described in the [Getting Started](GettingStarted.md) section. | |
| 21 | | |
| 22 | !!! warning "" | |
| 23 | ⚠️ The `resourceRoots` path "./thirdparty/customcontrol/spreadsheetimporter/v1_7_3" changed from version 0.34.0 to lowercase. Please make sure to use the correct path. | |
| 24 | | |
| 25 | ```json | |
| 26 | "componentUsages": { | |
| 27 | "spreadsheetImporter": { | |
| 28 | "name": "cc.spreadsheetimporter.v1_7_3" | |
| 29 | } | |
| 30 | }, | |
| 31 | "resourceRoots": { | |
| 32 | "cc.spreadsheetimporter.v1_7_3": "./thirdparty/customcontrol/spreadsheetimporter/v1_7_3" | |
| 33 | }, | |
| 34 | ``` | |
| 35 | | |
| 36 | ### Custom Action | |
| 37 | | |
| 38 | This is an example of how you could create the component and attach an event handler to the `checkBeforeRead` event with the types `Component` and `Component$CheckBeforeReadEventParameters` for the event parameters with an OData V4 Fiori Elements Application and UI5 Version 1.116. | |
| 39 | | |
| 40 | ```typescript | |
| 41 | import Component, { Component$ChangeBeforeCreateEvent, Component$CheckBeforeReadEvent, Component$UploadButtonPressEvent } from "cc/spreadsheetimporter/v1_7_3/Component"; | |
| 42 | import BaseController from "sap/fe/core/BaseController"; | |
| 43 | import ExtensionAPI from "sap/fe/core/ExtensionAPI"; | |
| 44 | | |
| 45 | export async function openSpreadsheetUploadDialog(this: ExtensionAPI) { | |
| 46 | const view = this.getRouting().getView(); | |
| 47 | const controller = view.getController() as BaseController; | |
| 48 | view.setBusyIndicatorDelay(0); | |
| 49 | view.setBusy(true); | |
| 50 | const spreadsheetUpload = (await controller.getAppComponent().createComponent({ | |
| 51 | usage: "spreadsheetImporter", | |
| 52 | async: true, | |
| 53 | componentData: { | |
| 54 | context: this, | |
| 55 | activateDraft: true | |
| 56 | } | |
| 57 | })) as Component; | |
| 58 | // event to check before uploaded to app | |
| 59 | spreadsheetUpload.attachCheckBeforeRead(function (event: Component$CheckBeforeReadEvent) { | |
| 60 | // example | |
| 61 | const sheetData = event.getParameter("sheetData") as any; | |
| 62 | event.getParameters().messages; | |
| 63 | let errorArray = []; | |
| 64 | for (const [index, row] of sheetData.entries()) { | |
| 65 | //check for invalid price | |
| 66 | for (const key in row) { | |
| 67 | if (key.endsWith("[price]") && row[key].rawValue > 100) { | |
| 68 | const error = { | |
| 69 | title: "Price too high (max 100)", | |
| 70 | row: index + 2, | |
| 71 | group: true, | |
| 72 | rawValue: row[key].rawValue, | |
| 73 | ui5type: "Error" | |
| 74 | }; | |
| 75 | errorArray.push(error); | |
| 76 | } | |
| 77 | } | |
| 78 | } | |
| 79 | (event.getSource() as Component).addArrayToMessages(errorArray); | |
| 80 | }, this); | |
| 81 | | |
| 82 | // event example to prevent uploading data to backend | |
| 83 | spreadsheetUpload.attachUploadButtonPress(function (event: Component$UploadButtonPressEvent) { | |
| 84 | //event.preventDefault(); | |
| 85 | //event.getParameter("payload"); | |
| 86 | }, this); | |
| 87 | | |
| 88 | // event to change data before send to backend | |
| 89 | spreadsheetUpload.attachChangeBeforeCreate(function (event: Component$ChangeBeforeCreateEvent) { | |
| 90 | let payload = event.getParameter("payload"); | |
| 91 | // round number from 12,56 to 12,6 | |
| 92 | if (payload.price) { | |
| 93 | payload.price = Number(payload.price).toFixed(1) | |
| 94 | } | |
| 95 | return payload; | |
| 96 | }, this); | |
| 97 | spreadsheetUpload.openSpreadsheetUploadDialog(); | |
| 98 | view.setBusy(false); | |
| 99 | } | |
| 100 | ``` | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/Update.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | !!! warning | |
| 2 | This feature is currently experimental and may not work as expected. | |
| 3 | Also only available for OData V4. | |
| 4 | Please provide feedback: https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/issues | |
| 5 | | |
| 6 | | |
| 7 | ## Usage | |
| 8 | | |
| 9 | It is recommended, especially for Entities with GUID, to first download the data with the Spreadsheet Importer and include the keys. | |
| 10 | | |
| 11 | 1. Download the data with the Spreadsheet Importer and include the keys. | |
| 12 | 2. Edit the spreadsheet | |
| 13 | 3. Upload the data with the Spreadsheet Importer. | |
| 14 | | |
| 15 | ## Getting started | |
| 16 | | |
| 17 | The minimal configuration to update entities instead of creating is: | |
| 18 | | |
| 19 | ```js | |
| 20 | this.spreadsheetUploadUpdate = await this.editFlow.getView().getController().getAppComponent() | |
| 21 | .createComponent({ | |
| 22 | usage: "spreadsheetImporter", | |
| 23 | async: true, | |
| 24 | componentData: { | |
| 25 | context: this, | |
| 26 | tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable", | |
| 27 | action: "UPDATE", | |
| 28 | deepDownloadConfig: { | |
| 29 | addKeysToExport: true, | |
| 30 | showOptions: false, | |
| 31 | filename: "Items" | |
| 32 | }, | |
| 33 | showDownloadButton: true | |
| 34 | } | |
| 35 | }); | |
| 36 | this.spreadsheetUploadUpdate.openSpreadsheetUploadDialog(); | |
| 37 | ``` | |
| 38 | | |
| 39 | This configuration will show the download button and download all the available data for the referenced table. | |
| 40 | When you press the download button, the data will be downloaded including the keys necessary for the update. | |
| 41 | | |
| 42 | You can then change the data in the spreadsheet and upload the data again. | |
| 43 | By default, only the changed properties are updated (partial update). You can change this by setting the `fullUpdate` property to `true` (see [Configuration](#configuration) below). | |
| 44 | | |
| 45 | ## How it works | |
| 46 | | |
| 47 | When you upload the file to the App, it will do the usual checks if the columns are in the data model and the data is valid (see [Checks](./Checks.md)). | |
| 48 | When the user presses the upload button, it will fetch all the data in the batch. To make sure all the data is fetched, it will fetch the data for active and draft separately. So for every batch a ListBinding is created and two requests are made with filters for the keys and for `IsActiveEntity = true` and `IsActiveEntity = false`. This is needed because of the separation of active and draft (see [why requests fail in CAP with OData draft enabled](https://cap.cloud.sap/docs/get-started/troubleshooting#why-do-some-requests-fail-if-i-set-odata-draft-enabled-on-my-entity)). | |
| 49 | | |
| 50 | This data is used to determine if the object is in draft or active mode, if the object exists at all, and for partial updates whether a field is changed. | |
| 51 | | |
| 52 | For every change, an `ODataContextBinding` is created and the data is updated. | |
| 53 | | |
| 54 | ## Things to consider / Drawbacks | |
| 55 | | |
| 56 | ### IsActiveEntity handling | |
| 57 | | |
| 58 | The column `IsActiveEntity` states the current state of the object (Draft or Active). In the spreadsheet file, the current state must match the state of the object. | |
| 59 | | |
| 60 | • If the state is wrong, a warning is shown and the user can still continue. If the user continues, the object will be updated in the current state that the object is actually in. | |
| 61 | For example, if in the spreadsheet file the `IsActiveEntity` column is set to `true` but the object is in draft mode, a warning will be shown, and if the user continues, the draft object will be updated with the data from the spreadsheet. | |
| 62 | | |
| 63 | ### Download only Active Entities | |
| 64 | | |
| 65 | If you download the data with the Spreadsheet Importer, only the active entities are downloaded. If you then update the data, the object will be updated in the current state that the object is in. | |
| 66 | So if the object is in draft mode, the data from the active state will still be used to update the draft object. | |
| 67 | | |
| 68 | ### Performance and batch size | |
| 69 | | |
| 70 | Because the update needs extra requests (fetch of active and draft objects, plus partial updates), the update is slower than a create operation. For mass updates, this can take some time. | |
| 71 | Because of the performance considerations, the batch size for update is limited to 100 per batch. | |
| 72 | | |
| 73 | ### Filter Limitations | |
| 74 | | |
| 75 | When exporting the data, currently all the data is exported. Any filters in a List Report are not respected at the moment. | |
| 76 | | |
| 77 | ## Configuration | |
| 78 | | |
| 79 | Below is a brief overview of the main configuration options relevant to updating. For the complete list, see the [Configuration documentation](./Configuration.md). | |
| 80 | | |
| 81 | | Option | Description | Default | | |
| 82 | | --------------- | ------------------------------------------------------------------------------ | ------- | | |
| 83 | | `fullUpdate` | Update all properties of the object (true). If false, only changed properties are updated. | false | | |
| 84 | | `columns` | Columns to update. | all | | |
| 85 | | |
| 86 | ### fullUpdate | |
| 87 | | |
| 88 | If `fullUpdate` is set to `true`, the component updates all properties of the object. | |
| 89 | If `fullUpdate` is set to `false` (default), only the properties that have changed are updated (partial update). | |
| 90 | | |
| 91 | ### columns | |
| 92 | | |
| 93 | The `columns` property is an array of strings. The strings are the names of the columns that should be updated. | |
| 94 | Columns that are not in the array will not be updated at all. This is useful if you only want to update a subset of properties. | |
| 95 | | |
| 96 | When using `fullUpdate = true`, the system will still honor `columns`—only those columns listed will be sent in the update request. For unlisted columns, no updates will be sent. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/UseCases.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Use Cases | |
| 2 | | |
| 3 | ## Mass Creation | |
| 4 | | |
| 5 | It is possible to use the Spreadsheet Component with big data sets and spreadsheet files. | |
| 6 | For example, we timed the upload of 10,000 rows. | |
| 7 | | |
| 8 | | Scenario | Rows | Time | | |
| 9 | |---|---|---| | |
| 10 | | CAP, Object Page, Draft Mode, Remote CAP Server | 10,090 | 01 Minute 28 seconds | | |
| 11 | | CAP, Object Page, Draft Mode, Local CAP Server | 10,090 | 17 seconds | | |
| 12 | | CAP, List Report, Draft Mode incl. Draft Activation, Remote CAP Server | 10,153 | 03 Minutes 49 seconds | | |
| 13 | | CAP, List Report, Draft Mode incl. Draft Activation, Local CAP Server | 10,153 | 50 seconds | | |
| 14 | | |
| 15 | Obviously, the time depends on the network speed and the server performance. | |
| 16 | Also, if you use draft activation, the time will increase as the draft activation takes some time. | |
| 17 | The times are measured with the default batch size of 1000 rows. Changing the batch did not improve performance in our tests. Setting the batch size too high or disabling the batch upload can lead to memory issues and server errors. | |
| 18 | | |
| 19 | ## Quick Data Entry for Custom Tables with Fiori Elements | |
| 20 | One of the advantages of the UI5 SpreadsheetUpload component is its ability to quickly add data to custom tables in combination with Fiori Elements. | |
| 21 | | |
| 22 | With the template file in hand, users can simply fill in the necessary data in the Spreadsheet and then use the UI5 SpreadsheetUpload component to upload the data to the custom table in SAP. This can be a huge time-saver for projects that require frequent data entry, such as inventory management or order processing. | |
| 23 | | |
| 24 | In addition to simplifying the data entry process, the UI5 SpreadsheetUpload component also allows for advanced data validation and manipulation. For example, developers can define event handlers to check data for errors before it is uploaded or to transform data to conform to the target data model. | |
| 25 | | |
| 26 | ## Data Migration | |
| 27 | Data migration is a common scenario where the UI5 SpreadsheetUpload component can be useful. Companies often need to migrate data between systems, and a Spreadsheet is a common format for storing data. The component can simplify the process of uploading large Spreadsheet files containing data for migration. Especially useful is the export feature of tables in Fiori Elements, as the Spreadsheet export files can usually be uploaded again directly. | |
| 28 | | |
| 29 | When using the UI5 SpreadsheetUpload component for data migration, there are a few things to keep in mind. First, since the component reads the Spreadsheet files on the frontend, it's important to ensure that the files are properly formatted and contain the correct data before uploading them to the system. Second, since the data is sent in batches, it's important to adjust the batch size accordingly to prevent the batch from becoming too large and causing issues during migration. Finally, it's important to ensure that the data is properly validated and processed on the backend to ensure that it is accurately and securely migrated to the target system. | |
| 30 | | |
| 31 | ## Data Validation and Reporting | |
| 32 | In addition to data migration, the UI5 SpreadsheetUpload component can also be useful for data validation and reporting scenarios. Companies may need to validate data in Spreadsheet files before uploading it to the system or generate reports from data in Spreadsheet files. The component can allow users to upload Spreadsheet files containing data for validation or reporting purposes, and the data can be processed and validated on the backend to ensure accuracy and security. | |
| 33 | | |
| 34 | ## Data Entry | |
| 35 | In some cases, users may prefer to enter data into a Spreadsheet file rather than using a web form or UI5 app. The UI5 SpreadsheetUpload component can be used to allow users to upload Spreadsheet files containing data for entry into the system. This can be useful in scenarios where users are more comfortable working with Spreadsheets or need to enter large amounts of data quickly. | |
| 36 | | |
| 37 | ## Custom Integrations | |
| 38 | Finally, the UI5 SpreadsheetUpload component can be used to build custom integrations with other systems that use Spreadsheet files as a data interchange format. For example, the component could be used to allow users to upload Spreadsheet files to a cloud storage service like Dropbox or Google Drive or to integrate with other third-party systems that use Spreadsheet files for data exchange. With the ability to read and write Spreadsheet files in the frontend, the possibilities for custom integrations are nearly endless. | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/UserDocumentation.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # User Documentation | |
| 2 | | |
| 3 | This User Documentation provides a brief overview of the Fiori Element Object Page scenario and uploading Order Items. | |
| 4 | | |
| 5 | 1\. Open the Spreadsheet Upload Dialog | |
| 6 | | |
| 7 | { loading=lazy } | |
| 8 | | |
| 9 | 2\. Download the Template | |
| 10 | | |
| 11 | To ensure a smooth process without errors, it is recommended to always download a new template. However, if you are confident that the data structure has not changed, you may use a previously downloaded template. | |
| 12 | | |
| 13 | { loading=lazy } | |
| 14 | | |
| 15 | 3\. Fill out the template | |
| 16 | | |
| 17 | Now, fill the template with the necessary data and save the file. | |
| 18 | | |
| 19 | { loading=lazy } | |
| 20 | | |
| 21 | 4\. Upload File to Application | |
| 22 | | |
| 23 | Click on the "Browse" button to upload the file. | |
| 24 | If the upload is successful, a message will appear saying "Upload Successful." | |
| 25 | | |
| 26 | { loading=lazy } | |
| 27 | | |
| 28 | 5\. Error Dialog | |
| 29 | | |
| 30 | During the upload process, various checks are performed in the background. If any errors are encountered, such as unfilled mandatory fields, they will be displayed in the error dialog. | |
| 31 | | |
| 32 | { loading=lazy } | |
| 33 | | |
| 34 | 6\. Send Data to Backend | |
| 35 | | |
| 36 | If no errors appear, click the "Upload" button to send the data to the backend. | |
| 37 | | |
| 38 | { loading=lazy } | |
| -------------------------------------------------------------------------------- | |
| /docs/pages/spreadsheetdownload.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Spreadsheet Deep Download | |
| 2 | | |
| 3 | !!! warning | |
| 4 | This new feature is experimental and may change in the future and currently only available for OData V4. | |
| 5 | If you deep download data, the OData Service need to support `expand`. | |
| 6 | | |
| 7 | This feature downloads data from the backend and converts it to a Spreadsheet file. | |
| 8 | | |
| 9 | The main difference between this feature and the integrated Spreadsheet Download is that it can download data from multiple sub-entities at once. | |
| 10 | | |
| 11 | For example, if you have Orders and OrderItems, you can download both entities at once and the data will be structured in the Spreadsheet. It is also possible to recursively download data from multiple entities indefinitely. | |
| 12 | | |
| 13 | - Orders | |
| 14 | - OrderItems | |
| 15 | - Info | |
| 16 | - ShippingInfos | |
| 17 | | |
| 18 | This means that you can download all Orders, including the OrderItems, ShippingInfos, and the Info of the OrderItems in one go. | |
| 19 | | |
| 20 | ## Configuration | |
| 21 | | |
| 22 | | Option | Description | Default | Type | | |
| 23 | | ------ | ----------- | ------- | ---- | | |
| 24 | | `addKeysToExport` | Adds keys to the export file | `false` | boolean | | |
| 25 | | `setDraftStatus` | Sets the draft status in `IsActiveEntity` | `false` | boolean | | |
| 26 | | `filename` | Defines the filename for the export file | Entity Name | string | | |
| 27 | | `deepExport` | Turn on to export of sibling entities | `false` | boolean | | |
| 28 | | `deepLevel` | Defines the level of sibling entities to export | `0` | number | | |
| 29 | | `showOptions` | Shows options dialog for users | `false` | boolean | | |
| 30 | | `columns` | Defines the columns to export | `{}` | object or array | | |
| 31 | | |
| 32 | ### Sample Usage | |
| 33 | | |
| 34 | The configuration is done in the `deepDownloadConfig` section of the component data: | |
| 35 | | |
| 36 | ```json | |
| 37 | componentData: { | |
| 38 | context: this, | |
| 39 | showDownloadButton: true, | |
| 40 | deepDownloadConfig: { | |
| 41 | deepLevel: 1, | |
| 42 | deepExport: false, | |
| 43 | addKeysToExport: false, | |
| 44 | showOptions: false, | |
| 45 | columns : { | |
| 46 | "OrderNo":{ | |
| 47 | "order": 1 | |
| 48 | }, | |
| 49 | "buyer": { | |
| 50 | "order": 3 | |
| 51 | }, | |
| 52 | "Items": { | |
| 53 | "quantity" : { | |
| 54 | "order": 2 | |
| 55 | }, | |
| 56 | "title": { | |
| 57 | "order": 4 | |
| 58 | } | |
| 59 | }, | |
| 60 | "Shipping": { | |
| 61 | "address" : { | |
| 62 | "order": 5 | |
| 63 | }, | |
| 64 | } | |
| 65 | } | |
| 66 | } | |
| 67 | } | |
| 68 | ``` | |
| 69 | | |
| 70 | ### columns | |
| 71 | | |
| 72 | **default:** `{}` | |
| 73 | | |
| 74 | This option defines the columns to export. | |
| 75 | By default, all columns are exported. | |
| 76 | It is possible to define the order of the columns by using the `order` property. | |
| 77 | To know which columns are available, it is possible to turn on `debug` as an option. | |
| 78 | With this option, the `mainEntity` is logged and then you can use it as a reference. | |
| 79 | | |
| 80 | It is also possible to define the columns as an array of strings but only if the deepLevel is 0. | |
| 81 | | |
| 82 | ### addKeysToExport | |
| 83 | | |
| 84 | **default:** `false` | |
| 85 | | |
| 86 | This option adds keys (which can be hidden in the UI) to the export, such as GUIDs. | |
| 87 | | |
| 88 | ### setDraftStatus | |
| 89 | | |
| 90 | **default:** `true` | |
| 91 | | |
| 92 | If the option `addKeysToExport` is set to `true` the keys, including the `IsActiveEntity` field, are added to the export. | |
| 93 | By default, the field `IsActiveEntity` is set to `false` if the field `HasDraftEntity` is set to `true`. | |
| 94 | This makes it easier to identify which rows have a draft entity for the reupload to update the entities. | |
| 95 | This option is only used if all the `IsActiveEntity` fields should be set to `true` in the export. | |
| 96 | | |
| 97 | | |
| 98 | ### filename | |
| 99 | | |
| 100 | **default:** Entity Name of Root Entity | |
| 101 | | |
| 102 | This option defines the filename for the export XLSX file. | |
| 103 | | |
| 104 | ### deepExport | |
| 105 | | |
| 106 | **default:** `false` | |
| 107 | | |
| 108 | This option determines whether sibling entities should be exported as well. | |
| 109 | If the deepExport is set to `false`, the deepLevel is set to `0`. | |
| 110 | | |
| 111 | ### deepLevel | |
| 112 | | |
| 113 | **default:** `0` | |
| 114 | | |
| 115 | This option defines how deep sibling entities should be exported. | |
| 116 | If the deepLevel is greater than 0, the columns option must a object. | |
| 117 | If the deepLevel is 0, `deepExport` is set to `false`. | |
| 118 | If the deepLevel is greater than 0, `deepExport` is set to `true`. | |
| 119 | | |
| 120 | ### showOptions | |
| 121 | | |
| 122 | **default:** `true` | |
| 123 | | |
| 124 | This option determines whether the options dialog should be shown to the user. | |
| 125 | | |
| 126 | ## API | |
| 127 | | |
| 128 | ### triggerDownloadSpreadsheet | |
| 129 | | |
| 130 | This method triggers the Spreadsheet Download without the need to open the spreadsheet upload dialog. | |
| 131 | The input parameter for `triggerDownloadSpreadsheet` is the same as the configuration for the `deepDownloadConfig`. | |
| 132 | You can overwrite the configuration from the component data by passing the configuration to the `triggerDownloadSpreadsheet` method. | |
| 133 | | |
| 134 | ```js | |
| 135 | download: async function () { | |
| 136 | this.spreadsheetUpload = await this.editFlow | |
| 137 | .getView() | |
| 138 | .getController() | |
| 139 | .getAppComponent() | |
| 140 | .createComponent({ | |
| 141 | usage: "spreadsheetImporter", | |
| 142 | async: true, | |
| 143 | componentData: { | |
| 144 | context: this, | |
| 145 | activateDraft: true, | |
| 146 | deepDownloadConfig: { | |
| 147 | deepLevel: 1, | |
| 148 | deepExport: true, | |
| 149 | addKeysToExport: true | |
| 150 | } | |
| 151 | } | |
| 152 | }); | |
| 153 | this.spreadsheetUpload.triggerDownloadSpreadsheet({ | |
| 154 | deepLevel: 2, | |
| 155 | deepExport: true, | |
| 156 | addKeysToExport: true | |
| 157 | }); | |
| 158 | } | |
| 159 | ``` | |
| 160 | | |
| 161 | ## Events | |
| 162 | | |
| 163 | See more details at [Events](Events.md#event-beforedownloadfileprocessing) | |
| 164 | | |
| 165 | ### `beforeDownloadFileProcessing` | |
| 166 | | |
| 167 | This event is fired before the data is converted to a spreadsheet file. Use this event to manipulate the data before it is converted. | |
| 168 | You can change directly the data parameter of the event as this is a reference to the data. | |
| 169 | | |
| 170 | ### `beforeDownloadFileExport` | |
| 171 | | |
| 172 | This event is fired just before the file is downloaded. Use this event to manipulate the filename or other parameters before the file is downloaded. | |
| 173 | | |
| 174 | ## Download Button | |
| 175 | | |
| 176 | Same as the [Button control](./Button.md), the Download Button can be used to trigger the Spreadsheet Download directly in the XML View. | |
| 177 | A sample usage can be found [here](https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/tree/main/examples/packages/ordersv4freestyle). | |
| 178 | For the configuration options, see [componentContainerData Configuration](./Configuration.md#componentcontainerdata). | |
| 179 | | |
| 180 | A usage can look like this: | |
| 181 | | |
| 182 | ```xml | |
| 183 | <mvc:View controllerName="ordersv4freestyle.controller.MainView" | |
| 184 | xmlns:mvc="sap.ui.core.mvc" xmlns:core="sap.ui.core" displayBlock="true" | |
| 185 | xmlns="sap.m"> | |
| 186 | <Page id="page" title="{i18n>title}"> | |
| 187 | <content> | |
| 188 | <VBox> | |
| 189 | <core:ComponentContainer | |
| 190 | id="test" | |
| 191 | width="100%" | |
| 192 | usage="spreadsheetImporter" | |
| 193 | propagateModel="true" | |
| 194 | async="true" | |
| 195 | settings="{ | |
| 196 | componentContainerData:{uploadButtonPress:'uploadButtonPress',buttonText:'Excel Download',buttonId:'downloadButton',downloadButton:true}, | |
| 197 | deepDownloadConfig:{ | |
| 198 | deepLevel: 0, | |
| 199 | deepExport: false, | |
| 200 | addKeysToExport: true, | |
| 201 | showOptions: false, | |
| 202 | filename: 'Orders12', | |
| 203 | columns : ["OrderNo"] | |
| 204 | } | |
| 205 | } | |
| 206 | }" | |
| 207 | /> | |
| 208 | <Button id="downloadButtonCode" text="Excel Download Code" press="onDownload"/> | |
| 209 | <Table id="ordersTable" items="{/Orders}"> | |
| 210 | <columns> | |
| 211 | <Column> | |
| 212 | <Text text="Order Number"/> | |
| 213 | </Column> | |
| 214 | <Column> | |
| 215 | <Text text="Buyer"/> | |
| 216 | </Column> | |
| 217 | <Column> | |
| 218 | <Text text="Created At"/> | |
| 219 | </Column> | |
| 220 | </columns> | |
| 221 | <items> | |
| 222 | <ColumnListItem> | |
| 223 | <cells> | |
| 224 | <Text text="{OrderNo}"/> | |
| 225 | <Text text="{buyer}"/> | |
| 226 | <Text text="{createdAt}"/> | |
| 227 | </cells> | |
| 228 | </ColumnListItem> | |
| 229 | </items> | |
| 230 | </Table> | |
| 231 | </VBox> | |
| 232 | </content> | |
| 233 | </Page> | |
| 234 | </mvc:View> | |
| 235 | | |
| 236 | ``` | |
| 237 | | |
| -------------------------------------------------------------------------------- | |
| /examples/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # Sample Setup for `ui5-cc-spreadsheetimporter` | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/anyupload/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Fri Aug 23 2024 14:43:55 GMT+0200 (Mitteleuropäische Sommerzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-elements| | |
| 6 | |**App Generator Version**<br>1.12.4| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>Custom Page V4| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/odata/v4/orders/ | |
| 11 | |**Module Name**<br>anyupload| | |
| 12 | |**Application Title**<br>Upload Spreadsheet to any OData API| | |
| 13 | |**Namespace**<br>com.spreadsheetimporter| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.127.1| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>True| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Main Entity**<br>Orders| | |
| 20 | | |
| 21 | ## anyupload | |
| 22 | | |
| 23 | | |
| 24 | | |
| 25 | ### Starting the generated app | |
| 26 | | |
| 27 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 28 | | |
| 29 | ``` | |
| 30 | npm start | |
| 31 | ``` | |
| 32 | | |
| 33 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 34 | | |
| 35 | ``` | |
| 36 | npm run start-mock | |
| 37 | ``` | |
| 38 | | |
| 39 | #### Pre-requisites: | |
| 40 | | |
| 41 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 42 | | |
| 43 | | |
| 44 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/anyupload/webapp/Component.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import BaseComponent from "sap/fe/core/AppComponent"; | |
| 2 | | |
| 3 | /** | |
| 4 | * @namespace com.spreadsheetimporter.anyupload | |
| 5 | */ | |
| 6 | export default class Component extends BaseComponent { | |
| 7 | | |
| 8 | public static metadata = { | |
| 9 | manifest: "json" | |
| 10 | }; | |
| 11 | | |
| 12 | /** | |
| 13 | * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. | |
| 14 | * @public | |
| 15 | * @override | |
| 16 | */ | |
| 17 | //public init() : void { | |
| 18 | // super.init(); | |
| 19 | //} | |
| 20 | } | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/anyupload/webapp/ext/main/Main.controller.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Controller from "sap/fe/core/PageController"; | |
| 2 | import Column from "sap/m/Column"; | |
| 3 | import Text from "sap/m/Text"; | |
| 4 | import ColumnListItem from "sap/m/ColumnListItem"; | |
| 5 | import Select, { Select$ChangeEventParameters } from "sap/m/Select"; | |
| 6 | import Table from "sap/m/Table"; | |
| 7 | import JSONModel from "sap/ui/model/json/JSONModel"; | |
| 8 | import ODataModelV4 from "sap/ui/model/odata/v4/ODataModel"; | |
| 9 | import ODataModelV2 from "sap/ui/model/odata/v2/ODataModel"; | |
| 10 | import Button from "sap/m/Button"; | |
| 11 | import MessageStrip, { MessageType } from "sap/m/MessageStrip"; | |
| 12 | | |
| 13 | | |
| 14 | /** | |
| 15 | * @namespace com.spreadsheetimporter.anyupload.ext.main.Main.controller | |
| 16 | */ | |
| 17 | export default class Main extends Controller { | |
| 18 | private selectedService: ODataModel; | |
| 19 | | |
| 20 | /** | |
| 21 | * Called when a controller is instantiated and its View controls (if available) are already created. | |
| 22 | * Can be used to modify the View before it is displayed, to bind event handlers and do other one-time initialization. | |
| 23 | * @memberOf com.spreadsheetimporter.anyupload.ext.main.Main | |
| 24 | */ | |
| 25 | public onInit(): void { | |
| 26 | const domain = "https://livedemo.spreadsheet-importer.com"; | |
| 27 | // const domain = "http://localhost:4004"; | |
| 28 | const capServices = [ | |
| 29 | { | |
| 30 | name: "Orders V2", | |
| 31 | url: `${domain}/odata/v2/Orders` | |
| 32 | }, | |
| 33 | { | |
| 34 | name: "Orders V4", | |
| 35 | url: `${domain}/odata/v4/orders` | |
| 36 | } | |
| 37 | ]; | |
| 38 | this.getView()?.setModel(new JSONModel(capServices), "capServices"); | |
| 39 | } | |
| 40 | | |
| 41 | public async onServiceChange(event: Select$ChangeEventParameters) { | |
| 42 | // @ts-ignore | |
| 43 | let selectedService = "" | |
| 44 | try { | |
| 45 | selectedService = (event.getSource() as Select).getSelectedItem()?.getBindingContext("capServices")?.getObject() as ServiceObject; | |
| 46 | if (selectedService?.url.includes("/v2/")) { | |
| 47 | this.selectedService = new ODataModelV2(selectedService.url); | |
| 48 | } else if (selectedService?.url.includes("/v4/")) { | |
| 49 | // Import ODataModel V4 at the top of the file | |
| 50 | // import ODataModelV4 from "sap/ui/model/odata/v4/ODataModel"; | |
| 51 | this.selectedService = new ODataModelV4({ | |
| 52 | serviceUrl: selectedService.url, | |
| 53 | synchronizationMode: "None" | |
| 54 | }); | |
| 55 | } else { | |
| 56 | console.error("Unknown OData version"); | |
| 57 | } | |
| 58 | await this.selectedService.getMetaModel().loaded(); | |
| 59 | } catch (error) { | |
| 60 | console.error("Error creating ODataModel:", error); | |
| 61 | } | |
| 62 | const url = selectedService.url; | |
| 63 | // query the single url | |
| 64 | fetch(url) | |
| 65 | .then((response) => response.json()) | |
| 66 | .then((data) => { | |
| 67 | // Handle the data here | |
| 68 | console.log(data); | |
| 69 | // set data to view json model | |
| 70 | let entitySets; | |
| 71 | if (url.includes("/v2/")) { | |
| 72 | entitySets = data.d.results.map((item: { name: string; url: string }) => ({ | |
| 73 | name: item.name, | |
| 74 | url: item.url, | |
| 75 | version: "V2" | |
| 76 | })); | |
| 77 | } else { | |
| 78 | entitySets = data.value.map((item: { name: string; url: string }) => ({ | |
| 79 | name: item.name, | |
| 80 | url: item.url, | |
| 81 | version: "V4" | |
| 82 | })); | |
| 83 | } | |
| 84 | // @ts-ignore | |
| 85 | entitySets.sort((a, b) => a.name.localeCompare(b.name)); | |
| 86 | this.getView()?.setModel(new JSONModel(entitySets), "capEntitySets"); | |
| 87 | | |
| 88 | // Reset entitysets select | |
| 89 | const entitySetSelect = this.byId("entitySetSelect") as Select; | |
| 90 | const entitySetText = this.byId("entitySetText") as Text; | |
| 91 | if (entitySetSelect) { | |
| 92 | entitySetSelect.setSelectedKey(null); | |
| 93 | const hasEntitySets = entitySets && entitySets.length > 0; | |
| 94 | entitySetSelect.setVisible(hasEntitySets); | |
| 95 | entitySetText.setVisible(hasEntitySets); | |
| 96 | | |
| 97 | if (!hasEntitySets) { | |
| 98 | const messageStrip = new MessageStrip({ | |
| 99 | text: "No entity sets are available", | |
| 100 | type: MessageType.Error, | |
| 101 | showIcon: true | |
| 102 | }); | |
| 103 | this.getView().addContent(messageStrip); | |
| 104 | } | |
| 105 | } | |
| 106 | | |
| 107 | // Make table and excel button not visible | |
| 108 | const dynamicTable = this.byId("dynamicTable") as Table; | |
| 109 | if (dynamicTable) { | |
| 110 | dynamicTable.setVisible(false); | |
| 111 | } | |
| 112 | | |
| 113 | const excelButton = this.byId("excelButton") as Button; | |
| 114 | if (excelButton) { | |
| 115 | excelButton.setVisible(false); | |
| 116 | } | |
| 117 | }) | |
| 118 | .catch((error) => { | |
| 119 | console.error("Error fetching data:", error); | |
| 120 | }); | |
| 121 | } | |
| 122 | | |
| 123 | public async onEntityChange(event: Select$ChangeEventParameters): Promise<void> { | |
| 124 | interface ServiceObject { | |
| 125 | name: string; | |
| 126 | url: string; | |
| 127 | version: string; | |
| 128 | } | |
| 129 | // @ts-ignore | |
| 130 | const selectedEntitySet = event.getParameter("selectedItem")?.getBindingContext("capEntitySets")?.getObject() as ServiceObject; | |
| 131 | await this.selectedService.getMetaModel().loaded(); | |
| 132 | const metadata = this.selectedService.getMetaModel().getObject("/"); | |
| 133 | // @ts-ignore | |
| 134 | const entitySets = this.selectedService.getMetaModel().getODataEntityContainer().entitySet; | |
| 135 | // find entitySet by entitySetId from entitySets | |
| 136 | const entitySet = entitySets.find((entitySet: any) => entitySet.name === selectedEntitySet.name); | |
| 137 | | |
| 138 | const entityType = this.selectedService.getMetaModel().getODataEntityType(entitySet.entityType); | |
| 139 | const dynamicTable = this.byId("dynamicTable") as Table; | |
| 140 | | |
| 141 | dynamicTable.removeAllColumns(); | |
| 142 | dynamicTable.unbindItems(); | |
| 143 | | |
| 144 | // Create columns based on entity properties | |
| 145 | entityType.property.forEach((property: any) => { | |
| 146 | return dynamicTable.addColumn( | |
| 147 | new Column({ | |
| 148 | header: new Text({ text: property.name }) | |
| 149 | }) | |
| 150 | ); | |
| 151 | }); | |
| 152 | | |
| 153 | //Create template for items | |
| 154 | const template = new ColumnListItem({ | |
| 155 | cells: entityType.property.map((property: any) => { | |
| 156 | return new Text({ text: `{${property.name}}` }); | |
| 157 | }) | |
| 158 | }); | |
| 159 | | |
| 160 | // Bind items to the table using the new model | |
| 161 | dynamicTable.setModel(this.selectedService); | |
| 162 | dynamicTable.bindItems({ | |
| 163 | path: `/${selectedEntitySet.name}`, | |
| 164 | template: template | |
| 165 | }); | |
| 166 | | |
| 167 | dynamicTable.setVisible(true); | |
| 168 | | |
| 169 | const excelButton = this.byId("excelButton") as Button; | |
| 170 | if (excelButton) { | |
| 171 | excelButton.setVisible(true); | |
| 172 | } | |
| 173 | } | |
| 174 | | |
| 175 | async onUpload(): void { | |
| 176 | const entitySelectObject = this.byId("entitySetSelect")?.getSelectedItem()?.getBindingContext("capEntitySets")?.getObject(); | |
| 177 | const serviceSelectObject = this.byId("serviceSelect")?.getSelectedItem()?.getBindingContext("capServices")?.getObject(); | |
| 178 | | |
| 179 | const componentData = { | |
| 180 | context: this | |
| 181 | } | |
| 182 | if(serviceSelectObject.name === "Orders V2") { | |
| 183 | componentData.createActiveEntity = true; | |
| 184 | } | |
| 185 | this.spreadsheetUpload = await this.getAppComponent().createComponent({ | |
| 186 | usage: "spreadsheetImporter", | |
| 187 | async: true, | |
| 188 | componentData: componentData | |
| 189 | }); | |
| 190 | this.spreadsheetUpload.openSpreadsheetUploadDialog(); | |
| 191 | } | |
| 192 | | |
| 193 | /** | |
| 194 | * Similar to onAfterRendering, but this hook is invoked before the controller's View is re-rendered | |
| 195 | * (NOT before the first rendering! onInit() is used for that one!). | |
| 196 | * @memberOf com.spreadsheetimporter.anyupload.ext.main.Main | |
| 197 | */ | |
| 198 | // public onBeforeRendering(): void { | |
| 199 | // | |
| 200 | // } | |
| 201 | | |
| 202 | /** | |
| 203 | * Called when the View has been rendered (so its HTML is part of the document). Post-rendering manipulations of the HTML could be done here. | |
| 204 | * This hook is the same one that SAPUI5 controls get after being rendered. | |
| 205 | * @memberOf com.spreadsheetimporter.anyupload.ext.main.Main | |
| 206 | */ | |
| 207 | // public onAfterRendering(): void { | |
| 208 | // | |
| 209 | // } | |
| 210 | | |
| 211 | /** | |
| 212 | * Called when the Controller is destroyed. Use this one to free resources and finalize activities. | |
| 213 | * @memberOf com.spreadsheetimporter.anyupload.ext.main.Main | |
| 214 | */ | |
| 215 | // public onExit(): void { | |
| 216 | // | |
| 217 | // } | |
| 218 | } | |
| 219 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv2fe/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Tue Nov 29 2022 16:32:36 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-elements| | |
| 6 | |**App Generator Version**<br>1.8.2| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>List Report Page V2| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/v2/orders | |
| 11 | |**Module Name**<br>ordersv2fe| | |
| 12 | |**Application Title**<br>Orders V2| | |
| 13 | |**Namespace**<br>ui.v2| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Main Entity**<br>Orders| | |
| 20 | |**Navigation Entity**<br>Items| | |
| 21 | | |
| 22 | ## ordersv2fe | |
| 23 | | |
| 24 | | |
| 25 | | |
| 26 | ### Starting the generated app | |
| 27 | | |
| 28 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 29 | | |
| 30 | ``` | |
| 31 | npm start | |
| 32 | ``` | |
| 33 | | |
| 34 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 35 | | |
| 36 | ``` | |
| 37 | npm run start-mock | |
| 38 | ``` | |
| 39 | | |
| 40 | #### Pre-requisites: | |
| 41 | | |
| 42 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 43 | | |
| 44 | | |
| 45 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv2fenondraft/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Tue Jan 03 2023 19:34:51 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-elements| | |
| 6 | |**App Generator Version**<br>1.8.2| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>List Report Page V2| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/v2/orders/ | |
| 11 | |**Module Name**<br>ordersv2fenondraft| | |
| 12 | |**Application Title**<br>Order V2 FE Non Draft| | |
| 13 | |**Namespace**<br>ui.v2| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Main Entity**<br>OrdersND| | |
| 20 | |**Navigation Entity**<br>Items| | |
| 21 | | |
| 22 | ## ordersv2fenondraft | |
| 23 | | |
| 24 | | |
| 25 | | |
| 26 | ### Starting the generated app | |
| 27 | | |
| 28 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 29 | | |
| 30 | ``` | |
| 31 | npm start | |
| 32 | ``` | |
| 33 | | |
| 34 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 35 | | |
| 36 | ``` | |
| 37 | npm run start-mock | |
| 38 | ``` | |
| 39 | | |
| 40 | #### Pre-requisites: | |
| 41 | | |
| 42 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 43 | | |
| 44 | | |
| 45 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv2freestylenondraft/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Tue Jan 03 2023 17:25:28 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-freestyle| | |
| 6 | |**App Generator Version**<br>1.8.2| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>2listdetail| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/v2/orders | |
| 11 | |**Module Name**<br>ordersv2freestyle| | |
| 12 | |**Application Title**<br>Order V2 Freestyle| | |
| 13 | |**Namespace**<br>ui.v2| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Object collection**<br>Orders| | |
| 20 | |**Object collection key**<br>ID| | |
| 21 | |**Object ID**<br>OrderNo| | |
| 22 | |**entityType**<br>Items| | |
| 23 | |**entitySet**<br>OrderItems| | |
| 24 | |**Line item collection key**<br>ID| | |
| 25 | |**Line item ID**<br>product_ID| | |
| 26 | | |
| 27 | ## ordersv2freestyle | |
| 28 | | |
| 29 | | |
| 30 | | |
| 31 | ### Starting the generated app | |
| 32 | | |
| 33 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 34 | | |
| 35 | ``` | |
| 36 | npm start | |
| 37 | ``` | |
| 38 | | |
| 39 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 40 | | |
| 41 | ``` | |
| 42 | npm run start-mock | |
| 43 | ``` | |
| 44 | | |
| 45 | #### Pre-requisites: | |
| 46 | | |
| 47 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 48 | | |
| 49 | | |
| 50 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv2freestylenondraft2/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Tue Jan 03 2023 17:25:28 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-freestyle| | |
| 6 | |**App Generator Version**<br>1.8.2| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>2listdetail| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/v2/orders | |
| 11 | |**Module Name**<br>ordersv2freestyle| | |
| 12 | |**Application Title**<br>Order V2 Freestyle| | |
| 13 | |**Namespace**<br>ui.v2| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Object collection**<br>Orders| | |
| 20 | |**Object collection key**<br>ID| | |
| 21 | |**Object ID**<br>OrderNo| | |
| 22 | |**entityType**<br>Items| | |
| 23 | |**entitySet**<br>OrderItems| | |
| 24 | |**Line item collection key**<br>ID| | |
| 25 | |**Line item ID**<br>product_ID| | |
| 26 | | |
| 27 | ## ordersv2freestyle | |
| 28 | | |
| 29 | | |
| 30 | | |
| 31 | ### Starting the generated app | |
| 32 | | |
| 33 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 34 | | |
| 35 | ``` | |
| 36 | npm start | |
| 37 | ``` | |
| 38 | | |
| 39 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 40 | | |
| 41 | ``` | |
| 42 | npm run start-mock | |
| 43 | ``` | |
| 44 | | |
| 45 | #### Pre-requisites: | |
| 46 | | |
| 47 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 48 | | |
| 49 | | |
| 50 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv2freestylenondraftopenui5/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Tue Jan 03 2023 17:25:28 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-freestyle| | |
| 6 | |**App Generator Version**<br>1.8.2| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>2listdetail| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/v2/orders | |
| 11 | |**Module Name**<br>ordersv2freestyle| | |
| 12 | |**Application Title**<br>Order V2 Freestyle| | |
| 13 | |**Namespace**<br>ui.v2| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Object collection**<br>Orders| | |
| 20 | |**Object collection key**<br>ID| | |
| 21 | |**Object ID**<br>OrderNo| | |
| 22 | |**entityType**<br>Items| | |
| 23 | |**entitySet**<br>OrderItems| | |
| 24 | |**Line item collection key**<br>ID| | |
| 25 | |**Line item ID**<br>product_ID| | |
| 26 | | |
| 27 | ## ordersv2freestyle | |
| 28 | | |
| 29 | | |
| 30 | | |
| 31 | ### Starting the generated app | |
| 32 | | |
| 33 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 34 | | |
| 35 | ``` | |
| 36 | npm start | |
| 37 | ``` | |
| 38 | | |
| 39 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 40 | | |
| 41 | ``` | |
| 42 | npm run start-mock | |
| 43 | ``` | |
| 44 | | |
| 45 | #### Pre-requisites: | |
| 46 | | |
| 47 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 48 | | |
| 49 | | |
| 50 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv4fe/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Tue Nov 29 2022 15:13:05 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-elements| | |
| 6 | |**App Generator Version**<br>1.8.2| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>List Report Page V4| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/ordersv4fe | |
| 11 | |**Module Name**<br>ordersv4fe| | |
| 12 | |**Application Title**<br>ordersv4fe| | |
| 13 | |**Namespace**<br>ui.v4| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Main Entity**<br>ordersv4fe| | |
| 20 | |**Navigation Entity**<br>Items| | |
| 21 | | |
| 22 | ## ordersv4fe | |
| 23 | | |
| 24 | | |
| 25 | | |
| 26 | ### Starting the generated app | |
| 27 | | |
| 28 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 29 | | |
| 30 | ``` | |
| 31 | npm start | |
| 32 | ``` | |
| 33 | | |
| 34 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 35 | | |
| 36 | ``` | |
| 37 | npm run start-mock | |
| 38 | ``` | |
| 39 | | |
| 40 | #### Pre-requisites: | |
| 41 | | |
| 42 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 43 | | |
| 44 | | |
| 45 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv4fets/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Sat Jun 24 2023 15:54:21 GMT+0200 (Mitteleuropäische Sommerzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-elements| | |
| 6 | |**App Generator Version**<br>1.9.5| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>List Report Page V4| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/Orders | |
| 11 | |**Module Name**<br>ordersv4fets| | |
| 12 | |**Application Title**<br>Orders V4 FE 108 Typescript| | |
| 13 | |**Namespace**<br>ui.v4| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>True| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Main Entity**<br>Orders| | |
| 20 | |**Navigation Entity**<br>Items| | |
| 21 | | |
| 22 | ## ordersv4fets | |
| 23 | | |
| 24 | | |
| 25 | | |
| 26 | ### Starting the generated app | |
| 27 | | |
| 28 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 29 | | |
| 30 | ``` | |
| 31 | npm start | |
| 32 | ``` | |
| 33 | | |
| 34 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 35 | | |
| 36 | ``` | |
| 37 | npm run start-mock | |
| 38 | ``` | |
| 39 | | |
| 40 | #### Pre-requisites: | |
| 41 | | |
| 42 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 43 | | |
| 44 | | |
| 45 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv4fets/webapp/Component.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import BaseComponent from "sap/fe/core/AppComponent"; | |
| 2 | | |
| 3 | /** | |
| 4 | * @namespace ui.v4.ordersv4fe | |
| 5 | */ | |
| 6 | export default class Component extends BaseComponent { | |
| 7 | public static metadata = { | |
| 8 | manifest: "json" | |
| 9 | }; | |
| 10 | | |
| 11 | /** | |
| 12 | * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. | |
| 13 | * @public | |
| 14 | * @override | |
| 15 | */ | |
| 16 | //public init() : void { | |
| 17 | // super.init(); | |
| 18 | //} | |
| 19 | } | |
| 20 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv4fets/webapp/ext/ListReportExtController.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Component, { Component$ChangeBeforeCreateEvent, Component$CheckBeforeReadEvent, Component$UploadButtonPressEvent } from "cc/spreadsheetimporter/v1_7_3/Component"; | |
| 2 | import BaseController from "sap/fe/core/BaseController"; | |
| 3 | import ExtensionAPI from "sap/fe/core/ExtensionAPI"; | |
| 4 | /** | |
| 5 | * Generated event handler. | |
| 6 | * | |
| 7 | * @param this reference to the 'this' that the event handler is bound to. | |
| 8 | * @param pageContext the context of the page on which the event was fired | |
| 9 | */ | |
| 10 | export async function openSpreadsheetUploadDialog(this: ExtensionAPI) { | |
| 11 | const view = this.getRouting().getView(); | |
| 12 | const controller = view.getController() as BaseController; | |
| 13 | view.setBusyIndicatorDelay(0); | |
| 14 | view.setBusy(true); | |
| 15 | const spreadsheetUpload = (await controller.getAppComponent().createComponent({ | |
| 16 | usage: "spreadsheetImporter", | |
| 17 | async: true, | |
| 18 | componentData: { | |
| 19 | context: this, | |
| 20 | activateDraft: true | |
| 21 | } | |
| 22 | })) as Component; | |
| 23 | // event to check before uploaded to app | |
| 24 | spreadsheetUpload.attachCheckBeforeRead(function (event: Component$CheckBeforeReadEvent) { | |
| 25 | // example | |
| 26 | const sheetData = event.getParameter("sheetData") as any; | |
| 27 | event.getParameters().messages; | |
| 28 | let errorArray = []; | |
| 29 | for (const [index, row] of sheetData.entries()) { | |
| 30 | //check for invalid price | |
| 31 | for (const key in row) { | |
| 32 | if (key.endsWith("[price]") && row[key].rawValue > 100) { | |
| 33 | const error = { | |
| 34 | title: "Price too high (max 100)", | |
| 35 | row: index + 2, | |
| 36 | group: true, | |
| 37 | rawValue: row[key].rawValue, | |
| 38 | ui5type: "Error" | |
| 39 | }; | |
| 40 | errorArray.push(error); | |
| 41 | } | |
| 42 | } | |
| 43 | } | |
| 44 | (event.getSource() as Component).addArrayToMessages(errorArray); | |
| 45 | }, this); | |
| 46 | | |
| 47 | // event example to prevent uploading data to backend | |
| 48 | spreadsheetUpload.attachUploadButtonPress(function (event: Component$UploadButtonPressEvent) { | |
| 49 | //event.preventDefault(); | |
| 50 | //event.getParameter("payload"); | |
| 51 | }, this); | |
| 52 | | |
| 53 | // event to change data before send to backend | |
| 54 | spreadsheetUpload.attachChangeBeforeCreate(function (event: Component$ChangeBeforeCreateEvent) { | |
| 55 | let payload = event.getParameter("payload"); | |
| 56 | // round number from 12,56 to 12,6 | |
| 57 | if (payload.price) { | |
| 58 | payload.price = Number(Number(payload.price).toFixed(1)); | |
| 59 | } | |
| 60 | return payload; | |
| 61 | }, this); | |
| 62 | spreadsheetUpload.openSpreadsheetUploadDialog(); | |
| 63 | view.setBusy(false); | |
| 64 | } | |
| 65 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv4fets/webapp/ext/ObjectPageExtController.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Component, { Component$ChangeBeforeCreateEvent, Component$CheckBeforeReadEvent } from "cc/spreadsheetimporter/v1_7_3/Component"; | |
| 2 | import BaseController from "sap/fe/core/BaseController"; | |
| 3 | import ExtensionAPI from "sap/fe/core/ExtensionAPI"; | |
| 4 | /** | |
| 5 | * Generated event handler. | |
| 6 | * | |
| 7 | * @param this reference to the 'this' that the event handler is bound to. | |
| 8 | * @param pageContext the context of the page on which the event was fired | |
| 9 | */ | |
| 10 | export async function openSpreadsheetUploadDialogTable(this: ExtensionAPI) { | |
| 11 | const view = this.getRouting().getView(); | |
| 12 | const controller = view.getController() as BaseController; | |
| 13 | view.setBusyIndicatorDelay(0); | |
| 14 | view.setBusy(true); | |
| 15 | const spreadsheetUpload = (await controller.getAppComponent().createComponent({ | |
| 16 | usage: "spreadsheetImporter", | |
| 17 | async: true, | |
| 18 | componentData: { | |
| 19 | context: this, | |
| 20 | tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable", | |
| 21 | columns: ["product_ID", "quantity", "title", "price", "validFrom", "timestamp", "date", "time", "boolean", "decimal"], | |
| 22 | mandatoryFields: ["product_ID", "quantity"], | |
| 23 | spreadsheetFileName: "Test.xlsx" | |
| 24 | } | |
| 25 | })) as Component; | |
| 26 | // event to check before uploaded to app | |
| 27 | spreadsheetUpload.attachCheckBeforeRead((event) => { | |
| 28 | const sheetData = event.getParameter("sheetData") as any; | |
| 29 | let errorArray = []; | |
| 30 | for (const [index, row] of sheetData.entries()) { | |
| 31 | //check for invalid price | |
| 32 | for (const key in row) { | |
| 33 | if (key.endsWith("[price]") && row[key].rawValue > 100) { | |
| 34 | const error = { | |
| 35 | title: "Price too high (max 100)", | |
| 36 | row: index + 2, | |
| 37 | group: true, | |
| 38 | rawValue: row[key].rawValue, | |
| 39 | ui5type: "Error" | |
| 40 | }; | |
| 41 | errorArray.push(error); | |
| 42 | } | |
| 43 | } | |
| 44 | } | |
| 45 | (event.getSource() as Component).addArrayToMessages(errorArray); | |
| 46 | }); | |
| 47 | // event to change data before send to backend | |
| 48 | spreadsheetUpload.attachChangeBeforeCreate((event) => { | |
| 49 | let payload = event.getParameter("payload"); | |
| 50 | // round number from 12,56 to 12,6 | |
| 51 | if (payload.price) { | |
| 52 | payload.price = Number(Number(payload.price).toFixed(1)); | |
| 53 | } | |
| 54 | return payload; | |
| 55 | }); | |
| 56 | | |
| 57 | spreadsheetUpload.openSpreadsheetUploadDialog(); | |
| 58 | view.setBusy(false); | |
| 59 | } | |
| 60 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv4fpm/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Tue Jan 03 2023 20:00:07 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-elements| | |
| 6 | |**App Generator Version**<br>1.8.2| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>Custom Page V4| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/orders/ | |
| 11 | |**Module Name**<br>ordersv4fpm| | |
| 12 | |**Application Title**<br>Orders V4 Flexible Programming Model| | |
| 13 | |**Namespace**<br>ui.v4| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.12| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | |**Main Entity**<br>Orders| | |
| 20 | | |
| 21 | ## ordersv4fpm | |
| 22 | | |
| 23 | | |
| 24 | | |
| 25 | ### Starting the generated app | |
| 26 | | |
| 27 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 28 | | |
| 29 | ``` | |
| 30 | npm start | |
| 31 | ``` | |
| 32 | | |
| 33 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 34 | | |
| 35 | ``` | |
| 36 | npm run start-mock | |
| 37 | ``` | |
| 38 | | |
| 39 | #### Pre-requisites: | |
| 40 | | |
| 41 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 42 | | |
| 43 | | |
| 44 | | |
| -------------------------------------------------------------------------------- | |
| /examples/packages/ordersv4freestyle/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | ## Application Details | |
| 2 | | | | |
| 3 | | ------------- | | |
| 4 | |**Generation Date and Time**<br>Fri Dec 06 2024 08:29:47 GMT+0100 (Mitteleuropäische Normalzeit)| | |
| 5 | |**App Generator**<br>@sap/generator-fiori-freestyle| | |
| 6 | |**App Generator Version**<br>1.12.4| | |
| 7 | |**Generation Platform**<br>Visual Studio Code| | |
| 8 | |**Template Used**<br>simple| | |
| 9 | |**Service Type**<br>OData Url| | |
| 10 | |**Service URL**<br>http://localhost:4004/odata/v4/Orders/ | |
| 11 | |**Module Name**<br>ordersv4freestyle| | |
| 12 | |**Application Title**<br>OrdersV4 Freestyle 120| | |
| 13 | |**Namespace**<br>| | |
| 14 | |**UI5 Theme**<br>sap_horizon| | |
| 15 | |**UI5 Version**<br>1.120.24| | |
| 16 | |**Enable Code Assist Libraries**<br>False| | |
| 17 | |**Enable TypeScript**<br>False| | |
| 18 | |**Add Eslint configuration**<br>False| | |
| 19 | | |
| 20 | ## ordersv4freestyle | |
| 21 | | |
| 22 | An SAP Fiori application. | |
| 23 | | |
| 24 | ### Starting the generated app | |
| 25 | | |
| 26 | - This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. In order to launch the generated app, simply run the following from the generated app root folder: | |
| 27 | | |
| 28 | ``` | |
| 29 | npm start | |
| 30 | ``` | |
| 31 | | |
| 32 | - It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: | |
| 33 | | |
| 34 | ``` | |
| 35 | npm run start-mock | |
| 36 | ``` | |
| 37 | | |
| 38 | #### Pre-requisites: | |
| 39 | | |
| 40 | 1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) | |
| 41 | | |
| 42 | | |
| 43 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/README.md: | |
| -------------------------------------------------------------------------------- | |
| 1 | # UI5 custom control `ui5-cc-spreadsheetimporter` | |
| 2 | | |
| 3 | A UI5 Module to integrate a Spreadsheet Upload for Fiori Element Apps. | |
| 4 | This control simply enables the mass upload of data, independent of the backend, OData version and Fiori scenario. | |
| 5 | This is made possible by reading the Spreadsheet file and using the standard APIs. | |
| 6 | The control will submit not the file, but just the data from the Spreadsheet File. | |
| 7 | The integration of the control is designed to be as simple as possible and, in the best case, requires no configuration. | |
| 8 | **** | |
| 9 | The aim is to support as many Fiori Scenarios and UI5 Versions as possible. | |
| 10 | See here for all currently [supported Versions](https://docs.spreadsheet-importer.com/pages/SupportVersions/). | |
| 11 | | |
| 12 |  | |
| 13 | | |
| 14 | ## Live Demo | |
| 15 | | |
| 16 | It is possible to try this out directly at: | |
| 17 | https://livedemo.spreadsheet-importer.com/ | |
| 18 | | |
| 19 | The app is an OData V4 app with UI5 version 1.120 and a CAP backend. | |
| 20 | The data is reset every hour on the hour. | |
| 21 | | |
| 22 | ## Install | |
| 23 | | |
| 24 | ```bash | |
| 25 | npm install ui5-cc-spreadsheetimporter | |
| 26 | ``` | |
| 27 | | |
| 28 | ## Getting Started | |
| 29 | | |
| 30 | You can find the official documentation here: | |
| 31 | | |
| 32 | https://docs.spreadsheet-importer.com/ | |
| 33 | | |
| 34 | ## Development | |
| 35 | | |
| 36 | You can find the documentation for development here: | |
| 37 | | |
| 38 | https://docs.spreadsheet-importer.com/pages/Development/GettingStarted/ | |
| 39 | | |
| 40 | ## Changelog | |
| 41 | | |
| 42 | See [CHANGELOG.md](CHANGELOG.md) | |
| 43 | | |
| 44 | ## License | |
| 45 | | |
| 46 | This project uses SheetJS Community Edition for processing spreadsheet data: | |
| 47 | | |
| 48 | SheetJS Community Edition -- https://sheetjs.com/ | |
| 49 | | |
| 50 | Copyright (C) 2012-present SheetJS LLC | |
| 51 | | |
| 52 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 53 | you may not use this file except in compliance with the License. | |
| 54 | You may obtain a copy of the License at | |
| 55 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 56 | | |
| 57 | Unless required by applicable law or agreed to in writing, software | |
| 58 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 59 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 60 | See the License for the specific language governing permissions and limitations under the License. | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/control/SpreadsheetDialog.gen.d.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Event from "sap/ui/base/Event"; | |
| 2 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; | |
| 3 | import { $DialogSettings } from "sap/m/Dialog"; | |
| 4 | | |
| 5 | declare module "./SpreadsheetDialog" { | |
| 6 | | |
| 7 | /** | |
| 8 | * Interface defining the settings object used in constructor calls | |
| 9 | */ | |
| 10 | interface $SpreadsheetDialogSettings extends $DialogSettings { | |
| 11 | decimalSeparator?: string | PropertyBindingInfo; | |
| 12 | availableOptions?: string[] | PropertyBindingInfo | `{${string}}`; | |
| 13 | component?: object | PropertyBindingInfo | `{${string}}`; | |
| 14 | fileDrop?: (event: SpreadsheetDialog$FileDropEvent) => void; | |
| 15 | decimalSeparatorChanged?: (event: SpreadsheetDialog$DecimalSeparatorChangedEvent) => void; | |
| 16 | availableOptionsChanged?: (event: SpreadsheetDialog$AvailableOptionsChangedEvent) => void; | |
| 17 | } | |
| 18 | | |
| 19 | export default interface SpreadsheetDialog { | |
| 20 | | |
| 21 | // property: decimalSeparator | |
| 22 | getDecimalSeparator(): string; | |
| 23 | setDecimalSeparator(decimalSeparator: string): this; | |
| 24 | | |
| 25 | // property: availableOptions | |
| 26 | getAvailableOptions(): string[]; | |
| 27 | setAvailableOptions(availableOptions: string[]): this; | |
| 28 | | |
| 29 | // property: component | |
| 30 | getComponent(): object; | |
| 31 | setComponent(component: object): this; | |
| 32 | | |
| 33 | // event: fileDrop | |
| 34 | attachFileDrop(fn: (event: SpreadsheetDialog$FileDropEvent) => void, listener?: object): this; | |
| 35 | attachFileDrop<CustomDataType extends object>(data: CustomDataType, fn: (event: SpreadsheetDialog$FileDropEvent, data: CustomDataType) => void, listener?: object): this; | |
| 36 | detachFileDrop(fn: (event: SpreadsheetDialog$FileDropEvent) => void, listener?: object): this; | |
| 37 | fireFileDrop(parameters?: SpreadsheetDialog$FileDropEventParameters): this; | |
| 38 | | |
| 39 | // event: decimalSeparatorChanged | |
| 40 | attachDecimalSeparatorChanged(fn: (event: SpreadsheetDialog$DecimalSeparatorChangedEvent) => void, listener?: object): this; | |
| 41 | attachDecimalSeparatorChanged<CustomDataType extends object>(data: CustomDataType, fn: (event: SpreadsheetDialog$DecimalSeparatorChangedEvent, data: CustomDataType) => void, listener?: object): this; | |
| 42 | detachDecimalSeparatorChanged(fn: (event: SpreadsheetDialog$DecimalSeparatorChangedEvent) => void, listener?: object): this; | |
| 43 | fireDecimalSeparatorChanged(parameters?: SpreadsheetDialog$DecimalSeparatorChangedEventParameters): this; | |
| 44 | | |
| 45 | // event: availableOptionsChanged | |
| 46 | attachAvailableOptionsChanged(fn: (event: SpreadsheetDialog$AvailableOptionsChangedEvent) => void, listener?: object): this; | |
| 47 | attachAvailableOptionsChanged<CustomDataType extends object>(data: CustomDataType, fn: (event: SpreadsheetDialog$AvailableOptionsChangedEvent, data: CustomDataType) => void, listener?: object): this; | |
| 48 | detachAvailableOptionsChanged(fn: (event: SpreadsheetDialog$AvailableOptionsChangedEvent) => void, listener?: object): this; | |
| 49 | fireAvailableOptionsChanged(parameters?: SpreadsheetDialog$AvailableOptionsChangedEventParameters): this; | |
| 50 | } | |
| 51 | | |
| 52 | /** | |
| 53 | * Interface describing the parameters of SpreadsheetDialog's 'fileDrop' event. | |
| 54 | */ | |
| 55 | export interface SpreadsheetDialog$FileDropEventParameters { | |
| 56 | files?: object[]; | |
| 57 | } | |
| 58 | | |
| 59 | /** | |
| 60 | * Interface describing the parameters of SpreadsheetDialog's 'decimalSeparatorChanged' event. | |
| 61 | */ | |
| 62 | export interface SpreadsheetDialog$DecimalSeparatorChangedEventParameters { | |
| 63 | decimalSeparator?: string; | |
| 64 | } | |
| 65 | | |
| 66 | /** | |
| 67 | * Interface describing the parameters of SpreadsheetDialog's 'availableOptionsChanged' event. | |
| 68 | */ | |
| 69 | export interface SpreadsheetDialog$AvailableOptionsChangedEventParameters { | |
| 70 | availableOptions?: string[]; | |
| 71 | } | |
| 72 | | |
| 73 | /** | |
| 74 | * Type describing the SpreadsheetDialog's 'fileDrop' event. | |
| 75 | */ | |
| 76 | export type SpreadsheetDialog$FileDropEvent = Event<SpreadsheetDialog$FileDropEventParameters>; | |
| 77 | | |
| 78 | /** | |
| 79 | * Type describing the SpreadsheetDialog's 'decimalSeparatorChanged' event. | |
| 80 | */ | |
| 81 | export type SpreadsheetDialog$DecimalSeparatorChangedEvent = Event<SpreadsheetDialog$DecimalSeparatorChangedEventParameters>; | |
| 82 | | |
| 83 | /** | |
| 84 | * Type describing the SpreadsheetDialog's 'availableOptionsChanged' event. | |
| 85 | */ | |
| 86 | export type SpreadsheetDialog$AvailableOptionsChangedEvent = Event<SpreadsheetDialog$AvailableOptionsChangedEventParameters>; | |
| 87 | } | |
| 88 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/control/SpreadsheetDialog.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Dialog from "sap/m/Dialog"; | |
| 2 | import type { MetadataOptions } from "sap/ui/core/Element"; | |
| 3 | import { AvailableOptions } from "../enums"; | |
| 4 | import SpreadsheetDialogRenderer from "./SpreadsheetDialogRenderer"; | |
| 5 | import ResourceModel from "sap/ui/model/resource/ResourceModel"; | |
| 6 | import ResourceBundle from "sap/base/i18n/ResourceBundle"; | |
| 7 | /** | |
| 8 | * Constructor for a new <code>cc.spreadsheetimporter.XXXnamespaceXXX.SpreadsheetDialog</code> control. | |
| 9 | * | |
| 10 | * Some class description goes here. | |
| 11 | * @extends Dialog | |
| 12 | * | |
| 13 | * @constructor | |
| 14 | * @public | |
| 15 | * @name cc.spreadsheetimporter.XXXnamespaceXXX.SpreadsheetDialog | |
| 16 | */ | |
| 17 | export default class SpreadsheetDialog extends Dialog { | |
| 18 | dropMessageShown: boolean; | |
| 19 | constructor(id?: string | $SpreadsheetDialogSettings); | |
| 20 | constructor(id?: string, settings?: $SpreadsheetDialogSettings); | |
| 21 | constructor(id?: string, settings?: $SpreadsheetDialogSettings) { | |
| 22 | super(id, settings); | |
| 23 | this.dropMessageShown = false; | |
| 24 | } | |
| 25 | | |
| 26 | static readonly metadata: MetadataOptions = { | |
| 27 | properties: { | |
| 28 | decimalSeparator: { type: "string" }, | |
| 29 | availableOptions: { type: "string[]" }, | |
| 30 | component: { type: "object" } | |
| 31 | }, | |
| 32 | events: { | |
| 33 | fileDrop: { | |
| 34 | parameters: { | |
| 35 | files: { type: "object[]" } | |
| 36 | } | |
| 37 | }, | |
| 38 | decimalSeparatorChanged: { | |
| 39 | parameters: { | |
| 40 | decimalSeparator: { type: "string" } | |
| 41 | } | |
| 42 | }, | |
| 43 | availableOptionsChanged: { | |
| 44 | parameters: { | |
| 45 | availableOptions: { type: "string[]" } | |
| 46 | } | |
| 47 | } | |
| 48 | } | |
| 49 | }; | |
| 50 | | |
| 51 | onAfterRendering(event: any) { | |
| 52 | super.onAfterRendering(event); | |
| 53 | const domRef = this.getDomRef(); | |
| 54 | domRef.addEventListener("dragover", this.handleDragOver.bind(this), false); | |
| 55 | domRef.addEventListener("dragenter", this.handleDragEnter.bind(this), false); | |
| 56 | domRef.addEventListener("dragleave", this.handleDragLeave.bind(this), false); | |
| 57 | domRef.addEventListener("drop", this.handleFileDrop.bind(this), false); | |
| 58 | } | |
| 59 | | |
| 60 | private handleDragOver(event: any) { | |
| 61 | event.preventDefault(); | |
| 62 | event.stopPropagation(); | |
| 63 | event.dataTransfer.dropEffect = "copy"; | |
| 64 | | |
| 65 | if (!this.dropMessageShown) { | |
| 66 | this.showDropMessage(true); | |
| 67 | } | |
| 68 | } | |
| 69 | | |
| 70 | private handleDragLeave(event: any) { | |
| 71 | // Check if the drag is actually leaving the dialog, not just moving between children | |
| 72 | if (!event.currentTarget.contains(event.relatedTarget)) { | |
| 73 | this.showDropMessage(false); | |
| 74 | } | |
| 75 | } | |
| 76 | | |
| 77 | private handleFileDrop(event: any) { | |
| 78 | event.preventDefault(); | |
| 79 | event.stopPropagation(); | |
| 80 | this.showDropMessage(false); | |
| 81 | const files = event.dataTransfer.files; | |
| 82 | this.fireFileDrop({ files: files } as SpreadsheetDialog$FileDropEventParameters); | |
| 83 | } | |
| 84 | | |
| 85 | private handleDragEnter(event: any) {} | |
| 86 | | |
| 87 | private showDropMessage(show: boolean) { | |
| 88 | // Ensure the current state matches the desired visibility | |
| 89 | if (this.dropMessageShown === show) { | |
| 90 | return; // No change is needed if the state is already correct | |
| 91 | } | |
| 92 | | |
| 93 | let dropMessage = this.getDomRef().querySelector(".drop-message"); | |
| 94 | if (!dropMessage) { | |
| 95 | // Create the message element if it doesn't exist | |
| 96 | dropMessage = document.createElement("div"); | |
| 97 | dropMessage.className = "drop-message"; | |
| 98 | dropMessage.textContent = ((this.getModel("i18n") as ResourceModel).getResourceBundle() as ResourceBundle).getText("spreadsheetimporter.dropMessage"); | |
| 99 | this.getDomRef().appendChild(dropMessage); | |
| 100 | } | |
| 101 | | |
| 102 | // Toggle visibility class based on the 'show' parameter | |
| 103 | dropMessage.classList.toggle("visible", show); | |
| 104 | // Update the flag to reflect the new state | |
| 105 | this.dropMessageShown = show; | |
| 106 | } | |
| 107 | | |
| 108 | public setDecimalSeparator(sDecimalSeparator: string) { | |
| 109 | if (sDecimalSeparator === "," || sDecimalSeparator === ".") { | |
| 110 | this.setProperty("decimalSeparator", sDecimalSeparator); | |
| 111 | this.fireDecimalSeparatorChanged({ decimalSeparator: sDecimalSeparator } as SpreadsheetDialog$DecimalSeparatorChangedEventParameters); | |
| 112 | return this; | |
| 113 | } else { | |
| 114 | throw new Error("Decimal separator must be either ',' or '.'"); | |
| 115 | } | |
| 116 | } | |
| 117 | | |
| 118 | public setAvailableOptions(aAvailableOptions: AvailableOptions[]) { | |
| 119 | for (let option of aAvailableOptions) { | |
| 120 | if (!Object.values(AvailableOptions).includes(option as AvailableOptions)) { | |
| 121 | throw new Error("Invalid option: " + option); | |
| 122 | } | |
| 123 | } | |
| 124 | this.setProperty("availableOptions", aAvailableOptions); | |
| 125 | this.fireAvailableOptionsChanged({ availableOptions: aAvailableOptions }) as SpreadsheetDialog$AvailableOptionsChangedEventParameters; | |
| 126 | return this; | |
| 127 | } | |
| 128 | | |
| 129 | exit() { | |
| 130 | // Remove event listeners to clean up | |
| 131 | const domRef = this.getDomRef(); | |
| 132 | if (domRef) { | |
| 133 | domRef.removeEventListener("dragover", this.handleDragOver.bind(this), false); | |
| 134 | domRef.removeEventListener("dragleave", this.handleDragLeave.bind(this), false); | |
| 135 | domRef.removeEventListener("drop", this.handleFileDrop.bind(this), false); | |
| 136 | } | |
| 137 | | |
| 138 | // Clean up the drop message element if needed | |
| 139 | let dropMessage = domRef?.querySelector(".drop-message"); | |
| 140 | if (dropMessage) { | |
| 141 | dropMessage.remove(); | |
| 142 | } | |
| 143 | this.dropMessageShown = false; // Reset visibility flag | |
| 144 | super.exit(); | |
| 145 | } | |
| 146 | | |
| 147 | static renderer: typeof SpreadsheetDialogRenderer = SpreadsheetDialogRenderer; | |
| 148 | } | |
| 149 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/control/SpreadsheetDialogRenderer.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import RenderManager from "sap/ui/core/RenderManager"; | |
| 2 | import DialogRenderer from "sap/m/DialogRenderer"; | |
| 3 | import SpreadsheetDialog from "./SpreadsheetDialog"; | |
| 4 | /** | |
| 5 | * @name cc.spreadsheetimporter.XXXnamespaceXXX.SpreadsheetDialog | |
| 6 | */ | |
| 7 | // ui5lint-disable-next-line -- can´t use apiVersion: 2, because of support for 1.71, remove when out of support | |
| 8 | export default { | |
| 9 | //apiVersion: 2, | |
| 10 | render: function (rm: RenderManager, control: SpreadsheetDialog) { | |
| 11 | // @ts-ignore | |
| 12 | DialogRenderer.render.apply(this, arguments); | |
| 13 | } | |
| 14 | }; | |
| 15 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/Logger.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | /** | |
| 3 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 4 | */ | |
| 5 | export default class Logger extends ManagedObject { | |
| 6 | returnObject(object: any) { | |
| 7 | return object; | |
| 8 | } | |
| 9 | } | |
| 10 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/Parser.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import Component from "../Component"; | |
| 3 | import { ArrayData, ListObject, Payload, PayloadArray, Property, ValueData } from "../types"; | |
| 4 | import MessageHandler from "./MessageHandler"; | |
| 5 | import Util from "./Util"; | |
| 6 | import { CustomMessageTypes, FieldMatchType, MessageType } from "../enums"; | |
| 7 | | |
| 8 | /** | |
| 9 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 10 | */ | |
| 11 | export default class Parser extends ManagedObject { | |
| 12 | static parseSpreadsheetData(sheetData: ArrayData, typeLabelList: ListObject, component: Component, messageHandler: MessageHandler, util: Util, isODataV4: Boolean) { | |
| 13 | const payloadArray: PayloadArray = []; | |
| 14 | // loop over data from spreadsheet file | |
| 15 | for (const [index, row] of sheetData.entries()) { | |
| 16 | let payload: Payload = {}; | |
| 17 | // check each specified column if availalble in spreadsheet data | |
| 18 | for (const [columnKey, metadataColumn] of typeLabelList.entries()) { | |
| 19 | // depending on parse type | |
| 20 | const value = Util.getValueFromRow(row, metadataColumn.label, columnKey, component.getFieldMatchType() as FieldMatchType); | |
| 21 | // depending on data type | |
| 22 | if (value && value.rawValue !== undefined && value.rawValue !== null && value.rawValue !== "") { | |
| 23 | const rawValue = value.rawValue; | |
| 24 | if (metadataColumn.type === "Edm.Boolean") { | |
| 25 | if (typeof rawValue === "boolean" || rawValue === "true" || rawValue === "false") { | |
| 26 | payload[columnKey] = Boolean(rawValue); | |
| 27 | } else { | |
| 28 | this.addMessageToMessages("spreadsheetimporter.valueNotABoolean", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 29 | } | |
| 30 | } else if (metadataColumn.type === "Edm.Date") { | |
| 31 | let date = rawValue; | |
| 32 | if (value.sheetDataType !== "d") { | |
| 33 | const parsedDate = new Date(rawValue); | |
| 34 | if (isNaN(parsedDate.getTime())) { | |
| 35 | this.addMessageToMessages("spreadsheetimporter.invalidDate", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 36 | continue; | |
| 37 | } | |
| 38 | date = parsedDate; | |
| 39 | } | |
| 40 | try { | |
| 41 | this.checkDate(date, metadataColumn, util, messageHandler, index); | |
| 42 | const dateString = `${date.getUTCFullYear()}-${("0" + (date.getUTCMonth() + 1)).slice(-2)}-${("0" + date.getUTCDate()).slice(-2)}`; | |
| 43 | payload[columnKey] = dateString; | |
| 44 | } catch (error) { | |
| 45 | this.addMessageToMessages("spreadsheetimporter.errorWhileParsing", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 46 | } | |
| 47 | } else if (metadataColumn.type === "Edm.DateTimeOffset" || metadataColumn.type === "Edm.DateTime") { | |
| 48 | let date = rawValue; | |
| 49 | if (value.sheetDataType !== "d") { | |
| 50 | const parsedDate = new Date(rawValue); | |
| 51 | if (isNaN(parsedDate.getTime())) { | |
| 52 | this.addMessageToMessages("spreadsheetimporter.invalidDate", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 53 | continue; | |
| 54 | } | |
| 55 | date = parsedDate; | |
| 56 | } | |
| 57 | try { | |
| 58 | this.checkDate(date, metadataColumn, util, messageHandler, index); | |
| 59 | if(!metadataColumn.precision){ | |
| 60 | // If precision is not defined, remove milliseconds from date (from '2023-11-25T00:00:00Z' to '2023-11-25T00:00:00.000Z') | |
| 61 | // see https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/issues/600 | |
| 62 | payload[columnKey] = date.toISOString().replace(/\.\d{3}/, '') | |
| 63 | } else { | |
| 64 | payload[columnKey] = date; | |
| 65 | } | |
| 66 | } catch (error) { | |
| 67 | this.addMessageToMessages("spreadsheetimporter.errorWhileParsing", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 68 | } | |
| 69 | } else if (metadataColumn.type === "Edm.TimeOfDay" || metadataColumn.type === "Edm.Time") { | |
| 70 | let date = rawValue; | |
| 71 | | |
| 72 | // Only try to parse as Date if it's not marked as a date in sheet data | |
| 73 | if (value.sheetDataType !== "d") { | |
| 74 | date = new Date(rawValue); | |
| 75 | } | |
| 76 | | |
| 77 | if (date && !isNaN(date.getTime())) { | |
| 78 | // Successfully parsed to Date, format to only time part | |
| 79 | const timeFormatted = date.toISOString().substring(11, 19); | |
| 80 | payload[columnKey] = timeFormatted; | |
| 81 | } else { | |
| 82 | // Call the new method to parse time pattern if excel data is text not date | |
| 83 | const parsedTime = this.parseTimePattern(rawValue, util, messageHandler, index, metadataColumn); | |
| 84 | if (parsedTime) { | |
| 85 | payload[columnKey] = parsedTime; | |
| 86 | } | |
| 87 | } | |
| 88 | } else if ( | |
| 89 | metadataColumn.type === "Edm.UInt8" || | |
| 90 | metadataColumn.type === "Edm.Int16" || | |
| 91 | metadataColumn.type === "Edm.Int32" || | |
| 92 | metadataColumn.type === "Edm.Integer" || | |
| 93 | metadataColumn.type === "Edm.Int64" || | |
| 94 | metadataColumn.type === "Edm.Integer64" || | |
| 95 | metadataColumn.type === "Edm.Byte" || | |
| 96 | metadataColumn.type === "Edm.SByte" | |
| 97 | ) { | |
| 98 | try { | |
| 99 | const valueInteger = this.checkInteger(value, metadataColumn, util, messageHandler, index, component); | |
| 100 | // according to odata v2 spec, integer values are strings, v4 are numbers | |
| 101 | if (isODataV4) { | |
| 102 | // int64 are always strings | |
| 103 | if (metadataColumn.type === "Edm.Int64" || metadataColumn.type === "Edm.Integer64") { | |
| 104 | payload[columnKey] = valueInteger.toString(); | |
| 105 | } else { | |
| 106 | payload[columnKey] = valueInteger; | |
| 107 | } | |
| 108 | } else { | |
| 109 | // for OData V2 | |
| 110 | if (metadataColumn.type === "Edm.Int16" || metadataColumn.type === "Edm.Int32" || metadataColumn.type === "Edm.Byte" || metadataColumn.type === "Edm.SByte") { | |
| 111 | payload[columnKey] = valueInteger; | |
| 112 | } else { | |
| 113 | payload[columnKey] = valueInteger.toString(); | |
| 114 | } | |
| 115 | } | |
| 116 | } catch (error) { | |
| 117 | this.addMessageToMessages("spreadsheetimporter.errorWhileParsing", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 118 | } | |
| 119 | } else if (metadataColumn.type === "Edm.Double" || metadataColumn.type === "Edm.Decimal") { | |
| 120 | try { | |
| 121 | const valueDouble = this.checkDouble(value, metadataColumn, util, messageHandler, index, component); | |
| 122 | // according to odata v2 spec, integer values are strings, v4 are numbers | |
| 123 | if (isODataV4) { | |
| 124 | if (metadataColumn.type === "Edm.Double") { | |
| 125 | payload[columnKey] = valueDouble; | |
| 126 | } | |
| 127 | if (metadataColumn.type === "Edm.Decimal") { | |
| 128 | payload[columnKey] = valueDouble.toString(); | |
| 129 | } | |
| 130 | } else { | |
| 131 | // for OData V2 | |
| 132 | payload[columnKey] = valueDouble.toString(); | |
| 133 | } | |
| 134 | } catch (error) { | |
| 135 | this.addMessageToMessages("spreadsheetimporter.errorWhileParsing", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 136 | } | |
| 137 | } else if (metadataColumn.type === "Edm.Guid") { | |
| 138 | try { | |
| 139 | // Check if the value matches GUID format | |
| 140 | const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; | |
| 141 | if (typeof rawValue === "string" && guidPattern.test(rawValue)) { | |
| 142 | payload[columnKey] = rawValue; | |
| 143 | } else { | |
| 144 | this.addMessageToMessages("spreadsheetimporter.invalidGuid", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 145 | } | |
| 146 | } catch (error) { | |
| 147 | this.addMessageToMessages("spreadsheetimporter.errorWhileParsing", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 148 | } | |
| 149 | } else { | |
| 150 | // assign "" only if rawValue is undefined or null | |
| 151 | payload[columnKey] = `${rawValue ?? ""}`; | |
| 152 | } | |
| 153 | } | |
| 154 | } | |
| 155 | if (component.getSpreadsheetRowPropertyName()) { | |
| 156 | // @ts-ignore | |
| 157 | payload[component.getSpreadsheetRowPropertyName()] = row["__rowNum__"] + 1; | |
| 158 | } | |
| 159 | payloadArray.push(payload); | |
| 160 | } | |
| 161 | return payloadArray; | |
| 162 | } | |
| 163 | | |
| 164 | static checkDate(value: any, metadataColumn: Property, util: Util, messageHandler: MessageHandler, index: number) { | |
| 165 | if (isNaN(value.getTime())) { | |
| 166 | this.addMessageToMessages("spreadsheetimporter.invalidDate", util, messageHandler, index, [metadataColumn.label], value.rawValue); | |
| 167 | return false; | |
| 168 | } | |
| 169 | return true; | |
| 170 | } | |
| 171 | | |
| 172 | static checkDouble(value: ValueData, metadataColumn: Property, util: Util, messageHandler: MessageHandler, index: number, component: Component) { | |
| 173 | const rawValue = value.rawValue; | |
| 174 | let valueDouble = rawValue; | |
| 175 | if (typeof rawValue === "string") { | |
| 176 | const normalizedString = Util.normalizeNumberString(rawValue, component); | |
| 177 | valueDouble = parseFloat(normalizedString); | |
| 178 | // check if value is a number a does contain anything other than numbers and decimal seperator | |
| 179 | if (/[^0-9.,]/.test(valueDouble) || parseFloat(normalizedString).toString() !== normalizedString) { | |
| 180 | // Error: Value does contain anything other than numbers and decimal seperator | |
| 181 | this.addMessageToMessages("spreadsheetimporter.parsingErrorNotNumber", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 182 | } | |
| 183 | } | |
| 184 | return valueDouble; | |
| 185 | } | |
| 186 | | |
| 187 | static checkInteger(value: ValueData, metadataColumn: Property, util: Util, messageHandler: MessageHandler, index: number, component: Component) { | |
| 188 | const rawValue = value.rawValue; | |
| 189 | let valueInteger = rawValue; | |
| 190 | if (!Number.isInteger(valueInteger)) { | |
| 191 | if (typeof rawValue === "string") { | |
| 192 | const normalizedString = Util.normalizeNumberString(rawValue, component); | |
| 193 | valueInteger = parseInt(normalizedString); | |
| 194 | // check if value is a number a does contain anything other than numbers | |
| 195 | if (/[^0-9]/.test(valueInteger) || parseInt(normalizedString).toString() !== normalizedString.toString()) { | |
| 196 | // Error: Value does contain anything other than numbers | |
| 197 | this.addMessageToMessages("spreadsheetimporter.parsingErrorNotWholeNumber", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 198 | } | |
| 199 | } | |
| 200 | } | |
| 201 | return valueInteger; | |
| 202 | } | |
| 203 | | |
| 204 | static addMessageToMessages(text: string, util: Util, messageHandler: MessageHandler, index: number, array?: any, rawValue?: any, formattedValue?: any) { | |
| 205 | messageHandler.addMessageToMessages({ | |
| 206 | title: util.geti18nText(text, array), | |
| 207 | row: index + 2, | |
| 208 | type: CustomMessageTypes.ParsingError, | |
| 209 | counter: 1, | |
| 210 | rawValue: rawValue, | |
| 211 | formattedValue: formattedValue, | |
| 212 | ui5type: MessageType.Error | |
| 213 | }); | |
| 214 | } | |
| 215 | | |
| 216 | /** | |
| 217 | * Parses a time string according to specific patterns and returns the local time as a string. | |
| 218 | * This method handles raw time strings and validates them against the expected format. | |
| 219 | * The method supports time strings in the format "HH:mm:ss" and "HH:mm:ss.sss", where: | |
| 220 | * - HH represents hours (00 to 23), | |
| 221 | * - mm represents minutes (00 to 59), | |
| 222 | * - ss represents seconds (00 to 59), | |
| 223 | * - sss represents milliseconds (000 to 999). | |
| 224 | * | |
| 225 | * If the time string is valid and the components are within their respective ranges, | |
| 226 | * it constructs a Date object and formats the time to respect the local timezone. | |
| 227 | * If the time string does not match the expected pattern or components are out of range, | |
| 228 | * it logs an appropriate error message. | |
| 229 | * | |
| 230 | * @param {string} rawValue - The raw time string to be parsed. | |
| 231 | * @param {Util} util - Utility class instance for accessing helper functions like i18n. | |
| 232 | * @param {MessageHandler} messageHandler - MessageHandler class instance for logging errors. | |
| 233 | * @param {number} index - The row index of the data being parsed, used for error reporting. | |
| 234 | * @param {Property} metadataColumn - The metadata for the column, including the type label. | |
| 235 | * @returns {string|null} - Returns a formatted time string if successful, otherwise null. | |
| 236 | */ | |
| 237 | static parseTimePattern(rawValue: any, util: Util, messageHandler: MessageHandler, index: number, metadataColumn: Property) { | |
| 238 | const timePattern = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?$/; | |
| 239 | const match = rawValue.match(timePattern); | |
| 240 | | |
| 241 | if (match) { | |
| 242 | const hours = parseInt(match[1], 10); | |
| 243 | const minutes = parseInt(match[2], 10); | |
| 244 | const seconds = match[3] ? parseInt(match[3], 10) : 0; | |
| 245 | const milliseconds = match[4] ? parseInt(match[4], 10) : 0; | |
| 246 | | |
| 247 | // Validate time components | |
| 248 | if (hours < 24 && minutes < 60 && seconds < 60) { | |
| 249 | // Construct a Date object from time components | |
| 250 | let today = new Date(); | |
| 251 | today.setHours(hours, minutes, seconds, milliseconds); | |
| 252 | // Format the time considering the local timezone | |
| 253 | const timeFormatted = `${today.getHours().toString().padStart(2, "0")}:${today.getMinutes().toString().padStart(2, "0")}:${today.getSeconds().toString().padStart(2, "0")}`; | |
| 254 | return timeFormatted; | |
| 255 | } else { | |
| 256 | this.addMessageToMessages("spreadsheetimporter.invalidTime", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 257 | } | |
| 258 | } else { | |
| 259 | this.addMessageToMessages("spreadsheetimporter.invalidTimeFormat", util, messageHandler, index, [metadataColumn.label], rawValue); | |
| 260 | } | |
| 261 | return null; | |
| 262 | } | |
| 263 | } | |
| 264 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/Preview.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import Button from "sap/m/Button"; | |
| 3 | import Column from "sap/m/Column"; | |
| 4 | import ColumnListItem from "sap/m/ColumnListItem"; | |
| 5 | import Dialog from "sap/m/Dialog"; | |
| 6 | import Table from "sap/m/Table"; | |
| 7 | import JSONModel from "sap/ui/model/json/JSONModel"; | |
| 8 | import Text from "sap/m/Text"; | |
| 9 | import Util from "./Util"; | |
| 10 | import { ListObject } from "../types"; | |
| 11 | | |
| 12 | /** | |
| 13 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 14 | */ | |
| 15 | export default class Preview extends ManagedObject { | |
| 16 | dialog: Dialog; | |
| 17 | util: Util; | |
| 18 | constructor(util: Util) { | |
| 19 | super(); | |
| 20 | this.util = util; | |
| 21 | } | |
| 22 | | |
| 23 | showPreview(payload: any, typeLabelList: ListObject, previewColumns: string[]) { | |
| 24 | const table = this.createDynamicTable(payload, typeLabelList, previewColumns); | |
| 25 | if (typeof table === "undefined") { | |
| 26 | return; | |
| 27 | } | |
| 28 | this.dialog = new Dialog({ | |
| 29 | title: this.util.geti18nText("spreadsheetimporter.previewTableName"), | |
| 30 | content: [table], | |
| 31 | buttons: [ | |
| 32 | new Button({ | |
| 33 | text: this.util.geti18nText("spreadsheetimporter.messageDialogButtonClose"), | |
| 34 | press: () => { | |
| 35 | this.dialog.close(); | |
| 36 | } | |
| 37 | }) | |
| 38 | ], | |
| 39 | afterClose: () => { | |
| 40 | this.dialog.destroy(); | |
| 41 | } | |
| 42 | }); | |
| 43 | | |
| 44 | this.dialog.open(); | |
| 45 | } | |
| 46 | | |
| 47 | createDynamicTable(data: any[], typeLabelList: ListObject, previewColumns: string[]) { | |
| 48 | const table = new Table(); | |
| 49 | | |
| 50 | // get all column names from the data to show all columns with data in the pr | |
| 51 | const aColumns = Preview.getAllKeys(data); | |
| 52 | | |
| 53 | aColumns.forEach((column) => { | |
| 54 | // check if column is in previewColumns | |
| 55 | if (previewColumns && previewColumns.length > 0 && previewColumns.indexOf(column) === -1) { | |
| 56 | return; | |
| 57 | } | |
| 58 | const type = typeLabelList.get(column); | |
| 59 | const label = type && type.label ? type.label : column; | |
| 60 | const sapMColumn = new Column({ | |
| 61 | header: new Text({ text: label }) | |
| 62 | }); | |
| 63 | | |
| 64 | table.addColumn(sapMColumn); | |
| 65 | }); | |
| 66 | | |
| 67 | // Create a template for table rows | |
| 68 | const template = new ColumnListItem(); | |
| 69 | aColumns.forEach((column) => { | |
| 70 | let oCell; | |
| 71 | if (typeof data[0][column] === "object" && data[0][column] instanceof Date) { | |
| 72 | // show date in the format dd.mm.yyyy | |
| 73 | oCell = new Text({ text: `{path: '${column}', type: 'sap.ui.model.type.Date'}` }); | |
| 74 | } else { | |
| 75 | oCell = new Text({ text: "{" + column + "}" }); | |
| 76 | } | |
| 77 | template.addCell(oCell); | |
| 78 | }); | |
| 79 | | |
| 80 | // Bind the data to the table | |
| 81 | const model = new JSONModel(); | |
| 82 | model.setData(data); | |
| 83 | table.setModel(model); | |
| 84 | table.bindItems({ path: "/", template: template }); | |
| 85 | return table; | |
| 86 | } | |
| 87 | | |
| 88 | static getAllKeys(data: any[]): string[] { | |
| 89 | const allKeys = new Set<string>(); | |
| 90 | | |
| 91 | data.forEach((obj) => { | |
| 92 | if (obj && typeof obj === "object") { | |
| 93 | Object.keys(obj).forEach((key) => allKeys.add(key)); | |
| 94 | } | |
| 95 | }); | |
| 96 | | |
| 97 | return Array.from(allKeys); | |
| 98 | } | |
| 99 | } | |
| 100 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/SheetHandler.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | // @ts-nocheck | |
| 2 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 3 | import * as XLSX from "xlsx"; | |
| 4 | import { Sheet2JSONOpts, WorkSheet } from "xlsx"; | |
| 5 | import { ArrayData } from "../types"; | |
| 6 | /** | |
| 7 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 8 | */ | |
| 9 | export default class SheetHandler extends ManagedObject { | |
| 10 | constructor() { | |
| 11 | super(); | |
| 12 | } | |
| 13 | | |
| 14 | static sheet_to_json(sheet: WorkSheet, opts?: Sheet2JSONOpts): ArrayData { | |
| 15 | if (sheet == null || sheet["!ref"] == null) return []; | |
| 16 | var val = { t: "n", v: 0 }, | |
| 17 | header = 0, | |
| 18 | offset = 1, | |
| 19 | hdr = [], | |
| 20 | v = 0, | |
| 21 | vv = ""; | |
| 22 | var r = { s: { r: 0, c: 0 }, e: { r: 0, c: 0 } }; | |
| 23 | var o = opts || {}; | |
| 24 | var range = o.range != null ? o.range : sheet["!ref"]; | |
| 25 | if (o.header === 1) header = 1; | |
| 26 | else if (o.header === "A") header = 2; | |
| 27 | else if (Array.isArray(o.header)) header = 3; | |
| 28 | else if (o.header == null) header = 0; | |
| 29 | switch (typeof range) { | |
| 30 | case "string": | |
| 31 | r = this.safe_decode_range(range); | |
| 32 | break; | |
| 33 | case "number": | |
| 34 | r = this.safe_decode_range(sheet["!ref"]); | |
| 35 | r.s.r = range; | |
| 36 | break; | |
| 37 | default: | |
| 38 | r = range; | |
| 39 | } | |
| 40 | if (header > 0) offset = 0; | |
| 41 | var rr = XLSX.utils.encode_row(r.s.r); | |
| 42 | var cols = []; | |
| 43 | var out = []; | |
| 44 | var outi = 0, | |
| 45 | counter = 0; | |
| 46 | var dense = sheet["!data"] != null; | |
| 47 | var R = r.s.r, | |
| 48 | C = 0; | |
| 49 | var header_cnt = {}; | |
| 50 | if (dense && !sheet["!data"][R]) sheet["!data"][R] = []; | |
| 51 | var colinfo = (o.skipHidden && sheet["!cols"]) || []; | |
| 52 | var rowinfo = (o.skipHidden && sheet["!rows"]) || []; | |
| 53 | for (C = r.s.c; C <= r.e.c; ++C) { | |
| 54 | if ((colinfo[C] || {}).hidden) continue; | |
| 55 | cols[C] = XLSX.utils.encode_col(C); | |
| 56 | val = dense ? sheet["!data"][R][C] : sheet[cols[C] + rr]; | |
| 57 | switch (header) { | |
| 58 | case 1: | |
| 59 | hdr[C] = C - r.s.c; | |
| 60 | break; | |
| 61 | case 2: | |
| 62 | hdr[C] = cols[C]; | |
| 63 | break; | |
| 64 | case 3: | |
| 65 | hdr[C] = o.header[C - r.s.c]; | |
| 66 | break; | |
| 67 | default: | |
| 68 | if (val == null) val = { w: "__EMPTY", t: "s" }; | |
| 69 | vv = v = XLSX.utils.format_cell(val, null, o); | |
| 70 | counter = header_cnt[v] || 0; | |
| 71 | if (!counter) header_cnt[v] = 1; | |
| 72 | else { | |
| 73 | do { | |
| 74 | vv = v + "_" + counter++; | |
| 75 | } while (header_cnt[vv]); | |
| 76 | header_cnt[v] = counter; | |
| 77 | header_cnt[vv] = 1; | |
| 78 | } | |
| 79 | hdr[C] = vv; | |
| 80 | } | |
| 81 | } | |
| 82 | for (R = r.s.r + offset; R <= r.e.r; ++R) { | |
| 83 | if ((rowinfo[R] || {}).hidden) continue; | |
| 84 | var row = this.make_json_row(sheet, r, R, cols, header, hdr, o); | |
| 85 | if (row.isempty === false || (header === 1 ? o.blankrows !== false : !!o.blankrows)) out[outi++] = row.row; | |
| 86 | } | |
| 87 | out.length = outi; | |
| 88 | const renamedOut = this.renameAttributes(out); | |
| 89 | return renamedOut; | |
| 90 | } | |
| 91 | | |
| 92 | static make_json_row(sheet: WorkSheet, r, R, cols, header, hdr, o) { | |
| 93 | var rr = XLSX.utils.encode_row(R); | |
| 94 | var defval = o.defval, | |
| 95 | raw = o.raw || !Object.prototype.hasOwnProperty.call(o, "raw"); | |
| 96 | var isempty = true, | |
| 97 | dense = sheet["!data"] != null; | |
| 98 | var row = header === 1 ? [] : {}; | |
| 99 | if (header !== 1) { | |
| 100 | if (Object.defineProperty) | |
| 101 | try { | |
| 102 | Object.defineProperty(row, "__rowNum__", { value: R, enumerable: false }); | |
| 103 | } catch (e) { | |
| 104 | row.__rowNum__ = R; | |
| 105 | } | |
| 106 | else row.__rowNum__ = R; | |
| 107 | } | |
| 108 | if (!dense || sheet["!data"][R]) | |
| 109 | for (var C = r.s.c; C <= r.e.c; ++C) { | |
| 110 | var val = dense ? (sheet["!data"][R] || [])[C] : sheet[cols[C] + rr]; | |
| 111 | if (val === undefined || val.t === undefined) { | |
| 112 | if (defval === undefined) continue; | |
| 113 | if (hdr[C] != null) { | |
| 114 | row[hdr[C]] = defval; | |
| 115 | } | |
| 116 | continue; | |
| 117 | } | |
| 118 | var v = val.v; | |
| 119 | switch (val.t) { | |
| 120 | case "z": | |
| 121 | if (v == null) break; | |
| 122 | continue; | |
| 123 | case "e": | |
| 124 | v = v == 0 ? null : void 0; | |
| 125 | break; | |
| 126 | case "s": | |
| 127 | case "d": | |
| 128 | case "b": | |
| 129 | case "n": | |
| 130 | break; | |
| 131 | default: | |
| 132 | throw new Error("unrecognized type " + val.t); | |
| 133 | } | |
| 134 | if (hdr[C] != null) { | |
| 135 | if (v == null) { | |
| 136 | if (val.t == "e" && v === null) row[hdr[C]] = null; | |
| 137 | else if (defval !== undefined) row[hdr[C]] = defval; | |
| 138 | else if (raw && v === null) row[hdr[C]] = null; | |
| 139 | else continue; | |
| 140 | } else { | |
| 141 | //row[hdr[C]] = raw && (val.t !== "n" || (val.t === "n" && o.rawNumbers !== false)) ? v : XLSX.utils.format_cell(val,v,o); | |
| 142 | row[hdr[C]] = val; | |
| 143 | } | |
| 144 | if (v != null) isempty = false; | |
| 145 | } | |
| 146 | } | |
| 147 | return { row: row, isempty: isempty }; | |
| 148 | } | |
| 149 | | |
| 150 | static safe_decode_range(range) { | |
| 151 | var o = { s: { c: 0, r: 0 }, e: { c: 0, r: 0 } }; | |
| 152 | var idx = 0, | |
| 153 | i = 0, | |
| 154 | cc = 0; | |
| 155 | var len = range.length; | |
| 156 | for (idx = 0; i < len; ++i) { | |
| 157 | if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break; | |
| 158 | idx = 26 * idx + cc; | |
| 159 | } | |
| 160 | o.s.c = --idx; | |
| 161 | | |
| 162 | for (idx = 0; i < len; ++i) { | |
| 163 | if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break; | |
| 164 | idx = 10 * idx + cc; | |
| 165 | } | |
| 166 | o.s.r = --idx; | |
| 167 | | |
| 168 | if (i === len || cc != 10) { | |
| 169 | o.e.c = o.s.c; | |
| 170 | o.e.r = o.s.r; | |
| 171 | return o; | |
| 172 | } | |
| 173 | ++i; | |
| 174 | | |
| 175 | for (idx = 0; i != len; ++i) { | |
| 176 | if ((cc = range.charCodeAt(i) - 64) < 1 || cc > 26) break; | |
| 177 | idx = 26 * idx + cc; | |
| 178 | } | |
| 179 | o.e.c = --idx; | |
| 180 | | |
| 181 | for (idx = 0; i != len; ++i) { | |
| 182 | if ((cc = range.charCodeAt(i) - 48) < 0 || cc > 9) break; | |
| 183 | idx = 10 * idx + cc; | |
| 184 | } | |
| 185 | o.e.r = --idx; | |
| 186 | return o; | |
| 187 | } | |
| 188 | | |
| 189 | static renameAttributes(dataArray) { | |
| 190 | const renameAttributesInObject = (obj) => { | |
| 191 | Object.keys(obj).forEach((key) => { | |
| 192 | if (obj[key].hasOwnProperty("v")) { | |
| 193 | obj[key].rawValue = obj[key].v; | |
| 194 | delete obj[key].v; | |
| 195 | } | |
| 196 | if (obj[key].hasOwnProperty("t")) { | |
| 197 | obj[key].sheetDataType = obj[key].t; | |
| 198 | delete obj[key].t; | |
| 199 | } | |
| 200 | if (obj[key].hasOwnProperty("z")) { | |
| 201 | obj[key].format = obj[key].z; | |
| 202 | delete obj[key].z; | |
| 203 | } | |
| 204 | if (obj[key].hasOwnProperty("w")) { | |
| 205 | obj[key].formattedValue = obj[key].w; | |
| 206 | delete obj[key].w; | |
| 207 | } | |
| 208 | }); | |
| 209 | return obj; | |
| 210 | }; | |
| 211 | | |
| 212 | return dataArray.map(renameAttributesInObject); | |
| 213 | } | |
| 214 | } | |
| 215 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/TableSelector.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import Dialog from "sap/m/Dialog"; | |
| 3 | import Select from "sap/m/Select"; | |
| 4 | import Button from "sap/m/Button"; | |
| 5 | import Text from "sap/m/Text"; | |
| 6 | import Item from "sap/ui/core/Item"; | |
| 7 | import ResourceModel from "sap/ui/model/resource/ResourceModel"; | |
| 8 | import ResourceBundle from "sap/base/i18n/ResourceBundle"; | |
| 9 | | |
| 10 | /** | |
| 11 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 12 | */ | |
| 13 | export default class TableSelector extends ManagedObject { | |
| 14 | private _tables: any[] = []; | |
| 15 | private _i18nModel: ResourceModel; | |
| 16 | | |
| 17 | constructor(view: any) { | |
| 18 | super(); | |
| 19 | this._tables = this._getTableObject(view); | |
| 20 | this._i18nModel = new ResourceModel({ | |
| 21 | bundleName: "cc.spreadsheetimporter.XXXnamespaceXXX.i18n.i18n" | |
| 22 | }); | |
| 23 | } | |
| 24 | | |
| 25 | public getTables(): any[] { | |
| 26 | return this._tables; | |
| 27 | } | |
| 28 | | |
| 29 | public chooseTable(): Promise<any> { | |
| 30 | return new Promise((resolve, reject) => { | |
| 31 | if (this._tables.length === 0) { | |
| 32 | reject(new Error("No tables found")); | |
| 33 | } | |
| 34 | if (this._tables.length === 1) { | |
| 35 | resolve(this._tables[0]); | |
| 36 | } | |
| 37 | const select = new Select(); | |
| 38 | | |
| 39 | this._tables.forEach((table) => { | |
| 40 | select.addItem( | |
| 41 | new Item({ | |
| 42 | key: table.getId(), | |
| 43 | text: table.getBinding("items").getPath() | |
| 44 | }) | |
| 45 | ); | |
| 46 | }); | |
| 47 | | |
| 48 | const i18n = this._i18nModel.getResourceBundle() as ResourceBundle; | |
| 49 | | |
| 50 | const dialog = new Dialog({ | |
| 51 | title: i18n.getText("spreadsheetimporter.tableSelectorDialogTitle"), | |
| 52 | type: "Message", | |
| 53 | content: [select], | |
| 54 | beginButton: new Button({ | |
| 55 | text: i18n.getText("spreadsheetimporter.ok"), | |
| 56 | press: () => { | |
| 57 | const selectedKey = select.getSelectedKey(); | |
| 58 | const selectedTable = this._tables.find((table) => table.getId() === selectedKey); | |
| 59 | resolve(selectedTable); | |
| 60 | dialog.close(); | |
| 61 | } | |
| 62 | }), | |
| 63 | afterClose: () => dialog.destroy(), | |
| 64 | endButton: new Button({ | |
| 65 | text: i18n.getText("close"), | |
| 66 | press: () => { | |
| 67 | reject(new Error(i18n.getText("close"))); | |
| 68 | dialog.close(); | |
| 69 | } | |
| 70 | }) | |
| 71 | }) as Dialog; | |
| 72 | | |
| 73 | dialog.open(); | |
| 74 | }); | |
| 75 | } | |
| 76 | | |
| 77 | private _getTableObject(view: any) { | |
| 78 | return view.findAggregatedObjects(true, function (object: any) { | |
| 79 | return object.isA("sap.m.Table") || object.isA("sap.ui.table.Table"); | |
| 80 | }); | |
| 81 | } | |
| 82 | | |
| 83 | public get tables(): any[] { | |
| 84 | return this._tables; | |
| 85 | } | |
| 86 | public set tables(value: any[]) { | |
| 87 | this._tables = value; | |
| 88 | } | |
| 89 | } | |
| 90 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/Util.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import Log from "sap/base/Log"; | |
| 3 | import type ResourceBundle from "sap/base/i18n/ResourceBundle"; | |
| 4 | import MessageBox from "sap/m/MessageBox"; | |
| 5 | import type { DeepDownloadConfig, FireEventReturnType, RowData, UpdateConfig, ValueData } from "../types"; | |
| 6 | import type Component from "../Component"; | |
| 7 | import type { FieldMatchType } from "../enums"; | |
| 8 | import ObjectPool from "sap/ui/base/ObjectPool"; | |
| 9 | import Event from "sap/ui/base/Event"; | |
| 10 | import ts from "sap/ui/model/odata/v4/ts"; | |
| 11 | /** | |
| 12 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 13 | */ | |
| 14 | export default class Util extends ManagedObject { | |
| 15 | private resourceBundle: ResourceBundle; | |
| 16 | | |
| 17 | constructor(resourceBundle: ResourceBundle) { | |
| 18 | super(); | |
| 19 | this.resourceBundle = resourceBundle; | |
| 20 | } | |
| 21 | | |
| 22 | static getValueFromRow(row: RowData, label: string, type: string, fieldMatchType: FieldMatchType): ValueData { | |
| 23 | let value: ValueData | undefined; | |
| 24 | if (fieldMatchType === "label") { | |
| 25 | value = row[label]; | |
| 26 | } | |
| 27 | if (fieldMatchType === "labelTypeBrackets") { | |
| 28 | try { | |
| 29 | value = Object.entries(row).find(([key]) => key.includes(`[${type}]`))[1] as ValueData; | |
| 30 | } catch (error) { | |
| 31 | Log.debug(`Not found ${type}`, undefined, "SpreadsheetUpload: Util"); | |
| 32 | } | |
| 33 | } | |
| 34 | return value; | |
| 35 | } | |
| 36 | | |
| 37 | geti18nText(text: string, array?: any): string { | |
| 38 | return this.resourceBundle.getText(text, array); | |
| 39 | } | |
| 40 | | |
| 41 | static changeDecimalSeperator(value: string): number { | |
| 42 | // Replace thousands separator with empty string | |
| 43 | value = value.replace(/[.]/g, ""); | |
| 44 | // Replace decimal separator with a dot | |
| 45 | value = value.replace(/[,]/g, "."); | |
| 46 | // Convert string to number | |
| 47 | return parseFloat(value); | |
| 48 | } | |
| 49 | | |
| 50 | static sleep(ms: number) { | |
| 51 | return new Promise((resolve) => setTimeout(resolve, ms)); | |
| 52 | } | |
| 53 | | |
| 54 | static showError(error: any, className: string, methodName: string) { | |
| 55 | let detailsContent: any = ""; | |
| 56 | let errorMessage = ""; | |
| 57 | try { | |
| 58 | // code error | |
| 59 | if (error.stack) { | |
| 60 | errorMessage = error.message; | |
| 61 | // convert urls to links and to remove lines of the url | |
| 62 | const regex = /(http[s]?:\/\/[^\s]+):(\d+):(\d+)/g; | |
| 63 | let errorStack = error.stack.replace(regex, '<a href="$1" target="_blank" class="sapMLnk">$1</a>:<span class="line-no">$2:$3</span>').replace(/\n/g, "<br/>"); | |
| 64 | detailsContent = errorStack; | |
| 65 | } else { | |
| 66 | // OData error | |
| 67 | const errorObject = JSON.parse(error.responseText); | |
| 68 | errorMessage = errorObject.error.message.value; | |
| 69 | detailsContent = errorObject; | |
| 70 | } | |
| 71 | } catch (error) { | |
| 72 | errorMessage = "Generic Error"; | |
| 73 | detailsContent = error; | |
| 74 | } | |
| 75 | Log.error(errorMessage, error, `${className}.${methodName}`); | |
| 76 | MessageBox.error(errorMessage, { | |
| 77 | details: detailsContent, | |
| 78 | initialFocus: MessageBox.Action.CLOSE, | |
| 79 | actions: [MessageBox.Action.OK] | |
| 80 | }); | |
| 81 | } | |
| 82 | | |
| 83 | static showErrorMessage(errorMessage: string, className: string, methodName: string) { | |
| 84 | Log.error(errorMessage, `${className}.${methodName}`); | |
| 85 | MessageBox.error(errorMessage, { | |
| 86 | initialFocus: MessageBox.Action.CLOSE, | |
| 87 | actions: [MessageBox.Action.CANCEL] | |
| 88 | }); | |
| 89 | } | |
| 90 | | |
| 91 | static getBrowserDecimalAndThousandSeparators(componentDecimalSeparator: string) { | |
| 92 | let decimalSeparator = ""; | |
| 93 | let thousandSeparator = ""; | |
| 94 | if (componentDecimalSeparator === ",") { | |
| 95 | thousandSeparator = "."; | |
| 96 | decimalSeparator = ","; | |
| 97 | return { thousandSeparator, decimalSeparator }; | |
| 98 | } | |
| 99 | if (componentDecimalSeparator === ".") { | |
| 100 | thousandSeparator = ","; | |
| 101 | decimalSeparator = "."; | |
| 102 | return { decimalSeparator, thousandSeparator }; | |
| 103 | } | |
| 104 | const sampleNumber = 12345.6789; | |
| 105 | const formatted = new Intl.NumberFormat(navigator.language).format(sampleNumber); | |
| 106 | | |
| 107 | const withoutDigits = formatted.replace(/\d/g, ""); | |
| 108 | decimalSeparator = withoutDigits.charAt(withoutDigits.length - 1); | |
| 109 | thousandSeparator = withoutDigits.charAt(0); | |
| 110 | | |
| 111 | return { decimalSeparator, thousandSeparator }; | |
| 112 | } | |
| 113 | | |
| 114 | static normalizeNumberString(numberString: string, component: Component) { | |
| 115 | const { decimalSeparator, thousandSeparator } = this.getBrowserDecimalAndThousandSeparators(component.getDecimalSeparator()); | |
| 116 | | |
| 117 | // Remove thousand separators | |
| 118 | const stringWithoutThousandSeparators = numberString.replace(new RegExp(`\\${thousandSeparator}`, "g"), ""); | |
| 119 | | |
| 120 | // Replace the default decimal separator with the standard one | |
| 121 | const standardNumberString = stringWithoutThousandSeparators.replace(decimalSeparator, "."); | |
| 122 | | |
| 123 | return standardNumberString; | |
| 124 | } | |
| 125 | | |
| 126 | static getRandomString(length: number): string { | |
| 127 | const characters: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
| 128 | let randomString: string = ""; | |
| 129 | | |
| 130 | for (let i: number = 0; i < length; i++) { | |
| 131 | const randomIndex: number = Math.floor(Math.random() * characters.length); | |
| 132 | randomString += characters.charAt(randomIndex); | |
| 133 | } | |
| 134 | | |
| 135 | return randomString; | |
| 136 | } | |
| 137 | | |
| 138 | static stringify(obj: any): string { | |
| 139 | const seen = new WeakSet(); | |
| 140 | | |
| 141 | return JSON.stringify(obj, (key, value) => { | |
| 142 | // Check if value is an object and not null | |
| 143 | if (typeof value === "object" && value !== null) { | |
| 144 | // Handle circular references | |
| 145 | if (seen.has(value)) { | |
| 146 | return; | |
| 147 | } | |
| 148 | seen.add(value); | |
| 149 | | |
| 150 | // Handle first-level objects | |
| 151 | const keys = Object.keys(value); | |
| 152 | if (keys.every((k) => typeof value[k] !== "object" || value[k] === null)) { | |
| 153 | let simpleObject: { [key: string]: any } = {}; | |
| 154 | for (let k in value) { | |
| 155 | if (typeof value[k] !== "object" || value[k] === null) { | |
| 156 | simpleObject[k] = value[k]; | |
| 157 | } | |
| 158 | } | |
| 159 | return simpleObject; | |
| 160 | } | |
| 161 | } | |
| 162 | return value; | |
| 163 | }); | |
| 164 | } | |
| 165 | | |
| 166 | static extractObjects(objects: any[]): Record<string, any>[] { | |
| 167 | return objects.map((obj) => obj.getObject()); | |
| 168 | } | |
| 169 | | |
| 170 | static downloadSpreadsheetFile(arrayBuffer: ArrayBuffer, fileName: string): void { | |
| 171 | const blob: Blob = new Blob([arrayBuffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); | |
| 172 | const url: string = URL.createObjectURL(blob); | |
| 173 | | |
| 174 | const a: HTMLAnchorElement = document.createElement("a"); | |
| 175 | a.href = url; | |
| 176 | a.download = fileName; | |
| 177 | document.body.appendChild(a); | |
| 178 | a.click(); | |
| 179 | document.body.removeChild(a); | |
| 180 | | |
| 181 | URL.revokeObjectURL(url); | |
| 182 | } | |
| 183 | | |
| 184 | static async getLanguage(): Promise<string> { | |
| 185 | try { | |
| 186 | // getCore is not available in UI5 version 2.0 and above, prefer this over sap.ui.getCore().getConfiguration().getLanguage() | |
| 187 | const Localization = await Util.loadUI5RessourceAsync("sap/base/i18n/Localization"); | |
| 188 | return Localization.getLanguage(); | |
| 189 | } catch (error) { | |
| 190 | Log.debug("sap/base/i18n/Localization not found", undefined, "SpreadsheetUpload: checkForODataErrors"); | |
| 191 | } | |
| 192 | // ui5lint-disable-next-line -- fallback for UI5 versions below 2.0 | |
| 193 | return sap.ui.getCore().getConfiguration().getLanguage(); | |
| 194 | } | |
| 195 | | |
| 196 | static async loadUI5RessourceAsync(moduleName: string): Promise<any> { | |
| 197 | const alreadyLoadedModule = sap.ui.require(moduleName); | |
| 198 | if (alreadyLoadedModule) { | |
| 199 | return Promise.resolve(alreadyLoadedModule); | |
| 200 | } | |
| 201 | | |
| 202 | return new Promise(function (resolve, reject) { | |
| 203 | sap.ui.require( | |
| 204 | [moduleName], | |
| 205 | function (Module: unknown) { | |
| 206 | resolve(Module); | |
| 207 | }, | |
| 208 | function (err: any) { | |
| 209 | reject(err); | |
| 210 | }, | |
| 211 | ); | |
| 212 | }); | |
| 213 | } | |
| 214 | | |
| 215 | /** | |
| 216 | * Asynchronously fires an event with the given name and parameters on the specified component. | |
| 217 | * With this method, async methods can be attached and also sync methods | |
| 218 | * instead of the standard generated fireEvent methods, we call the methods directly | |
| 219 | * using promises to wait for the event handlers to complete | |
| 220 | * | |
| 221 | * @param eventName - The name of the event to be fired. | |
| 222 | * @param eventParameters - The parameters to be passed to the event handlers. | |
| 223 | * @param component - The component on which the event is fired. | |
| 224 | * @returns A promise that resolves when all event handlers have completed. | |
| 225 | */ | |
| 226 | static async fireEventAsync(eventName: string, eventParameters: object, component: Component): Promise<FireEventReturnType> { | |
| 227 | let aEventListeners, | |
| 228 | event, | |
| 229 | promises = []; | |
| 230 | | |
| 231 | // @ts-ignore | |
| 232 | const eventPool = new ObjectPool(Event); | |
| 233 | | |
| 234 | aEventListeners = (component as any).mEventRegistry[eventName]; | |
| 235 | | |
| 236 | if (Array.isArray(aEventListeners)) { | |
| 237 | // Avoid issues with 'concurrent modification' (e.g. if an event listener unregisters itself). | |
| 238 | aEventListeners = aEventListeners.slice(); | |
| 239 | event = eventPool.borrowObject(eventName, component, eventParameters); // borrow event lazily | |
| 240 | | |
| 241 | for (let oInfo of aEventListeners) { | |
| 242 | try { | |
| 243 | // Assuming each handler returns a promise | |
| 244 | promises.push(oInfo.fFunction.call(null, event)); | |
| 245 | } catch (error) { | |
| 246 | Log.error("Error in event handler:", error as Error); | |
| 247 | } | |
| 248 | } | |
| 249 | } | |
| 250 | | |
| 251 | // Wait for all promises (i.e., async handlers) to resolve | |
| 252 | await Promise.all(promises); | |
| 253 | | |
| 254 | return { | |
| 255 | bPreventDefault: (event as any)?.bPreventDefault, | |
| 256 | mParameters: (event as any)?.mParameters, | |
| 257 | returnValue: promises[0] | |
| 258 | }; | |
| 259 | } | |
| 260 | | |
| 261 | static mergeDeepDownloadConfig(defaultConfig: DeepDownloadConfig, providedConfig?: DeepDownloadConfig): DeepDownloadConfig { | |
| 262 | if (!providedConfig) return defaultConfig; | |
| 263 | | |
| 264 | // Deep merge for spreadsheetExportConfig | |
| 265 | const mergedDeepDownloadConfig: DeepDownloadConfig = { | |
| 266 | ...defaultConfig, | |
| 267 | ...providedConfig | |
| 268 | }; | |
| 269 | | |
| 270 | return mergedDeepDownloadConfig; | |
| 271 | } | |
| 272 | | |
| 273 | static mergeUpdateConfig(defaultConfig: UpdateConfig, providedConfig?: UpdateConfig): UpdateConfig { | |
| 274 | if (!providedConfig) return defaultConfig; | |
| 275 | | |
| 276 | const mergedUpdateConfig: UpdateConfig = { | |
| 277 | ...defaultConfig, | |
| 278 | ...providedConfig | |
| 279 | }; | |
| 280 | | |
| 281 | return mergedUpdateConfig; | |
| 282 | } | |
| 283 | } | |
| 284 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/dialog/ODataMessageHandler.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import Dialog from "sap/m/Dialog"; | |
| 3 | import { Messages } from "../../types"; | |
| 4 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 5 | import Fragment from "sap/ui/core/Fragment"; | |
| 6 | import JSONModel from "sap/ui/model/json/JSONModel"; | |
| 7 | import Util from "../Util"; | |
| 8 | import Log from "sap/base/Log"; | |
| 9 | | |
| 10 | /** | |
| 11 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 12 | */ | |
| 13 | export default class ODataMessageHandler extends ManagedObject { | |
| 14 | private messages: Messages[] = []; | |
| 15 | private spreadsheetUploadController: SpreadsheetUpload; | |
| 16 | private messageDialog: Dialog; | |
| 17 | | |
| 18 | constructor(spreadsheetUploadController: SpreadsheetUpload) { | |
| 19 | super(); | |
| 20 | this.messages = []; | |
| 21 | this.spreadsheetUploadController = spreadsheetUploadController; | |
| 22 | } | |
| 23 | /** | |
| 24 | * Display messages. | |
| 25 | */ | |
| 26 | async displayMessages(messageData: any) { | |
| 27 | const messageModel = new JSONModel(messageData); | |
| 28 | if (!this.messageDialog) { | |
| 29 | this.messageDialog = (await Fragment.load({ | |
| 30 | name: "cc.spreadsheetimporter.XXXnamespaceXXX.fragment.ODataMessagesDialog", | |
| 31 | type: "XML", | |
| 32 | controller: this | |
| 33 | })) as Dialog; | |
| 34 | } | |
| 35 | this.messageDialog.setModel(this.spreadsheetUploadController.componentI18n, "i18n"); | |
| 36 | this.messageDialog.setModel(messageModel, "message"); | |
| 37 | // this.messageDialog.setModel(Message, "message"); | |
| 38 | this.messageDialog.open(); | |
| 39 | } | |
| 40 | | |
| 41 | private async onCloseMessageDialog() { | |
| 42 | this.messageDialog.close(); | |
| 43 | // reset message manager messages | |
| 44 | try { | |
| 45 | // sap.ui.core.Messaging is only available in UI5 version 1.118 and above, prefer this over sap.ui.getCore().getMessageManager() = Util.loadUI5RessourceAsync("sap/ui/core/Messaging"); | |
| 46 | const Messaging = await Util.loadUI5RessourceAsync("sap/ui/core/Messaging"); | |
| 47 | Messaging.removeAllMessages(); | |
| 48 | return; | |
| 49 | } catch (error) { | |
| 50 | Log.debug("sap/ui/core/Messaging not found", undefined, "SpreadsheetUpload: checkForODataErrors"); | |
| 51 | } | |
| 52 | // ui5lint-disable-next-line -- fallback for UI5 versions below 1.118 | |
| 53 | sap.ui.getCore().getMessageManager().removeAllMessages(); | |
| 54 | } | |
| 55 | } | |
| 56 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/dialog/OptionsDialog.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import Log from "sap/base/Log"; | |
| 3 | import Dialog from "sap/m/Dialog"; | |
| 4 | import Fragment from "sap/ui/core/Fragment"; | |
| 5 | import JSONModel from "sap/ui/model/json/JSONModel"; | |
| 6 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 7 | | |
| 8 | /** | |
| 9 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 10 | */ | |
| 11 | export default class OptionsDialog extends ManagedObject { | |
| 12 | spreadsheetUploadController: SpreadsheetUpload; | |
| 13 | optionsDialog: Dialog; | |
| 14 | availableOptions = ["strict", "fieldMatchType", "decimalSeperator"]; | |
| 15 | | |
| 16 | constructor(spreadsheetUploadController: any) { | |
| 17 | super(); | |
| 18 | this.spreadsheetUploadController = spreadsheetUploadController; | |
| 19 | } | |
| 20 | | |
| 21 | async openOptionsDialog() { | |
| 22 | let showOptionsToUser = this.spreadsheetUploadController.component.getAvailableOptions(); | |
| 23 | if (showOptionsToUser.length === 0) { | |
| 24 | showOptionsToUser = this.availableOptions; | |
| 25 | } | |
| 26 | const availableOptionsData = this.availableOptions.reduce( | |
| 27 | (acc, key) => { | |
| 28 | acc[key] = showOptionsToUser.includes(key); | |
| 29 | return acc; | |
| 30 | }, | |
| 31 | {} as Record<string, boolean> | |
| 32 | ); | |
| 33 | this.spreadsheetUploadController.spreadsheetUploadDialogHandler.getDialog().setBusy(true); | |
| 34 | const optionsModel = new JSONModel({ | |
| 35 | strict: this.spreadsheetUploadController.component.getStrict(), | |
| 36 | fieldMatchType: this.spreadsheetUploadController.component.getFieldMatchType(), | |
| 37 | decimalSeparator: this.spreadsheetUploadController.component.getDecimalSeparator() | |
| 38 | }); | |
| 39 | const showOptionsModel = new JSONModel(availableOptionsData); | |
| 40 | Log.debug("openOptionsDialog", undefined, "SpreadsheetUpload: Options", () => | |
| 41 | this.spreadsheetUploadController.component.logger.returnObject({ | |
| 42 | strict: this.spreadsheetUploadController.component.getStrict(), | |
| 43 | fieldMatchType: this.spreadsheetUploadController.component.getFieldMatchType(), | |
| 44 | decimalSeparator: this.spreadsheetUploadController.component.getDecimalSeparator() | |
| 45 | }) | |
| 46 | ); | |
| 47 | if (!this.optionsDialog) { | |
| 48 | this.optionsDialog = (await Fragment.load({ | |
| 49 | name: "cc.spreadsheetimporter.XXXnamespaceXXX.fragment.OptionsDialog", | |
| 50 | type: "XML", | |
| 51 | controller: this | |
| 52 | })) as Dialog; | |
| 53 | this.optionsDialog.setModel(this.spreadsheetUploadController.componentI18n, "i18n"); | |
| 54 | } | |
| 55 | this.optionsDialog.setModel(optionsModel, "options"); | |
| 56 | this.optionsDialog.setModel(showOptionsModel, "availableOptions"); | |
| 57 | this.optionsDialog.open(); | |
| 58 | this.spreadsheetUploadController.spreadsheetUploadDialogHandler.getDialog().setBusy(false); | |
| 59 | } | |
| 60 | | |
| 61 | onSave() { | |
| 62 | this.spreadsheetUploadController.component.setFieldMatchType((this.optionsDialog.getModel("options") as JSONModel).getProperty("/fieldMatchType")); | |
| 63 | this.spreadsheetUploadController.component.setStrict((this.optionsDialog.getModel("options") as JSONModel).getProperty("/strict")); | |
| 64 | this.spreadsheetUploadController.component.setDecimalSeparator((this.optionsDialog.getModel("options") as JSONModel).getProperty("/decimalSeparator")); | |
| 65 | this.optionsDialog.close(); | |
| 66 | } | |
| 67 | | |
| 68 | onCancel() { | |
| 69 | this.optionsDialog.close(); | |
| 70 | } | |
| 71 | } | |
| 72 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/download/DataAssigner.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | | |
| 3 | /** | |
| 4 | * @namespace cc.spreadsheetimporter.download.XXXnamespaceXXX | |
| 5 | */ | |
| 6 | export default class DataAssigner extends ManagedObject { | |
| 7 | /** | |
| 8 | * Recursively assigns data to entities and their sub-entities | |
| 9 | * @param data - The data to be assigned | |
| 10 | * @param entity - The entity to assign data to | |
| 11 | */ | |
| 12 | assignData(data: any, entity: any): void { | |
| 13 | // Iterate through properties of the current entity | |
| 14 | for (const property in entity) { | |
| 15 | // If the property signifies a fetchable entity | |
| 16 | if (entity[property].$XYZFetchableEntity) { | |
| 17 | let subEntityDataTotal = []; | |
| 18 | for (const row in data) { | |
| 19 | const currentEntity = data[row]; | |
| 20 | const subEntityData = currentEntity[property]; | |
| 21 | if (subEntityData) { | |
| 22 | subEntityDataTotal = subEntityDataTotal.concat(subEntityData); | |
| 23 | } | |
| 24 | delete currentEntity[property]; // remove the data | |
| 25 | } | |
| 26 | entity[property].$XYZData = subEntityDataTotal; | |
| 27 | | |
| 28 | // Recursive call to handle deeper levels | |
| 29 | this.assignData(subEntityDataTotal, entity[property].$XYZEntity); | |
| 30 | } | |
| 31 | } | |
| 32 | } | |
| 33 | | |
| 34 | /** | |
| 35 | * Assigns data to the root entity | |
| 36 | * @param data - The data to be assigned | |
| 37 | * @param entity - The root entity | |
| 38 | * @param deepLevel - The level of deep download | |
| 39 | */ | |
| 40 | assignDataRoot(data: any, entity: any, deepLevel: number): void { | |
| 41 | // If deepLevel is 0 and data is an array, handle each string as a column | |
| 42 | if (deepLevel === 0 && Array.isArray(data)) { | |
| 43 | entity.$XYZData = []; | |
| 44 | let row = {}; | |
| 45 | for (const column of data) { | |
| 46 | if (entity.hasOwnProperty(column) && !entity[column].$XYZFetchableEntity) { | |
| 47 | if (!entity["$XYZColumns"]) entity["$XYZColumns"] = []; | |
| 48 | // You can customize the value below as needed | |
| 49 | row[column] = column; | |
| 50 | } | |
| 51 | } | |
| 52 | entity.$XYZData.push(row); | |
| 53 | } else { | |
| 54 | // Iterate through properties of the current entity (existing code) | |
| 55 | entity.$XYZData = []; | |
| 56 | let row = {}; | |
| 57 | for (const column in data) { | |
| 58 | if (entity.hasOwnProperty(column) && !entity[column].$XYZFetchableEntity) { | |
| 59 | if (!entity["$XYZColumns"]) entity["$XYZColumns"] = []; | |
| 60 | row[column] = data[column]; | |
| 61 | } | |
| 62 | } | |
| 63 | entity.$XYZData.push(row); | |
| 64 | } | |
| 65 | } | |
| 66 | | |
| 67 | /** | |
| 68 | * Assigns columns to the root entity | |
| 69 | * @param data - The data containing column information | |
| 70 | * @param entity - The root entity | |
| 71 | * @param deepLevel - The level of deep download | |
| 72 | */ | |
| 73 | assignColumnsRoot(data: any, entity: any, deepLevel: number): void { | |
| 74 | // If deepLevel is 0 and data is an array, handle each string as a column | |
| 75 | if (deepLevel === 0 && Array.isArray(data)) { | |
| 76 | for (const column of data) { | |
| 77 | if (entity.hasOwnProperty(column) && !entity[column].$XYZFetchableEntity) { | |
| 78 | if (!entity["$XYZColumns"]) entity["$XYZColumns"] = []; | |
| 79 | entity["$XYZColumns"].push(column); | |
| 80 | } | |
| 81 | } | |
| 82 | } else { | |
| 83 | // If deepLevel is not 0, we expect data to be an object | |
| 84 | for (const column in data) { | |
| 85 | if (entity.hasOwnProperty(column) && !entity[column].$XYZFetchableEntity) { | |
| 86 | if (!entity["$XYZColumns"]) entity["$XYZColumns"] = []; | |
| 87 | entity["$XYZColumns"].push(column); | |
| 88 | } | |
| 89 | } | |
| 90 | } | |
| 91 | } | |
| 92 | | |
| 93 | /** | |
| 94 | * Recursively assigns columns to sub-entities | |
| 95 | * @param data - The data containing column information | |
| 96 | * @param entity - The entity to assign columns to | |
| 97 | * @param deepLevel - The level of deep download (if 0, we expect data to be a string array, not an object) | |
| 98 | */ | |
| 99 | assignColumns(data: any, entity: any, deepLevel: number): void { | |
| 100 | // New check: if deepLevel is 0 and "data" is an object instead of a string array, throw error | |
| 101 | if (deepLevel !== 0 && Array.isArray(data)) { | |
| 102 | throw new Error("For deepLevel=0 (no deep download), 'data' must be an object, not an string array"); | |
| 103 | } | |
| 104 | | |
| 105 | // Existing code - do not remove existing comments | |
| 106 | for (const property in entity) { | |
| 107 | // If the property signifies a fetchable entity | |
| 108 | if (entity[property].$XYZFetchableEntity && data.hasOwnProperty(property)) { | |
| 109 | let subEntity = entity[property].$XYZEntity; | |
| 110 | // Defined Columns in the pro config | |
| 111 | for (const column in data[property]) { | |
| 112 | if (subEntity.hasOwnProperty(column) && !subEntity[column].$XYZFetchableEntity) { | |
| 113 | if (!entity[property]["$XYZColumns"]) entity[property]["$XYZColumns"] = []; | |
| 114 | entity[property]["$XYZColumns"].push(column); | |
| 115 | } | |
| 116 | } | |
| 117 | this.assignColumns(data[property], entity[property].$XYZEntity, deepLevel); | |
| 118 | } | |
| 119 | } | |
| 120 | } | |
| 121 | } | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetDownload.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import { EntityDefinition, EntityObject, PropertyWithOrder, DeepDownloadConfig } from "../../types"; | |
| 3 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 4 | import Component from "../../Component"; | |
| 5 | import OData from "../odata/OData"; | |
| 6 | import Util from "../Util"; | |
| 7 | import SpreadsheetGenerator from "./SpreadsheetGenerator"; | |
| 8 | import DataAssigner from "./DataAssigner"; | |
| 9 | import Log from "sap/base/Log"; | |
| 10 | /** | |
| 11 | * @namespace cc.spreadsheetimporter.download.XXXnamespaceXXX | |
| 12 | */ | |
| 13 | export default class SpreadsheetDownload extends ManagedObject { | |
| 14 | spreadsheetUploadController: SpreadsheetUpload; | |
| 15 | component: Component; | |
| 16 | odataHandler: OData; | |
| 17 | private spreadsheetGenerator: SpreadsheetGenerator; | |
| 18 | private dataAssigner: DataAssigner; | |
| 19 | | |
| 20 | constructor(spreadsheetUploadController: SpreadsheetUpload, component: Component, odataHandler: OData) { | |
| 21 | super(); | |
| 22 | this.spreadsheetUploadController = spreadsheetUploadController; | |
| 23 | this.component = component; | |
| 24 | this.odataHandler = odataHandler; | |
| 25 | this.spreadsheetGenerator = new SpreadsheetGenerator(spreadsheetUploadController, component, odataHandler); | |
| 26 | this.dataAssigner = new DataAssigner(); | |
| 27 | } | |
| 28 | | |
| 29 | // Function to extract the properties from input config and metadata | |
| 30 | async _extractProperties(proConfigColumns: any, entityMetadata: any, entityType: string): Promise<PropertyWithOrder[]> { | |
| 31 | const labelList = await this.odataHandler.getLabelList([], entityType, this.component.getExcludeColumns()); | |
| 32 | let properties: PropertyWithOrder[] = []; | |
| 33 | | |
| 34 | for (let prop in proConfigColumns) { | |
| 35 | if (proConfigColumns[prop].order !== undefined && proConfigColumns[prop].data !== undefined && entityMetadata[prop]) { | |
| 36 | const label = labelList.get(prop); | |
| 37 | let headerName: string; | |
| 38 | if (label) { | |
| 39 | headerName = `${label.label} [${entityType.split(".").pop()}][${prop}]`; | |
| 40 | } else { | |
| 41 | headerName = `${prop} [${entityType.split(".").pop()}][${prop}]`; | |
| 42 | } | |
| 43 | properties.push({ name: headerName, order: proConfigColumns[prop].order }); | |
| 44 | } else if (typeof proConfigColumns[prop] === "object") { | |
| 45 | properties = properties.concat(await this._extractProperties(proConfigColumns[prop], entityMetadata[prop].$XYZEntity, entityMetadata[prop].$Type)); | |
| 46 | } | |
| 47 | } | |
| 48 | | |
| 49 | return properties; | |
| 50 | } | |
| 51 | | |
| 52 | _findAttributeByType(obj: Record<string, EntityObject>, typeToSearch: string): string | undefined { | |
| 53 | for (const key in obj) { | |
| 54 | if (obj.hasOwnProperty(key)) { | |
| 55 | const entity = obj[key]; | |
| 56 | if (entity.$Type === typeToSearch) { | |
| 57 | return key; | |
| 58 | } | |
| 59 | } | |
| 60 | } | |
| 61 | return undefined; // if not found | |
| 62 | } | |
| 63 | | |
| 64 | public async fetchData(deepDownloadConfig: DeepDownloadConfig) { | |
| 65 | const { mainEntity, expands } = this.odataHandler.getODataEntitiesRecursive(this.spreadsheetUploadController.getOdataType(), deepDownloadConfig.deepLevel); | |
| 66 | // Log the mainEntity and expands | |
| 67 | Log.debug("MainEntity:", mainEntity, "SpreadsheetDownload: fetchData"); | |
| 68 | Log.debug("Expands:", expands, "SpreadsheetDownload: fetchData"); | |
| 69 | const batchSize = 1000; | |
| 70 | const customBinding = this.odataHandler.getBindingFromBinding(this.spreadsheetUploadController.binding, expands); | |
| 71 | | |
| 72 | // Start fetching the batches | |
| 73 | const totalResults = await this.odataHandler.fetchBatch(customBinding, batchSize); | |
| 74 | const data = Util.extractObjects(totalResults); | |
| 75 | | |
| 76 | if((this.component.getDeepDownloadConfig() as DeepDownloadConfig).setDraftStatus && (this.component.getDeepDownloadConfig() as DeepDownloadConfig).addKeysToExport){ | |
| 77 | for(const row in data){ | |
| 78 | // check if the row has a draft entity and IsActiveEntity is in the data row | |
| 79 | if(data[row].HasDraftEntity && typeof data[row].IsActiveEntity !== 'undefined'){ | |
| 80 | data[row].IsActiveEntity = false; | |
| 81 | } | |
| 82 | } | |
| 83 | } | |
| 84 | | |
| 85 | // Use the DataAssigner for all data assignments | |
| 86 | this.dataAssigner.assignData(data, mainEntity); | |
| 87 | this.dataAssigner.assignDataRoot(deepDownloadConfig.columns, mainEntity, deepDownloadConfig.deepLevel); | |
| 88 | this.dataAssigner.assignColumnsRoot(deepDownloadConfig.columns, mainEntity, deepDownloadConfig.deepLevel); | |
| 89 | this.dataAssigner.assignColumns(deepDownloadConfig.columns, mainEntity, deepDownloadConfig.deepLevel); | |
| 90 | | |
| 91 | mainEntity.$XYZData = data; | |
| 92 | return mainEntity; | |
| 93 | } | |
| 94 | } | |
| 95 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetDownloadDialog.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import Dialog from "sap/m/Dialog"; | |
| 3 | import Fragment from "sap/ui/core/Fragment"; | |
| 4 | import JSONModel from "sap/ui/model/json/JSONModel"; | |
| 5 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 6 | import { DeepDownloadConfig } from "../../types"; | |
| 7 | import SpreadsheetUploadDialog from "../dialog/SpreadsheetUploadDialog"; | |
| 8 | import Util from "../Util"; | |
| 9 | | |
| 10 | /** | |
| 11 | * @namespace cc.spreadsheetimporter.download.XXXnamespaceXXX | |
| 12 | */ | |
| 13 | export default class SpreadsheetDownloadDialog extends ManagedObject { | |
| 14 | spreadsheetUploadController: SpreadsheetUpload; | |
| 15 | spreadsheetDownloadDialog: any; | |
| 16 | spreadsheetOptionsModel: JSONModel; | |
| 17 | componentI18n: any; | |
| 18 | component: any; | |
| 19 | spreadsheetUploadDialog: SpreadsheetUploadDialog; | |
| 20 | | |
| 21 | constructor(spreadsheetUploadController: any, spreadsheetUploadDialog: SpreadsheetUploadDialog) { | |
| 22 | super(); | |
| 23 | this.spreadsheetUploadDialog = spreadsheetUploadDialog; | |
| 24 | this.spreadsheetUploadController = spreadsheetUploadController; | |
| 25 | this.componentI18n = this.spreadsheetUploadController.componentI18n; | |
| 26 | this.component = this.spreadsheetUploadController.component; | |
| 27 | } | |
| 28 | | |
| 29 | async createSpreadsheetDownloadDialog(): Promise<void> { | |
| 30 | this.spreadsheetUploadController.view.setBusyIndicatorDelay(0); | |
| 31 | this.spreadsheetUploadController.view.setBusy(true); | |
| 32 | if (!this.spreadsheetDownloadDialog) { | |
| 33 | this.spreadsheetOptionsModel = new JSONModel(this.component.getDeepDownloadConfig()); | |
| 34 | const modelData = this.spreadsheetOptionsModel.getData(); | |
| 35 | this.spreadsheetOptionsModel.setProperty("/filename", modelData.filename || this.spreadsheetUploadController.getOdataType()); | |
| 36 | this.spreadsheetDownloadDialog = (await Fragment.load({ | |
| 37 | name: "cc.spreadsheetimporter.XXXnamespaceXXX.fragment.SpreadsheetDownload", | |
| 38 | type: "XML", | |
| 39 | controller: this | |
| 40 | })) as Dialog; | |
| 41 | this.spreadsheetDownloadDialog.setBusyIndicatorDelay(0); | |
| 42 | this.spreadsheetDownloadDialog.setModel(this.componentI18n, "i18n"); | |
| 43 | this.spreadsheetDownloadDialog.setModel(this.spreadsheetOptionsModel, "spreadsheetOptions"); | |
| 44 | this.spreadsheetDownloadDialog.setModel(this.component.getModel("device"), "device"); | |
| 45 | } | |
| 46 | this.spreadsheetUploadController.view.setBusy(false); | |
| 47 | } | |
| 48 | | |
| 49 | onSave() { | |
| 50 | const deepDownloadConfig = this.spreadsheetDownloadDialog.getModel("spreadsheetOptions").getData() as DeepDownloadConfig; | |
| 51 | const mergedConfig = Util.mergeDeepDownloadConfig(this.component.getDeepDownloadConfig(), deepDownloadConfig); | |
| 52 | this.component.setDeepDownloadConfig(mergedConfig); | |
| 53 | this.spreadsheetUploadDialog.onDownloadDataSpreadsheet(); | |
| 54 | this.spreadsheetDownloadDialog.close(); | |
| 55 | } | |
| 56 | | |
| 57 | onCancel() { | |
| 58 | this.spreadsheetDownloadDialog.close(); | |
| 59 | } | |
| 60 | } | |
| 61 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetGenerator.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import * as XLSX from "xlsx"; | |
| 3 | import { EntityDefinition, DeepDownloadConfig } from "../../types"; | |
| 4 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 5 | import Component from "../../Component"; | |
| 6 | import OData from "../odata/OData"; | |
| 7 | import Util from "../Util"; | |
| 8 | import Log from "sap/base/Log"; | |
| 9 | | |
| 10 | /** | |
| 11 | * @namespace cc.spreadsheetimporter.download.XXXnamespaceXXX | |
| 12 | */ | |
| 13 | export default class SpreadsheetGenerator extends ManagedObject { | |
| 14 | spreadsheetUploadController: SpreadsheetUpload; | |
| 15 | component: Component; | |
| 16 | odataHandler: OData; | |
| 17 | currentLang: string; | |
| 18 | | |
| 19 | constructor(spreadsheetUploadController: SpreadsheetUpload, component: Component, odataHandler: OData) { | |
| 20 | super(); | |
| 21 | this.spreadsheetUploadController = spreadsheetUploadController; | |
| 22 | this.component = component; | |
| 23 | this.odataHandler = odataHandler; | |
| 24 | } | |
| 25 | | |
| 26 | async downloadSpreadsheet(entityDefinition: EntityDefinition, spreadsheetExportConfig: DeepDownloadConfig): Promise<void> { | |
| 27 | this.currentLang = await Util.getLanguage(); | |
| 28 | let filename = spreadsheetExportConfig.filename || this.spreadsheetUploadController.getOdataType() + ".xlsx"; | |
| 29 | const wb = XLSX.utils.book_new(); // creating the new spreadsheet work book | |
| 30 | | |
| 31 | await this._appendRootEntitySheet(wb, entityDefinition, spreadsheetExportConfig); | |
| 32 | if (spreadsheetExportConfig.deepExport) { | |
| 33 | await this._appendSiblingsSheetsRecursively(wb, entityDefinition, entityDefinition, spreadsheetExportConfig); | |
| 34 | } | |
| 35 | | |
| 36 | // check if filename ends with .xlsx if not add it | |
| 37 | if (!filename.endsWith(".xlsx")) { | |
| 38 | filename = filename.concat(".xlsx"); | |
| 39 | } | |
| 40 | | |
| 41 | let isDefaultPrevented = false; | |
| 42 | | |
| 43 | try { | |
| 44 | const asyncEventBeforeDownloadFileExport = await Util.fireEventAsync("beforeDownloadFileExport", { workbook: wb, filename: filename }, this.component); | |
| 45 | isDefaultPrevented = asyncEventBeforeDownloadFileExport.bPreventDefault; | |
| 46 | } catch (error) { | |
| 47 | Log.error("Error while calling the beforeDownloadFileExport event", error as Error, "SpreadsheetGenerator.ts", "downloadSpreadsheet"); | |
| 48 | } | |
| 49 | | |
| 50 | if (isDefaultPrevented) { | |
| 51 | return; | |
| 52 | } | |
| 53 | | |
| 54 | // download the created spreadsheet file | |
| 55 | XLSX.writeFile(wb, filename); | |
| 56 | } | |
| 57 | | |
| 58 | private async _appendRootEntitySheet(wb: XLSX.WorkBook, entityDefinition: EntityDefinition, spreadsheetExportConfig: DeepDownloadConfig): Promise<void> { | |
| 59 | if (entityDefinition.$XYZData) { | |
| 60 | const data = entityDefinition.$XYZData; | |
| 61 | const labelList = await this.odataHandler.getLabelList([], this.spreadsheetUploadController.getOdataType(), this.component.getExcludeColumns()); | |
| 62 | if (spreadsheetExportConfig.addKeysToExport) { | |
| 63 | this.odataHandler.addKeys(labelList, this.spreadsheetUploadController.getOdataType()); | |
| 64 | } | |
| 65 | const sheet = this._getSheet(labelList, data, entityDefinition["$XYZColumns"]); | |
| 66 | const sheetName = this.spreadsheetUploadController.getOdataType().split(".").pop(); | |
| 67 | if (wb.SheetNames.includes(sheetName)) { | |
| 68 | sheetName.concat("_1"); | |
| 69 | } | |
| 70 | XLSX.utils.book_append_sheet(wb, sheet, sheetName); | |
| 71 | } | |
| 72 | } | |
| 73 | | |
| 74 | private async _appendSiblingsSheetsRecursively( | |
| 75 | wb: XLSX.WorkBook, | |
| 76 | entityDefinition: EntityDefinition, | |
| 77 | parentEntity: any, | |
| 78 | spreadsheetExportConfig: DeepDownloadConfig | |
| 79 | ): Promise<void> { | |
| 80 | for (const property in entityDefinition) { | |
| 81 | const currentEntity = entityDefinition[property]; | |
| 82 | | |
| 83 | if (!currentEntity.$XYZFetchableEntity) continue; | |
| 84 | if (!currentEntity.$XYZData) continue; | |
| 85 | | |
| 86 | const data = currentEntity.$XYZData; | |
| 87 | const labelList = await this.odataHandler.getLabelList([], currentEntity.$Type, this.component.getExcludeColumns()); | |
| 88 | if (spreadsheetExportConfig.addKeysToExport) { | |
| 89 | this.odataHandler.addKeys(labelList, currentEntity.$Type, parentEntity, currentEntity.$Partner); | |
| 90 | } | |
| 91 | | |
| 92 | const sheet = this._getSheet(labelList, data, currentEntity["$XYZColumns"]); | |
| 93 | let sheetName = currentEntity.$Type.split(".").pop(); | |
| 94 | let suffix = 0; | |
| 95 | let originalSheetName = sheetName; | |
| 96 | let availableLength = 31 - ("_" + suffix).length; | |
| 97 | sheetName = originalSheetName.substring(0, availableLength) + (suffix > 0 ? "_" + suffix : ""); | |
| 98 | | |
| 99 | while (wb.SheetNames.includes(sheetName)) { | |
| 100 | suffix++; | |
| 101 | availableLength = 31 - ("_" + suffix).length; | |
| 102 | sheetName = originalSheetName.substring(0, availableLength) + "_" + suffix; | |
| 103 | } | |
| 104 | | |
| 105 | XLSX.utils.book_append_sheet(wb, sheet, sheetName); | |
| 106 | | |
| 107 | // Recursively append other nested navigation properties. | |
| 108 | // Also, pass the current entity as parent for the next level. | |
| 109 | await this._appendSiblingsSheetsRecursively(wb, currentEntity.$XYZEntity, entityDefinition, spreadsheetExportConfig); | |
| 110 | } | |
| 111 | } | |
| 112 | | |
| 113 | private _getSheet(labelList: any, dataArray: any, columnsConfig: string[]): XLSX.WorkSheet { | |
| 114 | let rows = dataArray.length; | |
| 115 | let fieldMatchType = this.component.getFieldMatchType(); | |
| 116 | var worksheet = {} as XLSX.WorkSheet; | |
| 117 | let colWidths: { wch: number }[] = []; // array to store column widths | |
| 118 | const colWidthDefault = 15; | |
| 119 | const colWidthDate = 20; | |
| 120 | let col = 0; | |
| 121 | let startRow = 0; | |
| 122 | | |
| 123 | // remove columns from map labelist that are not in the config | |
| 124 | if (columnsConfig && columnsConfig.length > 0) { | |
| 125 | for (let key of labelList.keys()) { | |
| 126 | const label = labelList.get(key); | |
| 127 | if (!columnsConfig.includes(key) && !label?.$XYZKey) { | |
| 128 | labelList.delete(key); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | | |
| 133 | // add column headers and data | |
| 134 | for (let [key, value] of labelList.entries()) { | |
| 135 | let width = colWidthDefault; | |
| 136 | let cell = { v: "", t: "s" } as XLSX.CellObject; | |
| 137 | let label = ""; | |
| 138 | if (fieldMatchType === "label") { | |
| 139 | label = value.label; | |
| 140 | } | |
| 141 | if (fieldMatchType === "labelTypeBrackets") { | |
| 142 | label = `${value.label}[${key}]`; | |
| 143 | } | |
| 144 | worksheet[XLSX.utils.encode_cell({ c: col, r: startRow })] = { v: label, t: "s" }; | |
| 145 | | |
| 146 | for (const [index, data] of dataArray.entries()) { | |
| 147 | let sampleDataValue = ""; | |
| 148 | rows = index + 1 + startRow; | |
| 149 | if (data.hasOwnProperty(key)) { | |
| 150 | sampleDataValue = data[key]; | |
| 151 | } else { | |
| 152 | worksheet[XLSX.utils.encode_cell({ c: col, r: rows })] = { v: "", t: "s" }; // Set the cell as empty | |
| 153 | continue; // Move to the next iteration | |
| 154 | } | |
| 155 | | |
| 156 | cell = this._getCellForType(value.type, sampleDataValue); | |
| 157 | if (value.type === "Edm.DateTimeOffset" || value.type === "Edm.DateTime") { | |
| 158 | width = colWidthDate; | |
| 159 | } | |
| 160 | | |
| 161 | worksheet[XLSX.utils.encode_cell({ c: col, r: rows })] = cell; | |
| 162 | } | |
| 163 | colWidths.push({ wch: width }); | |
| 164 | col++; | |
| 165 | } | |
| 166 | | |
| 167 | worksheet["!ref"] = XLSX.utils.encode_range({ s: { c: 0, r: 0 }, e: { c: col - 1, r: dataArray.length + startRow } }); | |
| 168 | worksheet["!cols"] = colWidths; // assign the column widths to the worksheet | |
| 169 | return worksheet; | |
| 170 | } | |
| 171 | | |
| 172 | private _getCellForType(type: string, value: any): XLSX.CellObject { | |
| 173 | switch (type) { | |
| 174 | case "Edm.Boolean": | |
| 175 | return { v: value, t: "b" }; | |
| 176 | case "Edm.String": | |
| 177 | case "Edm.Guid": | |
| 178 | case "Edm.Any": | |
| 179 | return { v: value, t: "s" }; | |
| 180 | case "Edm.DateTimeOffset": | |
| 181 | case "Edm.DateTime": | |
| 182 | const format = this.currentLang.startsWith("en") ? "mm/dd/yyyy hh:mm AM/PM" : "dd.mm.yyyy hh:mm"; | |
| 183 | return { v: value, t: "d", z: format }; | |
| 184 | case "Edm.Date": | |
| 185 | return { v: value, t: "d" }; | |
| 186 | case "Edm.TimeOfDay": | |
| 187 | case "Edm.Time": | |
| 188 | return { v: value, t: "d", z: "hh:mm" }; | |
| 189 | case "Edm.UInt8": | |
| 190 | case "Edm.Int16": | |
| 191 | case "Edm.Int32": | |
| 192 | case "Edm.Integer": | |
| 193 | case "Edm.Int64": | |
| 194 | case "Edm.Integer64": | |
| 195 | case "Edm.Double": | |
| 196 | case "Edm.Decimal": | |
| 197 | return { v: value, t: "n" }; | |
| 198 | default: | |
| 199 | return { v: value, t: "s" }; | |
| 200 | } | |
| 201 | } | |
| 202 | | |
| 203 | private async _extractProperties(proConfigColumns: any, entityMetadata: any, entityType: string): Promise<PropertyWithOrder[]> { | |
| 204 | const labelList = await this.odataHandler.getLabelList([], entityType, this.component.getExcludeColumns()); | |
| 205 | let properties: PropertyWithOrder[] = []; | |
| 206 | | |
| 207 | for (let prop in proConfigColumns) { | |
| 208 | if (proConfigColumns[prop].order !== undefined && proConfigColumns[prop].data !== undefined && entityMetadata[prop]) { | |
| 209 | const label = labelList.get(prop); | |
| 210 | let headerName: string; | |
| 211 | if (label) { | |
| 212 | headerName = `${label.label} [${entityType.split(".").pop()}][${prop}]`; | |
| 213 | } else { | |
| 214 | headerName = `${prop} [${entityType.split(".").pop()}][${prop}]`; | |
| 215 | } | |
| 216 | properties.push({ name: headerName, order: proConfigColumns[prop].order }); | |
| 217 | } else if (typeof proConfigColumns[prop] === "object") { | |
| 218 | properties = properties.concat(await this._extractProperties(proConfigColumns[prop], entityMetadata[prop].$XYZEntity, entityMetadata[prop].$Type)); | |
| 219 | } | |
| 220 | } | |
| 221 | | |
| 222 | return properties; | |
| 223 | } | |
| 224 | } | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandler.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import { Columns, ListObject, Property } from "../../types"; | |
| 3 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 4 | | |
| 5 | /** | |
| 6 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 7 | */ | |
| 8 | export default abstract class MetadataHandler extends ManagedObject { | |
| 9 | public spreadsheetUploadController: SpreadsheetUpload; | |
| 10 | | |
| 11 | constructor(spreadsheetUploadController: any) { | |
| 12 | super(); | |
| 13 | this.spreadsheetUploadController = spreadsheetUploadController; | |
| 14 | } | |
| 15 | | |
| 16 | parseI18nText(i18nMetadataText: string, view: any): string { | |
| 17 | let translatedText = ""; | |
| 18 | | |
| 19 | // remove the symbols from the start and end of the string | |
| 20 | const trimmedStr = i18nMetadataText.slice(1, -1); | |
| 21 | // split the string by the ">" symbol | |
| 22 | const splitStr = trimmedStr.split(">"); | |
| 23 | // check if there are exactly 2 parts before and after the ">" symbol | |
| 24 | if (splitStr.length === 2) { | |
| 25 | const resourceBundleName = splitStr[0]; | |
| 26 | const i18nPropertyName = splitStr[1]; | |
| 27 | const resourceBundle = view.getModel(resourceBundleName).getResourceBundle(); | |
| 28 | translatedText = resourceBundle.getText(i18nPropertyName, undefined, true); | |
| 29 | } | |
| 30 | if (!translatedText || translatedText === "") { | |
| 31 | return ""; | |
| 32 | } else { | |
| 33 | return translatedText; | |
| 34 | } | |
| 35 | } | |
| 36 | | |
| 37 | abstract getLabelList(columns: Columns, odataType: string, odataEntityType: any, excludeColumns: Columns): ListObject; | |
| 38 | abstract getKeyList(odataEntityType: any): string[]; | |
| 39 | abstract getODataEntitiesRecursive(entityName: string, deepLevel: number): any; | |
| 40 | abstract getKeys(binding: any, payload: any, IsActiveEntity?: boolean, excludeIsActiveEntity?: boolean): Record<string, any>; | |
| 41 | } | |
| 42 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV2.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Log from "sap/base/Log"; | |
| 2 | import { Columns, Property, ListObject, PropertyArray } from "../../types"; | |
| 3 | import MetadataHandler from "./MetadataHandler"; | |
| 4 | /** | |
| 5 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 6 | */ | |
| 7 | export default class MetadataHandlerV2 extends MetadataHandler { | |
| 8 | constructor(spreadsheetUploadController: any) { | |
| 9 | super(spreadsheetUploadController); | |
| 10 | } | |
| 11 | | |
| 12 | public getLabelList(columns: Columns, odataType: string, odataEntityType: any, excludeColumns: Columns): ListObject { | |
| 13 | let listObject: ListObject = new Map(); | |
| 14 | | |
| 15 | // get the property list of the entity for which we need to download the template | |
| 16 | const properties: PropertyArray = odataEntityType.property; | |
| 17 | const entityTypeLabel: string = odataEntityType["sap:label"]; | |
| 18 | Log.debug("SpreadsheetUpload: Annotations", undefined, "SpreadsheetUpload: MetadataHandler", () => this.spreadsheetUploadController.component.logger.returnObject(odataEntityType)); | |
| 19 | | |
| 20 | // check if file name is not set | |
| 21 | if (!this.spreadsheetUploadController.component.getSpreadsheetFileName() && entityTypeLabel) { | |
| 22 | this.spreadsheetUploadController.component.setSpreadsheetFileName(`${entityTypeLabel}.xlsx`); | |
| 23 | } else if (!this.spreadsheetUploadController.component.getSpreadsheetFileName() && !entityTypeLabel) { | |
| 24 | this.spreadsheetUploadController.component.setSpreadsheetFileName(`Template.xlsx`); | |
| 25 | } | |
| 26 | | |
| 27 | // excludeColumns will remove the columns from the list if columns are present | |
| 28 | if (columns.length > 0 && excludeColumns.length > 0) { | |
| 29 | columns = columns.filter((column) => !excludeColumns.includes(column)); | |
| 30 | } | |
| 31 | | |
| 32 | if (columns.length > 0) { | |
| 33 | for (const propertyName of columns) { | |
| 34 | const property = properties.find((property: any) => property.name === propertyName); | |
| 35 | if (property) { | |
| 36 | let propertyObject: Property = {} as Property; | |
| 37 | propertyObject.label = this.getLabel(odataEntityType, properties, property, propertyName); | |
| 38 | if (!propertyObject.label) { | |
| 39 | propertyObject.label = propertyName; | |
| 40 | } | |
| 41 | propertyObject.type = property["type"]; | |
| 42 | propertyObject.maxLength = property["maxLength"]; | |
| 43 | listObject.set(propertyName, propertyObject); | |
| 44 | } else { | |
| 45 | Log.warning(`SpreadsheetUpload: Property ${propertyName} not found`); | |
| 46 | } | |
| 47 | } | |
| 48 | } else if (columns.length === 0 && excludeColumns.length > 0) { | |
| 49 | for (const property of properties) { | |
| 50 | // if property is in excludeColumns, skip it | |
| 51 | if (excludeColumns.includes(property.name)) { | |
| 52 | continue; | |
| 53 | } | |
| 54 | let hiddenProperty = false; | |
| 55 | const propertyName = property.name; | |
| 56 | try { | |
| 57 | hiddenProperty = property["com.sap.vocabularies.UI.v1.Hidden"].Bool === "true"; | |
| 58 | } catch (error) { | |
| 59 | Log.debug(`No hidden property on ${property.name}`, undefined, "SpreadsheetUpload: MetadataHandler"); | |
| 60 | } | |
| 61 | if (!hiddenProperty && !propertyName.startsWith("SAP__")) { | |
| 62 | let propertyObject: Property = {} as Property; | |
| 63 | propertyObject.label = this.getLabel(odataEntityType, properties, property, propertyName); | |
| 64 | propertyObject.type = property["type"]; | |
| 65 | propertyObject.maxLength = property["maxLength"]; | |
| 66 | listObject.set(propertyName, propertyObject); | |
| 67 | } | |
| 68 | } | |
| 69 | } else { | |
| 70 | for (const property of properties) { | |
| 71 | let hiddenProperty = false; | |
| 72 | const propertyName = property.name; | |
| 73 | try { | |
| 74 | hiddenProperty = property["com.sap.vocabularies.UI.v1.Hidden"].Bool === "true"; | |
| 75 | } catch (error) { | |
| 76 | Log.debug(`No hidden property on ${property.name}`, undefined, "SpreadsheetUpload: MetadataHandler"); | |
| 77 | } | |
| 78 | if (!hiddenProperty && !propertyName.startsWith("SAP__")) { | |
| 79 | let propertyObject: Property = {} as Property; | |
| 80 | propertyObject.label = this.getLabel(odataEntityType, properties, property, propertyName); | |
| 81 | propertyObject.type = property["type"]; | |
| 82 | propertyObject.maxLength = property["maxLength"]; | |
| 83 | listObject.set(propertyName, propertyObject); | |
| 84 | } | |
| 85 | } | |
| 86 | } | |
| 87 | | |
| 88 | return listObject; | |
| 89 | } | |
| 90 | | |
| 91 | private getLabel(odataEntityType: { [x: string]: any }, properties: any, property: { [x: string]: any }, propertyName: string) { | |
| 92 | let label = ""; | |
| 93 | if (property["sap:label"]) { | |
| 94 | label = property["sap:label"]; | |
| 95 | } | |
| 96 | try { | |
| 97 | const lineItemsAnnotations = odataEntityType["com.sap.vocabularies.UI.v1.LineItem"]; | |
| 98 | label = lineItemsAnnotations.find((dataField: { Value: { Path: any } }) => dataField.Value.Path === propertyName).Label.String; | |
| 99 | } catch (error) { | |
| 100 | Log.debug(`SpreadsheetUpload: ${propertyName} not found as a LineItem Label`, undefined, "SpreadsheetUpload: MetadataHandlerV2"); | |
| 101 | } | |
| 102 | if (typeof label === 'string' && label.startsWith("{") && label.endsWith("}")) { | |
| 103 | try { | |
| 104 | label = this.parseI18nText(label, this.spreadsheetUploadController.view); | |
| 105 | } catch (error) { | |
| 106 | Log.debug(`SpreadsheetUpload: ${label} not found as a Resource Bundle and i18n text`, undefined, "SpreadsheetUpload: MetadataHandlerV2"); | |
| 107 | } | |
| 108 | } | |
| 109 | | |
| 110 | if (label === "") { | |
| 111 | label = propertyName; | |
| 112 | } | |
| 113 | return label; | |
| 114 | } | |
| 115 | | |
| 116 | /** | |
| 117 | * Creates a list of properties that are defined mandatory in the OData metadata V2 | |
| 118 | * @param odataType | |
| 119 | **/ | |
| 120 | getKeyList(odataEntityType: any): string[] { | |
| 121 | let keys: string[] = []; | |
| 122 | if (this.spreadsheetUploadController.component.getSkipMandatoryFieldCheck()) { | |
| 123 | return keys; | |
| 124 | } | |
| 125 | | |
| 126 | for (const property of odataEntityType.property) { | |
| 127 | // if property is mandatory, field should be in spreadsheet file | |
| 128 | const propertyName = property.name; | |
| 129 | // skip sap property | |
| 130 | if (propertyName.startsWith("SAP__")) { | |
| 131 | continue; | |
| 132 | } | |
| 133 | if ( | |
| 134 | !this.spreadsheetUploadController.component.getSkipMandatoryFieldCheck() && | |
| 135 | property["com.sap.vocabularies.Common.v1.FieldControl"] && | |
| 136 | property["com.sap.vocabularies.Common.v1.FieldControl"]["EnumMember"] && | |
| 137 | property["com.sap.vocabularies.Common.v1.FieldControl"]["EnumMember"] === "com.sap.vocabularies.Common.v1.FieldControlType/Mandatory" | |
| 138 | ) { | |
| 139 | keys.push(propertyName); | |
| 140 | } | |
| 141 | } | |
| 142 | return keys; | |
| 143 | } | |
| 144 | | |
| 145 | getODataEntitiesRecursive(entityName: string, deepLevel: number): any { | |
| 146 | throw new Error("Method not implemented."); | |
| 147 | } | |
| 148 | | |
| 149 | getKeys(binding: any, payload: any, IsActiveEntity?: boolean, excludeIsActiveEntity: boolean = false): Record<string, any> { | |
| 150 | throw new Error("Method not implemented."); | |
| 151 | } | |
| 152 | } | |
| 153 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/odata/OData.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import ManagedObject from "sap/ui/base/ManagedObject"; | |
| 2 | import DraftController from "sap/ui/generic/app/transaction/DraftController"; | |
| 3 | import { Columns, FireEventReturnType, ListObject, Property } from "../../types"; | |
| 4 | import ODataMessageHandler from "../dialog/ODataMessageHandler"; | |
| 5 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 6 | import Log from "sap/base/Log"; | |
| 7 | import MetadataHandlerV2 from "./MetadataHandlerV2"; | |
| 8 | import MetadataHandlerV4 from "./MetadataHandlerV4"; | |
| 9 | import TableSelector from "../TableSelector"; | |
| 10 | import JSONModel from "sap/ui/model/json/JSONModel"; | |
| 11 | import Fragment from "sap/ui/core/Fragment"; | |
| 12 | import Dialog from "sap/m/Dialog"; | |
| 13 | import Util from "../Util"; | |
| 14 | import ODataListBindingV2 from "sap/ui/model/odata/v2/ODataListBinding"; | |
| 15 | import ODataListBindingV4 from "sap/ui/model/odata/v4/ODataListBinding"; | |
| 16 | import MessageHandler from "../MessageHandler"; | |
| 17 | import MessageBox from "sap/m/MessageBox"; | |
| 18 | | |
| 19 | /** | |
| 20 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 21 | */ | |
| 22 | export default abstract class OData extends ManagedObject { | |
| 23 | draftController: DraftController; | |
| 24 | odataMessageHandler: ODataMessageHandler; | |
| 25 | private _tables: any[] = []; | |
| 26 | busyDialog: Dialog; | |
| 27 | spreadsheetUploadController: SpreadsheetUpload; | |
| 28 | public createPromises: Promise<any>[] = []; | |
| 29 | public createContexts: any[] = []; | |
| 30 | messageHandler: MessageHandler; | |
| 31 | util: Util; | |
| 32 | constructor(spreadsheetUploadController: SpreadsheetUpload, messageHandler: MessageHandler, util: Util) { | |
| 33 | super(); | |
| 34 | this.odataMessageHandler = new ODataMessageHandler(spreadsheetUploadController); | |
| 35 | this.spreadsheetUploadController = spreadsheetUploadController; | |
| 36 | this.messageHandler = messageHandler; | |
| 37 | this.util = util; | |
| 38 | } | |
| 39 | | |
| 40 | /** | |
| 41 | * Helper method to call OData service. | |
| 42 | * @param {*} fnResolve - The resolve function for the Promise. | |
| 43 | * @param {*} fnReject - The reject function for the Promise. | |
| 44 | */ | |
| 45 | async callOdata(fnResolve: any, fnReject: any, spreadsheetUploadController: SpreadsheetUpload): Promise<void> { | |
| 46 | const component = spreadsheetUploadController.component; | |
| 47 | const tableObject = spreadsheetUploadController.tableObject; | |
| 48 | const payloadArray = spreadsheetUploadController.payloadArray; | |
| 49 | const binding = spreadsheetUploadController.binding; | |
| 50 | const context = spreadsheetUploadController.context; | |
| 51 | spreadsheetUploadController.errorsFound = false; | |
| 52 | | |
| 53 | // intializing the message manager for displaying the odata response messages | |
| 54 | try { | |
| 55 | // get binding of table to create rows | |
| 56 | const model = binding.getModel(); | |
| 57 | | |
| 58 | await this.createBusyDialog(spreadsheetUploadController); | |
| 59 | | |
| 60 | // Slice the array into chunks of 'batchSize' if necessary, if UPDATE max batch size is 100 | |
| 61 | const slicedPayloadArray = this.processPayloadArray(component.getBatchSize(), payloadArray); | |
| 62 | (this.busyDialog.getModel("busyModel") as JSONModel).setProperty("/progressText", `0/${payloadArray.length}`); | |
| 63 | let currentProgressPercent = 0; | |
| 64 | let currentProgressValue = 0; | |
| 65 | | |
| 66 | // Loop over the sliced array | |
| 67 | for (const batch of slicedPayloadArray) { | |
| 68 | // loop over data from spreadsheet file | |
| 69 | try { | |
| 70 | // default for draft scenarios we need to request the object first to get draft status otherwise the update will fail | |
| 71 | // with options the strategy could be changed to make the update quicker | |
| 72 | // request all objects in the batch first | |
| 73 | if (component.getAction() === "UPDATE") { | |
| 74 | await this.getObjects(model, binding, batch); | |
| 75 | // TODO: decide to continue or break depending on component.getContinueOnError() | |
| 76 | // TODO: if getContinueOnError is true, continue with successfull fetched objects | |
| 77 | } | |
| 78 | | |
| 79 | // maybe move this loop to createAsync and updateAsync --> parameter will change (breaking change) | |
| 80 | for (let payload of batch) { | |
| 81 | let fireEventAsyncReturn: FireEventReturnType; | |
| 82 | // skip draft and directly create | |
| 83 | if (component.getCreateActiveEntity()) { | |
| 84 | payload.IsActiveEntity = true; | |
| 85 | } | |
| 86 | // Extension method to manipulate payload | |
| 87 | try { | |
| 88 | fireEventAsyncReturn = await Util.fireEventAsync("changeBeforeCreate", { payload: payload }, component); | |
| 89 | } catch (error) { | |
| 90 | Log.error("Error while calling the changeBeforeCreate event", error as Error, "SpreadsheetUpload: callOdata"); | |
| 91 | } | |
| 92 | if (fireEventAsyncReturn.returnValue) { | |
| 93 | payload = fireEventAsyncReturn.returnValue; | |
| 94 | } | |
| 95 | if (component.getAction() === "CREATE") { | |
| 96 | this.createAsync(model, binding, payload); | |
| 97 | } | |
| 98 | if (component.getAction() === "UPDATE") { | |
| 99 | this.updateAsync(model, binding, payload); | |
| 100 | } | |
| 101 | } | |
| 102 | // wait for all drafts to be created | |
| 103 | await this.submitChanges(model); | |
| 104 | let errorsFoundLocal = await this.checkForErrors(model, binding, component.getShowBackendErrorMessages()); | |
| 105 | if (errorsFoundLocal) { | |
| 106 | Log.error("Error while calling the odata service", "SpreadsheetUpload: callOdata"); | |
| 107 | if (!component.getContinueOnError()) { | |
| 108 | this.busyDialog.close(); | |
| 109 | spreadsheetUploadController.errorsFound = true; | |
| 110 | this.resetContexts(); | |
| 111 | fnReject("Error while calling the odata service"); | |
| 112 | break; | |
| 113 | } | |
| 114 | } else { | |
| 115 | await this.waitForCreation(); | |
| 116 | } | |
| 117 | | |
| 118 | // check for and activate all drafts and wait for all draft to be created | |
| 119 | // only if createActiveEntity is false and IsActiveEntity is not used in the payload | |
| 120 | if (!component.getCreateActiveEntity() && component.getActivateDraft() && !errorsFoundLocal) { | |
| 121 | await this.waitForDraft(); | |
| 122 | } | |
| 123 | | |
| 124 | this.resetContexts(); | |
| 125 | currentProgressPercent = currentProgressPercent + (batch.length / payloadArray.length) * 100; | |
| 126 | currentProgressValue = currentProgressValue + batch.length; | |
| 127 | (this.busyDialog.getModel("busyModel") as JSONModel).setProperty("/progressPercent", currentProgressPercent); | |
| 128 | (this.busyDialog.getModel("busyModel") as JSONModel).setProperty("/progressText", `${currentProgressValue} / ${payloadArray.length}`); | |
| 129 | } catch (error) { | |
| 130 | if (component.getContinueOnError()) { | |
| 131 | Log.error("Error while calling the odata service", error as Error, "SpreadsheetUpload: callOdata"); | |
| 132 | } else { | |
| 133 | // throw error to stop processing | |
| 134 | throw error; | |
| 135 | } | |
| 136 | } | |
| 137 | } | |
| 138 | if (tableObject) { | |
| 139 | spreadsheetUploadController.refreshBinding(context, binding, tableObject); | |
| 140 | } | |
| 141 | this.busyDialog.close(); | |
| 142 | fnResolve(); | |
| 143 | } catch (error) { | |
| 144 | this.busyDialog.close(); | |
| 145 | this.resetContexts(); | |
| 146 | Log.error("Error while calling the odata service", error as Error, "SpreadsheetUpload: callOdata"); | |
| 147 | await this.showInternalErrorDialog(error); | |
| 148 | await this.checkForODataErrors(component.getShowBackendErrorMessages()); | |
| 149 | fnReject(error); | |
| 150 | } | |
| 151 | } | |
| 152 | | |
| 153 | public getBindingFromTable(tableObject: any): any { | |
| 154 | if (tableObject.getMetadata().getName() === "sap.m.Table" || tableObject.getMetadata().getName() === "sap.m.List") { | |
| 155 | return tableObject.getBinding("items"); | |
| 156 | } | |
| 157 | if (tableObject.getMetadata().getName() === "sap.ui.table.Table") { | |
| 158 | return tableObject.getBinding("rows"); | |
| 159 | } | |
| 160 | } | |
| 161 | | |
| 162 | public _getActionName(context: any, sOperation: string) { | |
| 163 | const model = (context?.getModel && context.getModel()) || context.getView().getModel(), | |
| 164 | metaModel = model.getMetaModel(), | |
| 165 | entitySetPath = metaModel.getMetaPath(context.getPath()); | |
| 166 | return metaModel.getObject("".concat(entitySetPath, "@com.sap.vocabularies.Common.v1.DraftRoot/").concat(sOperation)); | |
| 167 | } | |
| 168 | | |
| 169 | // Slice the array into chunks of 'batchSize' if necessary | |
| 170 | public processPayloadArray(batchSize: number, payloadArray: string | any[]) { | |
| 171 | // For UPDATE actions, enforce max batch size of 100 | |
| 172 | if (this.spreadsheetUploadController.component.getAction() === "UPDATE") { | |
| 173 | batchSize = Math.min(batchSize > 0 ? batchSize : 100, 100); | |
| 174 | } | |
| 175 | | |
| 176 | if (batchSize > 0) { | |
| 177 | let slicedPayloadArray = []; | |
| 178 | const numOfSlices = Math.ceil(payloadArray.length / batchSize); | |
| 179 | const equalSize = Math.ceil(payloadArray.length / numOfSlices); | |
| 180 | | |
| 181 | for (let i = 0; i < payloadArray.length; i += equalSize) { | |
| 182 | slicedPayloadArray.push(payloadArray.slice(i, i + equalSize)); | |
| 183 | } | |
| 184 | return slicedPayloadArray; | |
| 185 | } else { | |
| 186 | return [payloadArray]; | |
| 187 | } | |
| 188 | } | |
| 189 | | |
| 190 | public async getTableObject(tableId: string, view: any, spreadsheetUploadController: SpreadsheetUpload): any { | |
| 191 | // try get object page table | |
| 192 | if (!tableId) { | |
| 193 | this.tables = view.findAggregatedObjects(true, function (object: any) { | |
| 194 | return object.isA("sap.m.Table") || object.isA("sap.ui.table.Table"); | |
| 195 | }); | |
| 196 | if (this.tables.length > 1 && !spreadsheetUploadController.component.getUseTableSelector()) { | |
| 197 | throw new Error("Found more than one table on Object Page.\n Please specify table in option 'tableId'"); | |
| 198 | } else if (this.tables.length > 1 && spreadsheetUploadController.component.getUseTableSelector()) { | |
| 199 | const tableSelector = new TableSelector(view); | |
| 200 | let selectedTable; | |
| 201 | try { | |
| 202 | selectedTable = await tableSelector.chooseTable(); | |
| 203 | } catch (error) { | |
| 204 | // user canceled or no table found | |
| 205 | throw new Error(spreadsheetUploadController.util.geti18nText("spreadsheetimporter.tableSelectorDialogCancel")); | |
| 206 | } | |
| 207 | return selectedTable; | |
| 208 | } else if (this.tables.length === 0) { | |
| 209 | throw new Error("Found more than one table on Object Page.\n Please specify table in option 'tableId'"); | |
| 210 | } else { | |
| 211 | return this.tables[0]; | |
| 212 | } | |
| 213 | } else { | |
| 214 | return view.byId(tableId); | |
| 215 | } | |
| 216 | } | |
| 217 | | |
| 218 | private async createBusyDialog(spreadsheetUploadController: SpreadsheetUpload) { | |
| 219 | const busyModel = new JSONModel({ | |
| 220 | progressPercent: 0, | |
| 221 | progressText: "0" | |
| 222 | }); | |
| 223 | if (!this.busyDialog) { | |
| 224 | this.busyDialog = (await Fragment.load({ | |
| 225 | name: "cc.spreadsheetimporter.XXXnamespaceXXX.fragment.BusyDialogProgress", | |
| 226 | controller: this | |
| 227 | })) as Dialog; | |
| 228 | } | |
| 229 | this.busyDialog.setModel(busyModel, "busyModel"); | |
| 230 | this.busyDialog.setModel(spreadsheetUploadController.component.getModel("device"), "device"); | |
| 231 | this.busyDialog.setModel(spreadsheetUploadController.component.getModel("i18n"), "i18n"); | |
| 232 | this.busyDialog.open(); | |
| 233 | } | |
| 234 | | |
| 235 | private async checkForODataErrors(showBackendErrorMessages: Boolean) { | |
| 236 | if (showBackendErrorMessages) { | |
| 237 | try { | |
| 238 | // sap.ui.core.Messaging is only available in UI5 version 1.118 and above, prefer this over sap.ui.getCore().getMessageManager()saging = Util.loadUI5RessourceAsync("sap/ui/core/Messaging"); | |
| 239 | const Messaging = await Util.loadUI5RessourceAsync("sap/ui/core/Messaging"); | |
| 240 | const messages = Messaging.getMessageModel().getData(); | |
| 241 | if (messages.length > 0) { | |
| 242 | this.odataMessageHandler.displayMessages(messages); | |
| 243 | } | |
| 244 | return; | |
| 245 | } catch (error) { | |
| 246 | Log.debug("sap/ui/core/Messaging not found", undefined, "SpreadsheetUpload: checkForODataErrors"); | |
| 247 | } | |
| 248 | // ui5lint-disable-next-line -- fallback for UI5 versions below 1.118 | |
| 249 | const messages = sap.ui.getCore().getMessageManager().getMessageModel().getData(); | |
| 250 | if (messages.length > 0) { | |
| 251 | this.odataMessageHandler.displayMessages(messages); | |
| 252 | } | |
| 253 | } | |
| 254 | } | |
| 255 | | |
| 256 | private async showInternalErrorDialog(error: any) { | |
| 257 | MessageBox.error(error.message); | |
| 258 | } | |
| 259 | | |
| 260 | getView(context: any): any { | |
| 261 | return context._view || context.oView || context.getView(); | |
| 262 | } | |
| 263 | | |
| 264 | public get tables(): any[] { | |
| 265 | return this._tables; | |
| 266 | } | |
| 267 | public set tables(value: any[]) { | |
| 268 | this._tables = value; | |
| 269 | } | |
| 270 | | |
| 271 | abstract create(model: any, binding: any, payload: any): any; | |
| 272 | abstract createAsync(model: any, binding: any, payload: any): any; | |
| 273 | abstract updateAsync(model: any, binding: any, payload: any): any; | |
| 274 | abstract submitChanges(model: any): Promise<any>; | |
| 275 | abstract waitForCreation(): Promise<any>; | |
| 276 | abstract waitForDraft(): void; | |
| 277 | abstract resetContexts(): void; | |
| 278 | abstract getMetadataHandler(): MetadataHandlerV2 | MetadataHandlerV4; | |
| 279 | abstract getLabelList(columns: Columns, odataType: string, excludeColumns: Columns, binding?: any): Promise<ListObject>; | |
| 280 | abstract getKeyList(odataType: string, tableObject: any): Promise<string[]>; | |
| 281 | abstract getOdataType(binding: any, odataType: any): string; | |
| 282 | abstract checkForErrors(model: any, binding: any, showBackendErrorMessages: Boolean): Promise<boolean>; | |
| 283 | abstract createCustomBinding(binding: any): any; | |
| 284 | abstract getODataEntitiesRecursive(entityName: string, deepLevel: number): any; | |
| 285 | abstract getBindingFromBinding(binding: any, expand?: any): ODataListBindingV4 | ODataListBindingV2; | |
| 286 | abstract fetchBatch(customBinding: ODataListBindingV4 | ODataListBindingV2, batchSize: number): Promise<any>; | |
| 287 | abstract addKeys(labelList: ListObject, entityName: string, parentEntity?: any, partner?: string): void; | |
| 288 | abstract getObjects(model: any, binding: any, batch: any): Promise<any>; | |
| 289 | // Pro Methods | |
| 290 | } | |
| 291 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV2.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Log from "sap/base/Log"; | |
| 2 | import { Columns } from "../../types"; | |
| 3 | import SpreadsheetUpload from "../SpreadsheetUpload"; | |
| 4 | import OData from "./OData"; | |
| 5 | import MetadataHandlerV2 from "./MetadataHandlerV2"; | |
| 6 | import ODataListBinding from "sap/ui/model/odata/v2/ODataListBinding"; | |
| 7 | import ODataModel from "sap/ui/model/odata/v2/ODataModel"; | |
| 8 | import ODataMetaModel from "sap/ui/model/odata/ODataMetaModel"; | |
| 9 | import MessageHandler from "../MessageHandler"; | |
| 10 | import Util from "../Util"; | |
| 11 | | |
| 12 | /** | |
| 13 | * @namespace cc.spreadsheetimporter.XXXnamespaceXXX | |
| 14 | */ | |
| 15 | export default class ODataV2 extends OData { | |
| 16 | customBinding: ODataListBinding; | |
| 17 | submitChangesResponse: any; | |
| 18 | private metadataHandler: MetadataHandlerV2; | |
| 19 | | |
| 20 | constructor(spreadsheetUploadController: SpreadsheetUpload, messageHandler: MessageHandler, util: Util) { | |
| 21 | super(spreadsheetUploadController, messageHandler, util); | |
| 22 | this.metadataHandler = new MetadataHandlerV2(spreadsheetUploadController); | |
| 23 | } | |
| 24 | create(model: any, binding: any, payload: any) { | |
| 25 | const submitChangesPromise = (binding: ODataListBinding, payload: any) => { | |
| 26 | return new Promise((resolve, reject) => { | |
| 27 | // @ts-ignore | |
| 28 | let context = (this.customBinding.getModel() as ODataModel).createEntry(this.customBinding.sDeepPath, { | |
| 29 | properties: payload, | |
| 30 | success: () => { | |
| 31 | resolve(context); | |
| 32 | }, | |
| 33 | error: (error: Error) => { | |
| 34 | reject(error); | |
| 35 | } | |
| 36 | }); | |
| 37 | }); | |
| 38 | }; | |
| 39 | return submitChangesPromise(this.customBinding, payload); | |
| 40 | } | |
| 41 | | |
| 42 | createAsync(model: any, binding: any, payload: any) { | |
| 43 | const returnObject = this.create(model, this.customBinding, payload); | |
| 44 | this.createPromises.push(returnObject); | |
| 45 | } | |
| 46 | | |
| 47 | updateAsync(model: any, binding: any, payload: any) { | |
| 48 | throw new Error("Method not implemented."); | |
| 49 | } | |
| 50 | | |
| 51 | async checkForErrors(model: any, binding: any, showBackendErrorMessages: Boolean): Promise<boolean> { | |
| 52 | // check if this.submitChangesResponse and this.submitChangesResponse.__batchResponses exist | |
| 53 | if (this.submitChangesResponse && this.submitChangesResponse.__batchResponses) { | |
| 54 | const firstResponse = this.submitChangesResponse.__batchResponses[0]; | |
| 55 | // check if firstResponse and firstResponse.response exist and that statusCode is >= 400 | |
| 56 | if (firstResponse && firstResponse.response && firstResponse.response.statusCode >= 400) { | |
| 57 | // show messages from the Messages Manager Model | |
| 58 | if (showBackendErrorMessages) { | |
| 59 | this.odataMessageHandler.displayMessages(); | |
| 60 | } | |
| 61 | return true; | |
| 62 | } | |
| 63 | } | |
| 64 | return false; | |
| 65 | } | |
| 66 | | |
| 67 | async createCustomBinding(binding: any) { | |
| 68 | if (this.spreadsheetUploadController.component.getOdataType()) { | |
| 69 | const metaModel = this.spreadsheetUploadController.view.getModel().getMetaModel(); | |
| 70 | await metaModel.loaded(); | |
| 71 | const odataEntityType = metaModel.getODataEntityType(this.spreadsheetUploadController.component.getOdataType()); | |
| 72 | const odataEntitySet = metaModel.getODataEntityContainer().entitySet.find((item) => item.entityType === `${odataEntityType.namespace}.${odataEntityType.name}`); | |
| 73 | this.customBinding = new ODataListBinding(this.spreadsheetUploadController.view.getModel() as ODataModel, `/${odataEntitySet.name}`); | |
| 74 | } else { | |
| 75 | this.customBinding = binding; | |
| 76 | } | |
| 77 | } | |
| 78 | | |
| 79 | async submitChanges(model: ODataModel) { | |
| 80 | const submitChangesPromise = (model: ODataModel) => { | |
| 81 | return new Promise((resolve, reject) => { | |
| 82 | model.submitChanges({ | |
| 83 | success: (data: any) => { | |
| 84 | resolve(data); | |
| 85 | }, | |
| 86 | error: (error: Error) => { | |
| 87 | reject(error); | |
| 88 | } | |
| 89 | }); | |
| 90 | }); | |
| 91 | }; | |
| 92 | | |
| 93 | try { | |
| 94 | this.submitChangesResponse = await submitChangesPromise(model); | |
| 95 | } catch (error: any) { | |
| 96 | Log.error(error); | |
| 97 | } | |
| 98 | } | |
| 99 | | |
| 100 | async waitForCreation() { | |
| 101 | this.createContexts = await Promise.all(this.createPromises); | |
| 102 | } | |
| 103 | | |
| 104 | async waitForDraft(): Promise<any[]> { | |
| 105 | const activateActionsPromises = []; | |
| 106 | for (let index = 0; index < this.createContexts.length; index++) { | |
| 107 | const element = this.createContexts[index]; | |
| 108 | if (this.draftController.getDraftContext().hasDraft(element)) { | |
| 109 | // this will fail i.e. in a Object Page Table, maybe better way to check, hasDraft is still true | |
| 110 | try { | |
| 111 | const checkImport = this.draftController.getDraftContext().getODataDraftFunctionImportName(element, "ActivationAction"); | |
| 112 | if (checkImport !== null) { | |
| 113 | const activationPromise = this.draftController.activateDraftEntity(element, true, undefined); | |
| 114 | activateActionsPromises.push(activationPromise); | |
| 115 | } | |
| 116 | } catch (error) { | |
| 117 | Log.error("Activate Draft failed", error as Error, "SpreadsheetUpload: OdataV2"); | |
| 118 | } | |
| 119 | } | |
| 120 | } | |
| 121 | return Promise.all(activateActionsPromises); | |
| 122 | } | |
| 123 | | |
| 124 | async getOdataType(binding: any, odataType: any) { | |
| 125 | if (!odataType) { | |
| 126 | return binding._getEntityType().entityType; | |
| 127 | } else { | |
| 128 | const metaModel = this.spreadsheetUploadController.view.getModel().getMetaModel() as ODataMetaModel; | |
| 129 | await metaModel.loaded(); | |
| 130 | const odataEntityType = metaModel.getODataEntityType(odataType); | |
| 131 | if (!odataEntityType) { | |
| 132 | // filter out $kind | |
| 133 | const availableEntities = metaModel | |
| 134 | .getODataEntityContainer() | |
| 135 | .entitySet.map((item) => item.name) | |
| 136 | .join(); | |
| 137 | Log.error(`Error while getting specified OData Type. ${availableEntities}`, undefined, "SpreadsheetUpload: ODataV4"); | |
| 138 | throw new Error(`Error while getting specified OData Type. Available Entities: ${availableEntities}`); | |
| 139 | } | |
| 140 | return odataType; | |
| 141 | } | |
| 142 | } | |
| 143 | | |
| 144 | getObjects(model: any, binding: any, batch: any): Promise<any> { | |
| 145 | throw new Error("Method not implemented."); | |
| 146 | } | |
| 147 | | |
| 148 | async getLabelList(columns: Columns, odataType: string, excludeColumns: Columns, binding?: any) { | |
| 149 | const metaModel = binding.getModel().getMetaModel(); | |
| 150 | await metaModel.loaded(); | |
| 151 | const odataEntityType = metaModel.getODataEntityType(odataType); | |
| 152 | return this.getMetadataHandler().getLabelList(columns, odataType, odataEntityType, excludeColumns); | |
| 153 | } | |
| 154 | | |
| 155 | async getKeyList(odataType: string, binding: any) { | |
| 156 | const metaModel = binding.getModel().getMetaModel(); | |
| 157 | await metaModel.loaded(); | |
| 158 | const odataEntityType = metaModel.getODataEntityType(odataType); | |
| 159 | return this.getMetadataHandler().getKeyList(odataEntityType); | |
| 160 | } | |
| 161 | | |
| 162 | resetContexts() { | |
| 163 | this.createContexts = []; | |
| 164 | this.createPromises = []; | |
| 165 | } | |
| 166 | | |
| 167 | getMetadataHandler(): MetadataHandlerV2 { | |
| 168 | return this.metadataHandler; | |
| 169 | } | |
| 170 | | |
| 171 | getODataEntitiesRecursive(entityName: string, deepLevel: number): any { | |
| 172 | return this.metadataHandler.getODataEntitiesRecursive(entityName, deepLevel); | |
| 173 | } | |
| 174 | | |
| 175 | getBindingFromBinding(binding: ODataListBinding, expand?: any): ODataListBinding { | |
| 176 | throw new Error("Method not implemented."); | |
| 177 | } | |
| 178 | | |
| 179 | fetchBatch(customBinding: ODataListBinding, batchSize: number): Promise<any>{ | |
| 180 | throw new Error("Method not implemented."); | |
| 181 | } | |
| 182 | | |
| 183 | addKeys(labelList: ListObject, entityName: string, parentEntity?: any, partner?: string) {} | |
| 184 | } | |
| 185 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4RequestObjects.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import Filter from "sap/ui/model/Filter"; | |
| 2 | import FilterOperator from "sap/ui/model/FilterOperator"; | |
| 3 | import Context from "sap/ui/model/odata/v4/Context"; | |
| 4 | import ODataListBinding from "sap/ui/model/odata/v4/ODataListBinding"; | |
| 5 | import { CustomMessageTypes, MessageType } from "../../enums"; | |
| 6 | import MetadataHandlerV4 from "./MetadataHandlerV4"; | |
| 7 | import MessageHandler from "../MessageHandler"; | |
| 8 | import Util from "../Util"; | |
| 9 | import Log from "sap/base/Log"; | |
| 10 | | |
| 11 | export interface BatchContext { | |
| 12 | context: Context; | |
| 13 | path: string; | |
| 14 | keyPredicates: string; | |
| 15 | keys: string[]; | |
| 16 | payload: any; | |
| 17 | } | |
| 18 | | |
| 19 | export class ODataV4RequestObjects { | |
| 20 | private metadataHandler: MetadataHandlerV4; | |
| 21 | private messageHandler: MessageHandler; | |
| 22 | private util: Util; | |
| 23 | private contexts: BatchContext[] = []; | |
| 24 | | |
| 25 | constructor(metadataHandler: MetadataHandlerV4, messageHandler: MessageHandler, util: Util) { | |
| 26 | this.metadataHandler = metadataHandler; | |
| 27 | this.messageHandler = messageHandler; | |
| 28 | this.util = util; | |
| 29 | } | |
| 30 | | |
| 31 | public getContexts(): BatchContext[] { | |
| 32 | return this.contexts; | |
| 33 | } | |
| 34 | | |
| 35 | async getObjects(model: any, binding: any, batch: any): Promise<any[]> { | |
| 36 | Log.debug("Processing batch from spreadsheet", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 37 | batch, | |
| 38 | bindingPath: binding.getPath(), | |
| 39 | modelName: model.getMetadata().getName() | |
| 40 | })); | |
| 41 | let path = MetadataHandlerV4.getResolvedPath(binding); | |
| 42 | | |
| 43 | // Get both active and inactive contexts | |
| 44 | Log.debug("Fetching active entities...", undefined, "SpreadsheetUpload: ODataV4RequestObjects"); | |
| 45 | const { contexts: contextsTrue, objects: objectsTrue } = await this._getFilteredContexts(model, binding, path, batch, true); | |
| 46 | Log.debug("Found active entities", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 47 | count: objectsTrue.length, | |
| 48 | objects: objectsTrue | |
| 49 | })); | |
| 50 | | |
| 51 | Log.debug("Fetching inactive entities...", undefined, "SpreadsheetUpload: ODataV4RequestObjects"); | |
| 52 | const { contexts: contextsFalse, objects: objectsFalse } = await this._getFilteredContexts(model, binding, path, batch, false); | |
| 53 | Log.debug("Found inactive entities", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 54 | count: objectsFalse.length, | |
| 55 | objects: objectsFalse | |
| 56 | })); | |
| 57 | | |
| 58 | let objects = this.findEntitiesFromSpreadsheet(batch, objectsTrue, objectsFalse, binding); | |
| 59 | Log.debug("Matched entities from spreadsheet", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 60 | count: objects.length, | |
| 61 | objects | |
| 62 | })); | |
| 63 | | |
| 64 | // Store contexts | |
| 65 | this.contexts = this.getContextsFromPayload(batch, contextsTrue, contextsFalse, path, binding); | |
| 66 | Log.debug("Generated contexts", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 67 | count: this.contexts.length, | |
| 68 | contexts: this.contexts | |
| 69 | })); | |
| 70 | | |
| 71 | const errorFound = this.validateObjectsAndDraftStates(batch, objects, binding); | |
| 72 | Log.debug("Validation completed", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 73 | errorFound, | |
| 74 | messageCount: this.messageHandler.messages.length | |
| 75 | })); | |
| 76 | | |
| 77 | if (errorFound) { | |
| 78 | if (this.messageHandler.areMessagesPresent()) { | |
| 79 | try { | |
| 80 | await this.messageHandler.displayMessages(); | |
| 81 | | |
| 82 | // Log status changes | |
| 83 | const statusChanges = batch.map((payload, index) => { | |
| 84 | const keys = this.metadataHandler.getKeys(binding, payload); | |
| 85 | const keysWithoutIsActiveEntity = { ...keys }; | |
| 86 | delete keysWithoutIsActiveEntity.IsActiveEntity; | |
| 87 | | |
| 88 | const originalStatus = payload.IsActiveEntity; | |
| 89 | const oppositeStatusObject = payload.IsActiveEntity | |
| 90 | ? objectsFalse.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value)) | |
| 91 | : objectsTrue.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value)); | |
| 92 | | |
| 93 | return { | |
| 94 | index, | |
| 95 | keys: keysWithoutIsActiveEntity, | |
| 96 | originalStatus, | |
| 97 | newStatus: oppositeStatusObject?.IsActiveEntity, | |
| 98 | statusChanged: originalStatus !== oppositeStatusObject?.IsActiveEntity | |
| 99 | }; | |
| 100 | }); | |
| 101 | | |
| 102 | Log.debug("Status changes after user confirmation", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 103 | total: statusChanges.length, | |
| 104 | changed: statusChanges.filter(c => c.statusChanged).length, | |
| 105 | details: statusChanges | |
| 106 | })); | |
| 107 | | |
| 108 | // Update objects with correct draft status versions | |
| 109 | objects = batch.map(payload => { | |
| 110 | const keys = this.metadataHandler.getKeys(binding, payload); | |
| 111 | const keysWithoutIsActiveEntity = { ...keys }; | |
| 112 | delete keysWithoutIsActiveEntity.IsActiveEntity; | |
| 113 | | |
| 114 | // Find the object with opposite draft status | |
| 115 | const oppositeStatusObject = payload.IsActiveEntity | |
| 116 | ? objectsFalse.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value)) | |
| 117 | : objectsTrue.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value)); | |
| 118 | | |
| 119 | // Update payload to match actual status | |
| 120 | if (oppositeStatusObject) { | |
| 121 | payload.IsActiveEntity = oppositeStatusObject.IsActiveEntity; | |
| 122 | return oppositeStatusObject; | |
| 123 | } | |
| 124 | return payload; | |
| 125 | }); | |
| 126 | | |
| 127 | return objects; | |
| 128 | } catch (error) { | |
| 129 | Log.debug("Operation cancelled by user", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 130 | error: error.message | |
| 131 | })); | |
| 132 | throw new Error("Operation cancelled by user"); | |
| 133 | } | |
| 134 | } | |
| 135 | } | |
| 136 | | |
| 137 | return objects; | |
| 138 | } | |
| 139 | | |
| 140 | private async _getFilteredContexts( | |
| 141 | model: any, | |
| 142 | binding: any, | |
| 143 | path: string, | |
| 144 | batch: any[], | |
| 145 | isActive: boolean | |
| 146 | ): Promise<{ | |
| 147 | contexts: Context[]; | |
| 148 | objects: any[]; | |
| 149 | }> { | |
| 150 | // Create filters for each object in the batch | |
| 151 | const batchFilters = batch.map((payload) => { | |
| 152 | const keys = this.metadataHandler.getKeys(binding, payload); | |
| 153 | keys.IsActiveEntity = isActive; | |
| 154 | | |
| 155 | const keyFilters = Object.entries(keys).map(([property, value]) => new Filter(property, FilterOperator.EQ, value)); | |
| 156 | | |
| 157 | return new Filter({ | |
| 158 | filters: keyFilters, | |
| 159 | and: true | |
| 160 | }); | |
| 161 | }); | |
| 162 | | |
| 163 | // Combine all batch filters with OR | |
| 164 | const combinedFilter = new Filter({ | |
| 165 | filters: batchFilters, | |
| 166 | and: false | |
| 167 | }); | |
| 168 | | |
| 169 | // Bind the list with the filter | |
| 170 | const listBinding = model.bindList(path, null, [], [], { $updateGroupId: "$auto" }) as ODataListBinding; | |
| 171 | listBinding.filter(combinedFilter); | |
| 172 | | |
| 173 | // Request contexts and map to objects | |
| 174 | const contexts = await listBinding.requestContexts(0, batch.length); | |
| 175 | const objects = await Promise.all(contexts.map((context) => context.getObject())); | |
| 176 | | |
| 177 | return { contexts, objects }; | |
| 178 | } | |
| 179 | | |
| 180 | private findEntitiesFromSpreadsheet(batch: any[], objectsTrue: any[], objectsFalse: any[], binding: any): any[] { | |
| 181 | const matchResults = []; | |
| 182 | | |
| 183 | batch.forEach((payload, index) => { | |
| 184 | const keys = this.metadataHandler.getKeys(binding, payload); | |
| 185 | const keysWithoutIsActiveEntity = { ...keys }; | |
| 186 | delete keysWithoutIsActiveEntity.IsActiveEntity; | |
| 187 | | |
| 188 | // try to find the matching object from the spreadsheet in the requested objects | |
| 189 | const matchingObjectTrue = objectsTrue.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value)); | |
| 190 | const matchingObjectFalse = objectsFalse.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value)); | |
| 191 | | |
| 192 | let matchingObject = payload.IsActiveEntity ? matchingObjectTrue : matchingObjectFalse; | |
| 193 | | |
| 194 | // if the matching object is not found, try to find it in the opposite status objects, a error will be later shown in the validation | |
| 195 | if(!matchingObject) { | |
| 196 | matchingObject = payload.IsActiveEntity ? matchingObjectFalse : matchingObjectTrue; | |
| 197 | } | |
| 198 | | |
| 199 | matchResults.push({ | |
| 200 | index, | |
| 201 | keys: keysWithoutIsActiveEntity, | |
| 202 | requestedStatus: payload.IsActiveEntity, | |
| 203 | foundIn: matchingObjectTrue ? 'objectsTrue' : (matchingObjectFalse ? 'objectsFalse' : 'notFound'), | |
| 204 | object: matchingObject | |
| 205 | }); | |
| 206 | }); | |
| 207 | | |
| 208 | Log.debug("Entity matching results", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({ | |
| 209 | total: matchResults.length, | |
| 210 | found: matchResults.filter(r => r.object).length, | |
| 211 | notFound: matchResults.filter(r => !r.object).length, | |
| 212 | details: matchResults | |
| 213 | })); | |
| 214 | | |
| 215 | return matchResults.map(result => result.object); | |
| 216 | } | |
| 217 | | |
| 218 | private getContextsFromPayload(batch: any[], contextsTrue: Context[], contextsFalse: Context[], path: string, binding: any): BatchContext[] { | |
| 219 | return batch.map((payload) => { | |
| 220 | const keys = this.metadataHandler.getKeys(binding, payload); | |
| 221 | const keyPredicates = MetadataHandlerV4.formatKeyPredicates(keys, payload); | |
| 222 | const isActiveEntity = payload.IsActiveEntity; | |
| 223 | const keysWithoutIsActiveEntity = { ...keys }; | |
| 224 | delete keysWithoutIsActiveEntity.IsActiveEntity; | |
| 225 | | |
| 226 | const matchingContextTrue = contextsTrue.find((ctx) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => ctx.getObject()[key] === value)); | |
| 227 | const matchingContextFalse = contextsFalse.find((ctx) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => ctx.getObject()[key] === value)); | |
| 228 | | |
| 229 | let matchingContext = isActiveEntity ? matchingContextTrue : matchingContextFalse; | |
| 230 | | |
| 231 | if(!matchingContext) { | |
| 232 | matchingContext = isActiveEntity ? matchingContextFalse : matchingContextTrue; | |
| 233 | } | |
| 234 | | |
| 235 | return { | |
| 236 | context: matchingContext, | |
| 237 | path, | |
| 238 | keyPredicates: keyPredicates, | |
| 239 | keys: Object.keys(keys), | |
| 240 | payload | |
| 241 | }; | |
| 242 | }); | |
| 243 | } | |
| 244 | | |
| 245 | private validateObjectsAndDraftStates(batch: any[], objects: any[], binding: any): boolean { | |
| 246 | let errorFound = false; | |
| 247 | | |
| 248 | batch.forEach((batchItem, index) => { | |
| 249 | const keys = this.metadataHandler.getKeys(binding, batchItem); | |
| 250 | const keysWithoutIsActiveEntity = { ...keys }; | |
| 251 | delete keysWithoutIsActiveEntity.IsActiveEntity; | |
| 252 | | |
| 253 | const foundObject = objects.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value)); | |
| 254 | | |
| 255 | if (!foundObject) { | |
| 256 | errorFound = true; | |
| 257 | this.messageHandler.addMessageToMessages({ | |
| 258 | title: this.util.geti18nText("spreadsheetimporter.objectNotFound"), | |
| 259 | row: index + 1, | |
| 260 | type: CustomMessageTypes.ObjectNotFound, | |
| 261 | counter: 1, | |
| 262 | formattedValue: Object.entries(keys) | |
| 263 | .map(([key, value]) => `${key}=${value}`) | |
| 264 | .join(", "), | |
| 265 | ui5type: MessageType.Error | |
| 266 | }); | |
| 267 | return; | |
| 268 | } | |
| 269 | | |
| 270 | // Check for valid draft states only if batchItem.IsActiveEntity is defined | |
| 271 | if (batchItem.IsActiveEntity !== undefined) { | |
| 272 | if (foundObject.IsActiveEntity && !foundObject.HasDraftEntity && !batchItem.IsActiveEntity) { | |
| 273 | this.addDraftMismatchError(index, keys, "Draft", "Active"); | |
| 274 | errorFound = true; | |
| 275 | } else if (foundObject.IsActiveEntity && foundObject.HasDraftEntity && batchItem.IsActiveEntity) { | |
| 276 | this.addDraftMismatchError(index, keys, "Active", "Draft"); | |
| 277 | errorFound = true; | |
| 278 | } else if (!foundObject.IsActiveEntity && batchItem.IsActiveEntity) { | |
| 279 | this.addDraftMismatchError(index, keys, "Active", "Draft"); | |
| 280 | errorFound = true; | |
| 281 | } | |
| 282 | } | |
| 283 | }); | |
| 284 | | |
| 285 | return errorFound; | |
| 286 | } | |
| 287 | | |
| 288 | private addDraftMismatchError(index: number, keys: Record<string, any>, uploadedState: string, expectedState: string): void { | |
| 289 | this.messageHandler.addMessageToMessages({ | |
| 290 | title: this.util.geti18nText("spreadsheetimporter.draftEntityMismatch"), | |
| 291 | row: index + 1, | |
| 292 | type: CustomMessageTypes.DraftEntityMismatch, | |
| 293 | counter: 1, | |
| 294 | ui5type: MessageType.Error, | |
| 295 | formattedValue: [ | |
| 296 | Object.entries(keys) | |
| 297 | .map(([key, value]) => `${key}=${value}`) | |
| 298 | .join(", "), | |
| 299 | uploadedState, | |
| 300 | expectedState | |
| 301 | ] | |
| 302 | }); | |
| 303 | } | |
| 304 | } | |
| 305 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/enums.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import { CustomMessageType } from "./types"; | |
| 2 | | |
| 3 | export enum AvailableOptions { | |
| 4 | /** | |
| 5 | * Option for `strict` mode | |
| 6 | * @public | |
| 7 | */ | |
| 8 | Strict = "strict", | |
| 9 | | |
| 10 | /** | |
| 11 | * Changing the field match type | |
| 12 | * @public | |
| 13 | */ | |
| 14 | FieldMatchType = "fieldMatchType", | |
| 15 | | |
| 16 | /** | |
| 17 | * Changing the decimal seperator for number fields | |
| 18 | * @public | |
| 19 | */ | |
| 20 | DecimalSeperator = "decimalSeperator" | |
| 21 | } | |
| 22 | | |
| 23 | export enum FieldMatchType { | |
| 24 | /** | |
| 25 | * Default match type, property names in square brackets | |
| 26 | * @public | |
| 27 | */ | |
| 28 | LabelTypeBrackets = "labelTypeBrackets", | |
| 29 | | |
| 30 | /** | |
| 31 | * match type with only labels | |
| 32 | * @public | |
| 33 | */ | |
| 34 | Label = "label" | |
| 35 | } | |
| 36 | | |
| 37 | export const CustomMessageTypes: { [key: string]: CustomMessageType } = { | |
| 38 | MandatoryFieldNotFilled: { | |
| 39 | title: "MandatoryFieldNotFilled", | |
| 40 | group: true | |
| 41 | }, | |
| 42 | ColumnNotFound: { | |
| 43 | title: "ColumnNotFound", | |
| 44 | group: false | |
| 45 | }, | |
| 46 | ParsingError: { | |
| 47 | title: "ParsingError", | |
| 48 | group: true | |
| 49 | }, | |
| 50 | CustomErrorGroup: { | |
| 51 | title: "CustomErrorGroup", | |
| 52 | group: true | |
| 53 | }, | |
| 54 | CustomError: { | |
| 55 | title: "CustomError", | |
| 56 | group: false | |
| 57 | }, | |
| 58 | Formatting: { | |
| 59 | title: "Formatting", | |
| 60 | group: true | |
| 61 | }, | |
| 62 | DuplicateColumns: { | |
| 63 | title: "DuplicateColumns", | |
| 64 | group: false | |
| 65 | }, | |
| 66 | MaxLengthExceeded: { | |
| 67 | title: "MaxLengthExceeded", | |
| 68 | group: true | |
| 69 | }, | |
| 70 | ObjectNotFound: { | |
| 71 | title: "ObjectNotFound", | |
| 72 | group: true, | |
| 73 | update: true | |
| 74 | }, | |
| 75 | DraftEntityMismatch: { | |
| 76 | title: "DraftEntityMismatch", | |
| 77 | group: true, | |
| 78 | update: true | |
| 79 | }, | |
| 80 | DuplicateKeys: { | |
| 81 | title: "DuplicateKeys", | |
| 82 | group: true | |
| 83 | }, | |
| 84 | MissingKeys: { | |
| 85 | title: "MissingKeys", | |
| 86 | group: true, | |
| 87 | update: true | |
| 88 | } | |
| 89 | }; | |
| 90 | | |
| 91 | export enum MessageType { | |
| 92 | /** | |
| 93 | * Message is an error | |
| 94 | */ | |
| 95 | Error = "Error", | |
| 96 | /** | |
| 97 | * Message should be just an information | |
| 98 | */ | |
| 99 | Information = "Information", | |
| 100 | /** | |
| 101 | * Message has no specific level | |
| 102 | */ | |
| 103 | None = "None", | |
| 104 | /** | |
| 105 | * Message is a success message | |
| 106 | */ | |
| 107 | Success = "Success", | |
| 108 | /** | |
| 109 | * Message is a warning | |
| 110 | */ | |
| 111 | Warning = "Warning" | |
| 112 | } | |
| 113 | | |
| 114 | export enum Action { | |
| 115 | Create = "CREATE", | |
| 116 | Update = "UPDATE", | |
| 117 | Delete = "DELETE" | |
| 118 | } | |
| 119 | | |
| 120 | export const DefaultConfigs = { | |
| 121 | DeepDownload: { | |
| 122 | addKeysToExport: false, | |
| 123 | setDraftStatus: true, | |
| 124 | deepExport: false, | |
| 125 | deepLevel: 0, | |
| 126 | showOptions: true, | |
| 127 | columns: [] | |
| 128 | }, | |
| 129 | Update: { | |
| 130 | fullUpdate: false, | |
| 131 | columns: [] | |
| 132 | } | |
| 133 | } as const; | |
| 134 | | |
| -------------------------------------------------------------------------------- | |
| /packages/ui5-cc-spreadsheetimporter/src/types.d.ts: | |
| -------------------------------------------------------------------------------- | |
| 1 | import { MessageType } from "sap/ui/core/library"; | |
| 2 | import { Action, AvailableOptions } from "./enums"; | |
| 3 | | |
| 4 | export interface Tags { | |
| 5 | name: string; | |
| 6 | count: number; | |
| 7 | type: string; | |
| 8 | } | |
| 9 | | |
| 10 | export interface Property { | |
| 11 | maxLength?: number; | |
| 12 | type: string; | |
| 13 | label: string; | |
| 14 | precision?: number; | |
| 15 | $XYZKey?: boolean; | |
| 16 | } | |
| 17 | export type ListObject = Map<string, Property>; | |
| 18 | export type PropertyArray = { [key: string]: any }[]; | |
| 19 | export type Columns = string[]; | |
| 20 | | |
| 21 | export type CustomMessageType = { | |
| 22 | title: string; | |
| 23 | group: boolean; | |
| 24 | update?: boolean; | |
| 25 | }; | |
| 26 | | |
| 27 | export interface Messages { | |
| 28 | title: string; | |
| 29 | type: CustomMessageType; | |
| 30 | counter: number; | |
| 31 | row?: number; | |
| 32 | group?: boolean; | |
| 33 | rawValue?: any; | |
| 34 | formattedValue?: string; | |
| 35 | ui5type: MessageType; | |
| 36 | description?: string; | |
| 37 | details?: MessagesDetails[]; | |
| 38 | maxLength?: number; | |
| 39 | excededLength?: number; | |
| 40 | } | |
| 41 | | |
| 42 | export interface MessagesDetails { | |
| 43 | row?: number; | |
| 44 | description?: string; | |
| 45 | } | |
| 46 | | |
| 47 | export interface GroupedMessage { | |
| 48 | title: string; | |
| 49 | description?: string; | |
| 50 | ui5type?: MessageType; | |
| 51 | details?: MessagesDetails[]; | |
| 52 | } | |
| 53 | | |
| 54 | export type TransformedItem = { | |
| 55 | [key in keyof typeof headerMapping]: string | number; | |
| 56 | }; | |
| 57 | | |
| 58 | export type Payload = { | |
| 59 | [key: string]: any; | |
| 60 | }; | |
| 61 | export type PayloadArray = Payload[]; | |
| 62 | | |
| 63 | // SheetHandler | |
| 64 | export type SheetDataType = "b" | "e" | "n" | "d" | "s" | "z"; | |
| 65 | | |
| 66 | export interface ValueData { | |
| 67 | rawValue: any; | |
| 68 | sheetDataType: SheetDataType; | |
| 69 | format: string; | |
| 70 | formattedValue: string; | |
| 71 | sheetName: string; | |
| 72 | } | |
| 73 | | |
| 74 | export type ArrayData = { | |
| 75 | [key: string]: ValueData; | |
| 76 | }[]; | |
| 77 | | |
| 78 | export type RowData = { | |
| 79 | [key: string]: ValueData; | |
| 80 | }; | |
| 81 | | |
| 82 | export type AvailableOptionsType = keyof typeof AvailableOptions; | |
| 83 | | |
| 84 | export interface ComponentData { | |
| 85 | spreadsheetFileName?: string; | |
| 86 | action?: Action; | |
| 87 | context?: object; | |
| 88 | columns?: string[]; | |
| 89 | excludeColumns?: string[]; | |
| 90 | tableId?: string; | |
| 91 | odataType?: string; | |
| 92 | mandatoryFields?: string[]; | |
| 93 | fieldMatchType?: FieldMatchType; | |
| 94 | activateDraft?: boolean; | |
| 95 | batchSize?: number; | |
| 96 | standalone?: boolean; | |
| 97 | readAllSheets?: boolean; | |
| 98 | readSheet: number | string; | |
| 99 | spreadsheetRowPropertyName?: string; | |
| 100 | strict?: boolean; | |
| 101 | decimalSeparator?: string; | |
| 102 | hidePreview?: boolean; | |
| 103 | previewColumns?: string[]; | |
| 104 | skipMandatoryFieldCheck?: boolean; | |
| 105 | skipColumnsCheck?: boolean; | |
| 106 | skipMaxLengthCheck?: boolean; | |
| 107 | showBackendErrorMessages?: boolean; | |
| 108 | showOptions?: boolean; | |
| 109 | availableOptions?: AvailableOptionsType[]; | |
| 110 | debug?: boolean; | |
| 111 | hideSampleData?: boolean; | |
| 112 | spreadsheetTemplateFile?: string; | |
| 113 | useTableSelector?: boolean; | |
| 114 | sampleData?: object; | |
| 115 | continueOnError?: boolean; | |
| 116 | componentContainerData?: object; | |
| 117 | createActiveEntity?: boolean; | |
| 118 | i18nModel?: object; | |
| 119 | bindingCustom?: object; | |
| 120 | showDownloadButton?: boolean; | |
| 121 | deepDownloadConfig?: DeepDownloadConfig; | |
| 122 | updateConfig?: UpdateConfig; | |
| 123 | } | |
| 124 | | |
| 125 | export interface DeepDownloadConfig { | |
| 126 | addKeysToExport: boolean; | |
| 127 | setDraftStatus: boolean; | |
| 128 | deepExport: boolean; | |
| 129 | deepLevel: number; | |
| 130 | showOptions: boolean; | |
| 131 | columns: any; | |
| 132 | filename?: string; | |
| 133 | } | |
| 134 | | |
| 135 | export interface UpdateConfig { | |
| 136 | fullUpdate: boolean; | |
| 137 | columns: string[]; | |
| 138 | } | |
| 139 | | |
| 140 | export type FireEventReturnType = { | |
| 141 | bPreventDefault: boolean; | |
| 142 | mParameters: object; | |
| 143 | returnValue: object; | |
| 144 | }; | |
| 145 | | |
| 146 | export type ListObject = Map<string, Property>; | |
| 147 | | |
| 148 | export type PropertyObject = { | |
| 149 | propertyName: string; | |
| 150 | propertyValue: any; // Replace 'any' with a more specific type if possible. | |
| 151 | propertyLabel: [x: string]; | |
| 152 | }; | |
| 153 | | |
| 154 | interface EntityDefinition { | |
| 155 | $kind: string; | |
| 156 | $Key?: string[]; | |
| 157 | [key: string]: PropertyDefinition | NavigationPropertyDefinition | any; | |
| 158 | }; | |
| 159 | | |
| 160 | type EntityObject = { | |
| 161 | $kind: string; | |
| 162 | $Type?: string; | |
| 163 | $NavigationPropertyBinding?: Record<string, string>; | |
| 164 | }; | |
| 165 | | |
| 166 | interface PropertyWithOrder { | |
| 167 | name: string; | |
| 168 | order: number; | |
| 169 | } | |
| 170 | | |
| 171 | // Pro Types | |
| 172 | | |
| -------------------------------------------------------------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment