Browse Source

Improve workflow for save and publish.

pull/747/head
Sebastian 4 years ago
parent
commit
ad9130b64f
  1. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  2. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  3. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  4. 2
      backend/src/Squidex.Web/Resources.cs
  5. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  6. 11
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  7. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs
  8. 18
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs
  9. 4
      frontend/app/features/administration/pages/restore/restore-page.component.html
  10. 4
      frontend/app/features/content/pages/content/content-page.component.html
  11. 2
      frontend/app/framework/angular/forms/editors/dropdown.component.html
  12. 3
      frontend/app/framework/angular/forms/editors/dropdown.component.scss

5
backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs

@ -52,6 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Task.FromResult(Status.Draft); return Task.FromResult(Status.Draft);
} }
public Task<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user)
{
return Task.FromResult(true);
}
public Task<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) public Task<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user)
{ {
var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next);

18
backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs

@ -35,6 +35,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray();
} }
public async Task<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user)
{
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, null, user);
}
public async Task<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) public async Task<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user)
{ {
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
@ -49,13 +56,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, content.Data, user); return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, content.Data, user);
} }
public async Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, ContentData data, ClaimsPrincipal? user)
{
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, data, user);
}
public async Task<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) public async Task<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user)
{ {
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return result.ToArray(); return result.ToArray();
} }
private bool IsTrue(WorkflowCondition condition, ContentData data, ClaimsPrincipal? user) private bool IsTrue(WorkflowCondition condition, ContentData? data, ClaimsPrincipal? user)
{ {
if (condition?.Roles != null && user != null) if (condition?.Roles != null && user != null)
{ {
@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
if (!string.IsNullOrWhiteSpace(condition?.Expression)) if (!string.IsNullOrWhiteSpace(condition?.Expression) && data != null)
{ {
var vars = new ScriptVars var vars = new ScriptVars
{ {

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -22,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
Task<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user); Task<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user);
Task<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user);
Task<StatusInfo?> GetInfoAsync(IContentEntity content, Status status); Task<StatusInfo?> GetInfoAsync(IContentEntity content, Status status);
Task<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user); Task<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user);

2
backend/src/Squidex.Web/Resources.cs

@ -30,6 +30,8 @@ namespace Squidex.Web
public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(Permissions.AppContentsVersionDeleteOwn, schema); public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(Permissions.AppContentsVersionDeleteOwn, schema);
public bool CanChangeStatus(string schema) => IsAllowedForSchema(Permissions.AppContentsChangeStatus, schema);
public bool CanCancelContentStatus(string schema) => IsAllowedForSchema(Permissions.AppContentsChangeStatusCancelOwn, schema); public bool CanCancelContentStatus(string schema) => IsAllowedForSchema(Permissions.AppContentsChangeStatusCancelOwn, schema);
public bool CanUpdateContent(string schema) => IsAllowedForSchema(Permissions.AppContentsUpdateOwn, schema); public bool CanUpdateContent(string schema) => IsAllowedForSchema(Permissions.AppContentsUpdateOwn, schema);

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

@ -173,7 +173,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
} }
} }
if (content.NextStatuses != null && resources.CanUpdateContent(schema)) if (content.NextStatuses != null && resources.CanChangeStatus(schema))
{ {
foreach (var next in content.NextStatuses) foreach (var next in content.NextStatuses)
{ {

11
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
await result.AssignStatusesAsync(workflow, schema); await result.AssignStatusesAsync(workflow, schema);
result.CreateLinks(resources, schema.SchemaDef.Name); await result.CreateLinksAsync(resources, workflow, schema);
} }
return result; return result;
@ -60,20 +60,23 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray(); Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray();
} }
private void CreateLinks(Resources resources, string schema) private async Task CreateLinksAsync(Resources resources, IContentWorkflow workflow, ISchemaEntity schema)
{ {
var values = new { app = resources.App, schema }; var values = new { app = resources.App, schema = schema.SchemaDef.Name };
AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContents), values)); AddSelfLink(resources.Url<ContentsController>(x => nameof(x.GetContents), values));
if (resources.CanCreateContent(schema)) if (resources.CanCreateContent(values.schema))
{ {
AddPostLink("create", resources.Url<ContentsController>(x => nameof(x.PostContent), values)); AddPostLink("create", resources.Url<ContentsController>(x => nameof(x.PostContent), values));
if (resources.CanChangeStatus(values.schema) && await workflow.CanPublishInitialAsync(schema, resources.Context.User))
{
var publishValues = new { values.app, values.schema, publish = true }; var publishValues = new { values.app, values.schema, publish = true };
AddPostLink("create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues)); AddPostLink("create/publish", resources.Url<ContentsController>(x => nameof(x.PostContent), publishValues));
} }
} }
} }
}
} }

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs

@ -40,6 +40,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Equal(Status.Draft, result); Assert.Equal(Status.Draft, result);
} }
[Fact]
public async Task Should_allow_publish_on_create()
{
var result = await sut.CanPublishInitialAsync(null!, null);
Assert.True(result);
}
[Fact] [Fact]
public async Task Should_allow_if_transition_is_valid() public async Task Should_allow_if_transition_is_valid()
{ {

18
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs

@ -133,29 +133,15 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_allow_publish_on_create() public async Task Should_allow_publish_on_create()
{ {
var content = CreateContent(Status.Draft, 2); var result = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Editor"));
var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.Data, Mocks.FrontendUser("Editor"));
Assert.True(result); 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(Mocks.Schema(appId, schemaId), content.Data, Mocks.FrontendUser("Editor"));
Assert.False(result);
}
[Fact] [Fact]
public async Task Should_not_allow_publish_on_create_if_role_not_allowed() public async Task Should_not_allow_publish_on_create_if_role_not_allowed()
{ {
var content = CreateContent(Status.Draft, 2); var result = await sut.CanPublishInitialAsync(Mocks.Schema(appId, schemaId), Mocks.FrontendUser("Developer"));
var result = await sut.CanPublishOnCreateAsync(Mocks.Schema(appId, schemaId), content.Data, Mocks.FrontendUser("Developer"));
Assert.False(result); Assert.False(result);
} }

4
frontend/app/features/administration/pages/restore/restore-page.component.html

@ -47,9 +47,13 @@
<form [formGroup]="restoreForm.form" (ngSubmit)="restore()"> <form [formGroup]="restoreForm.form" (ngSubmit)="restore()">
<div class="row gx-2"> <div class="row gx-2">
<div class="col"> <div class="col">
<sqx-control-errors for="url"></sqx-control-errors>
<input class="form-control" formControlName="url" placeholder="{{ 'backups.restoreLastUrl' | sqxTranslate }}"> <input class="form-control" formControlName="url" placeholder="{{ 'backups.restoreLastUrl' | sqxTranslate }}">
</div> </div>
<div class="col"> <div class="col">
<sqx-control-errors for="name"></sqx-control-errors>
<input class="form-control" formControlName="name" placeholder="{{ 'backups.restoreNewAppName' | sqxTranslate }}"> <input class="form-control" formControlName="name" placeholder="{{ 'backups.restoreNewAppName' | sqxTranslate }}">
</div> </div>
<div class="col-auto"> <div class="col-auto">

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

@ -91,11 +91,11 @@
</ng-container> </ng-container>
<ng-template #noContentMenu> <ng-template #noContentMenu>
<button type="button" class="btn btn-secondary" (click)="save()" *ngIf="contentsState.canCreate | async"> <button type="button" class="btn btn-primary" (click)="save()" *ngIf="contentsState.canCreate | async">
{{ 'common.save' | sqxTranslate }} {{ 'common.save' | sqxTranslate }}
</button> </button>
<button type="submit" class="btn btn-primary ms-2" title="i18n:common.saveShortcut" shortcut="CTRL + SHIFT + S" *ngIf="contentsState.canCreateAndPublish | async"> <button type="submit" class="btn btn-success ms-2" title="i18n:common.saveShortcut" shortcut="CTRL + SHIFT + S" *ngIf="contentsState.canCreateAndPublish | async">
{{ 'contents.saveAndPublish' | sqxTranslate }} {{ 'contents.saveAndPublish' | sqxTranslate }}
</button> </button>
</ng-template> </ng-template>

2
frontend/app/framework/angular/forms/editors/dropdown.component.html

@ -5,10 +5,12 @@
autocapitalize="off"> autocapitalize="off">
<div class="control-dropdown-item" *ngIf="snapshot.selectedItem; let selectedItem"> <div class="control-dropdown-item" *ngIf="snapshot.selectedItem; let selectedItem">
<div>
<span class="truncate" *ngIf="!templateSelection">{{selectedItem}}</span> <span class="truncate" *ngIf="!templateSelection">{{selectedItem}}</span>
<ng-template *ngIf="templateSelection" [sqxTemplateWrapper]="templateSelection" [item]="selectedItem"></ng-template> <ng-template *ngIf="templateSelection" [sqxTemplateWrapper]="templateSelection" [item]="selectedItem"></ng-template>
</div> </div>
</div>
</div> </div>
<div class="items-container"> <div class="items-container">

3
frontend/app/framework/angular/forms/editors/dropdown.component.scss

@ -33,8 +33,11 @@ $color-input-disabled: #eef1f4;
.control-dropdown-item { .control-dropdown-item {
@include absolute(0, 1.75rem, 0, 0); @include absolute(0, 1.75rem, 0, 0);
align-items: center;
display: flex;
line-height: 1.2rem; line-height: 1.2rem;
overflow: hidden; overflow: hidden;
padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;

Loading…
Cancel
Save