Browse Source

Merge branch 'master' into master

pull/328/head
Avd6977 7 years ago
committed by GitHub
parent
commit
0ceb14ff9b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 32
      src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
  3. 2
      src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs
  4. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  5. 7
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
  6. 10
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  7. 25
      src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs
  8. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs
  9. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs
  10. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs
  11. 126
      src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
  12. 96
      src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs
  13. 88
      src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs
  14. 18
      src/Squidex.Domain.Apps.Entities/Comments/ICommentGrain.cs
  15. 13
      src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs
  16. 4
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  17. 1
      src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs
  18. 2
      src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs
  19. 20
      src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs
  20. 18
      src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs
  21. 20
      src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs
  22. 16
      src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs
  23. 51
      src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  24. 2
      src/Squidex.Infrastructure/Log/FileChannel.cs
  25. 7
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  26. 4
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  27. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs
  28. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/UpdateAssetDto.cs
  29. 136
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  30. 53
      src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs
  31. 48
      src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs
  32. 33
      src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs
  33. 2
      src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs
  34. 5
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
  35. 9
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  36. 2
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs
  37. 13
      src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
  38. 2
      src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml
  39. 5
      src/Squidex/Config/Domain/EntitiesServices.cs
  40. 7
      src/Squidex/Pipeline/Swagger/SwaggerHelper.cs
  41. 4
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  42. 2
      src/Squidex/app/features/api/api-area.component.html
  43. 6
      src/Squidex/app/features/apps/pages/apps-page.component.html
  44. 6
      src/Squidex/app/features/apps/pages/onboarding-dialog.component.html
  45. 1
      src/Squidex/app/features/content/declarations.ts
  46. 8
      src/Squidex/app/features/content/module.ts
  47. 1
      src/Squidex/app/features/content/pages/comments/comments-page.component.html
  48. 2
      src/Squidex/app/features/content/pages/comments/comments-page.component.scss
  49. 30
      src/Squidex/app/features/content/pages/comments/comments-page.component.ts
  50. 2
      src/Squidex/app/features/content/pages/content/content-field.component.html
  51. 27
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  52. 2
      src/Squidex/app/features/content/pages/content/content-history.component.html
  53. 2
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  54. 6
      src/Squidex/app/features/content/pages/content/content-page.component.html
  55. 8
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  56. 2
      src/Squidex/app/features/content/shared/array-editor.component.html
  57. 9
      src/Squidex/app/features/content/shared/array-editor.component.scss
  58. 8
      src/Squidex/app/features/content/shared/array-editor.component.ts
  59. 4
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  60. 4
      src/Squidex/app/features/content/shared/references-editor.component.ts
  61. 6
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  62. 2
      src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.html
  63. 2
      src/Squidex/app/features/rules/pages/rules/rule-element.component.html
  64. 6
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  65. 4
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  66. 2
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  67. 4
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  68. 2
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  69. 2
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html
  70. 2
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  71. 2
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  72. 2
      src/Squidex/app/features/settings/pages/plans/plans-page.component.html
  73. 6
      src/Squidex/app/framework/angular/forms/confirm-click.directive.ts
  74. 19
      src/Squidex/app/framework/angular/forms/date-time-editor.component.ts
  75. 11
      src/Squidex/app/framework/angular/forms/dropdown.component.ts
  76. 16
      src/Squidex/app/framework/angular/forms/slider.component.ts
  77. 11
      src/Squidex/app/framework/angular/forms/stars.component.ts
  78. 9
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  79. 11
      src/Squidex/app/framework/angular/forms/toggle.component.ts
  80. 2
      src/Squidex/app/framework/angular/forms/validators.ts
  81. 46
      src/Squidex/app/framework/angular/http/caching.interceptor.ts
  82. 12
      src/Squidex/app/framework/angular/http/http-extensions.ts
  83. 12
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  84. 1
      src/Squidex/app/framework/declarations.ts
  85. 6
      src/Squidex/app/framework/module.ts
  86. 4
      src/Squidex/app/framework/services/dialog.service.ts
  87. 6
      src/Squidex/app/framework/utils/immutable-array.ts
  88. 4
      src/Squidex/app/shared/components/asset.component.html
  89. 19
      src/Squidex/app/shared/components/comment.component.html
  90. 43
      src/Squidex/app/shared/components/comment.component.scss
  91. 38
      src/Squidex/app/shared/components/comment.component.ts
  92. 26
      src/Squidex/app/shared/components/comments.component.html
  93. 11
      src/Squidex/app/shared/components/comments.component.scss
  94. 80
      src/Squidex/app/shared/components/comments.component.ts
  95. 5
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  96. 2
      src/Squidex/app/shared/components/history.component.html
  97. 6
      src/Squidex/app/shared/components/search-form.component.html
  98. 2
      src/Squidex/app/shared/declarations.ts
  99. 3
      src/Squidex/app/shared/internal.ts
  100. 8
      src/Squidex/app/shared/module.ts

2
README.md

@ -14,7 +14,7 @@ Please join our community forum: https://support.squidex.io
## Status ## Status
Current Version 1.10.0. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap Current Version 1.11.0. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
## Prerequisites ## Prerequisites

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;
}
}
}

