Browse Source

Merge pull request #329 from Squidex/feature-comments

Feature comments
pull/330/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
e21ea2166e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
  2. 25
      src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs
  3. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs
  4. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs
  5. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs
  6. 126
      src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
  7. 96
      src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs
  8. 88
      src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs
  9. 18
      src/Squidex.Domain.Apps.Entities/Comments/ICommentGrain.cs
  10. 13
      src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs
  11. 4
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  12. 20
      src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs
  13. 18
      src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs
  14. 20
      src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs
  15. 16
      src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs
  16. 51
      src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  17. 4
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  18. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs
  19. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/UpdateAssetDto.cs
  20. 137
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  21. 53
      src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs
  22. 48
      src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs
  23. 33
      src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs
  24. 5
      src/Squidex/Config/Domain/EntitiesServices.cs
  25. 4
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  26. 1
      src/Squidex/app/features/content/declarations.ts
  27. 8
      src/Squidex/app/features/content/module.ts
  28. 1
      src/Squidex/app/features/content/pages/comments/comments-page.component.html
  29. 2
      src/Squidex/app/features/content/pages/comments/comments-page.component.scss
  30. 30
      src/Squidex/app/features/content/pages/comments/comments-page.component.ts
  31. 2
      src/Squidex/app/features/content/pages/content/content-history.component.html
  32. 2
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  33. 4
      src/Squidex/app/features/content/pages/content/content-page.component.html
  34. 6
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  35. 6
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  36. 2
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  37. 2
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  38. 2
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  39. 2
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  40. 2
      src/Squidex/app/framework/angular/forms/validators.ts
  41. 12
      src/Squidex/app/framework/angular/http/http-extensions.ts
  42. 19
      src/Squidex/app/shared/components/comment.component.html
  43. 43
      src/Squidex/app/shared/components/comment.component.scss
  44. 38
      src/Squidex/app/shared/components/comment.component.ts
  45. 26
      src/Squidex/app/shared/components/comments.component.html
  46. 11
      src/Squidex/app/shared/components/comments.component.scss
  47. 80
      src/Squidex/app/shared/components/comments.component.ts
  48. 2
      src/Squidex/app/shared/components/history.component.html
  49. 2
      src/Squidex/app/shared/declarations.ts
  50. 3
      src/Squidex/app/shared/internal.ts
  51. 8
      src/Squidex/app/shared/module.ts
  52. 2
      src/Squidex/app/shared/services/assets.service.ts
  53. 2
      src/Squidex/app/shared/services/auth.service.ts
  54. 152
      src/Squidex/app/shared/services/comments.service.spec.ts
  55. 136
      src/Squidex/app/shared/services/comments.service.ts
  56. 2
      src/Squidex/app/shared/services/contents.service.ts
  57. 2
      src/Squidex/app/shared/services/schemas.service.ts
  58. 4
      src/Squidex/app/shared/state/apps.state.spec.ts
  59. 5
      src/Squidex/app/shared/state/apps.state.ts
  60. 22
      src/Squidex/app/shared/state/comments.form.ts
  61. 119
      src/Squidex/app/shared/state/comments.state.spec.ts
  62. 128
      src/Squidex/app/shared/state/comments.state.ts
  63. 2
      src/Squidex/app/shared/state/contents.state.ts
  64. 2
      src/Squidex/app/shared/state/rules.state.ts
  65. 2
      src/Squidex/app/shared/state/schemas.state.ts
  66. 8
      src/Squidex/app/theme/icomoon/demo-files/demo.css
  67. 324
      src/Squidex/app/theme/icomoon/demo.html
  68. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  69. 1
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  70. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  71. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  72. 2
      src/Squidex/app/theme/icomoon/selection.json
  73. 40
      src/Squidex/app/theme/icomoon/style.css
  74. 162
      tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs
  75. 164
      tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs

32
src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Comments
{
public sealed class Comment
{
public Guid Id { get; }
public Instant Time { get; }
public RefToken User { get; }
public string Text { get; }
public Comment(Guid id, Instant time, RefToken user, string text)
{
Id = id;
Time = time;
Text = text;
User = user;
}
}
}

25
src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public abstract class CommentsCommand : SquidexCommand, IAggregateCommand, IAppCommand
{
public Guid CommentsId { get; set; }
public NamedId<Guid> AppId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return CommentsId; }
}
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class CreateComment : CommentsCommand
{
public Guid CommentId { get; } = Guid.NewGuid();
public string Text { get; set; }
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class DeleteComment : CommentsCommand
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class UpdateComment : CommentsCommand
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

126
src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs

@ -0,0 +1,126 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Comments.Guards;
using Squidex.Domain.Apps.Entities.Comments.State;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Comments
{
public sealed class CommentsGrain : DomainObjectGrainBase<CommentsState>, ICommentGrain
{
private readonly IStore<Guid> store;
private readonly List<Envelope<CommentsEvent>> events = new List<Envelope<CommentsEvent>>();
private CommentsState snapshot = new CommentsState { Version = EtagVersion.Empty };
private IPersistence persistence;
public override CommentsState Snapshot
{
get { return snapshot; }
}
public CommentsGrain(IStore<Guid> store, ISemanticLog log)
: base(log)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
protected override void ApplyEvent(Envelope<IEvent> @event)
{
snapshot = new CommentsState { Version = snapshot.Version + 1 };
events.Add(@event.To<CommentsEvent>());
}
protected override void RestorePreviousSnapshot(CommentsState previousSnapshot, long previousVersion)
{
snapshot = previousSnapshot;
}
protected override Task ReadAsync(Type type, Guid id)
{
persistence = store.WithEventSourcing<Guid>(GetType(), id, ApplyEvent);
return persistence.ReadAsync();
}
protected override async Task WriteAsync(Envelope<IEvent>[] events, long previousVersion)
{
if (events.Length > 0)
{
await persistence.WriteEventsAsync(events);
}
}
protected override Task<object> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{
case CreateComment createComment:
return UpsertAsync(createComment, c =>
{
GuardComments.CanCreate(c);
Create(c);
return EntityCreatedResult.Create(createComment.CommentId, Version);
});
case UpdateComment updateComment:
return UpsertAsync(updateComment, c =>
{
GuardComments.CanUpdate(events, c);
Update(c);
});
case DeleteComment deleteComment:
return UpsertAsync(deleteComment, c =>
{
GuardComments.CanDelete(events, c);
Delete(c);
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateComment command)
{
RaiseEvent(SimpleMapper.Map(command, new CommentCreated()));
}
public void Update(UpdateComment command)
{
RaiseEvent(SimpleMapper.Map(command, new CommentUpdated()));
}
public void Delete(DeleteComment command)
{
RaiseEvent(SimpleMapper.Map(command, new CommentDeleted()));
}
public Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any)
{
return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)version));
}
}
}

96
src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs

@ -0,0 +1,96 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Comments;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Comments
{
public sealed class CommentsResult
{
public List<Comment> CreatedComments { get; set; } = new List<Comment>();
public List<Comment> UpdatedComments { get; set; } = new List<Comment>();
public List<Guid> DeletedComments { get; set; } = new List<Guid>();
public long Version { get; set; }
public static CommentsResult FromEvents(IEnumerable<Envelope<CommentsEvent>> events, long currentVersion, int lastVersion)
{
var result = new CommentsResult { Version = currentVersion };
foreach (var @event in events.Skip(lastVersion < 0 ? 0 : lastVersion + 1))
{
switch (@event.Payload)
{
case CommentDeleted deleted:
{
var id = deleted.CommentId;
if (result.CreatedComments.Any(x => x.Id == id))
{
result.CreatedComments.RemoveAll(x => x.Id == id);
}
else if (result.UpdatedComments.Any(x => x.Id == id))
{
result.UpdatedComments.RemoveAll(x => x.Id == id);
result.DeletedComments.Add(id);
}
else
{
result.DeletedComments.Add(id);
}
break;
}
case CommentCreated created:
{
var comment = new Comment(
created.CommentId,
@event.Headers.Timestamp(),
@event.Payload.Actor,
created.Text);
result.CreatedComments.Add(comment);
break;
}
case CommentUpdated updated:
{
var id = updated.CommentId;
var comment = new Comment(
id,
@event.Headers.Timestamp(),
@event.Payload.Actor,
updated.Text);
if (result.CreatedComments.Any(x => x.Id == id))
{
result.CreatedComments.RemoveAll(x => x.Id == id);
result.CreatedComments.Add(comment);
}
else
{
result.UpdatedComments.Add(comment);
}
break;
}
}
}
return result;
}
}
}

88
src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs

