Browse Source

Status fixes for singleton contents.

pull/502/head
Sebastian 6 years ago
parent
commit
10d7341577
  1. 17
      backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  2. 19
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs
  3. 37
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  4. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs
  5. 49
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs
  6. 196
      frontend/app/features/content/pages/content/content-history-page.component.html
  7. 2
      frontend/app/features/content/pages/content/content-history-page.component.scss
  8. 19
      frontend/app/features/content/pages/content/content-history-page.component.ts
  9. 34
      frontend/app/features/content/pages/content/content-page.component.html
  10. 2
      frontend/app/shared/components/schema-category.component.ts

17
backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -23,11 +23,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command);
Validate.It(() => "Cannot created content.", e =>
{
ValidateData(command, e);
});
if (schema.SchemaDef.IsSingleton && command.ContentId != schema.Id)
{
throw new DomainException("Singleton content cannot be created.");
@ -37,6 +32,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
throw new DomainException("Content workflow prevents publishing.");
}
Validate.It(() => "Cannot created content.", e =>
{
ValidateData(command, e);
});
}
public static async Task CanUpdate(ContentState content, IContentWorkflow contentWorkflow, UpdateContent command)
@ -99,7 +99,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
if (schema.SchemaDef.IsSingleton)
{
throw new DomainException("Singleton content cannot be updated.");
if (content.NewVersion == null || command.Status != Status.Published)
{
throw new DomainException("Singleton content cannot be updated.");
}
return Task.CompletedTask;
}
return Validate.It(() => "Cannot change status.", async e =>

19
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichWithWorkflows.cs

@ -46,7 +46,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
var editingStatus = content.NewStatus ?? content.Status;
content.NextStatuses = await contentWorkflow.GetNextAsync(content, editingStatus, context.User);
if (content.IsSingleton)
{
if (editingStatus == Status.Draft)
{
content.NextStatuses = new[]
{
new StatusInfo(Status.Published, StatusColors.Published)
};
}
else
{
content.NextStatuses = Array.Empty<StatusInfo>();
}
}
else
{
content.NextStatuses = await contentWorkflow.GetNextAsync(content, editingStatus, context.User);
}
}
private async Task EnrichCanUpdateAsync(ContentEntity content, Context context)

37
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -150,35 +150,32 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
AddGetLink("previous", controller.Url<ContentsController>(x => nameof(x.GetContentVersion), versioned));
}
if (!content.IsSingleton)
if (NewStatus.HasValue)
{
if (NewStatus.HasValue)
if (controller.HasPermission(Permissions.AppContentsVersionDelete, app, schema))
{
if (controller.HasPermission(Permissions.AppContentsVersionDelete, app, schema))
{
AddDeleteLink("draft/delete", controller.Url<ContentsController>(x => nameof(x.DeleteVersion), values));
}
AddDeleteLink("draft/delete", controller.Url<ContentsController>(x => nameof(x.DeleteVersion), values));
}
else if (Status == Status.Published)
}
else if (Status == Status.Published)
{
if (controller.HasPermission(Permissions.AppContentsVersionCreate, app, schema))
{
if (controller.HasPermission(Permissions.AppContentsVersionCreate, app, schema))
{
AddPostLink("draft/create", controller.Url<ContentsController>(x => nameof(x.CreateDraft), values));
}
AddPostLink("draft/create", controller.Url<ContentsController>(x => nameof(x.CreateDraft), values));
}
}
if (content.NextStatuses != null)
if (content.NextStatuses != null)
{
foreach (var next in content.NextStatuses)
{
foreach (var next in content.NextStatuses)
{
AddPutLink($"status/{next.Status}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color);
}
AddPutLink($"status/{next.Status}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color);
}
}
if (controller.HasPermission(Permissions.AppContentsDelete, app, schema))
{
AddDeleteLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values));
}
if (content.IsSingleton == false && controller.HasPermission(Permissions.AppContentsDelete, app, schema))
{
AddDeleteLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values));
}
if (content.CanUpdate)

11
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