2
src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
foreach (var partitionItem in partition) foreach (var partitionItem in partition)
{ {
partitionType.AddStructuralProperty(partitionItem.Key, edmValueType); partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), edmValueType);
} }
edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false));

5
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -56,6 +56,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules
Guard.NotNull(rule, nameof(rule)); Guard.NotNull(rule, nameof(rule));
Guard.NotNull(@event, nameof(@event)); Guard.NotNull(@event, nameof(@event));
if (!rule.IsEnabled)
{
return null;
}
if (!(@event.Payload is AppEvent appEvent)) if (!(@event.Payload is AppEvent appEvent))
{ {
return null; return null;

7
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs

@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{ {
var value = nodeIn.Rhs.Value; var value = nodeIn.Rhs.Value;
if (value is Instant instant && if (value is Instant &&
!string.Equals(nodeIn.Lhs[0], "mt", StringComparison.OrdinalIgnoreCase) && !string.Equals(nodeIn.Lhs[0], "mt", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(nodeIn.Lhs[0], "ct", StringComparison.OrdinalIgnoreCase)) !string.Equals(nodeIn.Lhs[0], "ct", StringComparison.OrdinalIgnoreCase))
{ {
@ -70,6 +70,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
result[1] = field.Id.ToString(); result[1] = field.Id.ToString();
} }
if (result.Count > 2)
{
result[2] = result[2].UnescapeEdmField();
}
if (result.Count > 0) if (result.Count > 0)
{ {
if (result[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase)) if (result[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase))

10
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs

@ -21,13 +21,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
{ {
private const string TemplateName = "Blog"; private const string TemplateName = "Blog";
private const string SlugScript = @" private const string SlugScript = @"
var data = ctx.data; var data = ctx.data;
if (data.title && data.title.iv) { if (data.title && data.title.iv) {
data.slug = { iv: slugify(data.title.iv) }; data.slug = { iv: slugify(data.title.iv) };
} }
replace(data);"; replace(data);";
public async Task HandleAsync(CommandContext context, Func<Task> next) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {

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)

1
src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs

@ -58,6 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Edm
entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Status).ToCamelCase(), EdmPrimitiveTypeKind.String);
entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32); entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32);
entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false)); entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false));

2
src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs

@ -21,7 +21,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
public sealed class BackupSchemas : BackupHandler public sealed class BackupSchemas : BackupHandler
{ {
private readonly HashSet<NamedId<Guid>> schemaIds = new HashSet<NamedId<Guid>>();
private readonly Dictionary<string, Guid> schemasByName = new Dictionary<string, Guid>(); private readonly Dictionary<string, Guid> schemasByName = new Dictionary<string, Guid>();
private readonly FieldRegistry fieldRegistry; private readonly FieldRegistry fieldRegistry;
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
@ -43,7 +42,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
switch (@event.Payload) switch (@event.Payload)
{ {
case SchemaCreated schemaCreated: case SchemaCreated schemaCreated:
schemaIds.Add(schemaCreated.SchemaId);
schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id; schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id;
break; break;
} }

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);
} }

2
src/Squidex.Infrastructure/Log/FileChannel.cs