@ -0,0 +1,88 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Comments.Guards
{
public static class GuardComments
{
public static void CanCreate(CreateComment command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot create comment.", e =>
{
if (string.IsNullOrWhiteSpace(command.Text))
{
e("Text is required.", nameof(command.Text));
}
});
}
public static void CanUpdate(List<Envelope<CommentsEvent>> events, UpdateComment command)
{
Guard.NotNull(command, nameof(command));
var comment = FindComment(events, command.CommentId);
if (!comment.Payload.Actor.Equals(command.Actor))
{
throw new DomainException("Comment is created by another actor.");
}
Validate.It(() => "Cannot update comment.", e =>
{
if (string.IsNullOrWhiteSpace(command.Text))
{
e("Text is required.", nameof(command.Text));
}
});
}
public static void CanDelete(List<Envelope<CommentsEvent>> events, DeleteComment command)
{
Guard.NotNull(command, nameof(command));
var comment = FindComment(events, command.CommentId);
if (!comment.Payload.Actor.Equals(command.Actor))
{
throw new DomainException("Comment is created by another actor.");
}
}
private static Envelope<CommentCreated> FindComment(List<Envelope<CommentsEvent>> events, Guid commentId)
{
Envelope<CommentCreated> result = null;
foreach (var @event in events)
{
if (@event.Payload is CommentCreated created && created.CommentId == commentId)
{
result = @event.To<CommentCreated>();
}
else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId)
{
result = null;
}
}
if (result == null)
{
throw new DomainObjectNotFoundException(commentId.ToString(), "Comments", typeof(CommentsGrain));
}
return result;
}
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/ICommentGrain.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Comments
{
public interface ICommentGrain : IDomainObjectGrain
{
Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any);
}
}

13
src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Comments.State
{
public sealed class CommentsState : DomainObjectState<CommentsState>
{
}
}

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

@ -28,8 +28,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
public Status Status { get; set; }
public ScheduleJob ScheduleJob { get; set; } public ScheduleJob ScheduleJob { get; set; }
public RefToken CreatedBy { get; set; } public RefToken CreatedBy { get; set; }
@ -40,6 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
public NamedContentData DataDraft { get; set; } public NamedContentData DataDraft { get; set; }
public Status Status { get; set; }
public bool IsPending { get; set; } public bool IsPending { get; set; }
public static ContentEntity Create(CreateContent command, EntityCreatedResult<NamedContentData> result) public static ContentEntity Create(CreateContent command, EntityCreatedResult<NamedContentData> result)

