]> source.dussan.org Git - vaadin-framework.git/commitdiff
Migrate BuildingVaadinApplicationsOnTopOfActiviti
authorErik Lumme <erik@vaadin.com>
Thu, 14 Sep 2017 13:14:22 +0000 (16:14 +0300)
committerErik Lumme <erik@vaadin.com>
Thu, 14 Sep 2017 13:14:22 +0000 (16:14 +0300)
documentation/articles/BuildingVaadinApplicationsOnTopOfActiviti.asciidoc [new file with mode: 0644]
documentation/articles/contents.asciidoc
documentation/articles/img/architecture.png [new file with mode: 0644]
documentation/articles/img/complexdomain.png [new file with mode: 0644]
documentation/articles/img/complexdomain_saving.png [new file with mode: 0644]
documentation/articles/img/complexdomain_saving2.png [new file with mode: 0644]
documentation/articles/img/customForms.png [new file with mode: 0644]
documentation/articles/img/process.png [new file with mode: 0644]
documentation/articles/img/views.png [new file with mode: 0644]

diff --git a/documentation/articles/BuildingVaadinApplicationsOnTopOfActiviti.asciidoc b/documentation/articles/BuildingVaadinApplicationsOnTopOfActiviti.asciidoc
new file mode 100644 (file)
index 0000000..d7c8fdb
--- /dev/null
@@ -0,0 +1,584 @@
+[[building-vaadin-applications-on-top-of-activiti]]
+Building Vaadin applications on top of Activiti
+-----------------------------------------------
+
+by Petter Holmström
+
+[[introduction]]
+Introduction
+~~~~~~~~~~~~
+
+In this article, we are going to look at how the
+http://www.activiti.org[Activiti] BPM engine can be used together with
+Vaadin. We are going to do this in the form of a case study of a demo
+application that is available on
+https://github.com/peholmst/VaadinActivitiDemo[GitHub]. The code is
+licensed under Apache License 2.0 and can freely be used as a foundation
+for your own applications.
+
+[[the-example-process]]
+The Example Process
+^^^^^^^^^^^^^^^^^^^
+
+The following process is used in the demo application:
+
+image:img/process.png[Example process]
+
+Compared to the capabilities of Activiti and BPMN 2.0, the above process
+is almost ridiculously simple. However, it allows us to test the
+following things:
+
+* *Process start forms*, i.e. forms that need to be filled in before a
+process instance is created.
+* *User task forms*, i.e. forms that need to be filled in before a task
+can be marked as completed.
+* Parallell tasks
+* Different candidate groups (i.e. groups whose users are potential
+assignees of a certain task)
+
+Here is a short walk-through of the process:
+
+1.  Before a new process instance is created, the reporter has to fill
+in a _Submit bug report form_.
+2.  Once the instance has been created, two tasks are created:
+* *Update bug report*: a manager assigns priority and target version to
+the report. Potential assignees are members of the *managers* group.
+* *Accept bug report*: a developer accepts the bug report. Potential
+assignees are members of the *developers* group.
+3.  Both of these tasks require the assignee to fill in a form before
+they can be completed: the _Update bug report form_ and _Accept bug
+report form_, respectively.
+4.  Once the tasks have been completed, a new task is created, namely
+_Resolve bug report_. Potential assignees are members of the
+*developers* group. Ideally, this task should automatically be assigned
+to whoever claimed the *Accept bug report* task, but currently this is
+not implemented.
+5.  Before the task can be completed, the assignee has to fill in the
+_Resolve bug report form_.
+6.  All tasks have been completed and the process instance ends.
+
+[[prerequisites]]
+Prerequisites
+^^^^^^^^^^^^^
+
+In order to get the most out of this article, you should already be
+familiar with both Vaadin and Activiti. If not, there is enough free
+material available on both products' web sites to get you started.
+
+The demo application is a standard Java EE 6 web application and can be
+deployed to any JEE 6 web container, such as
+http://tomcat.apache.org[Tomcat 7]. It uses an embedded in-memory
+http://www.h2database.com[H2 database] for storing data, which means
+that all your data will be lost when the server is restarted.
+
+http://www.eclipse.org/downloads/packages/eclipse-ide-java-ee-developers/heliossr2[Eclipse
+3.6] and the http://vaadin.com/eclipse[Vaadin plugin] was used to create
+the application. Both the project files and the third-party libraries
+are included in the source code repository. At this point, I recommend
+you to download the source code before continuing.
+
+Once you have Eclipse, Tomcat and Git properly installed and configured,
+you can follow the following instructions to get the demo application up
+and running:
+
+1.  Open a command line and clone the Git repository:
+`git clone git://github.com/peholmst/VaadinActivitiDemo.git`
+2.  Start up Eclipse.
+3.  From the *File* menu, select *Import*.
+4.  Select *Existing Projects into Workspace* and click *Next*.
+5.  In the *Select root directory* field, click the *Browse* button and
+locate the cloned Git repository directory.
+6.  In the list of projects, check *VaadinActivitiDemo* and click
+*Finish*.
+7.  In the *Project Explorer*, right-click on *VaadinActivitiDemo*,
+point to *Run As* and select *Run on Server*.
+8.  Select the Tomcat 7 server and click *Finish*.
+9.  Open a web browser and point it to
+_http://localhost:8080/VaadinActivitiDemo_.
+
+[[scope]]
+Scope
+^^^^^
+
+As Activiti has a huge amount of features, we are only going to look at
+a small subset of them in order to keep the scope of this article under
+control. More specifically, we are going to look at the following two
+questions:
+
+1.  How easy (or hard) is it to create custom-built forms using Vaadin
+and plug these into Activiti?
+2.  How easy (or hard) is it to combine process data from Activiti with
+other domain data from e.g. JPA?
+
+[[application-architecture]]
+Application Architecture
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+In this section, we are going to briefly discuss the architecture of the
+demo application on a general level and show how it has been implemented
+on more technical level. A simplified version of the architecture is
+illustrated here:
+
+image:img/architecture.png[Application architecture]
+
+[[the-h2-database]]
+The H2 Database
+^^^^^^^^^^^^^^^
+
+The H2 database is used in in-memory mode and will start when the
+process engine is initialized and stop when the engine is destroyed. All
+you have to do is specify some connection parameters when you
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/activiti.cfg.xml[configure
+Activiti] and the rest will be handled automatically.
+
+[[the-activiti-engine-and-process-definitions]]
+The Activiti Engine and Process Definitions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The Activiti engine is initialized and destroyed by a servlet context
+listener, like so:
+
+[source,java]
+....
+@WebListener
+public class ProcessEngineServletContextListener implements ServletContextListener {
+  @Override
+  public void contextInitialized(ServletContextEvent event) {
+    ProcessEngines.init();
+    deployProcesses();
+  }
+
+  @Override
+  public void contextDestroyed(ServletContextEvent event) {
+    ProcessEngines.destroy();
+  }
+
+  private void deployProcesses() {
+    RepositoryService repositoryService = ProcessEngines.getDefaultProcessEngine().getRepositoryService();
+    repositoryService.createDeployment()
+      .addClasspathResource("path/to/bpmn-document.bpmn20.xml")
+      .deploy();
+  }
+}
+....
+
+Once the process engine has been initialized, the context listener
+deploys the BPMN 2.0 process definitions to it. In other words, the
+Activiti process engine becomes available as soon as the web application
+starts and remains up and running until the application is stopped. All
+the Vaadin application instances use the same Activiti engine.
+
+[[the-vaadin-application]]
+The Vaadin Application
+^^^^^^^^^^^^^^^^^^^^^^
+
+The Vaadin application is designed according to the
+http://en.wikipedia.org/wiki/Model-view-presenter[Model-View-Presenter]
+(MVP) pattern and is implemented using
+https://github.com/peholmst/MVP4Vaadin[MVP4Vaadin]. This gives us the
+following benefits:
+
+* Clear separation between logic and UI (makes unit testing easier).
+* View navigation becomes easier (e.g. the breadcrumb bar shown in the
+demo screencast is a built-in part of MVP4Vaadin).
+
+The following diagram illustrates the different views and potential
+navigation paths between them:
+
+image:img/views.png[Application views and navigation]
+
+When the application is first started, the
+https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/login[Login
+View] is displayed in the main window. Once the user has logged on, the
+main window is replaced with the
+https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/main[Main
+View]:
+
+[source,java]
+....
+public class DemoApplication extends Application implements ViewListener {
+  // Field declarations omitted
+
+  @Override
+  public void init() {
+     createAndShowLoginWindow();
+  }
+
+  private void createAndShowLoginWindow() {
+    // Implementation omitted
+  }
+
+  private void createAndShowMainWindow() {
+    // Implementation omitted
+  }
+
+  @Override
+  public void handleViewEvent(ViewEvent event) {
+    if (event instanceof UserLoggedInEvent) {
+      // Some code omitted
+      createAndShowMainWindow();
+    } // Other event handlers omitted
+  }
+  // Additional methods omitted.
+}
+....
+
+The main view acts as a controller and container for a number of
+embedded views:
+
+* The
+https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/home[Home
+View] is the main menu. From here, you can navigate to the _Process
+Browser View_ and the _Identity Management View_.
+* The
+https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/processes[Process
+Browser View] contains a list of all the available process definitions.
+From this view, you can start new process instances. If a process has a
+start form, you can also navigate to the _User Form View_.
+* The
+https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/identity[Identity
+Management View] allows you to manage users and user groups.
+* The
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/tasks/UnassignedTasksViewImpl.java[Unassigned
+Tasks View] contains a list of all unassigned tasks. You can navigate to
+this view from any other view. From this view, you can assign tasks to
+yourself.
+* The
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/tasks/MyTasksViewImpl.java[My
+Tasks View] contains a list of all tasks currently assigned to you. You
+can navigate to this view from any other view. From this view, you can
+complete tasks. If a task has a form, you can also navigate to the _User
+Form View_.
+* The
+https://github.com/peholmst/VaadinActivitiDemo/tree/master/src/com/github/peholmst/vaadinactivitidemo/ui/forms[User
+Form View] is responsible for displaying the _User Task Forms_, e.g.
+before a new process instance is created or before a task is completed.
+The information about which form to show (if any) is specified in the
+BPMN process definition. *Please note that when we are talking about
+forms in this article, we are referring to the Acticiti form concept. Do
+not confuse this with Vaadin forms.*
+
+These views (or technically speaking their corresponding presenters)
+communicate directly with the Activiti engine. For example, the
+following snippet is taken from the
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/processes/ProcessPresenter.java[`ProcessPresenter`]
+class:
+
+[source,java]
+....
+@Override
+public void init() {
+  getView().setProcessDefinitions(getAllProcessDefinitions());
+}
+
+public void startNewInstance(ProcessDefinition processDefinition) {
+  try {
+    if (processDefinitionHasForm(processDefinition)) {
+      openFormForProcessDefinition(processDefinition);
+    } else {
+      getRuntimeService().startProcessInstanceById(processDefinition.getId());
+      getView().showProcessStartSuccess(processDefinition);
+    }
+  } catch (RuntimeException e) {
+    getView().showProcessStartFailure(processDefinition);
+  }
+}
+
+private List<ProcessDefinition> getAllProcessDefinitions() {
+  ProcessDefinitionQuery query = getRepositoryService().createProcessDefinitionQuery();
+  return query.orderByProcessDefinitionName().asc().list();
+}
+
+private RepositoryService getRepositoryService() {
+  return ProcessEngines.getDefaultProcessEngine().getRepositoryService();
+}
+
+private RuntimeService getRuntimeService() {
+  return ProcessEngines.getDefaultProcessEngine().getRuntimeService();
+}
+....
+
+The Main View also regularly checks if there are new tasks available and
+notifies the user if that is the case. The
+http://vaadin.com/addon/refresher[Refresher] add-on is used to handle
+the polling.
+
+[[some-notes-on-mvp4vaadin]]
+Some Notes on MVP4Vaadin
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Thanks to MVP4Vaadin, navigation between views is very simple. For
+example, the following code snippet is taken from the
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/main/components/WindowHeader.java[`WindowHeader`]
+component, a part of the Main View implementation:
+
+[source,java]
+....
+@SuppressWarnings("serial")
+private Button createMyTasksButton() {
+  Button button = new Button();
+  button.addListener(new Button.ClickListener() {
+    @Override
+    public void buttonClick(ClickEvent event) {
+      mainPresenter.showMyTasks();
+    }
+  });
+  button.addStyleName(Reindeer.BUTTON_SMALL);
+  return button;
+}
+
+@SuppressWarnings("serial")
+private Button createUnassignedTasksButton() {
+  Button button = new Button();
+  button.addListener(new Button.ClickListener() {
+    @Override
+    public void buttonClick(ClickEvent event) {
+      mainPresenter.showUnassignedTasks();
+    }
+  });
+  button.addStyleName(Reindeer.BUTTON_SMALL);
+  return button;
+}
+....
+
+The corresponding snippets from the
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/main/MainPresenter.java[`MainPresenter`]
+class are as follows:
+
+[source,java]
+....
+public void showUnassignedTasks() {
+  getViewController().goToView(UnassignedTasksView.VIEW_ID);
+}
+
+public void showMyTasks() {
+  getViewController().goToView(MyTasksView.VIEW_ID);
+}
+....
+
+[[custom-forms]]
+Custom Forms
+~~~~~~~~~~~~
+
+As you may already know, it is possible to use automatic form generation
+with Activiti, but the generated forms are not Vaadin based. In this
+article, we are going to use custom-built Vaadin forms instead. Even
+though this forces us to write Java code for each form we want to use,
+it gives us some advantages:
+
+* It is possible to have more complex forms with differnt kinds of
+components.
+* It is possible to tailor the appearance and look and feel of the forms
+to the user's needs.
+* It is easy to plug in other infrastructure services such as EJBs and
+JPA entities.
+
+The following approach is used to implement custom forms in the demo
+application:
+
+image:img/customForms.png[Custom forms]
+
+Here is a short walk-through of the most important classes:
+
+* The
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/util/UserTaskForm.java[`UserTaskForm`]
+interface is implemented by all custom forms. This interface defines
+several methods, the most interesting of which are the following:
+** `populateForm(...)`: This method populates the form with initial data
+retrieved from the Activiti form service.
+** `getFormProperties()`: This method creates a map of the form data
+that will be sent to the Activiti form service when the form is
+submitted.
+* The
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/util/UserTaskFormContainer.java[`UserTaskFormContainer`]
+is a class that contains user task forms. Each form can be accessed by a
+unique form key, which in turn is used in BPMN-documents to refer to
+forms. The main Vaadin application class is responsible for creating and
+populating this container. *Please note, that this container class has
+nothing to do with Vaadin Data Containers.*
+* The
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/forms/UserFormViewImpl.java[`UserFormViewImpl`]
+class (and its corresponding presenter) is responsible for looking up
+the correct form (by its form key), populating it, displaying it to the
+user and finally submitting it.
+
+[[some-code-examples]]
+Some Code Examples
+^^^^^^^^^^^^^^^^^^
+
+We are now going to look at some snippets from the demo application
+source code.
+
+First up is a method from the
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/tasks/MyTasksPresenter.java[`MyTasksPresenter`]
+class that is invoked when the user wants to open the form for a
+specific task:
+
+[source,java]
+....
+public void openFormForTask(Task task) {
+  String formKey = getFormKey(task);
+  if (formKey != null) {
+    HashMap<String, Object> params = new HashMap<String, Object>();
+    params.put(UserFormView.KEY_FORM_KEY, formKey);
+    params.put(UserFormView.KEY_TASK_ID, task.getId());
+    getViewController().goToView(UserFormView.VIEW_ID, params);
+  }
+}
+....
+
+The method checks if the task has a form and asks the view controller (a
+part of MVP4Vaadin) to navigate to the User Form View if that is the
+case. The task ID and form key is passed to the view as a map of
+parameters.
+
+The next code example is a method of the
+https://github.com/peholmst/VaadinActivitiDemo/blob/master/src/com/github/peholmst/vaadinactivitidemo/ui/forms/UserFormPresenter.java[`UserFormPresenter`]
+class that is invoked when the view controller has navigated to the User
+Form View:
+
+[source,java]
+....
+@Override
+protected void viewShown(ViewController viewController,
+        Map<String, Object> userData, ControllableView oldView,
+        Direction direction) {
+  if (userData != null) {
+    String formKey = (String) userData.get(UserFormView.KEY_FORM_KEY);
+    if (userData.containsKey(UserFormView.KEY_TASK_ID)) {
+      String taskId = (String) userData.get(UserFormView.KEY_TASK_ID);
+      showTaskForm(formKey, taskId);
+    }
+    // The rest of the implementation is omitted
+  }
+}
+
+private void showTaskForm(String formKey, String taskId) {
+  UserTaskForm form = userTaskFormContainer.getForm(formKey);
+  TaskFormData formData = getFormService().getTaskFormData(taskId);
+  form.populateForm(formData, taskId);
+  getView().setForm(form);
+}
+....
+
+The method first extracts the task ID and form key from the parameter
+map. It then invokes a helper method that looks up the corresponding
+form data and form from the Activiti form service and the
+`UserTaskFormContainer`, respectively. Finally, the form is populated
+and shown to the user.
+
+The final example is a method (also from `UserFormPresenter`) that is
+invoked when the user submits the form:
+
+[source,java]
+....
+public void submitForm(UserTaskForm form) {
+  if (form.getFormType().equals(UserTaskForm.Type.START_FORM)) {
+    getFormService().submitStartFormData(form.getProcessDefinitionId(), form.getFormProperties());
+  } else if (form.getFormType().equals(UserTaskForm.Type.TASK_FORM)) {
+    getFormService().submitTaskFormData(form.getTaskId(), form.getFormProperties());
+  }
+  getViewController().goBack();
+}
+....
+
+As there are two different kinds of forms (process start forms and user
+task forms, respectively), the method has to start by checking which
+kind it is currently processing. Then, the information is submitted to
+the Activiti form service. Finally, the view controller is asked to
+navigate back to what ever page it was on before the User Form View
+became visible.
+
+[[complex-domain-objects]]
+Complex Domain Objects
+~~~~~~~~~~~~~~~~~~~~~~
+
+The demo application does not use any domain objects as all the
+information can be represented as Activiti process variables. However,
+in most real-world applications you probably want to use a dedicated
+domain model.
+
+We are now going to look at a potential design for combining Activiti
+with a complex domain model. *Please note that the design has not been
+tested in practice* - feel free to test it if you feel like it (and
+remember to tell me the results)!
+
+Here is a sketch of a process that involves a more complicated domain
+model than just a few strings:
+
+image:img/complexdomain.png[Complex domain]
+
+The idea is that although many different entities need to be created and
+stored throughout the process, only some small parts of the information
+is actually required to drive the process forward. For example, the
+*Send invoice* task does not necessarily need the entire invoice object;
+only the invoice number, order number and due date should be sufficient.
+Likewise, the *Receive payment* task needs only the invoice number to be
+able to check that the invoice has been paid, the timer needs the due
+date to be able to send out a new invoice, etc.
+
+[[implementation-ideas]]
+Implementation Ideas
+^^^^^^^^^^^^^^^^^^^^
+
+The actual forms that the users fill in could be implemented in Vaadin,
+as described previously in this article. When the form is submitted, the
+entities are saved to some data store (e.g. a relational database).
+After this, the necessary form properties are submitted to the Activiti
+form service, completing the task in question. In other words, Activiti
+is used to drive the process forward (i.e. define the business logic),
+whereas JPA or any other object persistence solution is used to store
+data.
+
+There are a few things to keep in mind, though:
+
+* How are transactions handled?
+* How is data validation performed?
+* How is security enforced?
+* Is versioning of the domain data required? How should it be
+implemented if so? (Activiti already maintains a history log of the
+process operations.)
+
+In smaller applications, the following design could be sufficient:
+
+image:img/complexdomain_saving.png[Complex domain saving]
+
+Here, the Presenter (in the MVP-pattern) is responsible for extracting
+the needed form properties from the domain data, saving the entity and
+submitting the form. This moves some of the logic to the UI layer, but
+for small applications this is not a big problem as the presenter is
+itself decoupled from the actual UI code.
+
+For larger applications, the following design could be a better
+approach:
+
+image:img/complexdomain_saving2.png[Complex domain saving 2]
+
+Here, both the repository and the form service engine is hidden behind a
+facade. A Data Transfer Object (DTO) is used to convey the data from the
+Presenter to the facade. This approach requires more code, but decouples
+the business layer from the UI layer even more. Security enforcement and
+transaction handling also become easier.
+
+[[summary]]
+Summary
+~~~~~~~
+
+In this article, we have looked at how the Activiti BPM engine and
+Vaadin fit together. We have covered how the engine is initialized and
+accessed by Vaadin application instances. We have also covered how
+custom-made Vaadin forms can be used instead of Activiti's own form
+generation. Finally, we have discussed a way of combining Activiti
+processes with a more complex domain model.
+
+The Activiti API is clear and does not force adopters to use a specific
+GUI technology. Therefore, it plays really well with Vaadin and should
+be concidered a serious alternative for process centric enterprise
+applications.
+
+Likewise, Vaadin should be considered a serious alternative as a front
+end technology for applications based on Activiti.
+
+If you have any comments or questions, for example if something in the
+article is unclear or confusing, feel free to either post them below or
+send them to me directly by e-mail.
index c92c9478dbbbb05b95950f1cc33e5a74798140b4..ac5e2b9644e662af9e874becf342816bff3894f2 100644 (file)
@@ -12,3 +12,4 @@
 - link:FindingTheCurrentRootAndApplication.asciidoc[Finding the current root and application]
 - link:CreatingABasicApplication.asciidoc[Creating a basic application]
 - link:JasperReportsOnVaadinSample.asciidoc[Jasper reports on Vaadin sample]