@ -13,7 +13,7 @@ namespace Squidex.Infrastructure.Log
{ {
private readonly FileLogProcessor processor; private readonly FileLogProcessor processor;
private readonly object lockObject = new object(); private readonly object lockObject = new object();
private bool isInitialized; private volatile bool isInitialized;
public FileChannel(string path) public FileChannel(string path)
{ {

7
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -10,6 +10,7 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
@ -58,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("assets/{id}/")] [Route("assets/{id}/")]
[ProducesResponseType(200)] [ProducesResponseType(200)]
[ApiCosts(0.5)] [ApiCosts(0.5)]
public async Task<IActionResult> GetAssetContent(Guid id, [FromQuery] int version = -1, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null) public async Task<IActionResult> GetAssetContent(Guid id, [FromQuery] long version = EtagVersion.Any, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null)
{ {
var entity = await assetRepository.FindAssetAsync(id); var entity = await assetRepository.FindAssetAsync(id);
@ -67,10 +68,12 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NotFound(); return NotFound();
} }
var assetId = entity.Id.ToString(); Response.Headers["ETag"] = entity.FileVersion.ToString();
return new FileCallbackResult(entity.MimeType, entity.FileName, async bodyStream => return new FileCallbackResult(entity.MimeType, entity.FileName, async bodyStream =>
{ {
var assetId = entity.Id.ToString();
if (entity.IsImage && (width.HasValue || height.HasValue)) if (entity.IsImage && (width.HasValue || height.HasValue))
{ {
var assetSuffix = $"{width}_{height}_{mode}"; var assetSuffix = $"{width}_{height}_{mode}";

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.

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

@ -0,0 +1,136 @@
// ==========================================================================
// 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>
/// <param name="version">The current version.</param>
/// <remarks>
/// When passing in a version you can retrieve all updates since then.
/// </remarks>
/// <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, [FromQuery] long 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 });
}
}
}

2
src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs

@ -43,6 +43,8 @@ namespace Squidex.Areas.Api.Controllers.Languages
{ {
var response = Language.AllLanguages.Select(LanguageDto.FromLanguage).ToList(); var response = Language.AllLanguages.Select(LanguageDto.FromLanguage).ToList();
Response.Headers["Etag"] = "1";
return Ok(response); return Ok(response);
} }
} }

5
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs

@ -14,10 +14,11 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Rules.Models namespace Squidex.Areas.Api.Controllers.Rules.Models
{ {
public sealed class RuleDto public sealed class RuleDto : IGenerateEtag
{ {
/// <summary> /// <summary>
/// The id of the rule. /// The id of the rule.
@ -49,7 +50,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// <summary> /// <summary>
/// The version of the rule. /// The version of the rule.
/// </summary> /// </summary>
public int Version { get; set; } public long Version { get; set; }
/// <summary> /// <summary>
/// Determines if the rule is enabled. /// Determines if the rule is enabled.

9
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NodaTime; using NodaTime;
using Squidex.Areas.Api.Controllers.Rules.Models; using Squidex.Areas.Api.Controllers.Rules.Models;
@ -32,6 +33,8 @@ namespace Squidex.Areas.Api.Controllers.Rules
[MustBeAppDeveloper] [MustBeAppDeveloper]
public sealed class RulesController : ApiController public sealed class RulesController : ApiController
{ {
private static readonly string RuleActionsEtag = string.Join(";", RuleElementRegistry.Actions.Select(x => x.Key)).Sha256();
private static readonly string RuleTriggersEtag = string.Join(";", RuleElementRegistry.Triggers.Select(x => x.Key)).Sha256();
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IRuleEventRepository ruleEventsRepository; private readonly IRuleEventRepository ruleEventsRepository;
@ -58,6 +61,8 @@ namespace Squidex.Areas.Api.Controllers.Rules
{ {
var response = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto())); var response = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto()));
Response.Headers["Etag"] = RuleActionsEtag;
return Ok(response); return Ok(response);
} }
@ -75,6 +80,8 @@ namespace Squidex.Areas.Api.Controllers.Rules
{ {
var response = RuleElementRegistry.Triggers.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto())); var response = RuleElementRegistry.Triggers.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto()));
Response.Headers["Etag"] = RuleTriggersEtag;
return Ok(response); return Ok(response);
} }
@ -96,6 +103,8 @@ namespace Squidex.Areas.Api.Controllers.Rules
var response = entities.Select(RuleDto.FromRule); var response = entities.Select(RuleDto.FromRule);
Response.Headers["ETag"] = response.ToManyEtag(0);
return Ok(response); return Ok(response);
} }

2
src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs

@ -108,7 +108,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary> /// <summary>
/// The version of the schema. /// The version of the schema.
/// </summary> /// </summary>
public int Version { get; set; } public long Version { get; set; }
public static SchemaDetailsDto FromSchema(ISchemaEntity schema) public static SchemaDetailsDto FromSchema(ISchemaEntity schema)
{ {

13
src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs

@ -7,6 +7,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -76,14 +77,14 @@ namespace Squidex.Areas.Frontend.Middlewares
return response; return response;
} }
var stylesTag = string.Empty; var sb = new StringBuilder();
foreach (var file in Styles) foreach (var file in Styles)
{ {
stylesTag += $"<link href=\"http://{Host}:{Port}/{file}\" rel=\"stylesheet\">"; sb.AppendLine($"<link href=\"http://{Host}:{Port}/{file}\" rel=\"stylesheet\">");
} }
response = response.Replace("</head>", $"{stylesTag}</head>"); response = response.Replace("</head>", $"{sb}</head>");
return response; return response;
} }
@ -95,14 +96,14 @@ namespace Squidex.Areas.Frontend.Middlewares
return response; return response;
} }
var scriptsTag = string.Empty; var sb = new StringBuilder();
foreach (var file in Scripts) foreach (var file in Scripts)
{ {
scriptsTag += $"<script type=\"text/javascript\" src=\"http://{Host}:{Port}/{file}\"></script>"; sb.AppendLine($"<script type=\"text/javascript\" src=\"http://{Host}:{Port}/{file}\"></script>");
} }
response = response.Replace("</body>", $"{scriptsTag}</body>"); response = response.Replace("</body>", $"{sb}</body>");
return response; return response;
} }

2
src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml

@ -52,7 +52,7 @@
I understand and agree that Squidex has integrated Google Analytics (with the anonymizer function). Google Analytics is a web analytics service to gather and analyse data about the behavior of users. I understand and agree that Squidex has integrated Google Analytics (with the anonymizer function). Google Analytics is a web analytics service to gather and analyse data about the behavior of users.
</p> </p>
<p> <p>
I accept the <a href="@Model.PrivacyUrl" target="_blank">privacy policies</a>. I accept the <a href="@Model.PrivacyUrl" target="_blank" rel="noopener">privacy policies</a>.
</p> </p>
</div> </div>
</div> </div>

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>();

