Browse Source

Simplified content status system.

pull/111/head
Sebastian Stehle 9 years ago
parent
commit
7c7a066e15
  1. 9
      src/Squidex.Domain.Apps.Core/Contents/Status.cs
  2. 33
      src/Squidex.Domain.Apps.Core/Contents/StatusFlow.cs
  3. 2
      src/Squidex.Domain.Apps.Core/Webhooks/WebhookSchema.cs
  4. 19
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs
  5. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentArchived.cs
  6. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentPublished.cs
  7. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentRestored.cs
  8. 2
      src/Squidex.Domain.Apps.Events/Contents/Old/ContentUnpublished.cs
  9. 13
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs
  10. 9
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs
  11. 25
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  12. 14
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs
  13. 5
      src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs
  14. 33
      src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs
  15. 5
      src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs
  16. 5
      src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs
  17. 12
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs
  18. 9
      src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs
  19. 14
      src/Squidex.Domain.Apps.Write/Contents/Commands/PublishContent.cs
  20. 14
      src/Squidex.Domain.Apps.Write/Contents/Commands/UnpublishContent.cs
  21. 47
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs
  22. 79
      src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs
  23. 5
      src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs
  24. 8
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  25. 11
      src/Squidex/Controllers/ContentApi/Models/ContentDto.cs
  26. 2
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  27. 12
      src/Squidex/app/features/content/shared/content-item.component.html
  28. 15
      src/Squidex/app/features/webhooks/pages/webhook.component.html
  29. 17
      src/Squidex/app/features/webhooks/pages/webhook.component.ts
  30. 44
      src/Squidex/app/shared/services/contents.service.spec.ts
  31. 51
      src/Squidex/app/shared/services/contents.service.ts
  32. 14
      src/Squidex/app/shared/services/webhooks.service.spec.ts
  33. 6
      src/Squidex/app/shared/services/webhooks.service.ts
  34. 34
      tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs
  35. 44
      tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs
  36. 13
      tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs
  37. 60
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs
  38. 139
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs
  39. 2
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs

9
src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs → src/Squidex.Domain.Apps.Core/Contents/Status.cs

@ -1,14 +1,17 @@
// ========================================================================== // ==========================================================================
// RestoreContent.cs // Status.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands namespace Squidex.Domain.Apps.Core.Contents
{ {
public sealed class RestoreContent : ContentCommand public enum Status
{ {
Draft,
Archived,
Published
} }
} }

33
src/Squidex.Domain.Apps.Core/Contents/StatusFlow.cs

@ -0,0 +1,33 @@
// ==========================================================================
// StatusFlow.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusFlow
{
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[Status.Draft] = new[] { Status.Published, Status.Archived },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
};
public static bool Exists(Status status)
{
return Flow.ContainsKey(status);
}
public static bool CanChange(Status status, Status toStatus)
{
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus);
}
}
}

2
src/Squidex.Domain.Apps.Core/Webhooks/WebhookSchema.cs

@ -21,7 +21,5 @@ namespace Squidex.Domain.Apps.Core.Webhooks
public bool SendDelete { get; set; } public bool SendDelete { get; set; }
public bool SendPublish { get; set; } public bool SendPublish { get; set; }
public bool SendUnpublish { get; set; }
} }
} }

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

@ -0,0 +1,19 @@
// ==========================================================================
// ContentStatusChanged.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentStatusChanged))]
public sealed class ContentStatusChanged : ContentEvent
{
public Status Status { get; set; }
}
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentArchived.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentArchived.cs

@ -6,11 +6,13 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents namespace Squidex.Domain.Apps.Events.Contents
{ {
[EventType(nameof(ContentArchived))] [EventType(nameof(ContentArchived))]
[Obsolete]
public sealed class ContentArchived : ContentEvent public sealed class ContentArchived : ContentEvent
{ {
} }

2
src/Squidex.Domain.Apps.Events/Contents/ContentPublished.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentPublished.cs

@ -6,11 +6,13 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents namespace Squidex.Domain.Apps.Events.Contents
{ {
[EventType(nameof(ContentPublished))] [EventType(nameof(ContentPublished))]
[Obsolete]
public sealed class ContentPublished : ContentEvent public sealed class ContentPublished : ContentEvent
{ {
} }

2
src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentRestored.cs

@ -6,11 +6,13 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents namespace Squidex.Domain.Apps.Events.Contents
{ {
[EventType(nameof(ContentRestored))] [EventType(nameof(ContentRestored))]
[Obsolete]
public sealed class ContentRestored : ContentEvent public sealed class ContentRestored : ContentEvent
{ {
} }

2
src/Squidex.Domain.Apps.Events/Contents/ContentUnpublished.cs → src/Squidex.Domain.Apps.Events/Contents/Old/ContentUnpublished.cs

@ -6,11 +6,13 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Events.Contents namespace Squidex.Domain.Apps.Events.Contents
{ {
[EventType(nameof(ContentUnpublished))] [EventType(nameof(ContentUnpublished))]
[Obsolete]
public sealed class ContentUnpublished : ContentEvent public sealed class ContentUnpublished : ContentEvent
{ {
} }

13
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs

@ -35,6 +35,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
[BsonRepresentation(BsonType.String)] [BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } public Guid Id { get; set; }
[BsonRequired]
[BsonElement("st")]
[BsonRepresentation(BsonType.String)]
public Status Status { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("ct")] [BsonElement("ct")]
public Instant Created { get; set; } public Instant Created { get; set; }
@ -43,14 +48,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
[BsonElement("mt")] [BsonElement("mt")]
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
[BsonRequired]
[BsonElement("pu")]
public bool IsPublished { get; set; }
[BsonRequired]
[BsonElement("dl")]
public bool IsArchived { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("dt")] [BsonElement("dt")]
public string DataText { get; set; } public string DataText { get; set; }

9
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs

@ -13,6 +13,7 @@ using System.Threading.Tasks;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.Contents.Repositories;
@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
this.database = database; this.database = database;
} }
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery) public async Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery)
{ {
var collection = GetCollection(app.Id); var collection = GetCollection(app.Id);
@ -80,7 +81,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
cursor = cursor =
collection collection
.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived) .Find(odataQuery, ids, schema.Id, schema.SchemaDef, status)
.Take(odataQuery) .Take(odataQuery)
.Skip(odataQuery) .Skip(odataQuery)
.Sort(odataQuery, schema.SchemaDef); .Sort(odataQuery, schema.SchemaDef);
@ -104,14 +105,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
return entities; return entities;
} }
public Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery) public Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery)
{ {
var collection = GetCollection(app.Id); var collection = GetCollection(app.Id);
IFindFluent<MongoContentEntity, MongoContentEntity> cursor; IFindFluent<MongoContentEntity, MongoContentEntity> cursor;
try try
{ {
cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, nonPublished, archived); cursor = collection.Find(odataQuery, ids, schema.Id, schema.SchemaDef, status);
} }
catch (NotSupportedException) catch (NotSupportedException)
{ {

25
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -9,6 +9,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
@ -17,6 +18,8 @@ using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
#pragma warning disable CS0612 // Type or member is obsolete
namespace Squidex.Domain.Apps.Read.MongoDb.Contents namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
public partial class MongoContentRepository public partial class MongoContentRepository
@ -61,8 +64,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsPublished)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Status));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsArchived));
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText));
}); });
} }
@ -99,7 +101,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
return collection.UpdateAsync(@event, headers, x => return collection.UpdateAsync(@event, headers, x =>
{ {
x.IsPublished = true; x.Status = Status.Published;
}); });
}); });
} }
@ -110,7 +112,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
return collection.UpdateAsync(@event, headers, x => return collection.UpdateAsync(@event, headers, x =>
{ {
x.IsPublished = false; x.Status = Status.Draft;
}); });
}); });
} }
@ -121,7 +123,18 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
return collection.UpdateAsync(@event, headers, x => return collection.UpdateAsync(@event, headers, x =>
{ {
x.IsArchived = true; x.Status = Status.Archived;
});
});
}
protected Task On(ContentStatusChanged @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, collection =>
{
return collection.UpdateAsync(@event, headers, x =>
{
x.Status = @event.Status;
}); });
}); });
} }
@ -132,7 +145,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
{ {
return collection.UpdateAsync(@event, headers, x => return collection.UpdateAsync(@event, headers, x =>
{ {
x.IsArchived = false; x.Status = Status.Draft;
}); });
}); });
} }