20
src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Comments
{
[EventType(nameof(CommentCreated))]
public sealed class CommentCreated : CommentsEvent
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

18
src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Comments
{
[EventType(nameof(CommentDeleted))]
public sealed class CommentDeleted : CommentsEvent
{
public Guid CommentId { get; set; }
}
}

20
src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Comments
{
[EventType(nameof(CommentUpdated))]
public sealed class CommentUpdated : CommentsEvent
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

16
src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Events.Comments
{
public abstract class CommentsEvent : AppEvent
{
public Guid CommentsId { get; set; }
}
}

51
src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -21,6 +21,13 @@ namespace Squidex.Infrastructure.Commands
private readonly ISemanticLog log; private readonly ISemanticLog log;
private Guid id; private Guid id;
private enum Mode
{
Create,
Update,
Upsert
}
public Guid Id public Guid Id
{ {
get { return id; } get { return id; }
@ -81,45 +88,65 @@ namespace Squidex.Infrastructure.Commands
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler, false); return InvokeAsync(command, handler, Mode.Create);
} }
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler?.ToAsync(), false); return InvokeAsync(command, handler?.ToAsync(), Mode.Create);
} }
protected Task<object> CreateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand protected Task<object> CreateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler.ToDefault<TCommand, object>(), false); return InvokeAsync(command, handler.ToDefault<TCommand, object>(), Mode.Create);
} }
protected Task<object> CreateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand protected Task<object> CreateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), false); return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Create);
} }
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler, true); return InvokeAsync(command, handler, Mode.Update);
} }
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler?.ToAsync(), true); return InvokeAsync(command, handler?.ToAsync(), Mode.Update);
} }
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), true); return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), Mode.Update);
} }
protected Task<object> UpdateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand protected Task<object> UpdateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), true); return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Update);
}
protected Task<object> UpsertReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Upsert);
} }
private async Task<object> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler, bool isUpdate) where TCommand : class, IAggregateCommand private async Task<object> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler, Mode mode) where TCommand : class, IAggregateCommand
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
@ -128,7 +155,7 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion);
} }
if (isUpdate && Version < 0) if (mode == Mode.Update && Version < 0)
{ {
try try
{ {
@ -141,7 +168,7 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectNotFoundException(id.ToString(), GetType()); throw new DomainObjectNotFoundException(id.ToString(), GetType());
} }
if (!isUpdate && Version >= 0) if (mode == Mode.Create && Version >= 0)
{ {
throw new DomainException("Object has already been created."); throw new DomainException("Object has already been created.");
} }
@ -158,7 +185,7 @@ namespace Squidex.Infrastructure.Commands
if (result == null) if (result == null)
{ {
if (isUpdate) if (mode == Mode.Update || (mode == Mode.Upsert && Version == 0))
{ {
result = new EntitySavedResult(Version); result = new EntitySavedResult(Version);
} }

4
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -215,7 +215,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetSavedResult>(); var result = context.Result<AssetSavedResult>();
var response = AssetReplacedDto.Create(command, result); var response = AssetReplacedDto.FromCommand(command, result);
return StatusCode(201, response); return StatusCode(201, response);
} }
@ -236,7 +236,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/{id}/")] [Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request) public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] UpdateAssetDto request)
{ {
await CommandBus.PublishAsync(request.ToCommand(id)); await CommandBus.PublishAsync(request.ToCommand(id));

2
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs

@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static AssetReplacedDto Create(UpdateAsset command, AssetSavedResult result) public static AssetReplacedDto FromCommand(UpdateAsset command, AssetSavedResult result)
{ {
var response = new AssetReplacedDto var response = new AssetReplacedDto
{ {

2
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs → src/Squidex/Areas/Api/Controllers/Assets/Models/UpdateAssetDto.cs

@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Entities.Assets.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
{ {
public sealed class AssetUpdateDto public sealed class UpdateAssetDto
{ {
/// <summary> /// <summary>
/// The new name of the asset. /// The new name of the asset.

137
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -0,0 +1,137 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Areas.Api.Controllers.Comments.Models;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Comments
{
/// <summary>
/// Manages comments for any kind of resource.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Comments))]
public sealed class CommentsController : ApiController
{
private readonly IGrainFactory grainFactory;
public CommentsController(ICommandBus commandBus, IGrainFactory grainFactory)
: base(commandBus)
{
this.grainFactory = grainFactory;
}
/// <summary>
/// Get all comments.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="commentsId">The id of the comments.</param>
/// <returns>
/// 200 => All comments returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentsDto), 200)]
[ApiCosts(0)]
public async Task<IActionResult> GetComments(string app, Guid commentsId)
{
if (!long.TryParse(Request.Headers["If-None-Match"], out var version))
{
version = EtagVersion.Any;
}
var result = await grainFactory.GetGrain<ICommentGrain>(commentsId).GetCommentsAsync(version);
var response = CommentsDto.FromResult(result);
Response.Headers["ETag"] = response.Version.ToString();
return Ok(response);
}
/// <summary>
/// Create a new comment.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="commentsId">The id of the comments.</param>
/// <param name="request">The comment object that needs to created.</param>
/// <returns>
/// 201 => Comment created.
/// 400 => Comment is not valid.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(0)]
public async Task<IActionResult> PostComment(string app, Guid commentsId, [FromBody] UpsertCommentDto request)
{
var command = request.ToCreateCommand(commentsId);
var context = await CommandBus.PublishAsync(command);
var response = CommentDto.FromCommand(command);
return CreatedAtAction(nameof(GetComments), new { commentsId }, response);
}
/// <summary>
/// Updates the comment.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="commentsId">The id of the comments.</param>
/// <param name="commentId">The id of the comment.</param>
/// <param name="request">The comment object that needs to updated.</param>
/// <returns>
/// 204 => Comment updated.
/// 400 => Comment text not valid.
/// 404 => Comment or app not found.
/// </returns>
[MustBeAppReader]
[HttpPut]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(0)]
public async Task<IActionResult> PutComment(string app, Guid commentsId, Guid commentId, [FromBody] UpsertCommentDto request)
{
await CommandBus.PublishAsync(request.ToUpdateComment(commentsId, commentId));
return NoContent();
}
/// <summary>
/// Deletes the comment.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="commentsId">The id of the comments.</param>
/// <param name="commentId">The id of the comment.</param>
/// <returns>
/// 204 => Comment deleted.
/// 404 => Comment or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteComment(string app, Guid commentsId, Guid commentId)
{
await CommandBus.PublishAsync(new DeleteComment { CommentsId = commentsId, CommentId = commentId });
return NoContent();
}
}
}

53
src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Core.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Comments.Models
{
public sealed class CommentDto
{
/// <summary>
/// The id of the comment.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The time when the comment was created or updated last.
/// </summary>
[Required]
public Instant Time { get; set; }
/// <summary>
/// The user who created or updated the comment.
/// </summary>
[Required]
public RefToken User { get; set; }
/// <summary>
/// The text of the comment.
/// </summary>
[Required]
public string Text { get; set; }
public static CommentDto FromComment(Comment comment)
{
return SimpleMapper.Map(comment, new CommentDto());
}
public static CommentDto FromCommand(CreateComment command)
{
return SimpleMapper.Map(command, new CommentDto { Id = command.CommentId, User = command.Actor, Time = SystemClock.Instance.GetCurrentInstant() });
}
}
}

48
src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Entities.Comments;
namespace Squidex.Areas.Api.Controllers.Comments.Models
{
public sealed class CommentsDto
{
/// <summary>
/// The created comments including the updates.
/// </summary>
public List<CommentDto> CreatedComments { get; set; }
/// <summary>
/// The updates comments since the last version.
/// </summary>
public List<CommentDto> UpdatedComments { get; set; }
/// <summary>
/// The deleted comments since the last version.
/// </summary>
public List<Guid> DeletedComments { get; set; }
/// <summary>
/// The current version.
/// </summary>
public long Version { get; set; }
public static CommentsDto FromResult(CommentsResult result)
{
return new CommentsDto
{
CreatedComments = result.CreatedComments.Select(CommentDto.FromComment).ToList(),
UpdatedComments = result.UpdatedComments.Select(CommentDto.FromComment).ToList(),
DeletedComments = result.DeletedComments,
Version = result.Version
};
}
}
}

33
src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Comments.Models
{
public sealed class UpsertCommentDto
{
/// <summary>
/// The comment text.
/// </summary>
[Required]
public string Text { get; set; }
public CreateComment ToCreateCommand(Guid commentsId)
{
return SimpleMapper.Map(this, new CreateComment { CommentsId = commentsId });
}
public UpdateComment ToUpdateComment(Guid commentsId, Guid commentId)
{
return SimpleMapper.Map(this, new UpdateComment { CommentsId = commentsId, CommentId = commentId });
}
}
}

5
src/Squidex/Config/Domain/EntitiesServices.cs

@ -24,6 +24,8 @@ using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Edm; using Squidex.Domain.Apps.Entities.Contents.Edm;
@ -160,6 +162,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<GrainCommandMiddleware<AppCommand, IAppGrain>>() services.AddSingletonAs<GrainCommandMiddleware<AppCommand, IAppGrain>>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<ContentCommand, IContentGrain>>() services.AddSingletonAs<GrainCommandMiddleware<ContentCommand, IContentGrain>>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();

4
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -55,14 +55,14 @@ export class UserPageComponent implements OnDestroy, OnInit {
if (value) { if (value) {
if (this.user) { if (this.user) {
this.usersState.update(this.user.user, value) this.usersState.update(this.user.user, value)
.subscribe(user => { .subscribe(() => {
this.userForm.submitCompleted(); this.userForm.submitCompleted();
}, error => { }, error => {
this.userForm.submitFailed(error); this.userForm.submitFailed(error);
}); });
} else { } else {
this.usersState.create(value) this.usersState.create(value)
.subscribe(user => { .subscribe(() => {
this.back(); this.back();
}, error => { }, error => {
this.userForm.submitFailed(error); this.userForm.submitFailed(error);

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

@ -5,6 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
export * from './pages/comments/comments-page.component';
export * from './pages/content/content-field.component'; export * from './pages/content/content-field.component';
export * from './pages/content/content-history.component'; export * from './pages/content/content-history.component';
export * from './pages/content/content-page.component'; export * from './pages/content/content-page.component';

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

@ -25,6 +25,7 @@ import {
ArrayEditorComponent, ArrayEditorComponent,
ArrayItemComponent, ArrayItemComponent,
AssetsEditorComponent, AssetsEditorComponent,
CommentsPageComponent,
ContentFieldComponent, ContentFieldComponent,
ContentHistoryComponent, ContentHistoryComponent,
ContentItemComponent, ContentItemComponent,
@ -75,7 +76,11 @@ const routes: Routes = [
data: { data: {
channel: 'contents.{contentId}' channel: 'contents.{contentId}'
} }
} },
{
path: 'comments',
component: CommentsPageComponent
}
] ]
} }
] ]
@ -95,6 +100,7 @@ const routes: Routes = [
ArrayEditorComponent, ArrayEditorComponent,
ArrayItemComponent, ArrayItemComponent,
AssetsEditorComponent, AssetsEditorComponent,
CommentsPageComponent,
ContentFieldComponent, ContentFieldComponent,
ContentHistoryComponent, ContentHistoryComponent,
ContentItemComponent, ContentItemComponent,

1
src/Squidex/app/features/content/pages/comments/comments-page.component.html

@ -0,0 +1 @@
<sqx-comments [commentsId]="commentsId"></sqx-comments>

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

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

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

@ -0,0 +1,30 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { allParams } from '@app/shared';
@Component({
selector: 'sqx-comments-page',
styleUrls: ['./comments-page.component.scss'],
templateUrl: './comments-page.component.html'
})
export class CommentsPageComponent implements OnInit {
public commentsId: string;
constructor(
private readonly route: ActivatedRoute
) {
}
public ngOnInit() {
this.commentsId = allParams(this.route)['contentId'];
}
}

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

@ -1,4 +1,4 @@
<sqx-panel desiredWidth="16rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
Activity Activity
</ng-container> </ng-container>

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

@ -51,7 +51,7 @@ export class ContentHistoryComponent {
timer(0, 10000), timer(0, 10000),
this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)) this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000))
).pipe( ).pipe(
switchMap(app => this.historyService.getHistory(this.appsState.appName, this.channel))); switchMap(() => this.historyService.getHistory(this.appsState.appName, this.channel)));
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,

4
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -125,6 +125,10 @@
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<a class="panel-link" routerLink="comments" routerLinkActive="active" #linkHistory>
<i class="icon-comments"></i>
</a>
<sqx-onboarding-tooltip helpId="history" [for]="linkHistory" position="leftTop" after="120000"> <sqx-onboarding-tooltip helpId="history" [for]="linkHistory" position="leftTop" after="120000">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>

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

@ -141,14 +141,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
if (this.content) { if (this.content) {
if (asProposal) { if (asProposal) {
this.contentsState.proposeUpdate(this.content, value) this.contentsState.proposeUpdate(this.content, value)
.subscribe(dto => { .subscribe(() => {
this.contentForm.submitCompleted(); this.contentForm.submitCompleted();
}, error => { }, error => {
this.contentForm.submitFailed(error); this.contentForm.submitFailed(error);
}); });
} else { } else {
this.contentsState.update(this.content, value) this.contentsState.update(this.content, value)
.subscribe(dto => { .subscribe(() => {
this.contentForm.submitCompleted(); this.contentForm.submitCompleted();
}, error => { }, error => {
this.contentForm.submitFailed(error); this.contentForm.submitFailed(error);
@ -156,7 +156,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
} }
} else { } else {
this.contentsState.create(value, publish) this.contentsState.create(value, publish)
.subscribe(dto => { .subscribe(() => {
this.back(); this.back();
}, error => { }, error => {
this.contentForm.submitFailed(error); this.contentForm.submitFailed(error);

6
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts

@ -121,7 +121,7 @@ export class RuleWizardComponent implements OnInit {
const requestDto = new CreateRuleDto(this.trigger, this.action); const requestDto = new CreateRuleDto(this.trigger, this.action);
this.rulesState.create(requestDto) this.rulesState.create(requestDto)
.subscribe(dto => { .subscribe(() => {
this.complete(); this.complete();
this.actionForm.submitCompleted(); this.actionForm.submitCompleted();
@ -134,7 +134,7 @@ export class RuleWizardComponent implements OnInit {
private updateTrigger() { private updateTrigger() {
this.rulesState.updateTrigger(this.rule, this.trigger) this.rulesState.updateTrigger(this.rule, this.trigger)
.subscribe(dto => { .subscribe(() => {
this.complete(); this.complete();
this.triggerForm.submitCompleted(); this.triggerForm.submitCompleted();
@ -145,7 +145,7 @@ export class RuleWizardComponent implements OnInit {
private updateAction() { private updateAction() {
this.rulesState.updateAction(this.rule, this.action) this.rulesState.updateAction(this.rule, this.action)
.subscribe(dto => { .subscribe(() => {
this.complete(); this.complete();
this.actionForm.submitCompleted(); this.actionForm.submitCompleted();

2
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts

@ -60,7 +60,7 @@ export class FieldWizardComponent implements OnInit {
if (value) { if (value) {
this.schemasState.addField(this.schema, value, this.parent) this.schemasState.addField(this.schema, value, this.parent)
.subscribe(dto => { .subscribe(() => {
this.addFieldForm.submitCompleted({ type: fieldTypes[0].type }); this.addFieldForm.submitCompleted({ type: fieldTypes[0].type });
if (next) { if (next) {

2
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts

@ -47,7 +47,7 @@ export class SchemaEditFormComponent implements OnInit {
if (value) { if (value) {
this.schemasState.update(this.schema, value) this.schemasState.update(this.schema, value)
.subscribe(dto => { .subscribe(() => {
this.complete(); this.complete();
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);

2
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts

@ -53,7 +53,7 @@ export class SchemaScriptsFormComponent implements OnInit {
if (value) { if (value) {
this.schemasState.configureScripts(this.schema, value) this.schemasState.configureScripts(this.schema, value)
.subscribe(dto => { .subscribe(() => {
this.complete(); this.complete();
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);

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

@ -94,7 +94,7 @@ export class ContributorsPageComponent implements OnInit {
const requestDto = new AppContributorDto(user, 'Editor'); const requestDto = new AppContributorDto(user, 'Editor');
this.contributorsState.assign(requestDto) this.contributorsState.assign(requestDto)
.subscribe(dto => { .subscribe(() => {
this.assignContributorForm.submitCompleted(); this.assignContributorForm.submitCompleted();
}, error => { }, error => {
this.assignContributorForm.submitFailed(error); this.assignContributorForm.submitFailed(error);

2
src/Squidex/app/framework/angular/forms/validators.ts

@ -126,7 +126,7 @@ export module ValidatorsEx {
} }
export function noop(): ValidatorFn { export function noop(): ValidatorFn {
return (control: AbstractControl) => { return () => {
return null; return null;
}; };
} }

12
src/Squidex/app/framework/angular/http/http-extensions.ts

@ -17,31 +17,31 @@ export module HTTP {
export function getVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> { export function getVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.get<T>(url, { observe: 'response', headers }), version); return handleVersion(http.get<T>(url, { observe: 'response', headers }));
} }
export function postVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> { export function postVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.post<T>(url, body, { observe: 'response', headers }), version); return handleVersion(http.post<T>(url, body, { observe: 'response', headers }));
} }
export function putVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> { export function putVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.put<T>(url, body, { observe: 'response', headers }), version); return handleVersion(http.put<T>(url, body, { observe: 'response', headers }));
} }
export function patchVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> { export function patchVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.request<T>('PATCH', url, { body, observe: 'response', headers }), version); return handleVersion(http.request<T>('PATCH', url, { body, observe: 'response', headers }));
} }
export function deleteVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> { export function deleteVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.delete<T>(url, { observe: 'response', headers }), version); return handleVersion(http.delete<T>(url, { observe: 'response', headers }));
} }
function createHeaders(version?: Version): HttpHeaders { function createHeaders(version?: Version): HttpHeaders {
@ -52,7 +52,7 @@ export module HTTP {
} }
} }
function handleVersion<T>(httpRequest: Observable<HttpResponse<T>>, version?: Version): Observable<Versioned<HttpResponse<T>>> { function handleVersion<T>(httpRequest: Observable<HttpResponse<T>>): Observable<Versioned<HttpResponse<T>>> {
return httpRequest.pipe(map((response: HttpResponse<T>) => { return httpRequest.pipe(map((response: HttpResponse<T>) => {
const etag = response.headers.get('etag') || ''; const etag = response.headers.get('etag') || '';

19
src/Squidex/app/shared/components/comment.component.html

@ -0,0 +1,19 @@
<div class="comment row no-gutters">
<div class="col col-auto">
<img class="user-picture" [attr.title]="comment.user | sqxUserNameRef:null" [attr.src]="comment.user | sqxUserPictureRef" />
</div>
<div class="col pl-2">
<div class="comment-message">
<div class="user-row">
<div class="user-ref">{{comment.user | sqxUserNameRef:null}}</div>
<button *ngIf="comment.user === userId" type="button" class="btn btn-sm btn-link btn-danger item-remove" (click)="deleting.emit()!">
<i class="icon-bin2"></i>
</button>
</div>
<div>{{comment.text}}</div>
<div class="comment-created text-muted">{{comment.time | sqxFromNow}}</div>
</div>
</div>
</div>

43
src/Squidex/app/shared/components/comment.component.scss

@ -0,0 +1,43 @@
@import '_vars';
@import '_mixins';
.user-ref {
font-weight: bold;
}
.item-remove {
@include absolute(-5px, -15px, auto, auto);
display: none;
}
.user-row {
& {
position: relative;
}
&:hover {
.item-remove {
display: block;
}
}
}
.comment {
& {
font-size: .9rem;
font-weight: normal;
margin-bottom: .75rem;
}
&-message {
margin-bottom: .375rem;
}
&-created {
font-size: .75rem;
}
}
.text-muted {
color: $color-history;
}

38
src/Squidex/app/shared/components/comment.component.ts

@ -0,0 +1,38 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { CommentDto, UpsertCommentForm } from '@app/shared/internal';
@Component({
selector: 'sqx-comment',
styleUrls: ['./comment.component.scss'],
templateUrl: './comment.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommentComponent {
public editForm = new UpsertCommentForm(this.formBuilder);
@Input()
public comment: CommentDto;
@Input()
public userId: string;
@Output()
public deleting = new EventEmitter();
@Output()
public updated = new EventEmitter<string>();
constructor(
private readonly formBuilder: FormBuilder
) {
}
}

26
src/Squidex/app/shared/components/comments.component.html

@ -0,0 +1,26 @@
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false" contentClass="grid">
<ng-container title>
Comments
</ng-container>
<ng-container content>
<div class="grid-content" #scrollMe [scrollTop]="scrollMe.scrollHeight">
<sqx-comment *ngFor="let comment of state.comments | async; trackBy: trackByComment"
[comment]="comment"
[userId]="userId"
(updated)="update(comment, $event)"
(deleting)="delete(comment)">
</sqx-comment>
</div>
<div class="grid-footer">
<form [formGroup]="commentForm.form" (submit)="comment()">
<input class="form-control" name="text" formControlName="text" placeholder="Create a comment" />
</form>
</div>
</ng-container>
</sqx-panel>

11
src/Squidex/app/shared/components/comments.component.scss

@ -0,0 +1,11 @@
@import '_vars';
@import '_mixins';
.grid-footer {
border-top-width: 1px;
}
.grid-body,
.grid-footer {
padding: 1rem;
}

80
src/Squidex/app/shared/components/comments.component.ts

@ -0,0 +1,80 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import {
AppsState,
AuthService,
CommentDto,
CommentsService,
CommentsState,
DialogService,
UpsertCommentForm
} from '@app/shared/internal';
@Component({
selector: 'sqx-comments',
styleUrls: ['./comments.component.scss'],
templateUrl: './comments.component.html'
})
export class CommentsComponent implements OnDestroy, OnInit {
private timer: Subscription;
public state: CommentsState;
public userId: string;
public commentForm = new UpsertCommentForm(this.formBuilder);
@Input()
public commentsId: string;
constructor(authService: AuthService,
private readonly appsState: AppsState,
private readonly commentsService: CommentsService,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder
) {
this.userId = authService.user!.token;
}
public ngOnDestroy() {
this.timer.unsubscribe();
}
public ngOnInit() {
this.state = new CommentsState(this.appsState, this.commentsId, this.commentsService, this.dialogs);
this.timer = timer(0, 4000).pipe(switchMap(() => this.state.load()), onErrorResumeNext()).subscribe();
}
public delete(comment: CommentDto) {
this.state.delete(comment.id).pipe(onErrorResumeNext()).subscribe();
}
public update(comment: CommentDto, text: string) {
this.state.update(comment.id, text).pipe(onErrorResumeNext()).subscribe();
}
public comment() {
const value = this.commentForm.submit();
if (value) {
this.state.create(value.text).pipe(onErrorResumeNext()).subscribe();
this.commentForm.submitCompleted({});
}
}
public trackByComment(index: number, comment: CommentDto) {
return comment.id;
}
}

2
src/Squidex/app/shared/components/history.component.html

@ -1,4 +1,4 @@
<sqx-panel desiredWidth="16rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
Activity Activity
</ng-container> </ng-container>

2
src/Squidex/app/shared/declarations.ts

@ -9,6 +9,8 @@ export * from './components/app-form.component';
export * from './components/asset.component'; export * from './components/asset.component';
export * from './components/assets-list.component'; export * from './components/assets-list.component';
export * from './components/assets-selector.component'; export * from './components/assets-selector.component';
export * from './components/comment.component';
export * from './components/comments.component';
export * from './components/help.component'; export * from './components/help.component';
export * from './components/geolocation-editor.component'; export * from './components/geolocation-editor.component';
export * from './components/history.component'; export * from './components/history.component';

3
src/Squidex/app/shared/internal.ts

@ -27,6 +27,7 @@ export * from './services/apps.service';
export * from './services/assets.service'; export * from './services/assets.service';
export * from './services/auth.service'; export * from './services/auth.service';
export * from './services/backups.service'; export * from './services/backups.service';
export * from './services/comments.service';
export * from './services/contents.service'; export * from './services/contents.service';
export * from './services/graphql.service'; export * from './services/graphql.service';
export * from './services/help.service'; export * from './services/help.service';
@ -49,6 +50,8 @@ export * from './state/backups.forms';
export * from './state/backups.state'; export * from './state/backups.state';
export * from './state/clients.forms'; export * from './state/clients.forms';
export * from './state/clients.state'; export * from './state/clients.state';
export * from './state/comments.form';
export * from './state/comments.state';
export * from './state/contents.forms'; export * from './state/contents.forms';
export * from './state/contents.state'; export * from './state/contents.state';
export * from './state/contributors.forms'; export * from './state/contributors.forms';

8
src/Squidex/app/shared/module.ts

@ -34,6 +34,9 @@ import {
BackupsService, BackupsService,
BackupsState, BackupsState,
ClientsState, ClientsState,
CommentComponent,
CommentsComponent,
CommentsService,
ContentMustExistGuard, ContentMustExistGuard,
ContentsService, ContentsService,
ContentsState, ContentsState,
@ -97,6 +100,8 @@ import {
AssetUrlPipe, AssetUrlPipe,
AssetsListComponent, AssetsListComponent,
AssetsSelectorComponent, AssetsSelectorComponent,
CommentComponent,
CommentsComponent,
FileIconPipe, FileIconPipe,
GeolocationEditorComponent, GeolocationEditorComponent,
HelpComponent, HelpComponent,
@ -122,6 +127,8 @@ import {
AssetUrlPipe, AssetUrlPipe,
AssetsListComponent, AssetsListComponent,
AssetsSelectorComponent, AssetsSelectorComponent,
CommentComponent,
CommentsComponent,
FileIconPipe, FileIconPipe,
GeolocationEditorComponent, GeolocationEditorComponent,
HelpComponent, HelpComponent,
@ -163,6 +170,7 @@ export class SqxSharedModule {
BackupsService, BackupsService,
BackupsState, BackupsState,
ClientsState, ClientsState,
CommentsService,
ContentMustExistGuard, ContentMustExistGuard,
ContentsService, ContentsService,
ContentsState, ContentsState,

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

@ -237,7 +237,7 @@ export class AssetsService {
throw 'Invalid'; throw 'Invalid';
} }
}), }),
tap(dto => { tap(() => {
this.analytics.trackEvent('Asset', 'Uploaded', appName); this.analytics.trackEvent('Asset', 'Uploaded', appName);
}), }),
pretifyError('Failed to upload asset. Please reload.')); pretifyError('Failed to upload asset. Please reload.'));

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

@ -185,7 +185,7 @@ export class AuthService {
} }
return true; return true;
}, err => { }, error => {
this.user$.next(null); this.user$.next(null);
return false; return false;

152
src/Squidex/app/shared/services/comments.service.spec.ts

@ -0,0 +1,152 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
CommentDto,
CommentsDto,
CommentsService,
DateTime,
UpsertCommentDto,
Version
} from './../';
describe('CommentsService', () => {
const user = 'me';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
CommentsService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get comments',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
let comments: CommentsDto;
commentsService.getComments('my-app', 'my-comments', new Version('123')).subscribe(result => {
comments = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-None-Match')).toBe('123');
req.flush({
createdComments: [{
id: '123',
text: 'text1',
time: '2016-10-12T10:10',
user: user
}],
updatedComments: [{
id: '456',
text: 'text2',
time: '2017-11-12T12:12',
user: user
}],
deletedComments: ['789'],
version: '9'
});
expect(comments!).toEqual(
new CommentsDto(
[
new CommentDto('123', DateTime.parseISO_UTC('2016-10-12T10:10'), 'text1', user)
], [
new CommentDto('456', DateTime.parseISO_UTC('2017-11-12T12:12'), 'text2', user)
], [
'789'
],
new Version('9'))
);
}));
it('should make get request to get comments and return empty result for 304',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
let comments: CommentsDto;
commentsService.getComments('my-app', 'my-comments', new Version('123')).subscribe(result => {
comments = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-None-Match')).toBe('123');
req.flush({}, { status: 304, statusText: 'NotModified' });
expect(comments!).toEqual(new CommentsDto([], [], [], new Version('123')));
}));
it('should make post request to create comment',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
let comment: CommentDto;
commentsService.postComment('my-app', 'my-comments', new UpsertCommentDto('text1')).subscribe(result => {
comment = <CommentDto>result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
id: '123',
text: 'text1',
time: '2016-10-12T10:10',
user: user
});
expect(comment!).toEqual(new CommentDto('123', DateTime.parseISO_UTC('2016-10-12T10:10'), 'text1', user));
}));
it('should make put request to replace comment content',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
commentsService.putComment('my-app', 'my-comments', '123', new UpsertCommentDto('text1')).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments/123');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make delete request to delete comment',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
commentsService.deleteComment('my-app', 'my-comments', '123').subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments/123');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
});

136
src/Squidex/app/shared/services/comments.service.ts

@ -0,0 +1,136 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import {
ApiUrlConfig,
DateTime,
Model,
pretifyError,
Types,
Version
} from '@app/framework';
export class CommentsDto extends Model {
constructor(
public readonly createdComments: CommentDto[],
public readonly updatedComments: CommentDto[],
public readonly deletedComments: string[],
public readonly version: Version
) {
super();
}
}
export class CommentDto extends Model {
constructor(
public readonly id: string,
public readonly time: DateTime,
public readonly text: string,
public readonly user: string
) {
super();
}
public with(value: Partial<CommentDto>): CommentDto {
return this.clone(value);
}
}
export class UpsertCommentDto {
constructor(
public readonly text: string
) {
}
}
@Injectable()
export class CommentsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getComments(appName: string, commentsId: string, version: Version): Observable<CommentsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}`);
const options = {
headers: new HttpHeaders().set('If-None-Match', version.value)
};
return this.http.get(url, options).pipe(
catchError(err => {
if (err.status === 304) {
return of(new CommentsDto([], [], [], version));
}
return throwError(err);
}),
map(response => {
if (Types.is(response, CommentsDto)) {
return response;
}
const body: any = response;
return new CommentsDto(
body.createdComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.updatedComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.deletedComments,
new Version(body.version)
);
}),
pretifyError('Failed to load comments.'));
}
public postComment(appName: string, commentsId: string, dto: UpsertCommentDto): Observable<CommentDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}`);
return this.http.post(url, dto).pipe(
map(response => {
const body: any = response;
return new CommentDto(
body.id,
DateTime.parseISO_UTC(body.time),
body.text,
body.user);
}),
pretifyError('Failed to create comment.'));
}
public putComment(appName: string, commentsId: string, commentId: string, dto: UpsertCommentDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}/${commentId}`);
return this.http.put(url, dto).pipe(
pretifyError('Failed to update comment.'));
}
public deleteComment(appName: string, commentsId: string, commentId: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}/${commentId}`);
return this.http.delete(url).pipe(
pretifyError('Failed to delete comment.'));
}
}

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

@ -190,7 +190,7 @@ export class ContentsService {
body.data, body.data,
response.version); response.version);
}), }),
tap(content => { tap(() => {
this.analytics.trackEvent('Content', 'Created', appName); this.analytics.trackEvent('Content', 'Created', appName);
}), }),
pretifyError('Failed to create content. Please reload.')); pretifyError('Failed to create content. Please reload.'));

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

@ -344,7 +344,7 @@ export class SchemasService {
body.scriptDelete, body.scriptDelete,
body.scriptChange); body.scriptChange);
}), }),
tap(schema => { tap(() => {
this.analytics.trackEvent('Schema', 'Created', appName); this.analytics.trackEvent('Schema', 'Created', appName);
}), }),
pretifyError('Failed to create schema. Please reload.')); pretifyError('Failed to create schema. Please reload.'));

4
src/Squidex/app/shared/state/apps.state.spec.ts

@ -87,7 +87,7 @@ describe('AppsState', () => {
appsService.setup(x => x.postApp(request)) appsService.setup(x => x.postApp(request))
.returns(() => of(newApp)); .returns(() => of(newApp));
appsState.create(request, now).subscribe(); appsState.create(request).subscribe();
expect(appsState.snapshot.apps.values).toEqual([newApp, ...oldApps]); expect(appsState.snapshot.apps.values).toEqual([newApp, ...oldApps]);
}); });
@ -101,7 +101,7 @@ describe('AppsState', () => {
appsService.setup(x => x.deleteApp(newApp.name)) appsService.setup(x => x.deleteApp(newApp.name))
.returns(() => of({})); .returns(() => of({}));
appsState.create(request, now).subscribe(); appsState.create(request).subscribe();
const appsAfterCreate = appsState.snapshot.apps.values; const appsAfterCreate = appsState.snapshot.apps.values;

5
src/Squidex/app/shared/state/apps.state.ts

@ -10,7 +10,6 @@ import { Observable, of } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
notify, notify,
@ -73,7 +72,7 @@ export class AppsState extends State<Snapshot> {
})); }));
} }
public create(request: CreateAppDto, now?: DateTime): Observable<AppDto> { public create(request: CreateAppDto): Observable<AppDto> {
return this.appsService.postApp(request).pipe( return this.appsService.postApp(request).pipe(
tap(dto => { tap(dto => {
this.next(s => { this.next(s => {
@ -86,7 +85,7 @@ export class AppsState extends State<Snapshot> {
public delete(name: string): Observable<any> { public delete(name: string): Observable<any> {
return this.appsService.deleteApp(name).pipe( return this.appsService.deleteApp(name).pipe(
tap(app => { tap(() => {
this.next(s => { this.next(s => {
const apps = s.apps.filter(x => x.name !== name); const apps = s.apps.filter(x => x.name !== name);

22
src/Squidex/app/shared/state/comments.form.ts

@ -0,0 +1,22 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form } from '@app/framework';
export class UpsertCommentForm extends Form<FormGroup> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
text: ['',
[
Validators.required
]
]
}));
}
}

119
src/Squidex/app/shared/state/comments.state.spec.ts

@ -0,0 +1,119 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import {
AppsState,
CommentDto,
CommentsDto,
CommentsService,
CommentsState,
DateTime,
DialogService,
ImmutableArray,
UpsertCommentDto,
Version
} from '@app/shared';
describe('CommentsState', () => {
const app = 'my-app';
const commentsId = 'my-comments';
const now = DateTime.today();
const user = 'not-me';
const oldComments = new CommentsDto([
new CommentDto('1', now, 'text1', user),
new CommentDto('2', now, 'text2', user)
], [], [], new Version('1'));
let dialogs: IMock<DialogService>;
let appsState: IMock<AppsState>;
let commentsService: IMock<CommentsService>;
let commentsState: CommentsState;
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
appsState = Mock.ofType<AppsState>();
appsState.setup(x => x.appName)
.returns(() => app);
commentsService = Mock.ofType<CommentsService>();
commentsService.setup(x => x.getComments(app, commentsId, new Version('')))
.returns(() => of(oldComments));
commentsState = new CommentsState(appsState.object, commentsId, commentsService.object, dialogs.object);
commentsState.load().subscribe();
});
it('should load and merge comments', () => {
const newComments = new CommentsDto([
new CommentDto('3', now, 'text3', user)
], [
new CommentDto('2', now, 'text2_2', user)
], ['1'], new Version('2'));
commentsService.setup(x => x.getComments(app, commentsId, new Version('1')))
.returns(() => of(newComments));
commentsState.load().subscribe();
expect(commentsState.snapshot.isLoaded).toBeTruthy();
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([
new CommentDto('2', now, 'text2_2', user),
new CommentDto('3', now, 'text3', user)
]));
commentsService.verify(x => x.getComments(app, commentsId, It.isAny()), Times.exactly(2));
});
it('should add comment to snapshot when created', () => {
const newComment = new CommentDto('3', now, 'text3', user);
commentsService.setup(x => x.postComment(app, commentsId, new UpsertCommentDto('text3')))
.returns(() => of(newComment));
commentsState.create('text3').subscribe();
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([
new CommentDto('1', now, 'text1', user),
new CommentDto('2', now, 'text2', user),
new CommentDto('3', now, 'text3', user)
]));
});
it('should update properties when updated', () => {
commentsService.setup(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2')))
.returns(() => of({}));
commentsState.update('2', 'text2_2', now).subscribe();
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([
new CommentDto('1', now, 'text1', user),
new CommentDto('2', now, 'text2_2', user)
]));
commentsService.verify(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2')), Times.once());
});
it('should remove comment from snapshot when deleted', () => {
commentsService.setup(x => x.deleteComment(app, commentsId, '2'))
.returns(() => of({}));
commentsState.delete('2').subscribe();
expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([
new CommentDto('1', now, 'text1', user)
]));
commentsService.verify(x => x.deleteComment(app, commentsId, '2'), Times.once());
});
});

128
src/Squidex/app/shared/state/comments.state.ts

@ -0,0 +1,128 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import {
DateTime,
DialogService,
ImmutableArray,
notify,
State,
Version
} from '@app/framework';
import {
CommentDto,
CommentsService,
UpsertCommentDto
} from './../services/comments.service';
import { AppsState } from './apps.state';
interface Snapshot {
comments: ImmutableArray<CommentDto>;
version: Version;
isLoaded?: boolean;
}
@Injectable()
export class CommentsState extends State<Snapshot> {
public comments =
this.changes.pipe(map(x => x.comments),
distinctUntilChanged());
public isLoaded =
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
constructor(
private readonly appsState: AppsState,
private readonly commentsId: string,
private readonly commentsService: CommentsService,
private readonly dialogs: DialogService
) {
super({ comments: ImmutableArray.empty(), version: new Version('') });
}
public load(): Observable<any> {
return this.commentsService.getComments(this.appName, this.commentsId, this.version).pipe(
tap(dtos => {
this.next(s => {
let comments = s.comments;
for (let created of dtos.createdComments) {
if (!comments.find(x => x.id === created.id)) {
comments = comments.push(created);
}
}
for (let updated of dtos.updatedComments) {
comments = comments.replaceBy('id', updated);
}
for (let deleted of dtos.deletedComments) {
comments = comments.filter(x => x.id !== deleted);
}
return { ...s, comments, isLoaded: true, version: dtos.version };
});
}),
notify(this.dialogs));
}
public create(text: string): Observable<any> {
return this.commentsService.postComment(this.appName, this.commentsId, new UpsertCommentDto(text)).pipe(
tap(dto => {
this.next(s => {
const comments = s.comments.push(dto);
return { ...s, comments };
});
}),
notify(this.dialogs));
}
public update(commentId: string, text: string, now?: DateTime): Observable<any> {
return this.commentsService.putComment(this.appName, this.commentsId, commentId, new UpsertCommentDto(text)).pipe(
tap(() => {
this.next(s => {
const comments = s.comments.map(c => c.id === commentId ? update(c, text, now || DateTime.now()) : c);
return { ...s, comments };
});
}),
notify(this.dialogs));
}
public delete(commentId: string): Observable<any> {
return this.commentsService.deleteComment(this.appName, this.commentsId, commentId).pipe(
tap(() => {
this.next(s => {
const comments = s.comments.filter(c => c.id !== commentId);
return { ...s, comments };
});
}),
notify(this.dialogs));
}
private get version() {
return this.snapshot.version;
}
private get appName() {
return this.appsState.appName;
}
}
const update = (comment: CommentDto, text: string, time: DateTime) =>
comment.with({ text, time });

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

@ -132,7 +132,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
notify(this.dialogs)); notify(this.dialogs));
} }
public create(request: any, publish: boolean, now?: DateTime) { public create(request: any, publish: boolean) {
return this.contentsService.postContent(this.appName, this.schemaName, request, publish).pipe( return this.contentsService.postContent(this.appName, this.schemaName, request, publish).pipe(
tap(dto => { tap(dto => {
this.dialogs.notifyInfo('Contents created successfully.'); this.dialogs.notifyInfo('Contents created successfully.');

2
src/Squidex/app/shared/state/rules.state.ts

@ -87,7 +87,7 @@ export class RulesState extends State<Snapshot> {
public delete(rule: RuleDto): Observable<any> { public delete(rule: RuleDto): Observable<any> {
return this.rulesService.deleteRule(this.appName, rule.id, rule.version).pipe( return this.rulesService.deleteRule(this.appName, rule.id, rule.version).pipe(
tap(dto => { tap(() => {
this.next(s => { this.next(s => {
const rules = s.rules.removeAll(x => x.id === rule.id); const rules = s.rules.removeAll(x => x.id === rule.id);

2
src/Squidex/app/shared/state/schemas.state.ts

@ -140,7 +140,7 @@ export class SchemasState extends State<Snapshot> {
public delete(schema: SchemaDto): Observable<any> { public delete(schema: SchemaDto): Observable<any> {
return this.schemasService.deleteSchema(this.appName, schema.name, schema.version).pipe( return this.schemasService.deleteSchema(this.appName, schema.name, schema.version).pipe(
tap(dto => { tap(() => {
return this.next(s => { return this.next(s => {
const schemas = s.schemas.filter(x => x.id !== schema.id); const schemas = s.schemas.filter(x => x.id !== schema.id);
const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? null : s.selectedSchema; const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? null : s.selectedSchema;

8
src/Squidex/app/theme/icomoon/demo-files/demo.css

@ -147,19 +147,19 @@ p {
font-size: 16px; font-size: 16px;
} }
.fs1 { .fs1 {
font-size: 32px; font-size: 24px;
} }
.fs2 { .fs2 {
font-size: 20px; font-size: 32px;
} }
.fs3 { .fs3 {
font-size: 32px; font-size: 20px;
} }
.fs4 { .fs4 {
font-size: 32px; font-size: 32px;
} }
.fs5 { .fs5 {
font-size: 24px; font-size: 32px;
} }
.fs6 { .fs6 {
font-size: 28px; font-size: 28px;

324
src/Squidex/app/theme/icomoon/demo.html

@ -9,11 +9,94 @@
<link rel="stylesheet" href="style.css"></head> <link rel="stylesheet" href="style.css"></head>
<body> <body>
<div class="bgc1 clearfix"> <div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;98)</small></h1> <h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;99)</small></h1>
</div> </div>
<div class="clearfix mhl ptl"> <div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 16</h1> <h1 class="mvm mtn fgc1">Grid Size: 24</h1>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-comments">
</span>
<span class="mls"> icon-comments</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e95f" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe95f;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-backup">
</span>
<span class="mls"> icon-backup</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e95b" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe95b;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-support">
</span>
<span class="mls"> icon-support</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e95a" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe95a;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-RichText">
</span>
<span class="mls"> icon-control-RichText</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e939" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe939;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1"> <div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-download">
</span>
<span class="mls"> icon-download</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e93e" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93e;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 16</h1>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-spinner2"> <span class="icon-spinner2">
@ -29,7 +112,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-star-full"> <span class="icon-star-full">
@ -45,7 +128,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-star-empty"> <span class="icon-star-empty">
@ -61,7 +144,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-twitter"> <span class="icon-twitter">
@ -77,7 +160,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-hour-glass"> <span class="icon-hour-glass">
@ -93,7 +176,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-spinner"> <span class="icon-spinner">
@ -109,7 +192,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-clock"> <span class="icon-clock">
@ -125,7 +208,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-bin2"> <span class="icon-bin2">
@ -141,7 +224,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-earth"> <span class="icon-earth">
@ -157,7 +240,7 @@
<input type="text" readonly value="earth, globe2" class="liga unitRight" /> <input type="text" readonly value="earth, globe2" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-elapsed"> <span class="icon-elapsed">
@ -173,7 +256,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-google"> <span class="icon-google">
@ -189,7 +272,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-lock"> <span class="icon-lock">
@ -205,7 +288,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-microsoft"> <span class="icon-microsoft">
@ -221,7 +304,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-pause"> <span class="icon-pause">
@ -237,7 +320,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-play"> <span class="icon-play">
@ -253,7 +336,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-reset"> <span class="icon-reset">
@ -269,7 +352,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-settings2"> <span class="icon-settings2">
@ -285,7 +368,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-timeout"> <span class="icon-timeout">
@ -301,7 +384,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs1"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-unlocked"> <span class="icon-unlocked">
@ -320,7 +403,7 @@
</div> </div>
<div class="clearfix mhl ptl"> <div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 20</h1> <h1 class="mvm mtn fgc1">Grid Size: 20</h1>
<div class="glyph fs2"> <div class="glyph fs3">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-grid1"> <span class="icon-grid1">
@ -336,7 +419,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs2"> <div class="glyph fs3">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-list"> <span class="icon-list">
@ -352,7 +435,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs2"> <div class="glyph fs3">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-info"> <span class="icon-info">
@ -368,26 +451,10 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-Stars">
</span>
<span class="mls"> icon-control-Stars</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e93a" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93a;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div> </div>
<div class="clearfix mhl ptl"> <div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 32</h1> <h1 class="mvm mtn fgc1">Grid Size: 32</h1>
<div class="glyph fs3"> <div class="glyph fs4">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Color"> <span class="icon-control-Color">
@ -403,7 +470,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs3"> <div class="glyph fs4">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-browser"> <span class="icon-browser">
@ -419,7 +486,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs3"> <div class="glyph fs4">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-checkmark"> <span class="icon-checkmark">
@ -435,7 +502,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs3"> <div class="glyph fs4">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Stars"> <span class="icon-control-Stars">
@ -454,7 +521,7 @@
</div> </div>
<div class="clearfix mhl ptl"> <div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: Unknown</h1> <h1 class="mvm mtn fgc1">Grid Size: Unknown</h1>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-prerender"> <span class="icon-prerender">
@ -470,7 +537,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-circle"> <span class="icon-circle">
@ -486,7 +553,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Slug"> <span class="icon-control-Slug">
@ -502,7 +569,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-Tags"> <span class="icon-type-Tags">
@ -518,7 +585,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-activity"> <span class="icon-activity">
@ -534,7 +601,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-history"> <span class="icon-history">
@ -550,7 +617,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-time"> <span class="icon-time">
@ -566,7 +633,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-add"> <span class="icon-add">
@ -582,7 +649,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-plus"> <span class="icon-plus">
@ -598,7 +665,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-check-circle"> <span class="icon-check-circle">
@ -614,7 +681,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-check-circle-filled"> <span class="icon-check-circle-filled">
@ -630,7 +697,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-close"> <span class="icon-close">
@ -646,7 +713,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-References"> <span class="icon-type-References">
@ -662,7 +729,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Checkbox"> <span class="icon-control-Checkbox">
@ -678,7 +745,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Dropdown"> <span class="icon-control-Dropdown">
@ -694,7 +761,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Input"> <span class="icon-control-Input">
@ -710,7 +777,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Radio"> <span class="icon-control-Radio">
@ -726,7 +793,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-TextArea"> <span class="icon-control-TextArea">
@ -742,7 +809,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Toggle"> <span class="icon-control-Toggle">
@ -758,7 +825,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-copy"> <span class="icon-copy">
@ -774,7 +841,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-dashboard"> <span class="icon-dashboard">
@ -790,7 +857,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-delete"> <span class="icon-delete">
@ -806,7 +873,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-bin"> <span class="icon-bin">
@ -822,7 +889,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-delete-filled"> <span class="icon-delete-filled">
@ -838,7 +905,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-document-delete"> <span class="icon-document-delete">
@ -854,7 +921,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-document-disable"> <span class="icon-document-disable">
@ -870,7 +937,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-document-publish"> <span class="icon-document-publish">
@ -886,7 +953,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-drag"> <span class="icon-drag">
@ -902,7 +969,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-filter"> <span class="icon-filter">
@ -918,7 +985,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-github"> <span class="icon-github">
@ -934,7 +1001,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-help"> <span class="icon-help">
@ -950,7 +1017,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-location"> <span class="icon-location">
@ -966,7 +1033,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Map"> <span class="icon-control-Map">
@ -982,7 +1049,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-Geolocation"> <span class="icon-type-Geolocation">
@ -998,7 +1065,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-logo"> <span class="icon-logo">
@ -1014,7 +1081,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-media"> <span class="icon-media">
@ -1030,7 +1097,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-Assets"> <span class="icon-type-Assets">
@ -1046,7 +1113,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-trigger-AssetChanged"> <span class="icon-trigger-AssetChanged">
@ -1062,7 +1129,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-more"> <span class="icon-more">
@ -1078,7 +1145,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-dots"> <span class="icon-dots">
@ -1094,7 +1161,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-pencil"> <span class="icon-pencil">
@ -1110,7 +1177,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-reference"> <span class="icon-reference">
@ -1126,7 +1193,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-schemas"> <span class="icon-schemas">
@ -1142,7 +1209,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-search"> <span class="icon-search">
@ -1158,7 +1225,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-settings"> <span class="icon-settings">
@ -1174,7 +1241,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-Boolean"> <span class="icon-type-Boolean">
@ -1190,7 +1257,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-DateTime"> <span class="icon-type-DateTime">
@ -1206,7 +1273,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-Json"> <span class="icon-type-Json">
@ -1222,7 +1289,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-json"> <span class="icon-json">
@ -1238,7 +1305,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-Number"> <span class="icon-type-Number">
@ -1254,7 +1321,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-type-String"> <span class="icon-type-String">
@ -1270,7 +1337,7 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs4"> <div class="glyph fs5">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-user"> <span class="icon-user">
@ -1287,73 +1354,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 24</h1>
<div class="glyph fs5">
<div class="clearfix bshadow0 pbs">
<span class="icon-backup">
</span>
<span class="mls"> icon-backup</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e95b" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe95b;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs5">
<div class="clearfix bshadow0 pbs">
<span class="icon-support">
</span>
<span class="mls"> icon-support</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e95a" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe95a;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs5">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-RichText">
</span>
<span class="mls"> icon-control-RichText</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e939" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe939;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs5">
<div class="clearfix bshadow0 pbs">
<span class="icon-download">
</span>
<span class="mls"> icon-download</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e93e" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93e;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl"> <div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 14</h1> <h1 class="mvm mtn fgc1">Grid Size: 14</h1>
<div class="glyph fs6"> <div class="glyph fs6">

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

1
src/Squidex/app/theme/icomoon/fonts/icomoon.svg

@ -102,6 +102,7 @@
<glyph unicode="&#xe95c;" glyph-name="twitter" d="M1024 733.6c-37.6-16.8-78.2-28-120.6-33 43.4 26 76.6 67.2 92.4 116.2-40.6-24-85.6-41.6-133.4-51-38.4 40.8-93 66.2-153.4 66.2-116 0-210-94-210-210 0-16.4 1.8-32.4 5.4-47.8-174.6 8.8-329.4 92.4-433 219.6-18-31-28.4-67.2-28.4-105.6 0-72.8 37-137.2 93.4-174.8-34.4 1-66.8 10.6-95.2 26.2 0-0.8 0-1.8 0-2.6 0-101.8 72.4-186.8 168.6-206-17.6-4.8-36.2-7.4-55.4-7.4-13.6 0-26.6 1.4-39.6 3.8 26.8-83.4 104.4-144.2 196.2-146-72-56.4-162.4-90-261-90-17 0-33.6 1-50.2 3 93.2-59.8 203.6-94.4 322.2-94.4 386.4 0 597.8 320.2 597.8 597.8 0 9.2-0.2 18.2-0.6 27.2 41 29.4 76.6 66.4 104.8 108.6z" /> <glyph unicode="&#xe95c;" glyph-name="twitter" d="M1024 733.6c-37.6-16.8-78.2-28-120.6-33 43.4 26 76.6 67.2 92.4 116.2-40.6-24-85.6-41.6-133.4-51-38.4 40.8-93 66.2-153.4 66.2-116 0-210-94-210-210 0-16.4 1.8-32.4 5.4-47.8-174.6 8.8-329.4 92.4-433 219.6-18-31-28.4-67.2-28.4-105.6 0-72.8 37-137.2 93.4-174.8-34.4 1-66.8 10.6-95.2 26.2 0-0.8 0-1.8 0-2.6 0-101.8 72.4-186.8 168.6-206-17.6-4.8-36.2-7.4-55.4-7.4-13.6 0-26.6 1.4-39.6 3.8 26.8-83.4 104.4-144.2 196.2-146-72-56.4-162.4-90-261-90-17 0-33.6 1-50.2 3 93.2-59.8 203.6-94.4 322.2-94.4 386.4 0 597.8 320.2 597.8 597.8 0 9.2-0.2 18.2-0.6 27.2 41 29.4 76.6 66.4 104.8 108.6z" />
<glyph unicode="&#xe95d;" glyph-name="star-full" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" /> <glyph unicode="&#xe95d;" glyph-name="star-full" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
<glyph unicode="&#xe95e;" glyph-name="star-empty" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" /> <glyph unicode="&#xe95e;" glyph-name="star-empty" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />
<glyph unicode="&#xe95f;" glyph-name="comments" d="M854 256.667v512h-684v-598l86 86h598zM854 852.667c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" /> <glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" /> <glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" /> <glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

2
src/Squidex/app/theme/icomoon/selection.json

File diff suppressed because one or more lines are too long

40
src/Squidex/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('fonts/icomoon.eot?z1dhmx'); src: url('fonts/icomoon.eot?jlrdyp');
src: url('fonts/icomoon.eot?z1dhmx#iefix') format('embedded-opentype'), src: url('fonts/icomoon.eot?jlrdyp#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?z1dhmx') format('truetype'), url('fonts/icomoon.ttf?jlrdyp') format('truetype'),
url('fonts/icomoon.woff?z1dhmx') format('woff'), url('fonts/icomoon.woff?jlrdyp') format('woff'),
url('fonts/icomoon.svg?z1dhmx#icomoon') format('svg'); url('fonts/icomoon.svg?jlrdyp#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -24,6 +24,21 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-comments:before {
content: "\e95f";
}
.icon-backup:before {
content: "\e95b";
}
.icon-support:before {
content: "\e95a";
}
.icon-control-RichText:before {
content: "\e939";
}
.icon-download:before {
content: "\e93e";
}
.icon-spinner2:before { .icon-spinner2:before {
content: "\e959"; content: "\e959";
} }
@ -90,9 +105,6 @@
.icon-info:before { .icon-info:before {
content: "\e93c"; content: "\e93c";
} }
.icon-control-Stars:before {
content: "\e93a";
}
.icon-control-Color:before { .icon-control-Color:before {
content: "\e94d"; content: "\e94d";
} }
@ -261,18 +273,6 @@
.icon-user:before { .icon-user:before {
content: "\e928"; content: "\e928";
} }
.icon-backup:before {
content: "\e95b";
}
.icon-support:before {
content: "\e95a";
}
.icon-control-RichText:before {
content: "\e939";
}
.icon-download:before {
content: "\e93e";
}
.icon-single-content:before { .icon-single-content:before {
content: "\e958"; content: "\e958";
} }

162
tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs

@ -0,0 +1,162 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using NodaTime;
using Squidex.Domain.Apps.Core.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Comments.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Comments
{
public class CommentsGrainTests : HandlerTestBase<CommentsGrain, CommentsState>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly Guid commentsId = Guid.NewGuid();
private readonly CommentsGrain sut;
protected override Guid Id
{
get { return commentsId; }
}
public CommentsGrainTests()
{
sut = new CommentsGrain(Store, A.Dummy<ISemanticLog>());
sut.OnActivateAsync(Id).Wait();
}
[Fact]
public async Task Create_should_create_events()
{
var command = new CreateComment { Text = "text1" };
var result = await sut.ExecuteAsync(CreateCommentsCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(command.CommentId, 0));
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult { Version = 0 });
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult
{
CreatedComments = new List<Comment>
{
new Comment(command.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), command.Actor, "text1")
},
Version = 0
});
LastEvents
.ShouldHaveSameEvents(
CreateCommentsEvent(new CommentCreated { CommentId = command.CommentId, Text = command.Text })
);
}
[Fact]
public async Task Update_should_create_events_and_update_state()
{
var createCommand = new CreateComment { Text = "text1" };
var updateCommand = new UpdateComment { Text = "text2", CommentId = createCommand.CommentId };
await sut.ExecuteAsync(CreateCommentsCommand(createCommand));
var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand));
result.ShouldBeEquivalent(new EntitySavedResult(1));
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult
{
CreatedComments = new List<Comment>
{
new Comment(createCommand.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), createCommand.Actor, "text2")
},
Version = 1
});
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult
{
UpdatedComments = new List<Comment>
{
new Comment(createCommand.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), createCommand.Actor, "text2")
},
Version = 1
});
LastEvents
.ShouldHaveSameEvents(
CreateCommentsEvent(new CommentUpdated { CommentId = createCommand.CommentId, Text = updateCommand.Text })
);
}
[Fact]
public async Task Delete_should_create_events_and_update_state()
{
var createCommand = new CreateComment { Text = "text1" };
var updateCommand = new UpdateComment { Text = "text2", CommentId = createCommand.CommentId };
var deleteCommand = new DeleteComment { CommentId = createCommand.CommentId };
await sut.ExecuteAsync(CreateCommentsCommand(createCommand));
await sut.ExecuteAsync(CreateCommentsCommand(updateCommand));
var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand));
result.ShouldBeEquivalent(new EntitySavedResult(2));
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { Version = 2 });
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult
{
DeletedComments = new List<Guid>
{
deleteCommand.CommentId
},
Version = 2
});
sut.GetCommentsAsync(1).Result.Should().BeEquivalentTo(new CommentsResult
{
DeletedComments = new List<Guid>
{
deleteCommand.CommentId
},
Version = 2
});
LastEvents
.ShouldHaveSameEvents(
CreateCommentsEvent(new CommentDeleted { CommentId = createCommand.CommentId })
);
}
private Task ExecuteDeleteAsync()
{
return sut.ExecuteAsync(CreateCommentsCommand(new DeleteComment()));
}
protected T CreateCommentsEvent<T>(T @event) where T : CommentsEvent
{
@event.CommentsId = commentsId;
return CreateEvent(@event);
}
protected T CreateCommentsCommand<T>(T command) where T : CommentsCommand
{
command.CommentsId = commentsId;
return CreateCommand(command);
}
}
}

164
tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs

@ -0,0 +1,164 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Comments.Guards
{
public class GuardCommentsTests
{
private readonly RefToken user1 = new RefToken(RefTokenType.Subject, "1");
private readonly RefToken user2 = new RefToken(RefTokenType.Subject, "2");
[Fact]
public void CanCreate_should_throw_exception_if_text_not_defined()
{
var command = new CreateComment();
ValidationAssert.Throws(() => GuardComments.CanCreate(command),
new ValidationError("Text is required.", "Text"));
}
[Fact]
public void CanCreate_should_not_throw_exception_if_text_defined()
{
var command = new CreateComment { Text = "text" };
GuardComments.CanCreate(command);
}
[Fact]
public void CanUpdate_should_throw_exception_if_text_not_defined()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
ValidationAssert.Throws(() => GuardComments.CanUpdate(events, command),
new ValidationError("Text is required.", "Text"));
}
[Fact]
public void CanUpdate_should_throw_exception_if_comment_from_another_user()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user2, Text = "text2" };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
Assert.Throws<DomainException>(() => GuardComments.CanUpdate(events, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_comment_not_found()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>();
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(events, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_comment_deleted_found()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(),
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>(),
};
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(events, command));
}
[Fact]
public void CanUpdate_should_not_throw_exception_if_comment_from_same_user()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
GuardComments.CanUpdate(events, command);
}
[Fact]
public void CanDelete_should_throw_exception_if_comment_from_another_user()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user2 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
Assert.Throws<DomainException>(() => GuardComments.CanDelete(events, command));
}
[Fact]
public void CanDelete_should_throw_exception_if_comment_not_found()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>();
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(events, command));
}
[Fact]
public void CanDelete_should_throw_exception_if_comment_deleted()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(),
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>(),
};
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(events, command));
}
[Fact]
public void CanDelete_should_not_throw_exception_if_comment_from_same_user()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
GuardComments.CanDelete(events, command);
}
}
}
Loading…
Cancel
Save