Skip to content

Instantly share code, notes, and snippets.

@marianfoo
Created February 24, 2025 20:45
Show Gist options
  • Select an option

  • Save marianfoo/ab3720c444076721d20232096f57fbb9 to your computer and use it in GitHub Desktop.

Select an option

Save marianfoo/ab3720c444076721d20232096f57fbb9 to your computer and use it in GitHub Desktop.
├── .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 | ![Spreadsheet Upload Dialog](/images/SpreadsheetUploadDialog.png "Spreadsheet Upload Dialog")
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 | [![Open Stable in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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 | ![Screenshot](../images/reusecomponentFE.jpg)
--------------------------------------------------------------------------------
/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 | [![Open Stable in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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 | ![Error Dialog](./../images/error_dialog.png){ 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 | ![Open Spreadsheet Upload Dialog](./../images/open_spreadsheetupload_dialog.png){ 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 | ![Download Template](./../images/download_template.png){ 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 | ![Fill out Spreadsheet File](./../images/fill_out_spreadsheet_file.png){ 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 | ![Upload Spreadsheet File to App](./../images/upload_file_to_app.png){ 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 | ![Error Dialog](./../images/error_dialog.png){ 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 | ![Send Data to Backend](./../images/send_data_to_backend.png){ 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 | ![Spreadsheet Upload Dialog](/images/SpreadsheetUploadDialog.png "Spreadsheet Upload Dialog")
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