UI/UX implementation guides

Subsections of UI/UX implementation guides

Making templates

Subsections of Making templates

Pages

GOERP uses golang templates while assembling the pages, so any options that are supported by go templates may be applied here. Please refer to the official docs to get more information. Pages creation process is like writing html code, including some features from go templating system.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>

</body>
</html>

Alternatively, GOERP have in-build layout system, so one layout can be re-used on many pages. All default layouts located in the partials section of the editor. Layouts have suffix -layout.

Here is example for the complex page, using layouts:

{{ template "dev-v2-layout" . }}

{{ define "title-block" }} Title of the tab {{ end }}

{{ define "content-block" }}
All content goes here, basically <main></main> should be in this section
{{ end }}

{{ define "js-block" }}
All js code goes here, <script></script>
{{ end }}

Content partials

Note

This section describes content partials, which have go-html content only. For java-script and css partials check ā€œJS and CSS partialsā€ section.

Partial is a part of document that can be re-used in several pages, which may be convenient if application consists of several pages with same content in some places. Letā€™s say we have application where navigation content repeated in every page, with partial we can put this content in one template and re-use it in every page. Sounds very convenient, but still they have some restrictions:

  • Partials can also contain unlimited partials, but the maximum depth (nested levels) is currently limited to 5
  • Partials cannot have js and css imports/blocks

Create partial

To create a partial, go to the template editor and pick Create -> Create new template, then define name and select type Partial from the dropdown. Editor will generate very simple initial code for the partial and append suffix -partial to the template name:

{{ define "my-cool-partial" }}
<!-- Feel free to write your awesome component using HTML and powerful templating options -->
{{ end }}

So lets update newly created partial with some content

{{ define "my-cool-partial" }}
    <h1>Hello Goerp!</h1>
{{ end }}

and now let’s inject partial into the page:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  {{ template "my-cool-partial" . }}
</body>
</html>

Passing data to the partial

In the last section we created simple partial and injected into the page. There is dot in the end of partial injection statement, which means that we are passing all data that was sent from back-end with the response to the partial. In this case we can use any available variable field, for example changing our <h1>Hello Goerp!</h1> to the <h1>Hello Goerp! Client code {{ .Data.Session.ClientCode }}</h1> will print the client code number.

However, in some cases we may want to pass specific set of variables instead of all available ones. In this case we can use in-build function that will produce variable of the key-value pairs (check Built-in helper functions topic mkMap func for more details). So we need to update our partial to use the variable: <h1>Hello Goerp! Client code {{ .clientCode }}</h1> and then pass this variable while injecting the template in our page: {{ template "my-cool-partial" mkMap "clientCode" 123456 }}. Or we can pass any part of available in page data, this will also work: {{ template "my-cool-partial" .Data.Session }} and then <h1>Hello Goerp! Client code {{ .ClientCode }}; session key {{ .SessionKey }}</h1>

JS and CSS partials

Warning

According to the new CSP (Content Security Policy) requirements, all inline css and js content will be blocked by browser.

Samples that would be blocked:

  • <p style="padding: 5px;">blocked</p>
  • <script>console.log("blocked")</script>
  • <header><stile>html {padding:5px;}</style></header>
  • <button onclick="funccall()">blocked</button>

Valid samples:

  • <header><link rel="stylesheet" href="{{ "partial-css" | staticFileLink }}"></header>
  • <header><link rel="stylesheet" href="https://link.to.my.css"></header>
  • <body><script src="{{ staticFileLink "partial-js" }}"></script></body>

To create a css/js partial, go to the template editor and pick Create -> Create new template, then define name and select type JS or CSS respectively. Editor will create empty file and append suffix -css or -js respectively to the template name. Now we can write any valid css/js code there, just like we would do in regular .js or .css files.

Now, when static partial is ready, we can link it with the page like this:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  
  {{/* Link css partial, as an option, calling linking function in a pipe */}}
  <link rel="stylesheet" href="{{ "partial-css" | staticFileLink }}">
  <title>Document</title>
</head>
<body>
  {{/* Page content */}}

  {{/* Link js partial, as an option, using regular linking function call */}}
  <script src="{{ staticFileLink "partial-js" }}"></script>
</body>
</html>

Java-script and css partials are static files, and they are not part of the goerp template. To link our templates with the static partials (js, css), we can use helper function staticFileLink like this: <link rel="stylesheet" href="{{ "partial-css" | staticFileLink }}">. Or make linking as usual css and js imports: <link rel="stylesheet" href="https://link.to.css.file">

Layouts

GOERP have in-build layout system, so one layout can be re-used on many pages. This feature would be very useful if application have many templates (pages) because with layouts we can encapsulate all general html into one component (e.g. css and js dependencies, general html like header, footer, menu, etc…).

Note

Layouts usage may look similar to the regular partial logic, however, they behave in absolutely different way. The main difference between layout and regular partial is that the former one contains code placeholders that are replaced with actual payload on the related page template, and the latter one doesn’t have any placeholders and contains only code that is related to this specific partial.

In other words:

  • in case of layouts, page template exports go-html content to the layout through block keywords by define‘ing those blocks;
  • in case of partial, page template imports go-html content from the partial through the template notation;
  • both, layout and partial, should be defined inside page by using template keyword, the only difference is that layouts must be defined at very beginning of the page template.
  • both, layout and partial cannot include other partials as dependencies

Under pages topic we already covered briefly layouts feature. Let’s dive into more details now.

Very simple example (layout)

{{ define "simple-layout" }}
<!-- Let's say we want to encapsulate page content that is the same for all pages in our application -->
<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Default Erply styles -->
  <link rel="stylesheet" href="https://assets.erply.com/bo-prototype/_style.css">
  <!-- Maybe some custom styles -->
  <link rel="stylesheet" href="https://assets.my-company.net/style.css">
  <!-- Regular fonts from google -->
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

  <!-- With this block we are creating placeholder which may be filled from the page template -->
  <!-- Here we may inject page specific css links or static file dependencies -->
  <!-- Please note, all block definition names must have -block suffix at the end -->
  {{ block "css-block" . }} {{ end }}

  <!-- Add page specific title -->
  <title>
    {{ block "title-block" . }} Default title {{ end }}
  </title>