7
src/Squidex/Pipeline/Swagger/SwaggerHelper.cs

@ -28,9 +28,10 @@ namespace Squidex.Pipeline.Swagger
using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md"))
{ {
var streamReader = new StreamReader(resourceStream); using (var streamReader = new StreamReader(resourceStream))
{
return streamReader.ReadToEnd(); return streamReader.ReadToEnd();
}
} }
} }

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);

2
src/Squidex/app/features/api/api-area.component.html

@ -14,7 +14,7 @@
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" target="_blank"> <a class="nav-link" href="/api/content/{{appsState.appName}}/docs" target="_blank" rel="noopener">
Swagger Swagger
</a> </a>
</li> </li>

6
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -52,7 +52,7 @@
<div class="card-text"> <div class="card-text">
<div>Start with our ready to use blog.</div> <div>Start with our ready to use blog.</div>
<div> <div>
Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Blog" (click)="$event.stopPropagation()" target="_blank">C#</a> Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Blog" (click)="$event.stopPropagation()" target="_blank" rel="noopener">C#</a>
</div> </div>
</div> </div>
</div> </div>
@ -69,7 +69,7 @@
<div class="card-text"> <div class="card-text">
<div>Create your profile page.</div> <div>Create your profile page.</div>
<div> <div>
Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Profile" (click)="$event.stopPropagation()" target="_blank">C#</a> Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Profile" (click)="$event.stopPropagation()" target="_blank" rel="noopener">C#</a>
</div> </div>
</div> </div>
</div> </div>
@ -86,7 +86,7 @@
<div class="card-text"> <div class="card-text">
<div>Create app for Squidex Identity.</div> <div>Create app for Squidex Identity.</div>
<div> <div>
<a href="https://github.com/Squidex/squidex-identity" (click)="$event.stopPropagation()" target="_blank">Project</a> <a href="https://github.com/Squidex/squidex-identity" (click)="$event.stopPropagation()" target="_blank" rel="noopener">Project</a>
</div> </div>
</div> </div>
</div> </div>

6
src/Squidex/app/features/apps/pages/onboarding-dialog.component.html

@ -125,18 +125,18 @@
<h1>Awesome, now you know the basics!</h1> <h1>Awesome, now you know the basics!</h1>
<p> <p>
But that's not all of the support we can provide. <br />You can go to <a href="https://docs.squidex.io/" target="_blank">https://docs.squidex.io/</a> to read more. But that's not all of the support we can provide. <br />You can go to <a href="https://docs.squidex.io/" target="_blank" rel="noopener">https://docs.squidex.io/</a> to read more.
</p> </p>
<p> <p>
Do you want to join our community? Do you want to join our community?
</p> </p>
<div> <div>
<a class="btn btn-success" href="https://support.squidex.io" target="_blank"> <a class="btn btn-success" href="https://support.squidex.io" target="_blank" rel="noopener">
Join our Forum Join our Forum
</a> &nbsp; </a> &nbsp;
<a class="btn btn-success" href="https://github.com/squidex/squidex" target="_blank"> <a class="btn btn-success" href="https://github.com/squidex/squidex" target="_blank" rel="noopener">
Join us on Github Join us on Github
</a> </a>
</div> </div>

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-field.component.html

@ -8,7 +8,7 @@
</sqx-language-selector> </sqx-language-selector>
</div> </div>
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000"> <sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors. Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>

27
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
@ -22,10 +22,9 @@ import {
@Component({ @Component({
selector: 'sqx-content-field', selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'], styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html', templateUrl: './content-field.component.html'
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContentFieldComponent implements OnChanges { export class ContentFieldComponent implements DoCheck, OnChanges {
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;
@ -49,20 +48,26 @@ export class ContentFieldComponent implements OnChanges {
public isInvalid: Observable<boolean>; public isInvalid: Observable<boolean>;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['fieldForm']) {
this.isInvalid = this.fieldForm.statusChanges.pipe(startWith(this.fieldForm.invalid), map(x => this.fieldForm.invalid));
}
}
public ngDoCheck() {
let control: AbstractControl;
if (this.field.isLocalizable) { if (this.field.isLocalizable) {
this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code]; control = this.fieldForm.controls[this.language.iso2Code];
} else { } else {
this.selectedFormControl = this.fieldForm.controls[fieldInvariant]; control = this.fieldForm.controls[fieldInvariant];
} }
if (changes['language']) { if (this.selectedFormControl !== control) {
if (Types.isFunction(this.selectedFormControl['_clearChangeFns'])) { if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) {
this.selectedFormControl['_clearChangeFns'](); this.selectedFormControl['_clearChangeFns']();
} }
}
if (changes['fieldForm']) { this.selectedFormControl = control;
this.isInvalid = this.fieldForm.statusChanges.pipe(startWith(this.fieldForm.invalid), map(x => this.fieldForm.invalid));
} }
} }
} }

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,

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

@ -125,7 +125,11 @@
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000"> <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">
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>
</ng-container> </ng-container>

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

@ -114,7 +114,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
if (!this.contentForm.form.dirty || !this.content) { if (!this.contentForm.form.dirty || !this.content) {
return of(true); return of(true);
} else { } else {
return this.dialogs.confirmUnsavedChanges(); return this.dialogs.confirm('Unsaved changes', 'You have unsaved changes, do you want to close the current content view and discard your changes?');
} }
} }
@ -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);

