Skip to main content
Version: Legacy

Adding New Page

This tutorial below explains backend and frontend development of adding a new page/process step by step.

UI / Frontend​

You'll generally be dealing with 4 files in src folder for a standard page which you can manage easily with JSON objects without coding javascript or typescript.

1) Menu Item Component​

views/Routes/YourPageName/YourPageNamePageConfig.js

It includes a generic component to be used in pages.js for menu. You'll just need to import related SamplePageConfig.js

import React from 'react';
import BasePage from '../../../components/BasePage';
import PageConfig from './SamplePageConfig'; // Edit this line

export default class SampleUserCls extends React.PureComponent {
render() {
return (
<BasePage config={PageConfig} />
)
}
}

2) Page Config​

views/Routes/YourPageName/YourPageNamePageConfig.tsx

Page structure specifying title, tabs, resource code, CRUD service urls, buttons and so on.

import { IPageConfig, Methods, pageConfigWrapper, Tables } from '../../../common/typeConfig';
import { getLocalizedText } from '../../../common/localizationManager';
import Constants from '../../../common/constants';

import SamplePage from '../../../entities/SamplePage';

const { LIST, GET, INSERT, UPDATE, DELETE, EXPORT, IMPORT } = Methods;

let pageConfig: IPageConfig = {
headerTitle: getLocalizedText('SEARCH_LIST_TITLE'),
tabs: [
{
title: 'User Title', // Tab title
type: SamplePage,
resourceCode: 'SamplePage_Resource_Code',
editOnModal: false, // Behaviour when clicked on a row of the table
list: {
url: `${Constants.ApiURL}/sampleService/list`
},
get: {
url: `${Constants.ApiURL}/sampleService/getById`
},
insert: {
url: `${Constants.ApiURL}/sampleService/insert`
},
update: {
url: `${Constants.ApiURL}/sampleService/update`
},
delete: {
url: `${Constants.ApiURL}/sampleService/delete`
},
allowedMethods: // Allowed buttons/actions of the table
[
LIST,
GET,
INSERT,
UPDATE,
DELETE,
IMPORT,
EXPORT,
DUPLICATE
],
},
],
};

export default pageConfigWrapper(pageConfig);

3) Model Class​

entities/YourPageNamePage.ts

Simply consists of the model class as JSON specifying components, groupings, rules, validations and so on.

import TableFormatters from '../common/TableFormatters';
import { Regexes } from '../common/validations';
import { IType, ComponentType, Visibility, typeConfigWrapper, IGroup, CaseStyles, LabelPositions } from '../common/typeConfig';
import { ParameterOptionTemplate } from '../common/typePresets';
import { getLocalizedText } from '../common/localizationManager';

const { TABLE, FORM, FILTER, BG_FILTER, BG_FORM, FK_FILTER, FK_FORM } = Visibility;

// We have 3 groups to gather components in order to have a better look & feel
let group1: IGroup = { title: 'User Info', columnSize: { all: 4 } };
let group2: IGroup = { title: 'Job Info', columnSize: { all: 4 } };
let group3: IGroup = { title: 'Profile Picture', columnSize: { all: 4 } };

