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 ServerSetValue Type
to: Text - Set
Name
to: SMTP PortSetType
to: NumberSetDefault value
to: 587 - Set
Name
to: SMTP LoginSetType
to: Text - Set
Name
to: SMTP PasswordSetType
to: Text - Set
Name
to: SMTP Email SetType
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
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.
- Create an "Deleted" bool attribute on the entity
- Create an entity command "Delete entity".
(entity , db, ctx) =>
{
entity.Deleted == true;
} - Add this command to desired locations - Action button on an entity Detail page, command to a List page or data grids...
- 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))