Browse Source

1) Restore implemented

2) Custom history view
pull/107/head
Sebastian Stehle 8 years ago
parent
commit
a38a0358a8
  1. 17
      src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs
  2. 4
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs
  3. 17
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  4. 3
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs
  5. 2
      src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs
  6. 8
      src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs
  7. 5
      src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs
  8. 13
      src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs
  9. 1
      src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs
  10. 2
      src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs
  11. 2
      src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs
  12. 14
      src/Squidex.Domain.Apps.Write/Contents/Commands/RestoreContent.cs
  13. 8
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs
  14. 24
      src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs
  15. 5
      src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs
  16. 15
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  17. 33
      src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs
  18. 12
      src/Squidex/Controllers/ContentApi/Models/ContentDto.cs
  19. 1
      src/Squidex/app/features/content/declarations.ts
  20. 5
      src/Squidex/app/features/content/module.ts
  21. 29
      src/Squidex/app/features/content/pages/content/content-history.component.html
  22. 39
      src/Squidex/app/features/content/pages/content/content-history.component.scss
  23. 101
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  24. 8
      src/Squidex/app/features/content/pages/messages.ts
  25. 4
      src/Squidex/app/shared/components/history.component.scss
  26. 14
      src/Squidex/app/shared/services/contents.service.spec.ts
  27. 8
      src/Squidex/app/shared/services/contents.service.ts
  28. 6
      src/Squidex/app/shared/services/history.service.spec.ts
  29. 2
      src/Squidex/app/shared/services/history.service.ts
  30. 15
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs
  31. 25
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs

17
src/Squidex.Domain.Apps.Events/Contents/ContentRestored.cs

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

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

@ -47,6 +47,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
[BsonElement("pu")]
public bool IsPublished { get; set; }
[BsonRequired]
[BsonElement("dl")]
public bool IsDeleted { get; set; }
[BsonRequired]
[BsonElement("dt")]
public string DataText { get; set; }

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

@ -62,6 +62,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.ReferencedIds));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsPublished));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted));
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText));
});
}
@ -114,6 +115,17 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
});
}
protected Task On(ContentRestored @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, collection =>
{
return collection.UpdateAsync(@event, headers, x =>
{
x.IsDeleted = false;
});
});
}
protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, async collection =>
@ -124,7 +136,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId));
await collection.DeleteOneAsync(x => x.Id == headers.AggregateId());
await collection.UpdateAsync(@event, headers, x =>
{
x.IsDeleted = true;
});
});
}

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

@ -67,7 +67,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.SchemaId, schemaId)
Filter.Eq(x => x.SchemaId, schemaId),
Filter.Eq(x => x.IsDeleted, false)
};
if (!nonPublished)

2
src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs

@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
[BsonRequired]
[BsonElement]
public int SessionEventIndex { get; set; }
public long Version { get; set; }
[BsonRequired]
[BsonElement]

8
src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs

@ -9,7 +9,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events;
@ -25,7 +24,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
{
private readonly List<IHistoryEventsCreator> creators;
private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
private int sessionEventCount;
public string Name
{
@ -64,7 +62,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
.Ascending(x => x.AppId)
.Ascending(x => x.Channel)
.Descending(x => x.Created)
.Descending(x => x.SessionEventIndex)),
.Descending(x => x.Version)),
collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) }));
}
@ -72,7 +70,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
{
var historyEventEntities =
await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix)
.SortByDescending(x => x.Created).ThenByDescending(x => x.SessionEventIndex).Limit(count)
.SortByDescending(x => x.Created).ThenBy(x => x.Version).Limit(count)
.ToListAsync();
return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
@ -90,7 +88,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
{
entity.Id = Guid.NewGuid();
entity.SessionEventIndex = Interlocked.Increment(ref sessionEventCount);
entity.Version = @event.Headers.EventStreamNumber();
entity.Channel = message.Channel;
entity.Message = message.Message;

5
src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs

@ -44,6 +44,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History
get { return inner.LastModified; }
}
public long Version
{
get { return inner.Version; }
}
public string Channel
{
get { return inner.Channel; }

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

@ -20,19 +20,22 @@ namespace Squidex.Domain.Apps.Read.Contents
: base(typeNameRegistry)
{
AddEventMessage<ContentCreated>(
"created content element.");
"created content item.");
AddEventMessage<ContentUpdated>(
"updated content element.");
"updated content item.");
AddEventMessage<ContentDeleted>(
"deleted content element.");
"deleted content item.");
AddEventMessage<ContentRestored>(
"restored content item.");
AddEventMessage<ContentPublished>(
"published content element.");
"published content item.");
AddEventMessage<ContentUnpublished>(
"unpublished content element.");
"unpublished content item.");
}
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)

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