2
src/Squidex/app/features/content/shared/array-editor.component.html

@ -1,4 +1,4 @@
<div class="array-container" *ngIf="arrayControl.controls.length > 0"> <div class="array-container" *ngIf="arrayControl.controls.length > 0" [sqxSortModel]="arrayControl.controls" (sqxSorted)="sort($event)">
<div class="item" *ngFor="let itemForm of arrayControl.controls; let i = index"> <div class="item" *ngFor="let itemForm of arrayControl.controls; let i = index">
<sqx-array-item <sqx-array-item
[form]="form" [form]="form"

9
src/Squidex/app/features/content/shared/array-editor.component.scss

@ -4,16 +4,11 @@
.array-container { .array-container {
background: $color-border; background: $color-border;
padding: 1rem; padding: 1rem;
padding-bottom: 1px;
position: relative; position: relative;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.item { .item {
& { margin-bottom: 1rem;
margin-bottom: 1rem;
}
&:last-child {
margin-bottom: 0;
}
} }

8
src/Squidex/app/features/content/shared/array-editor.component.ts

@ -6,7 +6,7 @@
*/ */
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormArray } from '@angular/forms'; import { AbstractControl, FormArray } from '@angular/forms';
import { import {
AppLanguageDto, AppLanguageDto,
@ -44,4 +44,10 @@ export class ArrayEditorComponent {
public addItem() { public addItem() {
this.form.insertArrayItem(this.field, this.language); this.form.insertArrayItem(this.field, this.language);
} }
public sort(controls: AbstractControl[]) {
for (let i = 0; i < controls.length; i++) {
this.arrayControl.setControl(i, controls[i]);
}
}
} }

4
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -74,11 +74,15 @@ export class AssetsEditorComponent implements ControlValueAccessor {
} }
} else { } else {
this.oldAssets = ImmutableArray.empty(); this.oldAssets = ImmutableArray.empty();
this.changeDetector.detectChanges();
} }
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {

4
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -104,11 +104,15 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
} }
} else { } else {
this.contentItems = ImmutableArray.empty(); this.contentItems = ImmutableArray.empty();
this.changeDetector.detectChanges();
} }
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {

6
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -26,7 +26,7 @@
</div> </div>
</a> </a>
<a class="card card-href" href="/api/docs" target="_blank"> <a class="card card-href" href="/api/docs" target="_blank" rel="noopener">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="/images/dashboard-api.png" /> <img src="/images/dashboard-api.png" />
@ -40,7 +40,7 @@
</div> </div>
</a> </a>
<a class="card card-href" href="https://support.squidex.io" target="_blank"> <a class="card card-href" href="https://support.squidex.io" target="_blank" rel="noopener">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="/images/dashboard-feedback.png" /> <img src="/images/dashboard-feedback.png" />
@ -54,7 +54,7 @@
</div> </div>
</a> </a>
<a class="card card-href" href="https://github.com/squidex/squidex" target="_blank"> <a class="card card-href" href="https://github.com/squidex/squidex" target="_blank" rel="noopener">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="/images/dashboard-github.png" /> <img src="/images/dashboard-github.png" />

2
src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.html

@ -8,7 +8,7 @@
<input type="text" class="form-control" id="accessToken" formControlName="accessToken" /> <input type="text" class="form-control" id="accessToken" formControlName="accessToken" />
<small class="form-text text-muted"> <small class="form-text text-muted">
The self issued access token. Can be created under <a target="_blank" href="https://medium.com/me/settings">https://medium.com/me/settings</a>. The self issued access token. Can be created under <a target="_blank" rel="noopener" href="https://medium.com/me/settings">https://medium.com/me/settings</a>.
</small> </small>
</div> </div>
</div> </div>

2
src/Squidex/app/features/rules/pages/rules/rule-element.component.html

@ -25,7 +25,7 @@
</div> </div>
<div class="large-link" *ngIf="element.readMore"> <div class="large-link" *ngIf="element.readMore">
<a [href]="element.readMore" target="_blank">Read More</a> <a [href]="element.readMore" target="_blank" rel="noopener">Read More</a>
</div> </div>
</div> </div>
</div> </div>

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();

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

@ -84,8 +84,8 @@
<i class="icon-help"></i> <i class="icon-help"></i>
</a> </a>
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000"> <sqx-onboarding-tooltip helpId="help" [for]="linkHelp" position="leftTop" after="180000">
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank">https://docs.squidex.io</a> for the full documentation. Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank" rel="noopener">https://docs.squidex.io</a> for the full documentation.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>

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);

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

