Browse Source

Temporary

pull/377/head
Sebastian Stehle 7 years ago
parent
commit
337abb03c4
  1. 5
      src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  2. 16
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs
  3. 23
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs
  4. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  5. 9
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  7. 26
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  8. 61
      src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  9. 8
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs
  11. 8
      src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  12. 11
      src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs
  13. 10
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  14. 6
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs
  15. 4
      src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs
  16. 4
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs
  17. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs
  18. 24
      src/Squidex.Web/Resource.cs
  19. 4
      src/Squidex.Web/ResourceLink.cs
  20. 11
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  21. 4
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  22. 32
      src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs
  23. 3
      src/Squidex/Config/Domain/StoreServices.cs
  24. 2
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html
  25. 2
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss
  26. 2
      src/Squidex/app/framework/utils/hateos.ts
  27. 6
      src/Squidex/app/shared/services/contents.service.spec.ts
  28. 6
      src/Squidex/app/shared/services/contents.service.ts
  29. 14
      src/Squidex/app/shared/state/contents.state.ts
  30. 33
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs
  31. 95
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs
  32. 37
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs
  33. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  34. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  35. 8
      tools/Migrate_01/MigrationPath.cs
  36. 42
      tools/Migrate_01/Migrations/MongoDb/CreateStatusColors.cs
  37. 7
      tools/Migrate_01/OldEvents/AppPlanChanged.cs
  38. 51
      tools/Migrate_01/OldEvents/ContentCreated.cs
  39. 60
      tools/Migrate_01/OldEvents/ContentStatusChanged.cs
  40. 52
      tools/Migrate_01/OldEvents/ContentStatusScheduled.cs

5
src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Contents
@ -16,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Contents
public static readonly Status Draft = new Status("Draft");
public static readonly Status Published = new Status("Published");
public const string FallbackColor = "#8091a5";
private readonly string name;
public string Name
@ -45,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Contents
public override string ToString()
{
return name;
return Name;
}
public static bool operator ==(Status lhs, Status rhs)

16
src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusColors
{
public const string Archived = "#eb3142";
public const string Draft = "#8091a5";
public const string Published = "#4bb958";
}
}

23
src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class StatusInfo
{
public Status Status { get; }
public string Color { get; }
public StatusInfo(Status status, string color)
{
Status = status;
Color = color;
}
}
}

4
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -53,6 +53,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonElement("ss")]
public Status Status { get; set; }
[BsonIgnoreIfNull]
[BsonElement("sc")]
public string StatusColor { get; set; }
[BsonIgnoreIfNull]
[BsonElement("do")]
[BsonJson]

9
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -19,15 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("AppContributorAssignedEvent",
"assigned {user:[Contributor]} as {[Role]}");
AddEventMessage("AppClientUpdatedEvent",
"updated client {[Id]}");
AddEventMessage("AppPlanChanged",
"changed plan to {[Plan]}");
AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as {[Role]}");

2
src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs

@ -38,6 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Status Status { get; set; }
public string StatusColor { get; set; }
public bool IsPending { get; set; }
}
}

26
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -83,9 +83,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data);
}
var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
var statusInfo = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
Create(c, status);
Create(c, statusInfo.Status, statusInfo.Color);
return Snapshot;
});
@ -127,6 +127,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
else
{
var statusInfo = await contentWorkflow.GetInfoAsync(c.Status);
StatusChange reason;
if (c.Status == Status.Published)
@ -144,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ctx.ExecuteScriptAsync(s => s.Change, reason, c, Snapshot.Data);
ChangeStatus(c, reason);
ChangeStatus(c, reason, statusInfo.Color);
}
}
}
@ -229,13 +231,21 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Snapshot;
}
public void Create(CreateContent command, Status status)
public void Create(CreateContent command, Status status, string color)
{
RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status }));
RaiseEvent(SimpleMapper.Map(command, new ContentCreated
{
Status = status,
StatusColor = color
}));
if (command.Publish)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged
{
Status = Status.Published,
StatusColor = StatusColors.Published
}));
}
}
@ -274,9 +284,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value }));
}
public void ChangeStatus(ChangeContentStatus command, StatusChange change)
public void ChangeStatus(ChangeContentStatus command, StatusChange change, string color)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change }));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change, StatusColor = color }));
}
private void RaiseEvent(SchemaEvent @event)