</head>
<body>

<menu class="menu-container">
  <!-- This menu will be included into all dependent pages -->
</menu>

{{ block "content-block" . }}
<main>
  <p>
    Another cool feature of the placeholder blocks is that they may contain default content,
    some kind of fall back in case if dependent page decides to leave this block undefined. So,
    if page that is using this layout doesn't have <code>{{ define "content-block" }} {{ end
    }}</code>
    section then <strong>this message shown by default</strong>.
  </p>
</main>
{{ end }}

<!-- Another sample how to pass some data from templates to the js using inputs -->
<input type="hidden" id="AUT_SESSION" value="{{ toJson .Session }}">
<input type="hidden" id="REQUEST_STATS" value="{{ toJson .Data.RequestStats }}">

<!-- global js dependencies -->
<script src="/assets/js/automat.deps.js" type="application/javascript"></script>
<script src="/assets/js/menu.bundle.js" type="application/javascript"></script>

<!-- placeholder for js dependencies, use static files to import js code -->
{{ block "js-block" . }} {{ end }}

</body>
</html>
{{ end }}

First of all, we are defining name of the layout, which must end with layout suffix. Don’t worry, suffix will be added automatically when creating new template from editor and selecting layout template type. So, for our sample during layout creation we enter simple name and -layout added by default.

Next, we have some regular page content that is, potentially, repeated on every page in our application.

The first block that would be replaced by content from the page that will use this layout is {{ block "css-block" . }} {{ end }}. Then we have title placeholder: {{ block "title-block" . }} Default title {{ end }} and then placeholder for the biggest part of the page, - main section: {{ block "content-block" . }} {{ end }}. Check default (fall back) content of this block in the sample. Finally, we have js placeholder where we can include any js related to the page, please refer to css and js partials for more information on how to include js and css code.

Warning

While defining blocks inside layout, always put -block suffix in names. If name will end with something else then goerp parser will process them as regular partials and mess up template parameters. This may lead to appear some unknown partials and mey produce unexpected behavior.

Very simple example (page)

Now, when we have defined the layout, we may want to use it inside our pages. Let’s say it 10th page of our application:

<!-- Page creation always starting from importing our layout -->
{{ template "simple-layout" . }}

<!-- Title for our 10th page -->
{{ define "title-block" }} 10th page of application {{ end }}

<!-- The biggest part, - content -->
{{ define "content-block" }}

<!-- Display errors -->
{{ range .Data.Errors }}
<div class="error-row">
  <span>{{ . }}</span>
</div>
{{ end }}

<!-- Include partials -->
{{ template "employee-query-form-partial" . }}

<!-- Write regular goerp template code -->
<table>
  <thead>
  <tr>
    <th>Id</th>
    <th>First Name</th>
    <th>Last Name</th>
  </tr>
  </thead>
  <tbody>
  {{ range .Data.AccountAdminApi.EmployeeList }}
  <tr>
    <td>{{ .Id }}</td>
    <td>{{ .FirstName }}</td>
    <td>{{ .LastName }}</td>
  </tr>
  {{ end }}
  </tbody>
</table>

<!-- Include js -->
{{ define "js-block" }}
<script src="{{ staticFileLink "my-cool-js" }}"></script>
{{ end }}

{{ end }}

First of all, we are importing layout to the page.

Tip

Notice dot at the end of layout definition? Yes, we can pass any data to the layout, with the dot we are passing everything that page have. However, we can pass some custom data and, for example, add tabs logic to the layout. Just a tip.

Next, we are defining title of our page with {{ define "title-block" }} 10th page of application {{ end }}. Next, in our template we have css-block, but we don’t need to include any css links/deps here, so we just skip this block. Next goes content of the page, the biggest part. There we can use other templates-partials and goerp templating logic.

Finally, we have js block implementation which includes static file.

When to use layouts?

Layouts are powerful feature of the goerp template engine. However, it is quite complex and may produce some wierd behaviour if it is used in a wrong way. Better to avoid using layouts if they are not simplifying your work, just follow KISS principles. Same advice regarding partials. For example, if your application have 3 pages with simple form and a couple of tables, then maybe would be more convenient to write all content inside one template-page, so don’t need to jump between partials during development or maintaining.

Here is a short tips list when to use layout and when to avoid it:

Layouts are handy when

  • My application have many pages with repeated content (such as menu, tabs, dependencies, etc.)
  • I have many applications that reuses same code on every page. Please note, in this case you still need to create duplicate layout and give it a new name, but you need just copy-paste the content.

Avoid layouts when

  • My application have few pages and duplicating similar content wouldn’t take much effort

Pagination

To implement pagination, query parameters are usually used, which indicate how much data should be returned on the current page, how many records per page should be displayed, and what page number should be shown. The most commonly used query parameters for pagination are:

Page or PageNo: current page number (starting from 1). RecordsOnPage: number of elements per page (usually used to limit the amount of data returned per page).

For example, a request to get the first 20 items of a product listing on the second page might look like this:

GET /products?page=2&RecordsOnPage=20
Note

NOTE For pagination to work it is important to place it inside the GET form and add the pagination data.

Pagination example

This code is for a pagination component that displays and navigates through a table of data with 20 records per page by default. It includes previous and next page buttons, a dropdown menu for selecting the number of records to display per page, and a button to update the table based on the selected number of records.

