Browse Source

Updates workflows class and service layer.

pull/382/head
Sebastian Stehle 7 years ago
parent
commit
f7cf965fb8
  1. 66
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  2. 27
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  3. 2
      src/Squidex.Domain.Apps.Core.Model/Named.cs
  4. 2
      src/Squidex/app/shared/services/contributors.service.spec.ts
  5. 108
      src/Squidex/app/shared/services/workflows.service.spec.ts
  6. 63
      src/Squidex/app/shared/services/workflows.service.ts
  7. 6
      src/Squidex/app/shared/state/workflows.state.spec.ts
  8. 2
      src/Squidex/app/shared/state/workflows.state.ts
  9. 2
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs
  10. 2
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs
  11. 2
      tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs
  12. 43
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs

66
src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -9,49 +9,57 @@ using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
public sealed class Workflow public sealed class Workflow : Named
{ {
private const string DefaultName = "Name";
private static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>(); private static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
public static readonly Workflow Default = new Workflow( public static readonly Workflow Default = CreateDefault();
new Dictionary<Status, WorkflowStep> public static readonly Workflow Empty = new Workflow(EmptySteps, default);
{
[Status.Archived] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Archived, true),
[Status.Draft] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Published] = new WorkflowTransition()
},
StatusColors.Draft),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Published)
}, Status.Draft);
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; }
public Status Initial { get; } public Status Initial { get; }
public Workflow(IReadOnlyDictionary<Status, WorkflowStep> steps, Status initial) public Workflow(IReadOnlyDictionary<Status, WorkflowStep> steps, Status initial, string name = null)
: base(name ?? DefaultName)
{ {
Steps = steps ?? EmptySteps; Steps = steps ?? EmptySteps;
Initial = initial; Initial = initial;
} }
public static Workflow CreateDefault(string name = null)
{
return new Workflow(
new Dictionary<Status, WorkflowStep>
{
[Status.Archived] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Archived, true),
[Status.Draft] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Published] = new WorkflowTransition()
},
StatusColors.Draft),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Published)
}, Status.Draft, name);
}
public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status)
{ {
if (TryGetStep(status, out var step)) if (TryGetStep(status, out var step))

27
src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -27,6 +27,20 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
} }
[Pure]
public Workflows Remove(Guid id)
{
return new Workflows(Without(id));
}
[Pure]
public Workflows Add(string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
return new Workflows(With(Guid.NewGuid(), Workflow.CreateDefault(name)));
}
[Pure] [Pure]
public Workflows Set(Workflow workflow) public Workflows Set(Workflow workflow)
{ {
@ -35,6 +49,19 @@ namespace Squidex.Domain.Apps.Core.Contents
return new Workflows(With(Guid.Empty, workflow)); return new Workflows(With(Guid.Empty, workflow));
} }
[Pure]
public Workflows Update(Guid id, Workflow workflow)
{
Guard.NotNull(workflow, nameof(workflow));
if (!ContainsKey(id))
{
return this;
}
return new Workflows(With(id, workflow));
}
public Workflow GetFirst() public Workflow GetFirst()
{ {
return Values.FirstOrDefault() ?? Workflow.Default; return Values.FirstOrDefault() ?? Workflow.Default;

2
src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs → src/Squidex.Domain.Apps.Core.Model/Named.cs

@ -7,7 +7,7 @@
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core
{ {
public abstract class Named public abstract class Named
{ {

2
src/Squidex/app/shared/services/contributors.service.spec.ts

@ -119,7 +119,7 @@ describe('ContributorsService', () => {
function contributorsResponse(...ids: number[]) { function contributorsResponse(...ids: number[]) {
return { return {
items: ids.map(id => ({ items: ids.map(id => ({
contributorId: `id${id}`, role: id % 2 === 0 ? 'Owner' : 'Developer', contributorId: `id${id}`, role: id % 2 === 0 ? 'Owner' : 'Developer',
_links: { _links: {
update: { method: 'PUT', href: `/contributors/id${id}` } update: { method: 'PUT', href: `/contributors/id${id}` }

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

@ -13,9 +13,9 @@ import {
ApiUrlConfig, ApiUrlConfig,
Resource, Resource,
Version, Version,
Versioned,
WorkflowDto, WorkflowDto,
WorkflowPayload, WorkflowsDto,
WorkflowsPayload,
WorkflowsService WorkflowsService
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -43,10 +43,10 @@ describe('WorkflowsService', () => {
it('should make a get request to get app workflows', it('should make a get request to get app workflows',
inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => { inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => {
let workflow: Versioned<WorkflowPayload>; let workflows: WorkflowsDto;
workflowsService.getWorkflow('my-app').subscribe(result => { workflowsService.getWorkflows('my-app').subscribe(result => {
workflow = result; workflows = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow');
@ -54,29 +54,81 @@ describe('WorkflowsService', () => {
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(workflowsResponse('Draft'), req.flush(workflowsResponse('1', '2'),
{ {
headers: { headers: {
etag: '2' etag: '2'
} }
}); });
expect(workflow!).toEqual({ payload: createWorkflow('Draft'), version: new Version('2') }); expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') });
})); }));
it('should make a put request to assign a workflow', it('should make a put request to create a workflow',
inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => {
let workflows: WorkflowsDto;
workflowsService.postWorkflow('my-app', { name: 'New' }, version).subscribe(result => {
workflows = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow/123');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(workflowsResponse('1', '2'), {
headers: {
etag: '2'
}
});
expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') });
}));
it('should make a put request to update a workflow',
inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => { inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
update: { method: 'PUT', href: '/api/apps/my-app/workflow' } update: { method: 'PUT', href: '/api/apps/my-app/workflow/123' }
} }
}; };
let workflow: Versioned<WorkflowPayload>; let workflows: WorkflowsDto;
workflowsService.putWorkflow('my-app', resource, {}, version).subscribe(result => { workflowsService.putWorkflow('my-app', resource, {}, version).subscribe(result => {
workflow = result; workflows = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow/123');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(workflowsResponse('1', '2'), {
headers: {
etag: '2'
}
});
expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') });
}));
it('should make a delete request to delete a workflow',
inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
delete: { method: 'DELETE', href: '/api/apps/my-app/workflow/123' }
}
};
let workflows: WorkflowsDto;
workflowsService.deleteWorkflow('my-app', resource, version).subscribe(result => {
workflows = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow');
@ -84,16 +136,25 @@ describe('WorkflowsService', () => {
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(workflowsResponse('Draft'), { req.flush(workflowsResponse('1', '2'), {
headers: { headers: {
etag: '2' etag: '2'
} }
}); });
expect(workflow!).toEqual({ payload: createWorkflow('Draft'), version: new Version('2') }); expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') });
})); }));
function workflowsResponse(name: string) { function workflowsResponse(...names: string[]) {
return {
items: names.map(name => workflowResponse(name)),
_links: {
create: { method: 'POST', href: '/workflows' }
}
};
}
function workflowResponse(name: string) {
return { return {
workflow: { workflow: {
steps: { steps: {
@ -125,10 +186,19 @@ describe('WorkflowsService', () => {
} }
}); });
export function createWorkflow(name: string): WorkflowPayload { export function createWorkflows(...names: string[]): WorkflowsPayload {
return { return {
workflow: new WorkflowDto({ items: names.map(name => createWorkflow(name)),
update: { method: 'PUT', href: '/api/workflows' } _links: {
create: { method: 'POST', href: '/workflows' }
},
canCreate: true
};
}
export function createWorkflow(name: string): WorkflowDto {
return new WorkflowDto({
update: { method: 'PUT', href: '/workflows' }
}, },
`${name}1`, `${name}1`,
[ [
@ -138,9 +208,7 @@ export function createWorkflow(name: string): WorkflowPayload {
[ [
{ from: `${name}1`, to: `${name}2`, expression: 'Expression1', role: 'Role1' }, { from: `${name}1`, to: `${name}2`, expression: 'Expression1', role: 'Role1' },
{ from: `${name}2`, to: `${name}1`, expression: 'Expression2', role: 'Role2' } { from: `${name}2`, to: `${name}1`, expression: 'Expression2', role: 'Role2' }
]), ]);
_links: {}
};
} }
describe('Workflow', () => { describe('Workflow', () => {

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

@ -24,8 +24,12 @@ import {
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export type WorkflowsDto = Versioned<WorkflowPayload>; export type WorkflowsDto = Versioned<WorkflowsPayload>;
export type WorkflowPayload = { workflow: WorkflowDto; } & Resource; export type WorkflowsPayload = {
readonly items: WorkflowDto[];
readonly canCreate: boolean;
} & Resource;
export class WorkflowDto { export class WorkflowDto {
public readonly _links: ResourceLinks; public readonly _links: ResourceLinks;
@ -227,6 +231,10 @@ export type WorkflowTransition = { from: string; to: string } & WorkflowTransiti
export type WorkflowTransitionView = { step: WorkflowStep } & WorkflowTransition; export type WorkflowTransitionView = { step: WorkflowStep } & WorkflowTransition;
export interface CreateWorkflowDto {
readonly name: string;
}
@Injectable() @Injectable()
export class WorkflowsService { export class WorkflowsService {
constructor( constructor(
@ -236,38 +244,69 @@ export class WorkflowsService {
) { ) {
} }
public getWorkflow(appName: string): Observable<Versioned<WorkflowPayload>> { public getWorkflows(appName: string): Observable<WorkflowsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflow`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflow`);
return HTTP.getVersioned(this.http, url).pipe( return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => { mapVersioned(({ body }) => {
return parseWorkflowPayload(body); return parseWorkflows(body);
}), }),
pretifyError('Failed to load workflows. Please reload.')); pretifyError('Failed to load workflows. Please reload.'));
} }
public putWorkflow(appName: string, resource: Resource, dto: any, version: Version): Observable<Versioned<WorkflowPayload>> { public postWorkflow(appName: string, dto: CreateWorkflowDto, version: Version): Observable<WorkflowsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflow`);
return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(({ body }) => {
return parseWorkflows(body);
}),
tap(() => {
this.analytics.trackEvent('Workflow', 'Created', appName);
}),
pretifyError('Failed to create workflow. Please reload.'));
}
public putWorkflow(appName: string, resource: Resource, dto: any, version: Version): Observable<WorkflowsDto> {
const link = resource._links['update']; const link = resource._links['update'];
const url = this.apiUrl.buildUrl(link.href); const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
mapVersioned(({ body }) => { mapVersioned(({ body }) => {
return parseWorkflowPayload(body); return parseWorkflows(body);
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Workflow', 'Configured', appName); this.analytics.trackEvent('Workflow', 'Updated', appName);
}), }),
pretifyError('Failed to configure Workflow. Please reload.')); pretifyError('Failed to update Workflow. Please reload.'));
}
public deleteWorkflow(appName: string, resource: Resource, version: Version): Observable<WorkflowsDto> {
const link = resource._links['delete'];
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
mapVersioned(({ body }) => {
return parseWorkflows(body);
}),
tap(() => {
this.analytics.trackEvent('Workflow', 'Deleted', appName);
}),
pretifyError('Failed to delete Workflow. Please reload.'));
} }
} }
function parseWorkflowPayload(response: any) { function parseWorkflows(response: any) {
const { workflow, _links } = response; const raw: any[] = response.items;
const items = raw.map(item =>
parseWorkflow(item));
const result = parseWorkflow(workflow); const { _links } = response;
return { workflow: result, _links }; return { items, _links, canCreate: hasAnyLink(_links, 'create') };
} }
function parseWorkflow(workflow: any) { function parseWorkflow(workflow: any) {

6
src/Squidex/app/shared/state/workflows.state.spec.ts

@ -47,7 +47,7 @@ describe('WorkflowsState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load workflow', () => { it('should load workflow', () => {
workflowsService.setup(x => x.getWorkflow(app)) workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable(); .returns(() => of(versioned(version, oldWorkflow))).verifiable();
workflowsState.load().subscribe(); workflowsState.load().subscribe();
@ -60,7 +60,7 @@ describe('WorkflowsState', () => {
}); });
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
workflowsService.setup(x => x.getWorkflow(app)) workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable(); .returns(() => of(versioned(version, oldWorkflow))).verifiable();
workflowsState.load(true).subscribe(); workflowsState.load(true).subscribe();
@ -73,7 +73,7 @@ describe('WorkflowsState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
workflowsService.setup(x => x.getWorkflow(app)) workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable(); .returns(() => of(versioned(version, oldWorkflow))).verifiable();
workflowsState.load().subscribe(); workflowsState.load().subscribe();

2
src/Squidex/app/shared/state/workflows.state.ts

@ -59,7 +59,7 @@ export class WorkflowsState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
return this.workflowsService.getWorkflow(this.appName).pipe( return this.workflowsService.getWorkflows(this.appName).pipe(
tap(({ version, payload }) => { tap(({ version, payload }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Workflow reloaded.'); this.dialogs.notifyInfo('Workflow reloaded.');

2
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs

@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var clients_1 = clients_0.Revoke("2"); var clients_1 = clients_0.Revoke("2");
Assert.NotSame(clients_0, clients_1); Assert.Empty(clients_1);
} }
} }
} }

2
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var patterns_1 = patterns_0.Remove(id); var patterns_1 = patterns_0.Remove(id);
Assert.NotSame(patterns_0, patterns_1); Assert.Empty(patterns_1);
} }
} }
} }

2
tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs

@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var roles_1 = roles_0.Remove(role); var roles_1 = roles_0.Remove(role);
Assert.NotSame(roles_0, roles_1); Assert.Empty(roles_1);
} }
[Fact] [Fact]

43
tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Xunit; using Xunit;
@ -33,5 +34,47 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
Assert.Single(workflows_1); Assert.Single(workflows_1);
Assert.Same(Workflow.Default, workflows_1[Guid.Empty]); Assert.Same(Workflow.Default, workflows_1[Guid.Empty]);
} }
[Fact]
public void Should_add_new_workflow_with_default_states()
{
var workflows_1 = workflows_0.Add("1");
Assert.Equal(workflows_1.GetFirst().Steps.Keys, new[] { Status.Archived, Status.Draft, Status.Published });
}
[Fact]
public void Should_update_workflow()
{
var workflows_1 = workflows_0.Add("1");
var workflows_2 = workflows_1.Update(workflows_1.Keys.First(), Workflow.Empty);
Assert.Empty(workflows_2.GetFirst().Steps.Keys);
}
[Fact]
public void Should_do_nothing_if_workflow_to_update_not_found()
{
var workflows_1 = workflows_0.Update(Guid.NewGuid(), Workflow.Empty);
Assert.Same(workflows_0, workflows_1);
}
[Fact]
public void Should_remove_workflow()
{
var workflows_1 = workflows_0.Add("1");
var workflows_2 = workflows_1.Remove(workflows_1.Keys.First());
Assert.Empty(workflows_2);
}
[Fact]
public void Should_do_nothing_if_workflow_to_remove_not_found()
{
var workflows_1 = workflows_0.Remove(Guid.NewGuid());
Assert.Empty(workflows_1);
}
} }
} }

Loading…
Cancel
Save