Browse Source

Merge branch 'master' into refactoring/sc

# Conflicts:
#	src/Squidex/app/features/content/shared/assets-editor.component.html
#	src/Squidex/app/features/content/shared/assets-editor.component.ts
#	src/Squidex/app/shared/components/assets-selector.component.ts
#	src/Squidex/app/shared/components/markdown-editor.component.html
#	src/Squidex/app/shared/components/markdown-editor.component.ts
#	src/Squidex/app/shared/components/permission.directive.ts
#	src/Squidex/app/shared/components/rich-editor.component.ts
pull/345/head
Sebastian Stehle 7 years ago
parent
commit
abdd7432e5
  1. 4
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  3. 4
      src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  4. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  5. 8
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  6. 2
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  7. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  8. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs
  9. 2
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  11. 2
      src/Squidex.Domain.Apps.Entities/EntityMapper.cs
  12. 4
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  13. 4
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  14. 2
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  15. 4
      src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs
  16. 4
      src/Squidex.Infrastructure/Log/LockingLogStore.cs
  17. 5
      src/Squidex.Infrastructure/Security/Extensions.cs
  18. 2
      src/Squidex.Infrastructure/States/IStore.cs
  19. 11
      src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs
  20. 4
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  21. 6
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  22. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  23. 2
      src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  24. 2
      src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  25. 13
      src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  26. 1
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  27. 2
      src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
  28. 2
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  29. 40
      src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  30. 9
      src/Squidex/Config/Logging.cs
  31. 9
      src/Squidex/Config/MyIdentityOptions.cs
  32. 11
      src/Squidex/Pipeline/Extensions.cs
  33. 9
      src/Squidex/Pipeline/Squid/SquidMiddleware.cs
  34. 2
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  35. 5
      src/Squidex/app/features/content/shared/assets-editor.component.html
  36. 22
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  37. 2
      src/Squidex/app/features/content/shared/references-editor.component.ts
  38. 2
      src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html
  39. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  40. 56
      src/Squidex/app/framework/angular/forms/file-drop.directive.ts
  41. 4
      src/Squidex/app/shared/components/assets-list.component.html
  42. 22
      src/Squidex/app/shared/components/assets-list.component.ts
  43. 4
      src/Squidex/app/shared/components/assets-selector.component.ts
  44. 6
      src/Squidex/app/shared/components/markdown-editor.component.html
  45. 47
      src/Squidex/app/shared/components/markdown-editor.component.ts
  46. 6
      src/Squidex/app/shared/components/rich-editor.component.html
  47. 84
      src/Squidex/app/shared/components/rich-editor.component.ts
  48. 1
      src/Squidex/app/shared/state/contents.forms.ts
  49. 10
      src/Squidex/appsettings.json
  50. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs
  51. 1
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  52. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

4
src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs

@ -37,12 +37,12 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null) public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null)
: base(id, name, partitioning, properties) : base(id, name, partitioning, properties, settings)
{ {
} }
public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties properties = null, IFieldSettings settings = null) public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties properties = null, IFieldSettings settings = null)
: this(id, name, partitioning, properties) : this(id, name, partitioning, properties, settings)
{ {
Guard.NotNull(fields, nameof(fields)); Guard.NotNull(fields, nameof(fields));

2
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
else else
{ {
fieldsByName = fieldsOrdered.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); fieldsByName = fieldsOrdered.ToDictionary(x => x.Name);
} }
} }

