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 @@ + + + + + +