From a6959e39d090ddd6cfdacf1247cf14c23444ac2f Mon Sep 17 00:00:00 2001 From: andreas-unleash <104830839+andreas-unleash@users.noreply.github.com> Date: Mon, 31 Oct 2022 13:56:04 +0200 Subject: [PATCH] approval flow poc --- src/lib/types/models/state-machine.ts | 101 ++++++++++++++++++ .../types/models/suggest-changes-approval.ts | 84 +++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 src/lib/types/models/state-machine.ts create mode 100644 src/lib/types/models/suggest-changes-approval.ts diff --git a/src/lib/types/models/state-machine.ts b/src/lib/types/models/state-machine.ts new file mode 100644 index 0000000000..2b1409295c --- /dev/null +++ b/src/lib/types/models/state-machine.ts @@ -0,0 +1,101 @@ +/* + * StateMachine.ts + * TypeScript finite state machine class with async transformations using promises. + * https://github.com/eram/ts-fsm/blob/master/src/stateMachine.ts + */ + +export interface ITransition { + fromState: STATE; + event: EVENT; + toState: STATE; + cb?: (...args: unknown[]) => Promise; +} + +export function transition( + fromState: STATE, + event: EVENT, + toState: STATE, + cb?: (...args: unknown[]) => Promise, +): ITransition { + return { fromState, event, toState, cb }; +} + +export class StateMachine { + protected current: STATE; + + // initalize the state-machine + constructor( + initState: STATE, + protected transitions: ITransition[] = [], + ) { + this.current = initState; + } + + addTransitions(transitions: ITransition[]): void { + transitions.forEach((tran) => this.transitions.push(tran)); + } + + getState(): STATE { + return this.current; + } + + can(event: EVENT): boolean { + return this.transitions.some( + (trans) => + trans.fromState === this.current && trans.event === event, + ); + } + + isFinal(): boolean { + // search for a transition that starts from current state. + // if none is found it's a terminal state. + return this.transitions.every( + (trans) => trans.fromState !== this.current, + ); + } + + // post event asynch + async dispatch(event: EVENT, ...args: unknown[]): Promise { + return new Promise((resolve, reject) => { + // delay execution to make it async + setTimeout( + (me: this) => { + // find transition + const found = this.transitions.some((tran) => { + if ( + tran.fromState === me.current && + tran.event === event + ) { + me.current = tran.toState; + if (tran.cb) { + try { + tran.cb(args).then(resolve).catch(reject); + } catch (e) { + console.error( + 'Exception caught in callback', + e, + ); + reject(); + } + } else { + resolve(); + } + return true; + } + return false; + }); + + // no such transition + if (!found) { + console.error( + `no transition: from ${me.current.toString()} event ${event.toString()}`, + ); + reject(); + } + }, + 0, + this, + ); + }); + } +} diff --git a/src/lib/types/models/suggest-changes-approval.ts b/src/lib/types/models/suggest-changes-approval.ts new file mode 100644 index 0000000000..fcc25339a9 --- /dev/null +++ b/src/lib/types/models/suggest-changes-approval.ts @@ -0,0 +1,84 @@ +import { StateMachine, transition } from './state-machine'; + +export enum Events { + SUBMIT, + REJECT, + APPROVE, + REQUEST_CHANGES, + APPLY, + CANCEL, +} + +export enum States { + DRAFT, + IN_REViEW, + APPROVED, + CHANGES_REQUESTED, + APPLIED, + REJECTED, + CANCELLED, +} +/* eslint-disable */ +class ApprovalFlow extends StateMachine { + + constructor(init: States = States.DRAFT) { + + super(init); + + const s = States; + const e = Events; + + + this.addTransitions([ + // fromState event toState callback + transition(s.DRAFT, e.SUBMIT, s.IN_REViEW, this.onSubmitted.bind(this)), + transition(s.DRAFT, e.CANCEL, s.CANCELLED, this.onCancelled.bind(this)), + transition(s.IN_REViEW, e.APPROVE, s.APPROVED, this.onApproved.bind(this)), + transition(s.APPROVED, e.APPLY, s.APPLIED, this.onApplied.bind(this)), + transition(s.APPROVED, e.CANCEL, s.CANCELLED, this.onCancelled.bind(this)), + transition(s.IN_REViEW, e.CANCEL, s.CANCELLED, this.onCancelled.bind(this)), + transition(s.IN_REViEW, e.REQUEST_CHANGES,s.CHANGES_REQUESTED, this.onChangesRequested.bind(this)), + transition(s.IN_REViEW, e.REJECT, s.REJECTED, this.onRejected.bind(this)), + transition(s.CHANGES_REQUESTED,e.SUBMIT, s.DRAFT, this.onSubmitted.bind(this)), + ]); + + } + + // public methods + async submitDraft() { return this.dispatch(Events.SUBMIT); } + + async cancel() { return this.dispatch(Events.CANCEL); } + + async approve() { return this.dispatch(Events.APPROVE); } + async apply() { return this.dispatch(Events.APPLY); } + async reject() { return this.dispatch(Events.REJECT); } + async requestChanges() { return this.dispatch(Events.REQUEST_CHANGES); } + + + // transition callbacks + private async onSubmitted() { + console.log(`onSubmitted...`); + } + + private async onRejected() { + console.log(`onRejected...`); + } + + + private async onChangesRequested() { + console.log(`onApproved...`); + } + + private async onCancelled() { + console.log(`onCancelled...`); + } + + private async onApproved() { + console.log(`onApproved...`); + } + + private async onApplied() { + console.log(`onApplied...`); + } + /* eslint-enable */ +}