61
src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs

@ -11,42 +11,77 @@ using System.Linq;
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
{
public sealed class DefaultContentWorkflow : IContentWorkflow
{
private static readonly Status[] All = { Status.Archived, Status.Draft, Status.Published };
private static readonly StatusInfo InfoArchived = new StatusInfo(Status.Archived, StatusColors.Archived);
private static readonly StatusInfo InfoDraft = new StatusInfo(Status.Draft, StatusColors.Draft);
private static readonly StatusInfo InfoPublished = new StatusInfo(Status.Published, StatusColors.Published);
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
private static readonly StatusInfo[] All =
{
[Status.Draft] = new[] { Status.Archived, Status.Published },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
InfoArchived,
InfoDraft,
InfoPublished
};
public Task<Status> GetInitialStatusAsync(ISchemaEntity schema)
private static readonly Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> Flow =
new Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)>
{
[Status.Archived] = (InfoArchived, new[]
{
InfoDraft
}),
[Status.Draft] = (InfoDraft, new[]
{
InfoArchived,
InfoPublished
}),
[Status.Published] = (InfoPublished, new[]
{
InfoDraft,
InfoArchived
})
};
public Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema)
{
return Task.FromResult(Status.Draft);
var result = InfoDraft;
return Task.FromResult(result);
}
public Task<bool> CanMoveToAsync(IContentEntity content, Status next)
{
return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next));
var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next);
return Task.FromResult(result);
}
public Task<bool> CanUpdateAsync(IContentEntity content)
{
return Task.FromResult(content.Status != Status.Archived);
var result = content.Status != Status.Archived;
return Task.FromResult(result);
}
public Task<Status[]> GetNextsAsync(IContentEntity content)
public Task<StatusInfo> GetInfoAsync(Status status)
{
return Task.FromResult(Flow.TryGetValue(content.Status, out var result) ? result : Array.Empty<Status>());
var result = Flow[status].Info;
return Task.FromResult(result);
}
public Task<StatusInfo[]> GetNextsAsync(IContentEntity content)
{
var result = Flow.TryGetValue(content.Status, out var step) ? step.Transitions : Array.Empty<StatusInfo>();
return Task.FromResult(result);
}
public Task<Status[]> GetAllAsync(ISchemaEntity schema)
public Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema)
{
return Task.FromResult(All);
}

8
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs

@ -78,6 +78,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The the status of the {schemaName} content."
});
AddField(new FieldType
{
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.StatusColor),
Description = $"The color status of the {schemaName} content."
});
AddField(new FieldType
{
Name = "url",

2
src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs

@ -24,6 +24,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
Status Status { get; }
string StatusColor { get; }
ScheduleJob ScheduleJob { get; }
NamedContentData Data { get; }

8
src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -13,14 +13,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentWorkflow
{
Task<Status> GetInitialStatusAsync(ISchemaEntity schema);
Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> CanMoveToAsync(IContentEntity content, Status next);
Task<bool> CanUpdateAsync(IContentEntity content);
Task<Status[]> GetNextsAsync(IContentEntity content);
Task<StatusInfo> GetInfoAsync(Status status);
Task<Status[]> GetAllAsync(ISchemaEntity schema);
Task<StatusInfo[]> GetNextsAsync(IContentEntity content);
Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema);
}
}

11
src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs

@ -16,21 +16,24 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public Guid Id { get; }
public Instant DueTime { get; }
public Status Status { get; }
public RefToken ScheduledBy { get; }
public string StatusColor { get; set; }
public Instant DueTime { get; }
public RefToken ScheduledBy { get; }
public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime)
public ScheduleJob(Guid id, Status status, string statusColor, RefToken scheduledBy, Instant dueTime)
{
Id = id;
ScheduledBy = scheduledBy;
Status = status;
StatusColor = statusColor;
DueTime = dueTime;
}
public static ScheduleJob Build(Status status, RefToken scheduledBy, Instant dueTime)
public static ScheduleJob Build(Status status, string statusColor, RefToken scheduledBy, Instant dueTime)
{
return new ScheduleJob(Guid.NewGuid(), status, scheduledBy, dueTime);
}

