Skip to main content

Cheat Sheet

A few examples for the establishment of more advanced features.

Send Email Notifications

This text explains how to establish an Action Button to send automated emails. It uses the foundation of the Support Ticket tutorial for entities and attributes.

1. Establish Email Settings

Establish the configurations for the email communication. Go to Config > App Features. Create a new page.

  • Set Name to: Email Notifications
  • Click Finish to establish the settings

Add the following Config Items (i.e., click Add Config Item):

  • Set Name to: SMTP ServerSet Value Type to: Text
  • Set Name to: SMTP PortSet Type to: NumberSet Default value to: 587
  • Set Name to: SMTP LoginSet Type to: Text
  • Set Name to: SMTP PasswordSet Type to: Text
  • Set Name to: SMTP Email Set Type to: Text

2. Add NuGet Package

At the top of the Config subsection, click Resources

  • Click Add Resource
  • Select “Add NuGet Package Assembly Resource”
  • Search for MailKit. Click Add Package
  • Click OK

3. Establish Custom Code

Go to Code > Custom Code. Create a new page.

  • Set Name of file to: StmpMailer
  • Click Finish
  • Copy and paste the following:
Click to expand the code block
using System.Collections.Generic;
using MailKit.Net.Smtp;
using MimeKit;
using MimeKit.Text;

namespace Util
{

internal class SmtpMailer
{
private readonly bool isEnabled;
private readonly string smtpServer;
private readonly int smtpPort;
private readonly string account;
private readonly string password;
private readonly string email;

public static readonly SmtpMailer Default = new SmtpMailer(
App.Features.EmailNotifications.IsEnabled,
App.Features.EmailNotifications.Config.SMTPServer,
App.Features.EmailNotifications.Config.SMTPPort,
App.Features.EmailNotifications.Config.SMTPLogin,
App.Features.EmailNotifications.Config.SMTPPassword,
App.Features.EmailNotifications.Config.SMTPEmail
);

public SmtpMailer(string smtpServer, int smtpPort, string account, string password) : this(true, smtpServer, smtpPort, account, password, account)
{

}

private SmtpMailer(bool isEnabled, string smtpServer, int smtpPort, string account, string password, string email)
{
this.isEnabled = isEnabled;
this.smtpServer = smtpServer;
this.smtpPort = smtpPort;
this.account = account;
this.password = password;
this.email = email;
}

public void SendEmail(string recipient, string subject, string content)
{
SendInternal(new [ ] { recipient }, subject, content, false);
}

public void SendHtmlEmail(string recipient, string subject, string htmlContent)
{
SendInternal(new [ ] { recipient }, subject, htmlContent, true);
}

public void SendMassEmail(IEnumerable<string> recipients, string subject, string content)
{
SendInternal(recipients, subject, content, false);
}

public void SendMassHtmlEmail(IEnumerable<string> recipients, string subject, string htmlContent)
{
SendInternal(recipients, subject, htmlContent, true);
}

public EmailBuilder NewEmail()
{
return new EmailBuilder(this);
}


private void SendInternal(IEnumerable<string> recipients, string subject, string content, bool isHtml, IEnumerable<EmailAttachment> attachments = null, string customSender = null)
{
if (!this.isEnabled)
{
return;
}

attachments = attachments ?? Enumerable.Empty<EmailAttachment>();


try
{
using(var client = new SmtpClient())
{
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse(customSender ?? this.email));
foreach(var recipient in recipients)
{
message.To.Add(MailboxAddress.Parse(recipient));
}

message.Subject = subject;

var bodyBuilder = new BodyBuilder();

if (isHtml)
bodyBuilder.HtmlBody = content;
else
bodyBuilder.TextBody = content;


foreach(var a in attachments)
{
bodyBuilder.Attachments.Add(a.FileName, a.ContentProvider(), ContentType.Parse("application/octet-stream"));
}

message.Body = bodyBuilder.ToMessageBody();

client.Connect(smtpServer, smtpPort);
client.Authenticate(new System.Net.NetworkCredential(account, password));
client.Send(message);
client.Disconnect(true);
}
}
catch (Exception e)
{
throw new Exception($"Could not send email [{smtpServer}:{smtpPort}, {account}]", e);
}
}

public class EmailBuilder
{
private string subject = null;
private string content = null;
private string sender = null;
private string replyTo = null;
private bool contentIsHtml = false;

private readonly List<string> recipients = new List<string>();
private readonly List<EmailAttachment> attachments = new List<EmailAttachment>();
private readonly SmtpMailer mailer;
public EmailBuilder(SmtpMailer mailer) { this.mailer = mailer; }

public EmailBuilder AddRecipient(string recipient)
{
this.recipients.Add(recipient);
return this;
}

public EmailBuilder AddRecipients(IEnumerable<string> recipients)
{
this.recipients.AddRange(recipients);
return this;
}

public EmailBuilder SetSender(string senderEmail)
{
this.sender = senderEmail;
return this;
}
public EmailBuilder SetReplyTo(string replyToEmail)
{
this.replyTo = replyToEmail;
return this;
}

public EmailBuilder SetSubject(string subject)
{
this.subject = subject;
return this;
}

public EmailBuilder SetPlainContent(string content)
{
this.content = content;
this.contentIsHtml = false;
return this;
}

public EmailBuilder SetHtmlContent(string content)
{
this.content = content;
this.contentIsHtml = true;
return this;
}

public EmailBuilder AddAttachments(IEnumerable<IDocument> documents)
{
foreach(var d in documents)
{
AddAttachment(d);
}
return this;
}

public EmailBuilder AddAttachments(IEnumerable<IDocumentRevision> documents)
{
foreach(var d in documents)
{
AddAttachment(d);
}
return this;
}

public EmailBuilder AddAttachment(IDocument document)
{
return AddAttachment(document.GetLatestRevision());
}

public EmailBuilder AddAttachment(IDocumentRevision documentRev)
{
if (documentRev == null)
{
return this;
}

this.attachments.Add(new EmailAttachment()
{
FileName = documentRev.FileName,
ContentProvider = () => documentRev.Content
});
return this;
}

public void Send()
{
mailer.SendInternal(recipients, subject, content, contentIsHtml, attachments, sender);
}
}


protected class EmailAttachment
{
public Func<byte[ ]> ContentProvider
{
get;
set;
}
public string FileName
{
get;
set;
}
}
}
}