let type: IType = {
userId: {
label: getLocalizedText('userId'),
isPrimaryId: true, // Almost every page should have one primary key
typeKey: 'user',
onValueChanged: (value, type, initial, isFilter, that) => {
type.password.valRules.acceptEmptyStrings = value > 0; //
type.password.updateState(); // Refresh the component state
},
},
infoMessage: {
typeInd: ComponentType.LABEL,
labelPosition: LabelPositions.NONE,
visibility: [FORM],
columnSize: { all: 12 },
defaultValue: "This page is specially designed for Live Preview with many different examples"
},
userName: {
label: "Name", // static label without localization
typeInd: ComponentType.FORM_CONTROL,
forceCaseTo: CaseStyles.UPPER_CASE, // Make the letters uppercase as typed
visibility: [FORM, FILTER, TABLE],
group: group1, // dislay in group1
valRules: {
minLength: 2, // must be at least 2 char-long
maxLength: 30 // must be at most 30 char-long
}
},
userSurname: { // if typeInd not specified, it is FORM_CONTROL (text-input) by default
label: "Surname",
forceCaseTo: CaseStyles.UPPER_CASE,
visibility: [FORM, FILTER, TABLE], // display in FORM=insert/update, FILTER=search criteria, TABLE=grid
group: group1,
valRules: {
minLength: 2,
maxLength: 50
}
},
companyId: {
label: getLocalizedText('COMPANY_LABEL'), // Multi-language label. COMPANY_LABEL is a keyCode in coreParameters
typeInd: ComponentType.DROPDOWN_ASYNC,
visibility: [FORM], // display only in insert/update mode
group: group2,
valRules: {
minLength: 1
},
optionConfig: { // state how to fill the options
listUrl: 'coreCompany/list', // service url and method
getValue: (item) => '@{companyId}',
getLabel: (item) => '@{companyName}',
filterBy: (type, inputText) => ({
Criteria:
{
companyId: type.companyId.value || 0, // post current companyId if exists
companyName: inputText // and post typed text to search and filter
}
}),
}
},
userTitle: {
label: getLocalizedText('JOB_TITLE_IN_COMPANY_LABEL'),
typeInd: ComponentType.DROPDOWN,
visibility: [FORM],
group: group1,
optionConfig: {
...ParameterOptionTemplate, // fill with options from coreParameters which are currently in browser's cache
filterBy: (type, inputText) => ({
keyCode: 'USER_TITLE', // use this keyCode to filter
}),
},
},
email: {
label: getLocalizedText('EMAIL_LABEL'),
visibility: [TABLE, FILTER, FORM],
group: group1,
valRules: {
regex: Regexes.email, // use pre-defined regular expression to validate
minLength: 6,
maxLength: 50,
customErrMsg: getLocalizedText("ENTER_VALID_EMAIL"), // use this multi-lingual custom message for regex mismatch
}
},
password: {
label: getLocalizedText('PASSWORD_LABEL'),
visibility: [FORM],
group: group1,
exportConfig: {
excelConfig: {
exportable: false // don't include when excel downloaded/exported
},
valRules: {
regex: Regexes.password, // use pre-defined regular expression to validate
customErrMsg: "This is a custom message when regex is not validated instantly"
},
customProps: {
type: 'password' // special attribute to mask the value with *
}
},
gender: {
label: getLocalizedText("GENDER_LABEL"),
typeInd: ComponentType.DROPDOWN,
visibility: [FORM],
group: group1,
optionConfig: {
...ParameterOptionTemplate,
filterBy: (type, inputText) => ({
keyCode: 'GENDER',
}),
}
},
phoneNumber: {
label: getLocalizedText('TELEPHONE_LABEL'),
typeInd: ComponentType.PHONE_INPUT, // phone input with country code. But you must handle country code.
visibility: [FORM],
group: group1,
valRules: {
acceptEmptyStrings: true, // allow to be empty. But if not empty, validate by regex and others
minLength: 9,
maxLength: 16
}
},
ibanNumber: {
label: getLocalizedText('ibanNumber'),
visibility: [FORM],
group: group1,
maskPattern: "AA99 999 99999999999999999", // A represents letter and 9 represents digits.
forceCaseTo: CaseStyles.UPPER_CASE,
valRules: {
acceptEmptyStrings: true,
minLength: 20,
maxLength: 32,
},
},
taxNumber: {
label: "Tax Number",
visibility: [FORM],
group: group2,
valRules: {
exactLength: 10 // must be exactly 10 char-long
}
},
countriesServed: {
label: "Countries Served",
typeInd: ComponentType.MULTIPLE_SELECT, // Multiple choice dropdown
visibility: [FORM],
group: group2,
optionConfig: {
...ParameterOptionTemplate,
filterBy: (type, inputText) => ({
keyCode: 'COUNTRY'
}),
},
},
cityId: {
label: getLocalizedText('CITY_LABEL'),
typeInd: ComponentType.DROPDOWN,
visibility: [FORM, FILTER],
group: group2,
optionConfig: {
...ParameterOptionTemplate,
filterBy: (type, inputText) => ({
keyCode: 'CITY',
}),
},
onValueChanged: (value, type, initial, isFilter, that) => { // fill countyId dropdown when city changes
if (!isFilter && type.countyId)
that.dataManagement.fillDropdownData(type.countyId); // trigger filling
if (isFilter && type.countyId_filter)
that.dataManagement.fillDropdownData(type.countyId_filter); // a second instance of countyId is created namely countyId_filter for filter. So fill it also
},
},
countyId: {
label: getLocalizedText('countyId'),
typeInd: ComponentType.DROPDOWN,
visibility: [FORM, FILTER],
group: group2,
optionConfig: {
...ParameterOptionTemplate,
filterBy: (type, inputText) => ({
keyCode: 'COUNTY',
parentValue: type.cityId.value || 0 // post cityId as well as keyCode to the web service
}),
},
filterOptionConfig: {
filterBy: (type, inputText) => ({
parentValue: type.cityId_filter.value || 0 // a second instance of cityId is created namely cityId_filter for filter. Get its value to post
}),
},
},
workStartDate: {
label: getLocalizedText('WORK_START_DATE'),
typeInd: ComponentType.DATE_PICKER,
visibility: [FORM, TABLE],
group: group2,
tableDataFormatter: TableFormatters.defaultDateFormatter // Format the date value in table display according
},
salary: {
label: 'Salary',
typeInd: ComponentType.NUMERIC_INPUT,
visibility: [FORM],
group: group2,
customProps: {
decimalPrecision: 3, // default is 2
thousandSeparator: ',', // default is dot
decimalSeparator: '.' // default is comma
},
valRules:{
customValidator: (value, typeElement) => {
return parseFloat(value as string) > 10000 ? 'Good Salary! Keep going on' : ''
}
}
},
note: {
label: "Note",
typeInd: ComponentType.TEXT_AREA,
labelPosition: LabelPositions.ABOVE_INPUT,
placeholder: "enter any note about the user or company",
visibility: [FORM],
group: group2,
customProps: {
rows: 5
}
},
picture: {
label: "Will not be shown",
typeInd: ComponentType.FILE_UPLOADER,
labelPosition: LabelPositions.NONE, // Display no label
visibility: [FORM],
group: group3,
customProps: {
accept: 'image/*' // allow only image files
}
},
status: {
label: getLocalizedText('statusName'),
typeInd: ComponentType.TOGGLE, // On-Off switch
visibility: [TABLE, FILTER, FORM],
group: group1,
defaultValue: 1, // Set to true/on. According to your data type you can set a boolean value
tableDataFormatter: TableFormatters.toggleFormatter // Change label to show in table according to the language
}
}
export default type;

4) Sidebar Menu & Navigation​

