diff --git a/.github/workflows/opa5-test.yml b/.github/workflows/opa5-test.yml
index 66552e6e7..4f756dbeb 100644
--- a/.github/workflows/opa5-test.yml
+++ b/.github/workflows/opa5-test.yml
@@ -8,7 +8,7 @@ on:
jobs:
test-opa5:
runs-on: ubuntu-latest
- if: "github.event.pull_request.title != 'chore: release main'"
+ if: "github.event.pull_request.title != 'chore: release main' && !github.event.pull_request.draft"
strategy:
fail-fast: false
matrix:
diff --git a/.github/workflows/ui5-lint.yml b/.github/workflows/ui5-lint.yml
index 370a132f3..659e98430 100644
--- a/.github/workflows/ui5-lint.yml
+++ b/.github/workflows/ui5-lint.yml
@@ -8,7 +8,7 @@ on:
jobs:
test-ui5-linter:
runs-on: ubuntu-latest
- if: "github.event.pull_request.title != 'chore: release main'"
+ if: "github.event.pull_request.title != 'chore: release main' && !github.event.pull_request.draft"
steps:
diff --git a/.github/workflows/wdi5-test.yml b/.github/workflows/wdi5-test.yml
index 97ded027c..52d130710 100644
--- a/.github/workflows/wdi5-test.yml
+++ b/.github/workflows/wdi5-test.yml
@@ -8,7 +8,7 @@ on:
jobs:
test-wdi5:
runs-on: ubuntu-latest
- if: "github.event.pull_request.title != 'chore: release main'"
+ if: "github.event.pull_request.title != 'chore: release main' && !github.event.pull_request.draft"
strategy:
fail-fast: false
matrix:
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ff6efdc74..be049629e 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- "packages/ui5-cc-spreadsheetimporter": "1.5.2"
+ "packages/ui5-cc-spreadsheetimporter": "1.6.0"
}
diff --git a/dev/testapps.json b/dev/testapps.json
index 97df071e1..50ec2a192 100644
--- a/dev/testapps.json
+++ b/dev/testapps.json
@@ -242,7 +242,8 @@
"testMapping": {
"specs": [
"../test/specs/all/**.test.js",
- "../test/specs/v4/**.test.js"
+ "../test/specs/v4/**.test.js",
+ "../test/specs/update/**.test.js"
]
},
"copyVersions": [
@@ -320,7 +321,8 @@
"appTitel": "Orders V4 FS 120",
"testMapping": {
"specs": [
- "../test/specs/download/**.test.js"
+ "../test/specs/download/**.test.js",
+ "../test/specs/updatefreestyle/**.test.js"
]
},
"copyVersions": []
diff --git a/docs/pages/Configuration.md b/docs/pages/Configuration.md
index 818da5f7f..c1f8eeae5 100644
--- a/docs/pages/Configuration.md
+++ b/docs/pages/Configuration.md
@@ -2,7 +2,7 @@
How to use them see [Example Code](#example-code)
-## Configuration Options
+## Configuration Overview
The table below summarizes the options available for the UI5 Spreadsheet Importer Component. Detailed explanations and examples for each option are provided in the linked sections.
@@ -31,6 +31,7 @@ The table below summarizes the options available for the UI5 Spreadsheet Importe
| Option | Description | Default | Type |
|-----------------------------------------------------|-------------------------------------------------------|-----------------|------------|
+| [`action`](#action) | Continues processing next batches even after errors. | `CREATE` | `string` |
| [`batchSize`](#batchsize) | Controls the size of batches sent to the backend. | `1000` | `number` |
| [`strict`](#strict) | Controls availability of the "Continue" button in error dialogs. | `false` | `boolean` |
| [`decimalSeparator`](#decimalseparator) | Sets the decimal separator for numbers. | Browser default | `string` |
@@ -39,6 +40,7 @@ The table below summarizes the options available for the UI5 Spreadsheet Importe
| [`skipColumnsCheck`](#skipcolumnscheck) | Skips the check for unknown columns not in metadata. | `false` | `boolean` |
| [`continueOnError`](#continueonerror) | Continues processing next batches even after errors. | `false` | `boolean` |
+
### Advanced Configuration Options
| Option | Description | Default | Type |
@@ -57,6 +59,8 @@ The table below summarizes the options available for the UI5 Spreadsheet Importe
---
+## Configuration Options
+
### `columns`
**default:** all fields
@@ -176,6 +180,15 @@ Of course, creating the draft entity and the subsequent activation takes longer
Together with the option `continueOnError`, it is also possible to create all entities and try to activate the other entities if the draft activation fails.
This means that at least all drafts are available.
+### `action`
+
+**default:** `CREATE`
+
+Options:
+
+- `CREATE` : Create
+- `UPDATE` : Update
+
### `batchSize`
**default:** `1.000`
@@ -192,6 +205,8 @@ The default value is 1,000, which means that when the number of lines in the Spr
If you set the `batchSize` to 0, the payload array will not be divided, and the entire array will be sent as a single batch request.
+For updates, the batch size is limited to 100.
+
### `standalone`
**default:** `false`
diff --git a/docs/pages/Development/Update.md b/docs/pages/Development/Update.md
new file mode 100644
index 000000000..3283ea661
--- /dev/null
+++ b/docs/pages/Development/Update.md
@@ -0,0 +1,54 @@
+
+Only V4 is supported for now.
+
+## OData V4
+
+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.
+Draft status will determined with `IsActiveEntity` property.
+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).
+The finding of the object result in a get request to the OData service for each row.
+After that the process knows the state of the object and can update it.
+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.
+
+
+## Technical Details
+
+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.
+Technically is has to query for `IsActiveEntity=true` and `IsActiveEntity=false` and combine the results.
+This will result in two get requests to the OData service for each row combined in two batch request for each batch.
+If a row is not found it is just not included in the List Binding.
+So the process will not fail if a row is not found and can match which objects are not found from the List Binding.
+If a object was not found the user can then decide to continue with the found objects or to cancel the process.
+Each context will then be used to update the object with `setProperty`.
+
+### Different States in Export
+
+For the export the process determines the state of the object by checking the `IsActiveEntity` and `HasDraftEntity` properties.
+
+
+#### List Report
+
+- `IsActiveEntity=true` and `HasDraftEntity=false` -> `IsActiveEntity` column is set to true
+- `IsActiveEntity=true` and `HasDraftEntity=true` -> `IsActiveEntity` column is set to false
+
+#### Object Page
+
+- `IsActiveEntity=true` and `HasDraftEntity=false` -> `IsActiveEntity` column is set to true
+- `IsActiveEntity=true` and `HasDraftEntity=true` -> `IsActiveEntity` column is set to false
+
+### Different States in Upload
+
+#### List Report
+
+- `IsActiveEntity=true` and `HasDraftEntity=false` -> update the object (expecting `IsActiveEntity=true` in the spreadsheet import)
+- `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)
+
+#### Table in Object Page
+
+##### Not in Edit Mode
+
+- `IsActiveEntity=true` and `HasDraftEntity=false` and `HasActiveEntity=false` -> update the object (expecting `IsActiveEntity=true` in the spreadsheet import)
+
+##### In Edit Mode
+
+- `IsActiveEntity=false` and `HasDraftEntity=false` and `HasActiveEntity=true` -> update the object (expecting `IsActiveEntity=false` in the spreadsheet import)
diff --git a/docs/pages/Update.md b/docs/pages/Update.md
new file mode 100644
index 000000000..479da3aa3
--- /dev/null
+++ b/docs/pages/Update.md
@@ -0,0 +1,95 @@
+!!! warning
+ This feature is currently experimental and may not work as expected.
+ Also only available for OData V4.
+ Please provide feedback: https://github.com/spreadsheetimporter/ui5-cc-spreadsheetimporter/issues
+
+
+## Usage
+
+It is recommended, especially for Entities with GUID, to first download the data with the Spreadsheet Importer and include the keys.
+
+1. Download the data with the Spreadsheet Importer and include the keys.
+2. Edit the spreadsheet
+3. Upload the data with the Spreadsheet Importer.
+
+## Getting started
+
+The minimal configuration to update entities instead of creating is:
+
+```js
+this.spreadsheetUploadUpdate = await this.editFlow.getView().getController().getAppComponent()
+.createComponent({
+ usage: "spreadsheetImporter",
+ async: true,
+ componentData: {
+ context: this,
+ tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable",
+ action: "UPDATE",
+ deepDownloadConfig: {
+ addKeysToExport: true,
+ showOptions: false,
+ filename: "Items"
+ },
+ showDownloadButton: true
+ }
+});
+```
+
+This configuration will show the download button and download all the available data for the referenced table.
+When you press the download button, the data will be downloaded including the keys necessary for the update.
+
+You can then change the data in the spreadsheet and upload the data again.
+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).
+
+## How it works
+
+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)).
+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)).
+
+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.
+
+For every change, an `ODataContextBinding` is created and the data is updated.
+
+## Things to consider / Drawbacks
+
+### IsActiveEntity handling
+
+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.
+
+• 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.
+ 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.
+
+### Download only Active Entities
+
+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.
+So if the object is in draft mode, the data from the active state will still be used to update the draft object.
+
+### Performance and batch size
+
+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.
+Because of the performance considerations, the batch size for update is limited to 100 per batch.
+
+### Filter Limitations
+
+When exporting the data, currently all the data is exported. Any filters in a List Report are not respected at the moment.
+
+## Configuration
+
+Below is a brief overview of the main configuration options relevant to updating. For the complete list, see the [Configuration documentation](./Configuration.md).
+
+| Option | Description | Default |
+| --------------- | ------------------------------------------------------------------------------ | ------- |
+| `fullUpdate` | Update all properties of the object (true). If false, only changed properties are updated. | false |
+| `columns` | Columns to update. | all |
+
+### fullUpdate
+
+If `fullUpdate` is set to `true`, the component updates all properties of the object.
+If `fullUpdate` is set to `false` (default), only the properties that have changed are updated (partial update).
+
+### columns
+
+The `columns` property is an array of strings. The strings are the names of the columns that should be updated.
+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.
+
+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.
\ No newline at end of file
diff --git a/examples/package.json b/examples/package.json
index c7adac774..8a537f8a2 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -37,7 +37,7 @@
"wdio-chromedriver-service": "8.1.1",
"wdio-timeline-reporter": "5.1.4",
"wdio-ui5-service": "3.0.0-rc.0",
- "webdriverio": "^9.4.1",
+ "webdriverio": "^9.5.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}
}
diff --git a/examples/packages/ordersv4fe/webapp/ext/ListReportExtController.js b/examples/packages/ordersv4fe/webapp/ext/ListReportExtController.js
index b239f50e3..5dae9bcc6 100644
--- a/examples/packages/ordersv4fe/webapp/ext/ListReportExtController.js
+++ b/examples/packages/ordersv4fe/webapp/ext/ListReportExtController.js
@@ -18,6 +18,11 @@ sap.ui.define(["sap/m/MessageToast"], function (MessageToast) {
async: true,
componentData: {
context: this,
+ // action: "UPDATE",
+ // updateConfig: {
+ // fullUpdate: false,
+ // columns: ["OrderNo"]
+ // },
createActiveEntity: true,
i18nModel: this.getModel("i18n"),
debug: true,
diff --git a/examples/packages/ordersv4fe/webapp/ext/ObjectPageExtController.js b/examples/packages/ordersv4fe/webapp/ext/ObjectPageExtController.js
index 42b54546f..b1b498dd3 100644
--- a/examples/packages/ordersv4fe/webapp/ext/ObjectPageExtController.js
+++ b/examples/packages/ordersv4fe/webapp/ext/ObjectPageExtController.js
@@ -14,6 +14,7 @@ sap.ui.define([], function () {
async: true,
componentData: {
context: this,
+
useTableSelector: true,
i18nModel: this.getModel("i18n")
}
@@ -77,11 +78,19 @@ sap.ui.define([], function () {
async: true,
componentData: {
context: this,
+ // action: "UPDATE",
+ // updateConfig: {
+ // fullUpdate: false
+ // },
+ showDownloadButton: true,
tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable",
columns: ["product_ID", "quantity", "title", "price", "validFrom", "timestamp", "date", "time", "boolean", "decimal", "byte","binary"],
mandatoryFields: ["product_ID", "quantity"],
spreadsheetFileName: "Test.xlsx",
i18nModel: this.getModel("i18n"),
+ deepDownloadConfig: {
+ depth: 0
+ },
sampleData: [
{
product_ID: "HT-1000",
@@ -180,6 +189,69 @@ sap.ui.define([], function () {
});
this.spreadsheetUploadTableShippingInfo.openSpreadsheetUploadDialog();
this.editFlow.getView().setBusy(false);
+ },
+
+ openSpreadsheetUploadDialogTableUpdate: async function (oEvent) {
+ this.spreadsheetUploadUpdate = await this.editFlow.getView().getController().getAppComponent()
+ .createComponent({
+ usage: "spreadsheetImporter",
+ async: true,
+ componentData: {
+ context: this,
+ tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable",
+ action: "UPDATE",
+ updateConfig: {
+ fullUpdate: false
+ },
+ deepDownloadConfig: {
+ addKeysToExport: true,
+ showOptions: false,
+ filename: "OrderItems"
+ },
+ showDownloadButton: true,
+ tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable",
+ columns: ["product_ID", "quantity", "title", "price", "validFrom", "timestamp", "date", "time", "boolean", "decimal", "byte","binary"],
+ mandatoryFields: ["product_ID", "quantity"]
+ }
+ });
+ this.spreadsheetUploadUpdate.openSpreadsheetUploadDialog();
+ },
+
+ openSpreadsheetUpdateDialog: async function (oEvent) {
+ this.spreadsheetUpload = await this.editFlow.getView().getController().getAppComponent()
+ .createComponent({
+ usage: "spreadsheetImporter",
+ async: true,
+ componentData: {
+ context: this,
+ tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable",
+ action: "UPDATE",
+ updateConfig: {
+ fullUpdate: false
+ },
+ }
+ });
+ this.spreadsheetUpload.openSpreadsheetUploadDialog();
+ },
+
+ dowloadItems: async function(event) {
+ this.spreadsheetUpload = await this.editFlow.getView().getController().getAppComponent()
+ .createComponent({
+ usage: "spreadsheetImporter",
+ async: true,
+ componentData: {
+ context: this,
+ tableId: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable",
+ createActiveEntity: true,
+ debug: false,
+ deepDownloadConfig: {
+ addKeysToExport: true,
+ showOptions: false,
+ filename: "OrderItems"
+ }
+ }
+ });
+ this.spreadsheetUpload.triggerDownloadSpreadsheet();
}
};
});
diff --git a/examples/packages/ordersv4fe/webapp/manifest.json b/examples/packages/ordersv4fe/webapp/manifest.json
index 47e2f63ff..eaa365911 100644
--- a/examples/packages/ordersv4fe/webapp/manifest.json
+++ b/examples/packages/ordersv4fe/webapp/manifest.json
@@ -212,6 +212,13 @@
"requiresSelection": false,
"enabled": "{ui>/isEditable}",
"text": "Spreadsheet Upload"
+ },
+ "ObjectPageExtControllerUpdate": {
+ "press": "ui.v4.ordersv4fe.ext.ObjectPageExtController.openSpreadsheetUploadDialogTableUpdate",
+ "visible": true,
+ "requiresSelection": false,
+ "enabled": "{ui>/isEditable}",
+ "text": "Spreadsheet Upload Update"
}
}
},
@@ -238,19 +245,6 @@
"text": "Spreadsheet Upload"
}
}
- },
- "body": {
- "sections": {
- "customSectionReuse": {
- "title": "Spreadsheet Upload",
- "embeddedComponent": {
- "name": "cc.spreadsheetimporter.v0_30_0",
- "settings": {
- "tableId": "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem-innerTable"
- }
- }
- }
- }
}
}
}
diff --git a/examples/packages/ordersv4freestyle/webapp/controller/OrdersTable.controller.js b/examples/packages/ordersv4freestyle/webapp/controller/OrdersTable.controller.js
new file mode 100644
index 000000000..eb95675fa
--- /dev/null
+++ b/examples/packages/ordersv4freestyle/webapp/controller/OrdersTable.controller.js
@@ -0,0 +1,85 @@
+sap.ui.define([
+ "sap/ui/core/mvc/Controller"
+],
+ /**
+ * @param {typeof sap.ui.core.mvc.Controller} Controller
+ */
+ function (Controller) {
+ "use strict";
+
+ return Controller.extend("ordersv4freestyle.controller.OrdersTable", {
+ onInit: function () {
+
+ },
+
+ onDownload: async function () {
+ this.spreadsheetUpload = await this.getView().getController().getOwnerComponent()
+ .createComponent({
+ usage: "spreadsheetImporter",
+ async: true,
+ componentData: {
+ context: this,
+ createActiveEntity: true,
+ debug: false,
+ deepDownloadConfig: {
+ deepLevel: 2,
+ deepExport: true,
+ addKeysToExport: true,
+ showOptions: false,
+ filename: "Orders123",
+ columns : {
+ "OrderNo":{
+ "order": 1
+ },
+ "buyer": {
+ "order": 3
+ },
+ "Items": {
+ "quantity" : {
+ "order": 2
+ },
+ "title": {
+ "order": 4
+ }
+ },
+ "Shipping": {
+ "address" : {
+ "order": 5
+ },
+ }
+ }
+ }
+ }
+ });
+
+ // this.spreadsheetUpload.attachBeforeDownloadFileProcessing(this.onBeforeDownloadFileProcessing, this);
+ // this.spreadsheetUpload.attachBeforeDownloadFileExport(this.onBeforeDownloadFileExport, this);
+
+ this.spreadsheetUpload.triggerDownloadSpreadsheet();
+ },
+
+ onMassUpdate: async function () {
+ this.spreadsheetUpload = await this.getView().getController().getOwnerComponent()
+ .createComponent({
+ usage: "spreadsheetImporter",
+ async: true,
+ componentData: {
+ context: this,
+ action: "UPDATE",
+ updateConfig: {
+ fullUpdate: false
+ },
+ }
+ });
+ this.spreadsheetUpload.openSpreadsheetUploadDialog();
+ },
+
+ // onBeforeDownloadFileProcessing: function (event) {
+ // event.getParameters().data.$XYZData[0].buyer = "Customer 123";
+ // },
+
+ // onBeforeDownloadFileExport: function (event) {
+ // event.getParameters().filename = "Orders123_modified";
+ // }
+ });
+ });
diff --git a/examples/packages/ordersv4freestyle/webapp/manifest.json b/examples/packages/ordersv4freestyle/webapp/manifest.json
index fa8e3090a..bcae9f459 100644
--- a/examples/packages/ordersv4freestyle/webapp/manifest.json
+++ b/examples/packages/ordersv4freestyle/webapp/manifest.json
@@ -132,6 +132,13 @@
"target": [
"TargetMainView"
]
+ },
+ {
+ "name": "RouteOrdersTable",
+ "pattern": "orders",
+ "target": [
+ "TargetOrdersTable"
+ ]
}
],
"targets": {
@@ -141,6 +148,13 @@
"clearControlAggregation": false,
"viewId": "MainView",
"viewName": "MainView"
+ },
+ "TargetOrdersTable": {
+ "viewType": "XML",
+ "transition": "slide",
+ "clearControlAggregation": false,
+ "viewId": "OrdersTable",
+ "viewName": "OrdersTable"
}
}
},
diff --git a/examples/packages/ordersv4freestyle/webapp/view/MainView.view.xml b/examples/packages/ordersv4freestyle/webapp/view/MainView.view.xml
index d98ce9707..5bf771c14 100644
--- a/examples/packages/ordersv4freestyle/webapp/view/MainView.view.xml
+++ b/examples/packages/ordersv4freestyle/webapp/view/MainView.view.xml
@@ -39,7 +39,7 @@
deepLevel: 2,
deepExport: true,
addKeysToExport: true,
- showOptions: true,
+ showOptions: false,
filename: 'Orders_Dialog',
columns : {
'OrderNo':{
diff --git a/examples/packages/ordersv4freestyle/webapp/view/OrdersTable.view.xml b/examples/packages/ordersv4freestyle/webapp/view/OrdersTable.view.xml
new file mode 100644
index 000000000..029982e1a
--- /dev/null
+++ b/examples/packages/ordersv4freestyle/webapp/view/OrdersTable.view.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/packages/server/db/annotations.cds b/examples/packages/server/db/annotations.cds
index 2d2a677d3..58776aa06 100644
--- a/examples/packages/server/db/annotations.cds
+++ b/examples/packages/server/db/annotations.cds
@@ -34,7 +34,9 @@ annotate OrdersService.Orders with @(
Identification: [ //Is the main field group
{Value: createdBy, Label:'{i18n>Customer}'},
{Value: createdAt, Label:'{i18n>Date}'},
- {Value: OrderNo },
+ {Value: OrderNo , Label:'{i18n>OrderNo}'},
+ {Value: currency.code, Label:'{i18n>Currency}'},
+ {Value: buyer, Label:'{i18n>buyer}'},
],
HeaderFacets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Created}', Target: '@UI.FieldGroup#Created'},
@@ -47,7 +49,8 @@ annotate OrdersService.Orders with @(
],
FieldGroup#Details: {
Data: [
- {Value: currency.code, Label:'{i18n>Currency}'}
+ {Value: currency.code, Label:'{i18n>Currency}'},
+ {Value: buyer, Label:'{i18n>buyer}'}
]
},
FieldGroup#Created: {
diff --git a/examples/packages/server/db/data/sap.capire.orders-OrderItems.csv b/examples/packages/server/db/data/sap.capire.orders-OrderItems.csv
index 21b4bd3c9..9a100450e 100644
--- a/examples/packages/server/db/data/sap.capire.orders-OrderItems.csv
+++ b/examples/packages/server/db/data/sap.capire.orders-OrderItems.csv
@@ -1,4 +1,8 @@
ID;order_ID;quantity;product_ID;title;quantity;price;validFrom;timestamp;date;time
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11;11.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15;11.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
-e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28;11.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
\ No newline at end of file
+e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28;11.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
+e9641166-e050-4261-bfee-d1e797e6cb8f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Superman;98;99.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
+5c6c1ecd-c954-42f0-8349-3a79cfbfab51;64e718c9-ff99-47f1-8ca3-950c850777d9;2;800;Batman;98;99.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
+5c6c1ecd-c954-42f0-8349-3a79cfbfab52;64e718c9-ff99-47f1-8ca3-950c850777d9;2;801;Deadpool;98;99.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
+5c6c1ecd-c954-42f0-8349-3a79cfbfab53;64e718c9-ff99-47f1-8ca3-950c850777d9;2;802;Antman;98;99.11;2022-01-01T12:00:00Z;2022-01-01T12:00:00Z;2022-01-01;12:00:00;
\ No newline at end of file
diff --git a/examples/packages/server/db/data/sap.capire.orders-Orders.csv b/examples/packages/server/db/data/sap.capire.orders-Orders.csv
index 431ee3412..15769c5f3 100644
--- a/examples/packages/server/db/data/sap.capire.orders-Orders.csv
+++ b/examples/packages/server/db/data/sap.capire.orders-Orders.csv
@@ -4,4 +4,5 @@ ID;createdAt;createdBy;buyer;OrderNo;currency_code
64e718c9-ff99-47f1-8ca3-950c850777d5;2019-01-29;jane.doe2@test.com;jane.doe2@test.com;200;EUR
64e718c9-ff99-47f1-8ca3-950c850777d6;2019-01-28;jane.doe4@test.com;jane.doe4@test.com;201;EUR
64e718c9-ff99-47f1-8ca3-950c850777d7;2019-01-28;jane.doe4@test.com;jane.doe4@test.com;202;EUR
-64e718c9-ff99-47f1-8ca3-950c850777d8;2019-01-28;jane.doe4@test.com;jane.doe4@test.com;203;EUR
\ No newline at end of file
+64e718c9-ff99-47f1-8ca3-950c850777d8;2019-01-28;jane.doe4@test.com;jane.doe4@test.com;203;EUR
+64e718c9-ff99-47f1-8ca3-950c850777d9;2019-01-28;jane.doe4@test.com;ObjectPageUploadTest;243;EUR
\ No newline at end of file
diff --git a/examples/packages/server/db/schema.cds b/examples/packages/server/db/schema.cds
index e5a323025..a92858d30 100644
--- a/examples/packages/server/db/schema.cds
+++ b/examples/packages/server/db/schema.cds
@@ -19,6 +19,7 @@ entity Orders : cuid, managed {
spreadsheetRow : Integer;
}
+@odata.draft.bypass
entity OrderItems : cuid {
order : Association to Orders;
product : Association to Products;
diff --git a/examples/test/specs/Objects/BaseUpload.js b/examples/test/specs/Objects/BaseUpload.js
new file mode 100644
index 000000000..343756b13
--- /dev/null
+++ b/examples/test/specs/Objects/BaseUpload.js
@@ -0,0 +1,80 @@
+const Base = require("./../Objects/Base");
+
+class BaseUpload {
+ constructor() {
+ this.base = new Base();
+ }
+
+ async uploadFile(filePath, uploadDialogButtonId, uploaderId, uploadButtonText = "Upload") {
+ // Check if dialog is already open
+ const spreadsheetUploadDialogFirstCheck = await browser.asControl({
+ selector: {
+ controlType: "sap.m.Dialog",
+ properties: {
+ contentWidth: "40vw"
+ },
+ searchOpenDialogs: true,
+
+ },
+ forceSelect: true
+ });
+
+ // Only open dialog if it's not already open
+ if (!spreadsheetUploadDialogFirstCheck?._domId) {
+ await this.base.pressById(uploadDialogButtonId);
+ }
+
+ const spreadsheetUploadDialog = await browser.asControl({
+ selector: {
+ controlType: "sap.m.Dialog",
+ properties: {
+ contentWidth: "40vw"
+ },
+ searchOpenDialogs: true
+
+ },
+ forceSelect: true
+ });
+
+ // Verify dialog is open
+ expect(spreadsheetUploadDialog.isOpen()).toBeTruthy();
+
+ // Remove block layer if present
+ try {
+ await browser.execute(() => {
+ const blockLayerPopup = document.getElementById("sap-ui-blocklayer-popup");
+ if (blockLayerPopup) {
+ blockLayerPopup.remove();
+ }
+ });
+ } catch (error) {
+ console.log("sap-ui-blocklayer-popup removed");
+ }
+
+ // Make file input visible
+ try {
+ await browser.execute(() => {
+ document.querySelector("input[type=file]").style.display = "block";
+ });
+ } catch (error) {}
+
+ // Set file path
+ const input = await $("input[type=file]");
+ await input.setValue(filePath);
+
+ // Click upload button
+ await browser
+ .asControl({
+ selector: {
+ controlType: "sap.m.Button",
+ properties: {
+ text: uploadButtonText
+ }
+ },
+ forceSelect: true
+ })
+ .press();
+ }
+}
+
+module.exports = BaseUpload;
diff --git a/examples/test/specs/download/DownloadSpreadsheetListReport.test.js b/examples/test/specs/download/DownloadSpreadsheetListReport.test.js
index 245aef4df..a35dea391 100644
--- a/examples/test/specs/download/DownloadSpreadsheetListReport.test.js
+++ b/examples/test/specs/download/DownloadSpreadsheetListReport.test.js
@@ -2,6 +2,18 @@ const path = require("path");
const fs = require("fs");
const XLSX = require("xlsx");
const Base = require("./../Objects/Base");
+const { wdi5 } = require("wdio-ui5-service");
+
+// Test Constants
+const TEST_CONSTANTS = {
+ EXPECTED_FILE_NAME: "Orders12.xlsx",
+ DOWNLOAD_TIMEOUT: 20000,
+ EXPECTED_ORDER_NUMBER: "2",
+ EXPECTED_FIELDS: {
+ ID: "ID[ID]",
+ ORDER_NUMBER: "Order Number[OrderNo]"
+ }
+};
describe("Download Spreadsheet List Report", () => {
let downloadDir;
@@ -9,36 +21,50 @@ describe("Download Spreadsheet List Report", () => {
before(async () => {
BaseClass = new Base();
scenario = global.scenario;
- // If you need it globally, you can set it on browser.config, or just reuse the same path logic as in wdio.conf.js.
- downloadDir = path.resolve(__dirname, "../../downloads"); // Adjust the relative path if needed
+ downloadDir = path.resolve(__dirname, "../../downloads");
});
it("should trigger download button", async () => {
// Trigger the download
- await BaseClass.pressById("__component0---downloadButton");
+ const object = await browser.asControl({
+ forceSelect: true,
+ selector: {
+ id: new RegExp(".*downloadButton$")
+ }
+ });
+ await object.press();
});
- it("should trigger download code", async () => {
+ it("should trigger download code", async () => {
// Trigger the download
await BaseClass.pressById("container-ordersv4freestyle---MainView--downloadButtonCode");
});
it("Download spreadsheet and verify content", async () => {
- const expectedFileName = "Orders12.xlsx";
+ const expectedFileName = TEST_CONSTANTS.EXPECTED_FILE_NAME;
- // Wait until the specific file is downloaded
- await browser.waitUntil(
- () => {
- const files = fs.readdirSync(downloadDir);
- return files.includes(expectedFileName);
- },
- {
- timeout: 20000,
- timeoutMsg: `Expected ${expectedFileName} to be downloaded within 20s`
- }
- );
+ try {
+ await browser.waitUntil(
+ async () => {
+ try {
+ const files = await fs.promises.readdir(downloadDir);
+ return files.includes(expectedFileName);
+ } catch (error) {
+ console.warn(`Error reading directory: ${error.message}`);
+ return false;
+ }
+ },
+ {
+ timeout: TEST_CONSTANTS.DOWNLOAD_TIMEOUT,
+ timeoutMsg: `Expected ${expectedFileName} to be downloaded within ${TEST_CONSTANTS.DOWNLOAD_TIMEOUT}ms`,
+ interval: 500 // Check every 500ms
+ }
+ );
+ } catch (error) {
+ throw new Error(`Download failed: ${error.message}`);
+ }
- const filePath = path.join(downloadDir, expectedFileName);
+ const filePath = path.join(downloadDir, TEST_CONSTANTS.EXPECTED_FILE_NAME);
expect(fs.existsSync(filePath)).toBeTruthy();
const workbook = XLSX.readFile(filePath);
@@ -49,14 +75,14 @@ describe("Download Spreadsheet List Report", () => {
expect(data.length).toBeGreaterThan(0);
if (data[0]) {
- expect(data[0]["ID[ID]"]).toBeDefined();
- expect(data[0]["Order Number[OrderNo]"]).toBe("2");
+ expect(data[0][TEST_CONSTANTS.EXPECTED_FIELDS.ID]).toBeDefined();
+ expect(data[0][TEST_CONSTANTS.EXPECTED_FIELDS.ORDER_NUMBER]).toBe(TEST_CONSTANTS.EXPECTED_ORDER_NUMBER);
}
});
it("Download spreadsheet and verify multiple sheets and OrderItems content", async () => {
const expectedFileName = "Orders123.xlsx";
-
+
// Wait until the specific file is downloaded
await browser.waitUntil(
() => {
@@ -74,29 +100,27 @@ describe("Download Spreadsheet List Report", () => {
const workbook = XLSX.readFile(filePath);
expect(workbook.SheetNames.length).toBe(4);
- expect(workbook.SheetNames).toContain('OrderItems');
+ expect(workbook.SheetNames).toContain("OrderItems");
- const orderItemsSheet = workbook.Sheets['OrderItems'];
+ const orderItemsSheet = workbook.Sheets["OrderItems"];
const orderItemsData = XLSX.utils.sheet_to_json(orderItemsSheet);
expect(orderItemsData.length).toBeGreaterThan(0);
- const orderItem = orderItemsData[0];
- expect(orderItem['ID[ID]']).toBeDefined();
- expect(orderItem['IsActiveEntity[IsActiveEntity]']).toBeDefined();
- expect(orderItem['Quantity[quantity]']).toBeDefined();
- expect(orderItem['order_ID[order_ID]']).toBeDefined();
+ const orderItem = orderItemsData[0];
+ expect(orderItem["ID[ID]"]).toBeDefined();
+ expect(orderItem["IsActiveEntity[IsActiveEntity]"]).toBeDefined();
+ expect(orderItem["Quantity[quantity]"]).toBeDefined();
+ expect(orderItem["order_ID[order_ID]"]).toBeDefined();
});
after(async () => {
- // Clean up
- let files = fs.readdirSync(downloadDir);
- const downloadedFile = files.find((file) => file.endsWith(".xlsx"));
- const filePath = path.join(downloadDir, downloadedFile);
- fs.unlinkSync(filePath);
- // remove content in download dir
- files = fs.readdirSync(downloadDir);
- files.forEach((file) => {
- fs.unlinkSync(path.join(downloadDir, file));
+ // Clean up - only delete test files
+ const testFiles = ['Orders12.xlsx', 'Orders123.xlsx'];
+ testFiles.forEach(file => {
+ const filePath = path.join(downloadDir, file);
+ if (fs.existsSync(filePath)) {
+ fs.unlinkSync(filePath);
+ }
});
});
});
diff --git a/examples/test/specs/update/DownloadAndUpdateSpreadsheetObjectPage.test.js b/examples/test/specs/update/DownloadAndUpdateSpreadsheetObjectPage.test.js
new file mode 100644
index 000000000..7a6a754d2
--- /dev/null
+++ b/examples/test/specs/update/DownloadAndUpdateSpreadsheetObjectPage.test.js
@@ -0,0 +1,178 @@
+const path = require("path");
+const fs = require("fs");
+const XLSX = require("xlsx");
+const Base = require("./../Objects/Base");
+const BaseUpload = require("./../Objects/BaseUpload");
+const { wdi5 } = require("wdio-ui5-service");
+
+const TEST_CONSTANTS = {
+ FILE: {
+ NAME: "OrderItems.xlsx",
+ TIMEOUT: 20000,
+ SHEET_NAME: "Sheet1"
+ },
+ ORDER: {
+ ID: "64e718c9-ff99-47f1-8ca3-950c850777d9",
+ NEW_QUANTITY: 999
+ },
+ SELECTORS: {
+ UPLOAD_DIALOG: {
+ BUTTON_ID: "ui.v4.ordersv4fe::OrdersObjectPage--fe::table::Items::LineItem::CustomAction::ObjectPageExtControllerUpdate",
+ UPLOADER_ID: "__uploader1",
+ UPLOAD_BUTTON_TEXT: "Upload"
+ },
+ OVERFLOW_BUTTON: "overflowButton$",
+ DOWNLOAD_BUTTON: "__button27"
+ },
+ API: {
+ BASE_URL: "http://localhost:4004/odata/v4/orders/Orders"
+ },
+ WAIT_TIME: 4000
+};
+
+describe("Download and Update Spreadsheet Object Page", () => {
+ let BaseClass, BaseUploadClass, downloadDir;
+
+ before(async () => {
+ BaseClass = new Base();
+ BaseUploadClass = new BaseUpload();
+ downloadDir = path.resolve(__dirname, "../../downloads");
+ });
+
+ it("should set entity to draft state", async () => {
+ const url = `${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.ORDER.ID},IsActiveEntity=true)/OrdersService.draftEdit`;
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ 'Accept': 'application/json;odata.metadata=minimal;IEEE754Compatible=true',
+ 'Content-Type': 'application/json;charset=UTF-8;IEEE754Compatible=true',
+ 'Accept-Language': 'en',
+ 'Prefer': 'handling=strict'
+ },
+ body: JSON.stringify({
+ PreserveChanges: true
+ })
+ });
+
+ expect(response.ok).toBeTruthy();
+ await BaseClass.dummyWait(1000);
+ });
+
+ it("should navigate to object page", async () => {
+ await wdi5.goTo(`#/orders(ID=${TEST_CONSTANTS.ORDER.ID},IsActiveEntity=false)`);
+ await BaseClass.dummyWait(1000);
+ });
+
+ it("should open upload dialog", async () => {
+ const uploadButton = await browser.asControl({
+ selector: {
+ id: TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.BUTTON_ID
+ }
+ });
+ await uploadButton.press();
+ });
+
+ it("should open overflow menu and download spreadsheet", async () => {
+ // Press overflow button
+ const overflowButton = await browser.asControl({
+ selector: {
+ controlType: "sap.m.Button",
+ searchOpenDialogs: true,
+ // Add any specific properties that identify the download button
+ // This could be an icon, text, or other unique identifier
+ properties: {
+ // Adjust these properties based on your button's characteristics
+ icon: "sap-icon://overflow"
+ // or text: "Download"
+ }
+ }
+ });
+ await overflowButton.press();
+
+ // Find download button in the toolbar content
+ const downloadButton = await browser.asControl({
+ selector: {
+ controlType: "sap.m.Button",
+ searchOpenDialogs: true,
+ // Add any specific properties that identify the download button
+ // This could be an icon, text, or other unique identifier
+ properties: {
+ // Adjust these properties based on your button's characteristics
+ text: "Download Data as Spreadsheet"
+ // or text: "Download"
+ }
+ }
+ });
+
+ await downloadButton.press();
+
+ // Wait for download
+ await browser.waitUntil(
+ () => {
+ const files = fs.readdirSync(downloadDir);
+ return files.includes(TEST_CONSTANTS.FILE.NAME);
+ },
+ {
+ timeout: TEST_CONSTANTS.FILE.TIMEOUT,
+ timeoutMsg: `Expected ${TEST_CONSTANTS.FILE.NAME} to be downloaded within ${TEST_CONSTANTS.FILE.TIMEOUT}ms`
+ }
+ );
+ });
+
+ it("should modify spreadsheet data", async () => {
+ const filePath = path.join(downloadDir, TEST_CONSTANTS.FILE.NAME);
+ const workbook = XLSX.readFile(filePath);
+ const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
+ const data = XLSX.utils.sheet_to_json(firstSheet);
+
+ // Update quantity for all rows
+ data.forEach(row => {
+ row["Quantity[quantity]"] = TEST_CONSTANTS.ORDER.NEW_QUANTITY;
+ });
+
+ // Save modified data
+ const workbookNew = XLSX.utils.book_new();
+ const worksheetNew = XLSX.utils.json_to_sheet(data);
+ XLSX.utils.book_append_sheet(workbookNew, worksheetNew, TEST_CONSTANTS.FILE.SHEET_NAME);
+ XLSX.writeFile(workbookNew, filePath);
+
+ this.filePath = filePath;
+ });
+
+ it("should upload modified file", async () => {
+ await BaseUploadClass.uploadFile(
+ this.filePath,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.BUTTON_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOADER_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOAD_BUTTON_TEXT
+ );
+ });
+
+ it("should save object page", async () => {
+ const saveButton = await browser.asControl({
+ selector: {
+ id: "ui.v4.ordersv4fe::OrdersObjectPage--fe::FooterBar::StandardAction::Save"
+ },
+ forceSelect: true
+ });
+ await saveButton.press();
+ await BaseClass.dummyWait(TEST_CONSTANTS.WAIT_TIME);
+ });
+
+ it("should verify updated quantities", async () => {
+ const response = await fetch(`${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.ORDER.ID},IsActiveEntity=true)/Items`);
+ const data = await response.json();
+
+ data.value.forEach(item => {
+ expect(item.quantity).toBe(TEST_CONSTANTS.ORDER.NEW_QUANTITY);
+ });
+ });
+
+ after(async () => {
+ // Cleanup downloaded files
+ const filePath = path.join(downloadDir, TEST_CONSTANTS.FILE.NAME);
+ if (fs.existsSync(filePath)) {
+ fs.unlinkSync(filePath);
+ }
+ });
+});
diff --git a/examples/test/specs/updatefreestyle/DownloadAndUpdateSpreadsheetListReport.test.js b/examples/test/specs/updatefreestyle/DownloadAndUpdateSpreadsheetListReport.test.js
new file mode 100644
index 000000000..d38e1da62
--- /dev/null
+++ b/examples/test/specs/updatefreestyle/DownloadAndUpdateSpreadsheetListReport.test.js
@@ -0,0 +1,359 @@
+const path = require("path");
+const fs = require("fs");
+const XLSX = require("xlsx");
+const Base = require("./../Objects/Base");
+const BaseUpload = require("./../Objects/BaseUpload");
+const { wdi5 } = require("wdio-ui5-service");
+
+// Test Constants
+const TEST_CONSTANTS = {
+ FILE: {
+ NAME: "Orders.xlsx",
+ TIMEOUT: 20000
+ },
+ UPDATES: {
+ ORDER_NUMBER: 100,
+ USER_ID: "Customer 123"
+ },
+ DOWNLOAD: {
+ WAIT_AFTER_UPLOAD: 4000,
+ TIMEOUT: 20000,
+ WAIT_AFTER_DOWNLOAD: 4000,
+ WAIT_AFTER_UPLOAD_AGAIN: 4000,
+ WAIT_AFTER_UPLOAD_AGAIN_2: 4000,
+ WAIT_AFTER_UPLOAD_AGAIN_3: 4000
+ },
+ SELECTORS: {
+ DOWNLOAD_BUTTON: ".*downloadButtonWithoutDialog$",
+ UPLOAD_DIALOG: {
+ BUTTON_ID: "container-ordersv4freestyle---OrdersTable--updatedButtonCode4",
+ UPLOADER_ID: "__uploader1",
+ UPLOAD_BUTTON_TEXT: "Upload"
+ }
+ },
+ API: {
+ BASE_URL: "http://localhost:4004/odata/v4/orders/Orders",
+ ROW_1_ID: "64e718c9-ff99-47f1-8ca3-950c850777d4",
+ ROW_2_ID: "64e718c9-ff99-47f1-8ca3-950c850777d5",
+ ROW_3_ID: "64e718c9-ff99-47f1-8ca3-950c850777d6",
+ ROW_4_ID: "64e718c9-ff99-47f1-8ca3-950c850777d7"
+ }
+};
+
+let BaseClass = undefined;
+let BaseUploadClass = undefined;
+
+describe("Download Spreadsheet List Report", () => {
+ let scenario;
+ let downloadDir;
+
+ before(async () => {
+ BaseClass = new Base();
+ BaseUploadClass = new BaseUpload();
+ scenario = global.scenario;
+ downloadDir = path.resolve(__dirname, "../../downloads");
+
+ await wdi5.goTo("#/orders");
+ });
+
+ it("should trigger download button", async () => {
+ const object = await browser.asControl({
+ forceSelect: true,
+ selector: {
+ id: new RegExp(TEST_CONSTANTS.SELECTORS.DOWNLOAD_BUTTON)
+ }
+ });
+ await object.press();
+ });
+
+ it("Download spreadsheet and verify content", async () => {
+ await browser.waitUntil(
+ () => {
+ const files = fs.readdirSync(downloadDir);
+ return files.includes(TEST_CONSTANTS.FILE.NAME);
+ },
+ {
+ timeout: TEST_CONSTANTS.FILE.TIMEOUT,
+ timeoutMsg: `Expected ${TEST_CONSTANTS.FILE.NAME} to be downloaded within ${TEST_CONSTANTS.FILE.TIMEOUT}ms`
+ }
+ );
+
+ this.filePath = path.join(downloadDir, TEST_CONSTANTS.FILE.NAME);
+ expect(fs.existsSync(this.filePath)).toBeTruthy();
+
+ const workbook = XLSX.readFile(this.filePath);
+ expect(workbook.SheetNames.length).toBeGreaterThan(0);
+
+ const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
+ const data = XLSX.utils.sheet_to_json(firstSheet);
+ expect(data.length).toBeGreaterThan(0);
+
+ // check for every data row that IsActiveEntity[IsActiveEntity] should be available and true
+ data.forEach((row) => {
+ expect(row["IsActiveEntity[IsActiveEntity]"]).toBeDefined();
+ expect(row["IsActiveEntity[IsActiveEntity]"]).toBe(true);
+ });
+ // change first row of Order Number[OrderNo] to 100
+ data[0]["Order Number[OrderNo]"] = TEST_CONSTANTS.UPDATES.ORDER_NUMBER;
+ // change second row of Benutzer-ID[buyer] to "Customer 123"
+ data[1]["User ID[buyer]"] = TEST_CONSTANTS.UPDATES.USER_ID;
+
+ // save the file
+ const workbookNew = XLSX.utils.book_new();
+ const worksheetNew = XLSX.utils.json_to_sheet(data);
+ XLSX.utils.book_append_sheet(workbookNew, worksheetNew, "Sheet1");
+ XLSX.writeFile(workbookNew, this.filePath);
+ });
+
+ it("upload file", async () => {
+ await BaseUploadClass.uploadFile(
+ this.filePath,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.BUTTON_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOADER_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOAD_BUTTON_TEXT
+ );
+ });
+
+ it("check if the file is uploaded", async () => {
+ await BaseClass.dummyWait(4000);
+
+ const row1 = await fetch(`${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.API.ROW_1_ID},IsActiveEntity=true)`);
+ const row2 = await fetch(`${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.API.ROW_2_ID},IsActiveEntity=true)`);
+ const row1Data = await row1.json();
+ const row2Data = await row2.json();
+
+ expect(row1Data.OrderNo).toBe(TEST_CONSTANTS.UPDATES.ORDER_NUMBER.toString());
+ expect(row2Data.buyer).toBe(TEST_CONSTANTS.UPDATES.USER_ID);
+ });
+
+ it("set entity to draft", async () => {
+ try {
+ // Make a POST request to create a draft version
+ const url = `${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.API.ROW_3_ID},IsActiveEntity=true)/OrdersService.draftEdit`;
+ console.log(url);
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ 'Accept': 'application/json;odata.metadata=minimal;IEEE754Compatible=true',
+ 'Content-Type': 'application/json;charset=UTF-8;IEEE754Compatible=true',
+ 'Accept-Language': 'en',
+ 'Prefer': 'handling=strict'
+ },
+ body: JSON.stringify({
+ PreserveChanges: true
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to set entity to draft: ${response.status} ${response.statusText}`);
+ }
+
+ await BaseClass.dummyWait(1000);
+ } catch (error) {
+ throw new Error(`Failed to set entity to draft: ${error.message}`);
+ }
+ });
+
+ it("delete Orders.xlsx file", async () => {
+ try {
+ const filePath = path.join(downloadDir, TEST_CONSTANTS.FILE.NAME);
+ if (fs.existsSync(filePath)) {
+ fs.unlinkSync(filePath);
+ }
+ expect(fs.existsSync(filePath)).toBeFalsy();
+ } catch (error) {
+ throw new Error(`Failed to delete file: ${error.message}`);
+ }
+ });
+
+ it("should trigger download button again", async () => {
+ try {
+ const object = await browser.asControl({
+ forceSelect: true,
+ selector: {
+ id: new RegExp(TEST_CONSTANTS.SELECTORS.DOWNLOAD_BUTTON)
+ }
+ });
+ await object.press();
+ } catch (error) {
+ throw new Error(`Failed to trigger download: ${error.message}`);
+ }
+ });
+
+ it("change excel file with wrong draft state and save", async () => {
+ try {
+ // Wait for download and verify file exists
+ await browser.waitUntil(
+ () => {
+ const files = fs.readdirSync(downloadDir);
+ return files.includes(TEST_CONSTANTS.FILE.NAME);
+ },
+ {
+ timeout: TEST_CONSTANTS.DOWNLOAD.TIMEOUT,
+ timeoutMsg: `Expected ${TEST_CONSTANTS.FILE.NAME} to be downloaded within ${TEST_CONSTANTS.DOWNLOAD.TIMEOUT}ms`
+ }
+ );
+
+ this.filePath = path.join(downloadDir, TEST_CONSTANTS.FILE.NAME);
+ const workbook = XLSX.readFile(this.filePath);
+ const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
+ const data = XLSX.utils.sheet_to_json(firstSheet);
+
+ // Update the draft and active entities
+ data.forEach((row) => {
+ if (row["ID[ID]"] === TEST_CONSTANTS.API.ROW_3_ID) {
+ row["Order Number[OrderNo]"] = "999";
+ row["IsActiveEntity[IsActiveEntity]"] = true;
+ }
+ if (row["ID[ID]"] === TEST_CONSTANTS.API.ROW_4_ID) {
+ row["Order Number[OrderNo]"] = "888";
+ row["IsActiveEntity[IsActiveEntity]"] = false;
+ }
+ });
+
+ // Save updated file
+ const workbookNew = XLSX.utils.book_new();
+ const worksheetNew = XLSX.utils.json_to_sheet(data);
+ XLSX.utils.book_append_sheet(workbookNew, worksheetNew, TEST_CONSTANTS.FILE.SHEET_NAME);
+ XLSX.writeFile(workbookNew, this.filePath);
+ } catch (error) {
+ throw new Error(`Failed to update excel file: ${error.message}`);
+ }
+ });
+
+ it("upload file again", async () => {
+ await BaseUploadClass.uploadFile(
+ this.filePath,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.BUTTON_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOADER_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOAD_BUTTON_TEXT
+ );
+ });
+
+ it("check if correct errors are shown", async () => {
+ const messageDialog = await browser.asControl({
+ selector: {
+ controlType: "sap.m.Dialog",
+ properties: {
+ title: "Upload Error"
+ },
+ searchOpenDialogs: true
+ }
+ });
+ const modelData = await messageDialog.getModel("messages");
+ const errorData = await modelData.getData();
+ const error = errorData._baseObject[0];
+ expect(error.title).toEqual('Active and draft entity mismatch');
+ expect(error.details.length).toEqual(2);
+ expect(error.details[0].description).toEqual('Uploaded Object ID=64e718c9-ff99-47f1-8ca3-950c850777d6, IsActiveEntity=true has Active status, but the current entity is Draft');
+ expect(error.details[1].description).toEqual('Uploaded Object ID=64e718c9-ff99-47f1-8ca3-950c850777d7, IsActiveEntity=false has Draft status, but the current entity is Active');
+ })
+
+ it("continue and upload data", async () => {
+ const continueAynwayButton = await browser.asControl({
+ selector: {
+ controlType: "sap.m.Button",
+ properties: {
+ text: "Continue Anyway"
+ },
+ searchOpenDialogs: true
+ }
+ });
+ await continueAynwayButton.press();
+ const continueButton = await browser.asControl({
+ selector: {
+ controlType: "sap.m.Button",
+ properties: {
+ text: "Continue"
+ },
+ searchOpenDialogs: true
+ }
+ });
+ await continueButton.press();
+
+ })
+
+ it("check if the file is uploaded", async () => {
+ await BaseClass.dummyWait(4000);
+
+ const row3 = await fetch(`${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.API.ROW_3_ID},IsActiveEntity=false)`);
+ const row4 = await fetch(`${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.API.ROW_4_ID},IsActiveEntity=true)`);
+ const row3Data = await row3.json();
+ const row4Data = await row4.json();
+
+ expect(row3Data.OrderNo).toBe("999");
+ expect(row4Data.OrderNo).toBe("888");
+ });
+
+ it("change excel back to correct state", async () => {
+ try {
+ // Wait for download and verify file exists
+ await browser.waitUntil(
+ () => {
+ const files = fs.readdirSync(downloadDir);
+ return files.includes(TEST_CONSTANTS.FILE.NAME);
+ },
+ {
+ timeout: TEST_CONSTANTS.DOWNLOAD.TIMEOUT,
+ timeoutMsg: `Expected ${TEST_CONSTANTS.FILE.NAME} to be downloaded within ${TEST_CONSTANTS.DOWNLOAD.TIMEOUT}ms`
+ }
+ );
+
+ this.filePath = path.join(downloadDir, TEST_CONSTANTS.FILE.NAME);
+ const workbook = XLSX.readFile(this.filePath);
+ const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
+ const data = XLSX.utils.sheet_to_json(firstSheet);
+
+ // Update the draft and active entities
+ data.forEach((row) => {
+ if (row["ID[ID]"] === TEST_CONSTANTS.API.ROW_3_ID) {
+ row["Order Number[OrderNo]"] = "788";
+ row["IsActiveEntity[IsActiveEntity]"] = false;
+ }
+ if (row["ID[ID]"] === TEST_CONSTANTS.API.ROW_4_ID) {
+ row["Order Number[OrderNo]"] = "987";
+ row["IsActiveEntity[IsActiveEntity]"] = true;
+ }
+ });
+
+ // Save updated file
+ const workbookNew = XLSX.utils.book_new();
+ const worksheetNew = XLSX.utils.json_to_sheet(data);
+ XLSX.utils.book_append_sheet(workbookNew, worksheetNew, TEST_CONSTANTS.FILE.SHEET_NAME);
+ XLSX.writeFile(workbookNew, this.filePath);
+ } catch (error) {
+ throw new Error(`Failed to update excel file: ${error.message}`);
+ }
+ })
+
+ it("upload file again", async () => {
+ await BaseUploadClass.uploadFile(
+ this.filePath,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.BUTTON_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOADER_ID,
+ TEST_CONSTANTS.SELECTORS.UPLOAD_DIALOG.UPLOAD_BUTTON_TEXT
+ );
+ });
+
+ it("check if the file is correctly uploaded", async () => {
+ await BaseClass.dummyWait(4000);
+
+ const row3 = await fetch(`${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.API.ROW_3_ID},IsActiveEntity=false)`);
+ const row4 = await fetch(`${TEST_CONSTANTS.API.BASE_URL}(ID=${TEST_CONSTANTS.API.ROW_4_ID},IsActiveEntity=true)`);
+ const row3Data = await row3.json();
+ const row4Data = await row4.json();
+
+ expect(row3Data.OrderNo).toBe("788");
+ expect(row4Data.OrderNo).toBe("987");
+ });
+
+ after(async () => {
+ const testFiles = [TEST_CONSTANTS.FILE.NAME];
+ testFiles.forEach((file) => {
+ const filePath = path.join(downloadDir, file);
+ if (fs.existsSync(filePath)) {
+ fs.unlinkSync(filePath);
+ }
+ });
+ });
+});
diff --git a/examples/test/testFiles/ListReportOrdersUpdate.xlsx b/examples/test/testFiles/ListReportOrdersUpdate.xlsx
new file mode 100644
index 000000000..2dea85227
Binary files /dev/null and b/examples/test/testFiles/ListReportOrdersUpdate.xlsx differ
diff --git a/examples/test/testFiles/ObjectPageItemsUpdate.xlsx b/examples/test/testFiles/ObjectPageItemsUpdate.xlsx
new file mode 100644
index 000000000..7076c20b0
Binary files /dev/null and b/examples/test/testFiles/ObjectPageItemsUpdate.xlsx differ
diff --git a/examples/test/wdio-base.conf.js b/examples/test/wdio-base.conf.js
index e5c969304..2c3b08e67 100644
--- a/examples/test/wdio-base.conf.js
+++ b/examples/test/wdio-base.conf.js
@@ -36,6 +36,7 @@ module.exports.config = {
maxInstances: 5,
//
browserName: "chrome",
+ browserVersion: 'stable',
"goog:chromeOptions": {
args:
process.argv.indexOf("--headless") > -1
diff --git a/mkdocs.yml b/mkdocs.yml
index d73c46e95..5e94bfe17 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -18,6 +18,7 @@ nav:
- Central Deployment: pages/CentralDeployment.md
- Button Control: pages/Button.md
- Deep Download: pages/spreadsheetdownload.md
+ - Update: pages/Update.md
- API Reference: 'pages/APIReference.md'
# - UI5 CLI: pages/CLI.md
- Pro:
diff --git a/packages/ui5-cc-spreadsheetimporter/src/Component.gen.d.ts b/packages/ui5-cc-spreadsheetimporter/src/Component.gen.d.ts
index 197fe15f9..36b5c7643 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/Component.gen.d.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/Component.gen.d.ts
@@ -9,6 +9,7 @@ declare module "./Component" {
*/
interface $ComponentSettings extends $UIComponentSettings {
spreadsheetFileName?: string | PropertyBindingInfo;
+ action?: string | PropertyBindingInfo;
context?: object | PropertyBindingInfo | `{${string}}`;
columns?: string[] | PropertyBindingInfo | `{${string}}`;
excludeColumns?: string[] | PropertyBindingInfo | `{${string}}`;
@@ -44,6 +45,7 @@ declare module "./Component" {
bindingCustom?: object | PropertyBindingInfo | `{${string}}`;
showDownloadButton?: boolean | PropertyBindingInfo | `{${string}}`;
deepDownloadConfig?: object | PropertyBindingInfo | `{${string}}`;
+ updateConfig?: object | PropertyBindingInfo | `{${string}}`;
preFileProcessing?: (event: Component$PreFileProcessingEvent) => void;
checkBeforeRead?: (event: Component$CheckBeforeReadEvent) => void;
changeBeforeCreate?: (event: Component$ChangeBeforeCreateEvent) => void;
@@ -59,6 +61,10 @@ declare module "./Component" {
getSpreadsheetFileName(): string;
setSpreadsheetFileName(spreadsheetFileName: string): this;
+ // property: action
+ getAction(): string;
+ setAction(action: string): this;
+
// property: context
getContext(): object;
setContext(context: object): this;
@@ -199,6 +205,10 @@ declare module "./Component" {
getDeepDownloadConfig(): object;
setDeepDownloadConfig(deepDownloadConfig: object): this;
+ // property: updateConfig
+ getUpdateConfig(): object;
+ setUpdateConfig(updateConfig: object): this;
+
// event: preFileProcessing
attachPreFileProcessing(fn: (event: Component$PreFileProcessingEvent) => void, listener?: object): this;
attachPreFileProcessing(data: CustomDataType, fn: (event: Component$PreFileProcessingEvent, data: CustomDataType) => void, listener?: object): this;
diff --git a/packages/ui5-cc-spreadsheetimporter/src/Component.ts b/packages/ui5-cc-spreadsheetimporter/src/Component.ts
index 9382e2b68..02da7ff20 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/Component.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/Component.ts
@@ -2,7 +2,7 @@ import UIComponent from "sap/ui/core/UIComponent";
import JSONModel from "sap/ui/model/json/JSONModel";
import Device from "sap/ui/Device";
import SpreadsheetUpload from "./controller/SpreadsheetUpload";
-import { ComponentData, DeepDownloadConfig, Messages } from "./types";
+import { ComponentData, DeepDownloadConfig, Messages, UpdateConfig } from "./types";
import Log from "sap/base/Log";
import ResourceModel from "sap/ui/model/resource/ResourceModel";
import Logger from "./controller/Logger";
@@ -11,6 +11,7 @@ import Button from "sap/m/Button";
import Controller from "sap/ui/core/mvc/Controller";
import View from "sap/ui/core/mvc/View";
import Util from "./controller/Util";
+import { DefaultConfigs } from "./enums";
/**
* @namespace cc.spreadsheetimporter.XXXnamespaceXXX
*/
@@ -24,6 +25,12 @@ export default class Component extends UIComponent {
constructor(idOrSettings?: string | $ComponentSettings);
constructor(id?: string, settings?: $ComponentSettings);
constructor(id?: string, settings?: $ComponentSettings) {
+ if (id?.deepDownloadConfig) {
+ id.deepDownloadConfig = Util.mergeDeepDownloadConfig(DefaultConfigs.DeepDownload, id.deepDownloadConfig);
+ }
+ if (id?.updateConfig) {
+ id.updateConfig = Util.mergeUpdateConfig(DefaultConfigs.Update, id.updateConfig);
+ }
this.settingsFromContainer = id;
super(id, settings);
}
@@ -33,6 +40,7 @@ export default class Component extends UIComponent {
manifest: "json",
properties: {
spreadsheetFileName: { type: "string", defaultValue: "Template.xlsx" },
+ action: { type: "string", defaultValue: "CREATE" },
context: { type: "object" },
// @ts-ignore
columns: { type: "string[]", defaultValue: [] },
@@ -72,7 +80,8 @@ export default class Component extends UIComponent {
componentContainerData: { type: "object" },
bindingCustom: { type: "object" },
showDownloadButton: { type: "boolean", defaultValue: false },
- deepDownloadConfig: { type: "object", defaultValue: {} }
+ deepDownloadConfig: { type: "object", defaultValue: {} },
+ updateConfig: { type: "object", defaultValue: {} }
//Pro Configurations
},
aggregations: {
@@ -138,6 +147,7 @@ export default class Component extends UIComponent {
componentData != null ? (Object.keys(componentData).length === 0 ? (this.settingsFromContainer as ComponentData) : componentData) : (this.settingsFromContainer as ComponentData);
this.getContentDensityClass();
this.setSpreadsheetFileName(compData?.spreadsheetFileName);
+ this.setAction(compData?.action);
this.setContext(compData?.context);
this.setColumns(compData?.columns);
this.setExcludeColumns(compData?.excludeColumns);
@@ -177,21 +187,11 @@ export default class Component extends UIComponent {
this.setShowOptions(true);
}
- const defaultDeepDownloadConfig: DeepDownloadConfig = {
- addKeysToExport: false,
- setDraftStatus: true,
- deepExport: false,
- deepLevel: 1,
- showOptions: true,
- columns: []
- };
-
- const mergedDeepDownloadConfig = Util.mergeConfig(defaultDeepDownloadConfig, compData.deepDownloadConfig)
+ const mergedDeepDownloadConfig = Util.mergeDeepDownloadConfig(DefaultConfigs.DeepDownload, compData.deepDownloadConfig)
this.setDeepDownloadConfig(mergedDeepDownloadConfig);
- // Pro Configurations - Start
-
- // Pro Configurations - End
+ const mergedUpdateConfig = Util.mergeUpdateConfig(DefaultConfigs.Update, compData.updateConfig)
+ this.setUpdateConfig(mergedUpdateConfig);
// // we could create a device model and use it
model = new JSONModel(Device);
@@ -275,7 +275,7 @@ export default class Component extends UIComponent {
await this.spreadsheetUpload.initializeComponent();
Log.debug("triggerDownloadSpreadsheet", undefined, "SpreadsheetUpload: Component");
if (deepDownloadConfig) {
- this.setDeepDownloadConfig(deepDownloadConfig);
+ this.setDeepDownloadConfig(Util.mergeDeepDownloadConfig(this.getDeepDownloadConfig() as DeepDownloadConfig, deepDownloadConfig));
}
this.spreadsheetUpload.triggerDownloadSpreadsheet();
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/MessageHandler.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/MessageHandler.ts
index f1a6771b6..75aab6a7b 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/MessageHandler.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/MessageHandler.ts
@@ -9,6 +9,10 @@ import { ValueState } from "sap/ui/core/library";
import Log from "sap/base/Log";
import { CustomMessageTypes, FieldMatchType, MessageType } from "../enums";
import * as XLSX from "xlsx";
+import Text from "sap/m/Text";
+import Button from "sap/m/Button";
+import { DialogType, ButtonType } from "sap/m/library";
+import MetadataHandlerV4 from "./odata/MetadataHandlerV4";
/**
* @namespace cc.spreadsheetimporter.XXXnamespaceXXX
@@ -193,6 +197,36 @@ export default class MessageHandler extends ManagedObject {
return availableKeyColumns;
}
+ checkDuplicateKeys(data: ArrayData) {
+ const keyNames = MetadataHandlerV4.getAnnotationProperties(this.spreadsheetUploadController.context, this.spreadsheetUploadController.getOdataType()).properties.$Key as string[];
+ const seenKeys = new Map();
+
+ data.forEach((row, index) => {
+ const keys = keyNames
+ .filter(key => key !== "IsActiveEntity")
+ .map(key => {
+ const matchingColumn = Object.keys(row).find(col => col.includes(`[${key}]`));
+ const value = matchingColumn ? row[matchingColumn].rawValue : undefined;
+ return `${key}=${value}`;
+ });
+ const keyString = JSON.stringify(keys);
+
+ if (seenKeys.has(keyString)) {
+ const errorMessage = {
+ title: this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.duplicateKeys"),
+ type: CustomMessageTypes.DuplicateKeys,
+ row: index + 2,
+ counter: 1,
+ ui5type: MessageType.Error,
+ formattedValue: keys.join(", ")
+ } as Messages;
+ this.addMessageToMessages(errorMessage);
+ } else {
+ seenKeys.set(keyString, index);
+ }
+ });
+ }
+
areMessagesPresent(): boolean {
if (this.messages.some((message) => message.counter > 0)) {
return true;
@@ -203,28 +237,54 @@ export default class MessageHandler extends ManagedObject {
/**
* Display messages.
*/
- async displayMessages(strict?: boolean) {
- this.messageDialog = (await Fragment.load({
- name: "cc.spreadsheetimporter.XXXnamespaceXXX.fragment.MessagesDialog",
- type: "XML",
- controller: this
- })) as Dialog;
- this.messageDialog.setModel(this.spreadsheetUploadController.componentI18n, "i18n");
- this.messageDialog.setModel(new JSONModel(), "messages");
- const messagesGrouped = this.groupMessages(this.messages);
- const sortedMessagesGrouped = this.sortMessagesByTitle(messagesGrouped);
- Log.debug("sortedMessagesGrouped", undefined, "SpreadsheetUpload: MessageHandler", () =>
- this.spreadsheetUploadController.component.logger.returnObject({ sortedMessagesGrouped: sortedMessagesGrouped })
- );
- (this.messageDialog.getModel("messages") as JSONModel).setData(sortedMessagesGrouped);
- const dialogState = this.getWorstType(sortedMessagesGrouped);
- const infoModel = new JSONModel({
- strict: this.spreadsheetUploadController.component.getStrict(),
- strictParameter: strict,
- dialogState: dialogState
+ async displayMessages(strict?: boolean): Promise {
+ return new Promise((resolve, reject) => {
+ Fragment.load({
+ name: "cc.spreadsheetimporter.XXXnamespaceXXX.fragment.MessagesDialog",
+ type: "XML",
+ controller: {
+ ...this,
+ onCloseMessageDialog: () => {
+ this.messageDialog.close();
+ this.messageDialog.destroy();
+ // rest file uploader content
+ this.spreadsheetUploadController.resetContent();
+ reject(new Error("Operation cancelled by user"));
+ },
+ onContinue: async () => {
+ // check if messages has type "ObjectNotFound"
+ if (this.messages.some((message) => message.type.update)) {
+ await this.showConfirmDialog();
+ this.messageDialog.close();
+ } else {
+ this.messageDialog.close();
+ const spreadsheetUploadDialog = this.spreadsheetUploadController.getSpreadsheetUploadDialog();
+ const payloadArrayLength = this.spreadsheetUploadController.payloadArray.length;
+ (spreadsheetUploadDialog.getModel("info") as JSONModel).setProperty("/dataRows", payloadArrayLength);
+ }
+ resolve();
+ },
+ onDownloadErrors: () => {
+ this.onDownloadErrors();
+ }
+ }
+ }).then((dialog: Dialog) => {
+ this.messageDialog = dialog;
+ this.messageDialog.setModel(this.spreadsheetUploadController.componentI18n, "i18n");
+ this.messageDialog.setModel(new JSONModel(), "messages");
+ const messagesGrouped = this.groupMessages(this.messages);
+ const sortedMessagesGrouped = this.sortMessagesByTitle(messagesGrouped);
+ (this.messageDialog.getModel("messages") as JSONModel).setData(sortedMessagesGrouped);
+ const dialogState = this.getWorstType(sortedMessagesGrouped);
+ const infoModel = new JSONModel({
+ strict: this.spreadsheetUploadController.component.getStrict(),
+ strictParameter: strict,
+ dialogState: dialogState
+ });
+ this.messageDialog.setModel(infoModel, "info");
+ this.messageDialog.open();
+ });
});
- this.messageDialog.setModel(infoModel, "info");
- this.messageDialog.open();
}
groupMessages(messages: Messages[]): GroupedMessage[] {
@@ -242,6 +302,12 @@ export default class MessageHandler extends ManagedObject {
messageText = this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.errorInRowWithValueFormatted", [message.row, message.formattedValue, message.rawValue]);
} else if (message.rawValue) {
messageText = this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.errorInRowWithValue", [message.row, message.rawValue]);
+ } else if (message.type === CustomMessageTypes.ObjectNotFound) {
+ messageText = this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.objectNotFoundWithKeys", [message.formattedValue]);
+ } else if (message.type === CustomMessageTypes.DraftEntityMismatch) {
+ messageText = this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.draftEntityMismatchRow", message.formattedValue);
+ } else if (message.type === CustomMessageTypes.DuplicateKeys) {
+ messageText = this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.duplicateKeysRow", [message.formattedValue]);
} else {
messageText = this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.errorInRow", [message.row]);
}
@@ -273,11 +339,23 @@ export default class MessageHandler extends ManagedObject {
this.spreadsheetUploadController.resetContent();
}
- private onContinue() {
- this.messageDialog.close();
- const spreadsheetUploadDialog = this.spreadsheetUploadController.getSpreadsheetUploadDialog();
- const payloadArrayLength = this.spreadsheetUploadController.payloadArray.length;
- (spreadsheetUploadDialog.getModel("info") as JSONModel).setProperty("/dataRows", payloadArrayLength);
+ private async onContinue() {
+ try {
+ // check if messages has type "ObjectNotFound"
+ if (this.messages.some((message) => message.type.update)) {
+ await this.showConfirmDialog();
+ // continue with successful fetched objects
+ }
+
+ this.messageDialog.close();
+ const spreadsheetUploadDialog = this.spreadsheetUploadController.getSpreadsheetUploadDialog();
+ const payloadArrayLength = this.spreadsheetUploadController.payloadArray.length;
+ (spreadsheetUploadDialog.getModel("info") as JSONModel).setProperty("/dataRows", payloadArrayLength);
+
+ } catch (error) {
+ // Handle cancellation
+ this.spreadsheetUploadController.resetContent();
+ }
}
onDownloadErrors() {
@@ -370,4 +448,37 @@ export default class MessageHandler extends ManagedObject {
// Convert MessageType to ValueState
return worstType as unknown as ValueState;
}
+
+ private showConfirmDialog(): Promise {
+ return new Promise((resolve, reject) => {
+ const confirmDialog = new Dialog({
+ type: DialogType.Message,
+ title: this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.confirmTitle"),
+ resizable: false,
+ content: new Text({
+ text: this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.confirmMessage")
+ }),
+ beginButton: new Button({
+ type: ButtonType.Emphasized,
+ text: this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.continue"),
+ press: () => {
+ confirmDialog.close();
+ resolve();
+ }
+ }),
+ endButton: new Button({
+ text: this.spreadsheetUploadController.util.geti18nText("spreadsheetimporter.cancel"),
+ press: () => {
+ confirmDialog.close();
+ reject(new Error("Operation cancelled by user"));
+ }
+ }),
+ afterClose: () => {
+ confirmDialog.destroy();
+ }
+ });
+
+ confirmDialog.open();
+ });
+ }
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/Parser.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/Parser.ts
index 9d6ed7e0a..48be7096e 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/Parser.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/Parser.ts
@@ -134,6 +134,18 @@ export default class Parser extends ManagedObject {
} catch (error) {
this.addMessageToMessages("spreadsheetimporter.errorWhileParsing", util, messageHandler, index, [metadataColumn.label], rawValue);
}
+ } else if (metadataColumn.type === "Edm.Guid") {
+ try {
+ // Check if the value matches GUID format
+ 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;
+ if (typeof rawValue === "string" && guidPattern.test(rawValue)) {
+ payload[columnKey] = rawValue;
+ } else {
+ this.addMessageToMessages("spreadsheetimporter.invalidGuid", util, messageHandler, index, [metadataColumn.label], rawValue);
+ }
+ } catch (error) {
+ this.addMessageToMessages("spreadsheetimporter.errorWhileParsing", util, messageHandler, index, [metadataColumn.label], rawValue);
+ }
} else {
// assign "" only if rawValue is undefined or null
payload[columnKey] = `${rawValue ?? ""}`;
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/SpreadsheetUpload.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/SpreadsheetUpload.ts
index 7527e74a8..7306c71ae 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/SpreadsheetUpload.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/SpreadsheetUpload.ts
@@ -1,7 +1,7 @@
import ManagedObject from "sap/ui/base/ManagedObject";
import Component from "../Component";
import XMLView from "sap/ui/core/mvc/XMLView";
-import { Messages, ListObject, ComponentData } from "../types";
+import { Messages, ListObject, ComponentData, DeepDownloadConfig } from "../types";
import ResourceModel from "sap/ui/model/resource/ResourceModel";
import ResourceBundle from "sap/base/i18n/ResourceBundle";
import OData from "./odata/OData";
@@ -13,7 +13,7 @@ import Log from "sap/base/Log";
import OptionsDialog from "./dialog/OptionsDialog";
import SpreadsheetDialog from "../control/SpreadsheetDialog";
import SpreadsheetUploadDialog from "./dialog/SpreadsheetUploadDialog";
-import { CustomMessageTypes } from "../enums";
+import { Action, CustomMessageTypes } from "../enums";
import VersionInfo from "sap/ui/VersionInfo";
/**
* @namespace cc.spreadsheetimporter.XXXnamespaceXXX
@@ -130,7 +130,7 @@ export default class SpreadsheetUpload extends ManagedObject {
throw new Error(this.util.geti18nText("spreadsheetimporter.bindingError"));
}
this.isODataV4 = this._checkIfODataIsV4(this.binding);
- this.odataHandler = this.createODataHandler(this);
+ this.odataHandler = this.createODataHandler(this, this.messageHandler, this.util);
this.spreadsheetUploadDialogHandler.setODataHandler(this.odataHandler);
this.controller = this.view.getController();
Log.debug("View", undefined, "SpreadsheetUpload: SpreadsheetUpload", () => this.component.logger.returnObject({ view: this.view }));
@@ -140,6 +140,10 @@ export default class SpreadsheetUpload extends ManagedObject {
this.odataKeyList = await this.odataHandler.getKeyList(this._odataType, this.binding);
Log.debug("odataKeyList", undefined, "SpreadsheetUpload: SpreadsheetUpload", () => this.component.logger.returnObject({ odataKeyList: this.odataKeyList }));
this.typeLabelList = await this.odataHandler.getLabelList(this.component.getColumns(), this._odataType, this.component.getExcludeColumns(), this.binding);
+ if(this.component.getAction() === Action.Update || this.component.getAction() === Action.Delete){
+ // keys are needed for the update/delete action in the labellist
+ this.odataHandler.addKeys(this.typeLabelList, this._odataType);
+ }
Log.debug("typeLabelList", undefined, "SpreadsheetUpload: SpreadsheetUpload", () => this.component.logger.returnObject({ typeLabelList: this.typeLabelList }));
if(this.isODataV4) {
@@ -167,11 +171,11 @@ export default class SpreadsheetUpload extends ManagedObject {
* @param {number} version - UI5 version number.
* @returns {OData} OData handler instance.
*/
- createODataHandler(spreadsheetUploadController: SpreadsheetUpload): OData {
+ createODataHandler(spreadsheetUploadController: SpreadsheetUpload, messageHandler: MessageHandler, util: Util): OData {
if (this.isODataV4) {
- return new ODataV4(spreadsheetUploadController);
+ return new ODataV4(spreadsheetUploadController, messageHandler, util);
} else {
- return new ODataV2(spreadsheetUploadController);
+ return new ODataV2(spreadsheetUploadController, messageHandler, util);
}
}
@@ -310,6 +314,15 @@ export default class SpreadsheetUpload extends ManagedObject {
if (options.hasOwnProperty("showDownloadButton")) {
this.component.setShowDownloadButton(options.showDownloadButton);
}
+ if (options.hasOwnProperty("action")) {
+ this.component.setAction(options.action);
+ }
+ if (options.hasOwnProperty("updateConfig")) {
+ this.component.setUpdateConfig(options.updateConfig);
+ }
+ if (options.hasOwnProperty("deepDownloadConfig")) {
+ this.component.setDeepDownloadConfig(Util.mergeDeepDownloadConfig(this.component.getDeepDownloadConfig() as DeepDownloadConfig, options.deepDownloadConfig));
+ }
// Special case for showOptions
if (options.availableOptions && options.availableOptions.length > 0) {
@@ -331,41 +344,45 @@ export default class SpreadsheetUpload extends ManagedObject {
}
}
- refreshBinding(context: any, binding: any, id: any) {
+ refreshBinding(context: any, binding: any, tableObject: any) {
+ const id = tableObject.getId();
+ let refreshFailed = true; // Track if all refresh attempts failed
+
if (context._controller?.getExtensionAPI()) {
// refresh binding in V4 FE context
try {
context._controller.getExtensionAPI().refresh(binding.getPath());
+ refreshFailed = false;
} catch (error) {
Log.error("Failed to refresh binding in V4 FE context: " + error);
}
} else if (context.extensionAPI) {
- let refreshFailed = false;
// refresh binding in V2 FE context
if (context.extensionAPI.refresh) {
try {
context.extensionAPI.refresh(binding.getPath(id));
+ refreshFailed = false;
} catch (error) {
Log.error("Failed to refresh binding in Object Page V2 FE context: " + error);
- refreshFailed = true;
}
}
if (context.extensionAPI.refreshTable) {
try {
context.extensionAPI.refreshTable(id);
+ refreshFailed = false;
} catch (error) {
Log.error("Failed to refresh binding in List Report V2 FE context: " + error);
- refreshFailed = true;
}
}
- // try refresh binding when refresh failed
- if (refreshFailed) {
- try {
- // force refresh only available for v2
- binding.refresh(true);
- } catch (error) {
- Log.error("Failed to refresh binding in other contexts: " + error);
- }
+ }
+
+ // Try direct binding refresh as last resort if all other attempts failed
+ if (refreshFailed) {
+ try {
+ // force refresh parameter only for v2
+ binding.refresh(this._checkIfODataIsV4(binding) ? undefined : true);
+ } catch (error) {
+ Log.error("Failed to refresh binding in other contexts: " + error);
}
}
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/Util.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/Util.ts
index 0a995c173..c8b3d6070 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/Util.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/Util.ts
@@ -2,7 +2,7 @@ import ManagedObject from "sap/ui/base/ManagedObject";
import Log from "sap/base/Log";
import type ResourceBundle from "sap/base/i18n/ResourceBundle";
import MessageBox from "sap/m/MessageBox";
-import type { DeepDownloadConfig, FireEventReturnType, RowData, ValueData } from "../types";
+import type { DeepDownloadConfig, FireEventReturnType, RowData, UpdateConfig, ValueData } from "../types";
import type Component from "../Component";
import type { FieldMatchType } from "../enums";
import ObjectPool from "sap/ui/base/ObjectPool";
@@ -258,7 +258,7 @@ export default class Util extends ManagedObject {
};
}
- static mergeConfig(defaultConfig: DeepDownloadConfig, providedConfig?: DeepDownloadConfig): DeepDownloadConfig {
+ static mergeDeepDownloadConfig(defaultConfig: DeepDownloadConfig, providedConfig?: DeepDownloadConfig): DeepDownloadConfig {
if (!providedConfig) return defaultConfig;
// Deep merge for spreadsheetExportConfig
@@ -269,4 +269,15 @@ export default class Util extends ManagedObject {
return mergedDeepDownloadConfig;
}
+
+ static mergeUpdateConfig(defaultConfig: UpdateConfig, providedConfig?: UpdateConfig): UpdateConfig {
+ if (!providedConfig) return defaultConfig;
+
+ const mergedUpdateConfig: UpdateConfig = {
+ ...defaultConfig,
+ ...providedConfig
+ };
+
+ return mergedUpdateConfig;
+ }
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/dialog/SpreadsheetUploadDialog.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/dialog/SpreadsheetUploadDialog.ts
index 0af2ba22d..269615fd4 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/dialog/SpreadsheetUploadDialog.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/dialog/SpreadsheetUploadDialog.ts
@@ -27,6 +27,7 @@ import SpreadsheetDownloadDialog from "../download/SpreadsheetDownloadDialog";
import SpreadsheetGenerator from "../download/SpreadsheetGenerator";
import SpreadsheetDownload from "../download/SpreadsheetDownload";
import OData from "../odata/OData";
+import { Action } from "../../enums";
type InputType = {
[key: string]: {
@@ -191,6 +192,9 @@ export default class SpreadsheetUploadDialog extends ManagedObject {
if (!this.component.getSkipColumnsCheck()) {
this.messageHandler.checkColumnNames(columnNames, this.component.getFieldMatchType(), this.spreadsheetUploadController.typeLabelList);
}
+ if(this.component.getAction() === Action.Update){
+ this.messageHandler.checkDuplicateKeys(spreadsheetSheetsData);
+ }
}
this.spreadsheetUploadController.payload = spreadsheetSheetsData;
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetDownloadDialog.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetDownloadDialog.ts
index 3147db046..58309cef2 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetDownloadDialog.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetDownloadDialog.ts
@@ -48,7 +48,7 @@ export default class SpreadsheetDownloadDialog extends ManagedObject {
onSave() {
const deepDownloadConfig = this.spreadsheetDownloadDialog.getModel("spreadsheetOptions").getData() as DeepDownloadConfig;
- const mergedConfig = Util.mergeConfig(this.component.getDeepDownloadConfig(), deepDownloadConfig);
+ const mergedConfig = Util.mergeDeepDownloadConfig(this.component.getDeepDownloadConfig(), deepDownloadConfig);
this.component.setDeepDownloadConfig(mergedConfig);
this.spreadsheetUploadDialog.onDownloadDataSpreadsheet();
this.spreadsheetDownloadDialog.close();
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetGenerator.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetGenerator.ts
index 050be4087..28682a5cc 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetGenerator.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/download/SpreadsheetGenerator.ts
@@ -62,7 +62,7 @@ export default class SpreadsheetGenerator extends ManagedObject {
if (spreadsheetExportConfig.addKeysToExport) {
this.odataHandler.addKeys(labelList, this.spreadsheetUploadController.getOdataType());
}
- const sheet = this._getSheet(labelList, data, entityDefinition.SiblingEntity.$Type, spreadsheetExportConfig, entityDefinition["$XYZColumns"], undefined);
+ const sheet = this._getSheet(labelList, data, entityDefinition["$XYZColumns"]);
const sheetName = this.spreadsheetUploadController.getOdataType().split(".").pop();
if (wb.SheetNames.includes(sheetName)) {
sheetName.concat("_1");
@@ -89,7 +89,7 @@ export default class SpreadsheetGenerator extends ManagedObject {
this.odataHandler.addKeys(labelList, currentEntity.$Type, parentEntity, currentEntity.$Partner);
}
- const sheet = this._getSheet(labelList, data, currentEntity.$Type, spreadsheetExportConfig, currentEntity["$XYZColumns"], entityDefinition.SiblingEntity.$Type);
+ const sheet = this._getSheet(labelList, data, currentEntity["$XYZColumns"]);
let sheetName = currentEntity.$Type.split(".").pop();
let suffix = 0;
let originalSheetName = sheetName;
@@ -110,7 +110,7 @@ export default class SpreadsheetGenerator extends ManagedObject {
}
}
- private _getSheet(labelList: any, dataArray: any, entityType: string, spreadsheetExportConfig: DeepDownloadConfig, columnsConfig: string[], parentEntityType?: string): XLSX.WorkSheet {
+ private _getSheet(labelList: any, dataArray: any, columnsConfig: string[]): XLSX.WorkSheet {
let rows = dataArray.length;
let fieldMatchType = this.component.getFieldMatchType();
var worksheet = {} as XLSX.WorkSheet;
@@ -146,7 +146,7 @@ export default class SpreadsheetGenerator extends ManagedObject {
for (const [index, data] of dataArray.entries()) {
let sampleDataValue = "";
rows = index + 1 + startRow;
- if (data[key]) {
+ if (data.hasOwnProperty(key)) {
sampleDataValue = data[key];
} else {
worksheet[XLSX.utils.encode_cell({ c: col, r: rows })] = { v: "", t: "s" }; // Set the cell as empty
@@ -172,7 +172,7 @@ export default class SpreadsheetGenerator extends ManagedObject {
private _getCellForType(type: string, value: any): XLSX.CellObject {
switch (type) {
case "Edm.Boolean":
- return { v: value.toString(), t: "b" };
+ return { v: value, t: "b" };
case "Edm.String":
case "Edm.Guid":
case "Edm.Any":
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandler.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandler.ts
index 773de9cc6..994f26602 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandler.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandler.ts
@@ -37,4 +37,5 @@ export default abstract class MetadataHandler extends ManagedObject {
abstract getLabelList(columns: Columns, odataType: string, odataEntityType: any, excludeColumns: Columns): ListObject;
abstract getKeyList(odataEntityType: any): string[];
abstract getODataEntitiesRecursive(entityName: string, deepLevel: number): any;
+ abstract getKeys(binding: any, payload: any, IsActiveEntity?: boolean, excludeIsActiveEntity?: boolean): Record;
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV2.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV2.ts
index cf1cb2073..b2fdc1c77 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV2.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV2.ts
@@ -145,4 +145,8 @@ export default class MetadataHandlerV2 extends MetadataHandler {
getODataEntitiesRecursive(entityName: string, deepLevel: number): any {
throw new Error("Method not implemented.");
}
+
+ getKeys(binding: any, payload: any, IsActiveEntity?: boolean, excludeIsActiveEntity: boolean = false): Record {
+ throw new Error("Method not implemented.");
+ }
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV4.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV4.ts
index 05e4d236f..cbf8a7d34 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV4.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/MetadataHandlerV4.ts
@@ -1,6 +1,7 @@
import Log from "sap/base/Log";
import { Columns, Property, ListObject, PropertyArray, PropertyObject } from "../../types";
import MetadataHandler from "./MetadataHandler";
+import ODataV4 from "./ODataV4";
/**
* @namespace cc.spreadsheetimporter.XXXnamespaceXXX
*/
@@ -14,12 +15,8 @@ export default class MetadataHandlerV4 extends MetadataHandler {
let entityTypeLabel;
const { annotations, properties } = MetadataHandlerV4.getAnnotationProperties(this.spreadsheetUploadController.context, odataType);
- Log.debug("SpreadsheetUpload: Annotations", undefined, "SpreadsheetUpload: MetadataHandler", () =>
- this.spreadsheetUploadController.component.logger.returnObject(annotations)
- );
- Log.debug("SpreadsheetUpload: Properties", undefined, "SpreadsheetUpload: MetadataHandler", () =>
- this.spreadsheetUploadController.component.logger.returnObject(properties)
- );
+ Log.debug("SpreadsheetUpload: Annotations", undefined, "SpreadsheetUpload: MetadataHandler", () => this.spreadsheetUploadController.component.logger.returnObject(annotations));
+ Log.debug("SpreadsheetUpload: Properties", undefined, "SpreadsheetUpload: MetadataHandler", () => this.spreadsheetUploadController.component.logger.returnObject(properties));
// try get facet label
try {
entityTypeLabel = annotations[odataType]["@com.sap.vocabularies.UI.v1.Facets"][0].Label;
@@ -82,7 +79,7 @@ export default class MetadataHandlerV4 extends MetadataHandler {
listObject.set(propertyName, propertyObject);
}
// if no annotation is found, still try to add the property
- if(!propertyLabel && !propertyName.startsWith("SAP__")){
+ if (!propertyLabel && !propertyName.startsWith("SAP__")) {
let propertyObject: Property = {} as Property;
propertyObject.label = this.getLabel(annotations, properties, propertyName, propertyLabel, odataType);
if (!propertyObject.label) {
@@ -114,7 +111,7 @@ export default class MetadataHandlerV4 extends MetadataHandler {
listObject.set(propertyName, propertyObject);
}
// if no annotation is found, still try to add the property
- if(!propertyLabel && !propertyName.startsWith("SAP__")){
+ if (!propertyLabel && !propertyName.startsWith("SAP__")) {
let propertyObject: Property = {} as Property;
propertyObject.label = this.getLabel(annotations, properties, propertyName, propertyLabel, odataType);
if (!propertyObject.label) {
@@ -143,7 +140,7 @@ export default class MetadataHandlerV4 extends MetadataHandler {
Log.debug(`v: ${propertyName} not found as a LineItem Label`, undefined, "SpreadsheetUpload: MetadataHandlerV4");
}
}
- if (typeof label === 'string' && label.startsWith("{") && label.endsWith("}")) {
+ if (typeof label === "string" && label.startsWith("{") && label.endsWith("}")) {
try {
label = this.parseI18nText(label, this.spreadsheetUploadController.view);
} catch (error) {
@@ -252,9 +249,8 @@ export default class MetadataHandlerV4 extends MetadataHandler {
}
}
}
-
}
-
+
_getExpandsRecursive(mainEntity: any, expands: any, parent?: string, parentExpand?: any, currentLevel: number = 0, deepLevel: number = 99) {
if (currentLevel >= deepLevel) return;
@@ -273,12 +269,38 @@ export default class MetadataHandlerV4 extends MetadataHandler {
}
}
+ // move to metadata handler
+ getKeys(binding: any, payload: any, IsActiveEntity?: boolean, excludeIsActiveEntity: boolean = false): Record {
+ // Get the resolved path
+ let path = MetadataHandlerV4.getResolvedPath(binding);
+
+ // Get the key properties for this entity type
+ const keyNames = MetadataHandlerV4.getAnnotationProperties(this.spreadsheetUploadController.context, this.spreadsheetUploadController.getOdataType()).properties.$Key as string[];
+
+ // Create a map of key names to their values from the payload
+ const keyMap: Record = {};
+ keyNames.forEach((key) => {
+ if (excludeIsActiveEntity && key === "IsActiveEntity") {
+ return;
+ }
+ if (key === "IsActiveEntity") {
+ // If IsActiveEntity is explicitly provided as a parameter, use it
+ // Otherwise, use the value from the payload
+ keyMap[key] = IsActiveEntity !== undefined ? IsActiveEntity : payload[key];
+ } else {
+ keyMap[key] = payload[key];
+ }
+ });
+
+ return keyMap;
+ }
+
/**
* Adds keys from entity to labelList so it will be added to the sheet
- * @param labelList
- * @param entityName
- * @param parentEntity
- * @param partner
+ * @param labelList
+ * @param entityName
+ * @param parentEntity
+ * @param partner
*/
addKeys(labelList: ListObject, entityName: string, parentEntity?: any, partner?: string) {
const { annotations, properties } = MetadataHandlerV4.getAnnotationProperties(this.spreadsheetUploadController.context, entityName);
@@ -305,7 +327,7 @@ export default class MetadataHandlerV4 extends MetadataHandler {
// Create a new Map to make sure the are in the beginning of the spreadsheet
const newLabelList = new Map();
-
+
// Add keys to the new Map
for (const key of keys) {
const propertyObject = {} as Property;
@@ -337,4 +359,38 @@ export default class MetadataHandlerV4 extends MetadataHandler {
const properties = model.getMetaModel().getData()[odataType];
return { annotations, properties };
}
+
+ static formatKeyPredicates(keys: Record, payload: Record): string {
+ // If IsActiveEntity is a key but not in payload, add it with true value as ODataV4 is not able to create draft entities
+ if ("IsActiveEntity" in keys && !("IsActiveEntity" in payload)) {
+ payload = { ...payload, IsActiveEntity: true };
+ }
+
+ const aKeyProperties = Object.keys(keys).map((key) => {
+ // Check if the key exists in our payload
+ if (!(key in payload)) {
+ throw new Error(`Required key property '${key}' not found in payload`);
+ }
+
+ // Encode the key and value
+ const encodedKey = encodeURIComponent(key);
+ const encodedValue = encodeURIComponent(payload[key]);
+
+ // Return the formatted key-value pair
+ return Object.keys(keys).length > 1 ? `${encodedKey}=${encodedValue}` : encodedValue;
+ });
+
+ return `${aKeyProperties.join(",")}`;
+ }
+
+ static getResolvedPath(binding: any): string {
+ let path = binding.getPath();
+ if (binding.getResolvedPath) {
+ path = binding.getResolvedPath();
+ } else {
+ // workaround for getResolvedPath only available from 1.88
+ path = binding.getModel().resolve(binding.getPath(), binding.getContext());
+ }
+ return path;
+ }
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/OData.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/OData.ts
index 2e0e0f986..7d7e3381c 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/OData.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/OData.ts
@@ -13,6 +13,7 @@ import Dialog from "sap/m/Dialog";
import Util from "../Util";
import ODataListBindingV2 from "sap/ui/model/odata/v2/ODataListBinding";
import ODataListBindingV4 from "sap/ui/model/odata/v4/ODataListBinding";
+import MessageHandler from "../MessageHandler";
/**
* @namespace cc.spreadsheetimporter.XXXnamespaceXXX
@@ -23,11 +24,16 @@ export default abstract class OData extends ManagedObject {
private _tables: any[] = [];
busyDialog: Dialog;
spreadsheetUploadController: SpreadsheetUpload;
-
- constructor(spreadsheetUploadController: SpreadsheetUpload) {
+ public createPromises: Promise[] = [];
+ public createContexts: any[] = [];
+ messageHandler: MessageHandler;
+ util: Util;
+ constructor(spreadsheetUploadController: SpreadsheetUpload, messageHandler: MessageHandler, util: Util) {
super();
this.odataMessageHandler = new ODataMessageHandler(spreadsheetUploadController);
this.spreadsheetUploadController = spreadsheetUploadController;
+ this.messageHandler = messageHandler;
+ this.util = util;
}
/**
@@ -50,7 +56,7 @@ export default abstract class OData extends ManagedObject {
await this.createBusyDialog(spreadsheetUploadController);
- // Slice the array into chunks of 'batchSize' if necessary
+ // Slice the array into chunks of 'batchSize' if necessary, if UPDATE max batch size is 100
const slicedPayloadArray = this.processPayloadArray(component.getBatchSize(), payloadArray);
(this.busyDialog.getModel("busyModel") as JSONModel).setProperty("/progressText", `0/${payloadArray.length}`);
let currentProgressPercent = 0;
@@ -60,6 +66,16 @@ export default abstract class OData extends ManagedObject {
for (const batch of slicedPayloadArray) {
// loop over data from spreadsheet file
try {
+ // default for draft scenarios we need to request the object first to get draft status otherwise the update will fail
+ // with options the strategy could be changed to make the update quicker
+ // request all objects in the batch first
+ if (component.getAction() === "UPDATE") {
+ await this.getObjects(model, binding, batch);
+ // TODO: decide to continue or break depending on component.getContinueOnError()
+ // TODO: if getContinueOnError is true, continue with successfull fetched objects
+ }
+
+ // maybe move this loop to createAsync and updateAsync --> parameter will change (breaking change)
for (let payload of batch) {
let fireEventAsyncReturn: FireEventReturnType;
// skip draft and directly create
@@ -75,7 +91,12 @@ export default abstract class OData extends ManagedObject {
if (fireEventAsyncReturn.returnValue) {
payload = fireEventAsyncReturn.returnValue;
}
- this.createAsync(model, binding, payload);
+ if (component.getAction() === "CREATE") {
+ this.createAsync(model, binding, payload);
+ }
+ if (component.getAction() === "UPDATE") {
+ this.updateAsync(model, binding, payload);
+ }
}
// wait for all drafts to be created
await this.submitChanges(model);
@@ -114,7 +135,7 @@ export default abstract class OData extends ManagedObject {
}
}
if (tableObject) {
- spreadsheetUploadController.refreshBinding(context, binding, tableObject.getId());
+ spreadsheetUploadController.refreshBinding(context, binding, tableObject);
}
this.busyDialog.close();
fnResolve();
@@ -145,6 +166,11 @@ export default abstract class OData extends ManagedObject {
// Slice the array into chunks of 'batchSize' if necessary
public processPayloadArray(batchSize: number, payloadArray: string | any[]) {
+ // For UPDATE actions, enforce max batch size of 100
+ if (this.spreadsheetUploadController.component.getAction() === "UPDATE") {
+ batchSize = Math.min(batchSize > 0 ? batchSize : 100, 100);
+ }
+
if (batchSize > 0) {
let slicedPayloadArray = [];
const numOfSlices = Math.ceil(payloadArray.length / batchSize);
@@ -238,6 +264,7 @@ export default abstract class OData extends ManagedObject {
abstract create(model: any, binding: any, payload: any): any;
abstract createAsync(model: any, binding: any, payload: any): any;
+ abstract updateAsync(model: any, binding: any, payload: any): any;
abstract submitChanges(model: any): Promise;
abstract waitForCreation(): Promise;
abstract waitForDraft(): void;
@@ -252,6 +279,6 @@ export default abstract class OData extends ManagedObject {
abstract getBindingFromBinding(binding: any, expand?: any): ODataListBindingV4 | ODataListBindingV2;
abstract fetchBatch(customBinding: ODataListBindingV4 | ODataListBindingV2, batchSize: number): Promise;
abstract addKeys(labelList: ListObject, entityName: string, parentEntity?: any, partner?: string): void;
-
+ abstract getObjects(model: any, binding: any, batch: any): Promise;
// Pro Methods
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV2.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV2.ts
index 24d95bef2..16bdd2d98 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV2.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV2.ts
@@ -6,19 +6,19 @@ import MetadataHandlerV2 from "./MetadataHandlerV2";
import ODataListBinding from "sap/ui/model/odata/v2/ODataListBinding";
import ODataModel from "sap/ui/model/odata/v2/ODataModel";
import ODataMetaModel from "sap/ui/model/odata/ODataMetaModel";
+import MessageHandler from "../MessageHandler";
+import Util from "../Util";
/**
* @namespace cc.spreadsheetimporter.XXXnamespaceXXX
*/
export default class ODataV2 extends OData {
- public createPromises: Promise[] = [];
- public createContexts: any[] = [];
customBinding: ODataListBinding;
submitChangesResponse: any;
private metadataHandler: MetadataHandlerV2;
- constructor(spreadsheetUploadController: SpreadsheetUpload) {
- super(spreadsheetUploadController);
+ constructor(spreadsheetUploadController: SpreadsheetUpload, messageHandler: MessageHandler, util: Util) {
+ super(spreadsheetUploadController, messageHandler, util);
this.metadataHandler = new MetadataHandlerV2(spreadsheetUploadController);
}
create(model: any, binding: any, payload: any) {
@@ -44,6 +44,10 @@ export default class ODataV2 extends OData {
this.createPromises.push(returnObject);
}
+ updateAsync(model: any, binding: any, payload: any) {
+ throw new Error("Method not implemented.");
+ }
+
async checkForErrors(model: any, binding: any, showBackendErrorMessages: Boolean): Promise {
// check if this.submitChangesResponse and this.submitChangesResponse.__batchResponses exist
if (this.submitChangesResponse && this.submitChangesResponse.__batchResponses) {
@@ -137,6 +141,10 @@ export default class ODataV2 extends OData {
}
}
+ getObjects(model: any, binding: any, batch: any): Promise {
+ throw new Error("Method not implemented.");
+ }
+
async getLabelList(columns: Columns, odataType: string, excludeColumns: Columns, binding?: any) {
const metaModel = binding.getModel().getMetaModel();
await metaModel.loaded();
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4.ts
index 03ea40592..231cd2518 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4.ts
@@ -1,4 +1,4 @@
-import { Columns, ListObject } from "../../types";
+import { Columns, ListObject, UpdateConfig } from "../../types";
import OData from "./OData";
import SpreadsheetUpload from "../SpreadsheetUpload";
import Util from "../Util";
@@ -6,6 +6,9 @@ import ODataListBinding from "sap/ui/model/odata/v4/ODataListBinding";
import Log from "sap/base/Log";
import MetadataHandlerV4 from "./MetadataHandlerV4";
import ODataModel from "sap/ui/model/odata/v4/ODataModel";
+import ODataContextBinding from "sap/ui/model/odata/v4/ODataContextBinding";
+import MessageHandler from "../MessageHandler";
+import { ODataV4RequestObjects, BatchContext } from './ODataV4RequestObjects';
type EntityObject = {
$kind: string;
@@ -17,16 +20,17 @@ type EntityObject = {
* @namespace cc.spreadsheetimporter.XXXnamespaceXXX
*/
export default class ODataV4 extends OData {
- public createPromises: Promise[] = [];
- public createContexts: any[] = [];
customBinding: ODataListBinding;
updateGroupId: string;
public metadataHandler: MetadataHandlerV4;
-
- constructor(spreadsheetUploadController: SpreadsheetUpload) {
- super(spreadsheetUploadController);
+ private contexts: BatchContext[];
+ private objectRetriever: ODataV4RequestObjects;
+ constructor(spreadsheetUploadController: SpreadsheetUpload, messageHandler: MessageHandler, util: Util) {
+ super(spreadsheetUploadController, messageHandler, util);
this.updateGroupId = Util.getRandomString(10);
this.metadataHandler = new MetadataHandlerV4(spreadsheetUploadController);
+ this.objectRetriever = new ODataV4RequestObjects(this.metadataHandler, messageHandler, util);
+ this.contexts = [];
}
create(model: any, binding: any, payload: any) {
@@ -43,6 +47,107 @@ export default class ODataV4 extends OData {
this.createPromises.push(returnObject.promise);
}
+ updateAsync(model: any, binding: any, payload: any) {
+ // also do this if we should check for draft entities, this should be default in draft scenarios
+ const keys = this.metadataHandler.getKeys(binding, payload);
+ Log.debug("Processing update operation", undefined, "SpreadsheetUpload: ODataV4", () => ({
+ keys,
+ payload,
+ bindingPath: binding.getPath()
+ }));
+
+ // Now use the keys to find the matching context
+ const currentContext = this.contexts.find((ctx) => Object.entries(keys).every(([key, value]) => ctx.payload[key] === value)) as BatchContext;
+ Log.debug("Found matching context", undefined, "SpreadsheetUpload: ODataV4", () => ({
+ found: !!currentContext,
+ contextDetails: currentContext
+ }));
+
+ if (!currentContext.context) {
+ if (!this.spreadsheetUploadController.component.getContinueOnError()) {
+ // in which context should we continue?
+ throw new Error("Could not find matching context for update operation");
+ } else {
+ Log.debug("No context found for update operation", undefined, "SpreadsheetUpload: ODataV4", () => ({
+ keys,
+ availableContexts: this.contexts.length
+ }));
+ return;
+ }
+ }
+ let { context } = currentContext;
+
+ const currentObject = context.getObject();
+ Log.debug("Current object state", undefined, "SpreadsheetUpload: ODataV4", () => ({
+ currentObject,
+ isDraft: currentObject.HasDraftEntity || !currentObject.IsActiveEntity
+ }));
+
+ // Determine if the current object is a draft or active entity
+ const isDraft = currentObject.HasDraftEntity || !currentObject.IsActiveEntity;
+
+ if (isDraft) {
+ // Switch to the draft entity by creating a new context with IsActiveEntity=false
+ payload.IsActiveEntity = false;
+ const draftKeyPredicates = MetadataHandlerV4.formatKeyPredicates(keys, payload);
+ const path = MetadataHandlerV4.getResolvedPath(binding);
+ Log.debug("Switching to draft entity", undefined, "SpreadsheetUpload: ODataV4", () => ({
+ draftKeyPredicates,
+ path,
+ groupId: this.updateGroupId
+ }));
+ const oDataContextBinding = binding.getModel().bindContext(`${path}(${draftKeyPredicates})`, undefined, { $$groupId: this.updateGroupId }) as ODataContextBinding;
+ context = oDataContextBinding.getBoundContext();
+ }
+
+ // Process all properties from payload except keys
+ Object.entries(payload).forEach(([property, newValue]) => {
+ // Skip if property is a key
+ if (property in keys) {
+ return;
+ }
+ // decide if the full import payload should be sent or only the changed properties
+ const fullUpdate = (this.spreadsheetUploadController.component.getUpdateConfig() as UpdateConfig).fullUpdate;
+ // only columns defined in the updateConfig should be updated
+ const columns = (this.spreadsheetUploadController.component.getUpdateConfig() as UpdateConfig).columns;
+
+ // Helper function to check if value is a date and if it has changed
+ const hasDateValueChanged = (oldValue: any, newValue: any): boolean => {
+ if (!newValue?.toISOString) return true; // not a date, continue with normal comparison
+ const formattedNewDate = newValue.toISOString().substr(0,10);
+ return oldValue !== formattedNewDate;
+ };
+
+ // Check if property should be updated
+ const isPropertyConfigured = columns.length === 0 || columns.includes(property);
+ const hasValueChanged = currentObject[property] !== newValue;
+ const isDateChangeValid = hasDateValueChanged(currentObject[property], newValue);
+
+ if (fullUpdate || (hasValueChanged && isPropertyConfigured && isDateChangeValid)) {
+ Log.debug("Updating property", undefined, "SpreadsheetUpload: ODataV4", () => ({
+ property,
+ oldValue: currentObject[property],
+ newValue,
+ isDate: !!newValue?.toISOString,
+ updateReason: fullUpdate ? 'fullUpdate' : 'valueChanged'
+ }));
+
+ this.createPromises.push(
+ context.setProperty(
+ property,
+ typeof newValue === "object" ? `${newValue.getUTCFullYear()}-${("0" + (newValue.getUTCMonth() + 1)).slice(-2)}-${("0" + newValue.getUTCDate()).slice(-2)}` : newValue
+ )
+ );
+ }
+ });
+
+ Log.debug("Update operation completed", undefined, "SpreadsheetUpload: ODataV4", () => ({
+ pendingPromises: this.createPromises.length,
+ isDraft,
+ context: context.getPath()
+ }));
+ }
+
async submitChanges(model: any): Promise {
return model.submitBatch(this.updateGroupId);
}
@@ -69,21 +174,15 @@ export default class ODataV4 extends OData {
createCustomBinding(binding: any) {
if (this.spreadsheetUploadController.component.getOdataType()) {
- const entityContainer = ODataV4.getContainerName(this.spreadsheetUploadController.context)
+ const entityContainer = ODataV4.getContainerName(this.spreadsheetUploadController.context);
const typeToSearch = this.spreadsheetUploadController.component.getOdataType();
const odataEntityTypeParameterPath = this._findAttributeByType(entityContainer, typeToSearch);
this.customBinding = this.spreadsheetUploadController.view
.getModel()
.bindList("/" + odataEntityTypeParameterPath, null, [], [], { $$updateGroupId: this.updateGroupId }) as ODataListBinding;
} else {
- let path = binding.getPath();
- if (binding.getResolvedPath) {
- path = binding.getResolvedPath();
- } else {
- // workaround for getResolvedPath only available from 1.88
- path = binding.getModel().resolve(binding.getPath(), binding.getContext());
- }
- this.customBinding = binding.getModel().bindList(path, null, [], [], { $$updateGroupId: this.updateGroupId });
+ let path = MetadataHandlerV4.getResolvedPath(binding);
+ this.customBinding = binding.getModel().bindList(path, this.contexts, [], [], { $$updateGroupId: this.updateGroupId });
}
}
@@ -124,7 +223,7 @@ export default class ODataV4 extends OData {
Log.error("Error while getting OData Type. Please specify 'odataType' in options", undefined, "SpreadsheetUpload: ODataV4");
}
} else {
- const entityContainer = ODataV4.getContainerName(this.spreadsheetUploadController.context)
+ const entityContainer = ODataV4.getContainerName(this.spreadsheetUploadController.context);
const odataEntityType = this._findAttributeByType(entityContainer, odataType);
if (!odataEntityType) {
// filter out $kind
@@ -230,4 +329,10 @@ export default class ODataV4 extends OData {
addKeys(labelList: ListObject, entityName: string, parentEntity?: any, partner?: string) {
this.metadataHandler.addKeys(labelList, entityName, parentEntity, partner);
}
+
+ async getObjects(model: any, binding: any, batch: any): Promise {
+ const objects = await this.objectRetriever.getObjects(model, binding, batch);
+ this.contexts = this.objectRetriever.getContexts();
+ return objects;
+ }
}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4RequestObjects.ts b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4RequestObjects.ts
new file mode 100644
index 000000000..2642b7348
--- /dev/null
+++ b/packages/ui5-cc-spreadsheetimporter/src/controller/odata/ODataV4RequestObjects.ts
@@ -0,0 +1,302 @@
+import Filter from "sap/ui/model/Filter";
+import FilterOperator from "sap/ui/model/FilterOperator";
+import Context from "sap/ui/model/odata/v4/Context";
+import ODataListBinding from "sap/ui/model/odata/v4/ODataListBinding";
+import { CustomMessageTypes, MessageType } from "../../enums";
+import MetadataHandlerV4 from "./MetadataHandlerV4";
+import MessageHandler from "../MessageHandler";
+import Util from "../Util";
+import Log from "sap/base/Log";
+
+export interface BatchContext {
+ context: Context;
+ path: string;
+ keyPredicates: string;
+ keys: string[];
+ payload: any;
+}
+
+export class ODataV4RequestObjects {
+ private metadataHandler: MetadataHandlerV4;
+ private messageHandler: MessageHandler;
+ private util: Util;
+ private contexts: BatchContext[] = [];
+
+ constructor(metadataHandler: MetadataHandlerV4, messageHandler: MessageHandler, util: Util) {
+ this.metadataHandler = metadataHandler;
+ this.messageHandler = messageHandler;
+ this.util = util;
+ }
+
+ public getContexts(): BatchContext[] {
+ return this.contexts;
+ }
+
+ async getObjects(model: any, binding: any, batch: any): Promise {
+ Log.debug("Processing batch from spreadsheet", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ batch,
+ bindingPath: binding.getPath(),
+ modelName: model.getMetadata().getName()
+ }));
+ let path = MetadataHandlerV4.getResolvedPath(binding);
+
+ // Get both active and inactive contexts
+ Log.debug("Fetching active entities...", undefined, "SpreadsheetUpload: ODataV4RequestObjects");
+ const { contexts: contextsTrue, objects: objectsTrue } = await this._getFilteredContexts(model, binding, path, batch, true);
+ Log.debug("Found active entities", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ count: objectsTrue.length,
+ objects: objectsTrue
+ }));
+
+ Log.debug("Fetching inactive entities...", undefined, "SpreadsheetUpload: ODataV4RequestObjects");
+ const { contexts: contextsFalse, objects: objectsFalse } = await this._getFilteredContexts(model, binding, path, batch, false);
+ Log.debug("Found inactive entities", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ count: objectsFalse.length,
+ objects: objectsFalse
+ }));
+
+ let objects = this.findEntitiesFromSpreadsheet(batch, objectsTrue, objectsFalse, binding);
+ Log.debug("Matched entities from spreadsheet", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ count: objects.length,
+ objects
+ }));
+
+ // Store contexts
+ this.contexts = this.getContextsFromPayload(batch, contextsTrue, contextsFalse, path, binding);
+ Log.debug("Generated contexts", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ count: this.contexts.length,
+ contexts: this.contexts
+ }));
+
+ const errorFound = this.validateObjectsAndDraftStates(batch, objects, binding);
+ Log.debug("Validation completed", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ errorFound,
+ messageCount: this.messageHandler.messages.length
+ }));
+
+ if (errorFound) {
+ if (this.messageHandler.areMessagesPresent()) {
+ try {
+ await this.messageHandler.displayMessages();
+
+ // Log status changes
+ const statusChanges = batch.map((payload, index) => {
+ const keys = this.metadataHandler.getKeys(binding, payload);
+ const keysWithoutIsActiveEntity = { ...keys };
+ delete keysWithoutIsActiveEntity.IsActiveEntity;
+
+ const originalStatus = payload.IsActiveEntity;
+ const oppositeStatusObject = payload.IsActiveEntity
+ ? objectsFalse.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value))
+ : objectsTrue.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value));
+
+ return {
+ index,
+ keys: keysWithoutIsActiveEntity,
+ originalStatus,
+ newStatus: oppositeStatusObject?.IsActiveEntity,
+ statusChanged: originalStatus !== oppositeStatusObject?.IsActiveEntity
+ };
+ });
+
+ Log.debug("Status changes after user confirmation", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ total: statusChanges.length,
+ changed: statusChanges.filter(c => c.statusChanged).length,
+ details: statusChanges
+ }));
+
+ // Update objects with correct draft status versions
+ objects = batch.map(payload => {
+ const keys = this.metadataHandler.getKeys(binding, payload);
+ const keysWithoutIsActiveEntity = { ...keys };
+ delete keysWithoutIsActiveEntity.IsActiveEntity;
+
+ // Find the object with opposite draft status
+ const oppositeStatusObject = payload.IsActiveEntity
+ ? objectsFalse.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value))
+ : objectsTrue.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value));
+
+ // Update payload to match actual status
+ if (oppositeStatusObject) {
+ payload.IsActiveEntity = oppositeStatusObject.IsActiveEntity;
+ return oppositeStatusObject;
+ }
+ return payload;
+ });
+
+ return objects;
+ } catch (error) {
+ Log.debug("Operation cancelled by user", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ error: error.message
+ }));
+ throw new Error("Operation cancelled by user");
+ }
+ }
+ }
+
+ return objects;
+ }
+
+ private async _getFilteredContexts(
+ model: any,
+ binding: any,
+ path: string,
+ batch: any[],
+ isActive: boolean
+ ): Promise<{
+ contexts: Context[];
+ objects: any[];
+ }> {
+ // Create filters for each object in the batch
+ const batchFilters = batch.map((payload) => {
+ const keys = this.metadataHandler.getKeys(binding, payload);
+ keys.IsActiveEntity = isActive;
+
+ const keyFilters = Object.entries(keys).map(([property, value]) => new Filter(property, FilterOperator.EQ, value));
+
+ return new Filter({
+ filters: keyFilters,
+ and: true
+ });
+ });
+
+ // Combine all batch filters with OR
+ const combinedFilter = new Filter({
+ filters: batchFilters,
+ and: false
+ });
+
+ // Bind the list with the filter
+ const listBinding = model.bindList(path, null, [], [], { $$updateGroupId: "$auto" }) as ODataListBinding;
+ listBinding.filter(combinedFilter);
+
+ // Request contexts and map to objects
+ const contexts = await listBinding.requestContexts(0, batch.length);
+ const objects = await Promise.all(contexts.map((context) => context.getObject()));
+
+ return { contexts, objects };
+ }
+
+ private findEntitiesFromSpreadsheet(batch: any[], objectsTrue: any[], objectsFalse: any[], binding: any): any[] {
+ const matchResults = [];
+
+ batch.forEach((payload, index) => {
+ const keys = this.metadataHandler.getKeys(binding, payload);
+ const keysWithoutIsActiveEntity = { ...keys };
+ delete keysWithoutIsActiveEntity.IsActiveEntity;
+
+ // try to find the matching object from the spreadsheet in the requested objects
+ const matchingObjectTrue = objectsTrue.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value));
+ const matchingObjectFalse = objectsFalse.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value));
+
+ let matchingObject = payload.IsActiveEntity ? matchingObjectTrue : matchingObjectFalse;
+
+ // 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
+ if(!matchingObject) {
+ matchingObject = payload.IsActiveEntity ? matchingObjectFalse : matchingObjectTrue;
+ }
+
+ matchResults.push({
+ index,
+ keys: keysWithoutIsActiveEntity,
+ requestedStatus: payload.IsActiveEntity,
+ foundIn: matchingObjectTrue ? 'objectsTrue' : (matchingObjectFalse ? 'objectsFalse' : 'notFound'),
+ object: matchingObject
+ });
+ });
+
+ Log.debug("Entity matching results", undefined, "SpreadsheetUpload: ODataV4RequestObjects", () => ({
+ total: matchResults.length,
+ found: matchResults.filter(r => r.object).length,
+ notFound: matchResults.filter(r => !r.object).length,
+ details: matchResults
+ }));
+
+ return matchResults.map(result => result.object);
+ }
+
+ private getContextsFromPayload(batch: any[], contextsTrue: Context[], contextsFalse: Context[], path: string, binding: any): BatchContext[] {
+ return batch.map((payload) => {
+ const keys = this.metadataHandler.getKeys(binding, payload);
+ const keyPredicates = MetadataHandlerV4.formatKeyPredicates(keys, payload);
+ const isActiveEntity = payload.IsActiveEntity;
+ const keysWithoutIsActiveEntity = { ...keys };
+ delete keysWithoutIsActiveEntity.IsActiveEntity;
+
+ const matchingContextTrue = contextsTrue.find((ctx) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => ctx.getObject()[key] === value));
+ const matchingContextFalse = contextsFalse.find((ctx) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => ctx.getObject()[key] === value));
+
+ let matchingContext = isActiveEntity ? matchingContextTrue : matchingContextFalse;
+
+ if(!matchingContext) {
+ matchingContext = isActiveEntity ? matchingContextFalse : matchingContextTrue;
+ }
+
+ return {
+ context: matchingContext,
+ path,
+ keyPredicates: keyPredicates,
+ keys: Object.keys(keys),
+ payload
+ };
+ });
+ }
+
+ private validateObjectsAndDraftStates(batch: any[], objects: any[], binding: any): boolean {
+ let errorFound = false;
+
+ batch.forEach((batchItem, index) => {
+ const keys = this.metadataHandler.getKeys(binding, batchItem);
+ const keysWithoutIsActiveEntity = { ...keys };
+ delete keysWithoutIsActiveEntity.IsActiveEntity;
+
+ const foundObject = objects.find((obj) => Object.entries(keysWithoutIsActiveEntity).every(([key, value]) => obj[key] === value));
+
+ if (!foundObject) {
+ errorFound = true;
+ this.messageHandler.addMessageToMessages({
+ title: this.util.geti18nText("spreadsheetimporter.objectNotFound"),
+ row: index + 1,
+ type: CustomMessageTypes.ObjectNotFound,
+ counter: 1,
+ formattedValue: Object.entries(keys)
+ .map(([key, value]) => `${key}=${value}`)
+ .join(", "),
+ ui5type: MessageType.Error
+ });
+ return;
+ }
+
+ // Check for valid draft states
+ if (foundObject.IsActiveEntity && !foundObject.HasDraftEntity && !batchItem.IsActiveEntity) {
+ this.addDraftMismatchError(index, keys, "Draft", "Active");
+ errorFound = true;
+ } else if (foundObject.IsActiveEntity && foundObject.HasDraftEntity && batchItem.IsActiveEntity) {
+ this.addDraftMismatchError(index, keys, "Active", "Draft");
+ errorFound = true;
+ } else if (!foundObject.IsActiveEntity && batchItem.IsActiveEntity) {
+ this.addDraftMismatchError(index, keys, "Active", "Draft");
+ errorFound = true;
+ }
+ });
+
+ return errorFound;
+ }
+
+ private addDraftMismatchError(index: number, keys: Record, uploadedState: string, expectedState: string): void {
+ this.messageHandler.addMessageToMessages({
+ title: this.util.geti18nText("spreadsheetimporter.draftEntityMismatch"),
+ row: index + 1,
+ type: CustomMessageTypes.DraftEntityMismatch,
+ counter: 1,
+ ui5type: MessageType.Error,
+ formattedValue: [
+ Object.entries(keys)
+ .map(([key, value]) => `${key}=${value}`)
+ .join(", "),
+ uploadedState,
+ expectedState
+ ]
+ });
+ }
+}
diff --git a/packages/ui5-cc-spreadsheetimporter/src/enums.ts b/packages/ui5-cc-spreadsheetimporter/src/enums.ts
index 4c626e5dc..02776fb02 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/enums.ts
+++ b/packages/ui5-cc-spreadsheetimporter/src/enums.ts
@@ -66,6 +66,20 @@ export const CustomMessageTypes: { [key: string]: CustomMessageType } = {
MaxLengthExceeded: {
title: "MaxLengthExceeded",
group: true
+ },
+ ObjectNotFound: {
+ title: "ObjectNotFound",
+ group: true,
+ update: true
+ },
+ DraftEntityMismatch: {
+ title: "DraftEntityMismatch",
+ group: true,
+ update: true
+ },
+ DuplicateKeys: {
+ title: "DuplicateKeys",
+ group: true
}
};
@@ -91,3 +105,24 @@ export enum MessageType {
*/
Warning = "Warning"
}
+
+export enum Action {
+ Create = "CREATE",
+ Update = "UPDATE",
+ Delete = "DELETE"
+}
+
+export const DefaultConfigs = {
+ DeepDownload: {
+ addKeysToExport: false,
+ setDraftStatus: true,
+ deepExport: false,
+ deepLevel: 0,
+ showOptions: true,
+ columns: []
+ },
+ Update: {
+ fullUpdate: false,
+ columns: []
+ }
+} as const;
diff --git a/packages/ui5-cc-spreadsheetimporter/src/fragment/BusyDialogProgress.fragment.xml b/packages/ui5-cc-spreadsheetimporter/src/fragment/BusyDialogProgress.fragment.xml
index 55198b005..6c342cd31 100644
--- a/packages/ui5-cc-spreadsheetimporter/src/fragment/BusyDialogProgress.fragment.xml
+++ b/packages/ui5-cc-spreadsheetimporter/src/fragment/BusyDialogProgress.fragment.xml
@@ -1,5 +1,5 @@
-