10
src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -45,16 +45,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
[DataMember]
public Status Status { get; set; }
[DataMember]
public string StatusColor { get; set; }
protected void On(ContentCreated @event)
{
SimpleMapper.Map(@event, this);
UpdateData(null, @event.Data, false);
if (Status == default)
{
Status = Status.Draft;
}
}
protected void On(ContentChangesPublished @event)
@ -68,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
{
ScheduleJob = null;
Status = @event.Status;
SimpleMapper.Map(@event, this);
if (@event.Status == Status.Published)
{

6
src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs

@ -19,12 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("SchemaCreatedEvent",
"created schema {[Name]}.");
AddEventMessage("ScriptsConfiguredEvent",
"configured script of schema {[Name]}.");
AddEventMessage<SchemaFieldsReordered>(
"reordered fields of schema {[Name]}.");

4
src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs

@ -10,11 +10,13 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentCreated))]
[EventType(nameof(ContentCreated), 2)]
public sealed class ContentCreated : ContentEvent
{
public Status Status { get; set; }
public string StatusColor { get; set; }
public NamedContentData Data { get; set; }
}
}

4
src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs

@ -10,11 +10,13 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentStatusChanged))]
[EventType(nameof(ContentStatusChanged), 2)]
public sealed class ContentStatusChanged : ContentEvent
{
public StatusChange Change { get; set; }
public Status Status { get; set; }
public string StatusColor { get; set; }
}
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentStatusScheduled.cs

@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Events.Contents
{
public Status Status { get; set; }
public string StatusColor { get; set; }
public Instant DueTime { get; set; }
}
}

24
src/Squidex.Web/Resource.cs

@ -24,38 +24,38 @@ namespace Squidex.Web
AddGetLink("self", href);
}
public void AddGetLink(string rel, string href)
public void AddGetLink(string rel, string href, string metadata = null)
{
AddLink(rel, "GET", href);
AddLink(rel, "GET", href, metadata);
}
public void AddPatchLink(string rel, string href)
public void AddPatchLink(string rel, string href, string metadata = null)
{
AddLink(rel, "PATCH", href);
AddLink(rel, "PATCH", href, metadata);
}
public void AddPostLink(string rel, string href)
public void AddPostLink(string rel, string href, string metadata = null)
{
AddLink(rel, "POST", href);
AddLink(rel, "POST", href, metadata);
}
public void AddPutLink(string rel, string href)
public void AddPutLink(string rel, string href, string metadata = null)
{
AddLink(rel, "PUT", href);
AddLink(rel, "PUT", href, metadata);
}
public void AddDeleteLink(string rel, string href)
public void AddDeleteLink(string rel, string href, string metadata = null)
{
AddLink(rel, "DELETE", href);
AddLink(rel, "DELETE", href, metadata);
}
public void AddLink(string rel, string method, string href)
public void AddLink(string rel, string method, string href, string metadata = null)
{
Guard.NotNullOrEmpty(rel, nameof(rel));
Guard.NotNullOrEmpty(href, nameof(href));
Guard.NotNullOrEmpty(method, nameof(method));
Links[rel] = new ResourceLink { Href = href, Method = method };
Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata };
}
}
}

4
src/Squidex.Web/ResourceLink.cs

@ -18,5 +18,9 @@ namespace Squidex.Web
[Required]
[Display(Description = "The link method.")]
public string Method { get; set; }
[Required]
[Display(Description = "Additional data about the link.")]
public string Metadata { get; set; }
}
}

