Simple Page

Page Description

In this article, the step-by-step process of creating the following webpage will be described:

The webpage consists of 2 functional parts. The main part is the Data Table that displays all Customers. The other part is the Modal that pops up when the Icon of one of the rows is clicked displaying a Form to edit the data of the corresponding Customer.

Data Table

The Data Table of the webpage has the columns "ID", "Name", "Street", "Postcode", "City" as well as a Column containing a clickable icon in the far left. Additionally, it has a search bar to facilitate searching for a specific Customer.

Editor

The Editor is a Modal that mainly consists of a Form with 5 Text Boxes to display and edit "Customer Code", "Customer Name", "Street", "Postcode" and "City". In the Footer, there is an "Update" Button to save the changes made to the data.

How to Create

Required Files and their General Structure

Since we are using the MVC Pattern (see Architecture Pattern), we require a Model file, a View file and a Controller file. For this example, we create a Controller "ExampleController.cs", a Model "ExampleCustomerModel.cs" and the View file "Customers.cshtml", which will be explained in detail below.

Controller file

The Controller file "ExampleController.cs" is stored in the "Controllers" folder (together with all other Controllers of the project). The Controller class needs to inherit from the class "BaseController". Addtionally, the Key Words "[Authorize]" and "[AuthorizeRole]" are put above the Signature to include Authorization features. So in total, the outer part of the Controller class looks like this:

namespace Advantech.Ctos.Web.Controllers
{
    [Authorize]
    [AuthorizeRole]
    public class ExampleController : BaseController
    {
        ...
    }
    
}    

Model file

The Model file "ExampleCustomerModel.cs" is stored in a new folder "Example" in the Business directory of the project. The Model class needs to inherit from the class "BaseModel". So in total, the outer part of the Model class looks like this:

namespace Advantech.Ctos.Business.Example
{
    public class ExampleCustomerModel : ModelBase
    {
        ...
    }
    
}

View file

The View file "Customers.cshtml" is stored in a new folder "Example" in the "Views" folder of the project (that gets automatically created when the Controller is created). The View file needs to be linked to the Model file which is why it needs to start with the following line:

@model Advantech.Ctos.Business.Example.ExampleCustomerModel

Furthermore, it is divided into 2 sections: the Content section (containing the <div> tags, etc) and the Script section (containing the JavaScript functions).

The Script section is located at the end of the View file and is identified by the following tag:

<script type="text/javascript">
    ...
</script>

So, in total, the View file looks like this:

@model Advantech.Ctos.Business.Example.ExampleCustomerModel

...

<script type="text/javascript">
    ...
</script>

Model Structure

In this context, "Model Structure" refers to the main components of the Model and their relation to each other without taking the methods of the Model into consideration. The Model Structure needs to match the View hierarchy. Because the View mainly consists of a Data Table, the Model needs to have 1 List Property, which will be named "CustomerList" in this example:

public List<CustomerItem> CustomerList { get; set; }

"CustomerList" is set to be public and needs to have the standard Getter and Setter functions, so that it can be accessed. The objects that it stores are of data type "CustomerItem", which is a custom made class that is intended to represent a Customer with all the details that are relevant to this page. It is defined by the end of the file "ExampleCustomerModel.cs" (outside of the "ExampleCustomerModel" class) in the following way:

public class CustomerItem
{
    public string CustomerId { get; set; }

    public string CustomerName { get; set; }

    public string Street { get; set; }

    public string Postcode { get; set; }

    public string City { get; set; }
}

It has 5 simple string Properties "CustomerId", "CustomerName", "Street", "Postcode", and "City".

Finally, a Constructor needs to be implemented that contains the initialization of all List Properties and Object Properties, which is in this case only "CustomerList":

public ExampleCustomerModel()
{
    CustomerList = new List<CustomerItem>();
}

The class "CustomerItem" does not require a Constructor as it does not have Properties that have a List or an Object as data type.

View Structure (Customers)

In this context, "View Structure" refers to the components of the Content section (without the Script section) of the View file. In the following, the single components will be described:

Page Title

The Page Title refers to the uppermost section of the webpage that contains the title of the webpage. It is identified by the class "content-title".

In this example, the title of the webpage is "Advantech Customers", so the code for the Page Title looks like this:

<div class="content-title">Advantech Customers</div>

