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
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
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…).
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 bydefine
‘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.
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.
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 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>
HTML layouts, components, etc.
- Check our UI components page for samples
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 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.
- 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.
- Dynamic api request whitelist - a list of calls that are allowed with the dynamic api feature.
- 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.
When enabled the dynamic requests would need to be added to the page
As of 1.234.5+ disabling of these features is not recommended and will be disabled for new templates.
Parameters whitelist
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.
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.
When enabled the used parameters need to be filled to the list of the page.
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.
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.
- Every customer that is registered can only access the same application, the same login cannot be used for another app on the account.
- 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.
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.
These routes can be accessed by prefixing the routes with /public/
Example:
- automat-eu.erply.com/104146/en/my-test-page
- 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.
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>
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
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.
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>
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.
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.
Workflow
To test it:
- Register a new user
- Login with the created customer
- 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:
- Custom path parameters that can be used on any page
- Alias mapping that can be used to map parameter values to other parameters
- 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
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
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
- Using the appropriate query parameters
/my-csv-page?CSV=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 |
---|---|---|
Xml | ?XML | .xml |
Csv | ?CSV | .csv |
Json | ?JSON | .json |
Txt | ?TXT | .txt |
Additional parameters
- PDF.FileName - Server will produce the data as a file stream instead, giving the file the requested name
- PDF.DPI - Dpi of the pdf, default 300
- PDF.Orientation - Orientation, default Portrait (Portrait, Landscape)
- DF.PageSize - File size, default A4 (A0, A1, A2, A3, A4, A5, A6, A7, Letter, Legal, Ledger, Tabloid, Folio, Executive)
- PDF.MarginTopMM - Set a top margin
- PDF.MarginBottomMM - Set a bottom margin
- PDF.MarginLeftMM - Set a left margin
- PDF.MarginRightMM - Set a right margin
- PDF.PageWidth - Set a custom page width (this will override the page size value)
- PDF.PageHeight - Set a custom page height (this will override the page size value)
Xml
- XML.FileName - Server will produce the data as a file stream instead, giving the file the requested name
Csv
- CSV.FileName - Server will produce the data as a file stream instead, giving the file the requested name
Json
- JSON.FileName - Server will produce the data as a file stream instead, giving the file the requested name
Txt
- 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.
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.
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
- The .xml document type suffix
- Application url configurations
- 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.
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§ion=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, useedit=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 }}
§ion=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 }}§ion=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.
- Go to Settings > Configuration Admin > App configuration.
- Click “Add new configuration” to create a new setting.
- 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 “§ion=…” Product card is product
,
inventory registration form is prodin
, employee form is orgperB
and so on.
The redirect URL supports placeholders:
{GOERP_URL}
- Base URL of Erply app store apps.{CLIENT_CODE}
- Account number{RECORD_ID}
- Record ID{LANGUAGE}
- Three-letter language code (used in Erply back office){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.
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
- Mostly static data pages.
- Pages that run slow running api requests or api mock pages where fresh by the second data is not always required.
- 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
Enter the name of the domain you wish to add eg example.com
Select a plan for the domain
Next you will see which DNS records will be imported automatically
Press next once configured
Now you will see the nameservers that have to be configured in the domain registrar
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.