4. Establish the Text for the Email

Go to Code > Templates. Create a new page.

  • Set Name of Template to: Text For Email
  • Set Template Code Language to: Html
  • Set Model Type to: Entity
  • Set Entity to: Ticket

Note: The attributes Title and Assigned To are used in the template text below.

  • Copy and paste the following:
Click to expand the code block
<html>

<head>
</head>

<body>
<table>
<tr>
<td>Ticket Assigned</td>
</tr>
<tr>
<td>Hi @Model.AssignedTo.Name ,</td>
</tr>
<tr>
<td>Ticket "@Model.Title" has been assigned to you.</td>
</tr>
<tr>
<td>Please resolve the issue as quickly as possible.</td>
</tr>
</table>
</body>

</html>
  • Click Finish

5. Establish Business Command to Send Email to Assigned Person

Go to Business > Commands. Create a new page.

  • Set Name to: Main Assigned Person
  • Set Type to: Entity Command
  • Set Entity to: Ticket
  • Copy and paste the following:
Click to expand the code block
(ticket, db, ctx) =>
{
var content = App.Templates.TextForEmail.Render(ticket);
var subject = "Task Assigned";
var mail = ticket.AssignedTo.Email;

//this renders a template with task its called on
App.Templates.TextForEmail.Render(ticket);
//this sends email
Util.SmtpMailer.Default.SendHtmlEmail(mail, subject, content);
}

