Browse Source

Finalize UI with tests.

pull/380/head
Sebastian Stehle 7 years ago
parent
commit
59cfe76aa5
  1. 18
      src/Squidex/app/framework/utils/immutable-array.ts
  2. 317
      src/Squidex/app/shared/services/workflows.service.spec.ts
  3. 82
      src/Squidex/app/shared/services/workflows.service.ts

18
src/Squidex/app/framework/utils/immutable-array.ts

@ -193,25 +193,11 @@ export class ImmutableArray<T> implements Iterable<T> {
} }
export function compareStringsAsc(a: string, b: string) { export function compareStringsAsc(a: string, b: string) {
if (a < b) { return a.localeCompare(b, undefined, { sensitivity: 'base' });
return -1;
}
if (a > b) {
return 1;
}
return 0;
} }
export function compareStringsDesc(a: string, b: string) { export function compareStringsDesc(a: string, b: string) {
if (a < b) { return a.localeCompare(b, undefined, { sensitivity: 'base' });
return 1;
}
if (a > b) {
return -1;
}
return 0;
} }
export function compareNumbersAsc(a: number, b: number) { export function compareNumbersAsc(a: number, b: number) {

317
src/Squidex/app/shared/services/workflows.service.spec.ts

@ -11,7 +11,7 @@ describe('Workflow', () => {
it('should create empty workflow', () => { it('should create empty workflow', () => {
const workflow = new WorkflowDto(); const workflow = new WorkflowDto();
expect(workflow.name).toEqual('Default'); expect(workflow.initial);
}); });
it('should add step to workflow', () => { it('should add step to workflow', () => {
@ -19,51 +19,68 @@ describe('Workflow', () => {
new WorkflowDto() new WorkflowDto()
.setStep('1', { color: '#00ff00' }); .setStep('1', { color: '#00ff00' });
expect(simplify(workflow)).toEqual({ expect(workflow.serialize()).toEqual({
_links: {}, steps: {
steps: [ '1': { transitions: {}, color: '#00ff00' }
{ name: '1', color: '#00ff00' } },
], initial: '1'
transitions: [],
name: 'Default'
}); });
}); });
it('should override settings if step already exists', () => { it('should override settings if step already exists', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto()
.setStep('1', { color: '#00ff00' }) .setStep('1', { color: '#00ff00', noUpdate: true })
.setStep('1', { color: 'red' }); .setStep('1', { color: 'red' });
expect(simplify(workflow)).toEqual({ expect(workflow.serialize()).toEqual({
_links: {}, steps: {
steps: [ '1': { transitions: {}, color: 'red', noUpdate: true }
{ name: '1', color: 'red' } },
], initial: '1'
transitions: [],
name: 'Default'
}); });
}); });
it('should not remove step if locked', () => { it('should return same workflow if step to update is locked', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto()
.setStep('1', { color: '#00ff00', isLocked: true }) .setStep('1', { color: '#00ff00', isLocked: true });
.setStep('2', { color: '#ff0000' })
.setTransition('1', '2', { expression: '1 === 2' })
.removeStep('1');
expect(simplify(workflow)).toEqual({ const updated = workflow.setStep('1', { color: 'red' });
_links: {},
steps: [ expect(updated).toBe(workflow);
{ name: '1', color: '#00ff00', isLocked: true }, });
{ name: '2', color: '#ff0000' }
], it('should sort steps case invariant', () => {
transitions: [ const workflow =
{ from: '1', to: '2', expression: '1 === 2' } new WorkflowDto()
], .setStep('Z')
name: 'Default' .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', () => { it('should remove step', () => {
@ -77,19 +94,46 @@ describe('Workflow', () => {
.setTransition('2', '3', { expression: '2 === 3' }) .setTransition('2', '3', { expression: '2 === 3' })
.removeStep('1'); .removeStep('1');
expect(simplify(workflow)).toEqual({ expect(workflow.serialize()).toEqual({
_links: {}, steps: {
steps: [ '2': {
{ name: '2', color: '#ff0000' }, transitions: {
{ name: '3', color: '#0000ff' } '3': { expression: '2 === 3' }
], },
transitions: [ color: '#ff0000'
{ from: '2', to: '3', expression: '2 === 3' } },
], '3': { transitions: {}, color: '#0000ff' }
name: 'Default' },
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', () => { it('should rename step', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto()
@ -99,90 +143,157 @@ describe('Workflow', () => {
.setTransition('1', '2', { expression: '1 === 2' }) .setTransition('1', '2', { expression: '1 === 2' })
.setTransition('2', '1', { expression: '2 === 1' }) .setTransition('2', '1', { expression: '2 === 1' })
.setTransition('2', '3', { expression: '2 === 3' }) .setTransition('2', '3', { expression: '2 === 3' })
.renameStep('1', '4'); .renameStep('1', 'a');
expect(simplify(workflow)).toEqual({ expect(workflow.serialize()).toEqual({
_links: {}, steps: {
steps: [ 'a': {
{ name: '4', color: '#00ff00' }, transitions: {
{ name: '2', color: '#ff0000' }, '2': { expression: '1 === 2' }
{ name: '3', color: '#0000ff' } },
], color: '#00ff00'
transitions: [ },
{ from: '4', to: '2', expression: '1 === 2' }, '2': {
{ from: '2', to: '4', expression: '2 === 1' }, transitions: {
{ from: '2', to: '3', expression: '2 === 3' } 'a': { expression: '2 === 1' },
], '3': { expression: '2 === 3' }
name: 'Default' },
color: '#ff0000'
},
'3': { transitions: {}, color: '#0000ff' }
},
initial: 'a'
}); });
}); });
it('should add transitions to workflow', () => { it('should add transitions to workflow', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto()
.setStep('1', { color: '#00ff00' }) .setStep('1')
.setStep('2', { color: '#ff0000' }) .setStep('2')
.setTransition('1', '2', { expression: '1 === 2' }) .setTransition('1', '2', { expression: '1 === 2' })
.setTransition('2', '1', { expression: '2 === 1' }); .setTransition('2', '1', { expression: '2 === 1' });
expect(simplify(workflow)).toEqual({ expect(workflow.serialize()).toEqual({
_links: {}, steps: {
steps: [ '1': {
{ name: '1', color: '#00ff00' }, transitions: {
{ name: '2', color: '#ff0000' } '2': { expression: '1 === 2' }
], }
transitions: [ },
{ from: '1', to: '2', expression: '1 === 2' }, '2': {
{ from: '2', to: '1', expression: '2 === 1' } transitions: {
], '1': { expression: '2 === 1' }
name: 'Default' }
}
},
initial: '1'
}); });
}); });
it('should add remove transition from workflow', () => { it('should remove transition from workflow', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto()
.setStep('1', { color: '#00ff00' }) .setStep('1')
.setStep('2', { color: '#ff0000' }) .setStep('2')
.setTransition('1', '2', { expression: '1 === 1' }) .setTransition('1', '2', { expression: '1 === 2' })
.setTransition('2', '1', { expression: '2 === 1' }) .setTransition('2', '1', { expression: '2 === 1' })
.removeTransition('1', '2'); .removeTransition('1', '2');
expect(simplify(workflow)).toEqual({ expect(workflow.serialize()).toEqual({
_links: {}, steps: {
steps: [ '1': { transitions: {}},
{ name: '1', color: '#00ff00' }, '2': {
{ name: '2', color: '#ff0000' } transitions: {
], '1': { expression: '2 === 1' }
transitions: [ }
{ from: '2', to: '1', expression: '2 === 1' } }
], },
name: 'Default' initial: '1'
}); });
}); });
it('should override settings if transition already exists', () => { it('should override settings if transition already exists', () => {
const workflow = const workflow =
new WorkflowDto() new WorkflowDto()
.setStep('1', { color: '#00ff00' }) .setStep('1')
.setStep('2', { color: '#ff0000' }) .setStep('2')
.setTransition('1', '2', { expression: '1 === 2' }) .setTransition('2', '1', { expression: '2 === 1', role: 'Role' })
.setTransition('1', '2', { expression: '1 !== 2' }); .setTransition('2', '1', { expression: '2 !== 1' });
expect(simplify(workflow)).toEqual({ expect(workflow.serialize()).toEqual({
_links: {}, steps: {
steps: [ '1': { transitions: {} },
{ name: '1', color: '#00ff00' }, '2': {
{ name: '2', color: '#ff0000' } transitions: {
], '1': { expression: '2 !== 1', role: 'Role' }
transitions: [ }
{ from: '1', to: '2', expression: '1 !== 2' } }
], },
name: 'Default' initial: '1'
}); });
}); });
});
function simplify(value: any) { it('should return same workflow if transition to update not found by from step', () => {
return JSON.parse(JSON.stringify(value)); 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'
});
});
});

82
src/Squidex/app/shared/services/workflows.service.ts

@ -5,30 +5,21 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { import { compareStringsAsc, ResourceLinks } from '@app/framework';
compareStringsAsc,
Model,
ResourceLinks
} from '@app/framework';
export class WorkflowDto extends Model<WorkflowDto> { export class WorkflowDto {
public readonly _links: ResourceLinks; public readonly _links: ResourceLinks;
constructor(links: ResourceLinks = {}, constructor(links: ResourceLinks = {},
public readonly name: string = 'Default', public readonly initial?: string,
public readonly steps: WorkflowStep[] = [], public readonly steps: WorkflowStep[] = [],
public readonly transitions: WorkflowTransition[] = [], private readonly transitions: WorkflowTransition[] = []
public readonly initial?: string
) { ) {
super();
this._links = links;
}
public onCloned() {
this.steps.sort((a, b) => compareStringsAsc(a.name, b.name)); this.steps.sort((a, b) => compareStringsAsc(a.name, b.name));
this.transitions.sort((a, b) => compareStringsAsc(a.to, b.to)); this.transitions.sort((a, b) => compareStringsAsc(a.to, b.to));
this._links = links;
} }
public getOpenSteps(step: WorkflowStep) { public getOpenSteps(step: WorkflowStep) {
@ -43,7 +34,7 @@ export class WorkflowDto extends Model<WorkflowDto> {
return this.steps.find(x => x.name === name)!; return this.steps.find(x => x.name === name)!;
} }
public setStep(name: string, values: Partial<WorkflowStepValues>) { public setStep(name: string, values: Partial<WorkflowStepValues> = {}) {
const found = this.getStep(name); const found = this.getStep(name);
if (found) { if (found) {
@ -64,28 +55,40 @@ export class WorkflowDto extends Model<WorkflowDto> {
initial = steps[0].name; initial = steps[0].name;
} }
return this.with({ steps, initial }); return new WorkflowDto(this._links, initial, steps, this.transitions);
} }
public setInitial(initial: string) { public setInitial(initial: string) {
const found = this.getStep(initial); const found = this.getStep(initial);
if (!found || initial === 'Published') { if (!found || found.isLocked) {
return this; return this;
} }
return this.with({ initial }); return new WorkflowDto(this._links, initial, this.steps, this.transitions);
} }
public removeStep(name: string) { public removeStep(name: string) {
const steps = this.steps.filter(s => s.name !== name || s.isLocked); const steps = this.steps.filter(s => s.name !== name || s.isLocked);
if (steps.length === this.steps.length) {
return this;
}
const transitions = const transitions =
steps.length !== this.steps.length ? steps.length !== this.steps.length ?
this.transitions.filter(t => t.from !== name && t.to !== name) : this.transitions.filter(t => t.from !== name && t.to !== name) :
this.transitions; 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) { public renameStep(name: string, newName: string) {
@ -115,16 +118,26 @@ export class WorkflowDto extends Model<WorkflowDto> {
return transition; 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) { public removeTransition(from: string, to: string) {
const transitions = this.transitions.filter(t => t.from !== from || t.to !== to); 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<WorkflowTransitionValues>) { public setTransition(from: string, to: string, values: Partial<WorkflowTransitionValues> = {}) {
const stepFrom = this.getStep(from); const stepFrom = this.getStep(from);
if (!stepFrom) { if (!stepFrom) {
@ -147,7 +160,28 @@ export class WorkflowDto extends Model<WorkflowDto> {
const transitions = [...this.transitions.filter(t => t !== found), { from, to, ...values }]; 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;
} }
} }

Loading…
Cancel
Save