14
src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs

@ -11,6 +11,7 @@ using System.Collections.Generic;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
@ -56,26 +57,21 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
return cursor; return cursor;
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished, bool archived) public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, Status[] status)
{ {
var filter = BuildQuery(query, ids, schemaId, schema, nonPublished, archived); var filter = BuildQuery(query, ids, schemaId, schema, status);
return cursor.Find(filter); return cursor.Find(filter);
} }
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, bool nonPublished, bool archived) public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, HashSet<Guid> ids, Guid schemaId, Schema schema, Status[] status)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
Filter.Eq(x => x.SchemaId, schemaId), Filter.Eq(x => x.SchemaId, schemaId),
Filter.Eq(x => x.IsArchived, archived) Filter.In(x => x.Status, status)
}; };
if (!nonPublished)
{
filters.Add(Filter.Eq(x => x.IsPublished, true));
}
if (ids != null && ids.Count > 0) if (ids != null && ids.Count > 0)
{ {
filters.Add(Filter.In(x => x.Id, ids)); filters.Add(Filter.In(x => x.Id, ids));

5
src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs

@ -12,6 +12,8 @@ using Squidex.Domain.Apps.Read.History;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
#pragma warning disable CS0612 // Type or member is obsolete
namespace Squidex.Domain.Apps.Read.Contents namespace Squidex.Domain.Apps.Read.Contents
{ {
public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase
@ -36,6 +38,9 @@ namespace Squidex.Domain.Apps.Read.Contents
AddEventMessage<ContentUnpublished>( AddEventMessage<ContentUnpublished>(
"unpublished content item."); "unpublished content item.");
AddEventMessage<ContentStatusChanged>(
"change status of content item to {[Status]}.");
} }
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event) protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)

33
src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs

@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Read.Contents
var content = await contentRepository.FindContentAsync(app, schema, id); var content = await contentRepository.FindContentAsync(app, schema, id);
if (content == null || (!content.IsPublished && !isFrontendClient)) if (content == null || (content.Status != Status.Published && !isFrontendClient))
{ {
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity));
} }
@ -83,10 +83,27 @@ namespace Squidex.Domain.Apps.Read.Contents
var parsedQuery = ParseQuery(app, query, schema); var parsedQuery = ParseQuery(app, query, schema);
var isFrontendClient = user.IsInClient("squidex-frontend"); var status = new List<Status>();
var taskForItems = contentRepository.QueryAsync(app, schema, isFrontendClient, archived, ids, parsedQuery); if (user.IsInClient("squidex-frontend"))
var taskForCount = contentRepository.CountAsync(app, schema, isFrontendClient, archived, ids, parsedQuery); {
if (archived)
{
status.Add(Status.Archived);
}
else
{
status.Add(Status.Draft);
status.Add(Status.Published);
}
}
else
{
status.Add(Status.Published);
}
var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), ids, parsedQuery);
var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), ids, parsedQuery);
await Task.WhenAll(taskForItems, taskForCount); await Task.WhenAll(taskForItems, taskForCount);
@ -155,14 +172,18 @@ namespace Squidex.Domain.Apps.Read.Contents
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid AppId { get; set; } public Guid AppId { get; set; }
public long Version { get; set; } public long Version { get; set; }
public bool IsArchived { get; set; }
public bool IsPublished { get; set; }
public Instant Created { get; set; } public Instant Created { get; set; }
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; } public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; } public RefToken LastModifiedBy { get; set; }
public NamedContentData Data { get; set; } public NamedContentData Data { get; set; }
public Status Status { get; set; }
} }
} }
} }