4
src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{ {
foreach (var sourceField in source.Ordered) foreach (var sourceField in source.Ordered)
{ {
if (!target.ByName.TryGetValue(sourceField.Name, out var targetField)) if (!target.ByName.TryGetValue(sourceField.Name, out _))
{ {
var id = sourceField.NamedId(); var id = sourceField.NamedId();
@ -184,7 +184,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField)
{ {
var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty; var fields = ((IArrayField)sourceField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, id, options); var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, id, options);

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

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return null; return null;
} }
if (!(@event.Payload is AppEvent appEvent)) if (!(@event.Payload is AppEvent))
{ {
return null; return null;
} }

8
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs

@ -33,9 +33,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger)
{ {
var typed = @event as TEnrichedEvent; if (@event is TEnrichedEvent typed)
if (typed != null)
{ {
return Trigger(typed, (TTrigger)trigger); return Trigger(typed, (TTrigger)trigger);
} }
@ -45,9 +43,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId)
{ {
var typed = @event as TEvent; if (@event is TEvent typed)
if (typed != null)
{ {
return Trigger(typed, (TTrigger)trigger, ruleId); return Trigger(typed, (TTrigger)trigger, ruleId);
} }

2
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
switch (value) switch (value)
{ {
case JsonNull n: case JsonNull _:
return JsValue.Null; return JsValue.Null;
case JsonScalar<string> s: case JsonScalar<string> s:
return new JsString(s.Value); return new JsString(s.Value);

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private IValidator CreateSchemaValidator(bool isPartial) private IValidator CreateSchemaValidator(bool isPartial)
{ {
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(); var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(schema.Fields.Count);
foreach (var field in schema.FieldsByName) foreach (var field in schema.FieldsByName)
{ {

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
} }
var nestedSchema = new Dictionary<string, (bool IsOptional, IValidator Validator)>(); var nestedSchema = new Dictionary<string, (bool IsOptional, IValidator Validator)>(field.Fields.Count);
foreach (var nestedField in field.Fields) foreach (var nestedField in field.Fields)
{ {

2
src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs

@ -29,8 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IUserResolver userResolver; private readonly IUserResolver userResolver;
private readonly IAppsByNameIndex appsByNameIndex; private readonly IAppsByNameIndex appsByNameIndex;
private readonly HashSet<string> contributors = new HashSet<string>(); private readonly HashSet<string> contributors = new HashSet<string>();
private readonly Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private Dictionary<string, string> usersWithEmail = new Dictionary<string, string>(); private Dictionary<string, string> usersWithEmail = new Dictionary<string, string>();
private Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private bool isReserved; private bool isReserved;
private string appName; private string appName;

2
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs

@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
[CollectionName("Index_AppsByName")] [CollectionName("Index_AppsByName")]
public sealed class GrainState public sealed class GrainState
{ {
public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>(); public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>(StringComparer.Ordinal);
} }
public AppsByNameIndexGrain(IStore<string> store) public AppsByNameIndexGrain(IStore<string> store)

2
src/Squidex.Domain.Apps.Entities/EntityMapper.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities
private static void SetCreated(IEntity entity, EnvelopeHeaders headers) private static void SetCreated(IEntity entity, EnvelopeHeaders headers)
{ {
if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) if (entity is IUpdateableEntity updateable && updateable.Created == default)
{ {
updateable.Created = headers.Timestamp(); updateable.Created = headers.Timestamp();
} }

4
src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs

@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{ {
var target = kvp.Value; var target = kvp.Value;
var (from, to) = GetDateRange(today, target.NumDays); var (from, _) = GetDateRange(today, target.NumDays);
if (!target.Triggered.HasValue || target.Triggered < from) if (!target.Triggered.HasValue || target.Triggered < from)
{ {
@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
await WriteStateAsync(); await WriteStateAsync();
} }
private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) private static (DateTime, DateTime) GetDateRange(DateTime today, int? numDays)
{ {
if (numDays.HasValue) if (numDays.HasValue)
{ {

4
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -220,9 +220,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var schemaSource = Snapshot.SchemaDef; var schemaSource = Snapshot.SchemaDef;
var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton);
var @events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options);
foreach (var @event in @events) foreach (var @event in events)
{ {
RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event));
} }

2
src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.States
return (existing.Doc, existing.Version); return (existing.Doc, existing.Version);
} }
return (default(T), EtagVersion.NotFound); return (default, EtagVersion.NotFound);
} }
} }

4
src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs

@ -82,7 +82,7 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
Task.Delay(ReconnectWaitMs, timerCts.Token).ContinueWith(t => Task.Delay(ReconnectWaitMs, timerCts.Token).ContinueWith(t =>
{ {
dispatcher.DispatchAsync(() => Subscribe()); dispatcher.DispatchAsync(Subscribe);
}).Forget(); }).Forget();
} }
else else
@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.EventSourcing
public async Task StopAsync() public async Task StopAsync()
{ {
await dispatcher.DispatchAsync(() => Unsubscribe()); await dispatcher.DispatchAsync(Unsubscribe);
await dispatcher.StopAndWaitAsync(); await dispatcher.StopAndWaitAsync();
timerCts.Cancel(); timerCts.Cancel();

4
src/Squidex.Infrastructure/Log/LockingLogStore.cs

@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.Log
break; break;
} }
await Task.Delay(2000); await Task.Delay(2000, cts.Token);
} }
if (!cts.IsCancellationRequested) if (!cts.IsCancellationRequested)
@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.Log
} }
else else
{ {
await stream.WriteAsync(LockedText, 0, LockedText.Length); await stream.WriteAsync(LockedText, 0, LockedText.Length, cts.Token);
} }
} }
} }