6. Add an Action Button

The email option needs to exist somewhere. For example, you might add it to the top of the Ticket Detail Page Go to UI > Entity Pages and double-click Ticket Detail

  • At the top of the Layout section, click Add Action
  • Set Entity Command to: Main Assigned Person
  • In the UI section, set Button Label to: Send Email
  • Set Icon to: Envelope
  • Click OK

7. Release the App

8. Create a Production Instance

A Production instance should be created in order to send an email to an actual user.

  • Click + Create Instance
  • On the Create New Application Instance page
  • Set Hosting to: Jetveo Cloud
  • Set Application Type to: Production
  • Set the Authentication as desired
  • Name the application as desired
  • Set Application Version to the most recent release
  • Click Create

9. Configure Email Settings on the App Overview

  • Click Settings
  • Click the Features tab
  • Mark Email Notifications
  • Click Configure

Input the following information (i.e. click Change) according to your email server: SMTP Server: TBD SMTP Port: 587 (This should already be established.) SMTP Login: TBD SMTP Password: TBD SMTP Email: TBD

CSV Import

It is useful to be able to import data. This text will explain how to import a three-column table from a CSV file. Every row represents one new record of an Entity Data entity which is stored as a database table

Learn more
Employee.

Go to Code > Custom code and create a new file EmployeeImporter. Code would look something like this:

Click to expand the code block
public class EmployeeImporter
{
[CsvHelper.Configuration.Attributes.Index(0)]
public string Name
{
get;
set;
}

[CsvHelper.Configuration.Attributes.Index(1)]
public string BirthDate
{
get;
set;
}

[CsvHelper.Configuration.Attributes.Index(2)]
public string Position
{
get;
set;
}
}

Now we will create a Custom command that will be used in the application to import the file.

Go to Business > Business commands and create a new Custom command without a result. Add a Document Attribute Document in the Model panel and add it to a view in View panel. Code of the Business command would look something like this:

Click to expand the code block
(model, db, ctx) =>
{
var numberFormat = new System.Globalization.NumberFormatInfo() { NumberDecimalSeparator = "." };
var import = CsvImporter.ImportData<EmployeeImporter>(model.Document);
var employees = db.EmployeeSet.Select(e => e.Name).ToList();
var positionLookup = EmployeePositions.AllEntries.ToDictionary(x => x.Name, x => x);
var row = 0;

foreach(var i in import)
{
row++;

if (!employees.Contains(i.Name))
{

var newEmployee = db.EmployeeSet.Add(new Employee()
{
Name = i.Name
});

if (positionLookup.ContainsKey(i.Position))
{
newEmployee.Position = positionLookup[i.Position];
}
else
{
throw new BusinessException("Employee position does not exist: " + i.Position + ". On row: " + row);
}

if(DateTime.TryParse(i.BirthDate, out dateValue))
{
newEmployee.BirthDate = dateValue;
}
else
{
throw new BusinessException("Employees birth date failed to parse: " + i.BirthDate + ". On row: " + row);
}
}
}
}

Now the only thing remaining is to hook the Custom command to a button for it to be accessible from the application.

Get Economic Subject from ARES by IČO (Identification Number)

ARES (Administrative register of economic subjects) is register where you can search for data on economic entities registered in the Czech Republic.

Go to Code > Custom code and create a new file Ares. Copy this code:

Click to expand the code block
using System.Net.Http.Json;
using System.Net.Http;

public class SubjektApiResponse
{
public string Ico { get; set; }

public string ObchodniJmeno { get; set; }

public DateTime DatumVzniku { get; set; }
}