11
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -71,10 +71,15 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
public Instant LastModified { get; set; }
/// <summary>
/// The the status of the content.
/// The status of the content.
/// </summary>
public Status Status { get; set; }
/// <summary>
/// The color of the status.
/// </summary>
public string StatusColor { get; set; }
/// <summary>
/// The version of the content.
/// </summary>
@ -161,9 +166,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
foreach (var next in nextStatuses)
{
if (controller.HasPermission(Helper.StatusPermission(app, schema, next)))
if (controller.HasPermission(Helper.StatusPermission(app, schema, next.Status)))
{
AddPutLink($"status/{next}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values));
AddPutLink($"status/{next.Status}", controller.Url<ContentsController>(x => nameof(x.PutContentStatus), values), next.Color);
}
}

4
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// The possible statuses.
/// </summary>
[Required]
public Status[] Statuses { get; set; }
public StatusInfoDto[] Statuses { get; set; }
public string ToEtag()
{
@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
{
var allStatuses = await contentWorkflow.GetAllAsync(schema);
Statuses = allStatuses.ToArray();
Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray();
}
private async Task AssignContentsAsync(IContentWorkflow contentWorkflow, IResultList<IContentEntity> contents, QueryContext context, ApiController controller)

32
src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public sealed class StatusInfoDto
{
/// <summary>
/// The name of the status.
/// </summary>
[Required]
public Status Status { get; set; }
/// <summary>
/// The color of the status.
/// </summary>
[Required]
public string Color { get; set; }
public static StatusInfoDto FromStatusInfo(StatusInfo statusInfo)
{
return new StatusInfoDto { Status = statusInfo.Status, Color = statusInfo.Color };
}
}
}

3
src/Squidex/Config/Domain/StoreServices.cs

@ -67,6 +67,9 @@ namespace Squidex.Config.Domain
services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService<IMongoClient>().GetDatabase(mongoContentDatabaseName)))
.As<IMigration>();
services.AddTransientAs(c => new CreateStatusColors(c.GetRequiredService<IMongoClient>().GetDatabase(mongoContentDatabaseName)))
.As<IMigration>();
services.AddSingletonAs<MongoMigrationStatus>()
.As<IMigrationStatus>();

2
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html

@ -17,7 +17,7 @@
<a class="sidebar-item" *ngFor="let query of contentsState.statusQueries | async; trackBy: trackByQuery" (click)="search(query.filter)"
[class.active]="isSelectedQuery(query.filter)">
{{query.name}}
<i class="icon-circle" [style.color]="query.color"></i> {{query.name}}
</a>
</div>

2
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss

@ -3,4 +3,4 @@
.text-muted {
pointer-events: none;
}
}

2
src/Squidex/app/framework/utils/hateos.ts

@ -13,7 +13,7 @@ export interface Resource {
}
export type ResourceLinks = { [rel: string]: ResourceLink };
export type ResourceLink = { href: string; method: ResourceMethod; };
export type ResourceLink = { href: string; method: ResourceMethod; metadata?: string; };
export type Metadata = { [rel: string]: string };

6
src/Squidex/app/shared/services/contents.service.spec.ts