5
src/Squidex.Infrastructure/Security/Extensions.cs

@ -23,6 +23,11 @@ namespace Squidex.Infrastructure.Security
return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value;
} }
public static string UserOrClientId(this ClaimsPrincipal principal)
{
return principal.OpenIdSubject() ?? principal.OpenIdClientId();
}
public static string OpenIdPreferredUserName(this ClaimsPrincipal principal) public static string OpenIdPreferredUserName(this ClaimsPrincipal principal)
{ {
return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value; return principal.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.PreferredUserName)?.Value;

2
src/Squidex.Infrastructure/States/IStore.cs

@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.States
{ {
public delegate void HandleEvent(Envelope<IEvent> @event); public delegate void HandleEvent(Envelope<IEvent> @event);
public delegate void HandleSnapshot<T>(T state); public delegate void HandleSnapshot<in T>(T state);
public interface IStore<in TKey> public interface IStore<in TKey>
{ {

11
src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs

@ -27,12 +27,19 @@ namespace Squidex.Shared.Identity
public static PermissionSet Permissions(this ClaimsPrincipal principal) public static PermissionSet Permissions(this ClaimsPrincipal principal)
{ {
return new PermissionSet(principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value))); return new PermissionSet(principal.Claims
.Where(x =>
x.Type == SquidexClaimTypes.Permissions ||
x.Type == SquidexClaimTypes.PermissionsClient)
.Select(x => new Permission(x.Value)));
} }
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal) public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal)
{ {
return principal.Claims.Where(c => c.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal)); return principal.Claims
.Where(x =>
x.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal) ||
x.Type.StartsWith(SquidexClaimTypes.PrefixClient, StringComparison.Ordinal));
} }
} }
} }

4
src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -21,8 +21,12 @@ namespace Squidex.Shared.Identity
public static readonly string Permissions = "urn:squidex:permissions"; public static readonly string Permissions = "urn:squidex:permissions";
public static readonly string PermissionsClient = "client_urn:squidex:permissions";
public static readonly string Prefix = "urn:squidex:"; public static readonly string Prefix = "urn:squidex:";
public static readonly string PrefixClient = "client_urn:squidex:";
public static readonly string PictureUrlStore = "store"; public static readonly string PictureUrlStore = "store";
} }
} }