<form method="get" data-pagination>
<!-- Pagination interface -->
<div class="pagination aligner aligner--contentStart aligner--centerVertical">
    <!-- Previous page button -->
    <button class="button--icon button--outlined icon-Chevron-Left"  id="previous-button"></button>

    <!-- Current page number -->
    <input type="hidden" name="Data.Example.PageNo"  id="page" value="{{ .Data.Example.PageNo }}">
    <p class="aligner aligner--centerVertical"> Page {{ .Data.Example.PageNo }} </p>

    <!-- Next page button -->
    <button class="button--icon  button--outlined icon-Chevron-Right"  id="next-button"></button>

    <!-- Results per page dropdown -->
    <p class="aligner aligner--centerVertical">Results per page</p>
    <select class="select margin-left-16 aligner aligner--centerVertical" name="Data.Example.RecordsOnPage" id="select">
        <option value="20"  {{if eq "20"  $.Data.Example.RecordsOnPage}}selected{{end}}>20</option>
        <option value="50" {{if eq "50"  $.Data.Example.RecordsOnPage}}selected{{end}}>50</option>
        <option value="100"  {{if eq "100"  $.Data.Example.RecordsOnPage}}selected{{end}}>100</option>
    </select>

    <!-- Show button to submit selected number of results per page -->
    <button  id="show"  class="button button--primary">Show</button>
</div>
</form>

Subsections of HTML layouts, components, etc.

Error and success states

Goerp server returns errors with every response. Errors are available in .Data.Errors variable which is array of strings. So it is possible to go through this array and display errors on page.

Adding small block into the page (or into layout) may look like that:

{{ range .Data.Errors }}
<div class="error-row">
  <span>{{ . }}</span>
</div>
{{ end }}

In addition to errors, response contains success flag which is available only after posting form with POST action and may be found in .Data.FormControl.PostActionSuccessful. So, right after errors block may be reasonable to add success message as well.

{{ if .Data.FormControl.PostActionSuccessful }}
<div class="success-row">
  <span>Success!</span>
</div>
{{ end }}
Note

NOTE When you add the errors and success flag, you are not writing any flags. Goerp editor will check errors and success flag by itself.

Form inputs

Subsections of Form inputs

Validation

The validation script is already embedded inside the script bundle.js

<script src="https://assets.erply.com/bo-prototype/js/bo-prototype.bundle.js"></script> 

In order for the field to be important, it is enough to write in the required in input

<div id="Mobile-error">
  <input type="text" required class="input input-fullWidth"
         id="Mobile" name="Mobile"
         placeholder="(e.g., +1 800 555 5555)"

  <!--It is important to write pattern parametrs-->
  pattern="[\d+\- ()]*">

  <!--It is important to write the notification field in-->
  <p class="text-error text-small" id="Mobile-text" style="display: none;">
    Required and should only contain numbers, plus, dashes, and spaces</p>
</div>

Ready patterns For email

pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"

This pattern checks that an email address starts with one or more alphanumeric characters, which can be followed by any number of dots, hyphens, plus signs, and percent signs. This is followed by the @ symbol followed by the domain name For zipcode

pattern="^\d{5}(?:[-\s]\d{4})?$"

This pattern checks that the Zipcode meets the following criteria: Consists of 5 digits For phone

pattern="[\d+\- ()]*"

Ready patterns Types For string

pattern="^[a-zA-Z\s]+$"

For numbers

pattern="^?\d+$"

Checkbox

The value of the checkbox is set based on the checkbox state. If checked, then it is true, otherwise false. Please note that input should have form-input class.

<input type="checkbox" id="formInputOnlineAppointmentsEnabled" class="form-input"
       name="AccountAdminApi.WarehouseInput.OnlineAppointmentsEnabled"
       {{ if .Data.AccountAdminApi.WarehouseInput.OnlineAppointmentsEnabled }} checked {{ end }}>

<!-- if not checked, passed hidden field, otherwise both but first one have higher priority (FIFO) -->
<input type="hidden" name="AccountAdminApi.WarehouseInput.OnlineAppointmentsEnabled" value="false">
<label for="formInputOnlineAppointmentsEnabled">Online appointments</label>

Public pages

Introduction

This option can either create completely public pages or pages that can be accessed via registration. On the pages that require registration the user accounts are created as customers.

Permission

Public pages are protected by 3 different methods.

  1. Erply user group permissions - on installation two user groups per app are created app-public-{app_name} and app-b2b-{app_name}, all data fetches to erply api’s will use the rights on these groups.
  2. Dynamic api request whitelist - a list of calls that are allowed with the dynamic api feature.
  3. Parameter whitelist - a list of parameters that we allow to be used.

Dynamic api request whitelist

The whitelist is under the ‘Publish settings’ tab in the editor view.

Publish settings Publish settings

When enabled the dynamic requests would need to be added to the page

Request whitelist Request whitelist

As of 1.234.5+ disabling of these features is not recommended and will be disabled for new templates.

Parameters whitelist

Note

This feature used to be connected to the Alias list (v1.188.5), from v1.189.0 this is now separated from it.

The following error is given when on public pages we send a parameter that is not added to the whitelist.

Parameter whitelist error Parameter whitelist error

The whitelist can be accessed under the ‘Publish settings’ in the editor view. The parameter whitelist counts in presets and request the whitelist (when used) so the values there do not need to be duplicated here.

Publish settings Publish settings

When enabled the used parameters need to be filled to the list of the page.

Parameter whitelist Parameter whitelist

If the permissions were previously set to the aliases then we can also use the ‘Import from alias’ button to automatically fill them based on the alias settings.

As of 1.234.5+ disabling of these features is not recommended and will be disabled for new templates.

Pattern matching rules for parameter whitelist

The whitelist supports pattern matching for the parameters. That would be very helpful when working with the json that contains or generates arrays. At the moment pattern matching is supported for digits <%d%> and string <%s%>.

To define a placeholder just put a <%d%> in the place where actual parameter name may have a number. Can be defined as many placeholders per parameter as needed.

For example, this rule:

KvsApi.Api.Json.saveData.string.entries.<%d%>.value.products.<%d%>.code

will much any of the following parameters:

KvsApi.Api.Json.saveData.string.entries.1.value.products.1.code
KvsApi.Api.Json.saveData.string.entries.2.value.products.1.code
KvsApi.Api.Json.saveData.string.entries.1.value.products.2.code
KvsApi.Api.Json.saveData.string.entries.2.value.products.2.code

