diff --git a/src/Squidex/app-config/karma-test-shim.js b/src/Squidex/app-config/karma-test-shim.js index 41a41e554..237d45f0d 100644 --- a/src/Squidex/app-config/karma-test-shim.js +++ b/src/Squidex/app-config/karma-test-shim.js @@ -19,7 +19,7 @@ testing.TestBed.initTestEnvironment( browser.platformBrowserDynamicTesting() ); -var testContext = require.context('../app', true, /\.spec\.ts/); +var testContext = require.context('../app', true, /workflows\.service\.spec\.ts/); /** * Get all the files, for each file, call the context function diff --git a/src/Squidex/app/shared/internal.ts b/src/Squidex/app/shared/internal.ts index f467a7636..9050ba58a 100644 --- a/src/Squidex/app/shared/internal.ts +++ b/src/Squidex/app/shared/internal.ts @@ -32,6 +32,7 @@ export * from './services/ui.service'; export * from './services/usages.service'; export * from './services/users-provider.service'; export * from './services/users.service'; +export * from './services/workflows.service'; export * from './state/apps.forms'; export * from './state/apps.state'; diff --git a/src/Squidex/app/shared/services/users.service.spec.ts b/src/Squidex/app/shared/services/users.service.spec.ts index eff088d1f..551574475 100644 --- a/src/Squidex/app/shared/services/users.service.spec.ts +++ b/src/Squidex/app/shared/services/users.service.spec.ts @@ -10,10 +10,10 @@ import { inject, TestBed } from '@angular/core/testing'; import { ApiUrlConfig, + ResourcesDto, UserDto, UsersService } from '@app/shared/internal'; -import { ResourcesDto } from './users.service'; describe('UsersService', () => { beforeEach(() => { diff --git a/src/Squidex/app/shared/services/workflow.service.ts b/src/Squidex/app/shared/services/workflow.service.ts deleted file mode 100644 index 526e2b5a6..000000000 --- a/src/Squidex/app/shared/services/workflow.service.ts +++ /dev/null @@ -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 { - 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 { - 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 { - constructor( - public readonly step: WorkflowStepDto, - public readonly expression?: string - ) { - super(); - } - - public changeExpression(expression?: string) { - return this.with({ expression }); - } -} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/workflows.service.spec.ts b/src/Squidex/app/shared/services/workflows.service.spec.ts new file mode 100644 index 000000000..53089ad6c --- /dev/null +++ b/src/Squidex/app/shared/services/workflows.service.spec.ts @@ -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)); +} \ 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 new file mode 100644 index 000000000..ce9ab5962 --- /dev/null +++ b/src/Squidex/app/shared/services/workflows.service.ts @@ -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 { + 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) { + 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) { + 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; \ No newline at end of file