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]