From 59cfe76aa5f3cf0b8cad2fc85a79a70d119a8823 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 27 Jun 2019 17:12:42 +0200 Subject: [PATCH] Finalize UI with tests. --- .../app/framework/utils/immutable-array.ts | 18 +- .../shared/services/workflows.service.spec.ts | 317 ++++++++++++------ .../app/shared/services/workflows.service.ts | 82 +++-- 3 files changed, 274 insertions(+), 143 deletions(-) diff --git a/src/Squidex/app/framework/utils/immutable-array.ts b/src/Squidex/app/framework/utils/immutable-array.ts index a8becffe4..36294a581 100644 --- a/src/Squidex/app/framework/utils/immutable-array.ts +++ b/src/Squidex/app/framework/utils/immutable-array.ts @@ -193,25 +193,11 @@ export class ImmutableArray implements Iterable { } export function compareStringsAsc(a: string, b: string) { - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - - return 0; + return a.localeCompare(b, undefined, { sensitivity: 'base' }); } export function compareStringsDesc(a: string, b: string) { - if (a < b) { - return 1; - } - if (a > b) { - return -1; - } - - return 0; + return a.localeCompare(b, undefined, { sensitivity: 'base' }); } export function compareNumbersAsc(a: number, b: number) { diff --git a/src/Squidex/app/shared/services/workflows.service.spec.ts b/src/Squidex/app/shared/services/workflows.service.spec.ts index 53089ad6c..c6d04745a 100644 --- a/src/Squidex/app/shared/services/workflows.service.spec.ts +++ b/src/Squidex/app/shared/services/workflows.service.spec.ts @@ -11,7 +11,7 @@ describe('Workflow', () => { it('should create empty workflow', () => { const workflow = new WorkflowDto(); - expect(workflow.name).toEqual('Default'); + expect(workflow.initial); }); it('should add step to workflow', () => { @@ -19,51 +19,68 @@ describe('Workflow', () => { new WorkflowDto() .setStep('1', { color: '#00ff00' }); - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '1', color: '#00ff00' } - ], - transitions: [], - name: 'Default' + expect(workflow.serialize()).toEqual({ + steps: { + '1': { transitions: {}, color: '#00ff00' } + }, + initial: '1' }); }); it('should override settings if step already exists', () => { const workflow = new WorkflowDto() - .setStep('1', { color: '#00ff00' }) + .setStep('1', { color: '#00ff00', noUpdate: true }) .setStep('1', { color: 'red' }); - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '1', color: 'red' } - ], - transitions: [], - name: 'Default' + expect(workflow.serialize()).toEqual({ + steps: { + '1': { transitions: {}, color: 'red', noUpdate: true } + }, + initial: '1' }); }); - it('should not remove step if locked', () => { + it('should return same workflow if step to update is locked', () => { const workflow = new WorkflowDto() - .setStep('1', { color: '#00ff00', isLocked: true }) - .setStep('2', { color: '#ff0000' }) - .setTransition('1', '2', { expression: '1 === 2' }) - .removeStep('1'); + .setStep('1', { color: '#00ff00', isLocked: true }); - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '1', color: '#00ff00', isLocked: true }, - { name: '2', color: '#ff0000' } - ], - transitions: [ - { from: '1', to: '2', expression: '1 === 2' } - ], - name: 'Default' - }); + const updated = workflow.setStep('1', { color: 'red' }); + + expect(updated).toBe(workflow); + }); + + it('should sort steps case invariant', () => { + const workflow = + new WorkflowDto() + .setStep('Z') + .setStep('a'); + + expect(workflow.steps).toEqual([ + { name: 'a' }, + { name: 'Z' } + ]); + }); + + it('should return same workflow if step to remove is locked', () => { + const workflow = + new WorkflowDto() + .setStep('1', { color: '#00ff00', isLocked: true }); + + const updated = workflow.removeStep('1'); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if step to remove not found', () => { + const workflow = + new WorkflowDto() + .setStep('1'); + + const updated = workflow.removeStep('3'); + + expect(updated).toBe(workflow); }); it('should remove step', () => { @@ -77,19 +94,46 @@ describe('Workflow', () => { .setTransition('2', '3', { expression: '2 === 3' }) .removeStep('1'); - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '2', color: '#ff0000' }, - { name: '3', color: '#0000ff' } - ], - transitions: [ - { from: '2', to: '3', expression: '2 === 3' } - ], - name: 'Default' + expect(workflow.serialize()).toEqual({ + steps: { + '2': { + transitions: { + '3': { expression: '2 === 3' } + }, + color: '#ff0000' + }, + '3': { transitions: {}, color: '#0000ff' } + }, + initial: '2' }); }); + it('should make first non-locked step the initial step if initial removed', () => { + const workflow = + new WorkflowDto() + .setStep('1') + .setStep('2', { isLocked: true }) + .setStep('3') + .removeStep('1'); + + expect(workflow.serialize()).toEqual({ + steps: { + '2': { transitions: {}, isLocked: true }, + '3': { transitions: {} } + }, + initial: '3' + }); + }); + + it('should unset initial step if initial removed', () => { + const workflow = + new WorkflowDto() + .setStep('1') + .removeStep('1'); + + expect(workflow.serialize()).toEqual({ steps: {}, initial: undefined }); + }); + it('should rename step', () => { const workflow = new WorkflowDto() @@ -99,90 +143,157 @@ describe('Workflow', () => { .setTransition('1', '2', { expression: '1 === 2' }) .setTransition('2', '1', { expression: '2 === 1' }) .setTransition('2', '3', { expression: '2 === 3' }) - .renameStep('1', '4'); - - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '4', color: '#00ff00' }, - { name: '2', color: '#ff0000' }, - { name: '3', color: '#0000ff' } - ], - transitions: [ - { from: '4', to: '2', expression: '1 === 2' }, - { from: '2', to: '4', expression: '2 === 1' }, - { from: '2', to: '3', expression: '2 === 3' } - ], - name: 'Default' + .renameStep('1', 'a'); + + expect(workflow.serialize()).toEqual({ + steps: { + 'a': { + transitions: { + '2': { expression: '1 === 2' } + }, + color: '#00ff00' + }, + '2': { + transitions: { + 'a': { expression: '2 === 1' }, + '3': { expression: '2 === 3' } + }, + color: '#ff0000' + }, + '3': { transitions: {}, color: '#0000ff' } + }, + initial: 'a' }); }); it('should add transitions to workflow', () => { const workflow = new WorkflowDto() - .setStep('1', { color: '#00ff00' }) - .setStep('2', { color: '#ff0000' }) + .setStep('1') + .setStep('2') .setTransition('1', '2', { expression: '1 === 2' }) .setTransition('2', '1', { expression: '2 === 1' }); - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '1', color: '#00ff00' }, - { name: '2', color: '#ff0000' } - ], - transitions: [ - { from: '1', to: '2', expression: '1 === 2' }, - { from: '2', to: '1', expression: '2 === 1' } - ], - name: 'Default' + expect(workflow.serialize()).toEqual({ + steps: { + '1': { + transitions: { + '2': { expression: '1 === 2' } + } + }, + '2': { + transitions: { + '1': { expression: '2 === 1' } + } + } + }, + initial: '1' }); }); - it('should add remove transition from workflow', () => { + it('should remove transition from workflow', () => { const workflow = new WorkflowDto() - .setStep('1', { color: '#00ff00' }) - .setStep('2', { color: '#ff0000' }) - .setTransition('1', '2', { expression: '1 === 1' }) + .setStep('1') + .setStep('2') + .setTransition('1', '2', { expression: '1 === 2' }) .setTransition('2', '1', { expression: '2 === 1' }) .removeTransition('1', '2'); - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '1', color: '#00ff00' }, - { name: '2', color: '#ff0000' } - ], - transitions: [ - { from: '2', to: '1', expression: '2 === 1' } - ], - name: 'Default' + expect(workflow.serialize()).toEqual({ + steps: { + '1': { transitions: {}}, + '2': { + transitions: { + '1': { expression: '2 === 1' } + } + } + }, + initial: '1' }); }); it('should override settings if transition already exists', () => { const workflow = new WorkflowDto() - .setStep('1', { color: '#00ff00' }) - .setStep('2', { color: '#ff0000' }) - .setTransition('1', '2', { expression: '1 === 2' }) - .setTransition('1', '2', { expression: '1 !== 2' }); - - expect(simplify(workflow)).toEqual({ - _links: {}, - steps: [ - { name: '1', color: '#00ff00' }, - { name: '2', color: '#ff0000' } - ], - transitions: [ - { from: '1', to: '2', expression: '1 !== 2' } - ], - name: 'Default' + .setStep('1') + .setStep('2') + .setTransition('2', '1', { expression: '2 === 1', role: 'Role' }) + .setTransition('2', '1', { expression: '2 !== 1' }); + + expect(workflow.serialize()).toEqual({ + steps: { + '1': { transitions: {} }, + '2': { + transitions: { + '1': { expression: '2 !== 1', role: 'Role' } + } + } + }, + initial: '1' }); }); -}); -function simplify(value: any) { - return JSON.parse(JSON.stringify(value)); -} \ No newline at end of file + it('should return same workflow if transition to update not found by from step', () => { + const workflow = + new WorkflowDto() + .setStep('1') + .setStep('2') + .setTransition('1', '2'); + + const updated = workflow.setTransition('3', '2', { role: 'Role' }); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if transition to update not found by to step', () => { + const workflow = + new WorkflowDto() + .setStep('1') + .setStep('2') + .setTransition('1', '2'); + + const updated = workflow.setTransition('1', '3', { role: 'Role' }); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if transition to remove not', () => { + const workflow = + new WorkflowDto() + .setStep('1') + .setStep('2') + .setTransition('1', '2'); + + const updated = workflow.removeTransition('1', '3'); + + expect(updated).toBe(workflow); + }); + + it('should return same workflow if step to make initial is locked', () => { + const workflow = + new WorkflowDto() + .setStep('1') + .setStep('2', { color: '#00ff00', isLocked: true }); + + const updated = workflow.setInitial('2'); + + expect(updated).toBe(workflow); + }); + + it('should set initial step', () => { + const workflow = + new WorkflowDto() + .setStep('1') + .setStep('2') + .setInitial('2'); + + expect(workflow.serialize()).toEqual({ + steps: { + '1': { transitions: {} }, + '2': { transitions: {} } + }, + initial: '2' + }); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/workflows.service.ts b/src/Squidex/app/shared/services/workflows.service.ts index aebb39e63..2b1f20034 100644 --- a/src/Squidex/app/shared/services/workflows.service.ts +++ b/src/Squidex/app/shared/services/workflows.service.ts @@ -5,30 +5,21 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { - compareStringsAsc, - Model, - ResourceLinks -} from '@app/framework'; +import { compareStringsAsc, ResourceLinks } from '@app/framework'; -export class WorkflowDto extends Model { +export class WorkflowDto { public readonly _links: ResourceLinks; constructor(links: ResourceLinks = {}, - public readonly name: string = 'Default', + public readonly initial?: string, public readonly steps: WorkflowStep[] = [], - public readonly transitions: WorkflowTransition[] = [], - public readonly initial?: string + private readonly transitions: WorkflowTransition[] = [] ) { - super(); - - this._links = links; - } - - public onCloned() { this.steps.sort((a, b) => compareStringsAsc(a.name, b.name)); this.transitions.sort((a, b) => compareStringsAsc(a.to, b.to)); + + this._links = links; } public getOpenSteps(step: WorkflowStep) { @@ -43,7 +34,7 @@ export class WorkflowDto extends Model { return this.steps.find(x => x.name === name)!; } - public setStep(name: string, values: Partial) { + public setStep(name: string, values: Partial = {}) { const found = this.getStep(name); if (found) { @@ -64,28 +55,40 @@ export class WorkflowDto extends Model { initial = steps[0].name; } - return this.with({ steps, initial }); + return new WorkflowDto(this._links, initial, steps, this.transitions); } public setInitial(initial: string) { const found = this.getStep(initial); - if (!found || initial === 'Published') { + if (!found || found.isLocked) { return this; } - return this.with({ initial }); + return new WorkflowDto(this._links, initial, this.steps, this.transitions); } public removeStep(name: string) { const steps = this.steps.filter(s => s.name !== name || s.isLocked); + if (steps.length === this.steps.length) { + return this; + } + const transitions = steps.length !== this.steps.length ? this.transitions.filter(t => t.from !== name && t.to !== name) : this.transitions; - return this.with({ steps, transitions }); + let initial = this.initial; + + if (initial === name) { + const first = steps.find(x => !x.isLocked); + + initial = first ? first.name : undefined; + } + + return new WorkflowDto(this._links, initial, steps, transitions); } public renameStep(name: string, newName: string) { @@ -115,16 +118,26 @@ export class WorkflowDto extends Model { return transition; }); - return this.with({ steps, transitions }); + let initial = this.initial; + + if (initial === name) { + initial = newName; + } + + return new WorkflowDto(this._links, initial, steps, transitions); } public removeTransition(from: string, to: string) { const transitions = this.transitions.filter(t => t.from !== from || t.to !== to); - return this.with({ transitions }); + if (transitions.length === this.transitions.length) { + return this; + } + + return new WorkflowDto(this._links, this.initial, this.steps, transitions); } - public setTransition(from: string, to: string, values?: Partial) { + public setTransition(from: string, to: string, values: Partial = {}) { const stepFrom = this.getStep(from); if (!stepFrom) { @@ -147,7 +160,28 @@ export class WorkflowDto extends Model { const transitions = [...this.transitions.filter(t => t !== found), { from, to, ...values }]; - return this.with({ transitions }); + return new WorkflowDto(this._links, this.initial, this.steps, transitions); + } + + public serialize(): any { + const result = { steps: {}, initial: this.initial }; + + for (let step of this.steps) { + const { name, ...values } = step; + + const s = { ...values, transitions: {} }; + + for (let transition of this.getTransitions(step)) { + const { from, to, step: _, ...t } = transition; + + s.transitions[to] = t; + } + + result.steps[name] = s; + } + + return result; + } }