6
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -57,12 +57,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetApps() public async Task<IActionResult> GetApps()
{ {
var userId = HttpContext.User.OpenIdSubject(); var userOrClientId = HttpContext.User.UserOrClientId();
var userPermissions = HttpContext.User.Permissions(); var userPermissions = HttpContext.User.Permissions();
var entities = await appProvider.GetUserApps(userId, userPermissions); var entities = await appProvider.GetUserApps(userOrClientId, userPermissions);
var response = entities.ToArray(a => AppDto.FromApp(a, userId, userPermissions, appPlansProvider)); var response = entities.ToArray(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider));
Response.Headers[HeaderNames.ETag] = response.ToManyEtag(); Response.Headers[HeaderNames.ETag] = response.ToManyEtag();

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

@ -57,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("assets/{id}/")] [Route("assets/{id}/")]
[ProducesResponseType(200)] [ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0.5)] [ApiCosts(0.5)]
public async Task<IActionResult> GetAssetContent(Guid id, [FromQuery] long version = EtagVersion.Any, [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)
{ {

2
src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs

@ -40,7 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpGet] [HttpGet]
[Route("apps/{app}/backups/{id}")] [Route("apps/{app}/backups/{id}")]
[ResponseCache(Duration = 3600 * 24 * 30)] [ResponseCache(Duration = 3600 * 24 * 30)]
[ProducesResponseType(200)] [ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0)] [ApiCosts(0)]
[AllowAnonymous] [AllowAnonymous]
public IActionResult GetBackupContent(string app, Guid id) public IActionResult GetBackupContent(string app, Guid id)

2
src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -21,7 +21,7 @@ using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups namespace Squidex.Areas.Api.Controllers.Backups
{ {
/// <summary> /// <summary>
/// Manages backups for app. /// Manages backups for apps.
/// </summary> /// </summary>
[ApiExplorerSettings(GroupName = nameof(Backups))] [ApiExplorerSettings(GroupName = nameof(Backups))]
public class BackupsController : ApiController public class BackupsController : ApiController

13
src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs

@ -11,12 +11,16 @@ using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models; using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared; using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups namespace Squidex.Areas.Api.Controllers.Backups
{ {
/// <summary>
/// Manages backups for apps.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Backups))]
public class RestoreController : ApiController public class RestoreController : ApiController
{ {
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
@ -28,17 +32,18 @@ namespace Squidex.Areas.Api.Controllers.Backups
} }
/// <summary> /// <summary>
/// Get current status. /// Get current restore status.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// 200 => Status returned. /// 200 => Status returned.
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("apps/restore/")] [Route("apps/restore/")]
[ProducesResponseType(typeof(RestoreJobDto), 200)]
[ApiPermission(Permissions.AdminRestoreRead)] [ApiPermission(Permissions.AdminRestoreRead)]
public async Task<IActionResult> GetJob() public async Task<IActionResult> GetJob()
{ {
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(User.OpenIdSubject()); var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id);
var job = await restoreGrain.GetJobAsync(); var job = await restoreGrain.GetJobAsync();
@ -64,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[ApiPermission(Permissions.AdminRestoreCreate)] [ApiPermission(Permissions.AdminRestoreCreate)]
public async Task<IActionResult> PostRestore([FromBody] RestoreRequest request) public async Task<IActionResult> PostRestore([FromBody] RestoreRequest request)
{ {
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(User.OpenIdSubject()); var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(SingleGrain.Id);
await restoreGrain.RestoreAsync(request.Url, request.Name); await restoreGrain.RestoreAsync(request.Url, request.Name);

1
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs

@ -14,7 +14,6 @@ using Squidex.Config;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger; using Squidex.Pipeline.Swagger;
using Squidex.Shared; using Squidex.Shared;

2
src/Squidex/Areas/Api/Controllers/Ping/PingController.cs

@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Ping
[Route("ping/{app}/")] [Route("ping/{app}/")]
[ApiPermission(Permissions.AppCommon)] [ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetPing(string app) public IActionResult GetAppPing(string app)
{ {
return NoContent(); return NoContent();
} }

2
src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -136,7 +136,7 @@ namespace Squidex.Areas.Api.Controllers.Users
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("users/{id}/picture/")] [Route("users/{id}/picture/")]
[ProducesResponseType(200)] [ProducesResponseType(typeof(FileResult), 200)]
[ResponseCache(Duration = 3600)] [ResponseCache(Duration = 3600)]
public async Task<IActionResult> GetUserPicture(string id) public async Task<IActionResult> GetUserPicture(string id)
{ {

40
src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityServer4; using IdentityServer4;
using IdentityServer4.Models; using IdentityServer4.Models;
@ -17,6 +18,8 @@ using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Areas.IdentityServer.Config namespace Squidex.Areas.IdentityServer.Config
{ {
@ -25,14 +28,17 @@ namespace Squidex.Areas.IdentityServer.Config
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(IOptions<MyUrlsOptions> urlsOptions, IAppProvider appProvider) public LazyClientStore(
IOptions<MyUrlsOptions> urlsOptions,
IOptions<MyIdentityOptions> identityOptions,
IAppProvider appProvider)
{ {
Guard.NotNull(urlsOptions, nameof(urlsOptions)); Guard.NotNull(urlsOptions, nameof(urlsOptions));
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider; this.appProvider = appProvider;
CreateStaticClients(urlsOptions); CreateStaticClients(urlsOptions, identityOptions);
} }
public async Task<Client> FindClientByIdAsync(string clientId) public async Task<Client> FindClientByIdAsync(string clientId)
@ -83,15 +89,15 @@ namespace Squidex.Areas.IdentityServer.Config
}; };
} }
private void CreateStaticClients(IOptions<MyUrlsOptions> urlsOptions) private void CreateStaticClients(IOptions<MyUrlsOptions> urlsOptions, IOptions<MyIdentityOptions> identityOptions)
{ {
foreach (var client in CreateStaticClients(urlsOptions.Value)) foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value))
{ {
staticClients[client.ClientId] = client; staticClients[client.ClientId] = client;
} }
} }
private static IEnumerable<Client> CreateStaticClients(MyUrlsOptions urlsOptions) private static IEnumerable<Client> CreateStaticClients(MyUrlsOptions urlsOptions, MyIdentityOptions identityOptions)
{ {
var frontendId = Constants.FrontendClient; var frontendId = Constants.FrontendClient;
@ -150,6 +156,30 @@ namespace Squidex.Areas.IdentityServer.Config
}, },
RequireConsent = false RequireConsent = false
}; };
if (identityOptions.IsAdminClientConfigured())
{
var id = identityOptions.AdminClientId;
yield return new Client
{
ClientId = id,
ClientName = id,
ClientSecrets = new List<Secret> { new Secret(identityOptions.AdminClientSecret.Sha256()) },
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
{
Constants.ApiScope,
Constants.RoleScope,
Constants.PermissionsScope
},
Claims = new List<Claim>
{
new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)
}
};
}
} }
} }
} }