5
src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs

@ -5,6 +5,7 @@
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -12,9 +13,7 @@ namespace Squidex.Domain.Apps.Read.Contents
{ {
public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion
{ {
bool IsPublished { get; } Status Status { get; }
bool IsArchived { get; }
NamedContentData Data { get; } NamedContentData Data { get; }
} }

5
src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas;
@ -17,11 +18,11 @@ namespace Squidex.Domain.Apps.Read.Contents.Repositories
{ {
public interface IContentRepository public interface IContentRepository
{ {
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery); Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds); Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds);
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, bool nonPublished, bool archived, HashSet<Guid> ids, ODataUriParser odataQuery); Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, ODataUriParser odataQuery);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id);
} }

12
src/Squidex.Domain.Apps.Read/Webhooks/WebhookEnqueuer.cs

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Webhooks; using Squidex.Domain.Apps.Core.Webhooks;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
@ -115,12 +116,11 @@ namespace Squidex.Domain.Apps.Read.Webhooks
private static bool Matchs(WebhookSchema webhookSchema, SchemaEvent @event) private static bool Matchs(WebhookSchema webhookSchema, SchemaEvent @event)
{ {
return return
(@event.SchemaId.Id == webhookSchema.SchemaId) && (@event.SchemaId.Id == webhookSchema.SchemaId) &&
(@event is ContentCreated && webhookSchema.SendCreate || (webhookSchema.SendCreate && @event is ContentCreated ||
@event is ContentUpdated && webhookSchema.SendUpdate || webhookSchema.SendUpdate && @event is ContentUpdated ||
@event is ContentDeleted && webhookSchema.SendDelete || webhookSchema.SendDelete && @event is ContentDeleted ||
@event is ContentPublished && webhookSchema.SendPublish || webhookSchema.SendPublish && @event is ContentStatusChanged statusChanged && statusChanged.Status == Status.Published);
@event is ContentUnpublished && webhookSchema.SendUnpublish);
} }
private string CreatePayload(Envelope<IEvent> @event, string eventType) private string CreatePayload(Envelope<IEvent> @event, string eventType)

9
src/Squidex.Domain.Apps.Write/Contents/Commands/ArchiveContent.cs → src/Squidex.Domain.Apps.Write/Contents/Commands/ChangeContentStatus.cs

@ -1,14 +1,17 @@
// ========================================================================== // ==========================================================================
// ArchiveContent.cs // ChangeContentStatus.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // =========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Write.Contents.Commands namespace Squidex.Domain.Apps.Write.Contents.Commands
{ {
public sealed class ArchiveContent : ContentCommand public sealed class ChangeContentStatus : ContentCommand
{ {
public Status Status { get; set; }
} }
} }

14
src/Squidex.Domain.Apps.Write/Contents/Commands/PublishContent.cs

@ -1,14 +0,0 @@
// ==========================================================================
// PublishContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public sealed class PublishContent : ContentCommand
{
}
}

14
src/Squidex.Domain.Apps.Write/Contents/Commands/UnpublishContent.cs

@ -1,14 +0,0 @@
// ==========================================================================
// UnpublishContent.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public sealed class UnpublishContent : ContentCommand
{
}
}

47
src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs

@ -111,55 +111,16 @@ namespace Squidex.Domain.Apps.Write.Contents
}); });
} }
protected Task On(PublishContent command, CommandContext context) protected Task On(ChangeContentStatus command, CommandContext context)
{ {
return handler.UpdateAsync<ContentDomainObject>(context, async content => return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{ {
var schemaAndApp = await ResolveSchemaAndAppAsync(command); var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Publish"); var scriptContext = CreateScriptContext(content, command, command.Status.ToString());
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "publish content"); scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "change content status");
content.Publish(command); content.ChangeStatus(command);
});
}
protected Task On(UnpublishContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Unpublish");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "unpublish content");
content.Unpublish(command);
});
}
protected Task On(ArchiveContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Archive");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "archive content");
content.Archive(command);
});
}
protected Task On(RestoreContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, "Restore");
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptChange, "restore content");
content.Restore(command);
}); });
} }

79
src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs

@ -22,8 +22,7 @@ namespace Squidex.Domain.Apps.Write.Contents
{ {
private bool isDeleted; private bool isDeleted;
private bool isCreated; private bool isCreated;
private bool isPublished; private Status status;
private bool isArchived;
private NamedContentData data; private NamedContentData data;
public bool IsDeleted public bool IsDeleted
@ -31,14 +30,9 @@ namespace Squidex.Domain.Apps.Write.Contents
get { return isDeleted; } get { return isDeleted; }
} }
public bool IsArchived public Status Status
{ {
get { return isArchived; } get { return status; }
}
public bool IsPublished
{
get { return isPublished; }
} }
public NamedContentData Data public NamedContentData Data
@ -63,24 +57,9 @@ namespace Squidex.Domain.Apps.Write.Contents
data = @event.Data; data = @event.Data;
} }
protected void On(ContentPublished @event) protected void On(ContentStatusChanged @event)
{
isPublished = true;
}
protected void On(ContentUnpublished @event)
{
isPublished = false;
}
protected void On(ContentArchived @event)
{
isArchived = true;
}
protected void On(ContentRestored @event)
{ {
isArchived = false; status = @event.Status;
} }
protected void On(ContentDeleted @event) protected void On(ContentDeleted @event)
@ -98,7 +77,7 @@ namespace Squidex.Domain.Apps.Write.Contents
if (command.Publish) if (command.Publish)
{ {
RaiseEvent(SimpleMapper.Map(command, new ContentPublished())); RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
} }
return this; return this;
@ -115,46 +94,14 @@ namespace Squidex.Domain.Apps.Write.Contents
return this; return this;
} }
public ContentDomainObject Restore(RestoreContent command) public ContentDomainObject ChangeStatus(ChangeContentStatus command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentRestored()));
return this;
}
public ContentDomainObject Archive(ArchiveContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentArchived()));
return this;
}
public ContentDomainObject Publish(PublishContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentPublished()));
return this;
}
public ContentDomainObject Unpublish(UnpublishContent command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted(); VerifyCreatedAndNotDeleted();
VerifyCanChangeStatus(command.Status);
RaiseEvent(SimpleMapper.Map(command, new ContentUnpublished())); RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged()));
return this; return this;
} }
@ -189,6 +136,14 @@ namespace Squidex.Domain.Apps.Write.Contents
return this; return this;
} }
private void VerifyCanChangeStatus(Status newStatus)
{
if (!StatusFlow.Exists(newStatus) && !StatusFlow.CanChange(status, newStatus))
{
throw new DomainException($"Content cannot be changed from status {status} to {newStatus}.");
}
}
private void VerifyNotCreated() private void VerifyNotCreated()
{ {
if (isCreated) if (isCreated)

5
src/Squidex/Controllers/Api/Webhooks/Models/WebhookSchemaDto.cs

@ -36,10 +36,5 @@ namespace Squidex.Controllers.Api.Webhooks.Models
/// True, when to send a message for published events. /// True, when to send a message for published events.
/// </summary> /// </summary>
public bool SendPublish { get; set; } public bool SendPublish { get; set; }
/// <summary>
/// True, when to send a message for unpublished events.
/// </summary>
public bool SendUnpublish { get; set; }
} }
} }

