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
{
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>();
public static readonly Workflow Default = 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);
public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(EmptySteps, default);
public IReadOnlyDictionary<Status, WorkflowStep> Steps { 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;
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)
{
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]
public Workflows Set(Workflow workflow)
{
@ -35,6 +49,19 @@ namespace Squidex.Domain.Apps.Core.Contents
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()
{
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;
namespace Squidex.Domain.Apps.Core.Apps
namespace Squidex.Domain.Apps.Core
{
public abstract class Named
{

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

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

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

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

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

@ -24,8 +24,12 @@ import {
Versioned
} from '@app/framework';
export type WorkflowsDto = Versioned<WorkflowPayload>;
export type WorkflowPayload = { workflow: WorkflowDto; } & Resource;
export type WorkflowsDto = Versioned<WorkflowsPayload>;
export type WorkflowsPayload = {
readonly items: WorkflowDto[];
readonly canCreate: boolean;
} & Resource;
export class WorkflowDto {
public readonly _links: ResourceLinks;
@ -227,6 +231,10 @@ export type WorkflowTransition = { from: string; to: string } & WorkflowTransiti
export type WorkflowTransitionView = { step: WorkflowStep } & WorkflowTransition;
export interface CreateWorkflowDto {
readonly name: string;
}
@Injectable()
export class WorkflowsService {
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`);
return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => {
return parseWorkflowPayload(body);
return parseWorkflows(body);
}),
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 url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
mapVersioned(({ body }) => {
return parseWorkflowPayload(body);
return parseWorkflows(body);
}),
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) {
const { workflow, _links } = response;
function parseWorkflows(response: any) {
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) {

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

@ -47,7 +47,7 @@ describe('WorkflowsState', () => {
describe('Loading', () => {
it('should load workflow', () => {
workflowsService.setup(x => x.getWorkflow(app))
workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable();
workflowsState.load().subscribe();
@ -60,7 +60,7 @@ describe('WorkflowsState', () => {
});
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();
workflowsState.load(true).subscribe();
@ -73,7 +73,7 @@ describe('WorkflowsState', () => {
describe('Updates', () => {
beforeEach(() => {
workflowsService.setup(x => x.getWorkflow(app))
workflowsService.setup(x => x.getWorkflows(app))
.returns(() => of(versioned(version, oldWorkflow))).verifiable();
workflowsState.load().subscribe();

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

@ -59,7 +59,7 @@ export class WorkflowsState extends State<Snapshot> {
this.resetState();
}
return this.workflowsService.getWorkflow(this.appName).pipe(
return this.workflowsService.getWorkflows(this.appName).pipe(
tap(({ version, payload }) => {
if (isReload) {
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");
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);
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);
Assert.NotSame(roles_0, roles_1);
Assert.Empty(roles_1);
}
[Fact]

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

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Xunit;
@ -33,5 +34,47 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
Assert.Single(workflows_1);
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