7.3:dev:quickstart:frontend

How do I prepare the development environment for frontend dev? At least a link should be there.

Frontend module is single npm packace with standard package.json descriptor and CzechIdM module descriptor:

{
  "name": "czechidm-example",
  "version": "7.3.0",
  "description": "Example module for CzechIdM devstack. This module can be duplicated and rename for create new optional CzechIdM module.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "CzechIdM",
    "example",
    "IdM"
  ],
  "author": "BCV solutions s.r.o",
  "license": "MIT"
}
module.exports = {
  'id': 'example',
  'npmName': 'czechidm-example',
  'backendId': 'example',
  'name': 'Example module for CzechIdM devstack.',
  'description': 'Example module for CzechIdM devstack. This module can be duplicated and renamed for create new optional CzechIdM module.'
};

Module is built together with czechidm-app module using gulp. See frontend installation guide.

The simplest way to create new module is to copy example module skeleton.

Each module could expose new urls with contents. We need to register routes.js descriptor in module descriptor:

module.exports = {
  'id': 'example',
  ...
  'mainRouteFile': 'routes.js',
  ...
};

Then we can add routes descriptor routes.js into the same folder as module descriptor with new route definition:

module.exports = {
  module: 'example',
  childRoutes: [
    {
      path: '/example/content',
      component: require('./src/content/ExampleContent')
    }
  ]
};

After route is definied, then we can add new content to path <module>/src/content/ExampleContent.js as route says:

import React from 'react';
import Helmet from 'react-helmet';
//
import { Basic } from 'czechidm-core';
 
/**
 * Example content (page)
 */
export default class ExampleContent extends Basic.AbstractContent {
 
  constructor(props, context) {
    super(props, context);
  }
 
  /**
   * "Shorcut" for localization
   */
  getContentKey() {
    return 'example:content.example';
  }
 
  render() {
    return (
      <div>
        <Helmet title={this.i18n('title')} />
 
        <Basic.PageHeader>
          <Basic.Icon value="link"/>
          {' '}
          {this.i18n('header')}
        </Basic.PageHeader>
 
        <Basic.Panel>
          <Basic.PanelBody>
            { this.i18n('text') }
          </Basic.PanelBody>
        </Basic.Panel>
      </div>
    );
  }
}

This content will be available on url <server>/example/content. Basic and Advanced component usage is recommended, when contents are created. Each component has their own readme with description and notes (e.g. AbstractComponent).

Localization keys are used in contents and components. Localization location needs to be registered in module descriptor:

module.exports = {
  'id': 'example',
  ...
  'mainLocalePath': 'src/locales/',
  ...
};

We can create localization json file with name en.json:

{
  "module": {
    "name": "Example module",
    "author": "BCV solutions s.r.o."
  },
  "content": {
    "example": {
      "header": "Example content",
      "label": "Example content",
      "title": "Example content available from navigation, added by czechidm-example module.",
      "text": "New example content."
    }
  }
}

See getContentKey() method above in ExampleContent (page) and the json structure - it fits together.

Now is localized content available from url. We can add item with link to navigation - add navigation item into module descriptor:

module.exports = {
  'id': 'example',
  ...
  'navigation': {
    'items': [
      {
        'id': 'example-main-menu',
        'labelKey': 'example:content.examples.label',
        'titleKey': 'example:content.examples.title',
        'icon': 'gift',
        'iconColor': '#FF8A80',
        'order': 9
      }
    ]
  }
  ...
};

Service consumes rest endpoints. This is the only place when url of rest endpoint can be defined. Services simply make calls to rest api - isomorphic-fetch is used.

Base services

  • RestApiService - calls http methods to given endpoint and wraps authentication tokens (xsrf).
  • AbstractService - basic CRUD method, whid are commons for all endpoints
  • Other services define concrete rest endpoint and add custom endpoint methods

Service example

Service with example product CRUD operations.

import { Services } from 'czechidm-core';
import { Domain } from 'czechidm-core';
 
/**
 * Example products
 */
export default class ExampleProductService extends Services.AbstractService {
 
  getApiPath() {
    return '/example-products';
  }
 
  getNiceLabel(entity) {
    if (!entity) {
      return '';
    }
    return `${entity.name} (${entity.code})`;
  }
 
  /**
   * Agenda supports authorization policies
   */
  supportsAuthorization() {
    return true;
  }
 
  /**
   * Group permission - all base permissions (`READ`, `WRITE`, ...) will be evaluated under this group
   */
  getGroupPermission() {
    return 'EXAMPLEPRODUCT';
  }
 
  /**
   * Almost all dtos doesn§t support rest `patch` method
   */
  supportsPatch() {
    return false;
  }
 
  /**
   * Returns default searchParameters for current entity type
   *
   * @return {object} searchParameters
   */
  getDefaultSearchParameters() {
    return super.getDefaultSearchParameters().setName(Domain.SearchParameters.NAME_QUICK).clearSort().setSort('name');
  }
}

The business logic is implemented inside manager. Manager use underlying service and adds redux state usage - holds application state and provide actions (encapsulated to managers) to change application state.

Manager example

/**
 * Example product manager
 */
export default class ExampleProductManager extends Managers.EntityManager {
 
  constructor() {
    super();
    this.service = new ExampleProductService();
  }
 
  getModule() {
    return 'example';
  }
 
  getService() {
    return this.service;
  }
 
  /**
   * Controlled entity
   */
  getEntityType() {
    return 'ExampleProduct';
  }
 
  /**
   * Collection name in search / find response
   */
  getCollectionType() {
    return 'exampleProducts';
  }
}

We can register services and managers in index.js files placed in folders, when services and managers are defined - it simpler to import services from one file:

