Timeline
NOTE: This advice relates to the legacy Angular frontend, which we are deprecating and is included for reference purposes until that work is complete.
Supervision deputyship timeline rendering
This document outlines the frontend implementation of the timeline. For the backend, see the documents available here
Models
The deputyship timeline provides events in an array of objects. The structure of each object is a wrapper model that is standard across all events, containing a bespoke object that pertains to that specific event. An abbreviated example is shown below:
{
"id":7,
"hash":"7",
"timestamp":"2018-09-06 14:53:44",
"eventType":"Opg\\Core\\Model\\Event\\Person\\PersonStatusChanged",
"user":{
"phoneNumber":"12345678",
"displayName":"case manager",
"email":"case.manager@opgtest.com"
},
"event":{
"personType":"Deputy",
"personUid":"7000-0000-2449",
"personName":"Mr Deputy One",
"previousStatus":null,
"status":"Inactive",
"changes":null
}
}
The event
property contains the bespoke event object, and the eventType provides a key (our implementation is to use the fully resolved class name) which is unique to the event.
In the supervision code the entire event tree is translated into local types, and each event is rendered individually using its own template. This process starts in timeline.effects.ts with something like this (correct at time of writing..):
timelineApiModel.forEach(event => {
timelineEvents.push(this._timelineModelFactory.createTimelineEventModelFromApiModel(event));
});
The TimelineModelFactory
receives each event, looks up the correct type using its key, and returns an ITimelineEvent
wrapper, with one of the predefined model type inside the event property. To follow the example used above, PersonStatusChangedEvent
. It uses a simple switch statement to achieve this:
let eventType = this.getLocalEventType(apiModel.eventType);
let event = new eventType(apiModel.event);
private getLocalEventType(type: string): typeof BaseEvent {
switch (type) {
case 'Opg\\Core\\Model\\Event\\Person\\AddressChanged':
return AddressChangedEvent;
case 'Opg\\Core\\Model\\Event\\Person\\PersonDetailsChanged':
return PersonDetailsChangedEvent;
...
}
}
The type is returned from the helper method, and the variable holding the type is used to create a new instance. Typescript allows us to do this with a parameter, but only because each type inherits from
BaseEvent
, which exposes a constructor with a data parameter.
Once the specific type to be used is found, it is passed the data for the event itself (note: not the event wrapper, but just the bespoke event data), and it is responsible for mapping that data into itself. This allows for bespoke data manipulation where the dislaying of data differs from the raw values. PersonAddressChanged
is a good example of this.
The wrapper is then created, mapped, and returned to the caller.
Once all of the events have been returned, the factory is called once more to group and sort the events (events are displayed in today, this week, this month etc groups).
public createTimelineEventGroupsFromEvents(events: ITimelineEvent[]): ITimelineEventGroup[] {
events = events.sort((a, b) => a.timestamp > b.timestamp ? -1 : 1);
let groups = {
'Today': new TimelineEventGroup('Today'),
'Week': new TimelineEventGroup('This week'),
'Month': new TimelineEventGroup('This month'),
'Older': new TimelineEventGroup('Older')
};
...
}
View rendering
Now that the timeline model has been constructed, grouped, and sorted, it must be rendered to the DOM. The main entry point for displaying timeline is TimelineListComponent. The timeline list receives a list of timeline events and renders each one into a TimelineTemplateLoaderComponent
instance.
<ng-container *ngFor="let event of group.events">
<timeline-template-loader [type]="event.eventType" [model]="event"></timeline-template-loader>
</ng-container>
The type and event data are passed into the template loader (the full event, including the wrapper). The TimelineTemplateLoaderComponent
then looks up the template to render, and uses a component factory to dynamically render the relevant template into the DOM.
private mappings = [
{ event: PersonDetailsChangedEvent, template: TimelinePersonDetailsChangedTemplate },
{ event: PersonAdditionalInfoChangedEvent, template: TimelinePersonAdditionalInfoChangedTemplate },
...
];
let componentType = this.getComponentType(this.type);
let factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
this.componentRef = this.container.createComponent(factory);
let instance = <BaseTemplateComponent>this.componentRef.instance;
instance.model = this.model;
Each template in the templates folder (./supervision/app/src/components/timeline/presentation/templates) derives from BaseTemplateComponent
, which provides some basic formatting methods, but most importantly the model property in which the event data sits. This allows us type safety at compile time when the above code is running.
Templates
The anatomy of a template is very simple. If the event is just a vanilla changeset event you might have something like:
<timeline-event-template-wrapper [model]="model" eventTitle="TIMELINE.EVENTS.DETAILS">
<timeline-generic-changeset [model]="model.event.changes"></timeline-generic-changeset>
</timeline-event-template-wrapper>
The TimelineEventTemplateWrapperComponent
renders the header of the event, including the name and user details. TimelineGenericChangesetComponent
will simply take the changes property of an event and render them as a list (taking care of any “changed to” or “set to” logic).
The generic changeset renderer utilises the type property added to each changeset in the array. The data type of any specific values is known by the bespoke class that contains the data (see model section above) and is responsible for detailing any types against changes. If unspecified, it will be rendered as a string. Special types available are
bool
,date
,datetime
If the template is a named event, rather than a list of data changes, then it may look more like:
<timeline-event-template-wrapper [model]="model" eventTitle="TIMELINE.EVENTS.DEPUTYLINK">
<p>
<strong>Linked</strong> to case {{ model.event.orderType?.toUpperCase() }} {{ model.event.orderCourtRef }} -
<ng-container *ngIf="model.event.orderId !== null && model.event.clientId !== null; then caseLink else caseText"></ng-container> on {{ formatDate(model.timestamp) }}
</p>
</timeline-event-template-wrapper>
<ng-template #caseLink>
<a [routerLink]="['/clients', model.event.clientId]" [queryParams]="{ order: model.event.orderId }" class="dotted-link">{{ model.event.orderUid }}</a>
</ng-template>
<ng-template #caseText>
{{ model.event.orderUid }}
</ng-template>
The TimelineEventTemplateWrapperComponent
is still used, but rather than a bulleted list of changes, this event provides a sentence with data interpolated, and a link to an entity. In this example, the template is used to display either a link (where the orderId is available), or text (where it is not).