@ -62,11 +62,13 @@ describe('ContentsService', () => {
contentResponse(12),
contentResponse(13)
],
statuses: ['Draft', 'Published']
statuses: [{
status: 'Draft', color: 'Gray'
}]
});
expect(contents!).toEqual(
new ContentsDto(['Draft', 'Published'], 10, [
new ContentsDto([{ status: 'Draft', color: 'Gray' }], 10, [
createContent(12),
createContent(13)
]));

6
src/Squidex/app/shared/services/contents.service.ts

@ -34,9 +34,11 @@ export class ScheduleDto {
}
}
export type StatusInfo = { status: string; color: string; };
export class ContentsDto extends ResultSet<ContentDto> {
constructor(
public readonly statuses: string[],
public readonly statuses: StatusInfo[],
total: number,
items: ContentDto[],
links?: ResourceLinks
@ -133,7 +135,7 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`);
return this.http.get<{ total: number, items: [], statuses: string[] } & Resource>(url).pipe(
return this.http.get<{ total: number, items: [], statuses: StatusInfo[] } & Resource>(url).pipe(
map(({ total, items, statuses, _links }) => {
const contents = items.map(x => parseContent(x));

14
src/Squidex/app/shared/state/contents.state.ts

@ -24,7 +24,7 @@ import { SchemaDto } from './../services/schemas.service';
import { AppsState } from './apps.state';
import { SchemasState } from './schemas.state';
import { ContentDto, ContentsService } from './../services/contents.service';
import { ContentDto, ContentsService, StatusInfo } from './../services/contents.service';
interface Snapshot {
// The current comments.
@ -40,7 +40,7 @@ interface Snapshot {
isLoaded?: boolean;
// The statuses.
statuses?: string[];
statuses?: StatusInfo[];
// The selected content.
selectedContent?: ContentDto | null;
@ -348,10 +348,12 @@ export class ManualContentsState extends ContentsStateBase {
}
}
function buildQueries(x: string[] | undefined): { name: string; filter: string; }[] {
return x ? x.map(s => buildQuery(s)) : [];
export type ContentQuery = { color: string; name: string; filter: string; };
function buildQueries(statuses: StatusInfo[] | undefined): ContentQuery[] {
return statuses ? statuses.map(s => buildQuery(s)) : [];
}
function buildQuery(s: string) {
return ({ name: s, filter: `$filter=status eq '${s}'` });
function buildQuery(s: StatusInfo) {
return ({ name: s.status, color: s.color, filter: `$filter=status eq '${s}'` });
}

33
tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs

@ -12,40 +12,49 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
{
public class StatusTests
{
[Fact]
public void Should_initialize_default()
{
Status status = default;
Assert.Equal("Unknown", status.Name);
Assert.Equal("Unknown", status.ToString());
}
[Fact]
public void Should_initialize_status_from_string()
{
var result = new Status("Custom");
var status = new Status("Custom");
Assert.Equal("Custom", result.Name);
Assert.Equal("Custom", result.ToString());
Assert.Equal("Custom", status.Name);
Assert.Equal("Custom", status.ToString());
}
[Fact]
public void Should_provide_draft_status()
{
var result = Status.Draft;
var status = Status.Draft;
Assert.Equal("Draft", result.Name);
Assert.Equal("Draft", result.ToString());
Assert.Equal("Draft", status.Name);
Assert.Equal("Draft", status.ToString());
}
[Fact]
public void Should_provide_archived_status()
{
var result = Status.Archived;
var status = Status.Archived;
Assert.Equal("Archived", result.Name);
Assert.Equal("Archived", result.ToString());
Assert.Equal("Archived", status.Name);
Assert.Equal("Archived", status.ToString());
}
[Fact]
public void Should_provide_published_status()
{
var result = Status.Published;
var status = Status.Published;
Assert.Equal("Published", result.Name);
Assert.Equal("Published", result.ToString());
Assert.Equal("Published", status.Name);
Assert.Equal("Published", status.ToString());
}
[Fact]

95
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IContentRepository contentRepository = A.Dummy<IContentRepository>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow()));
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE);
@ -102,12 +102,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.ReturnsLazily(x => x.GetArgument<ScriptContext>(0).Data);
A.CallTo(() => contentWorkflow.CanUpdateAsync(A<IContentEntity>.Ignored))
.Returns(true);
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>.Ignored, A<Status>.Ignored))
.Returns(true);
patched = patch.MergeInto(data);
sut = new ContentGrain(Store, A.Dummy<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), scriptEngine, contentWorkflow, contentRepository);
@ -132,9 +126,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Draft, sut.Snapshot.Status);
Assert.Equal(StatusColors.Draft, sut.Snapshot.StatusColor);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft, StatusColor = StatusColors.Draft })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>"))
@ -143,26 +140,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
.MustNotHaveHappened();
}
[Fact]
public async Task Create_should_create_events_and_update_state_with_custom_initial_status()
{
var command = new CreateContent { Data = data };
A.CallTo(() => contentWorkflow.GetInitialStatusAsync(schema))
.Returns(Status.Archived);
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Archived, sut.Snapshot.Status);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Archived })
);
}
[Fact]
public async Task Create_should_also_publish()
{
@ -172,10 +149,22 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Published, sut.Snapshot.Status);
Assert.Equal(StatusColors.Published, sut.Snapshot.StatusColor);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data }),
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
CreateContentEvent(new ContentCreated
{
Data = data,
Status = Status.Draft,
StatusColor = StatusColors.Draft
}),
CreateContentEvent(new ContentStatusChanged
{
Status = Status.Published,
StatusColor = StatusColors.Published
})
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>"))
@ -246,10 +235,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustNotHaveHappened();
@ -319,10 +305,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.ShouldBeEquivalent(sut.Snapshot);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustNotHaveHappened();
@ -343,7 +326,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })
CreateContentEvent(new ContentStatusChanged
{
Change = StatusChange.Published,
Status = Status.Published,
StatusColor = StatusColors.Published
})
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
@ -365,7 +353,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
CreateContentEvent(new ContentStatusChanged
{
Status = Status.Archived,
StatusColor = StatusColors.Archived
})
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
@ -388,7 +380,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished })
CreateContentEvent(new ContentStatusChanged
{
Change = StatusChange.Unpublished,
Status = Status.Draft,
StatusColor = StatusColors.Draft
})
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
@ -411,7 +408,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft })
CreateContentEvent(new ContentStatusChanged
{
Status = Status.Draft,
StatusColor = StatusColors.Draft
})
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
@ -472,11 +473,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task ChangeStatus_should_refresh_properties_and_revert_scheduling_when_invoked_by_scheduler()
{
await ExecuteCreateAsync();
await ExecuteScheduledAsync();
await ExecuteChangeStatusAsync(Status.Published, Instant.MaxValue);
var command = new ChangeContentStatus { Status = Status.Draft, JobId = sut.Snapshot.ScheduleJob.Id };
var command = new ChangeContentStatus { Status = Status.Published, JobId = sut.Snapshot.ScheduleJob.Id };
A.CallTo(() => contentWorkflow.CanMoveToAsync(sut.Snapshot, command.Status))
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>.Ignored, Status.Published))
.Returns(false);
var result = await sut.ExecuteAsync(CreateContentCommand(command));
@ -549,9 +550,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = otherData, AsDraft = true }));
}
private Task ExecuteScheduledAsync()
private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null)
{
return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = Status.Published, DueTime = Instant.MaxValue }));
return sut.ExecuteAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime }));
}
private Task ExecuteDeleteAsync()

37
tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs

@ -7,6 +7,7 @@
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Xunit;
@ -19,9 +20,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_draft_as_initial_status()
{
var expected = new StatusInfo(Status.Draft, StatusColors.Draft);
var result = await sut.GetInitialStatusAsync(null);
Assert.Equal(Status.Draft, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
@ -69,11 +72,15 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = CreateContent(Status.Draft);
var expected = new[] { Status.Archived, Status.Published };
var expected = new[]
{
new StatusInfo(Status.Archived, StatusColors.Archived),
new StatusInfo(Status.Published, StatusColors.Published)
};
var result = await sut.GetNextsAsync(content);
Assert.Equal(expected, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
@ -81,11 +88,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = CreateContent(Status.Archived);
var expected = new[] { Status.Draft };
var expected = new[]
{
new StatusInfo(Status.Draft, StatusColors.Draft)
};
var result = await sut.GetNextsAsync(content);
Assert.Equal(expected, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
@ -93,21 +103,30 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = CreateContent(Status.Published);
var expected = new[] { Status.Draft, Status.Archived };
var expected = new[]
{
new StatusInfo(Status.Archived, StatusColors.Archived),
new StatusInfo(Status.Draft, StatusColors.Draft)
};
var result = await sut.GetNextsAsync(content);
Assert.Equal(expected, result);
result.Should().BeEquivalentTo(expected);
}
[Fact]
public async Task Should_return_all_statuses()
{
var expected = new[] { Status.Archived, Status.Draft, Status.Published };
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(null);
Assert.Equal(expected, result);
result.Should().BeEquivalentTo(expected);
}
private IContentEntity CreateContent(Status status)

6
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -262,6 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
@ -320,6 +321,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
@ -406,6 +408,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
@ -462,6 +465,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
@ -605,6 +609,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
@ -653,6 +658,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{

3
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -157,7 +157,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
Data = data,
DataDraft = dataDraft,
Status = Status.Draft
Status = Status.Draft,
StatusColor = "red"
};
return content;

8
tools/Migrate_01/MigrationPath.cs

@ -17,7 +17,7 @@ namespace Migrate_01
{
public sealed class MigrationPath : IMigrationPath
{
private const int CurrentVersion = 17;
private const int CurrentVersion = 18;
private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider)
@ -115,6 +115,12 @@ namespace Migrate_01
yield return serviceProvider.GetService<RenameSlugField>();
}
// Version 18: Status colors introduced
if (version < 17)
{
yield return serviceProvider.GetService<CreateStatusColors>();
}
yield return serviceProvider.GetRequiredService<StartEventConsumers>();
}
}

42
tools/Migrate_01/Migrations/MongoDb/CreateStatusColors.cs

@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Migrations;
namespace Migrate_01.Migrations.MongoDb
{
public sealed class CreateStatusColors : IMigration
{
private readonly IMongoDatabase contentDatabase;
public CreateStatusColors(IMongoDatabase contentDatabase)
{
this.contentDatabase = contentDatabase;
}
public async Task UpdateAsync()
{
var collection = contentDatabase.GetCollection<BsonDocument>("State_Contents");
await collection.UpdateManyAsync(
Builders<BsonDocument>.Filter.Eq("ss", "Archived"),
Builders<BsonDocument>.Update.Set("sc", StatusColors.Archived));
await collection.UpdateManyAsync(
Builders<BsonDocument>.Filter.Eq("ss", "Draft"),
Builders<BsonDocument>.Update.Set("sc", StatusColors.Draft));
await collection.UpdateManyAsync(
Builders<BsonDocument>.Filter.Eq("ss", "Published"),
Builders<BsonDocument>.Update.Set("sc", StatusColors.Published));
}
}
}

7
tools/Migrate_01/OldEvents/AppPlanChangedOld.cs → tools/Migrate_01/OldEvents/AppPlanChanged.cs

@ -5,16 +5,19 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using AppPlanChangedV2 = Squidex.Domain.Apps.Events.Apps.AppPlanChanged;
namespace Migrate_01.OldEvents
{
[TypeName("AppPlanChanged")]
public sealed class AppPlanChangedOld : AppEvent, IMigrated<IEvent>
[Obsolete]
public sealed class AppPlanChanged : AppEvent, IMigrated<IEvent>
{
public string PlanId { get; set; }
@ -22,7 +25,7 @@ namespace Migrate_01.OldEvents
{
if (!string.IsNullOrWhiteSpace(PlanId))
{
return SimpleMapper.Map(this, new AppPlanChanged());
return SimpleMapper.Map(this, new AppPlanChangedV2());
}
else
{

51
tools/Migrate_01/OldEvents/ContentCreated.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using ContentCreatedV2 = Squidex.Domain.Apps.Events.Contents.ContentCreated;
namespace Migrate_01.OldEvents
{
[EventType(nameof(ContentCreated))]
[Obsolete]
public sealed class ContentCreated : ContentEvent, IMigrated<IEvent>
{
public Status Status { get; set; }
public NamedContentData Data { get; set; }
public IEvent Migrate()
{
var migrated = SimpleMapper.Map(this, new ContentCreatedV2());
if (migrated.Status == default)
{
migrated.Status = Status.Draft;
}
if (Status == Status.Archived)
{
migrated.StatusColor = StatusColors.Archived;
}
else if (Status == Status.Published)
{
migrated.StatusColor = StatusColors.Published;
}
else
{
migrated.StatusColor = StatusColors.Draft;
}
return this;
}
}
}

60
tools/Migrate_01/OldEvents/ContentStatusChanged.cs

@ -0,0 +1,60 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using ContentStatusChangedV2 = Squidex.Domain.Apps.Events.Contents.ContentStatusChanged;
namespace Migrate_01.OldEvents
{
[EventType(nameof(ContentStatusChanged))]
[Obsolete]
public sealed class ContentStatusChanged : ContentEvent, IMigrated<IEvent>
{
public string Change { get; set; }
public Status Status { get; set; }
public IEvent Migrate()
{
var migrated = SimpleMapper.Map(this, new ContentStatusChangedV2());
if (migrated.Status == default)
{
migrated.Status = Status.Draft;
}
if (Enum.TryParse<StatusChange>(Change, out var result))
{
migrated.Change = result;
}
else
{
migrated.Change = StatusChange.Change;
}
if (Status == Status.Archived)
{
migrated.StatusColor = StatusColors.Archived;
}
else if (Status == Status.Published)
{
migrated.StatusColor = StatusColors.Published;
}
else
{
migrated.StatusColor = StatusColors.Draft;
}
return this;
}
}
}

52
tools/Migrate_01/OldEvents/ContentStatusScheduled.cs

@ -0,0 +1,52 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using ContentStatusScheduledV2 = Squidex.Domain.Apps.Events.Contents.ContentStatusScheduled;
namespace Migrate_01.OldEvents
{
[EventType(nameof(ContentStatusScheduled))]
[Obsolete]
public sealed class ContentStatusScheduled : ContentEvent, IMigrated<IEvent>
{
public Status Status { get; set; }
public Instant DueTime { get; set; }
public IEvent Migrate()
{
var migrated = SimpleMapper.Map(this, new ContentStatusScheduledV2());
if (migrated.Status == default)
{
migrated.Status = Status.Draft;
}
if (Status == Status.Archived)
{
migrated.StatusColor = StatusColors.Archived;
}
else if (Status == Status.Published)
{
migrated.StatusColor = StatusColors.Published;
}
else
{
migrated.StatusColor = StatusColors.Draft;
}
return this;
}
}
}
Loading…
Cancel
Save