pages.js

Place its menu item.

Be aware of the code block below is simplified and shortened for better understanding.

import React from 'react';
import _ from 'lodash';
import { getLocalizedText } from './common/localizationManager';

const SamplePage = React.lazy(() => import('./views/Routes/SamplePage/SamplePageConfig'));

var PageInfos = {
// -- Addition Beg --
SamplePage: {
name: 'Sample Page Title',
url: `/SamplePage`,
component: SamplePage,
resourceCode: 'SamplePage_Resource_Code',
icon: 'icon-some',
}
// -- Addition End --
}

....

5) Here below is what you're gonna get by almost no-coding but only json-declaration.​

Outcome UI Sample Page view outcome

Automatic Validations and Corresponding Warning Messages UI Sample Page view with validation warnings

Search, List and the Table UI Sample Page's Search, List and the Table

With Data UI Sample Page with data

To form a customized UI page, check Custom Page for details.

Backend​

1) Model​

Be aware of the code block below is simplified and shortened for better understanding.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Microservice.DataLib.DBModels
{
public partial class SampleModelClass
{
[Column("userId")]
public int UserId { get; set; }

[Required]
[Column("userName")]
[StringLength(50)]
public string UserName { get; set; }

[Required]
[Column("userSurname")]
[StringLength(50)]
public string UserSurname { get; set; }

[Column("companyId")]
public int CompanyId { get; set; }

[Column("userTitle")]
public int? UserTitle { get; set; }

[Required]
[Column("email")]
[StringLength(80)]
public string Email { get; set; }

[Column("password")]
[StringLength(64)]
[HashedLogging] // Log the password as hashed
public string Password { get; set; }

[EncryptedPersistence] // Store the gender as encrypted since it is subject to GDPR
[IgnoreLogging] // Don't log the gender
[Column("gender")]
public short? Gender { get; set; }

....
}
}

2) DBContext​

