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

2
README.md

@ -14,7 +14,7 @@ Please join our community forum: https://support.squidex.io
## 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

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)
{
partitionType.AddStructuralProperty(partitionItem.Key, edmValueType);
partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), edmValueType);
}
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(@event, nameof(@event));
if (!rule.IsEnabled)
{
return null;
}
if (!(@event.Payload is AppEvent appEvent))
{
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;
if (value is Instant instant &&
if (value is Instant &&
!string.Equals(nodeIn.Lhs[0], "mt", 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();
}
if (result.Count > 2)
{
result[2] = result[2].UnescapeEdmField();
}
if (result.Count > 0)
{
if (result[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase))

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 Status Status { get; set; }
public ScheduleJob ScheduleJob { get; set; }
public RefToken CreatedBy { get; set; }
@ -40,6 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
public NamedContentData DataDraft { get; set; }
public Status Status { get; set; }
public bool IsPending { get; set; }
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.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
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.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
{
private readonly HashSet<NamedId<Guid>> schemaIds = new HashSet<NamedId<Guid>>();
private readonly Dictionary<string, Guid> schemasByName = new Dictionary<string, Guid>();
private readonly FieldRegistry fieldRegistry;
private readonly IGrainFactory grainFactory;
@ -43,7 +42,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
switch (@event.Payload)
{
case SchemaCreated schemaCreated:
schemaIds.Add(schemaCreated.SchemaId);
schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id;
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 Guid id;
private enum Mode
{
Create,
Update,
Upsert
}
public Guid 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
{
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
{
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
{
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
{
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
{
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
{
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
{
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
{
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));
@ -128,7 +155,7 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion);
}
if (isUpdate && Version < 0)
if (mode == Mode.Update && Version < 0)
{
try
{
@ -141,7 +168,7 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectNotFoundException(id.ToString(), GetType());
}
if (!isUpdate && Version >= 0)
if (mode == Mode.Create && Version >= 0)
{
throw new DomainException("Object has already been created.");
}
@ -158,7 +185,7 @@ namespace Squidex.Infrastructure.Commands
if (result == null)
{
if (isUpdate)
if (mode == Mode.Update || (mode == Mode.Upsert && Version == 0))
{
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 object lockObject = new object();
private bool isInitialized;
private volatile bool isInitialized;
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 Microsoft.AspNetCore.Mvc;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
@ -58,7 +59,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("assets/{id}/")]
[ProducesResponseType(200)]
[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);
@ -67,10 +68,12 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NotFound();
}
var assetId = entity.Id.ToString();
Response.Headers["ETag"] = entity.FileVersion.ToString();
return new FileCallbackResult(entity.MimeType, entity.FileName, async bodyStream =>
{
var assetId = entity.Id.ToString();
if (entity.IsImage && (width.HasValue || height.HasValue))
{
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 result = context.Result<AssetSavedResult>();
var response = AssetReplacedDto.Create(command, result);
var response = AssetReplacedDto.FromCommand(command, result);
return StatusCode(201, response);
}
@ -236,7 +236,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[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));

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

@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
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
{

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
{
public sealed class AssetUpdateDto
public sealed class UpdateAssetDto
{
/// <summary>
/// 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();
Response.Headers["Etag"] = "1";
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.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public sealed class RuleDto
public sealed class RuleDto : IGenerateEtag
{
/// <summary>
/// The id of the rule.
@ -49,7 +50,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// <summary>
/// The version of the rule.
/// </summary>
public int Version { get; set; }
public long Version { get; set; }
/// <summary>
/// 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.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Mvc;
using NodaTime;
using Squidex.Areas.Api.Controllers.Rules.Models;
@ -32,6 +33,8 @@ namespace Squidex.Areas.Api.Controllers.Rules
[MustBeAppDeveloper]
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 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()));
Response.Headers["Etag"] = RuleActionsEtag;
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()));
Response.Headers["Etag"] = RuleTriggersEtag;
return Ok(response);
}
@ -96,6 +103,8 @@ namespace Squidex.Areas.Api.Controllers.Rules
var response = entities.Select(RuleDto.FromRule);
Response.Headers["ETag"] = response.ToManyEtag(0);
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>
/// The version of the schema.
/// </summary>
public int Version { get; set; }
public long Version { get; set; }
public static SchemaDetailsDto FromSchema(ISchemaEntity schema)
{

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

@ -7,6 +7,7 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -76,14 +77,14 @@ namespace Squidex.Areas.Frontend.Middlewares
return response;
}
var stylesTag = string.Empty;
var sb = new StringBuilder();
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;
}
@ -95,14 +96,14 @@ namespace Squidex.Areas.Frontend.Middlewares
return response;
}
var scriptsTag = string.Empty;
var sb = new StringBuilder();
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;
}

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.
</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>
</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.Commands;
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.Commands;
using Squidex.Domain.Apps.Entities.Contents.Edm;
@ -160,6 +162,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<GrainCommandMiddleware<AppCommand, IAppGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<ContentCommand, IContentGrain>>()
.As<ICommandMiddleware>();

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

@ -28,11 +28,12 @@ namespace Squidex.Pipeline.Swagger
using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md"))
{
var streamReader = new StreamReader(resourceStream);
using (var streamReader = new StreamReader(resourceStream))
{
return streamReader.ReadToEnd();
}
}
}
public static SwaggerDocument CreateApiDocument(HttpContext context, MyUrlsOptions urlOptions, string appName)
{

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 (this.user) {
this.usersState.update(this.user.user, value)
.subscribe(user => {
.subscribe(() => {
this.userForm.submitCompleted();
}, error => {
this.userForm.submitFailed(error);
});
} else {
this.usersState.create(value)
.subscribe(user => {
.subscribe(() => {
this.back();
}, error => {
this.userForm.submitFailed(error);

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

@ -14,7 +14,7 @@
</a>
</li>
<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
</a>
</li>

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

@ -52,7 +52,7 @@
<div class="card-text">
<div>Start with our ready to use blog.</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>
@ -69,7 +69,7 @@
<div class="card-text">
<div>Create your profile page.</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>
@ -86,7 +86,7 @@
<div class="card-text">
<div>Create app for Squidex Identity.</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>

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

@ -125,18 +125,18 @@
<h1>Awesome, now you know the basics!</h1>
<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>
Do you want to join our community?
</p>
<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
</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
</a>
</div>

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

@ -5,6 +5,7 @@
* 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-history.component';
export * from './pages/content/content-page.component';

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

@ -25,6 +25,7 @@ import {
ArrayEditorComponent,
ArrayItemComponent,
AssetsEditorComponent,
CommentsPageComponent,
ContentFieldComponent,
ContentHistoryComponent,
ContentItemComponent,
@ -75,6 +76,10 @@ const routes: Routes = [
data: {
channel: 'contents.{contentId}'
}
},
{
path: 'comments',
component: CommentsPageComponent
}
]
}
@ -95,6 +100,7 @@ const routes: Routes = [
ArrayEditorComponent,
ArrayItemComponent,
AssetsEditorComponent,
CommentsPageComponent,
ContentFieldComponent,
ContentHistoryComponent,
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>
</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.
</sqx-onboarding-tooltip>
</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.
*/
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 { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
@ -22,10 +22,9 @@ import {
@Component({
selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
templateUrl: './content-field.component.html'
})
export class ContentFieldComponent implements OnChanges {
export class ContentFieldComponent implements DoCheck, OnChanges {
@Input()
public form: EditContentForm;
@ -49,20 +48,26 @@ export class ContentFieldComponent implements OnChanges {
public isInvalid: Observable<boolean>;
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) {
this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code];
control = this.fieldForm.controls[this.language.iso2Code];
} else {
this.selectedFormControl = this.fieldForm.controls[fieldInvariant];
control = this.fieldForm.controls[fieldInvariant];
}
if (changes['language']) {
if (Types.isFunction(this.selectedFormControl['_clearChangeFns'])) {
if (this.selectedFormControl !== control) {
if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) {
this.selectedFormControl['_clearChangeFns']();
}
}
if (changes['fieldForm']) {
this.isInvalid = this.fieldForm.statusChanges.pipe(startWith(this.fieldForm.invalid), map(x => this.fieldForm.invalid));
this.selectedFormControl = control;
}
}
}

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>
Activity
</ng-container>

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

@ -51,7 +51,7 @@ export class ContentHistoryComponent {
timer(0, 10000),
this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000))
).pipe(
switchMap(app => this.historyService.getHistory(this.appsState.appName, this.channel)));
switchMap(() => this.historyService.getHistory(this.appsState.appName, this.channel)));
constructor(
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>
</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.
</sqx-onboarding-tooltip>
</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) {
return of(true);
} 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 (asProposal) {
this.contentsState.proposeUpdate(this.content, value)
.subscribe(dto => {
.subscribe(() => {
this.contentForm.submitCompleted();
}, error => {
this.contentForm.submitFailed(error);
});
} else {
this.contentsState.update(this.content, value)
.subscribe(dto => {
.subscribe(() => {
this.contentForm.submitCompleted();
}, error => {
this.contentForm.submitFailed(error);
@ -156,7 +156,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
}
} else {
this.contentsState.create(value, publish)
.subscribe(dto => {
.subscribe(() => {
this.back();
}, 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">
<sqx-array-item
[form]="form"

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

@ -4,16 +4,11 @@
.array-container {
background: $color-border;
padding: 1rem;
padding-bottom: 1px;
position: relative;
margin-bottom: 1rem;
}
.item {
& {
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 { FormArray } from '@angular/forms';
import { AbstractControl, FormArray } from '@angular/forms';
import {
AppLanguageDto,
@ -44,4 +44,10 @@ export class ArrayEditorComponent {
public addItem() {
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 {
this.oldAssets = ImmutableArray.empty();
this.changeDetector.detectChanges();
}
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
}
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 {
this.contentItems = ImmutableArray.empty();
this.changeDetector.detectChanges();
}
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
}
public registerOnChange(fn: any) {

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

@ -26,7 +26,7 @@
</div>
</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-image">
<img src="/images/dashboard-api.png" />
@ -40,7 +40,7 @@
</div>
</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-image">
<img src="/images/dashboard-feedback.png" />
@ -54,7 +54,7 @@
</div>
</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-image">
<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" />
<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>
</div>
</div>

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

@ -25,7 +25,7 @@
</div>
<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>

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);
this.rulesState.create(requestDto)
.subscribe(dto => {
.subscribe(() => {
this.complete();
this.actionForm.submitCompleted();
@ -134,7 +134,7 @@ export class RuleWizardComponent implements OnInit {
private updateTrigger() {
this.rulesState.updateTrigger(this.rule, this.trigger)
.subscribe(dto => {
.subscribe(() => {
this.complete();
this.triggerForm.submitCompleted();
@ -145,7 +145,7 @@ export class RuleWizardComponent implements OnInit {
private updateAction() {
this.rulesState.updateAction(this.rule, this.action)
.subscribe(dto => {
.subscribe(() => {
this.complete();
this.actionForm.submitCompleted();

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

@ -84,8 +84,8 @@
<i class="icon-help"></i>
</a>
<sqx-onboarding-tooltip id="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.
<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" rel="noopener">https://docs.squidex.io</a> for the full documentation.
</sqx-onboarding-tooltip>
</ng-container>
</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) {
this.schemasState.update(this.schema, value)
.subscribe(dto => {
.subscribe(() => {
this.complete();
}, error => {
this.editForm.submitFailed(error);

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

@ -41,11 +41,11 @@
</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.
</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.
</sqx-onboarding-tooltip>
</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) {
this.schemasState.configureScripts(this.schema, value)
.subscribe(dto => {
.subscribe(() => {
this.complete();
}, 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 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">
<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">
Download:
<a href="{{backup | sqxBackupDownloadUrl}}" target="_blank">
<a href="{{backup | sqxBackupDownloadUrl}}" target="_blank" rel="noopener">
Ready
</a>
</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');
this.contributorsState.assign(requestDto)
.subscribe(dto => {
.subscribe(() => {
this.assignContributorForm.submitCompleted();
}, error => {
this.assignContributorForm.submitFailed(error);

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

@ -72,7 +72,7 @@
</div>
<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>
</ng-container>
</ng-container>

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

@ -76,11 +76,9 @@ export class ConfirmClickDirective implements OnDestroy {
.subscribe(result => {
this.isOpen = false;
if (result) {
if (result) {
this.clickConfirmed.delayEmit();
}
}
subscription.unsubscribe();

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

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

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

@ -5,7 +5,7 @@
* 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';
const KEY_ENTER = 13;
@ -46,6 +46,11 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngAfterContentInit() {
if (this.templates.length === 1) {
this.itemTemplate = this.selectionTemplate = this.templates.first;
@ -62,10 +67,14 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
public writeValue(obj: any) {
this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0);
this.changeDetector.detectChanges();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
}
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.
*/
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 { Types } from '@app/framework/internal';
@ -31,8 +31,6 @@ export class SliderComponent implements ControlValueAccessor {
private value: number;
private isDragging = false;
public isDisabled = false;
@ViewChild('bar')
public bar: ElementRef;
@ -48,16 +46,26 @@ export class SliderComponent implements ControlValueAccessor {
@Input()
public step = 1;
constructor(private readonly renderer: Renderer2) { }
public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2
) {
}
public writeValue(obj: any) {
this.lastValue = this.value = Types.isNumber(obj) ? obj : 0;
this.updateThumbPosition();
this.changeDetector.detectChanges();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
}
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.
*/
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 { Types } from '@app/framework/internal';
@ -52,12 +52,21 @@ export class StarsComponent implements ControlValueAccessor {
public value: number | null = 1;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public writeValue(obj: any) {
this.value = this.stars = Types.isNumber(obj) ? obj : 0;
this.changeDetector.markForCheck();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.markForCheck();
}
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.
*/
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 { Subscription } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
@ -129,6 +129,11 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
public addInput = new FormControl();
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngOnDestroy() {
this.subscription.unsubscribe();
}
@ -179,6 +184,8 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
} else {
this.items = [];
}
this.changeDetector.detectChanges();
}
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.
*/
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal';
@ -28,12 +28,21 @@ export class ToggleComponent implements ControlValueAccessor {
public isChecked: boolean | null = null;
public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public writeValue(obj: any) {
this.isChecked = Types.isBoolean(obj) ? obj : null;
this.changeDetector.detectChanges();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.detectChanges();
}
public registerOnChange(fn: any) {

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

@ -126,7 +126,7 @@ export module ValidatorsEx {
}
export function noop(): ValidatorFn {
return (control: AbstractControl) => {
return () => {
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>>> {
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>>> {
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>>> {
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>>> {
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>>> {
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 {
@ -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>) => {
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;
@Input()
public id: string;
public helpId: string;
@Input()
public after = 1000;
@ -62,9 +62,9 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
}
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(() => {
if (this.onboardingService.shouldShow(this.id)) {
if (this.onboardingService.shouldShow(this.helpId)) {
const forRect = this.for.getBoundingClientRect();
const x = forRect.left + 0.5 * forRect.width;
@ -81,14 +81,14 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
this.hideThis();
}, 10000);
this.onboardingService.disable(this.id);
this.onboardingService.disable(this.helpId);
}
}
}, this.after);
this.forMouseDownListener =
this.renderer.listen(this.for, 'mousedown', () => {
this.onboardingService.disable(this.id);
this.onboardingService.disable(this.helpId);
this.hideThis();
});
@ -106,7 +106,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
}
public hideThis() {
this.onboardingService.disable(this.id);
this.onboardingService.disable(this.helpId);
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/validators';
export * from './angular/http/caching.interceptor';
export * from './angular/http/loading.interceptor';
export * from './angular/http/http-extensions';

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

@ -13,6 +13,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {
AnalyticsService,
AutocompleteComponent,
CachingInterceptor,
CanDeactivateGuard,
ClipboardService,
ConfirmClickDirective,
@ -233,6 +234,11 @@ export class SqxFrameworkModule {
provide: HTTP_INTERCEPTORS,
useClass: LoadingInterceptor,
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);
}
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> {
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> {
if (!items || items.length === 0) {
if (items.length === 0) {
return this;
}
return new ImmutableArray<T>([...freeze(items), ...this.items]);
}
public push(...items: T[]): ImmutableArray<T> {
if (!items || items.length === 0) {
if (items.length === 0) {
return this;
}
return new ImmutableArray<T>([...this.items, ...freeze(items)]);
}
public remove(...items: T[]): ImmutableArray<T> {
if (!items || items.length === 0) {
if (items.length === 0) {
return this;
}

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

@ -17,7 +17,7 @@
<div class="overlay-background"></div>
<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>
</a>
@ -115,7 +115,7 @@
<img class="user-picture" [attr.title]="asset.lastModifiedBy | sqxUserNameRef" [attr.src]="asset.lastModifiedBy | sqxUserPictureRef" />
</div>
<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>
</a>
</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.
*/
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 {
@ -71,6 +71,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly uiState: UIState
@ -103,6 +104,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} else {
this.geolocationForm.enable();
}
this.changeDetector.detectChanges();
}
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>
Activity
</ng-container>

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

@ -10,7 +10,7 @@
<i class="icon-caret-down"></i>
</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!
</sqx-onboarding-tooltip>
</ng-container>
@ -30,7 +30,7 @@
</ng-container>
</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!
</sqx-onboarding-tooltip>
@ -70,7 +70,7 @@
</div>
<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>

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/assets-list.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/geolocation-editor.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/auth.service';
export * from './services/backups.service';
export * from './services/comments.service';
export * from './services/contents.service';
export * from './services/graphql.service';
export * from './services/help.service';
@ -49,6 +50,8 @@ export * from './state/backups.forms';
export * from './state/backups.state';
export * from './state/clients.forms';
export * from './state/clients.state';
export * from './state/comments.form';
export * from './state/comments.state';
export * from './state/contents.forms';
export * from './state/contents.state';
export * from './state/contributors.forms';

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

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

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

@ -195,9 +195,7 @@ export class AssetsService {
public uploadFile(appName: string, file: File, user: string, now: DateTime): Observable<number | AssetDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets`);
const req = new HttpRequest('POST', url, getFormData(file), {
reportProgress: true
});
const req = new HttpRequest('POST', url, getFormData(file), { reportProgress: true });
return this.http.request<any>(req).pipe(
filter(event =>
@ -237,7 +235,7 @@ export class AssetsService {
throw 'Invalid';
}
}),
tap(dto => {
tap(() => {
this.analytics.trackEvent('Asset', 'Uploaded', appName);
}),
pretifyError('Failed to upload asset. Please reload.'));
@ -276,12 +274,7 @@ export class AssetsService {
public replaceFile(appName: string, id: string, file: File, version: Version): Observable<number | Versioned<AssetReplacedDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}/content`);
const req = new HttpRequest('PUT', url, getFormData(file), {
headers: new HttpHeaders({
'If-Match': version.value
}),
reportProgress: true
});
const req = new HttpRequest('PUT', url, getFormData(file), { headers: new HttpHeaders().set('If-Match', version.value), reportProgress: true });
return this.http.request(req).pipe(
filter(event =>

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

Loading…
Cancel
Save