Architectural overview

Technical structure

The approval workflows platform is built using the same technologies as Totara Perform and Engage: database entities are extended by a domain model which implements the business logic and exposes an API to GraphQL resolvers. The resolvers implement a schema which is used by the Tui VueJS frontend to implement the user and administrative interfaces. Each endpoint has a controller class which sets up the initial page properties and hands everything off to the browser. From there, Tui takes over, drawing the interface, filling in language strings and user data, and requesting GraphQL queries and mutations based on user interaction.

New to approval workflows is the use of the XState framework to manage frontend application state. This allows us to systematically define and standardise the interaction of all the frontend components, and enables rapid, consistent development of single-page applications (SPAs).

In Totara terms, approval workflows creates a system category at the site level, and one in every tenant context. Each workflow is a course container of type container_approval. Each workflow assignment is an instance of mod_approval, meaning it is technically a course module/activity, but not currently one which can be used in regular courses.

  • Workflow types and approval forms are managed in the category context
  • Workflow configuration is managed in the course context
  • Applications are submitted and processed in the activity context

The bulk of the backend code is in server/mod/approval/.

The frontend lives in client/component/mod/approval/src/.

Code which deals specifically with the behind-the-scenes system categories and course containers is in server/container/type/approval/.

Backend orientation

In the database, all tables are prefixed with approval_, except for the Assignments table which must be named 'approval' to match the mod_approval component. Table names indicate parent-child relationships.

mod_approval has the usual set of Totara libraries (lib.php plus a classes/directory), and configurations (in the db/ and lang/ directories). It has a GraphQL schema and a set of persisted queries and mutations in the webapi/ directory. And it has a relatively small collection of endpoints scattered throughout.

There is also the form/ directory, which contains approvalform subplugins. 

To trace a page load you can start at the endpoint, but it's usually easier to start with the controller class in classes/controllers/<application|assignment|workflow>/.

To trace a GraphQL operation, you can start with the resolver in classes/webapi/resolvers/<mutation|query>/.

Both the endpoints and resolvers end up working with the model classes, found in classes/model/<application|assignment|form|workflow>/. These, in turn, rely on matching entity and repository classes for data storage/retrieval, including collections of related objects. For example, a workflow model instance $workflow has a workflow entity instance that provides data behind the workflow's properties ($workflow→name, $workflow->context) and collections of related objects ($workflow→assignments, $workflow->latest_version→stages).

Also of immediate interest will be the interactor classes, which consistently resolve how a user can interact with various objects in context (what capabilities they have), and the data_provider classes, which provide the filtered, sorted, paginated collections for the workflows and applications dashboard. data_providers are also used for the different user pickers, and for the list of override assignments.

The form_schema classes may also be useful when troubleshooting approvalform subplugins.

Frontend orientation

The frontend code separates business (effectively model and controller) logic from view logic. Business logic is held in js/ folder further specified to the entity or function (/application, /workflow).
View logic is held in the /components folder in Vue components. The two are integrated at the page entry point level with page-specific Vue components held in pages/ and the tui_xstate implementation of the XState library. 

Business logic is further separated into state machine folders that control the application state for the page or the page's subcomponents ('organisms' in the parlance of atomic design). For example, the page served /mod/approval/application/edit.php will have an entry point component at /pages/ApplicationEdit.vue and is controlled by a machine at js/application/edit/machine.js.

Within each folder, the state.js file provides a high-level overview of the possible states that the page or subcomponent can be in, the transitions between theses states, and any client-data updates or side-effects.  Apollo mutations and queries can be found in services.js , side-effects and client-data updates are in actions.js, and control-flow functions are in guards.js

Vue components interact with a machine by sending events with data payloads on user interactions and the $send() helper. Vue components react to state changes with the $matches() helper and client-data changes of the linked $context object. Data access to nested properties (in particular, Apollo query response objects) of the $context object uses standardised selector functions held in the selectors.js file and graphql_selectors/[query_name].js

Model conventions

There are a few sets of properties and behaviours that are common across several of the model classes:

  • The id_number property must generally be unique for each instance of the class, but is otherwise transparent to the model code.
  • The active property is an on-off switch; if something is set to active == false, then it acts as though it had been deleted except when referenced by an application. In most cases (but not all) instances are active == true when created. Model classes with this property implement activate(), can_deactivate(), and deactivate() methods.
  • Property status is one of draft, active, or archived. Objects with status have a lifecycle - when set to draft, they can only be seen by an admin. When published, they become active, and are ready to be used. When archived, they are closed to new uses, though existing use is generally allowed to continue. For example, a workflow_version that is archived won't allow new applications to be created for it, but existing applications may proceed until completed.
  • The active and status properties exist so that historic configurations can be maintained to support the display of, and reporting on, historic applications. If an old stage was deleted from the database rather than marked as active == false, any historic application that went through that stage (or was stuck on it) might not be displayable.
  • As per above, do not delete any inactive objects unless you have deleted all applications that reference them. 
  • Type properties will reference a specific type class, which implements an interface or extends a base class generic to that type – for example, the $assignment_approver->type classes user and relationship both implement the approver_type interface. As is traditional with Totara, we prefer to use integer codes for specifying type in the database, which are converted to string enums in the GraphQL resolvers, and the type classes perform these conversions as well as implementing other type-specific functionality.