Toolbar

The Toolbar refers to the section of the webpage that is located just below the Page Title and contains the buttons of the webpage. It is identified by the class "content-action" and the Buttons are recognized by the class "btn".

In this example, the webpage has 2 Buttons "Save" and "Delete" whose functionality should not be implemented in this example, thus they do not require an id. The "Save" Button should be grey in color but the color should only be applied to its margins which is rendered by using the class "btn-outline-secondary". The "Delete" Button should be red in color where the color is also only applied to the margins, which is rendered by the class "btn-outline-danger". So the overall code looks like this:

<div class="content-action">
    <a class="btn btn-outline-secondary">Save</a>
    <a class="btn btn-outline-danger">Delete</a>
</div>

Main Body

The Main Body refers to the main section of the webpage that contains the functionalities of the webpage. It is identified by the class "content-body".

In this example, the Main Body of this webpage only consists of a Data Table, so the <table> tag is used where the opening tag contains an id "tblCustomer" as well as the class= "display" (to adopt the standard design for Tables in the application). Else, a Table needs to contain a table head (<thead>) and a table body (<tbody>), which makes the code look like this:

<div class="content-body">
    <table id="tblCustomer" class="display">
        <thead>
            ...
        </thead>
        <tbody>
            ...
        </tbody>
    </table>
</div>

The table head contains the column headers. Because all of them should be displayed in a single row, they are encapsulated by a <tr> tag. Every column header is encapsulated by the <th> tag. With the column headers being "ID", "Name", "Street", "Postcode", and "City", the code for the table head would look like this:

<thead>
    <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Street</th>
        <th>Postcode</th>
        <th>City</th>
    </tr>
</thead>

However, the table of this webpage additionally has a column containing an icon. Icons are identified by <i> tags. The icon of this particular column is a list icon taken from Font Awesome, which is identified by the class "fa-list-alt". Because it should be a solid icon, it should also include the class "fa-solid". So the code for the icon itself looks like this:

<i class="fa-solid fa-list-alt"></i>

This line of code needs to be inserted in an additional <th> tag at the very beginning of the table head. So the overall code for the table head looks like this:

<thead>
    <tr>
        <th><i class="fa-solid fa-list-alt"></i></th>
        <th>ID</th>
        <th>Name</th>
        <th>Street</th>
        <th>Postcode</th>
        <th>City</th>
    </tr>
</thead>

The table body should list all the Work Orders that can be found in the "CustomerList" of the Model, i.e. every item in the "CustomerList" has an own row in the table. Since the number of rows depend on the number of items in "CustomerList" (which cannot be known in advance), the program should create a row for each item in "CustomerList". For this purpose, a "foreach" statement is used that does exactly that:

<tbody>
    @foreach (var item in Model.CustomerList)
    {
        <tr>
            ...
        </tr>
    }
</tbody>

Similar to how the <th> tag was used to specify the columns in the table head, now the <td> tag is used to specify the columns in the table body. The number and content of these <td> tags need to match the headers specified for the table head. Using the dot operator on the "item" variable of the loop, the respective properties of the Work Order are accessed: "CustomerId" for the "ID" column, "CustomerName" for the "Name" column, and so on.

Because the rows of the icon column all contain the same icon, the same code that was used for the header can be used here as well. However, because the icon in the rows should trigger an event when clicked, they additionally include an <a> tag that contains the class "go-detail" as well as a reference to the "CustomerId" Property of the respective item as data value ("data-value") to be able to address them in the Script section, which will be explained in detail later.

Overall, the code for the table body looks like this:

<div class="content-body">
    <table id="tblCustomer" class="display">
        <thead>
            <tr>
                <th><i class="fa-solid fa-list-alt"></i></th>
                <th>ID</th>
                <th>Name</th>
                <th>Street</th>
                <th>Postcode</th>
                <th>City</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model.CustomerList)
            {
                <tr>
                    <td><a class="go-detail" data-value="@item.CustomerId"><i class="fa-solid fa-list-alt"></i></a></td>
                    <td>@item.CustomerId</td>
                    <td>@item.CustomerName</td>
                    <td>@item.Street</td>
                    <td>@item.Postcode</td>
                    <td>@item.City</td>
                </tr>
            }
        </tbody>
    </table>
</div>

