Skip to content

Commit

Permalink
Merge pull request #75 from wmde/docs
Browse files Browse the repository at this point in the history
Add Documentation
  • Loading branch information
Abban authored Jun 12, 2023
2 parents 83843a8 + ab7feda commit 91d5d49
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 7 deletions.
38 changes: 38 additions & 0 deletions docs/BannerConductor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Banner Conductor

The Banner Conductor is the root Vue component and is responsible for handling the top level banner states, and is the gateway for interactions between the banners and the [pages](Page.md) they are displayed on. It is largely invisible to the developers.

Internally it uses a [Finite State Machine](https://en.wikipedia.org/wiki/Finite-state_machine) (FSM).

These states perform various tasks such as:

* Measuring impressions
* Sending tracking events
* Handling page resize events

You can get an overview of each state's dependencies by looking in the `StateFactory`.

Each state has 2 lifecycle methods `enter()` and `exit()` which are called automatically when the state is entered and exited. These methods return promises and allow the states to be run in an asynchronous manner, for example the `Pending` state will only resolve its promise when the banner delay timer has completed.

This is the state flow:

```mermaid
stateDiagram-v2
[*] --> Initial
Initial --> Pending
Pending --> Showing
Pending --> NotShown: has size issue
Showing --> Visible
Visible --> Hidden
NotShown --> [*]
Hidden --> [*]
```

The states are as follows:

* **Initial** This state does nothing, it is used as a default state when the FSM is instantiated.
* **Pending** This pre-sets the banner height on the Page and starts the banner display timer.
* **NotShown** When there is a size issue, or if the user interacts with certain elements on the page while the banner display timer is running the FSM will move to this state at the timer end. It will then mark the banner as not shown and fire tracking events.
* **Showing** This is the banner transition phase. It is in this state when the banner is animating into the page.
* **Visible** The banner is now finished transition and is visible to the user on the page. It will update the display counts and trigger the displayed tracking events when entered. _Note: This does not control which part of the banner is visible, that is the responsibility of the banner components._
* **Hidden** When the user has closed the banner.
24 changes: 24 additions & 0 deletions docs/DynamicContent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Dynamic Content

The campaign text is mostly static but contains some dynamic content. This content consists of things like:

* The number of days left in the campaign.
* The current day name.
* The average donation.
* The average number of donors.

This content is used to generate sentences that are displayed as part of the banner text content, and as values for the progress bar.

## Structure

### Generators
These are for building the sentences and progress bar items. There is a single generator per dynamic text item.

### Formatters
These are tools used by the generators for localising numbers depending on if the banners are in German or English.

### Campaign Projection
The campaign team measures the progress of the campaign and uploads the current measurements every few days. We use these measurements to project the numbers displayed in the dynamic text. The calculation starts at the last upload time up to the point in time when the user sees the banner. The CampaignProjection is responsible for doing the calculations for this projection.

### Dynamic Campaign Text
The DynamicCampaignText class acts as a factory that instantiates the various generators.
102 changes: 102 additions & 0 deletions docs/MultiStepDonationForm-and-Controllers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Multi Step Donation Form and Form Controllers

Since the 2022 campaign, a lot of tests had multistep forms, each one with a different flow. These tests are different enough functionally that they would require a new `DonationForm` component for each.

In order to avoid duplicating components per-test we opted to instead create a flexible `MultiStepDonationForm` that gets each form step as a named slot. Each named slot also gets an implementation of a `StepController`, which contains the logic that determines which form page to show, based on the user interaction with the current form page.

This module consists of the following parts:

## `MultiStepDonationForm.vue`

This initialises and presents the form:
1. It connects the individual form pages to the `StepController` instances to handle the flow of the submissions.
2. It wraps the `FormStep` components in its slots in a `Slider` component and starts the slides at the first slide.
3. From then on it acts as a bus between the `StepController` instances, `Slider` and the `FormSteps`.

## `FormModel`

This is a [Vuejs composable](https://vuejs.org/guide/reusability/composables.html) and contains the form state. It is globally available. This contains only the data that will be posted to the Fundraising Application and should not be modified for a single test.

## `FormSteps`

A banner form consists of one or more step components (components containing with form elements). Each step:

* Is in the directory `src/components/DonationForm/Forms`
* Handles its own validation.
* Emits a `submit` or `previous` event with data. The `MultiStepDonationForm` passes the events to the `StepController` and allows it to decide what to do next.
* Optionally resets state when they are entered or exited. For example if a user hits the back button we reset some forms to their default state, and we expect the user to fill them out again to proceed. The form can watch its `isCurrent` property to detect when it becomes the current step.
* Optionally modify the `FormModel` directly. The `FormModel` contains only data that will be posted to the Fundraising Application. Some form steps, like the `MainDonationForm` have fields that directly correspond to `FormModel` properties and will change it directly. Other forms, however, are for modifying this data as a side effect of the `submit` and `previous` actions. These forms pass the extra data to their `StepController` and it will decide what should change in the `FormModel`.

## `StepController`

There are multiple implementations of this interface, at least one per form step. It is responsible for handling the flow of the submission, instructing the `MultiStepDonationForm` either to submit or go to a different step.

Each controller instance has two methods, called by `MultiStepDonationForm`:
* `submit` This is called when a sub-form has been submitted. The data passed has already been validated. It may get extra data about the user input from the form step.
* `previous` This is called when the user clicks the back button.

The `MultiStepDonationForm` passes the following callbacks into each StepController:
* `goToStep` makes the `MultiStepDonationForm` go to a different page with the specified name. `MultiStepDonationForm` builds an internal mapping between the step names and the step index needed by its `Slider` component.
* `submit` makes the `MultiStepDonationForm` submit the current values of `FormModel` via the `SubmiForm` (see below).

Each `StepController` has a factory function that provides it with the slot names of the possible other steps it can go to.
The `StepController` factory functions follow the following naming schema:
`create<SubmitBehavior><StepName><OptionalSpecifier>`

* `<SubmitBehavior>` can either be `Intermediate` (meaning the step will not submit, but go to a different form step) or `Submittable` (meaning the controller will call `submit` or *may* go to a different step in some cases).
* `<StepName>` is the name of the form component, e.g. `MainDonationForm` or `AddressTypeForm`.
* `<OptionalSpecifier>` is an optional description of the controller behavior, e.g, `SinglePage`

Example factory functions: `createSubmittableAddressType`, `createIntermediateUpgradeToYearly`, `createSubmittableMainDonationFormSinglePage`

## `SubmitForm`

This is a hidden form. It contains the values from the `FormModel` as hidden fields and is submitted by the `MultiStepDonationForm` when a `StepController` invokes the `submit` callback. This data is the POSTed to the Fundraising Application as a standard HTTP POST request.

## In Summary

* The `MultistepDonationForm` acts like an event bus and keeps track which page is the current one.
* The `FormModel` contains the form state values.
* The `StepControllers` handle the form submission logic.
* The `FormSteps` handle their own validation and fire submit/previous events.
* Some `FormSteps` modify the model state directly or pass their data to the `StepController` for extra computing.
* The `SubmitForm` has hidden form fields with `FormModel` values and POSTs to the Fundraising Application.

```mermaid
stateDiagram-v2
FormModel: Globally available FormModel
StepControllers --> MultiStepDonationForm
MultiStepDonationForm --> FormStepOne
MultiStepDonationForm --> FormStepTwo
MultiStepDonationForm --> FormStepThree
MultiStepDonationForm --> SubmitForm: Used to post data to Fundraising Application
```

Below is an example happy path user flow.

```mermaid
sequenceDiagram
participant MultiStepDonationForm
participant FormStep1
participant StepController1
participant FormStep2
participant StepController2
participant SubmitForm
FormStep1 ->> MultiStepDonationForm: user submits step 1
MultiStepDonationForm ->> StepController1: call `submit` on controller
StepController1 ->> MultiStepDonationForm: calls `goToStep` callback
MultiStepDonationForm ->> FormStep2: shows next step
FormStep2 ->> MultiStepDonationForm: user submits step 2
MultiStepDonationForm ->> StepController2: calls `submit` on controller
StepController2 ->> MultiStepDonationForm: calls `submit` callback
MultiStepDonationForm ->> SubmitForm: posts form
```

## Notes for the future

This module isn't perfect. Depending on the direction of the future campaign tests we might improve some things about it.

* The form user flows are badly designed. For example, we don't know the reasoning why some forms clear their own state on back. This needs to be clarified before a redesign happens.
* `FormSteps` Some of these modify the `FormModel` directly, some pass extra data so the `StepController` can modify this data. These steps could be changed to each contain their own state and the `StepController` becomes responsible for changing all `FormModel` state, meaning the `FormModel` no longer needs to be so global.
35 changes: 35 additions & 0 deletions docs/Page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# The Page Classes

We display banners on wikipedia.org and wikipedia.de. These sites have different HTML markup, have a different campaign configuration structure, and have different ways of logging events. We use a Page interface with 2 implementations which ensures our code is compatible with these 2 environments.

They are used for:

* Mounting the banners in the correct place.
* Tracking events.
* Passing the calculated banner height from our banner javascript to css.

In our entry points we give the BannerConductor one of these implementations to perform actions as needed.

## PageOrg (Used on wikipedia.org)
This implementation is for interacting with wikipedia.org and is a little complex.

### The mw Object
ikipedia.org provides the global `mw` JavaScript object. For PageOrg we wrap the object in a `MediaWiki` interface and use it for things like:

* Discovering which skin the user has active.
* Discovering the current namespace of the page the banner is being showed on.
* Notifying Central Notice that a user has seen or interacted with a banner.
* Tracking events with Wikipedia's tracking tools.

### Skins

There are multiple skins that our banners can appear on:

* **Vector** This skin is the default on dewiki.
* **Vector 2022** This skin is the default on enwiki.
* **Minerva** This is the default mobile skin.

Each one of these skins has different markup and the users will interact with different parts of the skins that may prevent banner display so this needs to be set up per-skin.

## PageDe (Used on wikipedia.de)
This is the simplest of the 2 implementations. We use it only to mount the banner and send tracking events to our Matomo analytics.
3 changes: 2 additions & 1 deletion src/components/BannerConductor/BannerConductor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ async function closeHandler( closeEvent: TrackingEvent ): Promise<any> {
width: 100%;
z-index: 1000;
}
.wmde-banner-closed {
.wmde-banner--not-shown,
.wmde-banner--closed {
display: none;
}
</style>
9 changes: 3 additions & 6 deletions src/utils/DynamicContent/DynamicContent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DynamicProgressBarContent } from '@src/utils/DynamicContent/DynamicProgressBarContent';

export interface DynamicContent {
currentDayName: string;
currentDate: string;
Expand All @@ -7,10 +9,5 @@ export interface DynamicContent {
donorsNeededSentence: string;
goalDonationSum: string;
overallImpressionCount: number;
progressBarContent: {
percentageTowardsTarget: number,
donationTarget: string,
amountDonated: string,
amountNeeded: string
};
progressBarContent: DynamicProgressBarContent;
}

0 comments on commit 91d5d49

Please sign in to comment.