@ -41,11 +41,11 @@
</div> </div>
</div> </div>
<sqx-onboarding-tooltip id="history" [for]="buttonOptions" position="bottomRight" after="60000"> <sqx-onboarding-tooltip helpId="history" [for]="buttonOptions" position="bottomRight" after="60000">
Open the context menu to delete the schema or to create some scripts for content changes. Open the context menu to delete the schema or to create some scripts for content changes.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
<sqx-onboarding-tooltip id="history" [for]="buttonPublish" position="bottomRight" after="240000"> <sqx-onboarding-tooltip helpId="history" [for]="buttonPublish" position="bottomRight" after="240000">
Note, that you have to publish the schema before you can add content to it. Note, that you have to publish the schema before you can add content to it.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>

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/schemas/pages/schema/types/references-validation.component.html

@ -1,6 +1,6 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row"> <div class="form-group row">
<label for="{{field.fieldId}}_field-placeholder" class="col col-3 col-form-label" for="{{field.fieldId}}_fieldSchemaId">Schema</label> <label class="col col-3 col-form-label" for="{{field.fieldId}}_fieldSchemaId">Schema</label>
<div class="col col-6"> <div class="col col-6">
<select class="form-control" id="{{field.fieldId}}_fieldSchemaId" formControlName="schemaId"> <select class="form-control" id="{{field.fieldId}}_fieldSchemaId" formControlName="schemaId">

2
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -73,7 +73,7 @@
<div *ngIf="backup.stopped && !backup.isFailed"> <div *ngIf="backup.stopped && !backup.isFailed">
Download: Download:
<a href="{{backup | sqxBackupDownloadUrl}}" target="_blank"> <a href="{{backup | sqxBackupDownloadUrl}}" target="_blank" rel="noopener">
Ready Ready
</a> </a>
</div> </div>

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/features/settings/pages/plans/plans-page.component.html

@ -72,7 +72,7 @@
</div> </div>
<div *ngIf="plansState.hasPortal | async" class="billing-portal-link"> <div *ngIf="plansState.hasPortal | async" class="billing-portal-link">
Go to <a target="_blank" href="{{portalUrl}}">Billing Portal</a> for payment history and subscription overview. Go to <a target="_blank" rel="noopener" href="{{portalUrl}}">Billing Portal</a> for payment history and subscription overview.
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

6
src/Squidex/app/framework/angular/forms/confirm-click.directive.ts

@ -77,9 +77,7 @@ export class ConfirmClickDirective implements OnDestroy {
this.isOpen = false; this.isOpen = false;
if (result) { if (result) {
if (result) { this.clickConfirmed.delayEmit();
this.clickConfirmed.delayEmit();
}
} }
subscription.unsubscribe(); subscription.unsubscribe();
@ -95,4 +93,4 @@ export class ConfirmClickDirective implements OnDestroy {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
} }
} }

19
src/Squidex/app/framework/angular/forms/date-time-editor.component.ts

@ -44,8 +44,12 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
@Input() @Input()
public hideClear: boolean; public hideClear: boolean;
public timeControl = new FormControl(); @ViewChild('dateInput')
public dateInput: ElementRef;
public isDisabled = false;
public timeControl = new FormControl();
public dateControl = new FormControl(); public dateControl = new FormControl();
public get showTime() { public get showTime() {
@ -56,11 +60,6 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
return !!this.dateValue; return !!this.dateValue;
} }
@ViewChild('dateInput')
public dateInput: ElementRef;
public isDisabled = false;
constructor( constructor(
private readonly changeDetector: ChangeDetectorRef private readonly changeDetector: ChangeDetectorRef
) { ) {
@ -122,6 +121,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
this.dateControl.enable({ emitEvent: false }); this.dateControl.enable({ emitEvent: false });
this.timeControl.enable({ emitEvent: false }); this.timeControl.enable({ emitEvent: false });
} }
this.changeDetector.detectChanges();
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {
@ -143,9 +144,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
this.updateValue(); this.updateValue();
this.touched(); this.touched();
if (false) { this.changeDetector.detectChanges();
this.changeDetector.detectChanges();
}
} }
}); });
@ -219,4 +218,4 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
this.suppressEvents = false; this.suppressEvents = false;
} }
} }