8
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -206,7 +206,7 @@ namespace Squidex.Controllers.ContentApi
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new PublishContent { ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Published, ContentId = id, User = User };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -221,7 +221,7 @@ namespace Squidex.Controllers.ContentApi
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new UnpublishContent { ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id, User = User };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -236,7 +236,7 @@ namespace Squidex.Controllers.ContentApi
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new ArchiveContent { ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Archived, ContentId = id, User = User };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -251,7 +251,7 @@ namespace Squidex.Controllers.ContentApi
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new RestoreContent { ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id, User = User };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);

11
src/Squidex/Controllers/ContentApi/Models/ContentDto.cs

@ -52,14 +52,9 @@ namespace Squidex.Controllers.ContentApi.Models
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
/// <summary> /// <summary>
/// Indicates if the content item is published. /// Gets the status of the content.
/// </summary> /// </summary>
public bool? IsPublished { get; set; } public Status Status { get; set; }
/// <summary>
/// Indicates if the content item is archived.
/// </summary>
public bool IsArchived { get; set; }
/// <summary> /// <summary>
/// The version of the content. /// The version of the content.
@ -79,7 +74,7 @@ namespace Squidex.Controllers.ContentApi.Models
CreatedBy = command.Actor, CreatedBy = command.Actor,
LastModified = now, LastModified = now,
LastModifiedBy = command.Actor, LastModifiedBy = command.Actor,
IsPublished = command.Publish Status = command.Publish ? Status.Published : Status.Draft
}; };
return response; return response;

2
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -233,7 +233,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
} }
} }
if (this.content.isArchived) { if (this.content.status === 'Archived') {
this.contentForm.disable(); this.contentForm.disable();
} }
} }

12
src/Squidex/app/features/content/shared/content-item.component.html

@ -4,7 +4,7 @@
</span> </span>
</td> </td>
<td> <td>
<span class="item-published" [class.unpublished]="!content.isPublished"></span> <span class="item-published" [class.unpublished]="content.status !== 'Published'"></span>
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small> <small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td> </td>
@ -17,19 +17,19 @@
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]> <div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="!content.isPublished && !content.isArchived"> <a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Draft'">
Publish Publish
</a> </a>
<a class="dropdown-item" (click)="unpublishing.emit(); $event.stopPropagation()" *ngIf="content.isPublished && !content.isArchived"> <a class="dropdown-item" (click)="unpublishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Published'">
Unpublish Unpublish
</a> </a>
<a class="dropdown-item" (click)="archiving.emit(); $event.stopPropagation()" *ngIf="!content.isArchived"> <a class="dropdown-item" (click)="archiving.emit(); $event.stopPropagation()" *ngIf="content.status !== 'Archived'">
Archive Archive
</a> </a>
<a class="dropdown-item" (click)="restoring.emit(); $event.stopPropagation()" *ngIf="content.isArchived"> <a class="dropdown-item" (click)="restoring.emit(); $event.stopPropagation()" *ngIf="content.status === 'Archived'">
Restore Restore
</a> </a>
<a class="dropdown-item dropdown-item-delete" *ngIf="content.isArchived" <a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="deleting.emit()" (sqxConfirmClick)="deleting.emit()"
confirmTitle="Delete content" confirmTitle="Delete content"
confirmText="Do you really want to delete the content?"> confirmText="Do you really want to delete the content?">

15
src/Squidex/app/features/webhooks/pages/webhook.component.html

@ -55,7 +55,6 @@
<col style="width: 40px" /> <col style="width: 40px" />
<col style="width: 40px" /> <col style="width: 40px" />
<col style="width: 40px" /> <col style="width: 40px" />
<col style="width: 40px" />
</colgroup> </colgroup>
<tr> <tr>
@ -66,19 +65,16 @@
All All
</th> </th>
<th class="text-center" title="Created"> <th class="text-center" title="Created">
Cr C
</th> </th>
<th class="text-center" title="Updated"> <th class="text-center" title="Updated">
Up U
</th> </th>
<th class="text-center" title="Deleted"> <th class="text-center" title="Deleted">
Dl D
</th> </th>
<th class="text-center" title="Published"> <th class="text-center" title="Published">
Pu P
</th>
<th class="text-center" title="Unpublished">
Up
</th> </th>
<th></th> <th></th>
</tr> </tr>
@ -102,9 +98,6 @@
<td class="text-center" title="Published"> <td class="text-center" title="Published">
<input type="checkbox" [ngModel]="schema.sendPublish" (ngModelChange)="toggle(schema, 'sendPublish')" /> <input type="checkbox" [ngModel]="schema.sendPublish" (ngModelChange)="toggle(schema, 'sendPublish')" />
</td> </td>
<td class="text-center" title="Unpublished">
<input type="checkbox" [ngModel]="schema.sendUnpublish" (ngModelChange)="toggle(schema, 'sendUnpublish')" />
</td>
<td class="text-center"> <td class="text-center">
<button type="button" class="btn btn-link btn-secondary" <button type="button" class="btn btn-link btn-secondary"
(sqxConfirmClick)="removeSchema(schema)" (sqxConfirmClick)="removeSchema(schema)"