public class AresLib
{
static readonly HttpClient sharedClient = new()
{
BaseAddress = new Uri("https://ares.gov.cz"),
DefaultRequestHeaders =
{
{ "Accept", "application/json" }
}
};

public static async Task<SubjektApiResponse> GetSubjectByIco(string ico)
{
using (var response = await sharedClient.GetAsync($"ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/{ico}"))
{
await ThrowIfResponseNotOk(response);

var subjekt = await response.Content.ReadFromJsonAsync<SubjektApiResponse>();

return subjekt;
}
}

private static async Task ThrowIfResponseNotOk(HttpResponseMessage response)
{
if (response.Content == null || !response.IsSuccessStatusCode || response.Content.Headers?.ContentType?.MediaType != "application/json")
{
string responseString = "";
try
{
if(response.Content != null)
{
responseString = await response.Content.ReadAsStringAsync();
}
}
catch
{ }

throw new HttpRequestException($"Invalid response from ARES: HTTP status code {response.StatusCode}, URL {response.RequestMessage.RequestUri}, response:\n{responseString}");
}
}
}

Then you can use it in an Entity Command like this:

Click to expand the code block
(subject, db, ctx) =>
{
var subjectData = AresLib.GetSubjectByIco(subject.ICO).Result;
subject.Name = subjectData.ObchodniJmeno;
}

LINQ

Check user permissions

This code checks if a current app user has "MyClaim" Security claim:

(_ , db, ctx) => ctx.User.HasClaim(App.Security.Claims.MyClaim)

Use LINQ to narrow data source for Data grid

This code narrows collection of entities to entities where at least one of two attributes is true and current app user is set as an "owner".

(entity , db, ctx) => db.EntitySet.Where(e => (e.FirstAttribute == true || e.SecondAttribute == true) && e.Owner == ctx.User)

Preserve deleted entity records in app database

Using Delete System command is an irreversible operation. To preserve deleted entities, we need to keep them in database and just flag them as deleted.

  1. Create an "Deleted" bool attribute on the entity
  2. Create an entity command "Delete entity".
    (entity , db, ctx) =>
    {
    entity.Deleted == true;
    }
  3. Add this command to desired locations - Action button on an entity Detail page, command to a List page or data grids...
  4. Change data source for your grids and List pages
    (entity , db, ctx) => db.EntitySet.Where(e => e.Deleted == false)

Create a new entity record from code

Entity records don't have to be created only through Create pages. You can create entity records from code, commands, or expressions. As an example we will create a Log entity that has a Name text attribute, CreatedOn DateTime attribute and CreatedBy User attribute.

db.LogSet.Add(new Log
{
Name = "New Log created.",
CreatedOn = DateTime.Now,
CreatedBy = ctx.User
});

Throw a user-friendly exception

Some parts of your Custom code or Business commands might fail. If you suspect that this can happen, like when you parse text to different a type, you might want to catch the exception and show a personalized message to the app user.

throw new BusinessException("Excellent description of the exception.");

Change State of an entity from code

Sometimes you might want to change the state of an entity from code and not wait for users to do it on a Detail Page. For this example, assume that when all conditions for transiting to a specific state are met, we want to change the state automatically.

Create an Entity command without a result:

(entity, db, ctx) =>
{
if (entity.CanChangeState(EntityStatus.DesiredState))
{
entity.Status = EntityStatus.DesiredState;
}
}

We have to check if an entity can change the state first, or else the command could fail and we would get an exception in the Error log.

Next step is to create an Entity event to trigger this command on entity Update. Now every time an entity record is updated, the business command is invoked and if the entity can change state to "DesiredState" it will.

Generate an URL

Sometimes you need to generate an URL, usually to provide a link in an notification email. To generate valid URL, use App.Urls class:

var dashboardUrl = App.Urls.Dashboards.MyDashboard.Generate();
var entityPage = App.Urls.EntityPages.Entity_detail.Generate(entity);
var latestFileRevision = App.Urls.Documents.Generate(entity.Document.GetLatestRevision());

Select items from db only with certain codebook value

db.SupplierSet.Where(t => t.Trade != null && task.Report.Trades.Contains(t.Trade))