9
src/Squidex/Config/Logging.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
#define LOG_ALL_IDENTITY_SERVER_NONE
using System; using System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -40,7 +42,12 @@ namespace Squidex.Config
{ {
return level > LogLevel.Information; return level > LogLevel.Information;
} }
#if LOG_ALL_IDENTITY_SERVER
if (category.StartsWith("IdentityServer4.", StringComparison.OrdinalIgnoreCase))
{
return true;
}
#endif
return level >= LogLevel.Information; return level >= LogLevel.Information;
}); });
} }

9
src/Squidex/Config/MyIdentityOptions.cs

@ -13,6 +13,10 @@ namespace Squidex.Config
public string AdminPassword { get; set; } public string AdminPassword { get; set; }
public string AdminClientId { get; set; }
public string AdminClientSecret { get; set; }
public string GithubClient { get; set; } public string GithubClient { get; set; }
public string GithubSecret { get; set; } public string GithubSecret { get; set; }
@ -48,6 +52,11 @@ namespace Squidex.Config
return !string.IsNullOrWhiteSpace(AdminEmail) && !string.IsNullOrWhiteSpace(AdminPassword); return !string.IsNullOrWhiteSpace(AdminEmail) && !string.IsNullOrWhiteSpace(AdminPassword);
} }
public bool IsAdminClientConfigured()
{
return !string.IsNullOrWhiteSpace(AdminClientId) && !string.IsNullOrWhiteSpace(AdminClientSecret);
}
public bool IsOidcConfigured() public bool IsOidcConfigured()
{ {
return !string.IsNullOrWhiteSpace(OidcAuthority) && !string.IsNullOrWhiteSpace(OidcClient) && !string.IsNullOrWhiteSpace(OidcSecret); return !string.IsNullOrWhiteSpace(OidcAuthority) && !string.IsNullOrWhiteSpace(OidcClient) && !string.IsNullOrWhiteSpace(OidcSecret);

11
src/Squidex/Pipeline/Extensions.cs

@ -29,12 +29,17 @@ namespace Squidex.Pipeline
{ {
var parts = clientId.Split(':', '~'); var parts = clientId.Split(':', '~');
if (parts.Length != 2) if (parts.Length == 1)
{ {
return (null, null); return (null, parts[0]);
} }
return (parts[0], parts[1]); if (parts.Length == 2)
{
return (parts[0], parts[1]);
}
return (null, null);
} }
} }
} }

9
src/Squidex/Pipeline/Squid/SquidMiddleware.cs

@ -63,14 +63,9 @@ namespace Squidex.Pipeline.Squid
background = backgroundValue; background = backgroundValue;
} }
var isSmall = false; var isSmall = request.Query.TryGetValue("small", out _);
if (request.Query.TryGetValue("small", out _)) string svg;
{
isSmall = true;
}
var svg = string.Empty;
if (isSmall) if (isSmall)
{ {

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

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';

5
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -1,8 +1,9 @@
<div class="assets-container" [class.disabled]="snapshot.isDisabled" (paste)="pasteFiles($event)" tabindex="1000">
<div class="assets-container" [class.disabled]="snapshot.isDisabled" (sqxFileDrop)="addFiles($event)" tabindex="1000">
<div class="header list"> <div class="header list">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<div class="drop-area align-items-center" (click)="assetsDialog.show()"> <div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxFileDrop)="addFiles($event)">
Drop files or click here to add assets. Drop files or click here to add assets.
</div> </div>
</div> </div>

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

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
@ -104,23 +102,9 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
this.next(s => ({ ...s, assets })); this.next(s => ({ ...s, assets }));
} }
public pasteFiles(event: ClipboardEvent) { public addFiles(files: File[]) {
for (let i = 0; i < event.clipboardData.items.length; i++) { for (let file of files) {
const file = event.clipboardData.items[i].getAsFile(); this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
if (file) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
}
}
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
}
} }
} }

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

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';

2
src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html