17
src/Squidex/app/features/webhooks/pages/webhook.component.ts

@ -22,8 +22,7 @@ export interface WebhookSchemaForm {
sendCreate: boolean; sendCreate: boolean;
sendUpdate: boolean; sendUpdate: boolean;
sendDelete: boolean; sendDelete: boolean;
sendPublish: boolean; sendPublish: boolean
sendUnpublish: boolean;
} }
@Component({ @Component({
@ -85,8 +84,7 @@ export class WebhookComponent implements OnInit {
sendCreate: webhookSchema.sendCreate, sendCreate: webhookSchema.sendCreate,
sendUpdate: webhookSchema.sendUpdate, sendUpdate: webhookSchema.sendUpdate,
sendDelete: webhookSchema.sendDelete, sendDelete: webhookSchema.sendDelete,
sendPublish: webhookSchema.sendPublish, sendPublish: webhookSchema.sendPublish
sendUnpublish: webhookSchema.sendUnpublish
}); });
} else { } else {
return null; return null;
@ -117,8 +115,7 @@ export class WebhookComponent implements OnInit {
sendCreate: false, sendCreate: false,
sendUpdate: false, sendUpdate: false,
sendDelete: false, sendDelete: false,
sendPublish: false, sendPublish: false
sendUnpublish: false
})).sortByStringAsc(x => x.schema.name); })).sortByStringAsc(x => x.schema.name);
this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name); this.schemasToAdd = this.schemasToAdd.remove(this.schemaToAdd).sortByStringAsc(x => x.name);
@ -135,8 +132,7 @@ export class WebhookComponent implements OnInit {
schema.sendCreate, schema.sendCreate,
schema.sendUpdate, schema.sendUpdate,
schema.sendDelete, schema.sendDelete,
schema.sendPublish, schema.sendPublish)));
schema.sendUnpublish)));
this.emitUpdating(requestDto); this.emitUpdating(requestDto);
} }
@ -163,8 +159,6 @@ export class WebhookComponent implements OnInit {
schemaForm.sendUpdate = value; schemaForm.sendUpdate = value;
schemaForm.sendDelete = value; schemaForm.sendDelete = value;
schemaForm.sendPublish = value; schemaForm.sendPublish = value;
schemaForm.sendUnpublish = value;
return schemaForm; return schemaForm;
} }
@ -173,8 +167,7 @@ export class WebhookComponent implements OnInit {
schemaForm.sendCreate && schemaForm.sendCreate &&
schemaForm.sendUpdate && schemaForm.sendUpdate &&
schemaForm.sendDelete && schemaForm.sendDelete &&
schemaForm.sendPublish && schemaForm.sendPublish;
schemaForm.sendUnpublish;
return schemaForm; return schemaForm;
} }

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