Be aware of the code block below is simplified and shortened for better understanding.

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using CoreData.Common;
using CoreData.Infrastructure.Common;

namespace Microservice.DataLib.DBModels
{
public partial class Your_DBContext : ContextBase
{
public Your_DBContext()
{
}

public Your_DBContext(DbContextOptions<Your_DBContext> options)
: base(options)
{
}

public virtual DbSet<SampleModelClass> SampleModelClass { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);

if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseNpgsql(ConfigurationManager.GetConnectionString("PostgreSQL"), b => b.MigrationsAssembly("Microservice.API"));
}
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");

modelBuilder.Entity<SampleModelClass>(entity =>
{
entity.HasKey(e => e.userId)
.HasName("userId_pk");
});
}
}
}

3) Repository​

Microservice.DataLib.Repositories.SampleModelClassRepository.cs

using System.Collections.Generic;
using System.Linq;
using CoreData.Common;
using CoreType.Types;
using Microsoft.EntityFrameworkCore;
using Microservice.DataLib.DBModels;
using BaseRepository = Microservice.DataLib.Common.BaseRepository;
using CoreHelper = CoreData.Common.Helper;

namespace Microservice.DataLib.Repositories
{
public partial class SampleModelClassRepository : BaseRepository
{
public PaginationWrapper<SampleModelClass> List(RequestWithPagination<SampleModelClass> entity)
{
PaginationWrapper<SampleModelClass> res = new PaginationWrapper<SampleModelClass>();

using (var context = GetDbContext<Your_DBContext>())
{
res.List = context.Set<SampleModelClass>()
.AsNoTracking() // For EF Core performance, use this
.AddFilters(entity) // One of our extension methods that places "where" conditions
.AddSortings(entity)
.ToPaginatedList(entity);

return res;
}
}

public SampleModelClass GetById(SampleModelClass entity)
{
using (var context = GetDbContext<Your_DBContext>())
{
return context.Set<SampleModelClass>().Where(x => x.SampleModelClassId == entity.SampleModelClassId).FirstOrDefault();
}
}

public SampleModelClass Save(SampleModelClass entity)
{
using (var context = GetDbContext<Your_DBContext>())
{
context.Set<SampleModelClass>().Update(entity);
context.SaveChanges();
return entity;
}
}

public List<ResponseWrapper> BulkSave(List<SampleModelClass> dataList)
{
return CoreHelper.BulkSave(dataList, Save);
}

public bool Delete(SampleModelClass entity)
{
using (var context = GetDbContext<Your_DBContext>())
{
context.Set<SampleModelClass>().Remove(entity); // For soft delete, you can check "FAQ Backend"
context.SaveChanges();
return true;
}
}

}
}

4) Validator​

This is an example of FluentValidation. It is used in Controller methods insert and update.

Be aware of the code block below is simplified and shortened for better understanding.

Microservice.DataLib.Validators.SampleClassModelValidator.cs

using Microservice.DataLib.DBModels;
using CoreData.Validators;
using FluentValidation;

namespace Microservice.DataLib.Validators
{
public class SampleClassModelValidator : AbstractValidator<User>
{
public SampleClassModelValidatorValidator()
{
RuleFor(x => x.UserId)
.NotNull();

RuleFor(x => x.UserName)
.NotNull()
.NotEmpty()
.MinimumLength(2)
.MaximumLength(50);

RuleFor(x => x.UserSurname)
.NotNull()
.NotEmpty()
.Length(2, 50);

RuleFor(x => x.CompanyId)
.NotNull()
.NotEqual(0);

RuleFor(x => x.Email)
.EmailAddress()
.MaximumLength(80);

RuleFor(x => x.IdentificationNo)
.NotNull()
.MaximumLength(11);

RuleFor(x => x.Password)
.MaximumLength(64)
.Matches("some regex here");

RuleFor(x => x.Password)
.Equal(x => x.PasswordAgain);

...
}
}
}

Please visit FluentValidation official web site for details.

5) Controller​

Microservice.API.Controllers.SampleModelClassController.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CoreData;
using CoreData.CacheManager;
using CoreData.Validators;
using CoreSvc.Common;
using CoreSvc.Filters;
using CoreType.Types;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microservice.DataLib.DBModels;
using Microservice.DataLib.Validators;
using Microservice.DataLib.Repositories;
using CoreHelper = CoreData.Common.Helper;
using AdminHelper = Admin.Data.Common.Helper;

