Project created using SAP CAP technology, creating a local database for the back-end and an extended Fiori Elements to create a bulk CSV upload feature.
The project contains these folders and files :
File or Folder | Purpose |
---|---|
db/ |
your domain models and data go here |
srv/ |
your service models and code go here |
app/ |
content for UI frontends goes here |
package.json |
project metadata and configuration |
Tools you need to be able to develop this application
- SAP BAS
In this application we are going to develop both the back-end and the front-end part
This folder contains the scripts for creating the application's database. This is where data models are defined and relationships between tables are specified.
namespace my.bookshop;
entity Books {
key ID : UUID @Common.Label : 'ID' @ObjectModel.generator.UUID;
title : String @Common.Label : 'Title';
stock : Integer @Common.Label : 'Stock';
}
This code defines an entity called "Books" in the namespace "my.bookshop". The entity has three properties: "ID" of type UUID, which is also the primary key for the entity, "title" of type String, and "stock" of type Integer.
The properties are annotated with the @Common.Label annotation, which provides a label for the properties that can be used in user interfaces. The ID property is also annotated with the @ObjectModel.generator.UUID annotation, which indicates that the UUID values for the primary key will be generated by the Object Model Generator.
This is the folder that contains the backend service of the application. This is where the application's business logic, data models, and APIs are defined.
using my.bookshop as my from '../db/data-model';
service CatalogService {
entity Books as projection on my.Books;
}
This code defines a service called "CatalogService". It imports the "my.bookshop" namespace using the "my" alias and imports the data model from the "../db/data-model" file.
The service then defines an entity called "Books" using a projection on the "my.Books" entity. This means that the "Books" entity inherits all the properties and annotations of the "my.Books" entity.
The service can then expose APIs that allow clients to perform CRUD (Create, Read, Update, Delete) operations on the "Books" entity.
This folder contains the front-end web application. This is where the user interface of the application is defined.
This application is a Fiori Element, specifically a List Report. To perform the functionality of bulk record upload, we have followed the following steps.
- Upload-CSV_SAP-CAP/app/book_stock_control/annotations.cds
using CatalogService as service from '../../srv/cat-service';
@odata.draft.enabled
annotate service.Books with @(
UI.SelectionFields : [
title,
stock
],
UI.LineItem : [
{
$Type : 'UI.DataField',
Value : title,
},
{
$Type : 'UI.DataField',
Value : stock,
CriticalityRepresentation : #WithoutIcon,
Criticality : {$edmJson : {$If : [
{$Le : [
{$Path : 'stock'},
100
]},
1,
{$If : [
{$Ge : [
{$Path : 'stock'},
500
]},
3,
2
]}
]}}
}
]
);
annotate service.Books with @(
UI.FieldGroup #GeneratedGroup1 : {
$Type : 'UI.FieldGroupType',
Data : [
{
$Type : 'UI.DataField',
Value : title,
},
{
$Type : 'UI.DataField',
Value : stock,
},
],
},
UI.Facets : [{
$Type : 'UI.ReferenceFacet',
ID : 'GeneratedFacet1',
Label : 'General Information',
Target : '@UI.FieldGroup#GeneratedGroup1',
}, ]
) {
ID @(UI : {Hidden : true, });
};
This code imports a service called "CatalogService" from the '../../srv/cat-service' file. It also enables OData draft mode using the "@odata.draft.enabled" annotation.
The code then uses two annotations to modify the "Books" entity of the "CatalogService". The first annotation defines the selection fields and line items for the "Books" entity. It specifies that the "title" and "stock" fields should be used for selection and creates a line item with the title and stock fields.
The criticality of the stock field is also defined using an expression that sets a value of 1 if the stock is less than or equal to 100, 3 if the stock is greater than or equal to 500, and 2 otherwise.
The second annotation defines a field group and a reference facet for the "Books" entity. The field group contains the title and stock fields, and the reference facet is linked to the field group. The "ID" property is also defined using the "@UI" annotation to hide the ID field.
Overall, the annotations are used to modify the default behavior and appearance of the "Books" entity in the "CatalogService".
- Upload-CSV_SAP-CAP/app/book_stock_control/webapp/ext/view/PersonalizationDialog.fragment.xml
Within the webapp folder, we create the ext (extension) folder where we will create the necessary folders and files to extend the standard of a Fiori Element. Inside the ext folder, we create the view folder and the PersonalizationDialog.fragment.xml file.
This will be the popup from which we can download a CSV template to fill in with new records and then upload it to our database and display them in our table.
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core" xmlns:u="sap.ui.unified" xmlns:smartTable="sap.ui.comp.smarttable">
<Dialog title="{i18n>titleDialogCSV}" titleAlignment="Center">
<endButton>
<Button text="{i18n>btnClose}" type="Negative" press=".onCloseDialog" />
</endButton>
<beginButton>
<Button type="Success" icon="sap-icon://upload" text="{i18n>btnUpload}" press=".onUploadData" />
</beginButton>
<VBox justifyContent="Center" alignContent="Center" alignItems="Center">
<HBox alignContent="Center" alignItems="Center" class="sapUiSmallMarginTop">
<u:FileUploader icon="sap-icon://browse-folder" change="handleFiles" buttonText="{i18n>btnBrowse}" fileType="CSV" placeholder="{i18n>msgNonFileSelect}" />
</HBox>
<HBox alignContent="Center" alignItems="Center" class="sapUiSmallMarginTop sapUiSmallMarginBottom">
<Button type="Neutral" icon="sap-icon://download" text="{i18n>btnDowloadTmpl}" press=".onDownloadTemplate" />
</HBox>
<HBox>
<MessageStrip id="messageStripId" visible="false" text="{i18n>msgStrip}" type="Success" showIcon="true" class="sapUiSmallMarginBottom" />
</HBox>
</VBox>
</Dialog>
</core:FragmentDefinition>
This is an XML code defining a dialog fragment in a SAPUI5 application. The fragment contains a dialog box with a title, two buttons (one for closing the dialog and one for uploading data), a file uploader control, and a message strip control. The dialog also includes several layout containers such as VBox and HBox, which are used to organize the controls in a vertical and horizontal layout, respectively. The control properties are set using data binding to internationalization resources specified in the application's i18n file. The code also includes event handlers for the button presses and file upload/change events.
- Upload-CSV_SAP-CAP/app/book_stock_control/webapp/ext/controller/ListReportExt.controller.js
Once we have the controller created, we reference it in the manifest.
"extends": {
"extensions": {
"sap.ui.controllerExtensions": {
"sap.fe.templates.ListReport.ListReportController": {
"controllerName": "fiorielements.bookstockcontrol.ext.controller.ListReportExt"
}
}
}
}
We create the custom action, which will be the button that opens the popup to download or upload the CSV to our table.
"actions": {
"customAction": {
"id": "idUpladCSV",
"text": "Upload CSV",
"press": ".extension.fiorielements.bookstockcontrol.ext.controller.ListReportExt.onUploadCSV",
"requiresSelection": false
}
}
Finally, we create the logic in our controller to provide the functionality to open the popup with the custom action, to be able to upload and download the CSV.
sap.ui.define(
[
"sap/ui/core/mvc/ControllerExtension",
"sap/ui/core/Fragment",
"sap/m/MessageToast",
"sap/ui/model/json/JSONModel",
],
function (
ControllerExtension,
Fragment,
MessageToast,
JSONModel
) {
"use strict";
return ControllerExtension.extend(
"fiorielements.bookstockcontrol.ext.controller.ListReportExt",
{
onUploadCSV: function () {
// If the fragment is not loaded it generates it
Fragment.load({
name: "fiorielements.bookstockcontrol.ext.view.PersonalizationDialog", // Path of the fragment
controller: this, // Select this controller for the Dialog
}).then(
function (oDialog) {
this.pDialog = oDialog;
this.getView().addDependent(this.pDialog);
this.pDialog.open();
}.bind(this)
);
},
// Function to close the Dialog
onCloseDialog: function () {
// Close dialog en clear and reset the compent inside it
this.pDialog.close();
this.pDialog.destroy(true);
this.pDialog = null;
this.oFileData = undefined;
},
// Function to read CSV file and prepare data for upload once done show a success msg
handleFiles: function (oEvent) {
var oModelContentCsv = new JSONModel();
var oFileToRead = oEvent.getParameters().files["0"];
var reader = new FileReader();
// Save the register of the CSV inside an object
var loadHandler = function (oEvent) {
var csv = oEvent.target.result,
allTextLines = csv.split(/\r\n|\n/),
lines = [];
for (var i = 0; i < allTextLines.length; i++) {
var data = allTextLines[i].split(";"),
tarr = [];
for (var j = 0; j < data.length; j++) {
tarr.push(data[j]);
}
lines.push(tarr);
}
lines.splice(-1);
oModelContentCsv.setData(lines);
this.oFileData = oModelContentCsv.oData;
sap.ui.getCore().byId("messageStripId").setVisible(true);
}.bind(this);
// Error during the reading csv
var errorHandler = function (evt) {
if (evt.target.error.name == "NotReadableError") {
// Show message error read
var msgErrorRead = this.getView()
.getModel("i18n")
.getResourceBundle()
.getText("msgErrorRead");
MessageToast.show(msgErrorRead);
}
}.bind(this);
// Read file into memory as UTF-8
reader.readAsText(oFileToRead);
// Handle errors load
reader.onload = loadHandler;
reader.onerror = errorHandler;
},
// This function uploads a CSV file with book data to a table in a Fiori Elements application.
onUploadData: function () {
// If the object with the load records is not undefined, the object is read and the entries are created
if (this.oFileData != undefined) {
// Get binding of the inner table
var oBinding = this.getView()
.byId("fiorielements.bookstockcontrol::BooksList--fe::table::Books::LineItem-innerTable")
.getBinding();
var aEntries = [];
// Format and sort the CSV data for each entry
for (var i in this.oFileData) {
var oEntry = {};
for (var z in this.oFileData[i]) {
oEntry[this.oFileData[0][z]] = this.oFileData[i][z];
}
oEntry.IsActiveEntity = true;
aEntries.push(oEntry);
}
// Remove header row
aEntries.shift();
// Convert stock value to integer
for (let i = 0; i < aEntries.length; i++) {
aEntries[i].stock = parseInt(aEntries[i].stock);
}
// Create every entry
for (var x in aEntries) {
var oContext = oBinding.create(aEntries[x]);
}
// Activate each draft entry
oContext.created()
.then(function () {
var oBinding = this.getView()
.byId("fiorielements.bookstockcontrol::BooksList--fe::table::Books::LineItem-innerTable")
.getBinding();
this.iCompleted = 0;
this.iItems = 0;
// Loop through the table's contexts to find draft entries and activate them
oBinding.aContexts.forEach(function (oRecord) {
if (oRecord.sPath.indexOf("IsActiveEntity=false") !== -1) {
var draftActivateUrl = this.getView().getModel().sServiceUrl +
oRecord.sPath +
"/CatalogService.draftActivate?$select=HasActiveEntity,HasDraftEntity,ID,IsActiveEntity,stock,title&$expand=DraftAdministrativeData($select=DraftIsCreatedByMe,DraftUUID,InProcessByUser)";
// Make an AJAX request to activate the draft entry
$.ajax({
headers: {
Accept: "application/json;odata.metadata=minimal;IEEE754Compatible=true",
"Accept-Language": "en-US",
Prefer: "handling=strict",
"Content-Type": "application/json;charset=UTF-8;IEEE754Compatible=true",
},
url: draftActivateUrl,
type: "POST",
success: function () {
this.iCompleted++;
}.bind(this),
error: function (error) {
console.log(`Error ${error}`);
}.bind(this),
});
this.iItems++;
}
}.bind(this));
this.checkCompleted();
}.bind(this))
.catch(function () {
// Handle rejection of entity creation
var msgLoadError = this.getView()
.getModel("i18n")
.getResourceBundle()
.getText("msgLoadError");
// Show message Load with error
MessageToast.show(msgLoadError);
this.onCloseDialog();
}.bind(this));
} else {
// If the object is undefined show msg
var msgNonFileSelect = this.getView()
.getModel("i18n")
.getResourceBundle()
.getText("msgNonFileSelect");
// Show message non file selected
MessageToast.show(msgNonFileSelect);
}
},
checkCompleted: function () {
if (this.iCompleted === this.iItems) {
this.iCompleted = 0;
// Entry successfully created and Show message Load success
var msgLoadSuccess = this.getView()
.getModel("i18n")
.getResourceBundle()
.getText("msgLoadSuccess");
MessageToast.show(msgLoadSuccess);
this.onCloseDialog();
setTimeout(
function () {
var goBtn = this.getView().byId(
"fiorielements.bookstockcontrol::BooksList--fe::FilterBar::Books-btnSearch"
);
goBtn.firePress(true);
}.bind(this),
500
);
} else {
setTimeout(
function () {
this.checkCompleted();
}.bind(this),
500
);
}
},
// Function to download template
onDownloadTemplate: function () {
// define the heading for each row of the data
var csv = "title;stock";
var hiddenElement = document.createElement("a");
hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(csv);
// provide the name for the CSV file to be downloaded
hiddenElement.download = "TemplateCsv.csv";
hiddenElement.click();
},
}
);
}
);
UploadCSV.mp4
- CAP CDS
- Javascript / UI5
- Fiori Elements
Back-end:
- CAP CDS
Gateway:
- oData
Front-End:
- Javascript / UI5
- Fiori Elements
- Open a new terminal and run
cds watch
- (in VS Code simply choose Terminal > Run Task > cds watch)
- Start adding content, for example, a db/schema.cds.
Learn more at https://cap.cloud.sap/docs/get-started/.
⌨️ with ❤️ love GonzaloMB 😊