@ -204,6 +204,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
new ValidationError("Cannot change status from Draft to Published.", "Status"));
}
[Fact]
public async Task CanChangeStatus_should_not_throw_exception_if_singleton_is_published()
{
var schema = CreateSchema(true);
var content = CreateDraftContent(Status.Draft);
var command = new ChangeContentStatus { Status = Status.Published };
await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command);
}
[Fact]
public async Task CanChangeStatus_should_not_throw_exception_if_status_flow_valid()
{

49
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
@ -27,11 +28,55 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public EnrichWithWorkflowsTests()
{
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId));
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId));
sut = new EnrichWithWorkflows(contentWorkflow);
}
[Fact]
public async Task Should_enrich_content_with_next_statuses()
{
var content = new ContentEntity { SchemaId = schemaId };
var nexts = new[]
{
new StatusInfo(Status.Published, StatusColors.Published)
};
A.CallTo(() => contentWorkflow.GetNextAsync(content, content.Status, requestContext.User))
.Returns(nexts);
await sut.EnrichAsync(requestContext, new[] { content }, null!);
Assert.Equal(nexts, content.NextStatuses);
}
[Fact]
public async Task Should_enrich_content_with_next_statuses_if_draft_singleton()
{
var content = new ContentEntity { SchemaId = schemaId, IsSingleton = true, Status = Status.Draft };
await sut.EnrichAsync(requestContext, new[] { content }, null!);
Assert.Equal(Status.Published, content.NextStatuses.Single().Status);
A.CallTo(() => contentWorkflow.GetNextAsync(content, A<Status>._, requestContext.User))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_enrich_content_with_next_statuses_if_published_singleton()
{
var content = new ContentEntity { SchemaId = schemaId, IsSingleton = true, Status = Status.Published };
await sut.EnrichAsync(requestContext, new[] { content }, null!);
Assert.Empty(content.NextStatuses);
A.CallTo(() => contentWorkflow.GetNextAsync(content, A<Status>._, requestContext.User))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_enrich_content_with_status_color()
{
@ -108,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var content = new ContentEntity { SchemaId = schemaId };
var ctx = requestContext.WithResolveFlow(false);
var ctx = new Context(Mocks.ApiUser(), Mocks.App(appId)).WithResolveFlow(false);
await sut.EnrichAsync(ctx, new[] { content }, null!);

196
frontend/app/features/content/pages/content/content-history-page.component.html

@ -4,116 +4,114 @@
</ng-container>
<ng-container content>
<ng-container *ngIf="!schema.isSingleton">
<div class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete">
<ng-container *ngIf="!content.newStatus; else newVersion">
<button class="btn btn-success btn-block" (click)="createDraft() ">
New Draft
</button>
</ng-container>
<ng-template #newVersion>
<label>New Version</label>
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" [class.active]="dropdownNew.isOpen | async" #buttonOptions>
<sqx-content-status
layout="multiline"
[status]="content.newStatus"
[statusColor]="content.newStatusColor"
[scheduled]="content.scheduleJob">
</sqx-content-status>
</button>
<ng-container *sqxModal="dropdownNew;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
Change to <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<div class="dropdown-divider"></div>
</ng-container>
<div class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete">
<ng-container *ngIf="!content.newStatus; else newVersion">
<button class="btn btn-success btn-block" (click)="createDraft() ">
New Draft
</button>
</ng-container>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDraftDelete"
(sqxConfirmClick)="deleteDraft()"
confirmTitle="Delete content"
confirmText="Do you really want to delete this version?">
Delete this Version
<ng-template #newVersion>
<label>New Version</label>
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" [class.active]="dropdownNew.isOpen | async" #buttonOptions>
<sqx-content-status
layout="multiline"
[status]="content.newStatus"
[statusColor]="content.newStatusColor"
[scheduled]="content.scheduleJob">
</sqx-content-status>
</button>
<ng-container *sqxModal="dropdownNew;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
Change to <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</ng-template>
</div>
</ng-container>
<div class="section">
<label>Current Version</label>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDraftDelete"
(sqxConfirmClick)="deleteDraft()"
confirmTitle="Delete content"
confirmText="Do you really want to delete this version?">
Delete this Version
</a>
<div *ngIf="!content.newStatus; else newStatusOld">
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<sqx-content-status small="true"
layout="multiline"
[status]="content.status"
[statusColor]="content.statusColor"
[scheduled]="content.scheduleJob">
</sqx-content-status>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
Change to
<sqx-content-status small="true"
layout="text"
[status]="info.status"
[statusColor]="info.color">
</sqx-content-status>
</a>
<div class="dropdown-divider"></div>
</ng-container>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</div>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</ng-template>
</div>
<div class="section">
<label>Current Version</label>
<div *ngIf="!content.newStatus; else newStatusOld">
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<sqx-content-status small="true"
layout="multiline"
[status]="content.status"
[statusColor]="content.statusColor"
[scheduled]="content.scheduleJob">
</sqx-content-status>
</button>
<ng-template #newStatusOld>
<button type="button" class="btn btn-outline-secondary btn-block btn-status">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
layout="multiline">
</sqx-content-status>
</button>
</ng-template>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
Change to
<sqx-content-status small="true"
layout="text"
[status]="info.status"
[statusColor]="info.color">
</sqx-content-status>
</a>
<div class="dropdown-divider"></div>
</ng-container>
<sqx-form-hint marginTop="1">
Last Updated: {{content.lastModified | sqxFromNow}}
</sqx-form-hint>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</div>
</ng-container>
<ng-template #newStatusOld>
<button type="button" class="btn btn-outline-secondary btn-block btn-status">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
layout="multiline">
</sqx-content-status>
</button>
</ng-template>
<sqx-form-hint marginTop="1">
Last Updated: {{content.lastModified | sqxFromNow}}
</sqx-form-hint>
</div>
<div class="section">
<h3 class="bordered" *ngIf="!schema.isSingleton">History</h3>
<h3 class="bordered">History</h3>
<sqx-content-event *ngFor="let event of contentEvents | async; trackBy: trackByEvent"
[content]="content"

2
frontend/app/features/content/pages/content/content-history-page.component.scss

@ -1,5 +1,5 @@
.section {
margin-bottom: 1rem;
margin-bottom: 2rem;
&:last-child {
margin: 0;

19
frontend/app/features/content/pages/content/content-history-page.component.ts

@ -9,7 +9,7 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable, timer } from 'rxjs';
import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators';
import { filter, map, onErrorResumeNext, switchMap } from 'rxjs/operators';
import {
AppsState,
@ -62,22 +62,25 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit
this.own(
this.schemasState.selectedSchema
.subscribe(schema => {
this.schema = schema;
if (schema) {
this.schema = schema;
}
}));
this.own(
this.contentsState.selectedContent
.subscribe(content => {
if (content) {
const channel = `contents.${content.id}`;
this.contentEvents =
timer(0, 5000).pipe(
switchSafe((() => this.historyService.getHistory(this.appsState.appName, channel))));
this.content = content;
}
}));
this.contentEvents =
this.contentsState.selectedContent.pipe(
filter(x => !!x),
map(content => `contents.${content?.id}`),
switchSafe(channel => timer(0, 5000).pipe(map(() => channel))),
switchSafe(channel => this.historyService.getHistory(this.appsState.appName, channel)));
}
public changeStatus(status: string) {

34
frontend/app/features/content/pages/content/content-page.component.html

@ -18,28 +18,26 @@
New Content
</ng-template>
</ng-container>
<ng-container menu>
<ng-container *ngIf="content; else noContent">
<ng-container *ngIf="!schema.isSingleton">
<ng-container>
<sqx-preview-button [schema]="schema" [content]="content"></sqx-preview-button>
<div class="dropdown dropdown-options ml-1">
<ng-container *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-more"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
<div class="dropdown dropdown-options ml-1" *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-more"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</div>
</ng-container>

2
frontend/app/shared/components/schema-category.component.ts

@ -88,7 +88,7 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
public schemaRoute(schema: SchemaDto) {
if (schema.isSingleton && this.forContent) {
return [schema.name, schema.id];
return [schema.name, schema.id, 'history'];
} else {
return [schema.name];
}

Loading…
Cancel
Save