Subsections of Public pages

Protected

Introduction

Protected pages can only be accessed with a special customer session

Setup

In order to use the protected features we need a page that that is set up to allow B2B access.

This can be configured in the template editor mode under the ‘Publish setting’ right menu.

Note

As of version 1.248.0 its also possible to set an optional expiration date to the page (UTC unix value) after this timestamp has passed on the server the will no longer be accessible using the b2b endpoint. Use authentication config input set the value.

Also note that this setting is based on applications or auth domains.

Add protected configuration Add protected configuration

  1. Every customer that is registered can only access the same application, the same login cannot be used for another app on the account.
  2. If auth domain is provided then all the applications that share it will also share the customers.

Optionally the redirect field can be used to redirect requests that are not authenticated to a specific page (public login page for example).

Registration

Create a public page that implements the automat api’s registration form. Use the Session.Customer.ID to detect if the user is already logged in.

Optionally, the AutomatApi.B2BLoginRegisterInput.LoginOnSuccess can be used to automatically login newly created user and AutomatApi.B2BLoginRegisterInput.Redirect to redirect the user to a specific page after successful login.

Check the automat api B2BLoginRegisterInput data source docs for additional available fields.

<!-- Read registration errors -->
<div class="my-error-container">
    {{ range .Data.Errors }}
    <span class="my-error-message">{{ . }}</span>
    {{ end }}
</div>

{{ if .Session.Customer.ID }}
    <h1>Already registered</h1>
{{ else }}
    <h1>Register a new user</h1>
    <form method="post">
        <input type="hidden" name="postActionEntity" value="B2BLoginRegisterInput">
        <input type="hidden" name="postActionRedirect" value="b2b-login-demo-page">

        <!-- Configure autologin and redirect -->
        <input type="hidden" name="AutomatApi.B2BLoginRegisterInput.LoginOnSuccess" value="1">
        <input type="hidden" name="AutomatApi.B2BLoginRegisterInput.Redirect" value="b2b-2-members-page">

        <br>
        <label for="firstName">First name</label>
        <input type="text" id="firstName" name="AutomatApi.B2BLoginRegisterInput.Firstname">

        <br>
        <label for="lastName">Last name</label>
        <input type="text" id="lastName" name="AutomatApi.B2BLoginRegisterInput.Lastname">

        <br>
        <label for="email">Email</label>
        <input type="text" id="email" name="AutomatApi.B2BLoginRegisterInput.Username">

        <br>
        <label for="password">Password</label>
        <input type="password" id="password" name="AutomatApi.B2BLoginRegisterInput.Password">

        <br>
        <button type="submit">Register</button>
    </form>
{{ end }}

Login

Create a public page that implements the automat api’s login form. Use the Session.Customer.ID to detect if the user is already logged in.

Optionally the AutomatApi.B2BLoginInput.Redirect can be used to redirect the user to a specific page after successful login

<!-- Read login errors -->
<div class="my-error-container">
    {{ range .Data.Errors }}
    <span class="my-error-message">{{ . }}</span>
    {{ end }}
</div>

{{ if .Session.Customer.ID }}
    <p>Welcome {{ .Session.Customer.FirstName }}</p>
{{ else }}
    <form method="post">
        <label for="username">Email</label>
        <input type="text" id="username" name="AutomatApi.B2BLoginInput.Username"/>

        <br>

        <label for="password">Password</label>
        <input type="password" id="password" name="AutomatApi.B2BLoginInput.Password"/>

        <br>
        <button type="submit">Login</button>
    </form>
{{ end }}

Public

Introduction

Public pages are accessible to all users.

Setup

This can be added in the template edit view and enabling the “Allow B2B and public access” setting.

Note

As of version 1.248.0 its also possible to set an optional expiration date to the page (UTC unix value) after this timestamp has passed on the server the will no longer be accessible using the public endpoint. Use authentication config input set the value.

Add public configuration Add public configuration

These routes can be accessed by prefixing the routes with /public/

Example:

  1. automat-eu.erply.com/104146/en/my-test-page
  2. automat-eu.erply.com/public/104146/en/my-test-page

Using dynamics

Starting from version 1.246.0, GoErp allows to use dynamic api features while creating register, login and others b2b authentication calls. This allows to create multiple requests and chain data between them, including authorization calls.

Note

Those API calls works properly only inside GoErp templates, because b2b (public) authentication depends on the template configuration, therefore all b2b API endpoints requires b2bKey to be sent in the request header. This key is available inside session and could be chained to the API request header.

More about b2b API calls read in the Automat API documentation.

Registration page

A simple registration page using API and dynamics. All page configuration steps remains same as for model based b2b authentication.

<h1>Register page</h1>
<form method="post">
   <input type="hidden" name="AutomatApi.Api.Post.register" value="v1/b2b/register-user">
   <!-- Use chaining to pass b2b key to the header -->
   <input type="hidden" name="AutomatApi.Api.Header.register.<-b2bKey" value="Session.key">
   <!-- Use .Tools.B2bAuthDomain to get the domain -->
   <input type="hidden" name="AutomatApi.Api.Json.register.string.domain" value="{{ .Tools.B2bAuthDomain }}">
   <!--  Setup redirect on succeed, if needed. It will be triggered only if all calls in this 
   template are successful. So, if registration fails, we stay on this page, very useful. -->
   <input type="hidden" name="Form.Redirect" value="10-b2b-in-page">
   
   <fieldset>
      <label for="firstname">Firstname:</label>
      <input id="firstname" name="AutomatApi.Api.Json.register.string.firstname" value="">
      <label for="lastname">Lastname:</label>
      <input id="lastname" name="AutomatApi.Api.Json.register.string.lastname" value="">
   </fieldset>
   <fieldset>
      <label for="username">Username:</label>
      <input id="username" name="AutomatApi.Api.Json.register.string.username" value="">
      <label for="password">Password:</label>
      <input id="password" name="AutomatApi.Api.Json.register.string.password" value="">
   </fieldset>
    <!-- Login would be performed automatically only if this parameter set to 1 -->
   <input type="hidden" name="AutomatApi.Api.Json.register.string.triggerLoginAfterRegister" value="1">
   <button type="submit">Register</button>