@ -4,7 +4,7 @@
<sqx-control-errors for="condition" [submitted]="triggerFormSubmitted"></sqx-control-errors> <sqx-control-errors for="condition" [submitted]="triggerFormSubmitted"></sqx-control-errors>
<textarea class="form-control code" id="condition" formControlName="condition"></textarea> <textarea class="form-control code" id="condition" formControlName="condition" placeholder="Optional condition as javascript expression"></textarea>
</div> </div>
<div class="help"> <div class="help">

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -21,7 +21,7 @@
<span class="truncate">{{schema.schema.displayName}}</span> <span class="truncate">{{schema.schema.displayName}}</span>
</td> </td>
<td class="text-center"> <td class="text-center">
<input type="text" class="form-control code" [(ngModel)]="schema.condition" (blur)="updateValue()" /> <input type="text" class="form-control code" [(ngModel)]="schema.condition" (blur)="updateValue()" placeholder="Optional condition as javascript expression" />
</td> </td>
<td class="text-center"> <td class="text-center">
<button type="button" class="btn btn-text-secondary" (click)="removeSchema(schema)"> <button type="button" class="btn btn-text-secondary" (click)="removeSchema(schema)">

56
src/Squidex/app/framework/angular/forms/file-drop.directive.ts

@ -5,18 +5,33 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Directive, ElementRef, EventEmitter, HostListener, Output, Renderer2 } from '@angular/core'; // tslint:disable:prefer-for-of
import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2 } from '@angular/core';
import { Types } from './../../utils/types'; import { Types } from './../../utils/types';
const ImageTypes = [
'image/jpeg',
'image/png',
'image/jpg',
'image/gif'
];
@Directive({ @Directive({
selector: '[sqxFileDrop]' selector: '[sqxFileDrop]'
}) })
export class FileDropDirective { export class FileDropDirective {
private dragCounter = 0; private dragCounter = 0;
@Input()
public allowedFiles: string[];
@Input()
public onlyImages: boolean;
@Output('sqxFileDrop') @Output('sqxFileDrop')
public drop = new EventEmitter<FileList>(); public drop = new EventEmitter<File[]>();
constructor( constructor(
private readonly element: ElementRef, private readonly element: ElementRef,
@ -24,6 +39,25 @@ export class FileDropDirective {
) { ) {
} }
@HostListener('paste', ['$event'])
public onPaste(event: ClipboardEvent) {
const result: File[] = [];
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (this.isAllowedFile(file)) {
result.push(file!);
}
}
if (result.length > 0) {
this.drop.emit(result);
}
this.stopEvent(event);
}
@HostListener('dragend', ['$event']) @HostListener('dragend', ['$event'])
@HostListener('dragleave', ['$event']) @HostListener('dragleave', ['$event'])
public onDragEnd(event: DragDropEvent) { public onDragEnd(event: DragDropEvent) {
@ -57,7 +91,19 @@ export class FileDropDirective {
const hasFiles = this.hasFiles(event.dataTransfer.types); const hasFiles = this.hasFiles(event.dataTransfer.types);
if (hasFiles) { if (hasFiles) {
this.drop.emit(event.dataTransfer.files); const result: File[] = [];
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i);
if (this.isAllowedFile(file)) {
result.push(file!);
}
}
if (result.length > 0) {
this.drop.emit(result);
}
this.dragEnd(0); this.dragEnd(0);
this.stopEvent(event); this.stopEvent(event);
@ -85,6 +131,10 @@ export class FileDropDirective {
} }
} }
private isAllowedFile(file: File | null) {
return file && (!this.allowedFiles || this.allowedFiles.indexOf(file.type) >= 0) && (!this.onlyImages || ImageTypes.indexOf(file.type) >= 0);
}
private hasFiles(types: any): boolean { private hasFiles(types: any): boolean {
if (!types) { if (!types) {
return false; return false;

4
src/Squidex/app/shared/components/assets-list.component.html

@ -1,4 +1,4 @@
<div class="file-drop" (sqxFileDrop)="addFiles($event)" *ngIf="!isDisabled" (paste)="pasteFiles($event)"> <div class="file-drop" (sqxFileDrop)="addFiles($event)" *ngIf="!isDisabled">
<h3 class="file-drop-header">Drop files here to upload</h3> <h3 class="file-drop-header">Drop files here to upload</h3>
<div class="file-drop-or">or</div> <div class="file-drop-or">or</div>
@ -14,7 +14,7 @@
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div> <div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div>
</div> </div>
<div class="row assets" [class.unrow]="isListView" *ngIf="state.tagsNames | async; let tags" (paste)="pasteFiles($event)"> <div class="row assets" [class.unrow]="isListView" *ngIf="state.tagsNames | async; let tags" (paste)="addFiles($event)">
<sqx-asset *ngFor="let file of newFiles" [initFile]="file" <sqx-asset *ngFor="let file of newFiles" [initFile]="file"
[isListView]="isListView" [isListView]="isListView"
(failed)="remove(file)" (failed)="remove(file)"

22
src/Squidex/app/shared/components/assets-list.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
@ -78,23 +76,9 @@ export class AssetsListComponent {
this.newFiles = this.newFiles.remove(file); this.newFiles = this.newFiles.remove(file);
} }
public pasteFiles(event: ClipboardEvent) { public addFiles(files: File[]) {
for (let i = 0; i < event.clipboardData.items.length; i++) { for (let file of files) {
const file = event.clipboardData.items[i].getAsFile(); this.newFiles = this.newFiles.pushFront(file);
if (file) {
this.newFiles = this.newFiles.pushFront(file);
}
}
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file) {
this.newFiles = this.newFiles.pushFront(file);
}
} }
return true; return true;

4
src/Squidex/app/shared/components/assets-selector.component.ts

@ -5,8 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
@ -40,7 +38,7 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
constructor(changeDector: ChangeDetectorRef, constructor(changeDector: ChangeDetectorRef,
public readonly assetsState: AssetsDialogState, public readonly assetsState: AssetsDialogState,
private readonly localStore: LocalStoreService public readonly localStore: LocalStoreService
) { ) {
super(changeDector, { super(changeDector, {
selectedAssets: {}, selectedAssets: {},

6
src/Squidex/app/shared/components/markdown-editor.component.html

@ -1,11 +1,7 @@
<div #container class="drop-container"> <div #container class="drop-container" (sqxFileDrop)="insertFiles($event)" [onlyImages]="true">
<div #inner [class.fullscreen]="snapshot.isFullscreen"> <div #inner [class.fullscreen]="snapshot.isFullscreen">
<textarea class="form-control" #editor></textarea> <textarea class="form-control" #editor></textarea>
</div> </div>
<div class="file-drop drag drop-area">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div> </div>
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">

47
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -9,7 +9,11 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppsState,
AssetDto, AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
StatefulControlComponent, StatefulControlComponent,
@ -50,6 +54,9 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
public assetsDialog = new DialogModel(); public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly renderer: Renderer2, private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
@ -207,4 +214,44 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
this.assetsDialog.hide(); this.assetsDialog.hide();
} }
public insertFiles(files: File[]) {
const doc = this.simplemde.codemirror.getDoc();
for (let file of files) {
this.uploadFile(doc, file);
}
}
private uploadFile(doc: any, file: File) {
const uploadCursor = doc.getCursor();
const uploadText = `![Uploading file...${new Date()}]()`;
doc.replaceSelection(uploadText);
const replaceText = (replacement: string) => {
const cursor = doc.getCursor();
const text = doc.getValue().replace(uploadText, replacement);
doc.setValue(text);
if (uploadCursor && uploadCursor.line === cursor.line) {
const offset = replacement.length - uploadText.length;
doc.setCursor({ line: cursor.line, ch: cursor.ch + offset });
} else {
doc.setCursor(cursor);
}
};
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
replaceText(`![${asset.fileName}](${asset.url} '${asset.fileName}')`);
}
}, () => {
replaceText('FAILED');
});
}
} }

6
src/Squidex/app/shared/components/rich-editor.component.html

@ -1,9 +1,5 @@
<div class="drop-container"> <div class="drop-container" (sqxFileDrop)="insertFiles($event)" [onlyImages]="true">
<div class="editor" #editor></div> <div class="editor" #editor></div>
<div class="file-drop drag drop-area">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div> </div>
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">

84
src/Squidex/app/shared/components/rich-editor.component.ts

@ -5,11 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppsState,
AssetDto, AssetDto,
AssetsService,
AuthService,
DateTime,
DialogModel, DialogModel,
ExternalControlComponent, ExternalControlComponent,
ResourceLoaderService, ResourceLoaderService,
@ -22,6 +28,13 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true
}; };
const ImageTypes = [
'image/jpeg',
'image/png',
'image/jpg',
'image/gif'
];
@Component({ @Component({
selector: 'sqx-rich-editor', selector: 'sqx-rich-editor',
styleUrls: ['./rich-editor.component.scss'], styleUrls: ['./rich-editor.component.scss'],
@ -44,6 +57,9 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
public assetsDialog = new DialogModel(); public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
super(changeDetector); super(changeDetector);
@ -60,7 +76,7 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
public ngAfterViewInit() { public ngAfterViewInit() {
const self = this; const self = this;
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.4/tinymce.min.js').then(() => { this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.3/tinymce.min.js').then(() => {
tinymce.init(self.getEditorOptions()); tinymce.init(self.getEditorOptions());
}); });
} }
@ -75,11 +91,24 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
return { return {
convert_fonts_to_spans: true, convert_fonts_to_spans: true,
convert_urls: false, convert_urls: false,
plugins: 'code image media link lists advlist', plugins: 'code image media link lists advlist paste',
removed_menuitems: 'newdocument', removed_menuitems: 'newdocument',
resize: true, resize: true,
theme: 'modern',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets', toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets',
images_upload_handler: (blob: any, success: (url: string) => void, failed: () => void) => {
const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() });
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
success(asset.url);
}
}, () => {
failed();
});
},
setup: (editor: any) => { setup: (editor: any) => {
self.tinyEditor = editor; self.tinyEditor = editor;
self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design'); self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design');
@ -101,6 +130,28 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
} }
}); });
self.tinyEditor.on('paste', (event: ClipboardEvent) => {
for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile();
if (file && ImageTypes.indexOf(file.type) >= 0) {
self.uploadFile(file);
}
}
});
self.tinyEditor.on('drop', (event: DragEvent) => {
if (event.dataTransfer) {
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i);
if (file && ImageTypes.indexOf(file.type) >= 0) {
self.uploadFile(file);
}
}
}
});
self.tinyEditor.on('blur', () => { self.tinyEditor.on('blur', () => {
self.callTouched(); self.callTouched();
}); });
@ -144,4 +195,31 @@ export class RichEditorComponent extends ExternalControlComponent<string> implem
this.assetsDialog.hide(); this.assetsDialog.hide();
} }
public insertFiles(files: File[]) {
for (let file of files) {
this.uploadFile(file);
}
}
private uploadFile(file: File) {
const uploadText = `[Uploading file...${new Date()}]`;
this.tinyEditor.execCommand('mceInsertContent', false, uploadText);
const replaceText = (replacement: string) => {
const content = this.tinyEditor.getContent().replace(uploadText, replacement);
this.tinyEditor.setContent(content);
};
this.assetsService.uploadFile(this.appsState.appName, file, this.authState.user!.token, DateTime.now())
.subscribe(asset => {
if (Types.is(asset, AssetDto)) {
replaceText(`<img src="${asset.url}" alt="${asset.fileName}" />`);
}
}, () => {
replaceText('FAILED');
});
}
} }

