SAP Fiori, SAPUI5

My journey towards using UI5 UploadSet with CAP backend

Introduction

CAP is capable of storing media data. But does it work with UI5 upload controls such as UploadSet(*)?

The answer to above question is yes. However, UploadSet doesn’t work out-of-the-box with CAP so you need to write some code to make it adapt to CAP.

The following are the challenges when you try to integrate UploadSet with CAP.

  1. CAP expects to receive media data with PUT request, while UploadSet sends media data by POST request by default.
  2. In order to open a file (picture) in UploadSet, it has to be fetched in blob, which doesn’t happen naturally with UploadSet.

The good news is, you don’t have to develop a custom control to upload / download files to / from CAP.

UploadSet has several “hooks” for your own logic and with the help of those hooks, you can integrate UploadSet with CAP.

In this blog I’m going to develop a CAP service (Odata v4) and a simple UI5 app with UpaloadSet.

* UploadSet is a successor of UploadCollection, as UploadCollection was deprecated as of UI5 version 1.88.

Development

1. CAP project
2. UI5 app
i. Uploading files
ii. Downloading files
iii. Set meaningful names to downloaded files (optional)
iv. Show file icons (optional)

* The optional steps are not directly related to UploadSet + CAP integration, but rather to improve the appearance of the app.

1. CAP project

1.1. db/data-model.cds

Important annotations are below.

@Core.MediaType: Indicates that the element contains media data. The value of this annotation is either a string with the contained MIME type or is a path to the element that contains the MIME type.
@Core.IsMediaType: Indicates that the element contains a MIME type

namespace miyasuta.media;

using {
    cuid,
    managed
} from '@sap/cds/common';

entity Files: cuid, managed{
    @Core.MediaType: mediaType
    content: LargeBinary;
    @Core.IsMediaType: true
    mediaType: String;
    fileName: String;
    size: Integer;
    url: String;
}

1.2. srv/media-service.cds

There’s nothing special here. Just exposing Files entity to the service.

using { miyasuta.media as db } from '../db/data-model';

service Attachments {
    entity Files as projection on db.Files
}

1.3. srv/media-service.js

I’ve implemented create handler for filling download URL (just for convenience of the UI).

module.exports = async function () {
    this.before('CREATE', 'Files', req => {
        console.log('Create called')
        console.log(JSON.stringify(req.data))
        req.data.url = `/attachments/Files(${req.data.ID})/content`
    })
}

2. UI5 app

2.1. Uploading files

First, let’s focus on how to upload files.

manifest.json

The CAP OData service is added as datasource with path “/attachments”.

{
    "_version": "1.32.0",
    "sap.app": {
        ...
        "dataSources": {
            "mainService": {
                "uri": "/attachments/",
                "type": "OData",
                "settings": {
                    "odataVersion": "4.0",
                    "localUri": "localService/metadata.xml"
                }
            }
        },
    },
    ...
    "sap.ui5": {
       ...
        "models": {
            "i18n": {
                "type": "sap.ui.model.resource.ResourceModel",
                "settings": {
                    "bundleName": "miyasuta.attachments.i18n.i18n"
                }
            },
            "": {
                "type": "sap.ui.model.odata.v4.ODataModel",
                "settings": {
                  "synchronizationMode": "None",
                  "operationMode": "Server",
                  "autoExpandSelect": true,
                  "earlyRequests": true,
                  "groupProperties": {
                    "default": {
                      "submit": "Auto"
                    }
                  }
                },
                "dataSource": "mainService"
              }
        },

View

Let’s first examine UploadSet’s default upload behavior. To do that, I’ve created the following view, without event handlers for UploadSet.

<mvc:View
	controllerName="miyasuta.attachments.controller.App"
	xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true"
	xmlns="sap.m"
	xmlns:upload="sap.m.upload"
>
	<App id="app">
		<pages>
			<Page
				id="page"
				title="{i18n>title}"
			>
				<upload:UploadSet
					id="uploadSet"
					instantUpload="true"
					uploadEnabled="true"
					uploadUrl="/attachments/Files"				
					items="{
								path: '/Files',
								parameters: {
									$orderby: 'createdAt desc'
								},
								templateShareable: false}"
				>
					<upload:toolbar>
					</upload:toolbar>
					<upload:items>
						<upload:UploadSetItem
							fileName="{fileName}"
							mediaType="{mediaType}"
							url="{url}"
							enabledEdit="false"
							visibleEdit="false"
							openPressed="onOpenPressed"
						>
							<upload:attributes>
								<ObjectAttribute
									title="Uploaded By"
									text="{createdBy}"
									active="false"
								/>
								<ObjectAttribute
									title="Uploaded on"
									text="{createdAt}"
									active="false"
								/>
								<ObjectAttribute
									title="File Size"
									text="{size}"
									active="false"
								/>
							</upload:attributes>
						</upload:UploadSetItem>
					</upload:items>
				</upload:UploadSet>
			</Page>			
		</pages>
	</App>
</mvc:View>

The app looks like this.

when you upload a file, you’ll see an error below in the backend console. Here you find that data has been sent by POST request.

[cds] - POST /attachments/Files
[cds] - DeserializationError: No payload deserializer available for resource kind 'ENTITY' and mime type 'image/png'

To fix this, make the following changes to the UploadSet properties.

  • Set instantupload to “false” to prevent the default upload behavior
  • Remove uploadUrl, because we need to set this dynamically after receiving the entity’s key.
  • Add event handler for afterItemAdded event. We’ll be uploading a file here.
<upload:UploadSet
					id="uploadSet"
					instantUpload="false"
					uploadEnabled="true"
					afterItemAdded="onAfterItemAdded"
					uploadCompleted="onUploadCompleted"					
					items="{
								path: '/Files',
								parameters: {
									$orderby: 'createdAt desc'
								},
								templateShareable: false}"
				>