This is the code for the Data Table in the main body, however, the webpage also contains a Modal. Because the Modal is not part of the webpage itself but is an own window, another View file is created specifically for this section. The View file is named "CustomerDetail.cshtml" and using this name, it is being referenced using the HTML Helper for Modals, like this:

@Html.Modal("CustomerDetail", "Customer Detail", "420")
  1. The first parameter is the name of the View file

  2. The second parameter is the name of the title that should be displayed in the Header of the Modal

  3. The third parameter specifies the height/width of the Modal in px

View Structure (CustomerDetail)

The "CustomerDetail.cshtml" is created as described above for general View files. Its general structure is also the same as the one for "Customers.cshtml", however, there are 2 differences. First, it does not link to the whole Model file "ExampleCustomerModel" but only to the class "CustomerItem" that is defined within "ExampleCustomerModel" file (because the Editor is specifically for this type of object. Second, the Content section needs to be encapsulated within an if-statement because it should only be possible to edit if the selected "CustomerItem" is not empty. So the View file "CustomerDetail" generally looks like this:

@model Advantech.Ctos.Business.Example.CustomerItem

@if (Model != null)
{
    ...
}


<script type="text/javascript">

    ...
    
</script>

The Content section of "CustomerDetail.cshtml" only contains a Form. Similar to Tables, Forms are also done with <table> tags. However, this table is not made up of a head and a body part. It is made up of several rows (<tr>) that each have a Label and a Control Input (both encapsulated by <td> tags).

The Labels of this form should all have a width of 150px, so they all have the class "field150". For example, the Label for "Customer Name" would look like this:

<td class="field150">Customer Name</td>

All of the Control Inputs are Text Boxes, so they are done by using the HTML Helper for Text Boxes. Also, each of them should be of 100% width, so they all have the class "text100". For example, the Control Input for "Customer Name" would look like this:

<td>@Html.TextBoxFor(m => m.CustomerName, new { @class = "text100" }) </td>

The Property "CustomerId" (labeled "Customer Code") should not be editable but only displayed. For this purpose, it is additionally set to be "readonly":

<td>@Html.TextBoxFor(m => m.CustomerId, new { @class = "text100", @readonly = "readonly" }) </td>

Overall, the code for the Form looks like this:

<table class="w-100">
    <tr>
        <td class="field150">Customer Code</td>
        <td>@Html.TextBoxFor(m => m.CustomerId, new { @class = "text100", @readonly = "readonly" }) </td>
    </tr>
    <tr>
        <td class="field150">Customer Name</td>
        <td>@Html.TextBoxFor(m => m.CustomerName, new { @class = "text100" }) </td>
    </tr>
    <tr>
        <td class="field150">Street</td>
        <td>@Html.TextBoxFor(m => m.Street, new { @class = "text100" }) </td>
    </tr>
    <tr>
        <td class="field150">Postcode</td>
        <td>@Html.TextBoxFor(m => m.Postcode, new { @class = "text100" }) </td>
    </tr>
    <tr>
        <td class="field150">City</td>
        <td>@Html.TextBoxFor(m => m.City, new { @class = "text100" }) </td>
    </tr>
</table>

The table tag includes the class "w-100" to make it use up 100% of the available width.

To be able to submit the changes done in the Form, the above code needs to be encapsulated by the following:

@using(Ajax.BeginForm("updateCustomer", "example", null,
        new AjaxOptions()
        {
            HttpMethod = "POST",
            OnBegin = "onCustomerBegin",
            OnSuccess = "onCustomerSuccess"
        },
        new { @id = "frmCustomerDetail" }))
{
    ...
}

From bottom to top: "frmCustomerDetail" is the id that will be used to address this Form in the Script section, "onCustomerSuccess" is the name of the function in the Script section that handles a successful submission of the Form, "onCustomerBegin" is the name of the function that starts the process of submitting the Form, "POST" is the HttpMethod that is used to send the Action, "updateCustomer" is the name of that Action in the Controller file and "Example" refers to the name of the Controller (the first part of the name "ExampleController").

The detailed workings of the Saving process will be described at the end of this article.

So the overall Content section of the Modal View file looks like this:

@if (Model != null)
{

<div class="modal-body">
    @using(Ajax.BeginForm("updateCustomer", "example", null,
            new AjaxOptions()
            {
                HttpMethod = "POST",
                OnBegin = "onCustomerBegin",
                OnSuccess = "onCustomerSuccess"
            },
            new { @id = "frmCustomerDetail" }))
    {
        <table class="w-100">
            <tr>
                <td class="field150">Customer Code</td>
                <td>@Html.TextBoxFor(m => m.CustomerId, new { @class = "text100", @readonly = "readonly" }) </td>
            </tr>
            <tr>
                <td class="field150">Customer Name</td>
                <td>@Html.TextBoxFor(m => m.CustomerName, new { @class = "text100" }) </td>
            </tr>
            <tr>
                <td class="field150">Street</td>
                <td>@Html.TextBoxFor(m => m.Street, new { @class = "text100" }) </td>
            </tr>
            <tr>
                <td class="field150">Postcode</td>
                <td>@Html.TextBoxFor(m => m.Postcode, new { @class = "text100" }) </td>
            </tr>
            <tr>
                <td class="field150">City</td>
                <td>@Html.TextBoxFor(m => m.City, new { @class = "text100" }) </td>
            </tr>
        </table>
    }

</div>
    <div class="modal-footer">
        <a id="btn-customer-update" class="btn btn-primary">Update</a>
    </div>
}

How to Populate Tables with Data

Now that the Structure of everything is set, we can start implementing the different functionalities of the webpage. The first and most essential functionality to be implemented is the population of the Data Table because the other ones rely on it.

Controller file

Every functionality requires an Action in the Controller that handles its request. Since the Population of the Data Table should happen every time the page loads, the Action that is associated with this functionality is the same one that "gets" the page in the first place. Accordingly, an HTTPGET method is used and the Action is named according to what we want to be displayed as the URI of the basic webpage (in this case "Customers").

Within the Action, a new instance of the Model is created by calling the constructor of the same name "ExampleCustomerModel" and storing it in a variable "model". Using this variable "model", the method is called that gets all the Customers to be displayed in the List "GetCustomers", whose implementation will be described in the following section. Finally, the Action returns the View by using the function "PartialView" passing the "model" as parameter.

[HttpGet]
public ActionResult Customers()
{
    var model = new ExampleCustomerModel();
    model.GetCustomers();
    return PartialView(model);
}

Because this Action is responsible for the loading of the page, we additionally have to add it in the Navigation Editor of the application (under "Settings" in the Toolbar). There, click "Add Menu" and type in the Controller and Action name (here: "example/customers") in "Action Route". As "Group/Directory" select any of the directories, e.g. "Settings" (usually a new Group would be created beforehand that is named after the Controller "Example" but since this is just an example, we just take one of the existing Groups). Then type the name that should be displayed in the Navigation for this page (for example "Example Customer"). Do not forget to check "Set Enabled ".

After clicking "Save" in the Footer of the Modal, click the "Save" Button on the Navigation page.

Model file

Now we implement the function in the Model that is actually doing the Population of the Tables "GetCustomers". It should be set to public, does not have any return value (since it only populates "CustomerList") and does not require any parameters. Thus, the signature looks like this:

public void GetCustomers()

The data that should be populated in the Data Table is represented by the "CustomerList" Property, whose items are of the type "CustomerItem", which is specified in the same Model file. The data for "CustomerItem" are taken from (but not the same as) the "Customers" data repository.

So the first thing that needs to be done is to fetch the Customers. For this purpose, the "Get" function is used. "Get" has 4 arguments, however, only the first one is relevant for our purposes, which is the "filter" that specifies which items to fetch and which to leave out. In this case, we only want Customers whose "Street" Property is not empty.

The data repository where the Orders are taken from is "UnitWork.Customers" and the Customers should be stored in a variable "custList", thus the function call is implemented like this:

var custList = UnitWork.Customers.Get(x => x.Street != null);

Next, the "CustomerItem" Objects need to be created from the "Customer" data and inserted into the "CustomerList" one by one. For this purpose, a foreach loop is used that iterates through the fetched Customers and for each one, it creates a new "CustomerItem" assigning the right data to the respective Properties and adding it to "CustomerList". The loop is implemented like this:

foreach(var item in custList)
{
    CustomerList.Add(new CustomerItem()
    {
        CustomerId = item.ID,
        CustomerName = item.Name,
        Street = item.Street,
        Postcode = item.PostCode,
        City = item.City,
    });
}

Taking all the above into consideration, the "GetCustomers" method is implemented like this:

public void GetCustomers()
{
    var custList = UnitWork.Customers.Get(x => x.Street != null);

    foreach(var item in custList)
    {
        CustomerList.Add(new CustomerItem()
        {
            CustomerId = item.ID,
            CustomerName = item.Name,
            Street = item.Street,
            Postcode = item.PostCode,
            City = item.City,
        });
    }
}

View file

Now, the only thing left to do is to render the Data Table in the View file. Any functionality in the layout is implemented in the Script section of the View file. Since the structure of the Data Tables (i.e. the data) takes time before it is ready (because of the loops), the JavaScript that specifies the layout of the DataTable is encapsulated by the "$(document).ready" function, i.e. it only gets rendered after the structure is done. The Data Table has the id "tblCustomer" and the only feature that should be different from the default layout is that the it should not contain paging. (It should contain a search bar but since a data table contains a search bar by default, it does not need to be specified). Thus, the overall code looks like this:

    $(document).ready(function () {

        //configure table
        $("#tblCustomer").dataTable({
            paging: false,
        });

    })

How to Get Detailed Data

The next functionality that needs to be implemented is the Population of the Editor Form after a Customer in the Data Table has been selected.

Controller file

This functionality requires an Action of its own. It is very similar to the first Action in the sense that it also uses HTTPGET as method. It differs from the first Action in that it requires a parameter for the id of the selected Customer and only returns the Detail View, not the whole page. For this reason, the first parameter of "PartialView" is "CustomerDetail" (not "Customers") and the second parameter is "model.GetDetailCustomer(id)" (not "model"). Naming the Action "GetCustomerDetail", it is implemented like this:

[HttpGet]
public ActionResult GetCustomerDetail(string id)
{
    var model = new ExampleCustomerModel();
    return PartialView("CustomerDetail", model.GetDetailCustomer(id));
}

Model file

Now the function "GetDetailCustomer", that is used in the Action "GetCustomerDetail", will be implemented. Since it depends on the specific Customer that has been selected, it needs a parameter "customerId". Its return value is an object of type "CustomerItem".

First, it fetches the Customer corresponding to the selected row into the variable "custInfo". It does so by using the "GetById" function passing "customerId" as parameter. Then, it returns a new instance of "CustomerItem" where each Property of "custInfo" is assigned the corresponding property of "CustomerItem".

Overall, the function is implemented like this:

public CustomerItem GetDetailCustomer(string customerId)
{
    var custInfo = UnitWork.Customers.GetByID(customerId);

    return new CustomerItem()
    {
        CustomerId = custInfo.ID,
        CustomerName = custInfo.Name,
        Street = custInfo.Street,
        City = custInfo.City,
        Postcode = custInfo.PostCode,
    };
}

View file

The whole process of displaying the data of a specific Work Order in the Detail section is triggered by clicking the icon of a specific row in the first column of the Data Table. Since this functionality is also dependent on the Data Table, the JavaScript function is also encapsulated by the "$(document).ready" function together with the implementation of the Data Table.

The icon has the class "go-detail, so the click event function is applied on this class where the functionality is implemented in the following way:

$(".go-detail").click(function () {
    $.openModal("example/getCustomerDetail/" + $(this).attr("data-value"), "CustomerDetail");
})

The function "$.openModal" is used because the View "CustomerDetail.cshtml" should be rendered in a pop-up Modal. "$.openModal" has 2 arguments:

  1. "url" requires the URI of the appropriate Action together with all necessary parameters:

    • Because all Actions of this webpage are located in the "ExampleController" file, the first part of the URI is "example/"

    • Because the Action that "gets" the Partial View is named "getCustomerDetail", the second part of the URI is "getCustomerDetail/"

    • "GetCustomerDetail" requires a parameter "id" that specifies the specific Customer that should be displayed. Because this part of the URI is variable depending on which row was clicked, "+ $(this).attr("data-value")" is added to the URI string using "data-value" as reference that was already set to the respective CustomerId of the row in the implementation of the Data Table (see View Structure (Customers)).

  2. "vname" requires the name of the Modal that should be opened. In this case, it is "CustomerDetail".

How to Edit Data

The last and the most complex functionality that needs to be implemented is the one that takes care of the saving process after a change has been made to a specific Customer in the Editor Modal.

Controller file

When the AJAX header for the submission form has been created in the "CustomerDetail.cshtml" View file, it has been mentioned that "UpdateCustomer" is the Action corresponding to the Saving process. Since it is a functionality that updates data in the database, the HTTPPOST method is used. It also requires a parameter that passes the "CustomerItem" instance with the updated data. The Action returns a JSON object that contains a call of the function "UpdateCustomer" (a method in the Model that happens to have the same name as the Action and that willl be explained in detail in the next section). Because the function is implemented in the Model file, a new instance of the Model "ExampleCustomerModel" needs to be created beforehand to access it. Overall, the Action "UpdateCustomer" is implemented like this:

[HttpPost]
public ActionResult UpdateCustomer(CustomerItem item)
{
    var model = new ExampleCustomerModel();
    return Json(new { succeed = model.UpdateCustomer(item) }, JsonRequestBehavior.AllowGet);
}

Model file

The function "UpdateCustomer" evaluates if the Update has been successful or not, thus returns a boolean value. It requires the "CustomerItem" with the updated data as parameter as well (it will be called "data").

First, it fetches the corresponding Customer from the database using the "GetById" method and stores it in a variable "custInfo". It passes the value of the Property "CustomerId" of "data" as input parameter because this is the one that equals the id.

Then, the Properties of "custInfo" are assigned the corresponding (updated) values from the passed "CustomerItem" object.

After that, the "Update" method is applied on the Customers data repository passing the updated "custInfo" object as parameter.

Finally, it returns true, i.e. it only returns true when all of the above has been successfully executed without error.

Overall, the "UpdateCustomer" function is implemented like this:

public bool UpdateCustomer(CustomerItem data)
{
    var custInfo = UnitWork.Customers.GetByID(data.CustomerId);

    custInfo.Name = data.CustomerName;
    custInfo.Street = data.Street;
    custInfo.PostCode = data.Postcode;
    custInfo.City = data.City;

    UnitWork.Customers.Update(custInfo);

    return true;
}

View file

The whole process of saving an edited Work Order is triggered by clicking the "Update" Button in the footer of the Modal. When implementing the AJAX Header in the View file, it was also mentioned that there are two functions "onCustomerBegin" and "onCustomerSuccess" handling the submission process of the Form. So in total, the Saving process consists of three functionalities:

  1. the "Update" Button

  2. the "onCustomerBegin" function

  3. the "onCustomerSuccess" function

All three are implemented in the "CustomerDetail.cshtml" file.

The functionality of the "Update" Button is encapsulated by the "$(document).ready" function. It is an "on click" event that should trigger the submission of the form that has the id "frmCustomerDetail" (see View Structure (CustomerDetail)), thus the functionality is implemented like this:

$("#btn-customer-update").click(function () {
    $("#frmCustomerDetail").submit();
})

The functionality of the "onCustomerBegin" is NOT encapsulated by the "$(document).ready" function because it is an independent function.

While the submission process is on-going, the UI should be blocked. This is all that should be done when starting the saving process, thus the function is implemented like this:

function onCustomerBegin() { $.blockUI(); }

The functionality of the "onCustomerSuccess" is NOT encapsulated by the "$(document).ready" function because it is an independent function.

When the saving process has been successful, the modal should be closed and the page should be refreshed, so the updated data will be displayed. To evaluate if the process has been successful or not, the "$.response" function is used that evaluates the data, which is passed on as parameter, and executes the closing of the modal (using "$.closeModal" with the name of the modal "CustomerDetail" as parameter) and refreshing of the webpage (using "$.getContent" and "$.parseURL") only if the data succeeded. The whole function is implemented like this:

function onCustomerSuccess(data) {
    $.response(data, function () {
        if (data.succeed) {
            $.closeModal("CustomerDetail");
            $.getContent($.parseURL());
        }
    })
}

Registering Actions

As a final step, the Actions that have been implemented in the Controller need to be registered before the page can run. For this purpose, navigate to "Access Assignment" under "Settings", click the "Synch" Button, select "Example" in the "Resource Group" Dropdown List, click the "Check All" Button and finally click the "Save" Button. Now you can access the page with all its functionalities.

Last updated