</form>
Note

Please note that if the triggerLoginAfterRegister parameter is not passed or set to 0, the user will be registered but not logged in. Although, Form.Redirect will be triggered anyway and try to access the protected area (10-b2b-in-page in this case). Therefore, while user not logged in, the 10-b2b-in-page page configuration will redirect to the login page (if configured). So, this could be confusing.

Login page

Here we will just use the username and password, and redirect to the protected area when successful.

<h1>Login page</h1>
<form method="post">
   <input type="hidden" name="AutomatApi.Api.Post.login" value="v1/b2b/login">
   <!-- Use chaining to pass b2b key to the header -->
   <input type="hidden" name="AutomatApi.Api.Header.login.<-b2bKey" value="Session.key">
   <!-- Use .Tools.B2bAuthDomain to get the domain -->
   <input type="hidden" name="AutomatApi.Api.Json.login.string.domain" value="{{ .Tools.B2bAuthDomain }}">
   <!--  Setup redirect on succeed, if needed. It will be triggered only if all calls in this 
   template are successful. So, if login fails, we stay on this page, very useful. -->
   <input type="hidden" name="Form.Redirect" value="10-b2b-in-page">
   <fieldset>
      <label for="username">Username:</label>
      <input id="username" name="AutomatApi.Api.Json.login.string.username" value="">
      <label for="password">Password:</label>
      <input id="password" name="AutomatApi.Api.Json.login.string.password" value="">
   </fieldset>
   <button type="submit">Login</button>
</form>

Samples

In the following sample we have a registration, login and a members area. This shows how we restrict the members area to only read results for the logged in customer.

Page setup

Note

The public features only work if the registration, login and any members pages belong to the same application or auth domain.

Registration page

A simple registration page with minimal inputs.

<!DOCTYPE html>
<html>
<body>
    <h2>Register a new user</h2>
    <form method="post">
        <input type="hidden" name="postActionEntity" value="B2BLoginRegisterInput">
        <input type="hidden" name="postActionRedirect" value="b2b-2-login-page">

        <div class="user-box">
            <input type="text" name="AutomatApi.B2BLoginRegisterInput.Firstname" required="">
            <label>Firstname</label>
        </div>
        <div class="user-box">
            <input type="text" name="AutomatApi.B2BLoginRegisterInput.Lastname" required="">
            <label>Lastname</label>
        </div>
        <div class="user-box">
            <input type="text" name="AutomatApi.B2BLoginRegisterInput.Username" required="">
            <label>Email</label>
        </div>
        <div class="user-box">
            <input type="password" name="AutomatApi.B2BLoginRegisterInput.Password" required="">
            <label>Password</label>
        </div>

        <!-- Read possible registration errors -->
        <div>
            {{ range .Data.Errors }}
                <span>{{ . }}</span>
            {{ end }}
        </div>

        <button type="submit">Register</button>
    </form>
</body>
</html>

For publish settings we enable the ‘Allow B2B and public access’ checkbox

Also since registration allows more fields to be used we also need to fill in the parameter whitelist of the value we will allow to be used, in this case we will fill the ones we have defined in the form.

Add public configuration Add public configuration

Login page

Here we will just use the username and password, and redirect to the members area when successful.

<!DOCTYPE html>
<html>
<body>
    {{ if .Session.Customer.ID }}
        <h2>Already logged on!</h2>
    {{ else }}
    <h2>Login</h2>
    <form method="post">
        <input type="hidden" name="AutomatApi.B2BLoginInput.Redirect" value="b2b-2-members-page">

        <div class="user-box">
            <input type="text" name="AutomatApi.B2BLoginInput.Username" required="">
            <label>Username</label>
        </div>
        <div class="user-box">
            <input type="password" name="AutomatApi.B2BLoginInput.Password" required="">
            <label>Password</label>
        </div>
        
        <div class="my-error-container">
            {{ range .Data.Errors }}
            <span class="my-error-message">{{ . }}</span>
            {{ end }}
        </div>
        
        <button type="submit">Login</button>
    </form>
    {{ end }}
</body>
</html>

For publish settings we enable the ‘Allow B2B and public access’ checkbox.

Login model parameters are automatically whitelisted, so we should not need to fill them here.

Members page

On the members page we will generate a simple list of documents for the currently logged on member. We use the preset to read session id to the request and prevent it from being adjusted via any parameters.

<!DOCTYPE html>
<html>
<body>
    <h1>Members page</h1>
    <form method="post">
        <input type="hidden" name="AutomatApi.B2BLogoutInput.Logout" value="1">
        <button type="submit">Logout</button>
    </form>
    <h2>Welcome {{ .Session.Customer.FirstName }} to the members area</h2>

    <!-- Erply api response data in the 'records' field -->
    <h2>My orders</h2>
    <form method="POST">
        <!-- Request definition -->
        <input type="hidden" name="ErplyApi.Api.Post.getDocs" value="getSalesDocuments" data-preset-val="getSalesDocuments">
        <button type="submit">Reload orders</button>
    </form>

    <ul>
        {{ $salesDocs := (.Data.ErplyApi.Api.Requests.getDocs.Response.Get "records").Array }}
    
        {{ if $salesDocs }}
            {{ range $salesDocs }}
            <li>{{ .Get "id" }} / {{ .Get "type" }} / {{ .Get "clientName" }}</li>
            {{ end }}
        {{ else }}
            <li>You currently have no orders!</li>
        {{ end }}
    </ul>
</body>
</html>
Note

By default all public access groups do not have access to read documents. This right needs to be given under the public user groups (starting with ‘app_public’ and ‘app_b2b’) by the account administrator in the backoffice.

Every application or authentication domain will have a separate user group with rights assigned to them.

