From f7cf965fb88e0a6d4db5ec5ea00a54ad37f0f7fb Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 1 Jul 2019 16:33:25 +0200 Subject: [PATCH 01/18] Updates workflows class and service layer. --- .../Contents/Workflow.cs | 66 ++++++----- .../Contents/Workflows.cs | 27 +++++ .../{Apps => }/Named.cs | 2 +- .../services/contributors.service.spec.ts | 2 +- .../shared/services/workflows.service.spec.ts | 108 ++++++++++++++---- .../app/shared/services/workflows.service.ts | 63 ++++++++-- .../app/shared/state/workflows.state.spec.ts | 6 +- .../app/shared/state/workflows.state.ts | 2 +- .../Model/Apps/AppClientsTests.cs | 2 +- .../Model/Apps/AppPatternsTests.cs | 2 +- .../Model/Apps/RolesTests.cs | 2 +- .../Model/Contents/WorkflowsTests.cs | 43 +++++++ 12 files changed, 255 insertions(+), 70 deletions(-) rename src/Squidex.Domain.Apps.Core.Model/{Apps => }/Named.cs (93%) diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs index 859e546ef..c04a8ecc5 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ b/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 EmptySteps = new Dictionary(); - public static readonly Workflow Default = new Workflow( - new Dictionary - { - [Status.Archived] = - new WorkflowStep( - new Dictionary - { - [Status.Draft] = new WorkflowTransition() - }, - StatusColors.Archived, true), - [Status.Draft] = - new WorkflowStep( - new Dictionary - { - [Status.Archived] = new WorkflowTransition(), - [Status.Published] = new WorkflowTransition() - }, - StatusColors.Draft), - [Status.Published] = - new WorkflowStep( - new Dictionary - { - [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 Steps { get; } public Status Initial { get; } - public Workflow(IReadOnlyDictionary steps, Status initial) + public Workflow(IReadOnlyDictionary 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.Archived] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Archived, true), + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Archived] = new WorkflowTransition(), + [Status.Published] = new WorkflowTransition() + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [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)) diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index d027b8d32..2ffadc99d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/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; diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs b/src/Squidex.Domain.Apps.Core.Model/Named.cs similarity index 93% rename from src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs rename to src/Squidex.Domain.Apps.Core.Model/Named.cs index 69ba9a3c1..fd76c4e8f 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs +++ b/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 { diff --git a/src/Squidex/app/shared/services/contributors.service.spec.ts b/src/Squidex/app/shared/services/contributors.service.spec.ts index 250d4bba0..20f6c27d5 100644 --- a/src/Squidex/app/shared/services/contributors.service.spec.ts +++ b/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}` } diff --git a/src/Squidex/app/shared/services/workflows.service.spec.ts b/src/Squidex/app/shared/services/workflows.service.spec.ts index cdf96ce7f..09afc1d23 100644 --- a/src/Squidex/app/shared/services/workflows.service.spec.ts +++ b/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; + 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; + 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', () => { diff --git a/src/Squidex/app/shared/services/workflows.service.ts b/src/Squidex/app/shared/services/workflows.service.ts index ce9861f10..52e1ef3cd 100644 --- a/src/Squidex/app/shared/services/workflows.service.ts +++ b/src/Squidex/app/shared/services/workflows.service.ts @@ -24,8 +24,12 @@ import { Versioned } from '@app/framework'; -export type WorkflowsDto = Versioned; -export type WorkflowPayload = { workflow: WorkflowDto; } & Resource; +export type WorkflowsDto = Versioned; +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> { + public getWorkflows(appName: string): Observable { 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> { + public postWorkflow(appName: string, dto: CreateWorkflowDto, version: Version): Observable { + 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 { 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 { + 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) { diff --git a/src/Squidex/app/shared/state/workflows.state.spec.ts b/src/Squidex/app/shared/state/workflows.state.spec.ts index 4ad671a27..ae3b646c2 100644 --- a/src/Squidex/app/shared/state/workflows.state.spec.ts +++ b/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(); diff --git a/src/Squidex/app/shared/state/workflows.state.ts b/src/Squidex/app/shared/state/workflows.state.ts index b43a558e0..42bdbf2c0 100644 --- a/src/Squidex/app/shared/state/workflows.state.ts +++ b/src/Squidex/app/shared/state/workflows.state.ts @@ -59,7 +59,7 @@ export class WorkflowsState extends State { 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.'); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 9f95ac4b2..811dd97ef 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/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); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs index 56d615159..5e22067b7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs +++ b/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); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index e8337ac4e..ac89aa8ce 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/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] diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs index 37ce537c4..c81db61cb 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs +++ b/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); + } } } From d903691702862ff91170c231e97d58a3634f3e19 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 1 Jul 2019 16:57:55 +0200 Subject: [PATCH 02/18] Workflow management started. --- .../Apps/AppGrain.cs | 42 ++++++++++++++++--- .../Apps/Commands/AddWorkflow.cs | 14 +++++++ .../Apps/Commands/DeleteWorkflow.cs | 16 +++++++ ...ConfigureWorkflow.cs => UpdateWorkflow.cs} | 5 ++- .../Apps/Guards/GuardAppWorkflows.cs | 13 +++++- .../Apps/AppWorkflowAdded.cs | 17 ++++++++ .../Apps/AppWorkflowDeleted.cs | 18 ++++++++ ...lowConfigured.cs => AppWorkflowUpdated.cs} | 7 +++- .../Apps/Models/UpsertWorkflowDto.cs | 4 +- .../Apps/AppGrainTests.cs | 2 +- .../Apps/Guards/GuardAppWorkflowTests.cs | 32 +++++++------- .../OldEvents/AppWorkflowConfigured.cs | 29 +++++++++++++ 12 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs rename src/Squidex.Domain.Apps.Entities/Apps/Commands/{ConfigureWorkflow.cs => UpdateWorkflow.cs} (82%) create mode 100644 src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs create mode 100644 src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs rename src/Squidex.Domain.Apps.Events/Apps/{AppWorkflowConfigured.cs => AppWorkflowUpdated.cs} (78%) create mode 100644 tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index e17c17290..95ba1cefc 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -119,12 +119,32 @@ namespace Squidex.Domain.Apps.Entities.Apps return Snapshot; }); - case ConfigureWorkflow configureWorkflow: - return UpdateReturn(configureWorkflow, c => + case AddWorkflow addWorkflow: + return UpdateReturn(addWorkflow, c => { - GuardAppWorkflows.CanConfigure(c); + GuardAppWorkflows.CanAdd(c); - ConfigureWorkflow(c); + AddWorkflow(c); + + return Snapshot; + }); + + case UpdateWorkflow updateWorkflow: + return UpdateReturn(updateWorkflow, c => + { + GuardAppWorkflows.CanUpdate(Snapshot.Workflows, c); + + UpdateWorkflow(c); + + return Snapshot; + }); + + case DeleteWorkflow deleteWorkflow: + return UpdateReturn(deleteWorkflow, c => + { + GuardAppWorkflows.CanDelete(Snapshot.Workflows, c); + + DeleteWorkflow(c); return Snapshot; }); @@ -329,9 +349,19 @@ namespace Squidex.Domain.Apps.Entities.Apps RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); } - public void ConfigureWorkflow(ConfigureWorkflow command) + public void AddWorkflow(AddWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded())); + } + + public void UpdateWorkflow(UpdateWorkflow command) + { + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated())); + } + + public void DeleteWorkflow(DeleteWorkflow command) { - RaiseEvent(SimpleMapper.Map(command, new AppWorkflowConfigured())); + RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted())); } public void AddLanguage(AddLanguage command) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs new file mode 100644 index 000000000..3b70a4c68 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AddWorkflow : AppCommand + { + public string Name { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs new file mode 100644 index 000000000..c21492e79 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class DeleteWorkflow : AppCommand + { + public Guid WorkflowId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs similarity index 82% rename from src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureWorkflow.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs index efa2b503b..635936040 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs @@ -5,12 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class ConfigureWorkflow : AppCommand + public sealed class UpdateWorkflow : AppCommand { + public Guid WorkflowId { get; set; } + public Workflow Workflow { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs index 1e675ac8e..b802d55ef 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; @@ -13,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { public static class GuardAppWorkflows { - public static void CanConfigure(ConfigureWorkflow command) + public static void CanUpdate(Workflows workflows, UpdateWorkflow command) { Guard.NotNull(command, nameof(command)); @@ -72,5 +73,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } }); } + + internal static void CanAdd(AddWorkflow c) + { + throw new NotImplementedException(); + } + + internal static void CanDelete(Workflows workflows, DeleteWorkflow c) + { + throw new NotImplementedException(); + } } } diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs new file mode 100644 index 000000000..728b04b83 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppWorkflowAdded))] + public sealed class AppWorkflowAdded : AppEvent + { + public string Name { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs new file mode 100644 index 000000000..15d418994 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowDeleted.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppWorkflowDeleted))] + public sealed class AppWorkflowDeleted : AppEvent + { + public Guid WorkflowId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowConfigured.cs b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs similarity index 78% rename from src/Squidex.Domain.Apps.Events/Apps/AppWorkflowConfigured.cs rename to src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs index 65166ae97..672242ed8 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowConfigured.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowUpdated.cs @@ -5,14 +5,17 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Apps { - [EventType(nameof(AppWorkflowConfigured))] - public sealed class AppWorkflowConfigured : AppEvent + [EventType(nameof(AppWorkflowUpdated))] + public sealed class AppWorkflowUpdated : AppEvent { + public Guid WorkflowId { get; set; } + public Workflow Workflow { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs index d808c60b6..37577e9e5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs @@ -26,7 +26,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public Status Initial { get; set; } - public ConfigureWorkflow ToCommand() + public UpdateWorkflow ToCommand() { var workflow = new Workflow( Steps?.ToDictionary( @@ -39,7 +39,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models x.Value.NoUpdate)), Initial); - return new ConfigureWorkflow { Workflow = workflow }; + return new UpdateWorkflow { Workflow = workflow }; } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index ac892f78a..a584ef196 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -301,7 +301,7 @@ namespace Squidex.Domain.Apps.Entities.Apps [Fact] public async Task ConfigureWorkflow_should_create_events_and_update_state() { - var command = new ConfigureWorkflow { Workflow = Workflow.Default }; + var command = new UpdateWorkflow { Workflow = Workflow.Default }; await ExecuteCreateAsync(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs index 99f2f32ca..ce724c8c0 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs @@ -19,16 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Fact] public void CanConfigure_should_throw_exception_if_workflow_is_not_defined() { - var command = new ConfigureWorkflow(); + var command = new UpdateWorkflow(); - ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), new ValidationError("Workflow is required.", "Workflow")); } [Fact] public void CanConfigure_should_throw_exception_if_workflow_has_no_initial_step() { - var command = new ConfigureWorkflow + var command = new UpdateWorkflow { Workflow = new Workflow( new Dictionary @@ -38,14 +38,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards default) }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), new ValidationError("Initial step is required.", "Workflow.Initial")); } [Fact] public void CanConfigure_should_throw_exception_if_initial_step_is_published() { - var command = new ConfigureWorkflow + var command = new UpdateWorkflow { Workflow = new Workflow( new Dictionary @@ -55,14 +55,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Status.Published) }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); } [Fact] public void CanConfigure_should_throw_exception_if_workflow_does_not_have_published_state() { - var command = new ConfigureWorkflow + var command = new UpdateWorkflow { Workflow = new Workflow( new Dictionary @@ -72,14 +72,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Status.Draft) }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), new ValidationError("Workflow must have a published step.", "Workflow.Steps")); } [Fact] public void CanConfigure_should_throw_exception_if_workflow_step_is_not_defined() { - var command = new ConfigureWorkflow + var command = new UpdateWorkflow { Workflow = new Workflow( new Dictionary @@ -90,14 +90,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Status.Draft) }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), new ValidationError("Step is required.", "Workflow.Steps.Published")); } [Fact] public void CanConfigure_should_throw_exception_if_workflow_transition_is_invalid() { - var command = new ConfigureWorkflow + var command = new UpdateWorkflow { Workflow = new Workflow( new Dictionary @@ -113,14 +113,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Status.Draft) }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); } [Fact] public void CanConfigure_should_throw_exception_if_workflow_transition_is_not_defined() { - var command = new ConfigureWorkflow + var command = new UpdateWorkflow { Workflow = new Workflow( new Dictionary @@ -137,16 +137,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Status.Draft) }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); } [Fact] public void CanConfigure_should_not_throw_exception_if_workflow_is_valid() { - var command = new ConfigureWorkflow { Workflow = Workflow.Default }; + var command = new UpdateWorkflow { Workflow = Workflow.Default }; - GuardAppWorkflows.CanConfigure(command); + GuardAppWorkflows.CanUpdate(command); } } } diff --git a/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs b/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs new file mode 100644 index 000000000..df6da26bf --- /dev/null +++ b/tools/Migrate_01/OldEvents/AppWorkflowConfigured.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Migrate_01.OldEvents +{ + [EventType(nameof(AppWorkflowConfigured))] + [Obsolete] + public sealed class AppWorkflowConfigured : AppEvent, IMigrated + { + public Workflow Workflow { get; set; } + + public IEvent Migrate() + { + return SimpleMapper.Map(this, new AppWorkflowUpdated()); + } + } +} From d7624866b9e0c8b5409bfebc41171df22a513365 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 1 Jul 2019 17:27:41 +0200 Subject: [PATCH 03/18] App grain stuff. --- .../Contents/Workflows.cs | 9 +- .../Apps/Commands/AddWorkflow.cs | 9 ++ .../Apps/Guards/GuardAppWorkflows.cs | 32 +++++- .../Apps/State/AppState.cs | 14 ++- .../Apps/AppWorkflowAdded.cs | 3 + .../Model/Apps/AppClientsTests.cs | 2 +- .../Model/Apps/AppPatternsTests.cs | 2 +- .../Model/Apps/RolesTests.cs | 2 +- .../Model/Contents/WorkflowsTests.cs | 27 +++-- .../Apps/AppGrainTests.cs | 52 ++++++++- .../Apps/Guards/GuardAppPatternsTests.cs | 4 +- .../Apps/Guards/GuardAppRolesTests.cs | 4 +- .../Apps/Guards/GuardAppWorkflowTests.cs | 108 ++++++++++++++---- 13 files changed, 218 insertions(+), 50 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index 2ffadc99d..3675f1b81 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -34,11 +34,11 @@ namespace Squidex.Domain.Apps.Core.Contents } [Pure] - public Workflows Add(string name) + public Workflows Add(Guid workflowId, string name) { Guard.NotNullOrEmpty(name, nameof(name)); - return new Workflows(With(Guid.NewGuid(), Workflow.CreateDefault(name))); + return new Workflows(With(workflowId, Workflow.CreateDefault(name))); } [Pure] @@ -54,6 +54,11 @@ namespace Squidex.Domain.Apps.Core.Contents { Guard.NotNull(workflow, nameof(workflow)); + if (id == Guid.Empty) + { + return Set(workflow); + } + if (!ContainsKey(id)) { return this; diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs index 3b70a4c68..54ca7b4bb 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs @@ -5,10 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + namespace Squidex.Domain.Apps.Entities.Apps.Commands { public sealed class AddWorkflow : AppCommand { + public Guid WorkflowId { get; set; } + public string Name { get; set; } + + public AddWorkflow() + { + WorkflowId = Guid.NewGuid(); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs index b802d55ef..738b2f70a 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs @@ -14,11 +14,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { public static class GuardAppWorkflows { + public static void CanAdd(AddWorkflow command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add workflow.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + }); + } + public static void CanUpdate(Workflows workflows, UpdateWorkflow command) { Guard.NotNull(command, nameof(command)); - Validate.It(() => "Cannot configure workflow.", e => + GetWorkflowOrThrow(workflows, command.WorkflowId); + + Validate.It(() => "Cannot update workflow.", e => { if (command.Workflow == null) { @@ -74,14 +89,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards }); } - internal static void CanAdd(AddWorkflow c) + public static void CanDelete(Workflows workflows, DeleteWorkflow command) { - throw new NotImplementedException(); + Guard.NotNull(command, nameof(command)); + + GetWorkflowOrThrow(workflows, command.WorkflowId); } - internal static void CanDelete(Workflows workflows, DeleteWorkflow c) + private static Workflow GetWorkflowOrThrow(Workflows workflows, Guid id) { - throw new NotImplementedException(); + if (!workflows.TryGetValue(id, out var workflow)) + { + throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); + } + + return workflow; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index ac71df0bc..7e870d56c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -96,9 +96,19 @@ namespace Squidex.Domain.Apps.Entities.Apps.State Clients = Clients.Revoke(@event.Id); } - protected void On(AppWorkflowConfigured @event) + protected void On(AppWorkflowAdded @event) { - Workflows = Workflows.Set(@event.Workflow); + Workflows = Workflows.Add(@event.WorkflowId, @event.Name); + } + + protected void On(AppWorkflowUpdated @event) + { + Workflows = Workflows.Update(@event.WorkflowId, @event.Workflow); + } + + protected void On(AppWorkflowDeleted @event) + { + Workflows = Workflows.Remove(@event.WorkflowId); } protected void On(AppPatternAdded @event) diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs index 728b04b83..3e5627bee 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Apps @@ -12,6 +13,8 @@ namespace Squidex.Domain.Apps.Events.Apps [EventType(nameof(AppWorkflowAdded))] public sealed class AppWorkflowAdded : AppEvent { + public Guid WorkflowId { get; set; } + public string Name { get; set; } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 811dd97ef..843c2e424 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/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.Empty(clients_1); + Assert.NotEmpty(clients_1); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs index 5e22067b7..de159e090 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs +++ b/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.Empty(patterns_1); + Assert.NotEmpty(patterns_1); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index ac89aa8ce..591708388 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/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.Empty(roles_1); + Assert.NotEmpty(roles_1); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs index c81db61cb..8a3c485e8 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Linq; using Squidex.Domain.Apps.Core.Contents; using Xunit; @@ -38,20 +37,32 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_add_new_workflow_with_default_states() { - var workflows_1 = workflows_0.Add("1"); + var id = Guid.NewGuid(); - Assert.Equal(workflows_1.GetFirst().Steps.Keys, new[] { Status.Archived, Status.Draft, Status.Published }); + var workflows_1 = workflows_0.Add(id, "1"); + + Assert.Equal(workflows_1[id].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); + var id = Guid.NewGuid(); + + var workflows_1 = workflows_0.Add(id, "1"); + var workflows_2 = workflows_1.Update(id, Workflow.Empty); Assert.Empty(workflows_2.GetFirst().Steps.Keys); } + [Fact] + public void Should_update_workflow_with_default_guid() + { + var workflows_1 = workflows_0.Update(Guid.Empty, Workflow.Empty); + + Assert.NotEmpty(workflows_1); + } + [Fact] public void Should_do_nothing_if_workflow_to_update_not_found() { @@ -63,8 +74,10 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_remove_workflow() { - var workflows_1 = workflows_0.Add("1"); - var workflows_2 = workflows_1.Remove(workflows_1.Keys.First()); + var id = Guid.NewGuid(); + + var workflows_1 = workflows_0.Add(id, "1"); + var workflows_2 = workflows_1.Remove(id); Assert.Empty(workflows_2); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index a584ef196..6d3ac1bb3 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -38,6 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string planIdPaid = "premium"; private readonly string planIdFree = "free"; private readonly AppGrain sut; + private readonly Guid workflowId = Guid.NewGuid(); private readonly Guid patternId1 = Guid.NewGuid(); private readonly Guid patternId2 = Guid.NewGuid(); private readonly Guid patternId3 = Guid.NewGuid(); @@ -299,9 +300,9 @@ namespace Squidex.Domain.Apps.Entities.Apps } [Fact] - public async Task ConfigureWorkflow_should_create_events_and_update_state() + public async Task AddWorkflow_should_create_events_and_update_state() { - var command = new UpdateWorkflow { Workflow = Workflow.Default }; + var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; await ExecuteCreateAsync(); @@ -313,7 +314,47 @@ namespace Squidex.Domain.Apps.Entities.Apps LastEvents .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowConfigured { Workflow = Workflow.Default }) + CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) + ); + } + + [Fact] + public async Task UpdateWorkflow_should_create_events_and_update_state() + { + var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.NotEmpty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) + ); + } + + [Fact] + public async Task DeleteWorkflow_should_create_events_and_update_state() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Empty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) ); } @@ -540,6 +581,11 @@ namespace Squidex.Domain.Apps.Entities.Apps return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); } + private Task ExecuteAddWorkflowAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" })); + } + private Task ExecuteChangePlanAsync() { return sut.ExecuteAsync(CreateCommand(new ChangePlan { PlanId = planIdPaid })); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs index 3bb1902c1..3431e88ce 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanAdd_should_not_throw_exception_if_success() + public void CanAdd_should_not_throw_exception_if_command_is_valid() { var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = ".*" }; @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanDelete_should_not_throw_exception_if_success() + public void CanDelete_should_not_throw_exception_if_command_is_valid() { var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message"); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs index bd16881f7..c469ac3f4 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanAdd_should_not_throw_exception_if_success() + public void CanAdd_should_not_throw_exception_if_command_is_valid() { var command = new AddRole { Name = roleName }; @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanDelete_should_not_throw_exception_if_success() + public void CanDelete_should_not_throw_exception_if_command_is_valid() { var roles_1 = roles_0.Add(roleName); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs index ce724c8c0..d348f7014 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps.Commands; @@ -16,17 +17,54 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { public class GuardAppWorkflowTests { + private readonly Guid workflowId = Guid.NewGuid(); + private readonly Workflows workflows; + + public GuardAppWorkflowTests() + { + workflows = Workflows.Empty.Add(workflowId, "name"); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_is_not_defined() + { + var command = new AddWorkflow(); + + ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddWorkflow { Name = "my-workflow" }; + + GuardAppWorkflows.CanAdd(command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_not_found() + { + var command = new UpdateWorkflow + { + Workflow = Workflow.Empty, + WorkflowId = Guid.NewGuid() + }; + + Assert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command)); + } + [Fact] - public void CanConfigure_should_throw_exception_if_workflow_is_not_defined() + public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() { - var command = new UpdateWorkflow(); + var command = new UpdateWorkflow { WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Workflow is required.", "Workflow")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_has_no_initial_step() + public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() { var command = new UpdateWorkflow { @@ -35,15 +73,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { [Status.Published] = new WorkflowStep() }, - default) + default), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Initial step is required.", "Workflow.Initial")); } [Fact] - public void CanConfigure_should_throw_exception_if_initial_step_is_published() + public void CanUpdate_should_throw_exception_if_initial_step_is_published() { var command = new UpdateWorkflow { @@ -52,15 +91,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { [Status.Published] = new WorkflowStep() }, - Status.Published) + Status.Published), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_does_not_have_published_state() + public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() { var command = new UpdateWorkflow { @@ -69,15 +109,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { [Status.Draft] = new WorkflowStep() }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Workflow must have a published step.", "Workflow.Steps")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_step_is_not_defined() + public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() { var command = new UpdateWorkflow { @@ -87,15 +128,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Status.Published] = null, [Status.Draft] = new WorkflowStep() }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Step is required.", "Workflow.Steps.Published")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_transition_is_invalid() + public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() { var command = new UpdateWorkflow { @@ -110,15 +152,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards }), [Status.Draft] = new WorkflowStep() }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_transition_is_not_defined() + public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() { var command = new UpdateWorkflow { @@ -134,19 +177,36 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Status.Draft] = null }) }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); } [Fact] - public void CanConfigure_should_not_throw_exception_if_workflow_is_valid() + public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() + { + var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; + + GuardAppWorkflows.CanUpdate(workflows, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_workflow_not_found() + { + var command = new DeleteWorkflow { WorkflowId = Guid.NewGuid() }; + + Assert.Throws(() => GuardAppWorkflows.CanDelete(workflows, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_workflow_is_found() { - var command = new UpdateWorkflow { Workflow = Workflow.Default }; + var command = new DeleteWorkflow { WorkflowId = workflowId }; - GuardAppWorkflows.CanUpdate(command); + GuardAppWorkflows.CanDelete(workflows, command); } } } From 9ef8ea7809e3f670245dfa7be88f0afeaef5144d Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 1 Jul 2019 18:15:39 +0200 Subject: [PATCH 04/18] API updated. --- .../Apps/AppWorkflowsController.cs | 76 +++++++++++++++---- ...rkflowResponseDto.cs => AddWorkflowDto.cs} | 19 ++--- .../Api/Controllers/Apps/Models/AppDto.cs | 2 +- ...ertWorkflowDto.cs => UpdateWorkflowDto.cs} | 2 +- .../Controllers/Apps/Models/WorkflowDto.cs | 20 ++++- .../Controllers/Apps/Models/WorkflowsDto.cs | 49 ++++++++++++ 6 files changed, 137 insertions(+), 31 deletions(-) rename src/Squidex/Areas/Api/Controllers/Apps/Models/{WorkflowResponseDto.cs => AddWorkflowDto.cs} (53%) rename src/Squidex/Areas/Api/Controllers/Apps/Models/{UpsertWorkflowDto.cs => UpdateWorkflowDto.cs} (97%) create mode 100644 src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index be489f20e..abc16700a 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -5,11 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; @@ -36,13 +38,13 @@ namespace Squidex.Areas.Api.Controllers.Apps /// 404 => App not found. /// [HttpGet] - [Route("apps/{app}/workflow/")] - [ProducesResponseType(typeof(WorkflowResponseDto), 200)] + [Route("apps/{app}/workflows/")] + [ProducesResponseType(typeof(WorkflowsDto), 200)] [ApiPermission(Permissions.AppWorkflowsRead)] [ApiCosts(0)] - public IActionResult GetWorkflow(string app) + public IActionResult GetWorkflows(string app) { - var response = WorkflowResponseDto.FromApp(App, this); + var response = WorkflowsDto.FromApp(App, this); Response.Headers[HeaderNames.ETag] = App.Version.ToString(); @@ -50,21 +52,46 @@ namespace Squidex.Areas.Api.Controllers.Apps } /// - /// Configure workflow of the app. + /// Create a workflow. /// /// The name of the app. /// The new workflow. /// - /// 200 => Workflow configured. - /// 400 => Workflow is not valid. - /// 404 => App not found. + /// 200 => Workflow updated. + /// 400 => Workflow request is not valid. + /// 404 => Workflow or app not found. + /// + [HttpPost] + [Route("apps/{app}/workflows/")] + [ProducesResponseType(typeof(WorkflowsDto), 200)] + [ApiPermission(Permissions.AppWorkflowsUpdate)] + [ApiCosts(1)] + public async Task PostWorkflow(string app, [FromBody] AddWorkflowDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + /// + /// Update a workflow. + /// + /// The name of the app. + /// The new workflow. + /// The id of the workflow to update. + /// + /// 200 => Workflow updated. + /// 400 => Workflow request is not valid. + /// 404 => Workflow or app not found. /// [HttpPut] - [Route("apps/{app}/workflow/")] - [ProducesResponseType(typeof(WorkflowResponseDto), 200)] + [Route("apps/{app}/workflows/{id}")] + [ProducesResponseType(typeof(WorkflowsDto), 200)] [ApiPermission(Permissions.AppWorkflowsUpdate)] [ApiCosts(1)] - public async Task PutWorkflow(string app, [FromBody] UpsertWorkflowDto request) + public async Task PutWorkflow(string app, Guid id, [FromBody] UpdateWorkflowDto request) { var command = request.ToCommand(); @@ -73,12 +100,35 @@ namespace Squidex.Areas.Api.Controllers.Apps return Ok(response); } - private async Task InvokeCommandAsync(ICommand command) + /// + /// Delete a workflow. + /// + /// The name of the app. + /// The id of the workflow to update. + /// + /// 200 => Workflow deleted. + /// 404 => Workflow or app not found. + /// + [HttpDelete] + [Route("apps/{app}/workflows/{id}")] + [ProducesResponseType(typeof(WorkflowsDto), 200)] + [ApiPermission(Permissions.AppWorkflowsUpdate)] + [ApiCosts(1)] + public async Task DeleteWorkflow(string app, Guid id) + { + var command = new DeleteWorkflow { WorkflowId = id }; + + var response = await InvokeCommandAsync(command); + + return Ok(response); + } + + private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = WorkflowResponseDto.FromApp(result, this); + var response = WorkflowsDto.FromApp(result, this); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowResponseDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs similarity index 53% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowResponseDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs index 3186a7893..823794c6b 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowResponseDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AddWorkflowDto.cs @@ -6,27 +6,22 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Web; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure.Commands; namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class WorkflowResponseDto : Resource + public sealed class AddWorkflowDto { /// - /// The workflow. + /// The name of the workflow. /// [Required] - public WorkflowDto Workflow { get; set; } + public string Name { get; set; } - public static WorkflowResponseDto FromApp(IAppEntity app, ApiController controller) + public ICommand ToCommand() { - var result = new WorkflowResponseDto - { - Workflow = WorkflowDto.FromWorkflow(app.Workflows.GetFirst(), controller, app.Name) - }; - - return result; + return new AddWorkflow { Name = Name }; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index 46e37a1ea..ffa7b22fa 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -179,7 +179,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models if (controller.HasPermission(AllPermissions.AppWorkflowsRead, Name, permissions: permissions)) { - AddGetLink("workflows", controller.Url(x => nameof(x.GetWorkflow), values)); + AddGetLink("workflows", controller.Url(x => nameof(x.GetWorkflows), values)); } if (controller.HasPermission(AllPermissions.AppSchemasCreate, Name, permissions: permissions)) diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs similarity index 97% rename from src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs index 37577e9e5..6a7a23653 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class UpsertWorkflowDto + public sealed class UpdateWorkflowDto { /// /// The workflow steps. diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs index 3a6a3fecc..d347337da 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -17,6 +18,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class WorkflowDto : Resource { + /// + /// The workflow id. + /// + public Guid Id { get; set; } + /// /// The workflow steps. /// @@ -28,10 +34,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public Status Initial { get; set; } - public static WorkflowDto FromWorkflow(Workflow workflow, ApiController controller, string app) + public static WorkflowDto FromWorkflow(Guid id, Workflow workflow, ApiController controller, string app) { var result = new WorkflowDto { + Id = id, Steps = workflow.Steps.ToDictionary( x => x.Key, x => SimpleMapper.Map(x.Value, new WorkflowStepDto @@ -43,18 +50,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models Initial = workflow.Initial }; - return result.CreateLinks(controller, app); + return result.CreateLinks(controller, app, id); } - private WorkflowDto CreateLinks(ApiController controller, string app) + private WorkflowDto CreateLinks(ApiController controller, string app, Guid id) { - var values = new { app }; + var values = new { app, id }; if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app)) { AddPutLink("update", controller.Url(x => nameof(x.PutWorkflow), values)); } + if (controller.HasPermission(Permissions.AppWorkflowsDelete, app)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteWorkflow), values)); + } + return this; } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs new file mode 100644 index 000000000..b58be115c --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class WorkflowsDto : Resource + { + /// + /// The workflow. + /// + [Required] + public WorkflowDto[] Items { get; set; } + + public static WorkflowsDto FromApp(IAppEntity app, ApiController controller) + { + var result = new WorkflowsDto + { + Items = app.Workflows.Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value, controller, app.Name)).ToArray() + }; + + return result.CreateLinks(controller, app.Name); + } + + private WorkflowsDto CreateLinks(ApiController controller, string app) + { + var values = new { app }; + + AddSelfLink(controller.Url(x => nameof(x.GetWorkflows), values)); + + if (controller.HasPermission(Permissions.AppWorkflowsCreate, app)) + { + AddPostLink("create", controller.Url(x => nameof(x.PostWorkflow), values)); + } + + return this; + } + } +} From d848393770c07050fc9d8f48d96610bd1da73022 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 1 Jul 2019 19:23:34 +0200 Subject: [PATCH 05/18] Basic stuff. --- .../Contents/Workflow.cs | 2 +- .../Apps/Models/UpdateWorkflowDto.cs | 7 +- .../Controllers/Apps/Models/WorkflowDto.cs | 8 +- .../app/features/settings/declarations.ts | 1 + src/Squidex/app/features/settings/module.ts | 2 + .../pages/clients/clients-page.component.ts | 4 +- .../pages/workflows/workflow.component.html | 70 ++++++++++ .../pages/workflows/workflow.component.scss | 25 ++++ .../pages/workflows/workflow.component.ts | 120 ++++++++++++++++++ .../workflows/workflows-page.component.html | 56 ++++---- .../workflows/workflows-page.component.ts | 78 +++--------- .../settings/settings-area.component.html | 2 +- src/Squidex/app/shared/internal.ts | 1 + .../shared/services/workflows.service.spec.ts | 117 +++++++++-------- .../app/shared/services/workflows.service.ts | 51 ++++++-- src/Squidex/app/shared/state/clients.forms.ts | 2 +- .../app/shared/state/workflows.forms.ts | 24 ++++ .../app/shared/state/workflows.state.spec.ts | 52 +++++--- .../app/shared/state/workflows.state.ts | 56 +++++--- 19 files changed, 488 insertions(+), 190 deletions(-) create mode 100644 src/Squidex/app/features/settings/pages/workflows/workflow.component.html create mode 100644 src/Squidex/app/features/settings/pages/workflows/workflow.component.scss create mode 100644 src/Squidex/app/features/settings/pages/workflows/workflow.component.ts create mode 100644 src/Squidex/app/shared/state/workflows.forms.ts diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs index c04a8ecc5..90a430965 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs @@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.Contents { public sealed class Workflow : Named { - private const string DefaultName = "Name"; + private const string DefaultName = "Unnamed"; private static readonly IReadOnlyDictionary EmptySteps = new Dictionary(); public static readonly Workflow Default = CreateDefault(); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs index 6a7a23653..6a6f0bfa6 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs @@ -15,6 +15,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class UpdateWorkflowDto { + /// + /// The name of the workflow. + /// + public string Name { get; set; } + /// /// The workflow steps. /// @@ -37,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models y => new WorkflowTransition(y.Value.Expression, y.Value.Role)), x.Value.Color, x.Value.NoUpdate)), - Initial); + Initial, Name); return new UpdateWorkflow { Workflow = workflow }; } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs index d347337da..7906addaa 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs @@ -23,6 +23,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public Guid Id { get; set; } + /// + /// The name of the workflow. + /// + public string Name { get; set; } + /// /// The workflow steps. /// @@ -38,7 +43,6 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models { var result = new WorkflowDto { - Id = id, Steps = workflow.Steps.ToDictionary( x => x.Key, x => SimpleMapper.Map(x.Value, new WorkflowStepDto @@ -47,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models y => y.Key, y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role }) })), - Initial = workflow.Initial + Id = id, Name = workflow.Name, Initial = workflow.Initial }; return result.CreateLinks(controller, app, id); diff --git a/src/Squidex/app/features/settings/declarations.ts b/src/Squidex/app/features/settings/declarations.ts index a6f8916c6..1a0553935 100644 --- a/src/Squidex/app/features/settings/declarations.ts +++ b/src/Squidex/app/features/settings/declarations.ts @@ -20,6 +20,7 @@ export * from './pages/roles/role.component'; export * from './pages/roles/roles-page.component'; export * from './pages/workflows/workflow-step.component'; export * from './pages/workflows/workflow-transition.component'; +export * from './pages/workflows/workflow.component'; export * from './pages/workflows/workflows-page.component'; export * from './settings-area.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/settings/module.ts b/src/Squidex/app/features/settings/module.ts index bf7eddbf8..d77bf3601 100644 --- a/src/Squidex/app/features/settings/module.ts +++ b/src/Squidex/app/features/settings/module.ts @@ -31,6 +31,7 @@ import { RoleComponent, RolesPageComponent, SettingsAreaComponent, + WorkflowComponent, WorkflowsPageComponent, WorkflowStepComponent, WorkflowTransitionComponent @@ -213,6 +214,7 @@ const routes: Routes = [ RoleComponent, RolesPageComponent, SettingsAreaComponent, + WorkflowComponent, WorkflowsPageComponent, WorkflowTransitionComponent, WorkflowStepComponent diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts index 885085116..48bc18f61 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts @@ -9,8 +9,8 @@ import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { + AddClientForm, AppsState, - AttachClientForm, ClientDto, ClientsState, RolesState @@ -22,7 +22,7 @@ import { templateUrl: './clients-page.component.html' }) export class ClientsPageComponent implements OnInit { - public addClientForm = new AttachClientForm(this.formBuilder); + public addClientForm = new AddClientForm(this.formBuilder); constructor( public readonly appsState: AppsState, diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html new file mode 100644 index 000000000..14b64d4e1 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html @@ -0,0 +1,70 @@ +
+
+
+
+ {{workflow.displayName}} +
+
+
+ + + +
+
+
+
+ +
+
+
+ + +
+
+ +
+ + +
+ + +
+ + + + Optional name for the workflow. + +
+
+ + + + + +
+
+
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss b/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss new file mode 100644 index 000000000..253e84024 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss @@ -0,0 +1,25 @@ +@import '_vars'; +@import '_mixins'; + +.table-items-row-details { + &::before { + right: 4.55rem; + } +} + +.col-form-label { + min-width: 4rem; + max-width: 4rem; + text-align: left; +} + +.form-group { + margin-bottom: 2rem; + margin-left: 2rem; + max-width: 60rem; +} + +.btn-success { + margin-bottom: 1rem; + margin-left: 2rem; +} \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts b/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts new file mode 100644 index 000000000..ccfa13f95 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts @@ -0,0 +1,120 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Input, OnChanges } from '@angular/core'; + +import { + ErrorDto, + MathHelper, + RoleDto, + WorkflowDto, + WorkflowsState, + WorkflowStep, + WorkflowStepValues, + WorkflowTransition, + WorkflowTransitionValues +} from '@app/shared'; + +@Component({ + selector: 'sqx-workflow', + styleUrls: ['./workflow.component.scss'], + templateUrl: './workflow.component.html' +}) +export class WorkflowComponent implements OnChanges { + @Input() + public workflow: WorkflowDto; + + @Input() + public roles: RoleDto[]; + + public error: ErrorDto | null; + + public onBlur = { updateOn: 'blur' }; + + public isEditing = false; + public isEditable = false; + + constructor( + private readonly workflowsState: WorkflowsState + ) { + } + + public ngOnChanges() { + this.isEditable = this.workflow.canUpdate; + } + + public toggleEditing() { + this.isEditing = !this.isEditing; + } + + public remove() { + this.workflowsState.delete(this.workflow); + } + + public save() { + if (!this.isEditable) { + return; + } + + this.workflowsState.update(this.workflow) + .subscribe(() => { + this.error = null; + }, error => { + this.error = error; + }); + } + + public addStep() { + let index = this.workflow.steps.length; + + for (let i = index; i < index + 100; i++) { + const name = `Step${i}`; + + if (!this.workflow.getStep(name)) { + this.workflow = this.workflow.setStep(name, { color: MathHelper.randomColor() }); + return; + } + } + } + + public rename(name: string) { + this.workflow = this.workflow.rename(name); + } + + public setInitial(step: WorkflowStep) { + this.workflow = this.workflow.setInitial(step.name); + } + + public addTransiton(from: WorkflowStep, to: WorkflowStep) { + this.workflow = this.workflow.setTransition(from.name, to.name, {}); + } + + public removeTransition(from: WorkflowStep, transition: WorkflowTransition) { + this.workflow = this.workflow.removeTransition(from.name, transition.to); + } + + public updateTransition(update: { transition: WorkflowTransition, values: WorkflowTransitionValues }) { + this.workflow = this.workflow.setTransition(update.transition.from, update.transition.to, update.values); + } + + public updateStep(step: WorkflowStep, values: WorkflowStepValues) { + this.workflow = this.workflow.setStep(step.name, values); + } + + public renameStep(step: WorkflowStep, newName: string) { + this.workflow = this.workflow.renameStep(step.name, newName); + } + + public removeStep(step: WorkflowStep) { + this.workflow = this.workflow.removeStep(step.name); + } + + public trackByStep(step: WorkflowStep) { + return step.name; + } +} + diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html index 6fd2a206a..0faddce9f 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html @@ -1,47 +1,47 @@ - + Workflow - - - - - - - - + - - +
+ No workflows created yet. +
+ + + + +
- -
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts index a458bee00..4659b7781 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts +++ b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts @@ -6,17 +6,14 @@ */ import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { + AddWorkflowForm, AppsState, - MathHelper, RolesState, WorkflowDto, - WorkflowsState, - WorkflowStep, - WorkflowStepValues, - WorkflowTransition, - WorkflowTransitionValues + WorkflowsState } from '@app/shared'; @Component({ @@ -25,78 +22,45 @@ import { templateUrl: './workflows-page.component.html' }) export class WorkflowsPageComponent implements OnInit { - public workflow: WorkflowDto; + public addWorkflowForm = new AddWorkflowForm(this.formBuilder); constructor( public readonly appsState: AppsState, public readonly rolesState: RolesState, - public readonly workflowsState: WorkflowsState + public readonly workflowsState: WorkflowsState, + private readonly formBuilder: FormBuilder ) { } public ngOnInit() { - this.workflowsState.load() - .subscribe(workflow => { - this.workflow = workflow; - }); + this.workflowsState.load(); this.rolesState.load(); } public reload() { - this.workflowsState.load(true) - .subscribe(workflow => { - this.workflow = workflow; - }); + this.workflowsState.load(true); } - public save() { - this.workflowsState.save(this.workflow); - } - - public addStep() { - let index = this.workflow.steps.length; + public addWorkflow() { + const value = this.addWorkflowForm.submit(); - for (let i = index; i < index + 100; i++) { - const name = `Step${i}`; - - if (!this.workflow.getStep(name)) { - this.workflow = this.workflow.setStep(name, { color: MathHelper.randomColor() }); - return; - } + if (value) { + this.workflowsState.add(value.name) + .subscribe(() => { + this.addWorkflowForm.submitCompleted(); + }, error => { + this.addWorkflowForm.submitFailed(error); + }); } } - public setInitial(step: WorkflowStep) { - this.workflow = this.workflow.setInitial(step.name); - } - - public addTransiton(from: WorkflowStep, to: WorkflowStep) { - this.workflow = this.workflow.setTransition(from.name, to.name, {}); - } - - public removeTransition(from: WorkflowStep, transition: WorkflowTransition) { - this.workflow = this.workflow.removeTransition(from.name, transition.to); - } - - public updateTransition(update: { transition: WorkflowTransition, values: WorkflowTransitionValues }) { - this.workflow = this.workflow.setTransition(update.transition.from, update.transition.to, update.values); - } - - public updateStep(step: WorkflowStep, values: WorkflowStepValues) { - this.workflow = this.workflow.setStep(step.name, values); - } - - public renameStep(step: WorkflowStep, newName: string) { - this.workflow = this.workflow.renameStep(step.name, newName); - } - - public removeStep(step: WorkflowStep) { - this.workflow = this.workflow.removeStep(step.name); + public cancelAddWorkflow() { + this.addWorkflowForm.submitCompleted(); } - public trackByStep(index: number, step: WorkflowStep) { - return step.name; + public trackByWorkflow(index: number, workflow: WorkflowDto) { + return workflow.id; } } diff --git a/src/Squidex/app/features/settings/settings-area.component.html b/src/Squidex/app/features/settings/settings-area.component.html index 1a1a9bcb7..cee2481d3 100644 --- a/src/Squidex/app/features/settings/settings-area.component.html +++ b/src/Squidex/app/features/settings/settings-area.component.html @@ -45,7 +45,7 @@ diff --git a/src/Squidex/app/shared/internal.ts b/src/Squidex/app/shared/internal.ts index 71a0a1561..c53694089 100644 --- a/src/Squidex/app/shared/internal.ts +++ b/src/Squidex/app/shared/internal.ts @@ -63,6 +63,7 @@ export * from './state/rules.state'; export * from './state/schemas.forms'; export * from './state/schemas.state'; export * from './state/ui.state'; +export * from './state/workflows.forms'; export * from './state/workflows.state'; export * from './utils/messages'; diff --git a/src/Squidex/app/shared/services/workflows.service.spec.ts b/src/Squidex/app/shared/services/workflows.service.spec.ts index 09afc1d23..ec10fc178 100644 --- a/src/Squidex/app/shared/services/workflows.service.spec.ts +++ b/src/Squidex/app/shared/services/workflows.service.spec.ts @@ -49,7 +49,7 @@ describe('WorkflowsService', () => { 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/workflows'); expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); @@ -64,7 +64,7 @@ describe('WorkflowsService', () => { expect(workflows!).toEqual({ payload: createWorkflows('1', '2'), version: new Version('2') }); })); - it('should make a put request to create a workflow', + it('should make a post request to create a workflow', inject([WorkflowsService, HttpTestingController], (workflowsService: WorkflowsService, httpMock: HttpTestingController) => { let workflows: WorkflowsDto; @@ -73,7 +73,7 @@ describe('WorkflowsService', () => { workflows = result; }); - const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow/123'); + const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflows'); expect(req.request.method).toEqual('POST'); expect(req.request.headers.get('If-Match')).toEqual(version.value); @@ -92,7 +92,7 @@ describe('WorkflowsService', () => { const resource: Resource = { _links: { - update: { method: 'PUT', href: '/api/apps/my-app/workflow/123' } + update: { method: 'PUT', href: '/api/apps/my-app/workflows/123' } } }; @@ -102,7 +102,7 @@ describe('WorkflowsService', () => { workflows = result; }); - const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflow/123'); + const req = httpMock.expectOne('http://service/p/api/apps/my-app/workflows/123'); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toEqual(version.value); @@ -121,7 +121,7 @@ describe('WorkflowsService', () => { const resource: Resource = { _links: { - delete: { method: 'DELETE', href: '/api/apps/my-app/workflow/123' } + delete: { method: 'DELETE', href: '/api/apps/my-app/workflows/123' } } }; @@ -131,9 +131,9 @@ describe('WorkflowsService', () => { 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/workflows/123'); - expect(req.request.method).toEqual('PUT'); + expect(req.request.method).toEqual('DELETE'); expect(req.request.headers.get('If-Match')).toEqual(version.value); req.flush(workflowsResponse('1', '2'), { @@ -156,32 +156,28 @@ describe('WorkflowsService', () => { function workflowResponse(name: string) { return { - workflow: { - steps: { - [`${name}1`]: { - transitions: { - [`${name}2`]: { - expression: 'Expression1', role: 'Role1' - } - }, - color: `${name}1`, noUpdate: true + name: `name_${name}`, id: `id_${name}`, initial: `${name}1`, + steps: { + [`${name}1`]: { + transitions: { + [`${name}2`]: { + expression: 'Expression1', role: 'Role1' + } }, - [`${name}2`]: { - transitions: { - [`${name}1`]: { - expression: 'Expression2', role: 'Role2' - } - }, - color: `${name}2`, noUpdate: true - } + color: `${name}1`, noUpdate: true }, - initial: `${name}1`, - _links: { - update: { method: 'PUT', href: '/api/workflows' } + [`${name}2`]: { + transitions: { + [`${name}1`]: { + expression: 'Expression2', role: 'Role2' + } + }, + color: `${name}2`, noUpdate: true } }, - _links: {}, - canCreate: true + _links: { + update: { method: 'PUT', href: `/workflows/${name}` } + } }; } }); @@ -198,9 +194,9 @@ export function createWorkflows(...names: string[]): WorkflowsPayload { export function createWorkflow(name: string): WorkflowDto { return new WorkflowDto({ - update: { method: 'PUT', href: '/workflows' } + update: { method: 'PUT', href: `/workflows/${name}` } }, - `${name}1`, + `id_${name}`, `name_${name}`, `${name}1`, [ { name: `${name}1`, color: `${name}1`, noUpdate: true, isLocked: false }, { name: `${name}2`, color: `${name}2`, noUpdate: true, isLocked: false } @@ -213,17 +209,18 @@ export function createWorkflow(name: string): WorkflowDto { describe('Workflow', () => { it('should create empty workflow', () => { - const workflow = new WorkflowDto(); + const workflow = new WorkflowDto({}, 'id'); expect(workflow.initial); }); it('should add step to workflow', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1', { color: '#00ff00' }); expect(workflow.serialize()).toEqual({ + name: null, steps: { '1': { transitions: {}, color: '#00ff00' } }, @@ -233,11 +230,12 @@ describe('Workflow', () => { it('should override settings if step already exists', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1', { color: '#00ff00', noUpdate: true }) .setStep('1', { color: 'red' }); expect(workflow.serialize()).toEqual({ + name: null, steps: { '1': { transitions: {}, color: 'red', noUpdate: true } }, @@ -247,7 +245,7 @@ describe('Workflow', () => { it('should return same workflow if step to update is locked', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1', { color: '#00ff00', isLocked: true }); const updated = workflow.setStep('1', { color: 'red' }); @@ -257,7 +255,7 @@ describe('Workflow', () => { it('should sort steps case invariant', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('Z') .setStep('a'); @@ -269,7 +267,7 @@ describe('Workflow', () => { it('should return same workflow if step to remove is locked', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1', { color: '#00ff00', isLocked: true }); const updated = workflow.removeStep('1'); @@ -279,7 +277,7 @@ describe('Workflow', () => { it('should return same workflow if step to remove not found', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1'); const updated = workflow.removeStep('3'); @@ -289,7 +287,7 @@ describe('Workflow', () => { it('should remove step', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1', { color: '#00ff00' }) .setStep('2', { color: '#ff0000' }) .setStep('3', { color: '#0000ff' }) @@ -299,6 +297,7 @@ describe('Workflow', () => { .removeStep('1'); expect(workflow.serialize()).toEqual({ + name: null, steps: { '2': { transitions: { @@ -314,13 +313,14 @@ describe('Workflow', () => { it('should make first non-locked step the initial step if initial removed', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2', { isLocked: true }) .setStep('3') .removeStep('1'); expect(workflow.serialize()).toEqual({ + name: null, steps: { '2': { transitions: {}, isLocked: true }, '3': { transitions: {} } @@ -331,16 +331,16 @@ describe('Workflow', () => { it('should unset initial step if initial removed', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .removeStep('1'); - expect(workflow.serialize()).toEqual({ steps: {}, initial: undefined }); + expect(workflow.serialize()).toEqual({ name: null, steps: {}, initial: null }); }); it('should rename step', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1', { color: '#00ff00' }) .setStep('2', { color: '#ff0000' }) .setStep('3', { color: '#0000ff' }) @@ -350,6 +350,7 @@ describe('Workflow', () => { .renameStep('1', 'a'); expect(workflow.serialize()).toEqual({ + name: null, steps: { 'a': { transitions: { @@ -372,13 +373,14 @@ describe('Workflow', () => { it('should add transitions to workflow', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2') .setTransition('1', '2', { expression: '1 === 2' }) .setTransition('2', '1', { expression: '2 === 1' }); expect(workflow.serialize()).toEqual({ + name: null, steps: { '1': { transitions: { @@ -397,7 +399,7 @@ describe('Workflow', () => { it('should remove transition from workflow', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2') .setTransition('1', '2', { expression: '1 === 2' }) @@ -405,6 +407,7 @@ describe('Workflow', () => { .removeTransition('1', '2'); expect(workflow.serialize()).toEqual({ + name: null, steps: { '1': { transitions: {}}, '2': { @@ -419,13 +422,14 @@ describe('Workflow', () => { it('should override settings if transition already exists', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2') .setTransition('2', '1', { expression: '2 === 1', role: 'Role' }) .setTransition('2', '1', { expression: '2 !== 1' }); expect(workflow.serialize()).toEqual({ + name: null, steps: { '1': { transitions: {} }, '2': { @@ -440,7 +444,7 @@ describe('Workflow', () => { it('should return same workflow if transition to update not found by from step', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2') .setTransition('1', '2'); @@ -452,7 +456,7 @@ describe('Workflow', () => { it('should return same workflow if transition to update not found by to step', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2') .setTransition('1', '2'); @@ -464,7 +468,7 @@ describe('Workflow', () => { it('should return same workflow if transition to remove not', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2') .setTransition('1', '2'); @@ -476,7 +480,7 @@ describe('Workflow', () => { it('should return same workflow if step to make initial is locked', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2', { color: '#00ff00', isLocked: true }); @@ -487,12 +491,13 @@ describe('Workflow', () => { it('should set initial step', () => { const workflow = - new WorkflowDto() + new WorkflowDto({}, 'id') .setStep('1') .setStep('2') .setInitial('2'); expect(workflow.serialize()).toEqual({ + name: null, steps: { '1': { transitions: {} }, '2': { transitions: {} } @@ -501,4 +506,12 @@ describe('Workflow', () => { }); }); + it('should rename workflow', () => { + const workflow = + new WorkflowDto({}, 'id') + .rename('name'); + + expect(workflow.serialize()).toEqual({ name: 'name', steps: {}, initial: null }); + }); + }); \ 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 index 52e1ef3cd..9e8d06a54 100644 --- a/src/Squidex/app/shared/services/workflows.service.ts +++ b/src/Squidex/app/shared/services/workflows.service.ts @@ -20,6 +20,8 @@ import { pretifyError, Resource, ResourceLinks, + StringHelper, + Types, Version, Versioned } from '@app/framework'; @@ -35,9 +37,12 @@ export class WorkflowDto { public readonly _links: ResourceLinks; public readonly canUpdate: boolean; + public readonly canDelete: boolean; + + public readonly displayName: string; public static DEFAULT = - new WorkflowDto() + new WorkflowDto({}, 'id', 'name') .setStep('Draft', { color: '#8091a5' }) .setStep('Archived', { color: '#eb3142', noUpdate: true }) .setStep('Published', { color: '#4bb958', isLocked: true }) @@ -47,8 +52,11 @@ export class WorkflowDto { .setTransition('Published', 'Draft') .setTransition('Published', 'Archived'); - constructor(links: ResourceLinks = {}, - public readonly initial?: string, + constructor( + links: ResourceLinks = {}, + public readonly id: string, + public readonly name: string | null = null, + public readonly initial: string | null = null, public readonly steps: WorkflowStep[] = [], private readonly transitions: WorkflowTransition[] = [] ) { @@ -59,6 +67,9 @@ export class WorkflowDto { this._links = links; this.canUpdate = hasAnyLink(links, 'update'); + this.canDelete = hasAnyLink(links, 'delete'); + + this.displayName = StringHelper.firstNonEmpty(name, 'Unnamed Workflow'); } public getOpenSteps(step: WorkflowStep) { @@ -94,7 +105,7 @@ export class WorkflowDto { initial = steps[0].name; } - return new WorkflowDto(this._links, initial, steps, this.transitions); + return this.createNew({ initial, steps }); } public setInitial(initial: string) { @@ -104,7 +115,7 @@ export class WorkflowDto { return this; } - return new WorkflowDto(this._links, initial, this.steps, this.transitions); + return this.createNew({ initial }); } public removeStep(name: string) { @@ -124,10 +135,14 @@ export class WorkflowDto { if (initial === name) { const first = steps.find(x => !x.isLocked); - initial = first ? first.name : undefined; + initial = first ? first.name : null; } - return new WorkflowDto(this._links, initial, steps, transitions); + return this.createNew({ initial, steps, transitions }); + } + + public rename(name: string) { + return this.createNew({ name }); } public renameStep(name: string, newName: string) { @@ -163,7 +178,7 @@ export class WorkflowDto { initial = newName; } - return new WorkflowDto(this._links, initial, steps, transitions); + return this.createNew({ initial, steps, transitions }); } public removeTransition(from: string, to: string) { @@ -173,7 +188,7 @@ export class WorkflowDto { return this; } - return new WorkflowDto(this._links, this.initial, this.steps, transitions); + return this.createNew({ transitions }); } public setTransition(from: string, to: string, values: Partial = {}) { @@ -199,11 +214,11 @@ export class WorkflowDto { const transitions = [...this.transitions.filter(t => t !== found), { from, to, ...values }]; - return new WorkflowDto(this._links, this.initial, this.steps, transitions); + return this.createNew({ transitions }); } public serialize(): any { - const result = { steps: {}, initial: this.initial }; + const result = { steps: {}, initial: this.initial, name: this.name }; for (let step of this.steps) { const { name, ...values } = step; @@ -221,6 +236,14 @@ export class WorkflowDto { return result; } + + private createNew(values: { steps?: WorkflowStep[], transitions?: WorkflowTransition[], initial?: string | null, name?: string | null }) { + return new WorkflowDto(this._links, this.id, + Types.isUndefined(values.name) ? this.name : values.name, + Types.isUndefined(values.initial) ? this.initial : values.initial, + values.steps || this.steps, + values.transitions || this.transitions); + } } export type WorkflowStepValues = { color?: string; isLocked?: boolean; noUpdate?: boolean; }; @@ -245,7 +268,7 @@ export class WorkflowsService { } public getWorkflows(appName: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflow`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflows`); return HTTP.getVersioned(this.http, url).pipe( mapVersioned(({ body }) => { @@ -255,7 +278,7 @@ export class WorkflowsService { } public postWorkflow(appName: string, dto: CreateWorkflowDto, version: Version): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflow`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/workflows`); return HTTP.postVersioned(this.http, url, dto, version).pipe( mapVersioned(({ body }) => { @@ -329,5 +352,5 @@ function parseWorkflow(workflow: any) { } } - return new WorkflowDto(workflow._links, workflow.initial, steps, transitions); + return new WorkflowDto(workflow._links, workflow.id, workflow.name, workflow.initial, steps, transitions); } \ No newline at end of file diff --git a/src/Squidex/app/shared/state/clients.forms.ts b/src/Squidex/app/shared/state/clients.forms.ts index 4b1ad2372..fbd4c8ba7 100644 --- a/src/Squidex/app/shared/state/clients.forms.ts +++ b/src/Squidex/app/shared/state/clients.forms.ts @@ -25,7 +25,7 @@ export class RenameClientForm extends Form { } } -export class AttachClientForm extends Form { +export class AddClientForm extends Form { public hasNoName = hasNoValue$(this.form.controls['name']); constructor(formBuilder: FormBuilder) { diff --git a/src/Squidex/app/shared/state/workflows.forms.ts b/src/Squidex/app/shared/state/workflows.forms.ts new file mode 100644 index 000000000..7c0fc21ac --- /dev/null +++ b/src/Squidex/app/shared/state/workflows.forms.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { Form, hasNoValue$ } from '@app/framework'; + +export class AddWorkflowForm extends Form { + public hasNoName = hasNoValue$(this.form.controls['name']); + + constructor(formBuilder: FormBuilder) { + super(formBuilder.group({ + name: ['', + [ + Validators.required + ] + ] + })); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/state/workflows.state.spec.ts b/src/Squidex/app/shared/state/workflows.state.spec.ts index ae3b646c2..bb3567e3f 100644 --- a/src/Squidex/app/shared/state/workflows.state.spec.ts +++ b/src/Squidex/app/shared/state/workflows.state.spec.ts @@ -11,12 +11,12 @@ import { IMock, It, Mock, Times } from 'typemoq'; import { DialogService, versioned, - WorkflowPayload, + WorkflowsPayload, WorkflowsService, WorkflowsState } from '@app/shared/internal'; -import { createWorkflow } from '../services/workflows.service.spec'; +import { createWorkflows } from '../services/workflows.service.spec'; import { TestValues } from './_test-helpers'; @@ -28,7 +28,7 @@ describe('WorkflowsState', () => { version } = TestValues; - const oldWorkflow = createWorkflow('test'); + const oldWorkflows = createWorkflows('1', '2'); let dialogs: IMock; let workflowsService: IMock; @@ -48,11 +48,11 @@ describe('WorkflowsState', () => { describe('Loading', () => { it('should load workflow', () => { workflowsService.setup(x => x.getWorkflows(app)) - .returns(() => of(versioned(version, oldWorkflow))).verifiable(); + .returns(() => of(versioned(version, oldWorkflows))).verifiable(); workflowsState.load().subscribe(); - expect(workflowsState.snapshot.workflow).toEqual(oldWorkflow.workflow); + expect(workflowsState.snapshot.workflows.values).toEqual(oldWorkflows.items); expect(workflowsState.snapshot.isLoaded).toBeTruthy(); expect(workflowsState.snapshot.version).toEqual(version); @@ -61,7 +61,7 @@ describe('WorkflowsState', () => { it('should show notification on load when reload is true', () => { workflowsService.setup(x => x.getWorkflows(app)) - .returns(() => of(versioned(version, oldWorkflow))).verifiable(); + .returns(() => of(versioned(version, oldWorkflows))).verifiable(); workflowsState.load(true).subscribe(); @@ -74,28 +74,50 @@ describe('WorkflowsState', () => { describe('Updates', () => { beforeEach(() => { workflowsService.setup(x => x.getWorkflows(app)) - .returns(() => of(versioned(version, oldWorkflow))).verifiable(); + .returns(() => of(versioned(version, oldWorkflows))).verifiable(); workflowsState.load().subscribe(); }); - it('should update workflows when saved', () => { - const updated = createWorkflow('updated'); + it('should update workflows when workflow added', () => { + const updated = createWorkflows('1', '2', '3'); - const request = oldWorkflow.workflow.serialize(); - - workflowsService.setup(x => x.putWorkflow(app, oldWorkflow.workflow, request, version)) + workflowsService.setup(x => x.postWorkflow(app, { name: 'my-workflow' }, version)) .returns(() => of(versioned(newVersion, updated))).verifiable(); - workflowsState.save(oldWorkflow.workflow).subscribe(); + workflowsState.add('my-workflow' ).subscribe(); expectNewWorkflows(updated); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); }); - function expectNewWorkflows(updated: WorkflowPayload) { - expect(workflowsState.snapshot.workflow).toEqual(updated.workflow); + it('should update workflows when workflow updated', () => { + const updated = createWorkflows('1', '2', '3'); + + const request = oldWorkflows.items[0].serialize(); + + workflowsService.setup(x => x.putWorkflow(app, oldWorkflows.items[0], request, version)) + .returns(() => of(versioned(newVersion, updated))).verifiable(); + + workflowsState.update(oldWorkflows.items[0]).subscribe(); + + expectNewWorkflows(updated); + }); + + it('should update workflows when workflow deleted', () => { + const updated = createWorkflows('1', '2', '3'); + + workflowsService.setup(x => x.deleteWorkflow(app, oldWorkflows.items[0], version)) + .returns(() => of(versioned(newVersion, updated))).verifiable(); + + workflowsState.delete(oldWorkflows.items[0]).subscribe(); + + expectNewWorkflows(updated); + }); + + function expectNewWorkflows(updated: WorkflowsPayload) { + expect(workflowsState.snapshot.workflows.values).toEqual(updated.items); expect(workflowsState.snapshot.version).toEqual(newVersion); } }); diff --git a/src/Squidex/app/shared/state/workflows.state.ts b/src/Squidex/app/shared/state/workflows.state.ts index 42bdbf2c0..841365fba 100644 --- a/src/Squidex/app/shared/state/workflows.state.ts +++ b/src/Squidex/app/shared/state/workflows.state.ts @@ -13,7 +13,7 @@ import { tap } from 'rxjs/operators'; import { DialogService, - shareMapSubscribed, + ImmutableArray, shareSubscribed, State, Version @@ -23,38 +23,44 @@ import { AppsState } from './apps.state'; import { WorkflowDto, - WorkflowPayload, + WorkflowsPayload, WorkflowsService } from './../services/workflows.service'; interface Snapshot { // The current workflow. - workflow?: WorkflowDto; + workflows: ImmutableArray; // The app version. version: Version; // Indicates if the workflows are loaded. isLoaded?: boolean; + + // Indicates if the user can create new workflow. + canCreate?: boolean; } @Injectable() export class WorkflowsState extends State { - public workflow = - this.project(x => x.workflow); + public workflows = + this.project(x => x.workflows); public isLoaded = this.project(x => !!x.isLoaded); + public canCreate = + this.project(x => !!x.canCreate); + constructor( private readonly workflowsService: WorkflowsService, private readonly appsState: AppsState, private readonly dialogs: DialogService ) { - super({ version: Version.EMPTY }); + super({ workflows: ImmutableArray.empty(), version: Version.EMPTY }); } - public load(isReload = false): Observable { + public load(isReload = false): Observable { if (!isReload) { this.resetState(); } @@ -62,29 +68,47 @@ export class WorkflowsState extends State { return this.workflowsService.getWorkflows(this.appName).pipe( tap(({ version, payload }) => { if (isReload) { - this.dialogs.notifyInfo('Workflow reloaded.'); + this.dialogs.notifyInfo('Workflows reloaded.'); } - this.replaceWorkflow(payload, version); + this.replaceWorkflows(payload, version); }), - shareMapSubscribed(this.dialogs, x => x.payload.workflow)); + shareSubscribed(this.dialogs)); } - public save(workflow: WorkflowDto): Observable { - return this.workflowsService.putWorkflow(this.appName, workflow, workflow.serialize(), this.version).pipe( + public add(name: string): Observable { + return this.workflowsService.postWorkflow(this.appName, { name }, this.version).pipe( tap(({ version, payload }) => { - this.replaceWorkflow(payload, version); + this.replaceWorkflows(payload, version); + }), + shareSubscribed(this.dialogs)); + } + public update(workflow: WorkflowDto): Observable { + return this.workflowsService.putWorkflow(this.appName, workflow, workflow.serialize(), this.version).pipe( + tap(({ version, payload }) => { this.dialogs.notifyInfo('Workflow has been saved.'); + + this.replaceWorkflows(payload, version); + }), + shareSubscribed(this.dialogs)); + } + + public delete(workflow: WorkflowDto): Observable { + return this.workflowsService.deleteWorkflow(this.appName, workflow, this.version).pipe( + tap(({ version, payload }) => { + this.replaceWorkflows(payload, version); }), shareSubscribed(this.dialogs)); } - private replaceWorkflow(payload: WorkflowPayload, version: Version) { - const { workflow } = payload; + private replaceWorkflows(payload: WorkflowsPayload, version: Version) { + const { canCreate, items } = payload; + + const workflows = ImmutableArray.of(items); this.next(s => { - return { ...s, workflow, isLoaded: true, version }; + return { ...s, workflows, isLoaded: true, version, canCreate }; }); } From 91bc2cddc1f872cf94055159bb3742bd6955de04 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 1 Jul 2019 22:26:44 +0200 Subject: [PATCH 06/18] Schema ids. --- .../Contents/Workflow.cs | 30 +++- .../Newtonsoft/ConverterContractResolver.cs | 12 ++ .../Apps/Models/UpdateWorkflowDto.cs | 10 +- .../Controllers/Apps/Models/WorkflowDto.cs | 27 +-- .../pages/workflows/schema-tag-converter.ts | 38 ++++ .../pages/workflows/workflow.component.html | 14 +- .../pages/workflows/workflow.component.scss | 2 - .../pages/workflows/workflow.component.ts | 5 + .../workflows/workflows-page.component.html | 4 +- .../workflows/workflows-page.component.ts | 18 +- .../angular/forms/tag-editor.component.ts | 166 +++++++++++++----- .../shared/services/workflows.service.spec.ts | 31 +++- .../app/shared/services/workflows.service.ts | 49 +++--- .../app/shared/state/workflows.state.spec.ts | 4 +- .../Model/Contents/WorkflowTests.cs | 4 +- .../Apps/Guards/GuardAppWorkflowTests.cs | 24 +-- .../Contents/DynamicContentWorkflowTests.cs | 4 +- ...aryTests.cs => ReadOnlyCollectionTests.cs} | 37 +++- 18 files changed, 358 insertions(+), 121 deletions(-) create mode 100644 src/Squidex/app/features/settings/pages/workflows/schema-tag-converter.ts rename tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/{ReadOnlyDictionaryTests.cs => ReadOnlyCollectionTests.cs} (51%) diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs index 90a430965..391863c0e 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; namespace Squidex.Domain.Apps.Core.Contents @@ -13,26 +14,41 @@ namespace Squidex.Domain.Apps.Core.Contents { private const string DefaultName = "Unnamed"; private static readonly IReadOnlyDictionary EmptySteps = new Dictionary(); + private static readonly IReadOnlyList EmptySchemaIds = new List(); public static readonly Workflow Default = CreateDefault(); - public static readonly Workflow Empty = new Workflow(EmptySteps, default); + public static readonly Workflow Empty = new Workflow(default, EmptySteps); - public IReadOnlyDictionary Steps { get; } + public IReadOnlyDictionary Steps { get; } = EmptySteps; + + public IReadOnlyList SchemaIds { get; } = EmptySchemaIds; public Status Initial { get; } - public Workflow(IReadOnlyDictionary steps, Status initial, string name = null) + public Workflow( + Status initial, + IReadOnlyDictionary steps, + IReadOnlyList schemaIds = null, + string name = null) : base(name ?? DefaultName) { - Steps = steps ?? EmptySteps; - Initial = initial; + + if (steps != null) + { + Steps = steps; + } + + if (schemaIds != null) + { + SchemaIds = schemaIds; + } } public static Workflow CreateDefault(string name = null) { return new Workflow( - new Dictionary + Status.Draft, new Dictionary { [Status.Archived] = new WorkflowStep( @@ -57,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Contents [Status.Draft] = new WorkflowTransition() }, StatusColors.Published) - }, Status.Draft, name); + }, null, name); } public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs index fd7b6d533..a560e4abc 100644 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs +++ b/src/Squidex.Infrastructure/Json/Newtonsoft/ConverterContractResolver.cs @@ -36,6 +36,18 @@ namespace Squidex.Infrastructure.Json.Newtonsoft } } + protected override JsonArrayContract CreateArrayContract(Type objectType) + { + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)) + { + var implementationType = typeof(List<>).MakeGenericType(objectType.GetGenericArguments()); + + return base.CreateArrayContract(implementationType); + } + + return base.CreateArrayContract(objectType); + } + protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs index 6a6f0bfa6..4831f5ec0 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateWorkflowDto.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -26,6 +27,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models [Required] public Dictionary Steps { get; set; } + /// + /// The schema ids. + /// + public List SchemaIds { get; set; } + /// /// The initial step. /// @@ -34,6 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public UpdateWorkflow ToCommand() { var workflow = new Workflow( + Initial, Steps?.ToDictionary( x => x.Key, x => new WorkflowStep( @@ -42,7 +49,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models y => new WorkflowTransition(y.Value.Expression, y.Value.Role)), x.Value.Color, x.Value.NoUpdate)), - Initial, Name); + SchemaIds, + Name); return new UpdateWorkflow { Workflow = workflow }; } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs index 7906addaa..5e249085b 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs @@ -34,6 +34,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models [Required] public Dictionary Steps { get; set; } + /// + /// The schema ids. + /// + public IReadOnlyList SchemaIds { get; set; } + /// /// The initial step. /// @@ -41,18 +46,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public static WorkflowDto FromWorkflow(Guid id, Workflow workflow, ApiController controller, string app) { - var result = new WorkflowDto - { - Steps = workflow.Steps.ToDictionary( - x => x.Key, - x => SimpleMapper.Map(x.Value, new WorkflowStepDto - { - Transitions = x.Value.Transitions.ToDictionary( - y => y.Key, - y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role }) - })), - Id = id, Name = workflow.Name, Initial = workflow.Initial - }; + var result = SimpleMapper.Map(workflow, new WorkflowDto { Id = id }); + + result.Steps = workflow.Steps.ToDictionary( + x => x.Key, + x => SimpleMapper.Map(x.Value, new WorkflowStepDto + { + Transitions = x.Value.Transitions.ToDictionary( + y => y.Key, + y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role }) + })); return result.CreateLinks(controller, app, id); } diff --git a/src/Squidex/app/features/settings/pages/workflows/schema-tag-converter.ts b/src/Squidex/app/features/settings/pages/workflows/schema-tag-converter.ts new file mode 100644 index 000000000..1f3bd4ab0 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/workflows/schema-tag-converter.ts @@ -0,0 +1,38 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Converter, SchemaDto, TagValue } from '@app/shared'; + +export class SchemaTagConverter implements Converter { + public readonly suggestions: TagValue[]; + + constructor( + private readonly schemas: SchemaDto[] + ) { + this.suggestions = schemas.map(x => new TagValue(x.id, x.name, x.id)); + } + + public convertInput(input: string): TagValue | null { + const schema = this.schemas.find(x => x.name === input); + + if (schema) { + return new TagValue(schema.id, schema.name, schema.id); + } + + return null; + } + + public convertValue(value: any): TagValue | null { + const schema = this.schemas.find(x => x.id === value); + + if (schema) { + return new TagValue(schema.id, schema.name, schema.id); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html index 14b64d4e1..671b50f2e 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html @@ -36,7 +36,7 @@
-
+
+
+ + +
+ + + + Restrict this workflow to specific schemas or keep it empty for all schemas. + +
+
+ - +
No workflows created yet.
+ [workflow]="workflow" [roles]="roles" [schemasSource]="schemasSource">
- +
- + + Restrict this workflow to specific schemas or keep it empty for all schemas. diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts b/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts index 84c75d613..976fd86b8 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts @@ -90,6 +90,10 @@ export class WorkflowComponent implements OnChanges { this.workflow = this.workflow.rename(name); } + public changeSchemaIds(schemaIds: string[]) { + this.workflow = this.workflow.changeSchemaIds(schemaIds); + } + public setInitial(step: WorkflowStep) { this.workflow = this.workflow.setInitial(step.name); } diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index c6c735549..a0f6a4472 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -246,12 +246,12 @@ export class TagEditorComponent extends StatefulControlComponent i for (let value of obj) { if (Types.is(value, TagValue)) { items.push(value); - } - - const converted = this.converter.convertValue(obj); + } else { + const converted = this.converter.convertValue(value); - if (converted) { - items.push(value); + if (converted) { + items.push(converted); + } } } } @@ -289,11 +289,9 @@ export class TagEditorComponent extends StatefulControlComponent i } public resetSize() { - if (!CACHED_FONT) { - return; - } - - if (!this.inputElement.nativeElement) { + if (!CACHED_FONT || + !this.inputElement || + !this.inputElement.nativeElement) { return; } From 0606c9d02afde0dd90cdbafc697edebaff2408b6 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 2 Jul 2019 11:30:36 +0200 Subject: [PATCH 08/18] Added global validation logic for workflows. --- .../Contents/Workflow.cs | 4 +- .../Contents/Workflows.cs | 1 + .../Contents/DefaultWorkflowsValidator.cs | 57 +++++++++ .../Contents/IWorkflowsValidator.cs | 19 +++ .../Apps/AppWorkflowsController.cs | 12 +- .../Controllers/Apps/Models/WorkflowsDto.cs | 17 ++- src/Squidex/Config/Domain/EntitiesServices.cs | 3 + .../workflows/workflow-step.component.html | 2 +- .../pages/workflows/workflow.component.html | 7 +- .../pages/workflows/workflow.component.ts | 6 +- .../workflows/workflows-page.component.html | 13 +- .../workflows/workflows-page.component.scss | 8 +- .../shared/services/workflows.service.spec.ts | 8 ++ .../app/shared/services/workflows.service.ts | 6 +- .../app/shared/state/workflows.state.ts | 12 +- .../DefaultWorkflowsValidatorTests.cs | 115 ++++++++++++++++++ 16 files changed, 267 insertions(+), 23 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs index 391863c0e..f0187e361 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs @@ -13,9 +13,9 @@ namespace Squidex.Domain.Apps.Core.Contents public sealed class Workflow : Named { private const string DefaultName = "Unnamed"; - private static readonly IReadOnlyDictionary EmptySteps = new Dictionary(); - private static readonly IReadOnlyList EmptySchemaIds = new List(); + public static readonly IReadOnlyDictionary EmptySteps = new Dictionary(); + public static readonly IReadOnlyList EmptySchemaIds = new List(); public static readonly Workflow Default = CreateDefault(); public static readonly Workflow Empty = new Workflow(default, EmptySteps); diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index 3675f1b81..504ab5d79 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; +using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs new file mode 100644 index 000000000..75ddd704b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class DefaultWorkflowsValidator : IWorkflowsValidator + { + private readonly IAppProvider appProvider; + + public DefaultWorkflowsValidator(IAppProvider appProvider) + { + Guard.NotNull(appProvider, nameof(appProvider)); + + this.appProvider = appProvider; + } + + public async Task> ValidateAsync(Guid appId, Workflows workflows) + { + Guard.NotNull(workflows, nameof(workflows)); + + var errors = new List(); + + if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) + { + errors.Add("Multiple workflows cover all schemas."); + } + + var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); + + foreach (var schemaId in uniqueSchemaIds) + { + if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) + { + var schema = await appProvider.GetSchemaAsync(appId, schemaId); + + if (schema != null) + { + errors.Add($"The schema `{schema.SchemaDef.Name}` is covered by multiple workflows."); + } + } + } + + return errors; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs b/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs new file mode 100644 index 000000000..01c8574b4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IWorkflowsValidator + { + Task> ValidateAsync(Guid appId, Workflows workflows); + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index 22e003e75..0c3007269 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -12,6 +12,7 @@ using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; @@ -24,9 +25,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiExplorerSettings(GroupName = nameof(Apps))] public sealed class AppWorkflowsController : ApiController { - public AppWorkflowsController(ICommandBus commandBus) + private readonly IWorkflowsValidator workflowsValidator; + + public AppWorkflowsController(ICommandBus commandBus, IWorkflowsValidator workflowsValidator) : base(commandBus) { + this.workflowsValidator = workflowsValidator; } /// @@ -42,9 +46,9 @@ namespace Squidex.Areas.Api.Controllers.Apps [ProducesResponseType(typeof(WorkflowsDto), 200)] [ApiPermission(Permissions.AppWorkflowsRead)] [ApiCosts(0)] - public IActionResult GetWorkflows(string app) + public async Task GetWorkflows(string app) { - var response = WorkflowsDto.FromApp(App, this); + var response = await WorkflowsDto.FromAppAsync(workflowsValidator, App, this); Response.Headers[HeaderNames.ETag] = App.Version.ToString(); @@ -128,7 +132,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = WorkflowsDto.FromApp(result, this); + var response = await WorkflowsDto.FromAppAsync(workflowsValidator, result, this); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs index b58be115c..5e3515eba 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs @@ -5,10 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Shared; using Squidex.Web; @@ -22,13 +23,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models [Required] public WorkflowDto[] Items { get; set; } - public static WorkflowsDto FromApp(IAppEntity app, ApiController controller) + /// + /// The errros that should be fixed. + /// + [Required] + public string[] Errors { get; set; } + + public static async Task FromAppAsync(IWorkflowsValidator workflowsValidator, IAppEntity app, ApiController controller) { var result = new WorkflowsDto { - Items = app.Workflows.Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value, controller, app.Name)).ToArray() + Items = app.Workflows.Select(x => WorkflowDto.FromWorkflow(x.Key, x.Value, controller, app.Name)).ToArray(), }; + var errors = await workflowsValidator.ValidateAsync(app.Id, app.Workflows); + + result.Errors = errors.ToArray(); + return result.CreateLinks(controller, app.Name); } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 47c2e44b9..b9a9813c3 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -123,6 +123,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsOptional(); + services.AddSingletonAs() + .AsOptional(); + services.AddSingletonAs() .AsSelf(); diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html index 57c22536d..162be35c1 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html @@ -29,7 +29,7 @@ (Cannot be removed)
-
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html index b4d834a63..ca2dea003 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html @@ -1,12 +1,12 @@
-
+
{{workflow.displayName}}
- @@ -14,7 +14,8 @@ [disabled]="!workflow.canDelete" (sqxConfirmClick)="remove()" confirmTitle="Remove workflow" - confirmText="Do you really want to remove the workflow?"> + confirmText="Do you really want to remove the workflow?" + sqxStopClick>
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts b/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts index 976fd86b8..afd1c34a9 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.ts @@ -36,7 +36,7 @@ export class WorkflowComponent implements OnChanges { @Input() public schemasSource: SchemaTagConverter; - public error: ErrorDto | null; + public error: string | null; public onBlur = { updateOn: 'blur' }; @@ -68,8 +68,8 @@ export class WorkflowComponent implements OnChanges { this.workflowsState.update(this.workflow) .subscribe(() => { this.error = null; - }, error => { - this.error = error; + }, (error: ErrorDto) => { + this.error = error.displayMessage; }); } diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html index 535f3a7f0..135386ceb 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.html @@ -2,7 +2,7 @@ - Workflow + Workflows @@ -14,6 +14,17 @@ + +
+
    +
  • {{error}}
  • +
+
+
+ {{errors[0]}} +
+
+
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss index fbb752506..ad50cdf61 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss +++ b/src/Squidex/app/features/settings/pages/workflows/workflows-page.component.scss @@ -1,2 +1,8 @@ @import '_vars'; -@import '_mixins'; \ No newline at end of file +@import '_mixins'; + +.panel-alert { + ul { + margin: 0; + } +} \ 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 index a3bb52009..901a7b206 100644 --- a/src/Squidex/app/shared/services/workflows.service.spec.ts +++ b/src/Squidex/app/shared/services/workflows.service.spec.ts @@ -147,6 +147,10 @@ describe('WorkflowsService', () => { function workflowsResponse(...names: string[]) { return { + errors: [ + 'Error1', + 'Error2' + ], items: names.map(name => workflowResponse(name)), _links: { create: { method: 'POST', href: '/workflows' } @@ -187,6 +191,10 @@ describe('WorkflowsService', () => { export function createWorkflows(...names: string[]): WorkflowsPayload { return { + errors: [ + 'Error1', + 'Error2' + ], items: names.map(name => createWorkflow(name)), _links: { create: { method: 'POST', href: '/workflows' } diff --git a/src/Squidex/app/shared/services/workflows.service.ts b/src/Squidex/app/shared/services/workflows.service.ts index cc34d9e82..373160909 100644 --- a/src/Squidex/app/shared/services/workflows.service.ts +++ b/src/Squidex/app/shared/services/workflows.service.ts @@ -30,6 +30,8 @@ export type WorkflowsDto = Versioned; export type WorkflowsPayload = { readonly items: WorkflowDto[]; + readonly errors: string[]; + readonly canCreate: boolean; } & Resource; @@ -330,9 +332,9 @@ function parseWorkflows(response: any) { const items = raw.map(item => parseWorkflow(item)); - const { _links } = response; + const { errors, _links } = response; - return { items, _links, canCreate: hasAnyLink(_links, 'create') }; + return { errors, items, _links, canCreate: hasAnyLink(_links, 'create') }; } function parseWorkflow(workflow: any) { diff --git a/src/Squidex/app/shared/state/workflows.state.ts b/src/Squidex/app/shared/state/workflows.state.ts index 841365fba..6557dea6f 100644 --- a/src/Squidex/app/shared/state/workflows.state.ts +++ b/src/Squidex/app/shared/state/workflows.state.ts @@ -34,6 +34,9 @@ interface Snapshot { // The app version. version: Version; + // The errors. + errors: string[]; + // Indicates if the workflows are loaded. isLoaded?: boolean; @@ -46,6 +49,9 @@ export class WorkflowsState extends State { public workflows = this.project(x => x.workflows); + public errors = + this.project(x => x.errors); + public isLoaded = this.project(x => !!x.isLoaded); @@ -57,7 +63,7 @@ export class WorkflowsState extends State { private readonly appsState: AppsState, private readonly dialogs: DialogService ) { - super({ workflows: ImmutableArray.empty(), version: Version.EMPTY }); + super({ errors: [], workflows: ImmutableArray.empty(), version: Version.EMPTY }); } public load(isReload = false): Observable { @@ -103,12 +109,12 @@ export class WorkflowsState extends State { } private replaceWorkflows(payload: WorkflowsPayload, version: Version) { - const { canCreate, items } = payload; + const { canCreate, errors, items } = payload; const workflows = ImmutableArray.of(items); this.next(s => { - return { ...s, workflows, isLoaded: true, version, canCreate }; + return { ...s, workflows, errors, isLoaded: true, version, canCreate }; }); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs new file mode 100644 index 000000000..9a887a73b --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs @@ -0,0 +1,115 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class DefaultWorkflowsValidatorTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly DefaultWorkflowsValidator sut; + + public DefaultWorkflowsValidatorTests() + { + var schema = A.Fake(); + + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A.Ignored, false)) + .Returns(Task.FromResult(null)); + + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + .Returns(schema); + + sut = new DefaultWorkflowsValidator(appProvider); + } + + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_all_schemas() + { + var workflows = Workflows.Empty + .Add(Guid.NewGuid(), "workflow1") + .Add(Guid.NewGuid(), "workflow2"); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Equal(errors, new string[] { "Multiple workflows cover all schemas." }); + } + + [Fact] + public async Task Should_generate_error_if_multiple_workflows_cover_specific_schema() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })) + .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Equal(errors, new string[] { "The schema `my-schema` is covered by multiple workflows." }); + } + + [Fact] + public async Task Should_not_generate_error_if_schema_deleted() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var oldSchemaId = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })) + .Update(id2, new Workflow(default, Workflow.EmptySteps, new List { oldSchemaId })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_generate_errors_for_no_overlaps() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var workflows = Workflows.Empty + .Add(id1, "workflow1") + .Add(id2, "workflow2") + .Update(id1, new Workflow(default, Workflow.EmptySteps, new List { schemaId.Id })); + + var errors = await sut.ValidateAsync(appId.Id, workflows); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_generate_errors_for_empty_workflows() + { + var errors = await sut.ValidateAsync(appId.Id, Workflows.Empty); + + Assert.Empty(errors); + } + } +} From 22df4bda42c0a5dbf1434d5cbdbcab9b3922c27a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 2 Jul 2019 13:18:33 +0200 Subject: [PATCH 09/18] Improvements to tag editor. --- .../pages/workflows/workflow.component.html | 20 ++++-- .../pages/workflows/workflow.component.scss | 11 +++ .../angular/forms/tag-editor.component.html | 4 +- .../angular/forms/tag-editor.component.scss | 13 ++++ .../angular/forms/tag-editor.component.ts | 5 +- .../angular/modals/modal-view.directive.ts | 69 ++++++++----------- .../shared/components/asset.component.html | 2 +- 7 files changed, 73 insertions(+), 51 deletions(-) diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html index ca2dea003..dcfac9bda 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.html @@ -1,12 +1,21 @@
-
+
-
- {{workflow.displayName}} +
+ {{workflow.displayName}} +
+
+ +
- @@ -14,8 +23,7 @@ [disabled]="!workflow.canDelete" (sqxConfirmClick)="remove()" confirmTitle="Remove workflow" - confirmText="Do you really want to remove the workflow?" - sqxStopClick> + confirmText="Do you really want to remove the workflow?">
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss b/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss index bfb99a1aa..b39e484f1 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss +++ b/src/Squidex/app/features/settings/pages/workflows/workflow.component.scss @@ -7,11 +7,22 @@ } } +.workflow { + &-name { + @include truncate; + } +} + .col-form-label { min-width: 4rem; max-width: 4rem; } +.col-tags { + padding: .6rem 1rem; + padding-bottom: 0; +} + .form-group { margin-bottom: 2rem; margin-left: 2rem; diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index 927fe127f..14147ae43 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -1,10 +1,10 @@ -
- {{item}} + {{item}} i public singleLine = false; @Input() - public class: string; + public styleBlank = false; + + @Input() + public styleGray = false; @Input() public placeholder = ', to add tag'; diff --git a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts index 01cd544fb..22dc8c01e 100644 --- a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts @@ -21,9 +21,7 @@ import { RootViewComponent } from './root-view.component'; }) export class ModalViewDirective implements OnChanges, OnDestroy { private modalSubscription: Subscription | null = null; - private documentClickListener: Function | null = null; private renderedView: EmbeddedViewRef | null = null; - private static clickCounter = 0; @Input('sqxModalView') public modalView: DialogModel | ModalModel | any; @@ -44,13 +42,6 @@ export class ModalViewDirective implements OnChanges, OnDestroy { private readonly templateRef: TemplateRef, private readonly viewContainer: ViewContainerRef ) { - if (ModalViewDirective.clickCounter === 0) { - this.renderer.listen('document', 'click', () => { - ModalViewDirective.clickCounter++; - }); - - ModalViewDirective.clickCounter = 1; - } } public ngOnDestroy() { @@ -95,7 +86,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { this.renderer.setStyle(this.renderedView.rootNodes[0], 'display', 'block'); } - this.startListening(ModalViewDirective.clickCounter + 1); + this.startListening(); this.changeDetector.detectChanges(); } else if (!isOpen && this.renderedView) { @@ -114,40 +105,39 @@ export class ModalViewDirective implements OnChanges, OnDestroy { return this.placeOnRoot ? this.rootView.viewContainer : this.viewContainer; } - private startListening(clickCounter: number) { - if (!this.closeAuto) { + private startListening() { + if (this.closeAuto) { + document.addEventListener('click', this.documentClickListener, true); + } + } + + private documentClickListener = (event: MouseEvent) => { + if (!event.target || this.renderedView === null) { return; } - this.documentClickListener = - this.renderer.listen('document', 'click', (event: MouseEvent) => { - if (!event.target || this.renderedView === null || ModalViewDirective.clickCounter === clickCounter) { - return; - } + if (this.renderedView.rootNodes.length === 0) { + return; + } - if (this.renderedView.rootNodes.length === 0) { - return; - } + if (this.closeAlways) { + this.modalView.hide(); + } else { + try { + const rootNode = this.renderedView.rootNodes[0]; + const rootBounds = rootNode.getBoundingClientRect(); + + if (rootBounds.width > 0 && rootBounds.height > 0) { + const clickedInside = rootNode.contains(event.target); - if (this.closeAlways) { - this.modalView.hide(); - } else { - try { - const rootNode = this.renderedView.rootNodes[0]; - const rootBounds = rootNode.getBoundingClientRect(); - - if (rootBounds.width > 0 && rootBounds.height > 0) { - const clickedInside = rootNode.contains(event.target); - - if (!clickedInside && this.modalView) { - this.modalView.hide(); - } - } - } catch (ex) { - return; + if (!clickedInside && this.modalView) { + this.modalView.hide(); } } - }); + } catch (ex) { + return; + } + } } private unsubscribeToModal() { @@ -158,9 +148,6 @@ export class ModalViewDirective implements OnChanges, OnDestroy { } private unsubscribeToClick() { - if (this.documentClickListener) { - this.documentClickListener(); - this.documentClickListener = null; - } + document.removeEventListener('click', this.documentClickListener); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 7fe2798ce..e3d78800e 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -69,7 +69,7 @@
- +
{{asset.pixelWidth}}x{{asset.pixelHeight}}px, {{asset.fileSize | sqxFileSize}} From f42a47d9aed401bd04801fae166438bca563bc61 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 2 Jul 2019 16:31:17 +0200 Subject: [PATCH 10/18] Temp --- .../Contents/Workflows.cs | 8 ++ .../Contents/DynamicContentWorkflow.cs | 33 ++++++-- src/Squidex.Shared/Permissions.cs | 4 +- .../Contents/ContentsController.cs | 2 +- .../Generator/SchemaSwaggerGenerator.cs | 4 +- .../Areas/Api/Controllers/Contents/Helper.cs | 23 ------ .../Controllers/Contents/Models/ContentDto.cs | 21 +++-- .../Contents/Models/ContentsDto.cs | 2 +- .../Contents/DynamicContentWorkflowTests.cs | 80 +++++++++++++++++-- 9 files changed, 123 insertions(+), 54 deletions(-) delete mode 100644 src/Squidex/Areas/Api/Controllers/Contents/Helper.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index 504ab5d79..b5d86740c 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -50,6 +50,14 @@ namespace Squidex.Domain.Apps.Core.Contents return new Workflows(With(Guid.Empty, workflow)); } + [Pure] + public Workflows Set(Guid id, Workflow workflow) + { + Guard.NotNull(workflow, nameof(workflow)); + + return new Workflows(With(id, workflow)); + } + [Pure] public Workflows Update(Guid id, Workflow workflow) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs index 6a302fcce..20d15e902 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -34,21 +34,21 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task GetAllAsync(ISchemaEntity schema) { - var workflow = await GetWorkflowAsync(schema.AppId.Id); + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); } public async Task CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) { - var workflow = await GetWorkflowAsync(content.AppId.Id); + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content, user); } public async Task CanUpdateAsync(IContentEntity content) { - var workflow = await GetWorkflowAsync(content.AppId.Id); + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); if (workflow.TryGetStep(content.Status, out var step)) { @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task GetInfoAsync(IContentEntity content) { - var workflow = await GetWorkflowAsync(content.AppId.Id); + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); if (workflow.TryGetStep(content.Status, out var step)) { @@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task GetInitialStatusAsync(ISchemaEntity schema) { - var workflow = await GetWorkflowAsync(schema.AppId.Id); + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); var (status, step) = workflow.GetInitialStep(); @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var result = new List(); - var workflow = await GetWorkflowAsync(content.AppId.Id); + var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) { @@ -114,11 +114,28 @@ namespace Squidex.Domain.Apps.Entities.Contents return true; } - private async Task GetWorkflowAsync(Guid appId) + private async Task GetWorkflowAsync(Guid appId, Guid schemaId) { + Workflow result = null; + var app = await appProvider.GetAppAsync(appId); - return app?.Workflows.GetFirst(); + if (app != null) + { + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); + + if (result == null) + { + result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); + } + } + + if (result == null) + { + result = Workflow.Default; + } + + return result; } private static string GetColor(WorkflowStep step) diff --git a/src/Squidex.Shared/Permissions.cs b/src/Squidex.Shared/Permissions.cs index 62329248e..10ceb8fef 100644 --- a/src/Squidex.Shared/Permissions.cs +++ b/src/Squidex.Shared/Permissions.cs @@ -121,8 +121,8 @@ namespace Squidex.Shared public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create"; public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update"; - public const string AppContentsStatus = "squidex.apps.{app}.contents.{name}.status.{status}"; - public const string AppContentsDiscard = "squidex.apps.{app}.contents.{name}.discard"; + public const string AppContentsDraftDiscard = "squidex.apps.{app}.contents.{name}.draft.discard"; + public const string AppContentsDraftPublish = "squidex.apps.{app}.contents.{name}.draft.publish"; public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; public const string AppApi = "squidex.apps.{app}.api"; diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 095be8d07..6ddd9970d 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -396,7 +396,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/discard/")] [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsDiscard)] + [ApiPermission(Permissions.AppContentsDraftDiscard)] [ApiCosts(1)] public async Task DiscardDraft(string app, string name, Guid id) { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs index 4aad54547..bda1b710d 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs @@ -194,7 +194,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator operation.AddResponse("204", $"{schemaName} content status changed.", contentSchema); operation.AddResponse("400", "Content data valid."); - AddSecurity(operation, Permissions.AppContentsStatus); + AddSecurity(operation, Permissions.AppContentsMove); }); } @@ -209,7 +209,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator operation.AddResponse("400", "No pending draft."); operation.AddResponse("200", $"{schemaName} content status changed.", contentSchema); - AddSecurity(operation, Permissions.AppContentsDiscard); + AddSecurity(operation, Permissions.AppContentsDraftDiscard); }); } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs deleted file mode 100644 index 8644c925a..000000000 --- a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure.Security; -using Squidex.Shared; - -namespace Squidex.Areas.Api.Controllers.Contents -{ - public static class Helper - { - public static Permission StatusPermission(string app, string schema, Status status) - { - var id = Permissions.AppContentsStatus.Replace("{status}", status.Name); - - return Permissions.ForApp(id, app, schema); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 0725239e4..01775bd9c 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -122,12 +122,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models if (IsPending) { - if (controller.HasPermission(Permissions.AppContentsDiscard, app, schema)) + if (controller.HasPermission(Permissions.AppContentsDraftDiscard, app, schema)) { AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); } - if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published))) + if (controller.HasPermission(Permissions.AppContentsDraftPublish, app, schema)) { AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); } @@ -146,24 +146,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models } AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); - } - - if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) - { - AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); - } - if (content.Nexts != null) - { - foreach (var next in content.Nexts) + if (content.Nexts != null) { - if (controller.HasPermission(Helper.StatusPermission(app, schema, next.Status))) + foreach (var next in content.Nexts) { AddPutLink($"status/{next.Status}", controller.Url(x => nameof(x.PutContentStatus), values), next.Color); } } } + if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) + { + AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); + } + return this; } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 749e662d1..ebf991903 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -80,7 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models { AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); - if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published))) + if (controller.HasPermission(Permissions.AppContentsCreatePublished, app, schema)) { AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index 027bc03c1..ccffa1c8c 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities.Contents public class DynamicContentWorkflowTests { private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + private readonly NamedId simpleSchemaId = NamedId.Of(Guid.NewGuid(), "my-simple-schema"); private readonly IAppProvider appProvider = A.Fake(); private readonly IAppEntity appEntity = A.Fake(); private readonly DynamicContentWorkflow sut; @@ -56,13 +58,38 @@ namespace Squidex.Domain.Apps.Entities.Contents StatusColors.Published) }); + private readonly Workflow simpleWorkflow; + public DynamicContentWorkflowTests() { + simpleWorkflow = new Workflow( + Status.Draft, + new Dictionary + { + [Status.Draft] = + new WorkflowStep( + new Dictionary + { + [Status.Published] = new WorkflowTransition() + }, + StatusColors.Draft), + [Status.Published] = + new WorkflowStep( + new Dictionary + { + [Status.Draft] = new WorkflowTransition() + }, + StatusColors.Published) + }, + new List { simpleSchemaId.Id }); + + var workflows = Workflows.Empty.Set(workflow).Set(Guid.NewGuid(), simpleWorkflow); + A.CallTo(() => appProvider.GetAppAsync(appId.Id)) .Returns(appEntity); A.CallTo(() => appEntity.Workflows) - .Returns(Workflows.Empty.Set(workflow)); + .Returns(workflows); sut = new DynamicContentWorkflow(new JintScriptEngine(), appProvider); } @@ -229,24 +256,67 @@ namespace Squidex.Domain.Apps.Entities.Contents result.Should().BeEquivalentTo(expected); } - private ISchemaEntity CreateSchema() + [Fact] + public async Task Should_return_all_statuses_for_simple_schema_workflow() + { + var expected = new[] + { + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(CreateSchema(true)); + + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task Should_return_all_statuses_for_default_workflow_when_no_workflow_configured() + { + A.CallTo(() => appEntity.Workflows).Returns(Workflows.Empty); + + var expected = new[] + { + new StatusInfo(Status.Archived, StatusColors.Archived), + new StatusInfo(Status.Draft, StatusColors.Draft), + new StatusInfo(Status.Published, StatusColors.Published) + }; + + var result = await sut.GetAllAsync(CreateSchema(true)); + + result.Should().BeEquivalentTo(expected); + } + + private ISchemaEntity CreateSchema(bool simple = false) { var schema = A.Fake(); A.CallTo(() => schema.AppId).Returns(appId); + A.CallTo(() => schema.Id).Returns(simple ? simpleSchemaId.Id : schemaId.Id); return schema; } - private IContentEntity CreateContent(Status status, int value) + private IContentEntity CreateContent(Status status, int value, bool simple = false) { - var data = + var content = new ContentEntity { AppId = appId, Status = status }; + + if (simple) + { + content.SchemaId = simpleSchemaId; + } + else + { + content.SchemaId = schemaId; + } + + content.DataDraft = new NamedContentData() .AddField("field", new ContentFieldData() .AddValue("iv", value)); - return new ContentEntity { AppId = appId, Status = status, DataDraft = data }; + return content; } private ClaimsPrincipal User(string role) From b7c4c8f2e3a65a85402f29fa2ed62a7cc3257e60 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 2 Jul 2019 16:57:01 +0200 Subject: [PATCH 11/18] Temp. --- .../Api/Controllers/Contents/ContentsController.cs | 10 ---------- .../Contents/Generator/SchemaSwaggerGenerator.cs | 2 +- .../Api/Controllers/Contents/Models/ContentsDto.cs | 5 +---- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 6ddd9970d..4f75a0417 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -267,11 +267,6 @@ namespace Squidex.Areas.Api.Controllers.Contents { await contentQuery.GetSchemaOrThrowAsync(Context, name); - if (publish && !this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) - { - return new ForbidResult(); - } - var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; var response = await InvokeCommandAsync(app, name, command); @@ -367,11 +362,6 @@ namespace Squidex.Areas.Api.Controllers.Contents { await contentQuery.GetSchemaOrThrowAsync(Context, name); - if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) - { - return new ForbidResult(); - } - var command = request.ToCommand(id); var response = await InvokeCommandAsync(app, name, command); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs index bda1b710d..56209c00c 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs @@ -194,7 +194,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator operation.AddResponse("204", $"{schemaName} content status changed.", contentSchema); operation.AddResponse("400", "Content data valid."); - AddSecurity(operation, Permissions.AppContentsMove); + AddSecurity(operation, Permissions.AppContentsUpdate); }); } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index ebf991903..4d997492d 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -80,10 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models { AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); - if (controller.HasPermission(Permissions.AppContentsCreatePublished, app, schema)) - { - AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); - } + AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); } } From af38e08e5c407b3336364affa932c96f7189d7d3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 2 Jul 2019 17:53:02 +0200 Subject: [PATCH 12/18] Check publishing. --- .../Contents/Commands/ContentDataCommand.cs | 2 - .../Contents/Commands/ContentUpdateCommand.cs | 14 ++++ .../Contents/Commands/PatchContent.cs | 2 +- .../Contents/Commands/UpdateContent.cs | 2 +- .../Contents/ContentGrain.cs | 16 ++--- .../Contents/DefaultContentWorkflow.cs | 6 ++ .../Contents/DynamicContentWorkflow.cs | 15 +++-- .../Contents/Guards/GuardContent.cs | 7 +- .../Contents/IContentWorkflow.cs | 2 + .../Contents/DefaultContentWorkflowTests.cs | 8 +++ .../Contents/DynamicContentWorkflowTests.cs | 32 ++++++++- .../Contents/Guard/GuardContentTests.cs | 65 +++++++++++-------- 12 files changed, 125 insertions(+), 46 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs index 7f0842c16..f2eea4643 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs @@ -12,7 +12,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public abstract class ContentDataCommand : ContentCommand { public NamedContentData Data { get; set; } - - public bool AsDraft { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs new file mode 100644 index 000000000..63bd8a400 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.Commands +{ + public abstract class ContentUpdateCommand : ContentDataCommand + { + public bool AsDraft { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs index 80206cebd..6654339d9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public sealed class PatchContent : ContentDataCommand + public sealed class PatchContent : ContentUpdateCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs index 01f642d5c..aeb2ce59e 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands { - public sealed class UpdateContent : ContentDataCommand + public sealed class UpdateContent : ContentUpdateCommand { } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 9d4ba5eb3..ce07554a7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, Guid.Empty, () => "Failed to create content."); - GuardContent.CanCreate(ctx.Schema, c); + await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c); await ctx.ExecuteScriptAndTransformAsync(s => s.Create, "Create", c, c.Data); await ctx.EnrichAsync(c.Data); @@ -190,9 +190,9 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private async Task UpdateAsync(ContentDataCommand c, Func newDataFunc, bool partial) + private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial) { - var isProposal = c.AsDraft && Snapshot.Status == Status.Published; + var isProposal = command.AsDraft && Snapshot.Status == Status.Published; var currentData = isProposal ? @@ -207,22 +207,22 @@ namespace Squidex.Domain.Apps.Entities.Contents if (partial) { - await ctx.ValidatePartialAsync(c.Data); + await ctx.ValidatePartialAsync(command.Data); } else { - await ctx.ValidateAsync(c.Data); + await ctx.ValidateAsync(command.Data); } - newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, "Update", c, newData, Snapshot.Data); + newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, "Update", command, newData, Snapshot.Data); if (isProposal) { - ProposeUpdate(c, newData); + ProposeUpdate(command, newData); } else { - Update(c, newData); + Update(command, newData); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index 0f0075906..47c76f4e0 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -12,6 +12,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Contents { @@ -54,6 +55,11 @@ namespace Squidex.Domain.Apps.Entities.Contents return Task.FromResult(result); } + public Task CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) + { + return TaskHelper.True; + } + public Task CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) { var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs index 20d15e902..6788f21e5 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -43,7 +43,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); - return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content, user); + return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user); + } + + public async Task CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) + { + var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); + + return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user); } public async Task CanUpdateAsync(IContentEntity content) @@ -87,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) { - if (CanUse(transition, content, user)) + if (CanUse(transition, content.DataDraft, user)) { result.Add(new StatusInfo(to, GetColor(step))); } @@ -96,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return result.ToArray(); } - private bool CanUse(WorkflowTransition transition, IContentEntity content, ClaimsPrincipal user) + private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user) { if (!string.IsNullOrWhiteSpace(transition.Role)) { @@ -108,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (!string.IsNullOrWhiteSpace(transition.Expression)) { - return scriptEngine.Evaluate("data", content.DataDraft, transition.Expression); + return scriptEngine.Evaluate("data", data, transition.Expression); } return true; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 70f78d9b5..494237996 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { public static class GuardContent { - public static void CanCreate(ISchemaEntity schema, CreateContent command) + public static async Task CanCreate(ISchemaEntity schema, IContentWorkflow contentWorkflow, CreateContent command) { Guard.NotNull(command, nameof(command)); @@ -29,6 +29,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { throw new DomainException("Singleton content cannot be created."); } + + if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User)) + { + throw new DomainException("Content workflow prevents publishing."); + } } public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index fd2f9dd37..b9acaffc9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { Task GetInitialStatusAsync(ISchemaEntity schema); + Task CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user); + Task CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user); Task CanUpdateAsync(IContentEntity content); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index 6145d14ba..a738581b2 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -16,6 +16,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { private readonly DefaultContentWorkflow sut = new DefaultContentWorkflow(); + [Fact] + public async Task Should_always_allow_publish_on_create() + { + var result = await sut.CanPublishOnCreateAsync(null, null, null); + + Assert.True(result); + } + [Fact] public async Task Should_draft_as_initial_status() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index ccffa1c8c..911c756ec 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -104,6 +104,36 @@ namespace Squidex.Domain.Apps.Entities.Contents result.Should().BeEquivalentTo(expected); } + [Fact] + public async Task Should_allow_publish_on_create() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanPublishOnCreateAsync(CreateSchema(), content.DataDraft, User("Editor")); + + Assert.True(result); + } + + [Fact] + public async Task Should_not_allow_publish_on_create_if_data_is_invalid() + { + var content = CreateContent(Status.Draft, 4); + + var result = await sut.CanPublishOnCreateAsync(CreateSchema(), content.DataDraft, User("Editor")); + + Assert.False(result); + } + + [Fact] + public async Task Should_not_allow_publish_on_create_if_role_not_allowed() + { + var content = CreateContent(Status.Draft, 2); + + var result = await sut.CanPublishOnCreateAsync(CreateSchema(), content.DataDraft, User("Developer")); + + Assert.False(result); + } + [Fact] public async Task Should_check_is_valid_next() { @@ -125,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } [Fact] - public async Task Should_not_allow_transition_if_expression_does_not_evauate_to_true() + public async Task Should_not_allow_transition_if_data_not_valid() { var content = CreateContent(Status.Draft, 4); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs index b7e827dc3..ac1149312 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -27,51 +27,71 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard private readonly ClaimsPrincipal user = new ClaimsPrincipal(); private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); - [Fact] - public void CanCreate_should_throw_exception_if_data_is_null() + public GuardContentTests() { SetupSingleton(false); + } + [Fact] + public async Task CanCreate_should_throw_exception_if_data_is_null() + { var command = new CreateContent(); - ValidationAssert.Throws(() => GuardContent.CanCreate(schema, command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanCreate(schema, contentWorkflow, command), new ValidationError("Data is required.", "Data")); } [Fact] - public void CanCreate_should_throw_exception_if_singleton() + public async Task CanCreate_should_throw_exception_if_singleton() { SetupSingleton(true); var command = new CreateContent { Data = new NamedContentData() }; - Assert.Throws(() => GuardContent.CanCreate(schema, command)); + await Assert.ThrowsAsync(() => GuardContent.CanCreate(schema, contentWorkflow, command)); } [Fact] - public void CanCreate_should_not_throw_exception_if_singleton_and_id_is_schema_id() + public async Task CanCreate_should_not_throw_exception_if_singleton_and_id_is_schema_id() { SetupSingleton(true); var command = new CreateContent { Data = new NamedContentData(), ContentId = schema.Id }; - GuardContent.CanCreate(schema, command); + await GuardContent.CanCreate(schema, contentWorkflow, command); } [Fact] - public void CanCreate_should_not_throw_exception_if_data_is_not_null() + public async Task CanCreate_should_throw_exception_publish_not_allowed() { - SetupSingleton(false); + SetupCanCreatePublish(false); + var command = new CreateContent { Data = new NamedContentData(), Publish = true }; + + await Assert.ThrowsAsync(() => GuardContent.CanCreate(schema, contentWorkflow, command)); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_publishing_allowed() + { + SetupCanCreatePublish(true); + + var command = new CreateContent { Data = new NamedContentData(), Publish = true }; + + await Assert.ThrowsAsync(() => GuardContent.CanCreate(schema, contentWorkflow, command)); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_data_is_not_null() + { var command = new CreateContent { Data = new NamedContentData() }; - GuardContent.CanCreate(schema, command); + await GuardContent.CanCreate(schema, contentWorkflow, command); } [Fact] public async Task CanUpdate_should_throw_exception_if_data_is_null() { - SetupSingleton(false); SetupCanUpdate(true); var content = CreateContent(Status.Draft, false); @@ -84,7 +104,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanUpdate_should_throw_exception_if_workflow_blocks_it() { - SetupSingleton(false); SetupCanUpdate(false); var content = CreateContent(Status.Draft, false); @@ -96,7 +115,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanUpdate_should_not_throw_exception_if_data_is_not_null() { - SetupSingleton(false); SetupCanUpdate(true); var content = CreateContent(Status.Draft, false); @@ -108,7 +126,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanPatch_should_throw_exception_if_data_is_null() { - SetupSingleton(false); SetupCanUpdate(true); var content = CreateContent(Status.Draft, false); @@ -121,7 +138,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanPatch_should_throw_exception_if_workflow_blocks_it() { - SetupSingleton(false); SetupCanUpdate(false); var content = CreateContent(Status.Draft, false); @@ -133,7 +149,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanPatch_should_not_throw_exception_if_data_is_not_null() { - SetupSingleton(false); SetupCanUpdate(true); var content = CreateContent(Status.Draft, false); @@ -145,8 +160,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanChangeStatus_should_throw_exception_if_publishing_without_pending_changes() { - SetupSingleton(false); - var content = CreateContent(Status.Published, false); var command = new ChangeContentStatus { Status = Status.Published }; @@ -179,8 +192,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanChangeStatus_should_throw_exception_if_due_date_in_past() { - SetupSingleton(false); - var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast, User = user }; @@ -194,8 +205,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanChangeStatus_should_throw_exception_if_status_flow_not_valid() { - SetupSingleton(false); - var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published, User = user }; @@ -209,8 +218,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public async Task CanChangeStatus_should_not_throw_exception_if_status_flow_valid() { - SetupSingleton(false); - var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published, User = user }; @@ -231,8 +238,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public void CanDiscardChanges_should_not_throw_exception_if_pending() { - SetupSingleton(false); - var command = new DiscardChanges(); GuardContent.CanDiscardChanges(true, command); @@ -251,8 +256,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard [Fact] public void CanDelete_should_not_throw_exception() { - SetupSingleton(false); - var command = new DeleteContent(); GuardContent.CanDelete(schema, command); @@ -264,6 +267,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard .Returns(canUpdate); } + private void SetupCanCreatePublish(bool canCreate) + { + A.CallTo(() => contentWorkflow.CanPublishOnCreateAsync(schema, A.Ignored, user)) + .Returns(canCreate); + } + private void SetupSingleton(bool isSingleton) { A.CallTo(() => schema.SchemaDef) From 8fbc5f4f119dfdaf3766780c42c6e03e7d7a4085 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 3 Jul 2019 21:38:25 +0200 Subject: [PATCH 13/18] Deferred conversion. --- .../Contents/ContentQueryService.cs | 3 - src/Squidex.Web/ApiPermissionAttribute.cs | 1 - src/Squidex.Web/Deferred.cs | 42 +++++++++++++ src/Squidex.Web/ETagExtensions.cs | 61 +++++++++++++------ src/Squidex.Web/IGenerateEtag.cs | 18 ------ .../TypedJsonInheritanceConverter.cs} | 29 +++++---- .../Pipeline/DeferredActionFilter.cs | 26 ++++++++ src/Squidex.Web/{ => Pipeline}/ETagFilter.cs | 2 +- src/Squidex.Web/{ => Pipeline}/ETagOptions.cs | 2 +- .../Controllers/Apps/AppClientsController.cs | 7 ++- .../Apps/AppContributorsController.cs | 31 +++++----- .../Apps/AppLanguagesController.cs | 7 ++- .../Controllers/Apps/AppPatternsController.cs | 7 ++- .../Controllers/Apps/AppRolesController.cs | 16 +++-- .../Apps/AppWorkflowsController.cs | 7 ++- .../Api/Controllers/Apps/AppsController.cs | 7 ++- .../Api/Controllers/Apps/Models/AppDto.cs | 2 +- .../Controllers/Apps/Models/WorkflowsDto.cs | 1 - .../Controllers/Assets/AssetsController.cs | 37 ++++++----- .../Api/Controllers/Assets/Models/AssetDto.cs | 2 +- .../Controllers/Assets/Models/AssetsDto.cs | 10 --- .../Comments/CommentsController.cs | 8 ++- .../Contents/ContentsController.cs | 37 ++++++----- .../Controllers/Contents/Models/ContentDto.cs | 2 +- .../Contents/Models/ContentsDto.cs | 10 --- .../Languages/LanguagesController.cs | 5 +- .../Controllers/Plans/AppPlansController.cs | 7 ++- .../Rules/Models/RuleActionConverter.cs | 4 +- .../Api/Controllers/Rules/Models/RuleDto.cs | 2 +- .../Rules/Models/RuleTriggerDto.cs | 4 +- .../Api/Controllers/Rules/Models/RulesDto.cs | 5 -- .../Api/Controllers/Rules/RulesController.cs | 14 +++-- .../Schemas/Models/FieldPropertiesDto.cs | 4 +- .../Controllers/Schemas/Models/SchemaDto.cs | 2 +- .../Controllers/Schemas/Models/SchemasDto.cs | 5 -- .../Controllers/Schemas/SchemasController.cs | 14 +++-- .../Config/Domain/SerializationInitializer.cs | 1 + src/Squidex/Config/Web/WebServices.cs | 1 + 38 files changed, 271 insertions(+), 172 deletions(-) create mode 100644 src/Squidex.Web/Deferred.cs delete mode 100644 src/Squidex.Web/IGenerateEtag.cs rename src/Squidex.Web/{MyJsonInheritanceConverter.cs => Json/TypedJsonInheritanceConverter.cs} (79%) create mode 100644 src/Squidex.Web/Pipeline/DeferredActionFilter.cs rename src/Squidex.Web/{ => Pipeline}/ETagFilter.cs (98%) rename src/Squidex.Web/{ => Pipeline}/ETagOptions.cs (93%) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 306b49309..e88a08f67 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Microsoft.OData; @@ -23,9 +22,7 @@ using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries.OData; using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Security; using Squidex.Shared; -using Squidex.Shared.Identity; #pragma warning disable RECS0147 diff --git a/src/Squidex.Web/ApiPermissionAttribute.cs b/src/Squidex.Web/ApiPermissionAttribute.cs index e93e1fed2..f655b2c6f 100644 --- a/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/src/Squidex.Web/ApiPermissionAttribute.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Tasks; -using Squidex.Shared.Identity; namespace Squidex.Web { diff --git a/src/Squidex.Web/Deferred.cs b/src/Squidex.Web/Deferred.cs new file mode 100644 index 000000000..717182f49 --- /dev/null +++ b/src/Squidex.Web/Deferred.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Web +{ + public struct Deferred + { + private readonly Lazy> value; + + public Task Value + { + get { return value.Value; } + } + + private Deferred(Func> value) + { + this.value = new Lazy>(value); + } + + public static Deferred Response(Func factory) + { + Guard.NotNull(factory, nameof(factory)); + + return new Deferred(() => Task.FromResult(factory())); + } + + public static Deferred AsyncResponse(Func> factory) + { + Guard.NotNull(factory, nameof(factory)); + + return new Deferred(async () => await factory()); + } + } +} diff --git a/src/Squidex.Web/ETagExtensions.cs b/src/Squidex.Web/ETagExtensions.cs index 5ee961a9d..034a9b958 100644 --- a/src/Squidex.Web/ETagExtensions.cs +++ b/src/Squidex.Web/ETagExtensions.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Text; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; @@ -17,40 +18,54 @@ namespace Squidex.Web { private static readonly int GuidLength = Guid.Empty.ToString().Length; - public static string ToManyEtag(this IReadOnlyList items, long total = 0) where T : IGenerateETag + public static string ToEtag(this IReadOnlyList items, IEntityWithVersion app = null) where T : IEntity, IEntityWithVersion { using (Profiler.Trace("CalculateEtag")) { - var unhashed = Unhashed(items, total); + var unhashed = Unhashed(items, 0, app); return unhashed.Sha256Base64(); } } - private static string Unhashed(IReadOnlyList items, long total) where T : IGenerateETag + public static string ToEtag(this IResultList items, IEntityWithVersion app = null) where T : IEntity, IEntityWithVersion { - var sb = new StringBuilder((items.Count * (GuidLength + 4)) + 10); + using (Profiler.Trace("CalculateEtag")) + { + var unhashed = Unhashed(items, items.Total, app); + + return unhashed.Sha256Base64(); + } + } + + private static string Unhashed(IReadOnlyList items, long total, IEntityWithVersion app) where T : IEntity, IEntityWithVersion + { + var sb = new StringBuilder((items.Count * (GuidLength + 8)) + 10); + + for (var i = 0; i < items.Count; i++) + { + sb.Append(";"); + sb.Append(items[i].ToEtag()); + } - sb.Append(total); sb.Append("_"); + sb.Append(total); - if (items.Count > 0) + if (app != null) { - sb.Append(items[0].Id.ToString()); - sb.Append(items[0].Version); - - for (var i = 1; i < items.Count; i++) - { - sb.Append(";"); - sb.Append(items[i].Id.ToString()); - sb.Append(items[i].Version); - } + sb.Append("_"); + sb.Append(app.Version); } - return sb.ToString().Sha256Base64(); + return sb.ToString(); + } + + public static string ToSurrogateKey(this T item) where T : IEntity + { + return item.Id.ToString(); } - public static string ToSurrogateKeys(this IReadOnlyList items) where T : IGenerateETag + public static string ToSurrogateKeys(this IReadOnlyList items) where T : IEntity { if (items.Count == 0) { @@ -70,9 +85,17 @@ namespace Squidex.Web return sb.ToString(); } - public static string ToEtag(this T item) where T : IGenerateETag + public static string ToEtag(this T item, IEntityWithVersion app = null) where T : IEntity, IEntityWithVersion { - return item.Version.ToString(); + var result = $"{item.Id};{item.Version}"; + + if (app != null) + { + result += ";"; + result += app.Version; + } + + return result; } } } diff --git a/src/Squidex.Web/IGenerateEtag.cs b/src/Squidex.Web/IGenerateEtag.cs deleted file mode 100644 index 6986f1acc..000000000 --- a/src/Squidex.Web/IGenerateEtag.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Web -{ - public interface IGenerateETag - { - Guid Id { get; } - - long Version { get; } - } -} diff --git a/src/Squidex.Web/MyJsonInheritanceConverter.cs b/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs similarity index 79% rename from src/Squidex.Web/MyJsonInheritanceConverter.cs rename to src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs index ff3a1854e..21505bf6e 100644 --- a/src/Squidex.Web/MyJsonInheritanceConverter.cs +++ b/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs @@ -16,17 +16,16 @@ using Squidex.Infrastructure; #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Web +namespace Squidex.Web.Json { - public class MyJsonInheritanceConverter : JsonInheritanceConverter + public class TypedJsonInheritanceConverter : JsonInheritanceConverter { - private static readonly Dictionary DefaultMapping = new Dictionary(); - private readonly IReadOnlyDictionary maping; - - static MyJsonInheritanceConverter() + private static readonly Lazy> DefaultMapping = new Lazy>(() => { var baseName = typeof(T).Name; + var result = new Dictionary(); + void AddType(Type type) { var discriminator = type.Name; @@ -36,7 +35,7 @@ namespace Squidex.Web discriminator = discriminator.Substring(0, discriminator.Length - baseName.Length); } - DefaultMapping[discriminator] = type; + result[discriminator] = type; } foreach (var attribute in typeof(T).GetCustomAttributes()) @@ -66,17 +65,23 @@ namespace Squidex.Web } } } - } - public MyJsonInheritanceConverter(string discriminator) - : this(discriminator, DefaultMapping) + return result; + }); + + private readonly IReadOnlyDictionary maping; + + public TypedJsonInheritanceConverter(string discriminator) + : this(discriminator, DefaultMapping.Value) { } - public MyJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) + public TypedJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) : base(typeof(T), discriminator) { - maping = mapping ?? DefaultMapping; + Guard.NotNull(maping, nameof(maping)); + + maping = mapping; } protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) diff --git a/src/Squidex.Web/Pipeline/DeferredActionFilter.cs b/src/Squidex.Web/Pipeline/DeferredActionFilter.cs new file mode 100644 index 000000000..9e5b3b4f2 --- /dev/null +++ b/src/Squidex.Web/Pipeline/DeferredActionFilter.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Squidex.Web.Pipeline +{ + public sealed class DeferredActionFilter : IAsyncActionFilter + { + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + await next(); + + if (context.Result is ObjectResult objectResult && objectResult.Value is Deferred deferred) + { + objectResult.Value = await deferred.Value; + } + } + } +} diff --git a/src/Squidex.Web/ETagFilter.cs b/src/Squidex.Web/Pipeline/ETagFilter.cs similarity index 98% rename from src/Squidex.Web/ETagFilter.cs rename to src/Squidex.Web/Pipeline/ETagFilter.cs index b76772ad3..4dd680374 100644 --- a/src/Squidex.Web/ETagFilter.cs +++ b/src/Squidex.Web/Pipeline/ETagFilter.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; -namespace Squidex.Web +namespace Squidex.Web.Pipeline { public sealed class ETagFilter : IAsyncActionFilter { diff --git a/src/Squidex.Web/ETagOptions.cs b/src/Squidex.Web/Pipeline/ETagOptions.cs similarity index 93% rename from src/Squidex.Web/ETagOptions.cs rename to src/Squidex.Web/Pipeline/ETagOptions.cs index 8e832dbca..d6715b233 100644 --- a/src/Squidex.Web/ETagOptions.cs +++ b/src/Squidex.Web/Pipeline/ETagOptions.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Web +namespace Squidex.Web.Pipeline { public sealed class ETagOptions { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index b89697d26..92e749028 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -46,9 +46,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public IActionResult GetClients(string app) { - var response = ClientsDto.FromApp(App, this); + var response = Deferred.Response(() => + { + return ClientsDto.FromApp(App, this); + }); - Response.Headers[HeaderNames.ETag] = App.Version.ToString(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 021afa123..8d1534b74 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -48,9 +48,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public IActionResult GetContributors(string app) { - var response = ContributorsDto.FromApp(App, appPlansProvider, this, false); + var response = Deferred.Response(() => + { + return ContributorsDto.FromApp(App, appPlansProvider, this, false); + }); - Response.Headers[HeaderNames.ETag] = App.Version.ToString(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } @@ -73,18 +76,8 @@ namespace Squidex.Areas.Api.Controllers.Apps public async Task PostContributor(string app, [FromBody] AssignContributorDto request) { var command = request.ToCommand(); - var context = await CommandBus.PublishAsync(command); - var response = (ContributorsDto)null; - - if (context.PlainResult is IAppEntity newApp) - { - response = ContributorsDto.FromApp(newApp, appPlansProvider, this, false); - } - else if (context.PlainResult is InvitedResult invited) - { - response = ContributorsDto.FromApp(invited.App, appPlansProvider, this, true); - } + var response = await InvokeCommandAsync(command); return CreatedAtAction(nameof(GetContributors), new { app }, response); } @@ -117,10 +110,14 @@ namespace Squidex.Areas.Api.Controllers.Apps { var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = ContributorsDto.FromApp(result, appPlansProvider, this, false); - - return response; + if (context.PlainResult is InvitedResult invited) + { + return ContributorsDto.FromApp(invited.App, appPlansProvider, this, true); + } + else + { + return ContributorsDto.FromApp(context.Result(), appPlansProvider, this, false); + } } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 03064da7b..43498aa82 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -45,9 +45,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public IActionResult GetLanguages(string app) { - var response = AppLanguagesDto.FromApp(App, this); + var response = Deferred.Response(() => + { + return AppLanguagesDto.FromApp(App, this); + }); - Response.Headers[HeaderNames.ETag] = App.Version.ToString(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs index 022a20cab..74f9fc136 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs @@ -47,9 +47,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public IActionResult GetPatterns(string app) { - var response = PatternsDto.FromApp(App, this); + var response = Deferred.Response(() => + { + return PatternsDto.FromApp(App, this); + }); - Response.Headers[HeaderNames.ETag] = App.Version.ToString(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index d51daaf2d..ac427a567 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -47,9 +47,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public IActionResult GetRoles(string app) { - var response = RolesDto.FromApp(App, this); + var response = Deferred.Response(() => + { + return RolesDto.FromApp(App, this); + }); - Response.Headers[HeaderNames.ETag] = App.Version.ToString(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } @@ -67,11 +70,14 @@ namespace Squidex.Areas.Api.Controllers.Apps [ProducesResponseType(typeof(string[]), 200)] [ApiPermission(Permissions.AppRolesRead)] [ApiCosts(0)] - public async Task GetPermissions(string app) + public IActionResult GetPermissions(string app) { - var response = await permissionsProvider.GetPermissionsAsync(App); + var response = Deferred.AsyncResponse(() => + { + return permissionsProvider.GetPermissionsAsync(App); + }); - Response.Headers[HeaderNames.ETag] = string.Join(";", response).Sha256Base64(); + Response.Headers[HeaderNames.ETag] = string.Concat(response).Sha256Base64(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index 22e003e75..5377ea7d7 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -44,9 +44,12 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public IActionResult GetWorkflows(string app) { - var response = WorkflowsDto.FromApp(App, this); + var response = Deferred.Response(() => + { + return WorkflowsDto.FromApp(App, this); + }); - Response.Headers[HeaderNames.ETag] = App.Version.ToString(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 1d2b9c26a..6529dd4e5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -62,9 +62,12 @@ namespace Squidex.Areas.Api.Controllers.Apps var apps = await appProvider.GetUserApps(userOrClientId, userPermissions); - var response = apps.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)); + var response = Deferred.Response(() => + { + return apps.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)); + }); - Response.Headers[HeaderNames.ETag] = response.ToManyEtag(); + Response.Headers[HeaderNames.ETag] = apps.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index ffa7b22fa..1eee4cdd5 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -25,7 +25,7 @@ using AllPermissions = Squidex.Shared.Permissions; namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class AppDto : Resource, IGenerateETag + public sealed class AppDto : Resource { /// /// The name of the app. diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs index b58be115c..3f1e1ecd7 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowsDto.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.ComponentModel.DataAnnotations; using System.Linq; using Squidex.Domain.Apps.Entities.Apps; diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 38faed8f3..84f423dda 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -103,14 +103,17 @@ namespace Squidex.Areas.Api.Controllers.Assets { var assets = await assetQuery.QueryAsync(Context, Q.Empty.WithODataQuery(Request.QueryString.ToString()).WithIds(ids)); - var response = AssetsDto.FromAssets(assets, this, app); + var response = Deferred.Response(() => + { + return AssetsDto.FromAssets(assets, this, app); + }); - if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) + if (controllerOptions.Value.EnableSurrogateKeys && assets.Count <= controllerOptions.Value.MaxItemsForSurrogateKeys) { - Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys(); + Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys(); } - Response.Headers[HeaderNames.ETag] = response.ToEtag(); + Response.Headers[HeaderNames.ETag] = assets.ToEtag(); return Ok(response); } @@ -138,14 +141,17 @@ namespace Squidex.Areas.Api.Controllers.Assets return NotFound(); } - var response = AssetDto.FromAsset(asset, this, app); + var response = Deferred.Response(() => + { + return AssetDto.FromAsset(asset, this, app); + }); if (controllerOptions.Value.EnableSurrogateKeys) { - Response.Headers["Surrogate-Key"] = asset.Id.ToString(); + Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey(); } - Response.Headers[HeaderNames.ETag] = asset.Version.ToString(); + Response.Headers[HeaderNames.ETag] = asset.ToEtag(); return Ok(response); } @@ -175,10 +181,7 @@ namespace Squidex.Areas.Api.Controllers.Assets var command = new CreateAsset { File = assetFile }; - var context = await CommandBus.PublishAsync(command); - - var result = context.Result(); - var response = AssetDto.FromAsset(result.Asset, this, app, result.IsDuplicate); + var response = await InvokeCommandAsync(app, command); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); } @@ -263,10 +266,14 @@ namespace Squidex.Areas.Api.Controllers.Assets { var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = AssetDto.FromAsset(result, this, app); - - return response; + if (context.PlainResult is AssetCreatedResult created) + { + return AssetDto.FromAsset(created.Asset, this, app, created.IsDuplicate); + } + else + { + return AssetDto.FromAsset(context.Result(), this, app); + } } private async Task CheckAssetFileAsync(IReadOnlyList file) diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index 5c996cf0d..4ba2cb68f 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -18,7 +18,7 @@ using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Assets.Models { - public sealed class AssetDto : Resource, IGenerateETag + public sealed class AssetDto : Resource { /// /// The id of the asset. diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs index efd81147b..fbaa6dd46 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs @@ -27,16 +27,6 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Required] public AssetDto[] Items { get; set; } - public string ToEtag() - { - return Items.ToManyEtag(Total); - } - - public string ToSurrogateKeys() - { - return Items.ToSurrogateKeys(); - } - public static AssetsDto FromAssets(IResultList assets, ApiController controller, string app) { var response = new AssetsDto diff --git a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 6125003f5..735bd640a 100644 --- a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -55,9 +55,13 @@ namespace Squidex.Areas.Api.Controllers.Comments public async Task GetComments(string app, Guid commentsId, [FromQuery] long version = EtagVersion.Any) { var result = await grainFactory.GetGrain(commentsId).GetCommentsAsync(version); - var response = CommentsDto.FromResult(result); - Response.Headers[HeaderNames.ETag] = response.Version.ToString(); + var response = Deferred.Response(() => + { + return CommentsDto.FromResult(result); + }); + + Response.Headers[HeaderNames.ETag] = result.Version.ToString(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 095be8d07..8eb70829d 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -126,14 +127,17 @@ namespace Squidex.Areas.Api.Controllers.Contents { var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids).Ids); - var response = await ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); + var response = Deferred.AsyncResponse(() => + { + return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); + }); - if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) + if (ShouldProvideSurrogateKeys(contents)) { - Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys(); + Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); } - Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; + Response.Headers[HeaderNames.ETag] = contents.ToEtag(App); return Ok(response); } @@ -160,16 +164,19 @@ namespace Squidex.Areas.Api.Controllers.Contents { var contents = await contentQuery.QueryAsync(Context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); - var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); + var response = Deferred.AsyncResponse(async () => + { + var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name); - var response = await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); + return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); + }); - if (ShouldProvideSurrogateKeys(response)) + if (ShouldProvideSurrogateKeys(contents)) { - Response.Headers["Surrogate-Key"] = response.ToSurrogateKeys(); + Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); } - Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; + Response.Headers[HeaderNames.ETag] = contents.ToEtag(App); return Ok(response); } @@ -200,10 +207,10 @@ namespace Squidex.Areas.Api.Controllers.Contents if (controllerOptions.Value.EnableSurrogateKeys) { - Response.Headers["Surrogate-Key"] = content.Id.ToString(); + Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); } - Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; + Response.Headers[HeaderNames.ETag] = content.ToEtag(App); return Ok(response); } @@ -235,10 +242,10 @@ namespace Squidex.Areas.Api.Controllers.Contents if (controllerOptions.Value.EnableSurrogateKeys) { - Response.Headers["Surrogate-Key"] = content.Id.ToString(); + Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); } - Response.Headers[HeaderNames.ETag] = $"{response.ToEtag()}_{App.Version}"; + Response.Headers[HeaderNames.ETag] = content.ToEtag(App); return Ok(response.Data); } @@ -447,9 +454,9 @@ namespace Squidex.Areas.Api.Controllers.Contents return response; } - private bool ShouldProvideSurrogateKeys(ContentsDto response) + private bool ShouldProvideSurrogateKeys(IReadOnlyList response) { - return controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys; + return controllerOptions.Value.EnableSurrogateKeys && response.Count <= controllerOptions.Value.MaxItemsForSurrogateKeys; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 0725239e4..513abdbea 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -19,7 +19,7 @@ using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents.Models { - public sealed class ContentDto : Resource, IGenerateETag + public sealed class ContentDto : Resource { /// /// The if of the content item. diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 749e662d1..7e0d14ccd 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -37,16 +37,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models [Required] public StatusInfoDto[] Statuses { get; set; } - public string ToEtag() - { - return Items.ToManyEtag(Total); - } - - public string ToSurrogateKeys() - { - return Items.ToSurrogateKeys(); - } - public static async Task FromContentsAsync(IResultList contents, Context context, ApiController controller, ISchemaEntity schema, IContentWorkflow contentWorkflow) { diff --git a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs index e87b8c68c..62726a9bb 100644 --- a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs @@ -40,7 +40,10 @@ namespace Squidex.Areas.Api.Controllers.Languages [ApiPermission] public IActionResult GetLanguages() { - var response = Language.AllLanguages.Select(LanguageDto.FromLanguage).ToArray(); + var response = Deferred.Response(() => + { + return Language.AllLanguages.Select(LanguageDto.FromLanguage).ToArray(); + }); Response.Headers[HeaderNames.ETag] = "1"; diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index a14f220a3..99de9745b 100644 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -51,9 +51,12 @@ namespace Squidex.Areas.Api.Controllers.Plans { var hasPortal = appPlansBillingManager.HasPortal; - var response = AppPlansDto.FromApp(App, appPlansProvider, hasPortal); + var response = Deferred.Response(() => + { + return AppPlansDto.FromApp(App, appPlansProvider, hasPortal); + }); - Response.Headers[HeaderNames.ETag] = App.Version.ToString(); + Response.Headers[HeaderNames.ETag] = App.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs index 8f1da7b9e..b108b7be4 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs @@ -8,11 +8,11 @@ using System; using System.Collections.Generic; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Web; +using Squidex.Web.Json; namespace Squidex.Areas.Api.Controllers.Rules.Models { - public sealed class RuleActionConverter : MyJsonInheritanceConverter + public sealed class RuleActionConverter : TypedJsonInheritanceConverter { public static IReadOnlyDictionary Mapping { get; set; } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs index 39ec2ff60..e625f2f35 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs @@ -19,7 +19,7 @@ using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models { - public sealed class RuleDto : Resource, IGenerateETag + public sealed class RuleDto : Resource { /// /// The id of the rule. diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs index 9ac6cd699..4392bdfba 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs @@ -10,11 +10,11 @@ using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Web; +using Squidex.Web.Json; namespace Squidex.Areas.Api.Controllers.Rules.Models { - [JsonConverter(typeof(MyJsonInheritanceConverter), "triggerType")] + [JsonConverter(typeof(TypedJsonInheritanceConverter), "triggerType")] [KnownType(nameof(Subtypes))] public abstract class RuleTriggerDto { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs index c13c163fb..7379e019a 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs @@ -22,11 +22,6 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models [Required] public RuleDto[] Items { get; set; } - public string GenerateEtag() - { - return Items.ToManyEtag(0); - } - public static RulesDto FromRules(IEnumerable items, ApiController controller, string app) { var result = new RulesDto diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index c6213c93b..bd5ed4fbd 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -58,9 +58,12 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(0)] public IActionResult GetActions() { - var etag = string.Join(";", ruleRegistry.Actions.Select(x => x.Key)).Sha256Base64(); + var etag = string.Concat(ruleRegistry.Actions.Select(x => x.Key)).Sha256Base64(); - var response = ruleRegistry.Actions.ToDictionary(x => x.Key, x => RuleElementDto.FromDefinition(x.Value)); + var response = Deferred.Response(() => + { + return ruleRegistry.Actions.ToDictionary(x => x.Key, x => RuleElementDto.FromDefinition(x.Value)); + }); Response.Headers[HeaderNames.ETag] = etag; @@ -84,9 +87,12 @@ namespace Squidex.Areas.Api.Controllers.Rules { var rules = await appProvider.GetRulesAsync(AppId); - var response = RulesDto.FromRules(rules, this, app); + var response = Deferred.Response(() => + { + return RulesDto.FromRules(rules, this, app); + }); - Response.Headers[HeaderNames.ETag] = response.GenerateEtag(); + Response.Headers[HeaderNames.ETag] = rules.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index 02376143b..b09c7d002 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -11,11 +11,11 @@ using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Web; +using Squidex.Web.Json; namespace Squidex.Areas.Api.Controllers.Schemas.Models { - [JsonConverter(typeof(MyJsonInheritanceConverter), "fieldType")] + [JsonConverter(typeof(TypedJsonInheritanceConverter), "fieldType")] [KnownType(nameof(Subtypes))] public abstract class FieldPropertiesDto { diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 35fafd420..4b349216c 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -17,7 +17,7 @@ using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Schemas.Models { - public class SchemaDto : Resource, IGenerateETag + public class SchemaDto : Resource { /// /// The id of the schema. diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs index 596c80d07..ebdaa95ab 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemasDto.cs @@ -21,11 +21,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public SchemaDto[] Items { get; set; } - public string ToEtag() - { - return Items.ToManyEtag(); - } - public static SchemasDto FromSchemas(IList schemas, ApiController controller, string app) { var result = new SchemasDto diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 67e807a45..192c8fc87 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -50,9 +50,12 @@ namespace Squidex.Areas.Api.Controllers.Schemas { var schemas = await appProvider.GetSchemasAsync(AppId); - var response = SchemasDto.FromSchemas(schemas, this, app); + var response = Deferred.Response(() => + { + return SchemasDto.FromSchemas(schemas, this, app); + }); - Response.Headers[HeaderNames.ETag] = response.ToEtag(); + Response.Headers[HeaderNames.ETag] = schemas.ToEtag(); return Ok(response); } @@ -89,9 +92,12 @@ namespace Squidex.Areas.Api.Controllers.Schemas return NotFound(); } - var response = SchemaDetailsDto.FromSchemaWithDetails(schema, this, app); + var response = Deferred.Response(() => + { + return SchemaDetailsDto.FromSchemaWithDetails(schema, this, app); + }); - Response.Headers[HeaderNames.ETag] = schema.Version.ToString(); + Response.Headers[HeaderNames.ETag] = schema.ToEtag(); return Ok(response); } diff --git a/src/Squidex/Config/Domain/SerializationInitializer.cs b/src/Squidex/Config/Domain/SerializationInitializer.cs index 0254b318b..9d1bb868e 100644 --- a/src/Squidex/Config/Domain/SerializationInitializer.cs +++ b/src/Squidex/Config/Domain/SerializationInitializer.cs @@ -29,6 +29,7 @@ namespace Squidex.Config.Domain { this.jsonNetSerializer = jsonNetSerializer; this.jsonSerializer = jsonSerializer; + this.ruleRegistry = ruleRegistry; } diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index 580b214ad..e4e867d85 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -51,6 +51,7 @@ namespace Squidex.Config.Web services.AddMvc(options => { options.Filters.Add(); + options.Filters.Add(); options.Filters.Add(); options.Filters.Add(); }) From 5e31053554a897bfee2fc40223cffe71e3e20b49 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 3 Jul 2019 22:07:19 +0200 Subject: [PATCH 14/18] Stupid method removed. --- .../Schemas/Json/JsonFieldModel.cs | 2 +- .../Schemas/Json/JsonSchemaModel.cs | 10 +++++----- .../Contents/GraphQL/CachingGraphQLService.cs | 2 +- .../CollectionExtensions.cs | 16 +--------------- .../States/Persistence{TSnapshot,TKey}.cs | 2 +- src/Squidex.Web/ApiExceptionFilterAttribute.cs | 4 ++-- .../Areas/Api/Controllers/Apps/AppsController.cs | 3 ++- .../Controllers/Apps/Models/ContributorsDto.cs | 5 ++--- src/Squidex/WebStartup.cs | 1 + 9 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs index 729e6ab0c..3a7a90900 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json if (Properties is ArrayFieldProperties arrayProperties) { - var nested = Children?.ToArray(n => n.ToNestedField()) ?? Array.Empty(); + var nested = Children?.Map(n => n.ToNestedField()) ?? Array.Empty(); return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this); } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs index 83196b881..54c31c88f 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json SimpleMapper.Map(schema, this); Fields = - schema.Fields.ToArray(x => + schema.Fields.Select(x => new JsonFieldModel { Id = x.Id, @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json IsDisabled = x.IsDisabled, Partitioning = x.Partitioning.Key, Properties = x.RawProperties - }); + }).ToArray(); PreviewUrls = schema.PreviewUrls.ToDictionary(x => x.Key, x => x.Value); } @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json { if (field is ArrayField arrayField) { - return arrayField.Fields.ToArray(x => + return arrayField.Fields.Select(x => new JsonNestedFieldModel { Id = x.Id, @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json IsLocked = x.IsLocked, IsDisabled = x.IsDisabled, Properties = x.RawProperties - }); + }).ToArray(); } return null; @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json public Schema ToSchema() { - var fields = Fields.ToArray(f => f.ToField()) ?? Array.Empty(); + var fields = Fields.Map(f => f.ToField()) ?? Array.Empty(); var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 2d3d5e353..1fa48486c 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); - return (result.Any(x => x.HasError), result.ToArray(x => x.Response)); + return (result.Any(x => x.HasError), result.Map(x => x.Response)); } public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query) diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index a8f44e515..248197dc1 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -53,7 +53,7 @@ namespace Squidex.Infrastructure return source.Concat(Enumerable.Repeat(value, 1)); } - public static TResult[] ToArray(this T[] value, Func convert) + public static TResult[] Map(this T[] value, Func convert) { var result = new TResult[value.Length]; @@ -65,20 +65,6 @@ namespace Squidex.Infrastructure return result; } - public static TResult[] ToArray(this IReadOnlyCollection value, Func convert) - { - var result = new TResult[value.Count]; - var i = 0; - - foreach (var v in value) - { - result[i] = convert(v); - i++; - } - - return result; - } - public static int SequentialHashCode(this IEnumerable collection) { return collection.SequentialHashCode(EqualityComparer.Default); diff --git a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs index 48a30f22c..9d684fc59 100644 --- a/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs +++ b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs @@ -190,7 +190,7 @@ namespace Squidex.Infrastructure.States private EventData[] GetEventData(Envelope[] events, Guid commitId) { - return events.ToArray(x => eventDataFormatter.ToEventData(x, commitId, true)); + return events.Map(x => eventDataFormatter.ToEventData(x, commitId, true)); } private string GetStreamName() diff --git a/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/src/Squidex.Web/ApiExceptionFilterAttribute.cs index 3e195c0be..ce1b22b55 100644 --- a/src/Squidex.Web/ApiExceptionFilterAttribute.cs +++ b/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -93,7 +93,7 @@ namespace Squidex.Web private static string[] ToDetails(ValidationException ex) { - return ex.Errors?.ToArray(e => + return ex.Errors?.Select(e => { if (e.PropertyNames?.Any() == true) { @@ -103,7 +103,7 @@ namespace Squidex.Web { return e.Message; } - }); + }).ToArray(); } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 6529dd4e5..8a1b950df 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; @@ -64,7 +65,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var response = Deferred.Response(() => { - return apps.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)); + return apps.Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray(); }); Response.Headers[HeaderNames.ETag] = apps.ToEtag(); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs index 9d8baf5e3..b9e264241 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using System.Linq; using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Services; @@ -36,11 +37,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public static ContributorsDto FromApp(IAppEntity app, IAppPlansProvider plans, ApiController controller, bool isInvited) { - var contributors = app.Contributors.ToArray(x => ContributorDto.FromIdAndRole(x.Key, x.Value, controller, app.Name)); - var result = new ContributorsDto { - Items = contributors, + Items = app.Contributors.Select(x => ContributorDto.FromIdAndRole(x.Key, x.Value, controller, app.Name)).ToArray(), }; if (isInvited) diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 6bc48e67d..741aa75b0 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -35,6 +35,7 @@ using Squidex.Infrastructure.Translations; using Squidex.Pipeline.Plugins; using Squidex.Pipeline.Robots; using Squidex.Web; +using Squidex.Web.Pipeline; namespace Squidex { From 2ea39cf7a5e180e6b84514db4f76c3ac3d3c0924 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 4 Jul 2019 14:32:00 +0200 Subject: [PATCH 15/18] Fixes for performance improvement. --- .../Contents/ContentGrain.cs | 22 +++-- .../Contents/Guards/GuardContent.cs | 33 ++++--- .../Json/TypedJsonInheritanceConverter.cs | 4 +- .../Pipeline/DeferredActionFilter.cs | 4 +- .../pages/content/content-page.component.ts | 4 +- .../workflows/workflow-step.component.html | 2 +- .../angular/modals/modal-view.directive.ts | 6 +- .../app/shared/services/contents.service.ts | 2 + .../shared/services/workflows.service.spec.ts | 10 -- .../app/shared/services/workflows.service.ts | 92 +++++-------------- .../pages/internal/apps-menu.component.html | 2 +- .../pages/internal/apps-menu.component.ts | 5 - .../Contents/Guard/GuardContentTests.cs | 24 ++--- 13 files changed, 82 insertions(+), 128 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index ce07554a7..60f425b5e 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -93,17 +93,21 @@ namespace Squidex.Domain.Apps.Entities.Contents case UpdateContent updateContent: return UpdateReturnAsync(updateContent, async c => { - await GuardContent.CanUpdate(Snapshot, contentWorkflow, c); + var isProposal = c.AsDraft && Snapshot.Status == Status.Published; - return await UpdateAsync(c, x => c.Data, false); + await GuardContent.CanUpdate(Snapshot, contentWorkflow, c, isProposal); + + return await UpdateAsync(c, x => c.Data, false, isProposal); }); case PatchContent patchContent: return UpdateReturnAsync(patchContent, async c => { - await GuardContent.CanPatch(Snapshot, contentWorkflow, c); + var isProposal = c.AsDraft && Snapshot.Status == Status.Published; + + await GuardContent.CanPatch(Snapshot, contentWorkflow, c, isProposal); - return await UpdateAsync(c, c.Data.MergeInto, true); + return await UpdateAsync(c, c.Data.MergeInto, true, isProposal); }); case ChangeContentStatus changeContentStatus: @@ -111,9 +115,11 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { + var isChangeConfirm = Snapshot.IsPending && Snapshot.Status == Status.Published && c.Status == Status.Published; + var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to change content."); - await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c); + await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c, isChangeConfirm); if (c.DueTime.HasValue) { @@ -121,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } else { - if (Snapshot.IsPending && Snapshot.Status == Status.Published && c.Status == Status.Published) + if (isChangeConfirm) { ConfirmChanges(c); } @@ -190,10 +196,8 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial) + private async Task UpdateAsync(ContentUpdateCommand command, Func newDataFunc, bool partial, bool isProposal) { - var isProposal = command.AsDraft && Snapshot.Status == Status.Published; - var currentData = isProposal ? Snapshot.DataDraft : diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 494237996..9feaadbd4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards } } - public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command) + public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command, bool isProposal) { Guard.NotNull(command, nameof(command)); @@ -45,10 +45,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards ValidateData(command, e); }); - await ValidateCanUpdate(content, contentWorkflow); + if (!isProposal) + { + await ValidateCanUpdate(content, contentWorkflow); + } } - public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command) + public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command, bool isProposal) { Guard.NotNull(command, nameof(command)); @@ -57,7 +60,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards ValidateData(command, e); }); - await ValidateCanUpdate(content, contentWorkflow); + if (!isProposal) + { + await ValidateCanUpdate(content, contentWorkflow); + } } public static void CanDiscardChanges(bool isPending, DiscardChanges command) @@ -70,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards } } - public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command) + public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command, bool isChangeConfirm) { Guard.NotNull(command, nameof(command)); @@ -81,20 +87,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards return Validate.It(() => "Cannot change status.", async e => { - if (!await contentWorkflow.CanMoveToAsync(content, command.Status, command.User)) + if (isChangeConfirm) { - if (content.Status == command.Status && content.Status == Status.Published) + if (!content.IsPending) { - if (!content.IsPending) - { - e("Content has no changes to publish.", nameof(command.Status)); - } - } - else - { - e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); + e("Content has no changes to publish.", nameof(command.Status)); } } + else if (!await contentWorkflow.CanMoveToAsync(content, command.Status, command.User)) + { + e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); + } if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant()) { diff --git a/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs b/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs index 21505bf6e..f87d632fd 100644 --- a/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs +++ b/src/Squidex.Web/Json/TypedJsonInheritanceConverter.cs @@ -79,9 +79,7 @@ namespace Squidex.Web.Json public TypedJsonInheritanceConverter(string discriminator, IReadOnlyDictionary mapping) : base(typeof(T), discriminator) { - Guard.NotNull(maping, nameof(maping)); - - maping = mapping; + maping = mapping ?? DefaultMapping.Value; } protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue) diff --git a/src/Squidex.Web/Pipeline/DeferredActionFilter.cs b/src/Squidex.Web/Pipeline/DeferredActionFilter.cs index 9e5b3b4f2..e57a62981 100644 --- a/src/Squidex.Web/Pipeline/DeferredActionFilter.cs +++ b/src/Squidex.Web/Pipeline/DeferredActionFilter.cs @@ -15,9 +15,9 @@ namespace Squidex.Web.Pipeline { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - await next(); + var resultContext = await next(); - if (context.Result is ObjectResult objectResult && objectResult.Value is Deferred deferred) + if (resultContext.Result is ObjectResult objectResult && objectResult.Value is Deferred deferred) { objectResult.Value = await deferred.Value; } diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index cf274c475..8eee695c3 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -149,7 +149,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD this.contentForm.submitFailed(error); }); } else { - if (this.content && !this.content.canUpdate) { + if (this.content && !this.content.canUpdateAny) { return; } @@ -183,7 +183,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD private loadContent(data: any) { this.contentForm.loadContent(data); - this.contentForm.setEnabled(!this.content || this.content.canUpdate); + this.contentForm.setEnabled(!this.content || this.content.canUpdateAny); } public discardChanges() { diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html index 162be35c1..7e6d01324 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html @@ -13,7 +13,7 @@ [ngModelOptions]="onBlur" [ngModel]="step.color" (ngModelChange)="changeColor($event)" - [disabled]="step.isLocked || disabled"> + [disabled]="disabled">
diff --git a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts index 22dc8c01e..fa2637aaa 100644 --- a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts @@ -121,7 +121,11 @@ export class ModalViewDirective implements OnChanges, OnDestroy { } if (this.closeAlways) { - this.modalView.hide(); + const modal = this.modalView; + + setTimeout(() => { + modal.hide(); + }, 100); } else { try { const rootNode = this.renderedView.rootNodes[0]; diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 1096e3679..1120f6b99 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -65,6 +65,7 @@ export class ContentDto { public readonly canDraftPropose: boolean; public readonly canDraftPublish: boolean; public readonly canUpdate: boolean; + public readonly canUpdateAny: boolean; constructor(links: ResourceLinks, public readonly id: string, @@ -87,6 +88,7 @@ export class ContentDto { this.canDraftPropose = hasAnyLink(links, 'draft/propose'); this.canDraftPublish = hasAnyLink(links, 'draft/publish'); this.canUpdate = hasAnyLink(links, 'update'); + this.canUpdateAny = this.canUpdate || this.canDraftPropose; this.statusUpdates = Object.keys(links).filter(x => x.startsWith('status/')).map(x => ({ status: x.substr(7), color: links[x].metadata! })); } diff --git a/src/Squidex/app/shared/services/workflows.service.spec.ts b/src/Squidex/app/shared/services/workflows.service.spec.ts index 901a7b206..441bf0812 100644 --- a/src/Squidex/app/shared/services/workflows.service.spec.ts +++ b/src/Squidex/app/shared/services/workflows.service.spec.ts @@ -260,16 +260,6 @@ describe('Workflow', () => { }); }); - it('should return same workflow if step to update is locked', () => { - const workflow = - new WorkflowDto({}, 'id') - .setStep('1', { color: '#00ff00', isLocked: true }); - - const updated = workflow.setStep('1', { color: 'red' }); - - expect(updated).toBe(workflow); - }); - it('should sort steps case invariant', () => { const workflow = new WorkflowDto({}, 'id') diff --git a/src/Squidex/app/shared/services/workflows.service.ts b/src/Squidex/app/shared/services/workflows.service.ts index 373160909..3988acfa1 100644 --- a/src/Squidex/app/shared/services/workflows.service.ts +++ b/src/Squidex/app/shared/services/workflows.service.ts @@ -43,17 +43,6 @@ export class WorkflowDto extends Model { public readonly displayName: string; - public static DEFAULT = - new WorkflowDto({}, 'id', 'name') - .setStep('Draft', { color: '#8091a5' }) - .setStep('Archived', { color: '#eb3142', noUpdate: true }) - .setStep('Published', { color: '#4bb958', isLocked: true }) - .setTransition('Archived', 'Draft') - .setTransition('Draft', 'Archived') - .setTransition('Draft', 'Published') - .setTransition('Published', 'Draft') - .setTransition('Published', 'Archived'); - constructor( links: ResourceLinks = {}, public readonly id: string, @@ -94,27 +83,29 @@ export class WorkflowDto extends Model { } public setStep(name: string, values: Partial = {}) { - const found = this.getStep(name); - - if (found) { - const { name: _, ...existing } = found; + const old = this.getStep(name); - if (found.isLocked) { - return this; - } + const step = { ...old, name, ...values }; + const steps = [...this.steps.filter(s => s !== old), step]; - values = { ...existing, ...values }; + if (steps.length === 1) { + return this.with({ initial: name, steps }); + } else { + return this.with({ steps }); } + } - const steps = [...this.steps.filter(s => s !== found), { name, ...values }]; + public setTransition(from: string, to: string, values: Partial = {}) { + if (!this.getStep(from) || !this.getStep(to)) { + return this; + } - let initial = this.initial; + const old = this.transitions.find(x => x.from === from && x.to === to); - if (steps.length === 1) { - initial = steps[0].name; - } + const transition = { ...old, from, to, ...values }; + const transitions = [...this.transitions.filter(t => t !== old), transition]; - return this.with({ initial, steps }); + return this.with({ transitions }); } public setInitial(initial: string) { @@ -134,20 +125,15 @@ export class WorkflowDto extends Model { return this; } - const transitions = - steps.length !== this.steps.length ? - this.transitions.filter(t => t.from !== name && t.to !== name) : - this.transitions; - - let initial = this.initial; + const transitions = this.transitions.filter(t => t.from !== name && t.to !== name); - if (initial === name) { + if (this.initial === name) { const first = steps.find(x => !x.isLocked); - initial = first ? first.name : null; + return this.with({ initial: first ? first.name : null, steps, transitions }); + } else { + return this.with({ steps, transitions }); } - - return this.with({ initial, steps, transitions }); } public changeSchemaIds(schemaIds: string[]) { @@ -185,13 +171,11 @@ export class WorkflowDto extends Model { return transition; }); - let initial = this.initial; - - if (initial === name) { - initial = newName; + if (this.initial === name) { + return this.with({ initial: newName, steps, transitions }); + } else { + return this.with({ steps, transitions }); } - - return this.with({ initial, steps, transitions }); } public removeTransition(from: string, to: string) { @@ -204,32 +188,6 @@ export class WorkflowDto extends Model { return this.with({ transitions }); } - public setTransition(from: string, to: string, values: Partial = {}) { - const stepFrom = this.getStep(from); - - if (!stepFrom) { - return this; - } - - const stepTo = this.getStep(to); - - if (!stepTo) { - return this; - } - - const found = this.transitions.find(x => x.from === from && x.to === to); - - if (found) { - const { from: _, to: __, ...existing } = found; - - values = { ...existing, ...values }; - } - - const transitions = [...this.transitions.filter(t => t !== found), { from, to, ...values }]; - - return this.with({ transitions }); - } - public serialize(): any { const result = { steps: {}, schemaIds: this.schemaIds, initial: this.initial, name: this.name }; diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.html b/src/Squidex/app/shell/pages/internal/apps-menu.component.html index d78bbfb5e..7a3897564 100644 --- a/src/Squidex/app/shell/pages/internal/apps-menu.component.html +++ b/src/Squidex/app/shell/pages/internal/apps-menu.component.html @@ -29,7 +29,7 @@ diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts b/src/Squidex/app/shell/pages/internal/apps-menu.component.ts index 22fbc20bb..18dc1d355 100644 --- a/src/Squidex/app/shell/pages/internal/apps-menu.component.ts +++ b/src/Squidex/app/shell/pages/internal/apps-menu.component.ts @@ -36,11 +36,6 @@ export class AppsMenuComponent { ) { } - public createApp() { - this.appsMenu.hide(); - this.addAppDialog.show(); - } - public trackByApp(index: number, app: AppDto) { return app.id; } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs index ac1149312..8f69ae486 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new UpdateContent(); - await ValidationAssert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command, false), new ValidationError("Data is required.", "Data")); } @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new UpdateContent { Data = new NamedContentData() }; - await Assert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command)); + await Assert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command, false)); } [Fact] @@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new UpdateContent { Data = new NamedContentData() }; - await GuardContent.CanUpdate(content, contentWorkflow, command); + await GuardContent.CanUpdate(content, contentWorkflow, command, false); } [Fact] @@ -131,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new PatchContent(); - await ValidationAssert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command, false), new ValidationError("Data is required.", "Data")); } @@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new PatchContent { Data = new NamedContentData() }; - await Assert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command)); + await Assert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command, false)); } [Fact] @@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Draft, false); var command = new PatchContent { Data = new NamedContentData() }; - await GuardContent.CanPatch(content, contentWorkflow, command); + await GuardContent.CanPatch(content, contentWorkflow, command, false); } [Fact] @@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Published, false); var command = new ChangeContentStatus { Status = Status.Published }; - await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command, true), new ValidationError("Content has no changes to publish.", "Status")); } @@ -175,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Published, false); var command = new ChangeContentStatus { Status = Status.Draft }; - await Assert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command)); + await Assert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command, false)); } [Fact] @@ -186,7 +186,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard var content = CreateContent(Status.Published, true); var command = new ChangeContentStatus { Status = Status.Published }; - await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command); + await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command, true); } [Fact] @@ -198,7 +198,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard A.CallTo(() => contentWorkflow.CanMoveToAsync(content, command.Status, user)) .Returns(true); - await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command, false), new ValidationError("Due time must be in the future.", "DueTime")); } @@ -211,7 +211,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard A.CallTo(() => contentWorkflow.CanMoveToAsync(content, command.Status, user)) .Returns(false); - await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command, false), new ValidationError("Cannot change status from Draft to Published.", "Status")); } @@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard A.CallTo(() => contentWorkflow.CanMoveToAsync(content, command.Status, user)) .Returns(true); - await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command); + await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command, false); } [Fact] From 8476e5bfc0bdb9bac33b0e31644fc5fa18bedb63 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 4 Jul 2019 21:39:52 +0200 Subject: [PATCH 16/18] Performance improvements: 1. Caching for app tags 2. Caching for available languages. --- .../Tags/ITagService.cs | 6 ++--- .../Tags/{TagSet.cs => TagsExport.cs} | 2 +- .../Tags/TagsSet.cs | 26 +++++++++++++++++++ .../Assets/BackupAssets.cs | 2 +- .../Tags/GrainTagService.cs | 6 ++--- .../Tags/ITagGrain.cs | 6 ++--- .../Tags/TagGrain.cs | 12 +++++---- .../Controllers/Assets/AssetsController.cs | 6 +++-- .../app/shared/state/languages.state.ts | 22 +++++++++++++--- .../Tags/GrainTagServiceTests.cs | 2 +- .../Tags/TagGrainTests.cs | 2 +- 11 files changed, 68 insertions(+), 24 deletions(-) rename src/Squidex.Domain.Apps.Core.Operations/Tags/{TagSet.cs => TagsExport.cs} (88%) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs index f0fc88a3a..ad819ba57 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs @@ -19,11 +19,11 @@ namespace Squidex.Domain.Apps.Core.Tags Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids); - Task> GetTagsAsync(Guid appId, string group); + Task GetTagsAsync(Guid appId, string group); - Task GetExportableTagsAsync(Guid appId, string group); + Task GetExportableTagsAsync(Guid appId, string group); - Task RebuildTagsAsync(Guid appId, string group, TagSet tags); + Task RebuildTagsAsync(Guid appId, string group, TagsExport tags); Task ClearAsync(Guid appId, string group); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs similarity index 88% rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs rename to src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs index 530c28b00..d1f54ecf7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; namespace Squidex.Domain.Apps.Core.Tags { - public sealed class TagSet : Dictionary + public sealed class TagsExport : Dictionary { } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs new file mode 100644 index 000000000..8e87ee8ab --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core.Tags +{ + public sealed class TagsSet : Dictionary + { + public long Version { get; set; } + + public TagsSet() + { + } + + public TagsSet(IDictionary tags, long version) + : base(tags) + { + Version = version; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index 068a807de..44701ee16 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Assets private async Task RestoreTagsAsync(Guid appId, BackupReader reader) { - var tags = await reader.ReadJsonAttachmentAsync(TagsFile); + var tags = await reader.ReadJsonAttachmentAsync(TagsFile); await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags); } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs index ad8c37457..08f1ff835 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs @@ -45,17 +45,17 @@ namespace Squidex.Domain.Apps.Entities.Tags return GetGrain(appId, group).DenormalizeTagsAsync(ids); } - public Task> GetTagsAsync(Guid appId, string group) + public Task GetTagsAsync(Guid appId, string group) { return GetGrain(appId, group).GetTagsAsync(); } - public Task GetExportableTagsAsync(Guid appId, string group) + public Task GetExportableTagsAsync(Guid appId, string group) { return GetGrain(appId, group).GetExportableTagsAsync(); } - public Task RebuildTagsAsync(Guid appId, string group, TagSet tags) + public Task RebuildTagsAsync(Guid appId, string group, TagsExport tags) { return GetGrain(appId, group).RebuildAsync(tags); } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs index d43b6f022..be9a5bdfb 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs @@ -20,12 +20,12 @@ namespace Squidex.Domain.Apps.Entities.Tags Task> DenormalizeTagsAsync(HashSet ids); - Task> GetTagsAsync(); + Task GetTagsAsync(); - Task GetExportableTagsAsync(); + Task GetExportableTagsAsync(); Task ClearAsync(); - Task RebuildAsync(TagSet tags); + Task RebuildAsync(TagsExport tags); } } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs index 9062e3366..3053bf1a6 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Tags [CollectionName("Index_Tags")] public sealed class GrainState { - public TagSet Tags { get; set; } = new TagSet(); + public TagsExport Tags { get; set; } = new TagsExport(); } public TagGrain(IStore store) @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Tags return ClearStateAsync(); } - public Task RebuildAsync(TagSet tags) + public Task RebuildAsync(TagsExport tags) { State.Tags = tags; @@ -132,12 +132,14 @@ namespace Squidex.Domain.Apps.Entities.Tags return Task.FromResult(result); } - public Task> GetTagsAsync() + public Task GetTagsAsync() { - return Task.FromResult(State.Tags.Values.ToDictionary(x => x.Name, x => x.Count)); + var tags = State.Tags.Values.ToDictionary(x => x.Name, x => x.Count); + + return Task.FromResult(new TagsSet(tags, Persistence.Version)); } - public Task GetExportableTagsAsync() + public Task GetExportableTagsAsync() { return Task.FromResult(State.Tags); } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 84f423dda..0456767f0 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -77,9 +77,11 @@ namespace Squidex.Areas.Api.Controllers.Assets [ApiCosts(1)] public async Task GetTags(string app) { - var response = await tagService.GetTagsAsync(AppId, TagGroups.Assets); + var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); - return Ok(response); + Response.Headers[HeaderNames.ETag] = tags.Version.ToString(); + + return Ok(tags); } /// diff --git a/src/Squidex/app/shared/state/languages.state.ts b/src/Squidex/app/shared/state/languages.state.ts index 7f1a77f27..fa792951a 100644 --- a/src/Squidex/app/shared/state/languages.state.ts +++ b/src/Squidex/app/shared/state/languages.state.ts @@ -7,7 +7,7 @@ import { Injectable } from '@angular/core'; import { forkJoin, Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { map, shareReplay, tap } from 'rxjs/operators'; import { DialogService, @@ -65,6 +65,8 @@ type LanguageResultList = ImmutableArray; @Injectable() export class LanguagesState extends State { + private cachedLanguage$: Observable; + public languages = this.project(x => x.languages); @@ -96,9 +98,7 @@ export class LanguagesState extends State { this.resetState(); } - return forkJoin( - this.languagesService.getLanguages(), - this.appLanguagesService.getLanguages(this.appName)).pipe( + return forkJoin(this.getAllLanguages(), this.getAppLanguages()).pipe( map(args => { return { allLanguages: args[0], languages: args[1] }; }), @@ -166,6 +166,20 @@ export class LanguagesState extends State { return this.snapshot.version; } + private getAppLanguages() { + return this.appLanguagesService.getLanguages(this.appName); + } + + private getAllLanguages() { + if (!this.cachedLanguage$) { + this.cachedLanguage$ = + this.languagesService.getLanguages().pipe( + shareReplay(1)); + } + + return this.cachedLanguage$; + } + private createLanguage(language: AppLanguageDto, languages: AppLanguagesList): SnapshotLanguage { return { language, diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs index 5f449249f..aac32efb0 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Tags [Fact] public async Task Should_call_grain_when_rebuilding() { - var tags = new TagSet(); + var tags = new TagsExport(); await sut.RebuildTagsAsync(appId, TagGroups.Assets, tags); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs index 9642dd244..562ceb805 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Tags [Fact] public async Task Should_rebuild_tags() { - var tags = new TagSet + var tags = new TagsExport { ["id1"] = new Tag { Name = "name1", Count = 1 }, ["id2"] = new Tag { Name = "name2", Count = 2 }, From 73002fa38bd33c44bd7cff2830258ad8fa7de968 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 6 Jul 2019 18:53:17 +0200 Subject: [PATCH 17/18] Download url in backup dto. --- src/Squidex.Web/ResourceLink.cs | 1 - .../Backups/Models/BackupJobDto.cs | 2 ++ ...RestoreRequest.cs => RestoreRequestDto.cs} | 2 +- .../Controllers/Backups/RestoreController.cs | 2 +- .../Rules/Models/RuleActionProcessor.cs | 2 +- .../Controllers/Schemas/SchemasController.cs | 2 +- .../IdentityServer/Config/LazyClientStore.cs | 2 +- src/Squidex/app/features/settings/module.ts | 2 -- .../pages/backups/backups-page.component.html | 2 +- .../pages/backups/backups-page.component.ts | 2 ++ .../features/settings/pages/backups/pipes.ts | 23 +------------------ .../app/shared/services/backups.service.ts | 4 ++++ 12 files changed, 15 insertions(+), 31 deletions(-) rename src/Squidex/Areas/Api/Controllers/Backups/Models/{RestoreRequest.cs => RestoreRequestDto.cs} (95%) diff --git a/src/Squidex.Web/ResourceLink.cs b/src/Squidex.Web/ResourceLink.cs index ef54bfa98..d1caffc8d 100644 --- a/src/Squidex.Web/ResourceLink.cs +++ b/src/Squidex.Web/ResourceLink.cs @@ -19,7 +19,6 @@ namespace Squidex.Web [Display(Description = "The link method.")] public string Method { get; set; } - [Required] [Display(Description = "Additional data about the link.")] public string Metadata { get; set; } } diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs b/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs index 8f39a7140..5e0163380 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs @@ -62,6 +62,8 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteBackup), values)); } + AddGetLink("download", controller.Url(x => nameof(x.GetBackupContent), values)); + return this; } } diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs b/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs similarity index 95% rename from src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs rename to src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs index a6b103a05..f51bc342b 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequestDto.cs @@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Areas.Api.Controllers.Backups.Models { - public sealed class RestoreRequest + public sealed class RestoreRequestDto { /// /// The name of the app. diff --git a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs index 4426330de..8a5a5c4d1 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs @@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Backups [HttpPost] [Route("apps/restore/")] [ApiPermission(Permissions.AdminRestore)] - public async Task PostRestore([FromBody] RestoreRequest request) + public async Task PostRestore([FromBody] RestoreRequestDto request) { var restoreGrain = grainFactory.GetGrain(SingleGrain.Id); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs index b0337ed42..7bd538707 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs @@ -55,7 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models if (oldName != null) { context.Document.Definitions.Remove(oldName); - context.Document.Definitions.Add(action.Key, derivedSchema); + context.Document.Definitions.Add($"{action.Key}RuleActionDto", derivedSchema); } } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 192c8fc87..036999da6 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -114,7 +114,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPost] [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), 201)] [ApiPermission(Permissions.AppSchemasCreate)] [ApiCosts(1)] public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index 2df7efe1c..e07e6d059 100644 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -176,7 +176,7 @@ namespace Squidex.Areas.IdentityServer.Config }, Claims = new List { - new Claim(SquidexClaimTypes.Permissions, Permissions.Admin) + new Claim(SquidexClaimTypes.Permissions, Permissions.All) } }; } diff --git a/src/Squidex/app/features/settings/module.ts b/src/Squidex/app/features/settings/module.ts index d77bf3601..031f1a72e 100644 --- a/src/Squidex/app/features/settings/module.ts +++ b/src/Squidex/app/features/settings/module.ts @@ -16,7 +16,6 @@ import { } from '@app/shared'; import { - BackupDownloadUrlPipe, BackupDurationPipe, BackupsPageComponent, ClientComponent, @@ -199,7 +198,6 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ - BackupDownloadUrlPipe, BackupDurationPipe, BackupsPageComponent, ClientComponent, diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html index 94ade6d25..f55d7a783 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -72,7 +72,7 @@
Download: - + Ready
diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts index 9a0f1b700..13c0ce496 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts @@ -10,6 +10,7 @@ import { timer } from 'rxjs'; import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { + ApiUrlConfig, AppsState, BackupDto, BackupsState, @@ -23,6 +24,7 @@ import { }) export class BackupsPageComponent extends ResourceOwner implements OnInit { constructor( + public readonly apiUrl: ApiUrlConfig, public readonly appsState: AppsState, public readonly backupsState: BackupsState ) { diff --git a/src/Squidex/app/features/settings/pages/backups/pipes.ts b/src/Squidex/app/features/settings/pages/backups/pipes.ts index 7351ce9a6..d73192f42 100644 --- a/src/Squidex/app/features/settings/pages/backups/pipes.ts +++ b/src/Squidex/app/features/settings/pages/backups/pipes.ts @@ -7,12 +7,7 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { - ApiUrlConfig, - AppsState, - BackupDto, - Duration -} from '@app/shared'; +import { BackupDto, Duration } from '@app/shared'; @Pipe({ name: 'sqxBackupDuration', @@ -22,20 +17,4 @@ export class BackupDurationPipe implements PipeTransform { public transform(backup: BackupDto) { return Duration.create(backup.started, backup.stopped!).toString(); } -} - -@Pipe({ - name: 'sqxBackupDownloadUrl', - pure: true -}) -export class BackupDownloadUrlPipe implements PipeTransform { - constructor( - private readonly apiUrl: ApiUrlConfig, - private readonly appsState: AppsState - ) { - } - - public transform(backup: BackupDto) { - return this.apiUrl.buildUrl(`api/apps/${this.appsState.appName}/backups/${backup.id}`); - } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/backups.service.ts b/src/Squidex/app/shared/services/backups.service.ts index 3af49a2c7..9ecf9a270 100644 --- a/src/Squidex/app/shared/services/backups.service.ts +++ b/src/Squidex/app/shared/services/backups.service.ts @@ -41,6 +41,8 @@ export class BackupDto { public readonly canDelete: boolean; + public readonly downloadUrl: string; + constructor( links: ResourceLinks, public readonly id: string, @@ -53,6 +55,8 @@ export class BackupDto { this._links = links; this.canDelete = hasAnyLink(links, 'delete'); + + this.downloadUrl = links['download'].href; } } From bce63be8718a6eb1a0abdf97e9e8556bf961312b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 6 Jul 2019 21:46:05 +0200 Subject: [PATCH 18/18] Tests fixed. --- .../shared/services/backups.service.spec.ts | 57 ++++++++++++------- .../app/shared/state/backups.state.spec.ts | 8 +-- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/Squidex/app/shared/services/backups.service.spec.ts b/src/Squidex/app/shared/services/backups.service.spec.ts index 11f7d81fe..9713a023a 100644 --- a/src/Squidex/app/shared/services/backups.service.spec.ts +++ b/src/Squidex/app/shared/services/backups.service.spec.ts @@ -16,6 +16,7 @@ import { BackupsService, DateTime, Resource, + ResourceLinks, RestoreDto } from '@app/shared/internal'; @@ -52,30 +53,16 @@ describe('BackupsService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ - items: [{ - id: '1', - started: '2017-02-03', - stopped: '2017-02-04', - handledEvents: 13, - handledAssets: 17, - status: 'Failed', - _links: {} - }, - { - id: '2', - started: '2018-02-03', - stopped: null, - handledEvents: 23, - handledAssets: 27, - status: 'Completed', - _links: {} - }] + items: [ + backupResponse(12), + backupResponse(13) + ] }); expect(backups!).toEqual( new BackupsDto([ - new BackupDto({}, '1', DateTime.parseISO_UTC('2017-02-03'), DateTime.parseISO_UTC('2017-02-04'), 13, 17, 'Failed'), - new BackupDto({}, '2', DateTime.parseISO_UTC('2018-02-03'), null, 23, 27, 'Completed') + createBackup(12), + createBackup(13) ])); })); @@ -203,4 +190,32 @@ describe('BackupsService', () => { req.flush({}); })); -}); \ No newline at end of file + + function backupResponse(id: number) { + return { + id: `id${id}`, + started: `${id % 1000 + 2000}-12-12T10:10:00`, + stopped: id % 2 === 0 ? `${id % 1000 + 2000}-11-11T10:10:00` : null, + handledEvents: id * 17, + handledAssets: id * 23, + status: id % 2 === 0 ? 'Status' : 'Failed', + _links: { + download: { method: 'GET', href: '/api/backups/1' } + } + }; + } +}); + +export function createBackup(id: number) { + const links: ResourceLinks = { + download: { method: 'GET', href: '/api/backups/1' } + }; + + return new BackupDto(links, + `id${id}`, + DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), + id % 2 === 0 ? DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`) : null, + id * 17, + id * 23, + id % 2 === 0 ? 'Status' : 'Failed'); +} \ No newline at end of file diff --git a/src/Squidex/app/shared/state/backups.state.spec.ts b/src/Squidex/app/shared/state/backups.state.spec.ts index 948589f35..22d5bb952 100644 --- a/src/Squidex/app/shared/state/backups.state.spec.ts +++ b/src/Squidex/app/shared/state/backups.state.spec.ts @@ -10,24 +10,24 @@ import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; import { - BackupDto, BackupsDto, BackupsService, BackupsState, - DateTime, DialogService } from '@app/shared/internal'; import { TestValues } from './_test-helpers'; +import { createBackup } from './../services/backups.service.spec'; + describe('BackupsState', () => { const { app, appsState } = TestValues; - const backup1 = new BackupDto({}, 'id1', DateTime.now(), null, 1, 1, 'Started'); - const backup2 = new BackupDto({}, 'id2', DateTime.now(), null, 2, 2, 'Started'); + const backup1 = createBackup(12); + const backup2 = createBackup(13); let dialogs: IMock; let backupsService: IMock;