...
import { ExampleProductService } from '../services';
...

where relative path leeds to module service's index.js:

import ExampleProductService from './ExampleProductService';
 
const ServiceRoot = {
  ExampleProductService
};
 
ServiceRoot.version = '0.1.0';
module.exports = ServiceRoot;

Created manager can be used together with Basic.AbstractTableContent component to provide CRUD methods for persisted entity - see ExampleProductTable.

Authorities and permissions are primarily evaluated on backend, but if we need to hide some navigation items, routes and contents on frontend, then SecurityManager can be used for this purpose.

Secure route

SecurityManager is not called directly, but each route can have access property, which is controlled by SecurityManager#checkAccess method on the backgroud:

module.exports = {
  module: 'example',
  childRoutes: [
    ...
    {
      path: '/example/products',
      component: require('./src/content/example-product/ExampleProducts'),
      access: [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ]
    },
    ...
  ]
};

Url <server>/example/products will be available only to logged identity with authority EXAMPLEPRODUCT_READ in this example.

Secure navigation item

Navigation item is secured the same way as route by access parameter:

module.exports = {
  'id': 'example',
  ...
  'navigation': {
    'items': [
      {
        'id': 'example-main-menu',
        'labelKey': 'example:content.examples.label',
        'titleKey': 'example:content.examples.title',
        'icon': 'gift',
        'iconColor': '#FF8A80',
        'order': 9,
        'items': [
          {
            'id': 'example-products',
            'type': 'DYNAMIC',
            'section': 'main',
            'icon': 'gift',
            'labelKey': 'example:content.example-products.label',
            'titleKey': 'example:content.example-products.title',
            'order': 20,
            'path': '/example/products',
            'access': [ { 'type': 'HAS_ANY_AUTHORITY', 'authorities': ['EXAMPLEPRODUCT_READ'] } ]
          }
        ]
      },
      ...
    ]
  }
};

Secure components

SecurityManager can be used direcly for hide some component, e.g. button on some content:

...
<Basic.Button
  type="submit"
  level="success"
  showLoadingIcon
  showLoadingText={ this.i18n('button.saving') }
  rendered={ SecurityManager.hasAuthority('ROLECATALOGUE_UPDATE') }>
  { this.i18n('button.save') }
</Basic.Button>
...

Button will be shown only for logged identity with authority ROLECATALOGUE_UPDATE.

Secure form

When agenda supports authorization policies = permissions for data, then form can be secured using manager's canRead, canSave, canDelete methods. Permissions, what currently logged identity can do with selected record, has to be loaded at first. Permission are stored in redux store and are loaded together with entity by default (see EntityManager#fetchEntity method) - full example.

Backend returns error code response, when some error occurs. We can localize this error on frontend by adding error section into localization file:

...
  "error": {
    "EXAMPLE_SERVER_ERROR": {
      "title": "Example server error",
      "message": "Example server with parameter [{{parameter}}]."
    },
    "EXAMPLE_CLIENT_ERROR": {
      "title": "Example client error",
      "message": "Example client error, bad value given [{{parameter}}]."
    }
  },
...

Component descriptor can be used for register component, which can be added into existing contents or which can override some existing component (e.g. when different behavior is needed). We need to register component.js descriptor in module descriptor:

module.exports = {
  'id': 'example',
  ...
  'mainComponentDescriptorFile': 'component-descriptor.js',
  ...

Example - dashboard component

Then we can add component-descriptor.js into the same folder as module descriptor with new component definition:

module.exports = {
  'id': 'example',
  'name': 'Example',
  'description': 'Components for Example module',
  'components': [
    {
      'id': 'exampleDashboard',
      'type': 'dashboard',
      'span': '4',
      'order': '4',
      'component': require('./src/content/dashboards/ExampleDashboard')
    }
  ]
};

And then we can add new content to path <module>/src/content/dashboards/ExampleDashboard.js as component definition says:

/**
 * Example dashbord panel
 */
export default class ExampleDashboard extends Basic.AbstractContent {
 
  /**
   * "Shorcut" for localization
   */
  getContentKey() {
    return 'example:content.dashboard.exampleDashboard';
  }
 
  render() {
    return (
      <Basic.Panel>
        <Basic.PanelHeader text={ this.i18n('header') }/>
        <Basic.PanelBody>
          <Basic.Panel className="panel-warning">
            <Basic.PanelHeader>
              <Basic.Row>
                <Basic.Col lg={ 3 }>
                  <Basic.Icon type="fa" icon="dashboard" className="fa-5x"/>
                </Basic.Col>
                <Basic.Col lg={ 9 }>
                  <div><strong>{ this.i18n('title') }</strong></div>
                  <div>{ this.i18n('text') }</div>
                </Basic.Col>
              </Basic.Row>
            </Basic.PanelHeader>
          </Basic.Panel>
        </Basic.PanelBody>
      </Basic.Panel>
    );
  }
}

Custom theme can be created in module. Theme contains less files and images. Core less variables can be overriden.

We start with copy core default theme into new module's theme folder and rename theme folder from default to example. Then we select new theme in frontend configuration under our state and profile (e.g. development.json):

{
  "env": "development",
  ...
  "theme": "czechidm-example/themes/example",
  ...
}

Then we can change some colors and logo in module's theme <module>/themes/example/css/main.less:

@brand-success: #A13432;
@idm-base-color: #A13432;
@idm-link-color: #A13432;
 
header {
 
  .home {
    background-image: url("../images/logo.png");
  }
}

and replace logo image on path <module>/themes/example/images/logo.png.

New theme will be used after application is built (gulp build) or started (gulp) under our stage and profile.