SAP CAP Fiori Frontends

 

Introduction

In this article we look at setting up a basic Fiori frontend for a SAP CAP application.

Original article here.

Project Structure

my-bookshop/
├── db/
│   ├── schema.cds
│   └── data/
│       └── my.bookshop-Books.csv
├── srv/
│   ├── cat-service.cds
│   ├── cat-service.js
│   └── annotations.cds
├── app/
│   └── books/               <- Fiori UI
│       ├── webapp/
│       │   ├── i18n/
│       │   │   └── i18n.properties
│       │   ├── Component.ts
│       │   ├── index.html
│       │   └── manifest.json
│       ├── package.json
│       ├── tsconfig.json
│       └── ui5.yaml
└── package.json

Setting up the Server

Run the following to set up a new SAP CAP project:

mkdir my-bookshop && cd my-bookshop
cds init .
cds add nodejs
mkdir -p db/data
mkdir -p app/books/webapp/i18n

Edit ./package.json to add an SQLite database:

{
  ...
  “cds”: {
    “requires”: {
      “db”: {
        “kind”: “sqlite”,
        “credentials”: {
          “database”: “db.sqlite”
        }
      }
    }
  }
}

Install packages the dependencies as follows:

npm install

Set up the database at ./db/schema.cds as follows:

namespace my.bookshop;
entity Books {
  key ID : UUID;
  title  : String(100);
  author : String(100);
  stock  : Integer;
  price  : Decimal(10,2);
}

Add some test data at ./db/data/my.bookshop-Books.csv:

ID,title,author,stock,price
00000000-0000-0000-0000-000000000000,The Great Gatsby,F. Scott Fitzgerald,50,9.99
00000000-0000-0000-0000-000000000001,To Kill a Mockingbird,Harper Lee,30,12.99
00000000-0000-0000-0000-000000000002,1984,George Orwell,75,8.99

Define the service at ./srv/cat-service.cds as follows:

using my.bookshop as db from ‘../db/schema’;
service CatalogService @(path: ‘/odata/v4/catalog’) {
  entity Books as projection on db.Books actions {
    action restock(quantity: Integer) returns Books;
  };
}

Implementation this server at ./srv/cat-service.js:

const cds = require(’@sap/cds’)
module.exports = class CatalogService extends cds.ApplicationService {
  async init() {
    const { Books } = this.entities
    this.on(’restock’, Books, async (req) => {
      const { quantity } = req.data
      const id = req.params[0]?.ID
      if (!quantity || quantity < 1) {
        return req.error(400, ‘Quantity must be at least 1’)
      }
      await UPDATE(Books).set({ stock: quantity }).where({ ID: id })
      return SELECT.one(Books).where({ ID: id })
    })
    await super.init()
  }
}

Add UI annotations at ./srv/annotations.cds:

using CatalogService from ‘./cat-service’;
annotate CatalogService.Books with @(
  UI.HeaderInfo: {
    TypeName      : ‘Book’,
    TypeNamePlural: ‘Books’,
    Title         : { Value: title }
  },
  UI.SelectionFields: [ title, author ],
  UI.LineItem: [
    { Value: title,  Label: ‘Title’  },
    { Value: author, Label: ‘Author’ },
    { Value: stock,  Label: ‘Stock’  },
    { Value: price,  Label: ‘Price’  }
  ],
  UI.FieldGroup#Main: {
    Label: ‘Book Details’,
    Data : [
      { Value: title  },
      { Value: author },
      { Value: stock  },
      { Value: price  }
    ]
  },
  UI.Facets: [{
    $Type : ‘UI.ReferenceFacet’,
    Label : ‘Details’,
    Target: ‘@UI.FieldGroup#Main’
  }],
  UI.Identification: [{
    $Type : ‘UI.DataFieldForAction’,
    Action: ‘CatalogService.restock’,
    Label : ‘Restock’
  }]
);

Next, deploy database by running the following:

cds deploy

Now you can run server using the following:

npm run watch

Finally test server by running the following:

curl ‘http://localhost:4004/odata/v4/catalog/$metadata

Make sure the UI annotations are visible in the output.

Setting up the Fiori Frontend

Edit ./app/books/package.json as follows:

{
  “name”: “books-ui”,
  “version”: 0.0.1,
  “private”: true,
  “devDependencies”: {
    “@ui5/cli”: “^4,
    “@sap/ux-ui5-tooling”: 1,
    “@sapui5/types”: “~1.130.0,
    “ui5-tooling-transpile”: “^3,
    “typescript”: “^5
  },
  “scripts”: {
    “start”: “fiori run --open index.html”,
    “build”: “ui5 build --all”,
    “tsc”: “tsc --noEmit”
  }
}

Set up the TypeScript configuration at ./app/books/tsconfig.json as follows:

{
  “compilerOptions”: {
    “target”: “es2022,
    “module”: “es2022,
    “skipLibCheck”: true,
    “allowJs”: true,
    “strict”: true,
    “strictPropertyInitialization”: false,
    “moduleResolution”: “node”,
    “rootDir”: “./webapp”,
    “outDir”: “./dist”,
    “baseUrl”: “.”,
    “paths”: {
      “my/bookshop/books/*”: [”./webapp/*”]
    },
    “typeRoots”: [
      “./node_modules/@types”,
      “./node_modules/@sapui5/types”
    ]
  },
  “include”: [”./webapp/**/*”]
}

Configure ui5 at ./app/books/ui5.yaml as follows:

specVersion: “4.0”
metadata:
  name: my.bookshop.books
type: application
server:
  customMiddleware:
    - name: fiori-tools-proxy
      afterMiddleware: compression
      configuration:
        ignoreCertErrors: false
        backend:
          - path: /odata/v4
            url: http://localhost:4004
        ui5:
          path:
            - /resources
            - /test-resources
          url: https://sapui5.hana.ondemand.com
    - name: fiori-tools-appreload
      afterMiddleware: compression
      configuration:
        port: 35729
        path: webapp
        delay: 300
    - name: ui5-tooling-transpile-middleware
      afterMiddleware: compression
      configuration:
        transformModulesToUI5:
          overridesToOverride: true
        excludePatterns:
          - /Component-preload.js
builder:
  customTasks:
    - name: ui5-tooling-transpile-task
      afterTask: replaceVersion
      configuration:
        transformModulesToUI5:
          overridesToOverride: true

Add a landing page at ./app/books/webapp/index.html:

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8”>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0”>
    <meta http-equiv=”X-UA-Compatible” content=”IE=edge”>
    <title>Bookshop</title>
    <style>
        html, body, body > div, #container, #container-uiarea { height: 100%; }
    </style>
    <script
        id=”sap-ui-bootstrap”
        src=”/resources/sap-ui-core.js”
        data-sap-ui-theme=”sap_horizon”
        data-sap-ui-resource-roots=’{”my.bookshop.books”: “./”}’
        data-sap-ui-on-init=”module:sap/ui/core/ComponentSupport”
        data-sap-ui-compat-version=”edge”
        data-sap-ui-async=”true”
        data-sap-ui-frame-options=”trusted”>
    </script>
</head>
<body class=”sapUiBody sapUiSizeCompact” id=”content”>
    <div
        data-sap-ui-component
        data-name=”my.bookshop.books”
        data-id=”container”
        data-settings=’{”id”: “my.bookshop.books”}’
        data-handle-validation=”true”>
    </div>
</body>
</html>

Add the main component ./app/books/webapp/Component.ts:

import AppComponent from “sap/fe/core/AppComponent”;
/**
 * @namespace my.bookshop.books
 */
export default class Component extends AppComponent {
  public static metadata = {
    manifest: “json”
  };
}

Add manifest at ./app/books/webapp/manifest.json:

{
  “_version”: 1.65.0,
  “sap.app”: {
    “id”: “my.bookshop.books”,
    “type”: “application”,
    “i18n”: “i18n/i18n.properties”,
    “applicationVersion”: { “version”: 0.0.1 },
    “title”: {{appTitle}},
    “description”: {{appDescription}}”,
    “dataSources”: {
      “mainService”: {
        “uri”: “/odata/v4/catalog/”,
        “type”: “OData”,
        “settings”: {
          “odataVersion”: 4.0
        }
      }
    }
  },
  “sap.ui5”: {
    “flexEnabled”: false,
    “dependencies”: {
      “minUI5Version”: 1.120.0,
      “libs”: {
        “sap.m”: {},
        “sap.ui.core”: {},
        “sap.fe.core”: {},
        “sap.fe.templates”: {}
      }
    },
    “models”: {
      “i18n”: {
        “type”: “sap.ui.model.resource.ResourceModel”,
        “settings”: {
          “bundleName”: “my.bookshop.books.i18n.i18n”,
          “supportedLocales”: [”“],
          “fallbackLocale”: “”
        }
      },
      “@i18n”: {
        “type”: “sap.ui.model.resource.ResourceModel”,
        “uri”: “i18n/i18n.properties”
      },
      “”: {
        “dataSource”: “mainService”,
        “preload”: true,
        “settings”: {
          “operationMode”: “Server”,
          “autoExpandSelect”: true,
          “earlyRequests”: true
        }
      }
    },
    “routing”: {
      “config”: {},
      “routes”: [
        {
          “pattern”: “:?query:”,
          “name”: “BookList”,
          “target”: “BookList”
        },
        {
          “pattern”: “Books({key}):?query:”,
          “name”: “BookObjectPage”,
          “target”: “BookObjectPage”
        }
      ],
      “targets”: {
        “BookList”: {
          “type”: “Component”,
          “id”: “BookList”,
          “name”: “sap.fe.templates.ListReport”,
          “options”: {
            “settings”: {
              “contextPath”: “/Books”,
              “variantManagement”: “None”,
              “initialLoad”: true,
              “navigation”: {
                “Books”: {
                  “detail”: {
                    “route”: “BookObjectPage”
                  }
                }
              }
            }
          }
        },
        “BookObjectPage”: {
          “type”: “Component”,
          “id”: “BookObjectPage”,
          “name”: “sap.fe.templates.ObjectPage”,
          “options”: {
            “settings”: {
              “contextPath”: “/Books”,
              “editableHeaderContent”: false
            }
          }
        }
      }
    }
  }
}

Add the translations at ./app/books/webapp/i18n/i18n.properties:

appTitle=Bookshop
appDescription=A simple SAP Fiori bookshop app

Next, install the dependencies:

cd app/books
npm install

Finally, run the Fiori application:

npm start

The Fiori frontend should open up in the browser automatically.

References

Comments

Popular Posts