11
src/Squidex/app/framework/angular/forms/dropdown.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'; import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const KEY_ENTER = 13; const KEY_ENTER = 13;
@ -46,6 +46,11 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
public isDisabled = false; public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngAfterContentInit() { public ngAfterContentInit() {
if (this.templates.length === 1) { if (this.templates.length === 1) {
this.itemTemplate = this.selectionTemplate = this.templates.first; this.itemTemplate = this.selectionTemplate = this.templates.first;
@ -62,10 +67,14 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
public writeValue(obj: any) { public writeValue(obj: any) {
this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0); this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0);
this.changeDetector.detectChanges();
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {

16
src/Squidex/app/framework/angular/forms/slider.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
@ -31,8 +31,6 @@ export class SliderComponent implements ControlValueAccessor {
private value: number; private value: number;
private isDragging = false; private isDragging = false;
public isDisabled = false;
@ViewChild('bar') @ViewChild('bar')
public bar: ElementRef; public bar: ElementRef;
@ -48,16 +46,26 @@ export class SliderComponent implements ControlValueAccessor {
@Input() @Input()
public step = 1; public step = 1;
constructor(private readonly renderer: Renderer2) { } public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2
) {
}
public writeValue(obj: any) { public writeValue(obj: any) {
this.lastValue = this.value = Types.isNumber(obj) ? obj : 0; this.lastValue = this.value = Types.isNumber(obj) ? obj : 0;
this.updateThumbPosition(); this.updateThumbPosition();
this.changeDetector.detectChanges();
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {

11
src/Squidex/app/framework/angular/forms/stars.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
@ -52,12 +52,21 @@ export class StarsComponent implements ControlValueAccessor {
public value: number | null = 1; public value: number | null = 1;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public writeValue(obj: any) { public writeValue(obj: any) {
this.value = this.stars = Types.isNumber(obj) ? obj : 0; this.value = this.stars = Types.isNumber(obj) ? obj : 0;
this.changeDetector.markForCheck();
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
this.changeDetector.markForCheck();
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {

9
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
@ -129,6 +129,11 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
public addInput = new FormControl(); public addInput = new FormControl();
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngOnDestroy() { public ngOnDestroy() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
@ -179,6 +184,8 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
} else { } else {
this.items = []; this.items = [];
} }
this.changeDetector.detectChanges();
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {

11
src/Squidex/app/framework/angular/forms/toggle.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
@ -28,12 +28,21 @@ export class ToggleComponent implements ControlValueAccessor {
public isChecked: boolean | null = null; public isChecked: boolean | null = null;
public isDisabled = false; public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public writeValue(obj: any) { public writeValue(obj: any) {
this.isChecked = Types.isBoolean(obj) ? obj : null; this.isChecked = Types.isBoolean(obj) ? obj : null;
this.changeDetector.detectChanges();
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
} }
public registerOnChange(fn: any) { public registerOnChange(fn: any) {

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;
}; };
} }

46
src/Squidex/app/framework/angular/http/caching.interceptor.ts

@ -0,0 +1,46 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Injectable} from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { Types } from './../../internal';
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
private readonly cache: { [url: string]: HttpResponse<any> } = {};
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.method === 'GET' && req.reportProgress === false) {
const cacheEntry = this.cache[req.url];
if (cacheEntry) {
req = req.clone({ headers: req.headers.set('If-None-Match', cacheEntry.headers.get('Etag')!) });
}
return next.handle(req).pipe(
tap(response => {
if (Types.is(response, HttpResponse)) {
if (response.headers.get('Etag')) {
this.cache[req.url] = response;
}
}
}),
catchError(error => {
if (Types.is(error, HttpErrorResponse) && error.status === 304 && cacheEntry) {
return of(cacheEntry);
} else {
return throwError(error);
}
}));
} else {
return next.handle(req);
}
}
}

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') || '';

12
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -34,7 +34,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
public for: any; public for: any;
@Input() @Input()
public id: string; public helpId: string;
@Input() @Input()
public after = 1000; public after = 1000;
@ -62,9 +62,9 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
if (this.for && this.id && Types.isFunction(this.for.addEventListener)) { if (this.for && this.helpId && Types.isFunction(this.for.addEventListener)) {
this.showTimer = setTimeout(() => { this.showTimer = setTimeout(() => {
if (this.onboardingService.shouldShow(this.id)) { if (this.onboardingService.shouldShow(this.helpId)) {
const forRect = this.for.getBoundingClientRect(); const forRect = this.for.getBoundingClientRect();
const x = forRect.left + 0.5 * forRect.width; const x = forRect.left + 0.5 * forRect.width;
@ -81,14 +81,14 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
this.hideThis(); this.hideThis();
}, 10000); }, 10000);
this.onboardingService.disable(this.id); this.onboardingService.disable(this.helpId);
} }
} }
}, this.after); }, this.after);
this.forMouseDownListener = this.forMouseDownListener =
this.renderer.listen(this.for, 'mousedown', () => { this.renderer.listen(this.for, 'mousedown', () => {
this.onboardingService.disable(this.id); this.onboardingService.disable(this.helpId);
this.hideThis(); this.hideThis();
}); });
@ -106,7 +106,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
} }
public hideThis() { public hideThis() {
this.onboardingService.disable(this.id); this.onboardingService.disable(this.helpId);
this.ngOnDestroy(); this.ngOnDestroy();
} }

1
src/Squidex/app/framework/declarations.ts

@ -27,6 +27,7 @@ export * from './angular/forms/toggle.component';
export * from './angular/forms/transform-input.directive'; export * from './angular/forms/transform-input.directive';
export * from './angular/forms/validators'; export * from './angular/forms/validators';
export * from './angular/http/caching.interceptor';
export * from './angular/http/loading.interceptor'; export * from './angular/http/loading.interceptor';
export * from './angular/http/http-extensions'; export * from './angular/http/http-extensions';

6
src/Squidex/app/framework/module.ts

@ -13,6 +13,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { import {
AnalyticsService, AnalyticsService,
AutocompleteComponent, AutocompleteComponent,
CachingInterceptor,
CanDeactivateGuard, CanDeactivateGuard,
ClipboardService, ClipboardService,
ConfirmClickDirective, ConfirmClickDirective,
@ -233,6 +234,11 @@ export class SqxFrameworkModule {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: LoadingInterceptor, useClass: LoadingInterceptor,
multi: true multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: CachingInterceptor,
multi: true
} }
] ]
}; };

