mirror of https://github.com/Squidex/squidex.git
6 changed files with 290 additions and 140 deletions
@ -1,138 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
import { Model, ResourceLinks } from '@app/shared'; |
|
||||
|
|
||||
export class WorkflowDto extends Model<WorkflowDto> { |
|
||||
public readonly _links: ResourceLinks; |
|
||||
|
|
||||
constructor(links: ResourceLinks, |
|
||||
public readonly name: string, |
|
||||
public readonly steps: WorkflowStepDto[] |
|
||||
) { |
|
||||
super(); |
|
||||
|
|
||||
this._links = links; |
|
||||
} |
|
||||
|
|
||||
public addStep(name: string, color: string) { |
|
||||
let found = this.steps.find(x => x.name === name); |
|
||||
|
|
||||
if (found) { |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
const steps = [...this.steps, new WorkflowStepDto(name, color, [])]; |
|
||||
|
|
||||
return this.with({ steps }); |
|
||||
} |
|
||||
|
|
||||
public changeStepColor(name: string, color: string) { |
|
||||
return this.updateStep(name, x => x.changeColor(color)); |
|
||||
} |
|
||||
|
|
||||
public changeStepName(name: string, newName: string) { |
|
||||
return this.updateStep(name, x => x.changeName(newName)); |
|
||||
} |
|
||||
|
|
||||
public changeTransition(from: string, to: string, expression?: string) { |
|
||||
return this.updateStep(from, x => x.changeExpression(to, expression)); |
|
||||
} |
|
||||
|
|
||||
public removeStep(name: string) { |
|
||||
const steps = this.steps.filter(x => x.name !== name).map(x => x.removeTransition(name)); |
|
||||
|
|
||||
return this.with({ steps }); |
|
||||
} |
|
||||
|
|
||||
private updateStep(name: string, updater: (step: WorkflowStepDto) => WorkflowStepDto) { |
|
||||
let found = this.steps.find(x => x.name === name); |
|
||||
|
|
||||
if (!found) { |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
const newStep = updater(found); |
|
||||
|
|
||||
const steps = this.steps.map(x => x.name === name ? newStep : x.replaceTransition(newStep)); |
|
||||
|
|
||||
return this.with({ steps }); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export class WorkflowStepDto extends Model<WorkflowStepDto> { |
|
||||
constructor( |
|
||||
public readonly name: string, |
|
||||
public readonly color: string, |
|
||||
public readonly transitions: WorkflowTransitionDto[] |
|
||||
) { |
|
||||
super(); |
|
||||
} |
|
||||
|
|
||||
public changeName(name: string) { |
|
||||
return this.with({ name }); |
|
||||
} |
|
||||
|
|
||||
public changeColor(color: string) { |
|
||||
return this.with({ color }); |
|
||||
} |
|
||||
|
|
||||
public changeExpression(to: string, expression?: string) { |
|
||||
return this.updateTransition(to, x => x.changeExpression(expression)); |
|
||||
} |
|
||||
|
|
||||
public addTransition(step: WorkflowStepDto) { |
|
||||
let found = this.transitions.find(x => x.step.name === step.name); |
|
||||
|
|
||||
if (found) { |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
const transitions = [...this.transitions, new WorkflowTransitionDto(step) ]; |
|
||||
|
|
||||
return this.with({ transitions }); |
|
||||
} |
|
||||
|
|
||||
public replaceTransition(step: WorkflowStepDto) { |
|
||||
const transitions = this.transitions.map(x => x.step.name === step.name ? new WorkflowTransitionDto(step, x.expression) : x); |
|
||||
|
|
||||
return this.with({ transitions }); |
|
||||
} |
|
||||
|
|
||||
public removeTransition(to: string) { |
|
||||
const transitions = this.transitions.filter(x => x.step.name !== to); |
|
||||
|
|
||||
return this.with({ transitions }); |
|
||||
} |
|
||||
|
|
||||
private updateTransition(to: string, updater: (step: WorkflowTransitionDto) => WorkflowTransitionDto) { |
|
||||
let found = this.transitions.find(x => x.step.name === to); |
|
||||
|
|
||||
if (!found) { |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
const newTransition = updater(found); |
|
||||
|
|
||||
const transitions = this.transitions.map(x => x === found ? newTransition : x); |
|
||||
|
|
||||
return this.with({ transitions }); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export class WorkflowTransitionDto extends Model<WorkflowTransitionDto> { |
|
||||
constructor( |
|
||||
public readonly step: WorkflowStepDto, |
|
||||
public readonly expression?: string |
|
||||
) { |
|
||||
super(); |
|
||||
} |
|
||||
|
|
||||
public changeExpression(expression?: string) { |
|
||||
return this.with({ expression }); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,188 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { WorkflowDto } from '@app/shared/internal'; |
||||
|
|
||||
|
describe('Workflow', () => { |
||||
|
it('should create empty workflow', () => { |
||||
|
const workflow = new WorkflowDto(); |
||||
|
|
||||
|
expect(workflow.name).toEqual('Default'); |
||||
|
}); |
||||
|
|
||||
|
it('should add step to workflow', () => { |
||||
|
const workflow = |
||||
|
new WorkflowDto() |
||||
|
.setStep('1', { color: '#00ff00' }); |
||||
|
|
||||
|
expect(simplify(workflow)).toEqual({ |
||||
|
_links: {}, |
||||
|
steps: [ |
||||
|
{ name: '1', color: '#00ff00' } |
||||
|
], |
||||
|
transitions: [], |
||||
|
name: 'Default' |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should override settings if step already exists', () => { |
||||
|
const workflow = |
||||
|
new WorkflowDto() |
||||
|
.setStep('1', { color: '#00ff00' }) |
||||
|
.setStep('1', { color: 'red' }); |
||||
|
|
||||
|
expect(simplify(workflow)).toEqual({ |
||||
|
_links: {}, |
||||
|
steps: [ |
||||
|
{ name: '1', color: 'red' } |
||||
|
], |
||||
|
transitions: [], |
||||
|
name: 'Default' |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should not remove step if locked', () => { |
||||
|
const workflow = |
||||
|
new WorkflowDto() |
||||
|
.setStep('1', { color: '#00ff00', isLocked: true }) |
||||
|
.setStep('2', { color: '#ff0000' }) |
||||
|
.setTransition('1', '2', { expression: '1 === 2' }) |
||||
|
.removeStep('1'); |
||||
|
|
||||
|
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' |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should remove step', () => { |
||||
|
const workflow = |
||||
|
new WorkflowDto() |
||||
|
.setStep('1', { color: '#00ff00' }) |
||||
|
.setStep('2', { color: '#ff0000' }) |
||||
|
.setStep('3', { color: '#0000ff' }) |
||||
|
.setTransition('1', '2', { expression: '1 === 2' }) |
||||
|
.setTransition('1', '3', { expression: '1 === 3' }) |
||||
|
.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' |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should rename step', () => { |
||||
|
const workflow = |
||||
|
new WorkflowDto() |
||||
|
.setStep('1', { color: '#00ff00' }) |
||||
|
.setStep('2', { color: '#ff0000' }) |
||||
|
.setStep('3', { color: '#0000ff' }) |
||||
|
.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' |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should add transitions to workflow', () => { |
||||
|
const workflow = |
||||
|
new WorkflowDto() |
||||
|
.setStep('1', { color: '#00ff00' }) |
||||
|
.setStep('2', { color: '#ff0000' }) |
||||
|
.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' |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should add remove transition from workflow', () => { |
||||
|
const workflow = |
||||
|
new WorkflowDto() |
||||
|
.setStep('1', { color: '#00ff00' }) |
||||
|
.setStep('2', { color: '#ff0000' }) |
||||
|
.setTransition('1', '2', { expression: '1 === 1' }) |
||||
|
.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' |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
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' |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
function simplify(value: any) { |
||||
|
return JSON.parse(JSON.stringify(value)); |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Model, ResourceLinks } from '@app/framework'; |
||||
|
|
||||
|
export class WorkflowDto extends Model<WorkflowDto> { |
||||
|
public readonly _links: ResourceLinks; |
||||
|
|
||||
|
constructor(links: ResourceLinks = {}, |
||||
|
public readonly name: string = 'Default', |
||||
|
public readonly steps: WorkflowStep[] = [], |
||||
|
public readonly transitions: WorkflowTransition[] = [] |
||||
|
) { |
||||
|
super(); |
||||
|
|
||||
|
this._links = links; |
||||
|
} |
||||
|
|
||||
|
public setStep(name: string, values: Partial<WorkflowStepValues>) { |
||||
|
const steps = [...this.steps.filter(s => s.name !== name), { name, ...values }]; |
||||
|
|
||||
|
return this.with({ steps }); |
||||
|
} |
||||
|
|
||||
|
public removeStep(name: string) { |
||||
|
const steps = this.steps.filter(s => s.name !== name || s.isLocked); |
||||
|
|
||||
|
const transitions = |
||||
|
steps.length !== this.steps.length ? |
||||
|
this.transitions.filter(t => t.from !== name && t.to !== name) : |
||||
|
this.transitions; |
||||
|
|
||||
|
return this.with({ steps, transitions }); |
||||
|
} |
||||
|
|
||||
|
public renameStep(name: string, newName: string) { |
||||
|
const steps = this.steps.map(step => { |
||||
|
if (step.name === name) { |
||||
|
return { ...step, name: newName }; |
||||
|
} |
||||
|
|
||||
|
return step; |
||||
|
}); |
||||
|
|
||||
|
const transitions = this.transitions.map(transition => { |
||||
|
if (transition.from === name || transition.to === name) { |
||||
|
let newTransition = { ...transition }; |
||||
|
|
||||
|
if (newTransition.from === name) { |
||||
|
newTransition.from = newName; |
||||
|
} |
||||
|
|
||||
|
if (newTransition.to === name) { |
||||
|
newTransition.to = newName; |
||||
|
} |
||||
|
|
||||
|
return newTransition; |
||||
|
} |
||||
|
|
||||
|
return transition; |
||||
|
}); |
||||
|
|
||||
|
return this.with({ steps, transitions }); |
||||
|
} |
||||
|
|
||||
|
public removeTransition(from: string, to: string) { |
||||
|
const transitions = this.transitions.filter(t => t.from !== from || t.to !== to); |
||||
|
|
||||
|
return this.with({ transitions }); |
||||
|
} |
||||
|
|
||||
|
public setTransition(from: string, to: string, values: Partial<WorkflowTransitionValues>) { |
||||
|
const stepFrom = this.steps.find(s => s.name === from); |
||||
|
|
||||
|
if (!stepFrom) { |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
const stepTo = this.steps.find(s => s.name === to); |
||||
|
|
||||
|
if (!stepTo) { |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
const transitions = [...this.transitions.filter(t => t.from !== from || t.to !== to), { from, to, ...values }]; |
||||
|
|
||||
|
return this.with({ transitions }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export type WorkflowStepValues = { color?: string; isLocked?: boolean; }; |
||||
|
export type WorkflowStep = { name: string } & WorkflowStepValues; |
||||
|
|
||||
|
export type WorkflowTransitionValues = { expression?: string }; |
||||
|
export type WorkflowTransition = { from: string; to: string } & WorkflowTransitionValues; |
||||
Loading…
Reference in new issue