@ -26,7 +26,7 @@ describe('ContentDto', () => {
const version = new Version('1'); const version = new Version('1');
it('should update data property and user info when updating', () => { it('should update data property and user info when updating', () => {
const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.update({ data: 2 }, modifier, modified); const content_2 = content_1.update({ data: 2 }, modifier, modified);
expect(content_2.data).toEqual({ data: 2 }); expect(content_2.data).toEqual({ data: 2 });
@ -34,38 +34,38 @@ describe('ContentDto', () => {
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
}); });
it('should update isPublished property and user info when publishing', () => { it('should update status property and user info when publishing', () => {
const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.publish(modifier, modified); const content_2 = content_1.publish(modifier, modified);
expect(content_2.isPublished).toBeTruthy(); expect(content_2.status).toEqual('Published');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
}); });
it('should update isPublished property and user info when unpublishing', () => { it('should update status property and user info when unpublishing', () => {
const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.unpublish(modifier, modified); const content_2 = content_1.unpublish(modifier, modified);
expect(content_2.isPublished).toBeFalsy(); expect(content_2.status).toEqual('Draft');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
}); });
it('should update isArchived property and user info when archiving', () => { it('should update status property and user info when archiving', () => {
const content_1 = new ContentDto('1', false, false, creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.archive(modifier, modified); const content_2 = content_1.archive(modifier, modified);
expect(content_2.isArchived).toBeTruthy(); expect(content_2.status).toEqual('Archived');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
}); });
it('should update isArchived property and user info when restoring', () => { it('should update status property and user info when restoring', () => {
const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Archived', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.restore(modifier, modified); const content_2 = content_1.restore(modifier, modified);
expect(content_2.isArchived).toBeFalsy(); expect(content_2.status).toEqual('Draft');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
}); });
@ -73,7 +73,7 @@ describe('ContentDto', () => {
it('should update data property when setting data', () => { it('should update data property when setting data', () => {
const newData = {}; const newData = {};
const content_1 = new ContentDto('1', true, false, creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.setData(newData); const content_2 = content_1.setData(newData);
expect(content_2.data).toBe(newData); expect(content_2.data).toBe(newData);
@ -119,7 +119,7 @@ describe('ContentsService', () => {
items: [ items: [
{ {
id: 'id1', id: 'id1',
isPublished: true, status: 'Published',
created: '2016-12-12T10:10', created: '2016-12-12T10:10',
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
@ -129,7 +129,7 @@ describe('ContentsService', () => {
}, },
{ {
id: 'id2', id: 'id2',
isPublished: true, status: 'Published',
created: '2016-10-12T10:10', created: '2016-10-12T10:10',
createdBy: 'Created2', createdBy: 'Created2',
lastModified: '2017-10-12T10:10', lastModified: '2017-10-12T10:10',
@ -142,12 +142,12 @@ describe('ContentsService', () => {
expect(contents).toEqual( expect(contents).toEqual(
new ContentsDto(10, [ new ContentsDto(10, [
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1', new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
{}, {},
new Version('11')), new Version('11')),
new ContentDto('id2', true, false, 'Created2', 'LastModifiedBy2', new ContentDto('id2', 'Published', 'Created2', 'LastModifiedBy2',
DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2016-10-12T10:10'),
DateTime.parseISO_UTC('2017-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'),
{}, {},
@ -222,7 +222,7 @@ describe('ContentsService', () => {
req.flush({ req.flush({
id: 'id1', id: 'id1',
isPublished: true, status: 'Published',
created: '2016-12-12T10:10', created: '2016-12-12T10:10',
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
@ -232,7 +232,7 @@ describe('ContentsService', () => {
}); });
expect(content).toEqual( expect(content).toEqual(
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1', new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
{}, {},
@ -280,7 +280,7 @@ describe('ContentsService', () => {
req.flush({ req.flush({
id: 'id1', id: 'id1',
isPublished: true, status: 'Published',
created: '2016-12-12T10:10', created: '2016-12-12T10:10',
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
@ -290,7 +290,7 @@ describe('ContentsService', () => {
}); });
expect(content).toEqual( expect(content).toEqual(
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1', new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
{}, {},

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

@ -30,8 +30,7 @@ export class ContentsDto {
export class ContentDto { export class ContentDto {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly isPublished: boolean, public readonly status: string,
public readonly isArchived: boolean,
public readonly createdBy: string, public readonly createdBy: string,
public readonly lastModifiedBy: string, public readonly lastModifiedBy: string,
public readonly created: DateTime, public readonly created: DateTime,
@ -44,8 +43,7 @@ export class ContentDto {
public setData(data: any): ContentDto { public setData(data: any): ContentDto {
return new ContentDto( return new ContentDto(
this.id, this.id,
this.isPublished, this.status,
this.isArchived,
this.createdBy, this.createdBy,
this.lastModifiedBy, this.lastModifiedBy,
this.created, this.created,
@ -55,43 +53,25 @@ export class ContentDto {
} }
public publish(user: string, now?: DateTime): ContentDto { public publish(user: string, now?: DateTime): ContentDto {
return new ContentDto( return this.changeStatus('Published', user, now);
this.id,
true,
this.isArchived,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
this.version);
} }
public unpublish(user: string, now?: DateTime): ContentDto { public unpublish(user: string, now?: DateTime): ContentDto {
return new ContentDto( return this.changeStatus('Draft', user, now);
this.id,
false,
this.isArchived,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
this.version);
} }
public archive(user: string, now?: DateTime): ContentDto { public archive(user: string, now?: DateTime): ContentDto {
return new ContentDto( return this.changeStatus('Archived', user, now);
this.id,
this.isPublished,
true,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
this.version);
} }
public restore(user: string, now?: DateTime): ContentDto { public restore(user: string, now?: DateTime): ContentDto {
return this.changeStatus('Draft', user, now);
}
private changeStatus(status: string, user: string, now?: DateTime): ContentDto {
return new ContentDto( return new ContentDto(
this.id, this.id,
this.isPublished, status,
false,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.data, this.data,
@ -101,8 +81,7 @@ export class ContentDto {
public update(data: any, user: string, now?: DateTime): ContentDto { public update(data: any, user: string, now?: DateTime): ContentDto {
return new ContentDto( return new ContentDto(
this.id, this.id,
this.isPublished, this.status,
this.isArchived,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
data, data,
@ -159,8 +138,7 @@ export class ContentsService {
return new ContentsDto(response.total, items.map(item => { return new ContentsDto(response.total, items.map(item => {
return new ContentDto( return new ContentDto(
item.id, item.id,
item.isPublished, item.status,
item.isArchived === true,
item.createdBy, item.createdBy,
item.lastModifiedBy, item.lastModifiedBy,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
@ -179,8 +157,7 @@ export class ContentsService {
.map(response => { .map(response => {
return new ContentDto( return new ContentDto(
response.id, response.id,
response.isPublished, response.status,
response.isArchived === true,
response.createdBy, response.createdBy,
response.lastModifiedBy, response.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.created),
@ -209,7 +186,7 @@ export class ContentsService {
.map(response => { .map(response => {
return new ContentDto( return new ContentDto(
response.id, response.id,
response.isPublished, false, response.status,
response.createdBy, response.createdBy,
response.lastModifiedBy, response.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.created),

14
src/Squidex/app/shared/services/webhooks.service.spec.ts

@ -34,8 +34,8 @@ describe('WebhookDto', () => {
const webhook_2 = const webhook_2 =
webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2', webhook_1.update(new UpdateWebhookDto('http://squidex.io/hook2',
[ [
new WebhookSchemaDto('1', true, true, true, true, true), new WebhookSchemaDto('1', true, true, true, true),
new WebhookSchemaDto('2', true, true, true, true, true) new WebhookSchemaDto('2', true, true, true, true)
]), modifier, modified); ]), modifier, modified);
expect(webhook_2.url).toEqual('http://squidex.io/hook2'); expect(webhook_2.url).toEqual('http://squidex.io/hook2');
@ -99,15 +99,13 @@ describe('WebhooksService', () => {
sendCreate: true, sendCreate: true,
sendUpdate: true, sendUpdate: true,
sendDelete: true, sendDelete: true,
sendPublish: true, sendPublish: true
sendUnpublish: true
}, { }, {
schemaId: '2', schemaId: '2',
sendCreate: true, sendCreate: true,
sendUpdate: true, sendUpdate: true,
sendDelete: true, sendDelete: true,
sendPublish: true, sendPublish: true
sendUnpublish: true
}] }]
} }
]); ]);
@ -118,8 +116,8 @@ describe('WebhooksService', () => {
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
version, version,
[ [
new WebhookSchemaDto('1', true, true, true, true, true), new WebhookSchemaDto('1', true, true, true, true),
new WebhookSchemaDto('2', true, true, true, true, true) new WebhookSchemaDto('2', true, true, true, true)
], ],
'http://squidex.io/hook', 1, 2, 3, 4) 'http://squidex.io/hook', 1, 2, 3, 4)
]); ]);

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

@ -58,8 +58,7 @@ export class WebhookSchemaDto {
public readonly sendCreate: boolean, public readonly sendCreate: boolean,
public readonly sendUpdate: boolean, public readonly sendUpdate: boolean,
public readonly sendDelete: boolean, public readonly sendDelete: boolean,
public readonly sendPublish: boolean, public readonly sendPublish: boolean
public readonly sendUnpublish: boolean
) { ) {
} }
} }
@ -125,8 +124,7 @@ export class WebhooksService {
schema.sendCreate, schema.sendCreate,
schema.sendUpdate, schema.sendUpdate,
schema.sendDelete, schema.sendDelete,
schema.sendPublish, schema.sendPublish));
schema.sendUnpublish));
return new WebhookDto( return new WebhookDto(
item.id, item.id,

34
tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs

@ -0,0 +1,34 @@
// ==========================================================================
// StatusFlowTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Xunit;
namespace Squidex.Domain.Apps.Core.Contents
{
public class StatusFlowTests
{
[Fact]
public void Should_make_tests()
{
Assert.True(StatusFlow.Exists(Status.Draft));
Assert.True(StatusFlow.Exists(Status.Archived));
Assert.True(StatusFlow.Exists(Status.Published));
Assert.True(StatusFlow.CanChange(Status.Draft, Status.Archived));
Assert.True(StatusFlow.CanChange(Status.Draft, Status.Published));
Assert.True(StatusFlow.CanChange(Status.Published, Status.Draft));
Assert.True(StatusFlow.CanChange(Status.Published, Status.Archived));
Assert.True(StatusFlow.CanChange(Status.Archived, Status.Draft));
Assert.False(StatusFlow.Exists((Status)int.MaxValue));
Assert.False(StatusFlow.CanChange(Status.Archived, Status.Published));
}
}
}

44
tests/Squidex.Domain.Apps.Read.Tests/Contents/ContentQueryServiceTests.cs

@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Read.Contents namespace Squidex.Domain.Apps.Read.Contents
@ -37,17 +38,20 @@ namespace Squidex.Domain.Apps.Read.Contents
private readonly Guid contentId = Guid.NewGuid(); private readonly Guid contentId = Guid.NewGuid();
private readonly NamedContentData data = new NamedContentData(); private readonly NamedContentData data = new NamedContentData();
private readonly NamedContentData transformedData = new NamedContentData(); private readonly NamedContentData transformedData = new NamedContentData();
private readonly ClaimsPrincipal user = new ClaimsPrincipal(); private readonly ClaimsPrincipal user;
private readonly ClaimsIdentity identity = new ClaimsIdentity();
private readonly EdmModelBuilder modelBuilder = A.Fake<EdmModelBuilder>(); private readonly EdmModelBuilder modelBuilder = A.Fake<EdmModelBuilder>();
private readonly ContentQueryService sut; private readonly ContentQueryService sut;
public ContentQueryServiceTests() public ContentQueryServiceTests()
{ {
user = new ClaimsPrincipal(identity);
A.CallTo(() => app.Id).Returns(appId); A.CallTo(() => app.Id).Returns(appId);
A.CallTo(() => content.Id).Returns(contentId); A.CallTo(() => content.Id).Returns(contentId);
A.CallTo(() => content.Data).Returns(data); A.CallTo(() => content.Data).Returns(data);
A.CallTo(() => content.IsPublished).Returns(true); A.CallTo(() => content.Status).Returns(Status.Published);
sut = new ContentQueryService(contentRepository, schemas, scriptEngine, modelBuilder); sut = new ContentQueryService(contentRepository, schemas, scriptEngine, modelBuilder);
} }
@ -116,15 +120,43 @@ namespace Squidex.Domain.Apps.Read.Contents
} }
[Fact] [Fact]
public async Task Should_return_contents_from_repository_and_transform() public async Task Should_return_non_archived_contents_from_repository_and_transform()
{
await TestManyRequest(true, false, Status.Draft, Status.Published);
}
[Fact]
public async Task Should_return_archived_contents_from_repository_and_transform()
{
await TestManyRequest(true, true, Status.Archived);
}
[Fact]
public async Task Should_return_draft_contents_from_repository_and_transform()
{ {
await TestManyRequest(false, false, Status.Published);
}
[Fact]
public async Task Should_return_draft_contents_from_repository_and_transform_when_requesting_archive_as_non_frontend()
{
await TestManyRequest(false, true, Status.Published);
}
private async Task TestManyRequest(bool isFrontend, bool archive, params Status[] status)
{
if (isFrontend)
{
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend"));
}
var ids = new HashSet<Guid>(); var ids = new HashSet<Guid>();
A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)) A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false))
.Returns(schema); .Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, false, true, ids, A<ODataUriParser>.Ignored)) A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids, A<ODataUriParser>.Ignored))
.Returns(new List<IContentEntity> { content }); .Returns(new List<IContentEntity> { content });
A.CallTo(() => contentRepository.CountAsync(app, schema, false, true, ids, A<ODataUriParser>.Ignored)) A.CallTo(() => contentRepository.CountAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids, A<ODataUriParser>.Ignored))
.Returns(123); .Returns(123);
A.CallTo(() => schema.ScriptQuery) A.CallTo(() => schema.ScriptQuery)
@ -133,7 +165,7 @@ namespace Squidex.Domain.Apps.Read.Contents
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "<query-script>")) A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "<query-script>"))
.Returns(transformedData); .Returns(transformedData);
var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, true, ids, null); var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, ids, null);
Assert.Equal(123, result.Total); Assert.Equal(123, result.Total);
Assert.Equal(schema, result.Schema); Assert.Equal(schema, result.Schema);

13
tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs

@ -16,23 +16,18 @@ namespace Squidex.Domain.Apps.Read.Contents.TestData
public sealed class FakeContentEntity : IContentEntity public sealed class FakeContentEntity : IContentEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid AppId { get; set; } public Guid AppId { get; set; }
public Instant Created { get; set; } public long Version { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; } public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; } public RefToken LastModifiedBy { get; set; }
public long Version { get; set; }
public bool IsPublished { get; set; }
public bool IsArchived { get; set; }
public NamedContentData Data { get; set; } public NamedContentData Data { get; set; }
public Status Status { get; set; }
} }
} }

60
tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs

@ -176,75 +176,21 @@ namespace Squidex.Domain.Apps.Write.Contents
} }
[Fact] [Fact]
public async Task Publish_should_publish_domain_object() public async Task ChangeStatus_should_publish_domain_object()
{ {
A.CallTo(() => schema.ScriptChange) A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>"); .Returns("<change-script>");
CreateContent(); CreateContent();
var context = CreateContextForCommand(new PublishContent { ContentId = contentId, User = user }); var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published });
await TestUpdate(content, async _ => await TestUpdate(content, async _ =>
{ {
await sut.HandleAsync(context); await sut.HandleAsync(context);
}); });
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "publish content")).MustHaveHappened(); A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "change content status")).MustHaveHappened();
}
[Fact]
public async Task Unpublish_should_unpublish_domain_object()
{
A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>");
CreateContent();
var context = CreateContextForCommand(new UnpublishContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "unpublish content")).MustHaveHappened();
}
[Fact]
public async Task Archive_should_archive_domain_object()
{
A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>");
CreateContent();
var context = CreateContextForCommand(new ArchiveContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "archive content")).MustHaveHappened();
}
[Fact]
public async Task Restore_should_restore_domain_object()
{
A.CallTo(() => schema.ScriptChange)
.Returns("<change-script>");
CreateContent();
var context = CreateContextForCommand(new RestoreContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>", "restore content")).MustHaveHappened();
} }
[Fact] [Fact]

139
tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs

@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Write.Contents
sut.GetUncomittedEvents() sut.GetUncomittedEvents()
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data }), CreateContentEvent(new ContentCreated { Data = data }),
CreateContentEvent(new ContentPublished()) CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
); );
} }
@ -195,148 +195,38 @@ namespace Squidex.Domain.Apps.Write.Contents
} }
[Fact] [Fact]
public void Publish_should_throw_exception_if_not_created() public void ChangeStatus_should_throw_exception_if_not_created()
{ {
Assert.Throws<DomainException>(() => Assert.Throws<DomainException>(() =>
{ {
sut.Publish(CreateContentCommand(new PublishContent())); sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
}); });
} }
[Fact] [Fact]
public void Publish_should_throw_exception_if_content_is_deleted() public void ChangeStatus_should_throw_exception_if_content_is_deleted()
{ {
CreateContent(); CreateContent();
DeleteContent(); DeleteContent();
Assert.Throws<DomainException>(() => Assert.Throws<DomainException>(() =>
{ {
sut.Publish(CreateContentCommand(new PublishContent())); sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
}); });
} }
[Fact] [Fact]
public void Publish_should_refresh_properties_and_create_events() public void ChangeStatus_should_refresh_properties_and_create_events()
{ {
CreateContent(); CreateContent();
sut.Publish(CreateContentCommand(new PublishContent())); sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published }));
Assert.True(sut.IsPublished); Assert.Equal(Status.Published, sut.Status);
sut.GetUncomittedEvents() sut.GetUncomittedEvents()
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateContentEvent(new ContentPublished()) CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
}
[Fact]
public void Unpublish_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Unpublish(CreateContentCommand(new UnpublishContent()));
});
}
[Fact]
public void Unpublish_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Unpublish(CreateContentCommand(new UnpublishContent()));
});
}
[Fact]
public void Unpublish_should_refresh_properties_and_create_events()
{
CreateContent();
PublishContent();
sut.Unpublish(CreateContentCommand(new UnpublishContent()));
Assert.False(sut.IsPublished);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUnpublished())
);
}
[Fact]
public void Archive_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Archive(CreateContentCommand(new ArchiveContent()));
});
}
[Fact]
public void Archive_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Archive(CreateContentCommand(new ArchiveContent()));
});
}
[Fact]
public void Archive_should_refresh_properties_and_create_events()
{
CreateContent();
sut.Archive(CreateContentCommand(new ArchiveContent()));
Assert.True(sut.IsArchived);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentArchived())
);
}
[Fact]
public void Restore_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Restore(CreateContentCommand(new RestoreContent()));
});
}
[Fact]
public void Restore_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Restore(CreateContentCommand(new RestoreContent()));
});
}
[Fact]
public void Restore_should_refresh_properties_and_create_events()
{
CreateContent();
ArchiveContent();
sut.Restore(CreateContentCommand(new RestoreContent()));
Assert.False(sut.IsArchived);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentRestored())
); );
} }
@ -390,16 +280,9 @@ namespace Squidex.Domain.Apps.Write.Contents
((IAggregate)sut).ClearUncommittedEvents(); ((IAggregate)sut).ClearUncommittedEvents();
} }
private void PublishContent() private void ChangeStatus(Status status)
{
sut.Publish(CreateContentCommand(new PublishContent()));
((IAggregate)sut).ClearUncommittedEvents();
}
private void ArchiveContent()
{ {
sut.Archive(CreateContentCommand(new ArchiveContent())); sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status }));
((IAggregate)sut).ClearUncommittedEvents(); ((IAggregate)sut).ClearUncommittedEvents();
} }

2
tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs

@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Write.Contents
var eventData2 = new EventData(); var eventData2 = new EventData();
var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId<Guid>(appId, "my-app") }; var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId<Guid>(appId, "my-app") };
var event2 = new ContentPublished(); var event2 = new ContentStatusChanged();
var events = new List<StoredEvent> var events = new List<StoredEvent>
{ {

Loading…
Cancel
Save