4
src/Squidex/app/framework/services/dialog.service.ts

@ -82,10 +82,6 @@ export class DialogService {
this.notificationsStream$.next(notification); this.notificationsStream$.next(notification);
} }
public confirmUnsavedChanges(): Observable<boolean> {
return this.confirm('Unsaved changes', 'You have unsaved changes, do you want to close the current content view?');
}
public confirm(title: string, text: string): Observable<boolean> { public confirm(title: string, text: string): Observable<boolean> {
const request = new DialogRequest(title, text); const request = new DialogRequest(title, text);

6
src/Squidex/app/framework/utils/immutable-array.ts

@ -138,21 +138,21 @@ export class ImmutableArray<T> implements Iterable<T> {
} }
public pushFront(...items: T[]): ImmutableArray<T> { public pushFront(...items: T[]): ImmutableArray<T> {
if (!items || items.length === 0) { if (items.length === 0) {
return this; return this;
} }
return new ImmutableArray<T>([...freeze(items), ...this.items]); return new ImmutableArray<T>([...freeze(items), ...this.items]);
} }
public push(...items: T[]): ImmutableArray<T> { public push(...items: T[]): ImmutableArray<T> {
if (!items || items.length === 0) { if (items.length === 0) {
return this; return this;
} }
return new ImmutableArray<T>([...this.items, ...freeze(items)]); return new ImmutableArray<T>([...this.items, ...freeze(items)]);
} }
public remove(...items: T[]): ImmutableArray<T> { public remove(...items: T[]): ImmutableArray<T> {
if (!items || items.length === 0) { if (items.length === 0) {
return this; return this;
} }

4
src/Squidex/app/shared/components/asset.component.html

@ -17,7 +17,7 @@
<div class="overlay-background"></div> <div class="overlay-background"></div>
<div class="overlay-menu"> <div class="overlay-menu">
<a class="file-download" [attr.href]="asset | sqxAssetUrl" target="_blank" (click)="$event.stopPropagation()"> <a class="file-download" [attr.href]="asset | sqxAssetUrl" target="_blank" rel="noopener" (click)="$event.stopPropagation()">
<i class="icon-download"></i> <i class="icon-download"></i>
</a> </a>
@ -115,7 +115,7 @@
<img class="user-picture" [attr.title]="asset.lastModifiedBy | sqxUserNameRef" [attr.src]="asset.lastModifiedBy | sqxUserPictureRef" /> <img class="user-picture" [attr.title]="asset.lastModifiedBy | sqxUserNameRef" [attr.src]="asset.lastModifiedBy | sqxUserPictureRef" />
</div> </div>
<div class="col col-actions text-right"> <div class="col col-actions text-right">
<a class="btn btn-link btn-secondary" [attr.href]="asset | sqxAssetUrl" target="_blank" (click)="$event.stopPropagation()"> <a class="btn btn-link btn-secondary" [attr.href]="asset | sqxAssetUrl" target="_blank" rel="noopener" (click)="$event.stopPropagation()">
<i class="icon-download"></i> <i class="icon-download"></i>
</a> </a>
</div> </div>

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;
}
}

5
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
@ -71,6 +71,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
public isDisabled = false; public isDisabled = false;
constructor( constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService, private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly uiState: UIState private readonly uiState: UIState
@ -103,6 +104,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} else { } else {
this.geolocationForm.enable(); this.geolocationForm.enable();
} }
this.changeDetector.detectChanges();
} }
private setDisabledStateOSM(isDisabled: boolean): void { private setDisabledStateOSM(isDisabled: boolean): void {

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>

6
src/Squidex/app/shared/components/search-form.component.html

@ -10,7 +10,7 @@
<i class="icon-caret-down"></i> <i class="icon-caret-down"></i>
</a> </a>
<sqx-onboarding-tooltip id="contentArchive" [for]="expand" position="bottomRight" after="60000"> <sqx-onboarding-tooltip helpId="contentArchive" [for]="expand" position="bottomRight" after="60000">
Click this icon to show the advanced search menu and to show the archive! Click this icon to show the advanced search menu and to show the archive!
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>
@ -30,7 +30,7 @@
</ng-container> </ng-container>
</form> </form>
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000"> <sqx-onboarding-tooltip helpId="contentFind" [for]="inputFind" position="bottomRight" after="120000">
Search for content using full text search over all fields and languages! Search for content using full text search over all fields and languages!
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
@ -70,7 +70,7 @@
</div> </div>
<div class="link"> <div class="link">
Read more about filtering in the <a href="https://docs.squidex.io/04-guides/02-api.html" target="_blank">Documentation</a>. Read more about filtering in the <a href="https://docs.squidex.io/04-guides/02-api.html" target="_blank" rel="noopener">Documentation</a>.
</div> </div>
</div> </div>
</div> </div>

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,

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save