For publish settings we enable the ‘Allow B2B access’ checkbox as we will only want logged in members to access it.

We also set the redirection to the name of the login page, so whenever its being accessed without a proper session it will be automatically redirected.

Add permissions Add permissions

We are also using dynamic api here, so we will add the ErplyApi.Api.Post.getDocs -> getSalesDocuments to the request whitelist.

Under URl configuration we add the preset ErplyApi.Api.PostParam.getDocs.<-clientID : Session.customer.ID This will write the current session customer id to the request when it is being done, since we do not allow the parameter to be adjusted in the parameters list then it cannot be changed to anything else.

Add preset chain Add preset chain

Workflow

To test it:

  1. Register a new user
  2. Login with the created customer
  3. The members area only displays the members sales documents (use backoffice to create them or create a new page that creates the documents for the member using the same method)

Url alias

Introduction

Url alias features allow as to generate better url’s for the application. Using of dynamic api can generate a large url parameters set that cannot be easily read.

Alias feature adds the following functionalities:

  1. Custom path parameters that can be used on any page
  2. Alias mapping that can be used to map parameter values to other parameters
  3. Single parameter can be assigned to multiple values so multiple api calls would not require a separate parameter if the value is the same.

Custom path parameters

Each route can use up to 3 custom parameters (path1, path2 and path3).

If we had a page accessible from /my-store. We can navigate to the page also with /my-store/something_1 , /my-store/something_1/something_1 and /my-store/something_1/something_2/something_3

To read the values we can use regular custom parameters fetch.

{{ .Data.Parameters.path1 }}
{{ .Data.Parameters.path2 }}
{{ .Data.Parameters.path3 }}

Alias mapping

Aliases can be added from the url configuration menu

URL configuration URL configuration

Parameter name would be the alias (some parameter) whose value would be moved to the configured parameters.

If we had a request with a custom parameter /my-store?productId=100 then we can use the alias to set value for the dynamic input.

productId -> ErplyApi.Api.PostParam.productsRq.productID

Add alias Add alias

this would mean that we can load the product api call with the custom parameter, and we would not need to use the dynamic api parameter declaration syntax in the url as it would be aliased to the correct value.

Note that the same parameter can be mapped to multiple parameters so if there are multiple api calls that expect the same value then we can use this to just use one in the url.

Path to alias

We can also map path parameters to the aliases. For this just use the one of the path parameters (path1, path2 or path3).

path1 -> ErplyApi.Api.PostParam.productsRq.productID

This would mean that we can navigate to the page by just /my-store/101 and this would map the value of 101 to the ErplyApi.Api.PostParam.productsRq.productID parameter.

Content types

Introduction

With inbuilt methods its possible to make the server render page content data with different content type header or even produce a file stream for the content.

For this to work we would need to construct the data in the template in correct format we expect to return.

Methods to return

There are 2 ways we can make these calls

  1. Using the appropriate query parameters
/my-csv-page?CSV=1
  1. Using file type suffix
/my-csv-page.csv

Note that -page suffix is not required, a link without it will lead to the correct route.

/my-csv.csv

Supported content types

Type Parameter Path suffix
Pdf ?PDF .pdf
Xml ?XML .xml
Csv ?CSV .csv
Json ?JSON .json
Txt ?TXT .txt

Additional parameters

Pdf

  1. PDF.FileName - Server will produce the data as a file stream instead, giving the file the requested name
  2. PDF.DPI - Dpi of the pdf, default 300
  3. PDF.Orientation - Orientation, default Portrait (Portrait, Landscape)
  4. DF.PageSize - File size, default A4 (A0, A1, A2, A3, A4, A5, A6, A7, Letter, Legal, Ledger, Tabloid, Folio, Executive)
  5. PDF.MarginTopMM - Set a top margin
  6. PDF.MarginBottomMM - Set a bottom margin
  7. PDF.MarginLeftMM - Set a left margin
  8. PDF.MarginRightMM - Set a right margin
  9. PDF.PageWidth - Set a custom page width (this will override the page size value)
  10. PDF.PageHeight - Set a custom page height (this will override the page size value)

Xml

  1. XML.FileName - Server will produce the data as a file stream instead, giving the file the requested name

Csv

  1. CSV.FileName - Server will produce the data as a file stream instead, giving the file the requested name

Json

  1. JSON.FileName - Server will produce the data as a file stream instead, giving the file the requested name

Txt

  1. TXT.FileName - Server will produce the data as a file stream instead, giving the file the requested name

Setting content type in the template

This will apply same rules as setting the content type in the url, but in this case entire template would be processed using most suited template processor (not related to PDF). For example, setting content type to json or xml would process template through text processor, not html, removes all html related validations/encoding and makes process much faster.

Content type setting in template Content type setting in template

Navigation

Introduction

Only page, css and js types can be accessed with the url. Regular implementation url’s in the editor would look like this

{instance}/{clientCode}/en/editor/my-products-page

This would be accessible from

{instance}/{clientCode}/en/my-products-page

Note that -page suffix is not required and a link without it would still be routing to the correct location

{instance}/{clientCode}/en/my-products

Public / B2B

If the public features are used then the endpoint would prefix it with public

{instance}/public/{clientCode}/en/my-products

Sub paths for navigation

We can use the sub paths for navigation inside the templates.

Usually 3 extra sub paths can be used. We can access the path values with an indexed value from the path.

https://template-engine-eu10.erply.com/104706/en/test-food-ordering-choose-table-page/test1/test2/test3

We can then access the values using

{{ .Data.Parameters.path1 }} // test1
{{ .Data.Parameters.path2 }} // test2
{{ .Data.Parameters.path3 }} // test3

Goerp has a special parameter that can be used to get the last path parameter as well (all content after the last non-encoded /) https://template-engine-eu10.erply.com/public/104146/et/some-page/test1/test2/test3/test4%2Ftest5

{{ .Data.Parameters.pathx }} // test4%2Ftest5

These routes will always navigate to the parent page, but we can set up special rules in the page to make some changes based on the path parameters.