namespace Microservice.API.Controllers
{
[Authorize]
[Route("[controller]")]
[Resources("SamplePage_Resource_Code")] // It should be compatible with pages.js and YourPageNamePageConfig.tsx
public class SampleModelClassController : BaseController
{
private readonly SampleModelClassRepository _mainRepository = new SampleModelClassRepository();
private readonly SampleModelClassValidator _SampleModelClassValidator = new SampleModelClassValidator();
private readonly HubContext _hubContext;

public SampleModelClassController(HubContext hubContext)
{
_hubContext = hubContext;
}

[HttpPost("list")]
[ClaimRequirement(ActionType.List)]
public ResponseWrapper List([FromBody] RequestWithPagination<SampleModelClass> request)
{
ResponseWrapper genericResponse = new ResponseWrapper();

genericResponse.Data = _mainRepository.List(request);
genericResponse.Message = DistributedCache.Get(Messages.PROCESS_SUCCESSFUL);
genericResponse.Success = true;
return genericResponse;
}

[HttpPost("getById")]
[ClaimRequirement(ActionType.GetRecord)]
public ResponseWrapper GetById([FromBody] SampleModelClass request)
{
ResponseWrapper genericResponse = new ResponseWrapper();

genericResponse.Data = _mainRepository.GetById(request);
genericResponse.Message = DistributedCache.Get(Messages.PROCESS_SUCCESSFUL);
genericResponse.Success = true;

return genericResponse;
}

[HttpPost("insert")]
[ClaimRequirement(ActionType.Insert)]
public ResponseWrapper Insert([FromBody] SampleModelClass request)
{
_SampleModelClassValidator.ValidateAndThrow(request); // FluentValidation
return Save(request);
}

[HttpPost("update")]
[ClaimRequirement(ActionType.Update)]
public ResponseWrapper Update([FromBody] SampleModelClass request)
{
_SampleModelClassValidator.ValidateAndThrow(request); // FluentValidation
return Save(request);
}

private ResponseWrapper Save(SampleModelClass request)
{
ResponseWrapper genericResponse = new ResponseWrapper();

SampleModelClass res = _mainRepository.Save(request);

if (CoreHelper.GetPrimaryIdVal(res) > 0)
{
genericResponse.Data = res;
genericResponse.Message = DistributedCache.Get(Messages.PROCESS_SUCCESSFUL);
genericResponse.Success = true;
}
else
genericResponse.Message = DistributedCache.Get(Messages.PROCESS_FAILED);

return genericResponse;
}

[HttpPost("bulkSave")]
[ClaimRequirement(ActionType.Import)]
public ResponseWrapper BulkSave([FromBody] RequestWithExcelData<SampleModelClass> request)
{
ResponseWrapper genericResponse = new ResponseWrapper();

if (request != null)
{
Task.Run(() => BulkSaveAsync(request, _mainRepository.BulkSave, HttpContext.Request));

genericResponse.Message = DistributedCache.Get(Messages.PROCESS_SUCCESSFUL); // Multilanguage message
genericResponse.Success = true;
}
else
genericResponse.Message = DistributedCache.Get(Messages.PROCESS_FAILED);

return genericResponse;
}

private async Task BulkSaveAsync<T>(RequestWithExcelData<T> request, Func<List<T>, List<ResponseWrapper>> BulkSave, HttpRequest httpRequest)
{
var notification = await AdminHelper.BulkSaveWithNotificationAsync(request, BulkSave, Session, httpRequest.GetDisplayUrl());
await _hubContext.Send(Session, notification, SocketActionType.EXCEL_IMPORT);
}

[HttpPost("delete")]
[ClaimRequirement(ActionType.Delete)]
public ResponseWrapper Delete([FromBody] SampleModelClass request)
{
ResponseWrapper genericResponse = new ResponseWrapper();

if (_mainRepository.Delete(request))
{
genericResponse.Message = DistributedCache.Get(Messages.PROCESS_SUCCESSFUL);
genericResponse.Success = true;
}
else
genericResponse.Message = DistributedCache.Get(Messages.PROCESS_FAILED);

return genericResponse;
}
}
}