+- link:BuildingVaadinApplicationsOnTopOfActiviti.asciidoc[Building Vaadin applications on top of Activiti]
diff --git a/documentation/articles/img/architecture.png b/documentation/articles/img/architecture.png
new file mode 100644 (file)
index 0000000..fa09301
Binary files /dev/null and b/documentation/articles/img/architecture.png differ
diff --git a/documentation/articles/img/complexdomain.png b/documentation/articles/img/complexdomain.png
new file mode 100644 (file)
index 0000000..050049d
Binary files /dev/null and b/documentation/articles/img/complexdomain.png differ
diff --git a/documentation/articles/img/complexdomain_saving.png b/documentation/articles/img/complexdomain_saving.png
new file mode 100644 (file)
index 0000000..97cf0cc
Binary files /dev/null and b/documentation/articles/img/complexdomain_saving.png differ
diff --git a/documentation/articles/img/complexdomain_saving2.png b/documentation/articles/img/complexdomain_saving2.png
new file mode 100644 (file)
index 0000000..af4d5d4
Binary files /dev/null and b/documentation/articles/img/complexdomain_saving2.png differ
diff --git a/documentation/articles/img/customForms.png b/documentation/articles/img/customForms.png
new file mode 100644 (file)
index 0000000..0edb698
Binary files /dev/null and b/documentation/articles/img/customForms.png differ
diff --git a/documentation/articles/img/process.png b/documentation/articles/img/process.png
new file mode 100644 (file)
index 0000000..f8b1b3e
Binary files /dev/null and b/documentation/articles/img/process.png differ
diff --git a/documentation/articles/img/views.png b/documentation/articles/img/views.png
new file mode 100644 (file)
index 0000000..fd4bb48
Binary files /dev/null and b/documentation/articles/img/views.png differ