{{ if eq .Data.Parameters.path1 "test1" }}
    {{ template "my-route-a-partial" . }}
{{ else if  eq .Data.Parameters.path1 "test2" }}
    {{ template "my-route-b-partial" . }}
{{ else }} 
    {{ template "my-default-partial" . }}
{{ end }}

We can use the tools helper to make navigating sub paths easier

{{ if .Tools.IsPath "my-route-a" }}
    {{ template "my-route-a-partial" . }}
{{ else .Tools.IsPath "my-route-a/my-route-b" }}
    {{ template "my-route-b-partial" . }}
{{ else .Tools.IsPath "my-route-a/my-route-b/my-route-c" }}
    {{ template "my-route-c-partial" . }}
{{ else }} 
    {{ template "my-default-partial" . }}
{{ end }}

Subsections of Navigation

Sitemap

To produce a sitemap we can use the combination of multiple features

  1. The .xml document type suffix
  2. Application url configurations
  3. Automat api helper to produce us a list of possible pages

Create the file

Create a new page type and make it publicly accessible.

Under url configuration lets fill a static preset for the automat api for the application we want to produce the list for.

Name: AutomatApi.Api.Get.myRequest1 Value: v1/application-sitemap/{My_app_uuid_here}

Also add the name and value pair to the request whitelist.

Add the following content to the page

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    {{ $records := .Data.AutomatApi.Api.Requests.myRequest1.Response.Get "records" }}
    {{ range $records.Array }}
    <url>
        <loc>{{ .Get "loc" }}</loc>
        <lastmod>{{ .Get "lastMod" }}</lastmod>
    </url>
    {{ end }}
</urlset>

This will produce the sitemap file for us when accessing it from the approriate endpoint with the correct document type suffix.

{instance}/{clientCode}/en/editor/my-sitemap.xml

You can also manually produce and edit as you like.

Mapping sub paths

If some pages are using the sub paths features then we can use the url configuration sub paths area to map them. This will make the automat api return all mapped sub paths to the sitemap.

We can use the hide checkbox there to make the page not be generated into the sitemap aswell.

Robots

We can use the txt type renderer to output the robots.txt file. Note that this is only useful for instances that implement a custom domain, as the robots file needs to be at the root of the page.

URL configuration URL configuration

A basic robots file would have the following content and would be called using the .txt document type suffix.

{my-custom-domain}/robots.txt
ser-agent: Googlebot
Disallow: /nogooglebot/

User-agent: *
Allow: /

Sitemap: http://{my-instance}/public/104146/en/sitemap.xml

Back office integration

Linking from GoERP to a back office page

Back office URLs have a predictable structure. Here is a sample URL (a product card):