Controller

The following is the initial state of the controller.

In the method onAfterItemAdded, we first create a new entity of File (method: _createEntity).

After receiving the entity’s key, we then construct an URL and upload a file by PUT request (method: _uploadContent).

sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/m/MessageToast"
],
	function (
		Controller,
		MessageToast
	) {
		"use strict";

		return Controller.extend("miyasuta.attachments.controller.App", {
			onInit: function () {
				
			},

			onAfterItemAdded: function (oEvent) {
				var item = oEvent.getParameter("item")
				this._createEntity(item)
				.then((id) => {
					this._uploadContent(item, id);
				})
				.catch((err) => {
					console.log(err);
				})
			},

			onUploadCompleted: function (oEvent) {
				var oUploadSet = this.byId("uploadSet");
				oUploadSet.removeAllIncompleteItems();
				oUploadSet.getBinding("items").refresh();
			},

			onOpenPressed: function (oEvent) {	
				// to be implemented			
			},

			_createEntity: function (item) {
					var data = {
						mediaType: item.getMediaType(),
						fileName: item.getFileName(),
						size: item.getFileObject().size
					};
	
					var settings = {
						url: "/attachments/Files",
						method: "POST",
						headers: {
							"Content-type": "application/json"
						},
						data: JSON.stringify(data)
					}
	
				return new Promise((resolve, reject) => {
					$.ajax(settings)
						.done((results, textStatus, request) => {
							resolve(results.ID);
						})
						.fail((err) => {
							reject(err);
						})
				})				
			},

			_uploadContent: function (item, id) {
				var url = `/attachments/Files(${id})/content`
				item.setUploadUrl(url);	
				var oUploadSet = this.byId("uploadSet");
				oUploadSet.setHttpRequestMethod("PUT")
				oUploadSet.uploadItem(item);
			}			
		});
	});

Now, if you upload an image file, you’ll see it successfully uploaded.

The screenshot below is the result of GET request from Postman. So far, so good!

URL: http://localhost:4004/attachments/Files(<uuid>)/content

But what if you click the link on the item?

A new browser tab opens with black screen with a small white square in the middle. This is NOT what I’ve uploaded (or, it is supposed to look)!

2.2. Downloading files

To fix above issue, we’ll implement onOpenPressed method and overwrite the default behavior. To open a picture properly, we need to specify response type as blob (see method: _download ).

onOpenPressed: function (oEvent) {
				oEvent.preventDefault();
				var item = oEvent.getSource();
				this._download(item)
					.then((blob) => {
						var url = window.URL.createObjectURL(blob);
						//open in the browser
						window.open(url);					
					})
					.catch((err)=> {
						console.log(err);
					});					
			},


			_download: function (item) {
				var settings = {
					url: item.getUrl(),
					method: "GET",
					xhrFields:{
						responseType: "blob"
					}
				}	

				return new Promise((resolve, reject) => {
					$.ajax(settings)
					.done((result, textStatus, request) => {
						resolve(result);
					})
					.fail((err) => {
						reject(err);
					})
				});						
			},

As a result, the image is shown in the browser correctly.

2.3. Set meaningful names to downloaded files (optional)

While images, pdf files and text files are opened in a new browser tab, other types of files such as Word or Excel are downloaded to PC (I haven’t tested all file types).

Downloaded files get random guids as file name. Can we make the file names more meaningful ones?

To achieve this, I’ve changed the onOpenPressed method to download files, instead of using window.open() method. The following is the revised code.

onOpenPressed: function (oEvent) {
				oEvent.preventDefault();
				var item = oEvent.getSource();
				this._fileName = item.getFileName();
				this._download(item)
					.then((blob) => {
						var url = window.URL.createObjectURL(blob);
						// //open in the browser
						// window.open(url);

						//download
						var link = document.createElement('a');
						link.href = url;
						link.setAttribute('download', this._fileName);
						document.body.appendChild(link);
						link.click();
						document.body.removeChild(link);						
					})
					.catch((err)=> {
						console.log(err);
					});					
			},

As a result, downloaded files get their original names.

2.4 Show file icons (optional)

You might remember, that when you used UploadCollection, you would see file icons to the left of file name as shown below.

Although UploadSet seems to reserve space for icons, icons are not displayed.

My workaround is to use thumbnailUrl property with formatter. In the formatter, I’ve set an icon URL according to mimeType. But I feel icons should be shown without writhing such code.

If someone knows a better way, please let me know in the comment section below.

<upload:UploadSetItem
							fileName="{fileName}"
							mediaType="{mediaType}"
							url="{url}"
							thumbnailUrl="{
								path: 'mediaType',
								formatter: '.formatThumbnailUrl'
							}"							
							...
						>

This is formatter code.

formatThumbnailUrl: function (mediaType) {
				var iconUrl;
				switch (mediaType) {
					case "image/png":
						iconUrl = "sap-icon://card";
						break;
					case "text/plain":
						iconUrl = "sap-icon://document-text";
						break;
					case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
						iconUrl = "sap-icon://excel-attachment";
						break;
					case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
						iconUrl = "sap-icon://doc-attachment";
						break;
					case "application/pdf":
						iconUrl = "sap-icon://pdf-attachment";
						break;
					default:
						iconUrl = "sap-icon://attachment";
				}
				return iconUrl;
			}

Finally, icons are displayed.

Leave a Reply

Your email address will not be published. Required fields are marked *