@ -156,6 +156,7 @@ namespace Squidex.Domain.Apps.Read.Contents
public Guid Id { get; set; }
public Guid AppId { get; set; }
public long Version { get; set; }
public bool IsDeleted { get; set; }
public bool IsPublished { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }

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

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Read.Contents
{
bool IsPublished { get; }
bool IsDeleted { get; }
NamedContentData Data { get; }
}
}

2
src/Squidex.Domain.Apps.Read/History/IHistoryEventEntity.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Read.History
string Message { get; }
long Version { get; }
RefToken Actor { get; }
}
}

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

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

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

@ -150,6 +150,14 @@ namespace Squidex.Domain.Apps.Write.Contents
});
}
protected Task On(RestoreContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, content =>
{
content.Restore(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))

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

@ -72,6 +72,11 @@ namespace Squidex.Domain.Apps.Write.Contents
isDeleted = true;
}
protected void On(ContentRestored @event)
{
isDeleted = false;
}
public ContentDomainObject Create(CreateContent command)
{
Guard.Valid(command, nameof(command), () => "Cannot create content");
@ -99,6 +104,17 @@ namespace Squidex.Domain.Apps.Write.Contents
return this;
}
public ContentDomainObject Restore(RestoreContent command)
{
Guard.NotNull(command, nameof(command));
VerifyDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentRestored()));
return this;
}
public ContentDomainObject Publish(PublishContent command)
{
Guard.NotNull(command, nameof(command));
@ -159,6 +175,14 @@ namespace Squidex.Domain.Apps.Write.Contents
}
}
private void VerifyDeleted()
{
if (!isDeleted)
{
throw new DomainException("Content has not been deleted.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (isDeleted || !isCreated)

5
src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs

@ -35,5 +35,10 @@ namespace Squidex.Controllers.Api.History.Models
/// The time when the event happened.
/// </summary>
public Instant Created { get; set; }
/// <summary>
/// The version identifier.
/// </summary>
public long Version { get; set; }
}
}

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

@ -208,6 +208,21 @@ namespace Squidex.Controllers.ContentApi
return NoContent();
}
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/restore")]
[ApiCosts(1)]
public async Task<IActionResult> RestoreContent(string name, Guid id)
{
await contentQuery.FindSchemaAsync(App, name);
var command = new RestoreContent { ContentId = id, User = User };
await CommandBus.PublishAsync(command);
return NoContent();
}
[MustBeAppEditor]
[HttpDelete]
[Route("content/{app}/{name}/{id}")]

33
src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs

@ -92,6 +92,7 @@ namespace Squidex.Controllers.ContentApi.Generator
GenerateSchemaGetOperation(),
GenerateSchemaUpdateOperation(),
GenerateSchemaPatchOperation(),
GenerateSchemaRestoreOperation(),
GenerateSchemaPublishOperation(),
GenerateSchemaUnpublishOperation(),
GenerateSchemaDeleteOperation()
@ -109,6 +110,7 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Query{schemaKey}Contents";
operation.Summary = $"Queries {schemaName} contents.";
operation.Security = ReaderSecurity;
operation.Description = SchemaQueryDescription;
@ -119,8 +121,6 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.AddQueryParameter("orderby", JsonObjectType.String, "Optional OData order definition.");
operation.AddResponse("200", $"{schemaName} content retrieved.", CreateContentsSchema(schemaName, contentSchema));
operation.Security = ReaderSecurity;
});
}
@ -130,10 +130,9 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Get{schemaKey}Content";
operation.Summary = $"Get a {schemaName} content.";
operation.Security = ReaderSecurity;
operation.AddResponse("200", $"{schemaName} content found.", contentSchema);
operation.Security = ReaderSecurity;
});
}
@ -143,13 +142,12 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Create{schemaKey}Content";
operation.Summary = $"Create a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content.");
operation.AddResponse("201", $"{schemaName} created.", contentSchema);
operation.Security = EditorSecurity;
});
}
@ -159,12 +157,11 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Update{schemaKey}Content";
operation.Summary = $"Update a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("201", $"{schemaName} element updated.", dataSchema);
operation.Security = EditorSecurity;
});
}
@ -174,12 +171,11 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Path{schemaKey}Content";
operation.Summary = $"Patchs a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription);
operation.AddResponse("201", $"{schemaName} element patched.", dataSchema);
operation.Security = EditorSecurity;
});
}
@ -189,10 +185,9 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Publish{schemaKey}Content";
operation.Summary = $"Publish a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} element published.");
operation.Security = EditorSecurity;
});
}
@ -202,10 +197,21 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Unpublish{schemaKey}Content";
operation.Summary = $"Unpublish a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} element unpublished.");
});
}
private SwaggerOperations GenerateSchemaRestoreOperation()
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/restore", operation =>
{
operation.OperationId = $"Restore{schemaKey}Content";
operation.Summary = $"Restore a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} element restored.");
});
}
@ -215,10 +221,9 @@ namespace Squidex.Controllers.ContentApi.Generator
{
operation.OperationId = $"Delete{schemaKey}Content";
operation.Summary = $"Delete a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content deleted.");
operation.Security = EditorSecurity;
});
}

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