https://t10.erply.com/12345/?lang=eng&section=product&edit=23
  • The base URL (https://t10.erply.com/12345/) is available as variable {{ .Session.User.BOLoginUrl }}.
  • Parameter lang is the current language. Use {{ .Session.Language.LegacyCode }}. Please always set this parameter; if the user has switched to a particular language, they want this language to be remembered as they navigate through the system.
  • Parameter section identifies the page: the form or the list view.
  • Parameter edit is used on forms and indicates the ID of the record. If you want to open a new empty form, use edit=new.

Prefilling back office forms

On an empty form, any field can be prefilled with a URL parameter. Use the form field’s name attribute as the URL parameter name.

The following URL opens a new invoice form. (For clarity, it has been split into multiple lines.)

  • The document’s type will be set to “Receipt” (ID = 2),
  • customer ID will be set to 99
  • and warehouse ID to 2.
{{ .Session.User.BOLoginUrl }}
?lang={{ .Session.Language.LegacyCode }}
&section=invoice
&edit=new
&invoice_type_id=2
&invoice_orgper_idDat_client=99
&invoice_warehouse_id=2

This approach only works for an empty form, not for saved records.

Prefilling back office list filters

Use this approach if a GoERP page must link to a legacy back office list view.

Erply back office list views use method="post" forms, so filter presets need to be supplied as POST parameters.

For example, opening the invoices page with the “Creator” filter applied (invoices created by employee with ID 123) requires submitting a form:

<form method="post" action="{{ .Session.User.BOLoginUrl }}?lang={{ .Session.Language.LegacyCode }}&section=invoices">
    <input type="hidden" name="search_orgper_idDat_author" value="123">
</form>

Replacing a back office page with a GoERP page

Back office forms can be configured to redirect to other URLs.

If a customer installs an app that manages products, they can make a product card always open in that app. (And likewise, employee forms can open in an employee app, and customer cards in a CRM app).

At the moment this is a manual CAFA configuration step that must be done individually on each account.

  1. Go to Settings > Configuration Admin > App configuration.
  2. Click “Add new configuration” to create a new setting.
  3. Fill in the form as follows:
Field Value
Application bo_ui
Level Company
Level ID leave empty
Type ui_replacements
Name Use any value, for example the name of the app that is going to handle the redirects
Value Type JSON
Value Define a JSON object as instructed below.

Example (shown with all possible supported adjustments, all components optional):

{
  "redirect_creation_form": [
    {
      "section": "prodin",
      "url": "https://example.com/{CLIENT_CODE}/new-inventory-registration-page"
    }
  ],
  "redirect_edit_form": [
    {
      "section": "product",
      "url": "{GOERP_URL}/{CLIENT_CODE}/{ISO_LANGUAGE}/edit-product-page?productID={RECORD_ID}"
    }
  ],
  "redirect_page": [
    {
      "section": "products",
      "url": "https://example.com/{CLIENT_CODE}/product-list"
    }
  ],
  "add_tabs": [
    {
      "section": "orgperC",
      "name": "metadata",
      "title": {
        "en": "Metadata",
        "fr": "MƩtadonnƩes"
      },
      "url": "{GOERP_URL}/{CLIENT_CODE}/{ISO_LANGUAGE}/customer-metadata-page?customerID={RECORD_ID}"
    }
  ],
  "replace_tabs": [
    {
      "section": "orgperC",
      "name": "contracts_new",
      "title": {
        "en": "A better tab for customer contracts"
      },
      "url": "{GOERP_URL}/{CLIENT_CODE}/{ISO_LANGUAGE}/customer-contracts-page?customerID={RECORD_ID}",
      "replace": "orgperC_contracts"
    }
  ]
}

Erply section name can be found from the URL: it’s the keyword following “&section=…” Product card is product, inventory registration form is prodin, employee form is orgperB and so on.

The redirect URL supports placeholders:

  1. {GOERP_URL} - Base URL of Erply app store apps.
  2. {CLIENT_CODE} - Account number
  3. {RECORD_ID} - Record ID
  4. {LANGUAGE} - Three-letter language code (used in Erply back office)
  5. {ISO_LANGUAGE} - Two-letter language code (used in app store apps)

As many JSON objects can be created as needed.

Cache

Introduction

Cache features are optional features that can be used to make the page loads quite a bit faster. These features mean that api requests are not always made and the created content is loaded from cache instead.

GoErp implements 2 different caching options. The options are disabled by default as it can affect the behaviour of the pages quite a bit.

The cache settings are only available for page type templates. The settings area can be found under the ‘Publish settings’ area in the template edit view.

Cache settings Cache settings

Browser cache headers

Cache is separate for every client.

This feature utilizes the default browsers cache mechanism using the modified date values and cache max-age modifiers. With this feature the browser caches the contents and sends a request with the modified date value on the next request, the server will respond with not modified if the value has not changed.

Note that the modified value in this case it the date when the page template was updated, any changes from used api’s in the template do not update the value.

Can be used to speed up templates that do not always need the most up-to-date values. You can specify the max-age in seconds. Content will be re-loaded if the max-age is reached or the template page is updated.

GoErp server cache

Single cached element for all clients.

The feature caches the contents on the server instead and returns the data without running the api requests. The cached data is shared between multiple requests.

Should only be used on pure static pages or pages that serve non-current session related data.

A custom age value can be provided in minutes.

Use cases

  1. Mostly static data pages.
  2. Pages that run slow running api requests or api mock pages where fresh by the second data is not always required.
  3. Browser cache headers with a small age can also be used to make a false sense of speed or reduce possible flickering, but note if the data indeed changes it can appear jumpy depending on the layout of the page.

Domain setup for cloudflare

Internal configuration

Access cloudflare dashboard at https://dash.cloudflare.com/ Select add site as illustrated in the below picture

select select

Enter the name of the domain you wish to add eg example.com

Select a plan for the domain

select select

Next you will see which DNS records will be imported automatically

select select

Press next once configured

Now you will see the nameservers that have to be configured in the domain registrar

select select

External configuration

Client will receive nameservers they must point their domain towards For example

liz.ns.cloudflare.com ram.ns.cloudflare.com

This process varies from registrar to registrar Example instructions based on registrar

https://www.ionos.com/help/domains/using-your-own-name-servers/using-your-own-name-servers-for-a-domain/ https://help.101domain.com/kb/managing-name-server-records https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-name-servers-glue-records.html#domain-name-servers-glue-records-adding-changing https://help.blacknight.com/hc/en-us/articles/212512229-Changing-nameservers-in-cp-blacknight-com https://www.bluehost.com/help/article/custom-nameservers https://directnic.com/knowledge/article/33:how%2Bdo%2Bi%2Bmodify%2Bname%2Bservers%2Bfor%2Bmy%2Bdomain%2Bname%253F http://www.dnsmadeeasy.com/support/faq/ https://www.domain.com/help/article/domain-management-how-to-update-nameservers https://www.dotster.com/help/article/domain-management-how-to-update-nameservers https://help.dreamhost.com/hc/en-us/articles/360038897151 https://kb.easydns.com/knowledge/settingchanging-nameservers/ https://help.enom.com/hc/en-us/articles/115000486451-Nameservers-NS https://www.fastdomain.com/hosting/help/transfer_client_start https://billing.flokinet.is/index.php?rp=/knowledgebase/57/Nameserver-and-DNS-records.html https://docs.gandi.net/en/domain_names/common_operations/changing_nameservers.html https://www.godaddy.com/help/change-nameservers-for-your-domain-names-664 https://www.hostgator.com/help/article/changing-name-servers https://hostico.ro/docs/setarea-nameserverelor-din-contul-de-client-hostico/ https://my.hostmonster.com/cgi/help/222 https://support.hover.com/support/solutions/articles/201000064742-changing-your-domain-nameservers https://faq.internetbs.net/hc/en-gb/articles/4516921367837-How-to-update-Nameservers-for-a-domain https://www.ipage.com/help/article/domain-management-how-to-update-nameservers https://support.melbourneit.au/docs/modifying-domain-nameservers-or-dns-records https://support.moniker.com/hc/en-gb/articles/10101271418653-How-to-update-Nameservers-for-a-domain https://www.name.com/support/articles/205934457-registering-custom-nameservers https://www.namecheap.com/support/knowledgebase/article.aspx/767/10/how-can-i-change-the-nameservers-for-my-domain https://www.networksolutions.com/manage-it/edit-nameservers.jsp https://docs.ovh.com/gb/en/domains/web_hosting_general_information_about_dns_servers/#step-2-edit-your-domains-dns-servers https://kb.porkbun.com/article/22-how-to-change-your-nameservers https://support.rackspace.com/how-to/rackspace-name-servers/ https://www.register.com/knowledge https://support.squarespace.com/hc/articles/4404183898125-Nameservers-and-DNSSEC-for-Squarespace-managed-domains#toc-open-the-domain-s-advanced-settings https://kb.site5.com/dns-2/custom-nameservers/ https://cloud.ibm.com/docs/dns?topic=dns-add-edit-or-delete-custom-name-servers-for-a-domain https://helpcenter.yola.com/hc/articles/360012492660-Changing-your-name-servers

Once done it might take up to 24 hours for the changes to take effect.