1
src/Squidex/app/shared/state/contents.forms.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:prefer-for-of // tslint:disable:prefer-for-of
import { FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';

10
src/Squidex/appsettings.json

@ -263,6 +263,16 @@
* Enable password auth. Set this to false if you want to disable local login, leaving only 3rd party login options. * Enable password auth. Set this to false if you want to disable local login, leaving only 3rd party login options.
*/ */
"allowPasswordAuth": true, "allowPasswordAuth": true,
/*
* Initial admin user.
*/
"adminEmail": "",
"adminPassword": "",
/*
* Client with all admin permissions.
*/
"adminClientId": "",
"adminClientSecret": "",
/* /*
* Settings for Google auth (keep empty to disable). * Settings for Google auth (keep empty to disable).
*/ */

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
{ {
lhs.Should().BeOfType(rhs.GetType()); lhs.Should().BeOfType(rhs.GetType());
((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties().Excluding((IMemberInfo x) => x.SelectedMemberPath == "Properties.IsFrozen")); ((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties().Excluding(x => x.SelectedMemberPath == "Properties.IsFrozen"));
} }
public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs) public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs)

1
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs

@ -35,7 +35,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
private readonly string actionDescription = "MyDescription"; private readonly string actionDescription = "MyDescription";
private readonly Guid ruleId = Guid.NewGuid(); private readonly Guid ruleId = Guid.NewGuid();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry();
private readonly RuleService sut; private readonly RuleService sut;

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Change = "<change-script>", Change = "<change-script>",
Create = "<create-script>", Create = "<create-script>",
Delete = "<delete-script>", Delete = "<delete-script>",
Update = "<update-script>", Update = "<update-script>"
}; };
var schemaDef = var schemaDef =

Loading…
Cancel
Save