@ -52,15 +52,25 @@ namespace Squidex.Controllers.ContentApi.Models
public Instant LastModified { get; set; }
/// <summary>
/// Indicates if the content element is publihed.
/// Indicates if the content element is published.
/// </summary>
public bool? IsPublished { get; set; }
/// <summary>
/// Indicates if the content element is deleted.
/// </summary>
public bool IsDeleted { get; set; }
/// <summary>
/// The version of the content.
/// </summary>
public long Version { get; set; }
public bool ShouldSerializeIsDeleted()
{
return IsDeleted;
}
public static ContentDto Create(CreateContent command, EntityCreatedResult<NamedContentData> result)
{
var now = SystemClock.Instance.GetCurrentInstant();

1
src/Squidex/app/features/content/declarations.ts

@ -6,6 +6,7 @@
*/
export * from './pages/content/content-field.component';
export * from './pages/content/content-history.component';
export * from './pages/content/content-page.component';
export * from './pages/contents/contents-page.component';
export * from './pages/contents/search-form.component';

5
src/Squidex/app/features/content/module.ts

@ -11,7 +11,6 @@ import { DndModule } from 'ng2-dnd';
import {
CanDeactivateGuard,
HistoryComponent,
ResolveAppLanguagesGuard,
ResolveContentGuard,
ResolvePublishedSchemaGuard,
@ -22,6 +21,7 @@ import {
import {
AssetsEditorComponent,
ContentFieldComponent,
ContentHistoryComponent,
ContentPageComponent,
ContentItemComponent,
ContentsPageComponent,
@ -76,7 +76,7 @@ const routes: Routes = [
children: [
{
path: 'history',
component: HistoryComponent,
component: ContentHistoryComponent,
data: {
channel: 'contents.{contentId}'
}
@ -112,6 +112,7 @@ const routes: Routes = [
declarations: [
AssetsEditorComponent,
ContentFieldComponent,
ContentHistoryComponent,
ContentItemComponent,
ContentPageComponent,
ContentsPageComponent,

29
src/Squidex/app/features/content/pages/content/content-history.component.html

@ -0,0 +1,29 @@
<sqx-panel panelWidth="16rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title">Activity</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div *ngFor="let event of events | async" class="event">
<div class="event-left">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:'I'" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="event-main">
<div class="event-message">
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:'I'}}</span> <span [innerHTML]="format(event.message) | async"></span>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
<a class="event-load">Load this Version</a>
</div>
</div>
</div>
</div>
</sqx-panel>

39
src/Squidex/app/features/content/pages/content/content-history.component.scss

@ -0,0 +1,39 @@
@import '_vars';
@import '_mixins';
.event {
& {
@include flex-box;
margin-bottom: 1rem;
}
&-main {
@include flex-grow(1);
}
&-load {
& {
font-size: .9rem;
font-weight: normal;
cursor: pointer;
color: $color-theme-blue !important;
}
&:focus,
&:hover {
text-decoration: underline !important;
}
}
&-left {
min-width: 2.8rem;
max-width: 2.8rem;
margin-top: .3rem;
}
&-created {
font-size: .65rem;
font-weight: normal;
color: $color-text-decent;
}
}

101
src/Squidex/app/features/content/pages/content/content-history.component.ts

@ -0,0 +1,101 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import {
allParams,
AppComponentBase,
AppsStoreService,
DialogService,
HistoryChannelUpdated,
HistoryEventDto,
HistoryService,
MessageBus,
UsersProviderService
} from 'shared';
const REPLACEMENT_TEMP = '$TEMP$';
@Component({
selector: 'sqx-history',
styleUrls: ['./content-history.component.scss'],
templateUrl: './content-history.component.html'
})
export class ContentHistoryComponent extends AppComponentBase {
public get channel(): string {
let channelPath = this.route.snapshot.data['channel'];
if (channelPath) {
const params = allParams(this.route);
for (let key in params) {
if (params.hasOwnProperty(key)) {
const value = params[key];
channelPath = channelPath.replace(`{${key}}`, value);
}
}
}
return channelPath;
}
public events: Observable<HistoryEventDto[]> =
Observable.timer(0, 10000)
.merge(this.messageBus.of(HistoryChannelUpdated).delay(1000))
.switchMap(() => this.appNameOnce())
.switchMap(app => this.historyService.getHistory(app, this.channel).retry(2));
constructor(appsStore: AppsStoreService, dialogs: DialogService,
private readonly users: UsersProviderService,
private readonly historyService: HistoryService,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute
) {
super(dialogs, appsStore);
}
private userName(userId: string): Observable<string> {
const parts = userId.split(':');
if (parts[0] === 'subject') {
return this.users.getUser(parts[1], 'Me').map(u => u.displayName);
} else {
if (parts[1].endsWith('client')) {
return Observable.of(parts[1]);
} else {
return Observable.of(`${parts[1]}-client`);
}
}
}
public format(message: string): Observable<string> {
let foundUserId: string | null = null;
message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => {
if (type === 'user') {
foundUserId = id;
return REPLACEMENT_TEMP;
} else {
return id;
}
});
message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => {
return `<span class="marker-ref">${marker}</span>`;
});
if (foundUserId) {
return this.userName(foundUserId).map(t => message.replace(REPLACEMENT_TEMP, `<span class="user-ref">${t}</span>`));
}
return Observable.of(message);
}
}

8
src/Squidex/app/features/content/pages/messages.ts

@ -27,3 +27,11 @@ export class ContentDeleted {
) {
}
}
export class ContentVersionSelected {
constructor(
public readonly id: string,
public readonly version: number
) {
}
}

4
src/Squidex/app/shared/components/history.component.scss

@ -7,10 +7,6 @@
margin-bottom: 1rem;
}
&-message {
font-size: .9rem;
}
&-main {
@include flex-grow(1);
}

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

@ -22,7 +22,7 @@ describe('ContentDto', () => {
it('should update data property and user info when updating', () => {
const now = DateTime.now();
const content_1 = new ContentDto('1', false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null);
const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null);
const content_2 = content_1.update({ data: 2 }, 'me', now);
expect(content_2.data).toEqual({ data: 2 });
@ -33,7 +33,7 @@ describe('ContentDto', () => {
it('should update isPublished property and user info when publishing', () => {
const now = DateTime.now();
const content_1 = new ContentDto('1', false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null);
const content_1 = new ContentDto('1', false, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null);
const content_2 = content_1.publish('me', now);
expect(content_2.isPublished).toBeTruthy();
@ -44,7 +44,7 @@ describe('ContentDto', () => {
it('should update isPublished property and user info when unpublishing', () => {
const now = DateTime.now();
const content_1 = new ContentDto('1', true, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null);
const content_1 = new ContentDto('1', true, false, 'other', 'other', DateTime.now(), DateTime.now(), { data: 1 }, null);
const content_2 = content_1.unpublish('me', now);
expect(content_2.isPublished).toBeFalsy();
@ -115,12 +115,12 @@ describe('ContentsService', () => {
expect(contents).toEqual(
new ContentsDto(10, [
new ContentDto('id1', true, 'Created1', 'LastModifiedBy1',
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},
new Version('11')),
new ContentDto('id2', true, 'Created2', 'LastModifiedBy2',
new ContentDto('id2', true, false, 'Created2', 'LastModifiedBy2',
DateTime.parseISO_UTC('2016-10-12T10:10'),
DateTime.parseISO_UTC('2017-10-12T10:10'),
{},
@ -205,7 +205,7 @@ describe('ContentsService', () => {
});
expect(content).toEqual(
new ContentDto('id1', true, 'Created1', 'LastModifiedBy1',
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},
@ -263,7 +263,7 @@ describe('ContentsService', () => {
});
expect(content).toEqual(
new ContentDto('id1', true, 'Created1', 'LastModifiedBy1',
new ContentDto('id1', true, false, 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'),
{},

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

@ -31,6 +31,7 @@ export class ContentDto {
constructor(
public readonly id: string,
public readonly isPublished: boolean,
public readonly isDeleted: boolean,
public readonly createdBy: string,
public readonly lastModifiedBy: string,
public readonly created: DateTime,
@ -44,6 +45,7 @@ export class ContentDto {
return new ContentDto(
this.id,
true,
this.isDeleted,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
@ -54,6 +56,7 @@ export class ContentDto {
return new ContentDto(
this.id,
false,
this.isDeleted,
this.createdBy, user,
this.created, now || DateTime.now(),
this.data,
@ -64,6 +67,7 @@ export class ContentDto {
return new ContentDto(
this.id,
this.isPublished,
this.isDeleted,
this.createdBy, user,
this.created, now || DateTime.now(),
data,
@ -117,6 +121,7 @@ export class ContentsService {
return new ContentDto(
item.id,
item.isPublished,
item.isDeleted === true,
item.createdBy,
item.lastModifiedBy,
DateTime.parseISO_UTC(item.created),
@ -136,6 +141,7 @@ export class ContentsService {
return new ContentDto(
response.id,
response.isPublished,
response.isDeleted === true,
response.createdBy,
response.lastModifiedBy,
DateTime.parseISO_UTC(response.created),
@ -164,7 +170,7 @@ export class ContentsService {
.map(response => {
return new ContentDto(
response.id,
response.isPublished,
response.isPublished, false,
response.createdBy,
response.lastModifiedBy,
DateTime.parseISO_UTC(response.created),

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

@ -52,20 +52,22 @@ describe('HistoryService', () => {
actor: 'User1',
eventId: '1',
message: 'Message 1',
version: 2,
created: '2016-12-12T10:10'
},
{
actor: 'User2',
eventId: '2',
message: 'Message 2',
version: 3,
created: '2016-12-13T10:10'
}
]);
expect(events).toEqual(
[
new HistoryEventDto('1', 'User1', 'Message 1', DateTime.parseISO_UTC('2016-12-12T10:10')),
new HistoryEventDto('2', 'User2', 'Message 2', DateTime.parseISO_UTC('2016-12-13T10:10'))
new HistoryEventDto('1', 'User1', 'Message 1', 2, DateTime.parseISO_UTC('2016-12-12T10:10')),
new HistoryEventDto('2', 'User2', 'Message 2', 3, DateTime.parseISO_UTC('2016-12-13T10:10'))
]);
}));
});

2
src/Squidex/app/shared/services/history.service.ts

@ -22,6 +22,7 @@ export class HistoryEventDto {
public readonly eventId: string,
public readonly actor: string,
public readonly message: string,
public readonly version: number,
public readonly created: DateTime
) {
}
@ -47,6 +48,7 @@ export class HistoryService {
item.eventId,
item.actor,
item.message,
item.version,
DateTime.parseISO_UTC(item.created));
});
})

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

@ -229,6 +229,21 @@ namespace Squidex.Domain.Apps.Write.Contents
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<delete-script>", "delete content")).MustHaveHappened();
}
[Fact]
public async Task Restore_should_update_domain_object()
{
CreateContent();
content.Delete(new DeleteContent());
var command = CreateContextForCommand(new RestoreContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(command);
});
}
private void CreateContent()
{
content.Create(new CreateContent { Data = data });

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

@ -303,6 +303,31 @@ namespace Squidex.Domain.Apps.Write.Contents
);
}
[Fact]
public void Restore_should_throw_exception_if_not_deleted()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateContentCommand(new DeleteContent()));
});
}
[Fact]
public void Restore_should_update_properties_create_events()
{
CreateContent();
DeleteContent();
sut.Restore(CreateContentCommand(new RestoreContent()));
Assert.False(sut.IsDeleted);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentRestored())
);
}
private void CreateContent()
{
sut.Create(CreateContentCommand(new CreateContent { Data = data }));

Loading…
Cancel
Save