Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/479/head
Sebastian 7 years ago
parent
commit
107c507967
  1. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  2. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  4. 9
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs
  5. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs
  6. 9
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs
  7. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
  8. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  9. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  11. 16
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  12. 9
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  13. 43
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs
  14. 22
      backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs
  15. 213
      backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs
  16. 6
      backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary.cs
  17. 150
      backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs
  18. 1
      backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
  19. 3
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs
  20. 12
      frontend/app-config/webpack.config.js
  21. 2
      frontend/app/app.component.scss
  22. 2
      frontend/app/features/administration/administration-area.component.scss
  23. 3
      frontend/app/features/administration/pages/cluster/cluster-page.component.scss
  24. 24
      frontend/app/features/administration/pages/event-consumers/event-consumer.component.html
  25. 0
      frontend/app/features/administration/pages/event-consumers/event-consumer.component.scss
  26. 28
      frontend/app/features/administration/pages/event-consumers/event-consumer.component.ts
  27. 3
      frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.scss
  28. 3
      frontend/app/features/administration/pages/restore/restore-page.component.scss
  29. 3
      frontend/app/features/administration/pages/users/user-page.component.scss
  30. 20
      frontend/app/features/administration/pages/users/user.component.html
  31. 0
      frontend/app/features/administration/pages/users/user.component.scss
  32. 24
      frontend/app/features/administration/pages/users/user.component.ts
  33. 2
      frontend/app/features/administration/pages/users/users-page.component.scss
  34. 7
      frontend/app/features/administration/state/users.forms.ts
  35. 3
      frontend/app/features/api/api-area.component.scss
  36. 3
      frontend/app/features/api/pages/graphql/graphql-page.component.scss
  37. 3
      frontend/app/features/apps/pages/apps-page.component.scss
  38. 2
      frontend/app/features/apps/pages/news-dialog.component.html
  39. 3
      frontend/app/features/apps/pages/news-dialog.component.scss
  40. 4
      frontend/app/features/apps/pages/news-dialog.component.ts
  41. 2
      frontend/app/features/apps/pages/onboarding-dialog.component.html
  42. 3
      frontend/app/features/apps/pages/onboarding-dialog.component.scss
  43. 4
      frontend/app/features/apps/pages/onboarding-dialog.component.ts
  44. 3
      frontend/app/features/assets/declarations.ts
  45. 6
      frontend/app/features/assets/module.ts
  46. 18
      frontend/app/features/assets/pages/asset-tags.component.html
  47. 0
      frontend/app/features/assets/pages/asset-tags.component.scss
  48. 42
      frontend/app/features/assets/pages/asset-tags.component.ts
  49. 29
      frontend/app/features/assets/pages/assets-filters-page.component.html
  50. 2
      frontend/app/features/assets/pages/assets-filters-page.component.scss
  51. 5
      frontend/app/features/assets/pages/assets-filters-page.component.ts
  52. 3
      frontend/app/features/assets/pages/assets-page.component.scss
  53. 33
      frontend/app/features/content/declarations.ts
  54. 8
      frontend/app/features/content/module.ts
  55. 2
      frontend/app/features/content/pages/comments/comments-page.component.scss
  56. 4
      frontend/app/features/content/pages/content/content-field.component.html
  57. 3
      frontend/app/features/content/pages/content/content-field.component.scss
  58. 6
      frontend/app/features/content/pages/content/content-field.component.ts
  59. 3
      frontend/app/features/content/pages/content/content-history-page.component.scss
  60. 3
      frontend/app/features/content/pages/content/content-page.component.scss
  61. 17
      frontend/app/features/content/pages/content/field-languages.component.html
  62. 0
      frontend/app/features/content/pages/content/field-languages.component.scss
  63. 37
      frontend/app/features/content/pages/content/field-languages.component.ts
  64. 23
      frontend/app/features/content/pages/contents/contents-filters-page.component.html
  65. 3
      frontend/app/features/content/pages/contents/contents-filters-page.component.scss
  66. 9
      frontend/app/features/content/pages/contents/contents-filters-page.component.ts
  67. 4
      frontend/app/features/content/pages/contents/contents-page.component.scss
  68. 8
      frontend/app/features/content/pages/contents/custom-view-editor.component.html
  69. 3
      frontend/app/features/content/pages/contents/custom-view-editor.component.scss
  70. 28
      frontend/app/features/content/pages/contents/custom-view-editor.component.ts
  71. 3
      frontend/app/features/content/pages/schemas/schemas-page.component.scss
  72. 138
      frontend/app/features/content/shared/content-list-field.component.ts
  73. 119
      frontend/app/features/content/shared/content-list-header.component.ts
  74. 69
      frontend/app/features/content/shared/content-selector-item.component.ts
  75. 2
      frontend/app/features/content/shared/content-status.component.scss
  76. 73
      frontend/app/features/content/shared/content-value-editor.component.ts
  77. 42
      frontend/app/features/content/shared/content-value.component.ts
  78. 2
      frontend/app/features/content/shared/due-time-selector.component.scss
  79. 13
      frontend/app/features/content/shared/due-time-selector.component.ts
  80. 0
      frontend/app/features/content/shared/forms/array-editor.component.html
  81. 3
      frontend/app/features/content/shared/forms/array-editor.component.scss
  82. 0
      frontend/app/features/content/shared/forms/array-editor.component.ts
  83. 12
      frontend/app/features/content/shared/forms/array-item.component.html
  84. 3
      frontend/app/features/content/shared/forms/array-item.component.scss
  85. 16
      frontend/app/features/content/shared/forms/array-item.component.ts
  86. 0
      frontend/app/features/content/shared/forms/assets-editor.component.html
  87. 3
      frontend/app/features/content/shared/forms/assets-editor.component.scss
  88. 9
      frontend/app/features/content/shared/forms/assets-editor.component.ts
  89. 0
      frontend/app/features/content/shared/forms/field-editor.component.html
  90. 3
      frontend/app/features/content/shared/forms/field-editor.component.scss
  91. 0
      frontend/app/features/content/shared/forms/field-editor.component.ts
  92. 0
      frontend/app/features/content/shared/forms/stock-photo-editor.component.html
  93. 3
      frontend/app/features/content/shared/forms/stock-photo-editor.component.scss
  94. 4
      frontend/app/features/content/shared/forms/stock-photo-editor.component.ts
  95. 0
      frontend/app/features/content/shared/list/content-list-cell.directive.ts
  96. 62
      frontend/app/features/content/shared/list/content-list-field.component.html
  97. 0
      frontend/app/features/content/shared/list/content-list-field.component.scss
  98. 76
      frontend/app/features/content/shared/list/content-list-field.component.ts
  99. 56
      frontend/app/features/content/shared/list/content-list-header.component.html
  100. 0
      frontend/app/features/content/shared/list/content-list-header.component.scss

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppClients : ArrayDictionary<string, AppClient>
public sealed class AppClients : ImmutableDictionary<string, AppClient>
{
public static readonly AppClients Empty = new AppClients();
@ -21,8 +21,8 @@ namespace Squidex.Domain.Apps.Core.Apps
{
}
public AppClients(KeyValuePair<string, AppClient>[] items)
: base(items)
public AppClients(Dictionary<string, AppClient> inner)
: base(inner)
{
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppContributors : ArrayDictionary<string, string>
public sealed class AppContributors : ImmutableDictionary<string, string>
{
public static readonly AppContributors Empty = new AppContributors();
@ -20,8 +20,8 @@ namespace Squidex.Domain.Apps.Core.Apps
{
}
public AppContributors(KeyValuePair<string, string>[] items)
: base(items)
public AppContributors(Dictionary<string, string> inner)
: base(inner)
{
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPatterns : ArrayDictionary<Guid, AppPattern>
public sealed class AppPatterns : ImmutableDictionary<Guid, AppPattern>
{
public static readonly AppPatterns Empty = new AppPatterns();
@ -21,8 +21,8 @@ namespace Squidex.Domain.Apps.Core.Apps
{
}
public AppPatterns(KeyValuePair<Guid, AppPattern>[] items)
: base(items)
public AppPatterns(Dictionary<Guid, AppPattern> inner)
: base(inner)
{
}

9
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs

@ -29,14 +29,9 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
protected override AppClients ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, JsonAppClient>>(reader);
var json = serializer.Deserialize<Dictionary<string, JsonAppClient>>(reader)!;
return new AppClients(json.Select(Convert).ToArray());
}
private static KeyValuePair<string, AppClient> Convert(KeyValuePair<string, JsonAppClient> kvp)
{
return new KeyValuePair<string, AppClient>(kvp.Key, kvp.Value.ToClient());
return new AppClients(json.ToDictionary(x => x.Key, x => x.Value.ToClient()));
}
}
}

5
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
@ -29,9 +28,9 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
protected override AppContributors ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, string>>(reader);
var json = serializer.Deserialize<Dictionary<string, string>>(reader)!;
return new AppContributors(json.ToArray());
return new AppContributors(json!);
}
}
}

9
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs

@ -29,14 +29,9 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
protected override AppPatterns ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<Guid, JsonAppPattern>>(reader);
var json = serializer.Deserialize<Dictionary<Guid, JsonAppPattern>>(reader)!;
return new AppPatterns(json.Select(Convert).ToArray());
}
private static KeyValuePair<Guid, AppPattern> Convert(KeyValuePair<Guid, JsonAppPattern> kvp)
{
return new KeyValuePair<Guid, AppPattern>(kvp.Key, kvp.Value.ToPattern());
return new AppPatterns(json.ToDictionary(x => x.Key, x => x.Value.ToPattern()));
}
}
}

7
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs

@ -37,12 +37,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
return Roles.Empty;
}
return new Roles(json.Select(Convert));
}
private static KeyValuePair<string, Role> Convert(KeyValuePair<string, string[]> kvp)
{
return new KeyValuePair<string, Role>(kvp.Key, new Role(kvp.Key, new PermissionSet(kvp.Value)));
return new Roles(json.ToDictionary(x => x.Key, x => new Role(x.Key, new PermissionSet(x.Value))));
}
}
}

16
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class Roles
{
private readonly ArrayDictionary<string, Role> inner;
private readonly ImmutableDictionary<string, Role> inner;
public static readonly IReadOnlyDictionary<string, Role> Defaults = new Dictionary<string, Role>
{
@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Apps
Clean(Permissions.AppWorkflows)))
};
public static readonly Roles Empty = new Roles(new ArrayDictionary<string, Role>());
public static readonly Roles Empty = new Roles(new ImmutableDictionary<string, Role>());
public int CustomCount
{
@ -70,14 +70,14 @@ namespace Squidex.Domain.Apps.Core.Apps
get { return inner.Values.Union(Defaults.Values); }
}
private Roles(ArrayDictionary<string, Role> roles)
private Roles(ImmutableDictionary<string, Role> roles)
{
inner = roles;
}
public Roles(IEnumerable<KeyValuePair<string, Role>> items)
public Roles(Dictionary<string, Role> roles)
{
inner = new ArrayDictionary<string, Role>(Cleaned(items));
inner = new ImmutableDictionary<string, Role>(Cleaned(roles));
}
[Pure]
@ -172,12 +172,12 @@ namespace Squidex.Domain.Apps.Core.Apps
return permission.Substring(1);
}
private static KeyValuePair<string, Role>[] Cleaned(IEnumerable<KeyValuePair<string, Role>> items)
private static Dictionary<string, Role> Cleaned(Dictionary<string, Role> inner)
{
return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray();
return inner.Where(x => !Defaults.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value);
}
private Roles Create(ArrayDictionary<string, Role> newRoles)
private Roles Create(ImmutableDictionary<string, Role> newRoles)
{
return ReferenceEquals(inner, newRoles) ? this : new Roles(newRoles);
}

3
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
@ -31,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
{
var json = serializer.Deserialize<Dictionary<Guid, Workflow>>(reader);
return new Workflows(json.ToArray());
return new Workflows(json!);
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class Workflows : ArrayDictionary<Guid, Workflow>
public sealed class Workflows : ImmutableDictionary<Guid, Workflow>
{
public static readonly Workflows Empty = new Workflows();
@ -22,8 +22,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
public Workflows(KeyValuePair<Guid, Workflow>[] items)
: base(items)
public Workflows(Dictionary<Guid, Workflow> inner)
: base(inner)
{
}

16
backend/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs

@ -58,14 +58,20 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
var file = uploadImage.File;
var image = await assetThumbnailGenerator.GetImageInfoAsync(file.OpenRead());
if (image == null)
using (var uploadStream = file.OpenRead())
{
throw new ValidationException("File is not an image.");
var image = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream);
if (image == null)
{
throw new ValidationException("File is not an image.");
}
}
await appImageStore.UploadAsync(uploadImage.AppId, file.OpenRead());
using (var uploadStream = file.OpenRead())
{
await appImageStore.UploadAsync(uploadImage.AppId, uploadStream);
}
}
}
}

9
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -149,11 +149,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile)
{
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
using (var uploadStream = command.File.OpenRead())
{
await assetFileStore.UploadAsync(tempFile, hashStream);
using (var hashStream = new HasherStream(uploadStream, HashAlgorithmName.SHA256))
{
await assetFileStore.UploadAsync(tempFile, hashStream);
command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64();
command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64();
}
}
}

43
backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs

@ -29,32 +29,35 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
if (command.Type == AssetType.Unknown)
{
var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
if (imageInfo != null)
using (var uploadStream = command.File.OpenRead())
{
command.Type = AssetType.Image;
command.Metadata.SetPixelWidth(imageInfo.PixelWidth);
command.Metadata.SetPixelHeight(imageInfo.PixelHeight);
var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream);
if (tags != null)
if (imageInfo != null)
{
tags.Add("image");
command.Type = AssetType.Image;
var wh = imageInfo.PixelWidth + imageInfo.PixelHeight;
command.Metadata.SetPixelWidth(imageInfo.PixelWidth);
command.Metadata.SetPixelHeight(imageInfo.PixelHeight);
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
if (tags != null)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
tags.Add("image");
var wh = imageInfo.PixelWidth + imageInfo.PixelHeight;
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
}
}
}
}

22
backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs

@ -77,19 +77,23 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
public Task<ImageInfo?> GetImageInfoAsync(Stream source)
{
return Task.Run(() =>
ImageInfo? result = null;
try
{
try
{
var image = Image.Load(source);
var image = Image.Identify(source);
return new ImageInfo(image.Width, image.Height);
}
catch
if (image != null)
{
return null;
result = new ImageInfo(image.Width, image.Height);
}
});
}
catch
{
result = null;
}
return Task.FromResult(result);
}
}
}

213
backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs

@ -1,213 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
#pragma warning disable IDE0044 // Add readonly modifier
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Infrastructure.Collections
{
public class ArrayDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> where TKey : notnull
{
private static readonly KeyValuePair<TKey, TValue>[] EmptyItems = Array.Empty<KeyValuePair<TKey, TValue>>();
private readonly IEqualityComparer<TKey> keyComparer;
private KeyValuePair<TKey, TValue>[] items;
public TValue this[TKey key]
{
get
{
if (!TryGetValue(key, out var value))
{
throw new KeyNotFoundException();
}
return value;
}
}
public IEnumerable<TKey> Keys
{
get { return items.Select(x => x.Key); }
}
public IEnumerable<TValue> Values
{
get { return items.Select(x => x.Value); }
}
public int Count
{
get { return items.Length; }
}
public ArrayDictionary()
: this(EqualityComparer<TKey>.Default, EmptyItems)
{
}
public ArrayDictionary(KeyValuePair<TKey, TValue>[] items)
: this(EqualityComparer<TKey>.Default, items)
{
}
public ArrayDictionary(IEqualityComparer<TKey> keyComparer, KeyValuePair<TKey, TValue>[] items)
{
Guard.NotNull(items);
Guard.NotNull(keyComparer);
this.items = items;
this.keyComparer = keyComparer;
}
public bool IsUnchanged(KeyValuePair<TKey, TValue>[] values)
{
return ReferenceEquals(values, items);
}
public ArrayDictionary<TKey, TValue> With(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null)
{
return With<ArrayDictionary<TKey, TValue>>(key, value, valueComparer);
}
public TArray With<TArray>(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null) where TArray : ArrayDictionary<TKey, TValue>
{
var index = IndexOf(key);
if (index < 0)
{
var result = new KeyValuePair<TKey, TValue>[items.Length + 1];
Array.Copy(items, 0, result, 0, items.Length);
result[^1] = new KeyValuePair<TKey, TValue>(key, value);
return Create<TArray>(result);
}
var existing = items[index].Value;
if (valueComparer == null || !valueComparer.Equals(value, existing))
{
var result = new KeyValuePair<TKey, TValue>[items.Length];
Array.Copy(items, 0, result, 0, items.Length);
result[index] = new KeyValuePair<TKey, TValue>(key, value);
return Create<TArray>(result);
}
return Self<TArray>();
}
public ArrayDictionary<TKey, TValue> Without(TKey key)
{
return Without<ArrayDictionary<TKey, TValue>>(key);
}
public TArray Without<TArray>(TKey key) where TArray : ArrayDictionary<TKey, TValue>
{
var index = IndexOf(key);
if (index < 0)
{
return Self<TArray>();
}
if (Count == 1)
{
return Create<TArray>(EmptyItems);
}
var result = new KeyValuePair<TKey, TValue>[items.Length - 1];
Array.Copy(items, 0, result, 0, index);
Array.Copy(items, index + 1, result, index, items.Length - index - 1);
return Create<TArray>(result);
}
private TArray Self<TArray>() where TArray : ArrayDictionary<TKey, TValue>
{
return (this as TArray)!;
}
private TArray Create<TArray>(KeyValuePair<TKey, TValue>[] newItems) where TArray : ArrayDictionary<TKey, TValue>
{
if (ReferenceEquals(items, newItems))
{
return Self<TArray>();
}
var newClone = (TArray)MemberwiseClone();
newClone.items = newItems;
return newClone;
}
public bool ContainsKey(TKey key)
{
var index = IndexOf(key);
return index >= 0;
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
var index = IndexOf(key);
if (index >= 0)
{
value = items[index].Value;
return true;
}
else
{
value = default!;
return false;
}
}
private int IndexOf(TKey key)
{
for (var i = 0; i < items.Length; i++)
{
if (keyComparer.Equals(items[i].Key, key))
{
return i;
}
}
return -1;
}
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
{
return GetEnumerable(items).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return items.GetEnumerator();
}
private static IEnumerable<T2> GetEnumerable<T2>(IEnumerable<T2> array)
{
return array;
}
}
}

6
backend/src/Squidex.Infrastructure/Collections/ArrayDictionary.cs → backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary.cs

@ -11,11 +11,11 @@ using System.Linq;
namespace Squidex.Infrastructure.Collections
{
public static class ArrayDictionary
public static class ImmutableDictionary
{
public static ArrayDictionary<TKey, TValue> ToArrayDictionary<TKey, TValue>(this IEnumerable<TValue> source, Func<TValue, TKey> keyExtractor) where TKey : notnull
public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TKey, TValue>(this IEnumerable<TValue> source, Func<TValue, TKey> keyExtractor) where TKey : notnull
{
return new ArrayDictionary<TKey, TValue>(source.Select(x => new KeyValuePair<TKey, TValue>(keyExtractor(x), x)).ToArray());
return new ImmutableDictionary<TKey, TValue>(source.ToDictionary(keyExtractor));
}
}
}

150
backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs

@ -0,0 +1,150 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
#pragma warning disable IDE0044 // Add readonly modifier
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Infrastructure.Collections
{
public class ImmutableDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> where TKey : notnull
{
private static readonly Dictionary<TKey, TValue> EmptyInner = new Dictionary<TKey, TValue>();
private Dictionary<TKey, TValue> inner;
public TValue this[TKey key]
{
get
{
if (!TryGetValue(key, out var value))
{
throw new KeyNotFoundException();
}
return value;
}
}
public IEnumerable<TKey> Keys
{
get { return inner.Keys; }
}
public IEnumerable<TValue> Values
{
get { return inner.Values; }
}
public int Count
{
get { return inner.Count; }
}
public ImmutableDictionary()
: this(EmptyInner)
{
}
public ImmutableDictionary(Dictionary<TKey, TValue> inner)
{
Guard.NotNull(inner);
this.inner = inner;
}
public ImmutableDictionary<TKey, TValue> With(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null)
{
return With<ImmutableDictionary<TKey, TValue>>(key, value, valueComparer);
}
public TArray With<TArray>(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null) where TArray : ImmutableDictionary<TKey, TValue>
{
if (!TryGetValue(key, out var found) || !IsEqual(value, found, valueComparer))
{
var clone = new Dictionary<TKey, TValue>(inner)
{
[key] = value
};
return Create<TArray>(clone);
}
return Self<TArray>();
}
private static bool IsEqual(TValue lhs, TValue rhs, IEqualityComparer<TValue>? comparer = null)
{
return comparer == null || comparer.Equals(lhs, rhs);
}
public ImmutableDictionary<TKey, TValue> Without(TKey key)
{
return Without<ImmutableDictionary<TKey, TValue>>(key);
}
public TArray Without<TArray>(TKey key) where TArray : ImmutableDictionary<TKey, TValue>
{
if (!inner.ContainsKey(key))
{
return Self<TArray>();
}
if (Count == 1)
{
return Create<TArray>(EmptyInner);
}
var clone = new Dictionary<TKey, TValue>(inner);
clone.Remove(key);
return Create<TArray>(clone);
}
private TArray Self<TArray>() where TArray : ImmutableDictionary<TKey, TValue>
{
return (this as TArray)!;
}
private TArray Create<TArray>(Dictionary<TKey, TValue> clone) where TArray : ImmutableDictionary<TKey, TValue>
{
var newClone = (TArray)MemberwiseClone();
newClone.inner = clone;
return newClone;
}
public bool ContainsKey(TKey key)
{
return inner.ContainsKey(key);
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
return inner.TryGetValue(key, out value);
}
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
{
return GetEnumerable(inner).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return inner.GetEnumerator();
}
private static IEnumerable<TItem> GetEnumerable<TItem>(IEnumerable<TItem> collection)
{
return collection;
}
}
}

1
backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs

@ -16,6 +16,7 @@ namespace Squidex.Areas.Frontend.Middlewares
public sealed class WebpackMiddleware
{
private const string WebpackUrl = "http://localhost:3000/index.html";
private readonly RequestDelegate next;
public WebpackMiddleware(RequestDelegate next)

3
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
@ -30,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact]
public void Should_create_roles_without_defaults()
{
var roles = new Roles(Roles.Defaults.ToArray());
var roles = new Roles(new Dictionary<string, Role>(Roles.Defaults));
Assert.Equal(0, roles.CustomCount);
}

12
frontend/app-config/webpack.config.js

@ -37,6 +37,8 @@ module.exports = function (env) {
const isCoverage = env && env.coverage;
const isAot = isProduction;
const configFile = isTests ? 'tsconfig.spec.json' : 'tsconfig.app.json';
const config = {
mode: isProduction ? 'production' : 'development',
@ -66,7 +68,9 @@ module.exports = function (env) {
],
plugins: [
new plugins.TsconfigPathsPlugin()
new plugins.TsconfigPathsPlugin({
configFile
})
]
},
@ -145,6 +149,10 @@ module.exports = function (env) {
}, {
loader: 'sass-loader',
options: {
prependData: `
@import '_vars';
@import '_mixins';
`,
sassOptions: {
includePaths: [root('app', 'theme')]
}
@ -285,7 +293,7 @@ module.exports = function (env) {
entryModule: 'app/app.module#AppModule',
sourceMap: !isProduction,
skipCodeGeneration: !isAot,
tsConfigPath: './tsconfig.json'
tsConfigPath: configFile
})
);
}

2
frontend/app/app.component.scss

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

2
frontend/app/features/administration/administration-area.component.scss

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

3
frontend/app/features/administration/pages/cluster/cluster-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
iframe {
@include absolute(0, 0, 0, 0);
@include force-height(100%);

24
frontend/app/features/administration/pages/event-consumers/event-consumer.component.html

@ -0,0 +1,24 @@
<tr [class.faulted]="eventConsumer.error && eventConsumer.error?.length > 0">
<td class="cell-auto">
<span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="error.emit()" [class.hidden]="!eventConsumer.error || eventConsumer.error?.length === 0"></i>
{{eventConsumer.name}}
</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg">
<button type="button" class="btn btn-text" (click)="reset()" *ngIf="eventConsumer.canReset" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button type="button" class="btn btn-text" (click)="start()" *ngIf="eventConsumer.canStart" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button type="button" class="btn btn-text" (click)="stop()" *ngIf="eventConsumer.canStop" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>

0
frontend/app/features/administration/pages/event-consumers/event-consumer.component.scss

28
frontend/app/features/administration/pages/event-consumers/event-consumer.component.ts

@ -13,32 +13,8 @@ import { EventConsumerDto, EventConsumersState } from '@app/features/administrat
@Component({
selector: '[sqxEventConsumer]',
template: `
<tr [class.faulted]="eventConsumer.error && eventConsumer.error?.length > 0">
<td class="cell-auto">
<span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="error.emit()" [class.hidden]="!eventConsumer.error || eventConsumer.error?.length === 0"></i>
{{eventConsumer.name}}
</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg">
<button type="button" class="btn btn-text" (click)="reset()" *ngIf="eventConsumer.canReset" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button type="button" class="btn btn-text" (click)="start()" *ngIf="eventConsumer.canStart" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button type="button" class="btn btn-text" (click)="stop()" *ngIf="eventConsumer.canStop" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
`,
styleUrls: ['./event-consumer.component.scss'],
templateUrl: './event-consumer.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EventConsumerComponent {

3
frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.faulted {
& {
color: $color-theme-error;

3
frontend/app/features/administration/pages/restore/restore-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
$circle-size: 2rem;
h3 {

3
frontend/app/features/administration/pages/users/user-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.form-group-section {
margin-top: 2rem;
}

20
frontend/app/features/administration/pages/users/user.component.html

@ -0,0 +1,20 @@
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" title="{{user.displayName}}" [src]="user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-text" (click)="lock()" sqxStopClick *ngIf="user.canLock" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button type="button" class="btn btn-text" (click)="unlock()" sqxStopClick *ngIf="user.canUnlock" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>

0
frontend/app/features/administration/pages/users/user.component.scss

24
frontend/app/features/administration/pages/users/user.component.ts

@ -13,28 +13,8 @@ import { UserDto, UsersState } from '@app/features/administration/internal';
@Component({
selector: '[sqxUser]',
template: `
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" title="{{user.displayName}}" [src]="user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-text" (click)="lock()" sqxStopClick *ngIf="user.canLock" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button type="button" class="btn btn-text" (click)="unlock()" sqxStopClick *ngIf="user.canUnlock" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
`,
styleUrls: ['./user.component.scss'],
templateUrl: './user.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {

2
frontend/app/features/administration/pages/users/users-page.component.scss

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

7
frontend/app/features/administration/state/users.forms.ts

@ -1,3 +1,10 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, ValidatorsEx } from '@app/shared';

3
frontend/app/features/api/api-area.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.nav-link {
padding-bottom: .6rem;
padding-top: .6rem;

3
frontend/app/features/api/pages/graphql/graphql-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
// Graphiql hints
::ng-deep {
@import '~graphiql/dist/show-hint';

3
frontend/app/features/apps/pages/apps-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.apps {
&-title {
font-size: 1.4rem;

2
frontend/app/features/apps/pages/news-dialog.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog large="true" (close)="emitClose()">
<sqx-modal-dialog size="lg" (close)="close.emit()">
<ng-container title>
New Features
</ng-container>

3
frontend/app/features/apps/pages/news-dialog.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
:host ::ng-deep {
img {
@include box-shadow(0, 4px, 20px, .2);

4
frontend/app/features/apps/pages/news-dialog.component.ts

@ -21,10 +21,6 @@ export class NewsDialogComponent {
@Input()
public features: ReadonlyArray<FeatureDto>;
public emitClose() {
this.close.emit();
}
public trackByFeature(index: number, feature: FeatureDto) {
return feature;
}

2
frontend/app/features/apps/pages/onboarding-dialog.component.html

@ -1,6 +1,6 @@
<sqx-modal-dialog [showHeader]="false">
<ng-container content>
<a class="header-right modal-close" (click)="emitClose()">Skip Tour</a>
<a class="header-right modal-close" (click)="close.emit()">Skip Tour</a>
<div class="onboarding-step" *ngIf="step === 0">
<img @fade class="header-left" src="./images/logo-white-small.png" />

3
frontend/app/features/apps/pages/onboarding-dialog.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
$size-width: 825px;
$size-height: 576px;

4
frontend/app/features/apps/pages/onboarding-dialog.component.ts

@ -23,10 +23,6 @@ export class OnboardingDialogComponent {
@Output()
public close = new EventEmitter();
public emitClose() {
this.close.emit();
}
public next() {
this.step = this.step + 1;
}

3
frontend/app/features/assets/declarations.ts

@ -6,4 +6,5 @@
*/
export * from './pages/assets-filters-page.component';
export * from './pages/assets-page.component';
export * from './pages/assets-page.component';
export * from './pages/asset-tags.component';

6
frontend/app/features/assets/module.ts

@ -12,7 +12,8 @@ import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import {
AssetsFiltersPageComponent,
AssetsPageComponent
AssetsPageComponent,
AssetTagsComponent
} from './declarations';
const routes: Routes = [
@ -36,7 +37,8 @@ const routes: Routes = [
],
declarations: [
AssetsFiltersPageComponent,
AssetsPageComponent
AssetsPageComponent,
AssetTagsComponent
]
})
export class SqxFeatureAssetsModule {}

18
frontend/app/features/assets/pages/asset-tags.component.html

@ -0,0 +1,18 @@
<a class="sidebar-item" (click)="reset.emit()" [class.active]="isEmpty()">
<div class="row">
<div class="col">
All tags
</div>
</div>
</a>
<a class="sidebar-item" *ngFor="let tag of tags; trackBy: trackByTag" (click)="toggle.emit(tag.name)" [class.active]="isSelected(tag)">
<div class="row">
<div class="col">
{{tag.name}}
</div>
<div class="col-auto">
{{tag.count}}
</div>
</div>
</a>

0
frontend/app/features/assets/pages/asset-tags.component.scss

42
frontend/app/features/assets/pages/asset-tags.component.ts

@ -0,0 +1,42 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Tag, TagsSelected } from '@app/shared';
@Component({
selector: 'sqx-asset-tags',
styleUrls: ['./asset-tags.component.scss'],
templateUrl: './asset-tags.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetTagsComponent {
@Output()
public reset = new EventEmitter();
@Output()
public toggle = new EventEmitter<string>();
@Input()
public tags: ReadonlyArray<Tag>;
@Input()
public tagsSelected: TagsSelected;
public isEmpty() {
return Object.keys(this.tagsSelected).length === 0;
}
public isSelected(tag: Tag) {
return this.tagsSelected[tag.name] === true;
}
public trackByTag(index: number, tag: Tag) {
return tag.name;
}
}

29
frontend/app/features/assets/pages/assets-filters-page.component.html

@ -6,32 +6,19 @@
<ng-container content>
<h3>Tags</h3>
<a class="sidebar-item" (click)="resetTags()" [class.active]="assetsState.isTagSelectionEmpty()">
<div class="row">
<div class="col">
All tags
</div>
</div>
</a>
<a class="sidebar-item" *ngFor="let tag of assetsState.tags | async; trackBy: trackByTag" (click)="toggleTag(tag.name)"
[class.active]="assetsState.isTagSelected(tag.name)">
<div class="row">
<div class="col">
{{tag.name}}
</div>
<div class="col-auto">
{{tag.count}}
</div>
</div>
</a>
<sqx-asset-tags
(reset)="resetTags()"
[tags]="assetsState.tags | async"
[tagsSelected]="assetsState.tagsSelected | async"
(toggle)="toggleTag($event)">
</sqx-asset-tags>
<hr />
<sqx-shared-queries types="contents"
[queryUsed]="assetsState.assetsQuery | async"
[queries]="assetsQueries"
[queryUsed]="isQueryUsed"
(search)="search($event.query)">
(search)="search($event)">
</sqx-shared-queries>
</ng-container>
</sqx-panel>

2
frontend/app/features/assets/pages/assets-filters-page.component.scss

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

5
frontend/app/features/assets/pages/assets-filters-page.component.ts

@ -11,7 +11,6 @@ import {
AssetsState,
Queries,
Query,
SavedQuery,
UIState
} from '@app/shared';
@ -29,10 +28,6 @@ export class AssetsFiltersPageComponent {
) {
}
public isQueryUsed = (query: SavedQuery) => {
return this.assetsState.isQueryUsed(query);
}
public search(query: Query) {
this.assetsState.search(query);
}

3
frontend/app/features/assets/pages/assets-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.search ::ng-deep {
.form-control {
@include border-radius-right;

33
frontend/app/features/content/declarations.ts

@ -15,21 +15,24 @@ export * from './pages/contents/contents-page.component';
export * from './pages/contents/custom-view-editor.component';
export * from './pages/schemas/schemas-page.component';
export * from './shared/array-editor.component';
export * from './shared/array-item.component';
export * from './shared/assets-editor.component';
export * from './shared/content-list-cell.directive';
export * from './shared/content-list-field.component';
export * from './shared/content-list-header.component';
export * from './shared/content-selector-item.component';
export * from './shared/content-status.component';
export * from './shared/content-value-editor.component';
export * from './shared/content-value.component';
export * from './shared/content.component';
export * from './shared/contents-selector.component';
export * from './shared/due-time-selector.component';
export * from './shared/field-editor.component';
export * from './shared/preview-button.component';
export * from './shared/reference-item.component';
export * from './shared/references-editor.component';
export * from './shared/stock-photo-editor.component';
export * from './shared/forms/array-editor.component';
export * from './shared/forms/array-item.component';
export * from './shared/forms/assets-editor.component';
export * from './shared/forms/field-editor.component';
export * from './shared/forms/stock-photo-editor.component';
export * from './shared/list/content-list-cell.directive';
export * from './shared/list/content-list-field.component';
export * from './shared/list/content-list-header.component';
export * from './shared/list/content-value-editor.component';
export * from './shared/list/content-value.component';
export * from './shared/list/content.component';
export * from './shared/references/content-selector-item.component';
export * from './shared/references/content-selector.component';
export * from './shared/references/reference-item.component';
export * from './shared/references/references-editor.component';

8
frontend/app/features/content/module.ts

@ -32,10 +32,10 @@ import {
ContentListHeaderComponent,
ContentListWidthPipe,
ContentPageComponent,
ContentSelectorComponent,
ContentSelectorItemComponent,
ContentsFiltersPageComponent,
ContentsPageComponent,
ContentsSelectorComponent,
ContentStatusComponent,
ContentValueComponent,
ContentValueEditorComponent,
@ -118,16 +118,16 @@ const routes: Routes = [
CommentsPageComponent,
ContentComponent,
ContentFieldComponent,
ContentHistoryPageComponent,
ContentListCellDirective,
ContentListWidthPipe,
ContentListFieldComponent,
ContentListHeaderComponent,
ContentHistoryPageComponent,
ContentListWidthPipe,
ContentPageComponent,
ContentSelectorComponent,
ContentSelectorItemComponent,
ContentsFiltersPageComponent,
ContentsPageComponent,
ContentsSelectorComponent,
ContentStatusComponent,
ContentValueComponent,
ContentValueEditorComponent,

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

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

4
frontend/app/features/content/pages/content/content-field.component.html

@ -9,7 +9,7 @@
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="emitLanguageChange($event)"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
@ -67,7 +67,7 @@
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="emitLanguageChange($event)"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">

3
frontend/app/features/content/pages/content/content-field.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.table-items-row {
border-left-width: 4px;
position: relative;

6
frontend/app/features/content/pages/content/content-field.component.ts

@ -90,7 +90,7 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
this.isDifferent =
value$(this.fieldForm).pipe(
combineLatest(value$(this.fieldFormCompare),
(lhs, rhs) => !Types.jsJsonEquals(lhs, rhs)));
(lhs, rhs) => !Types.equals(lhs, rhs)));
}
}
@ -184,10 +184,6 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
}
}
public emitLanguageChange(language: AppLanguageDto) {
this.languageChange.emit(language);
}
public prefix(language: AppLanguageDto) {
return `(${language.iso2Code})`;
}

3
frontend/app/features/content/pages/content/content-history-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
:host ::ng-deep {
.user-ref {
font-weight: 500;

3
frontend/app/features/content/pages/content/content-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.btn-status {
margin-left: 2rem;
}

17
frontend/app/features/content/pages/content/field-languages.component.html

@ -0,0 +1,17 @@
<ng-container *ngIf="field.isLocalizable && languages.length > 1">
<button *ngIf="!field.properties.isComplexUI" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="toggleShowAllControls()">
{{showAllControls ? 'Single Language' : 'All Languages'}}
</button>
<ng-container *ngIf="field.properties.isComplexUI || !showAllControls">
<sqx-language-selector size="sm" #buttonLanguages
[selectedLanguage]="language"
(selectedLanguageChange)="languageChange.emit($event)"
[languages]="languages">
</sqx-language-selector>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</ng-container>
</ng-container>

0
frontend/app/features/content/pages/content/field-languages.component.scss

37
frontend/app/features/content/pages/content/field-languages.component.ts

@ -11,43 +11,30 @@ import { AppLanguageDto, RootFieldDto } from '@app/shared';
@Component({
selector: 'sqx-field-languages',
template: `
<ng-container *ngIf="field.isLocalizable && languages.length > 1">
<button *ngIf="!field.properties.isComplexUI" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="showAllControlsChange.emit(!showAllControls)">
{{showAllControls ? 'Single Language' : 'All Languages'}}
</button>
<ng-container *ngIf="field.properties.isComplexUI || !showAllControls">
<sqx-language-selector size="sm" #buttonLanguages
[selectedLanguage]="language"
(selectedLanguageChange)="languageChange.emit($event)"
[languages]="languages">
</sqx-language-selector>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</ng-container>
</ng-container>
`,
styleUrls: ['./field-languages.component.scss'],
templateUrl: './field-languages.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FieldLanguagesComponent {
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Output()
public showAllControlsChange = new EventEmitter<boolean>();
@Input()
public field: RootFieldDto;
@Input()
public showAllControls: boolean;
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Input()
public language: AppLanguageDto;
@Input()
public languages: ReadonlyArray<AppLanguageDto>;
@Input()
public field: RootFieldDto;
public toggleShowAllControls() {
this.showAllControlsChange.emit(this.showAllControls);
}
}

23
frontend/app/features/content/pages/contents/contents-filters-page.component.html

@ -4,29 +4,30 @@
</ng-container>
<ng-container content>
<a class="sidebar-item" *ngFor="let default of schemaQueries.defaultQueries; trackBy: trackByQuery" (click)="search(default.query)"
[class.active]="isQueryUsed(default)">
{{default.name}}
</a>
<sqx-query-list
[queryUsed]="contentsState.contentsQuery | async"
[queries]="schemaQueries.defaultQueries"
(search)="search($event)">
</sqx-query-list>
<hr />
<div class="sidebar-section">
<h3>Status Queries</h3>
<a class="sidebar-item status" *ngFor="let status of contentsState.statusQueries | async; trackBy: trackByQuery" (click)="search(status.query)"
[class.active]="isQueryUsed(status)">
<i class="icon-circle" [style.color]="status.color"></i> {{status.name}}
</a>
<sqx-query-list
[queryUsed]="contentsState.contentsQuery | async"
[queries]="contentsState.statusQueries | async"
(search)="search($event)">
</sqx-query-list>
</div>
<hr />
<sqx-shared-queries types="contents"
[queryUsed]="contentsState.contentsQuery | async"
[queries]="schemaQueries"
[queryUsed]="isQueryUsed"
(search)="search($event.query)">
(search)="search($event)">
</sqx-shared-queries>
</ng-container>
</sqx-panel>

3
frontend/app/features/content/pages/contents/contents-filters-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.status {
@include truncate;
}

9
frontend/app/features/content/pages/contents/contents-filters-page.component.ts

@ -12,7 +12,6 @@ import {
Queries,
Query,
ResourceOwner,
SavedQuery,
SchemasState,
UIState
} from '@app/shared';
@ -41,15 +40,7 @@ export class ContentsFiltersPageComponent extends ResourceOwner implements OnIni
}));
}
public isQueryUsed = (query: SavedQuery) => {
return this.contentsState.isQueryUsed(query);
}
public search(query: Query) {
this.contentsState.search(query);
}
public trackByQuery(index: number, query: { name: string }) {
return query.name;
}
}

4
frontend/app/features/content/pages/contents/contents-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.content {
cursor: pointer;
}
@ -27,6 +24,7 @@
&-button {
@include absolute(null, 1rem, -2.375rem);
z-index: 1000;
}
}

8
frontend/app/features/content/pages/contents/custom-view-editor.component.html

@ -7,16 +7,16 @@
<hr />
<div
<div
cdkDropList
[cdkDropListData]="fieldNames"
(cdkDropListDropped)="drop($event)">
<div *ngFor="let field of fieldNames; trackBy: random" cdkDrag>
<div *ngFor="let field of fieldNames" cdkDrag>
<i class="icon-drag2 drag-handle"></i>
<div class="form-check">
<input class="form-check-input" type="checkbox" checked (change)="removeField(field)" id="field_{{field}}">
<input class="form-check-input" type="checkbox" checked (click)="removeField(field)" id="field_{{field}}">
<label class="form-check-label" for="field_{{field}}">
{{field}}
</label>
@ -31,7 +31,7 @@
<i class="icon-drag2 drag-handle invisible"></i>
<div class="form-check">
<input class="form-check-input" type="checkbox" (change)="addField(field)" id="field_{{field}}">
<input class="form-check-input" type="checkbox" (click)="addField(field)" id="field_{{field}}">
<label class="form-check-label" for="field_{{field}}">
{{field}}
</label>

3
frontend/app/features/content/pages/contents/custom-view-editor.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.container {
max-height: 400px;
overflow-x: hidden;

28
frontend/app/features/content/pages/contents/custom-view-editor.component.ts

@ -17,14 +17,14 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Out
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomViewEditorComponent implements OnChanges {
@Input()
public allFields: ReadonlyArray<string>;
@Output()
public fieldNamesChange = new EventEmitter<ReadonlyArray<string>>();
@Input()
public fieldNames: ReadonlyArray<string>;
@Output()
public fieldNamesChange = new EventEmitter<ReadonlyArray<string>>();
@Input()
public allFields: ReadonlyArray<string>;
public fieldsNotAdded: ReadonlyArray<string>;
@ -32,29 +32,25 @@ export class CustomViewEditorComponent implements OnChanges {
this.fieldsNotAdded = this.allFields.filter(n => this.fieldNames.indexOf(n) < 0);
}
public random() {
return Math.random();
}
public drop(event: CdkDragDrop<string[]>) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
this.updateFields(event.container.data);
}
public updateFields(fieldNames: ReadonlyArray<string>) {
this.fieldNamesChange.emit(fieldNames);
this.updateFieldNames(event.container.data);
}
public resetDefault() {
this.updateFields([]);
this.updateFieldNames([]);
}
public addField(field: string) {
this.updateFields([...this.fieldNames, field]);
this.updateFieldNames([...this.fieldNames, field]);
}
public removeField(field: string) {
this.updateFields(this.fieldNames.filter(x => x !== field));
this.updateFieldNames(this.fieldNames.filter(x => x !== field));
}
private updateFieldNames(fieldNames: ReadonlyArray<string>) {
this.fieldNamesChange.emit(fieldNames);
}
}

3
frontend/app/features/content/pages/schemas/schemas-page.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.btn-collapse {
& {
font-size: 1.2rem;

138
frontend/app/features/content/shared/content-list-field.component.ts

@ -1,138 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
ContentDto,
getContentValue,
LanguageDto,
MetaFields,
RootFieldDto,
TableField,
Types
} from '@app/shared';
@Component({
selector: 'sqx-content-list-field',
template: `
<ng-container [ngSwitch]="fieldName">
<ng-container *ngSwitchCase="metaFields.id">
<small class="truncate">{{content.id}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.created">
<small class="truncate">{{content.created | sqxFromNow}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByAvatar">
<img class="user-picture" title="{{content.createdBy | sqxUserNameRef}}" [src]="content.createdBy | sqxUserPictureRef" />
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByName">
<small class="truncate">{{content.createdBy | sqxUserNameRef}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModified">
<small class="truncate">{{content.lastModified | sqxFromNow}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [src]="content.lastModifiedBy | sqxUserPictureRef" />
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByName">
<small class="truncate">{{content.lastModifiedBy | sqxUserNameRef}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.status">
<span class="truncate">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
</sqx-content-status>
{{content.status}}
</span>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusNext">
<span class="truncate" *ngIf="content.scheduleJob; let job">
{{job.status}} at {{job.dueTime | sqxShortDate}}
</span>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusColor">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
</sqx-content-status>
</ng-container>
<ng-container *ngSwitchCase="metaFields.version">
<small class="truncate">{{content.version.value}}</small>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-container *ngIf="isInlineEditable && patchAllowed; else displayTemplate">
<sqx-content-value-editor [form]="patchForm" [field]="field"></sqx-content-value-editor>
</ng-container>
<ng-template #displayTemplate>
<sqx-content-value [value]="value"></sqx-content-value>
</ng-template>
</ng-container>
</ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentListFieldComponent implements OnChanges {
@Input()
public field: TableField;
@Input()
public content: ContentDto;
@Input()
public patchAllowed: boolean;
@Input()
public patchForm: FormGroup;
@Input()
public language: LanguageDto;
public value: any;
public ngOnChanges() {
this.reset();
}
public reset() {
if (Types.is(this.field, RootFieldDto)) {
const { value, formatted } = getContentValue(this.content, this.language, this.field);
if (this.patchForm) {
const formControl = this.patchForm.controls[this.field.name];
if (formControl) {
formControl.setValue(value);
}
}
this.value = formatted;
}
}
public get metaFields() {
return MetaFields;
}
public get isInlineEditable() {
return Types.is(this.field, RootFieldDto) ? this.field.isInlineEditable : false;
}
public get fieldName() {
return Types.is(this.field, RootFieldDto) ? this.field.name : this.field;
}
}

119
frontend/app/features/content/shared/content-list-header.component.ts

@ -1,119 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import {
LanguageDto,
MetaFields,
Query,
RootFieldDto,
TableField,
Types
} from '@app/shared';
@Component({
selector: 'sqx-content-list-header',
template: `
<ng-container [ngSwitch]="fieldName">
<ng-container *ngSwitchCase="metaFields.id">
<sqx-table-header text="Id"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.created">
<sqx-table-header text="Created"
[sortable]="true"
[fieldPath]="'created'"
[query]="query"
(queryChange)="queryChange.emit($event)"
[language]="language">
</sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByAvatar">
<sqx-table-header text="By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByName">
<sqx-table-header text="Created By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModified">
<sqx-table-header text="Updated"
[sortable]="true"
[fieldPath]="'lastModified'"
[query]="query"
(queryChange)="queryChange.emit($event)"
[language]="language">
</sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar">
<sqx-table-header text="By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByName">
<sqx-table-header text="Modified By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.status">
<sqx-table-header text="Status"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusNext">
<sqx-table-header text="Next Status"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusColor">
<sqx-table-header text="Status"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.version">
<sqx-table-header text="Version"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchDefault>
<sqx-table-header [text]="fieldDisplayName"
[sortable]="isSortable"
[fieldPath]="fieldPath"
[query]="query"
(queryChange)="queryChange.emit($event)"
[language]="language">
</sqx-table-header>
</ng-container>
</ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentListHeaderComponent {
@Input()
public field: TableField;
@Output()
public queryChange = new EventEmitter<Query>();
@Input()
public query: Query;
@Input()
public language: LanguageDto;
public get metaFields() {
return MetaFields;
}
public get isSortable() {
return Types.is(this.field, RootFieldDto) ? this.field.properties.isSortable : false;
}
public get fieldName() {
return Types.is(this.field, RootFieldDto) ? this.field.name : this.field;
}
public get fieldDisplayName() {
return Types.is(this.field, RootFieldDto) ? this.field.displayName : '';
}
public get fieldPath() {
if (Types.isString(this.field)) {
return this.field;
} else if (this.field.isLocalizable && this.language) {
return `data.${this.field.name}.${this.language.iso2Code}`;
} else {
return `data.${this.field.name}.iv`;
}
}
}

69
frontend/app/features/content/shared/content-selector-item.component.ts

@ -1,69 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import {
ContentDto,
LanguageDto,
SchemaDetailsDto
} from '@app/shared';
/* tslint:disable:component-selector */
@Component({
selector: '[sqxContentSelectorItem]',
template: `
<tr (click)="toggle()">
<td class="cell-select" sqxStopClick>
<input type="checkbox" class="form-check"
[disabled]="!selectable"
[ngModel]="selected || !selectable"
(ngModelChange)="emitSelectedChange($event)" />
</td>
<td sqxContentListCell="meta.lastModifiedBy.avatar">
<sqx-content-list-field field="meta.lastModifiedBy.avatar" [content]="content" [language]="language"></sqx-content-list-field>
</td>
<td *ngFor="let field of schema.defaultReferenceFields" [sqxContentListCell]="field">
<sqx-content-list-field [field]="field" [content]="content" [language]="language"></sqx-content-list-field>
</td>
</tr>
<tr class="spacer"></tr>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentSelectorItemComponent {
@Output()
public selectedChange = new EventEmitter<boolean>();
@Input()
public selected = false;
@Input()
public selectable = true;
@Input()
public language: LanguageDto;
@Input()
public schema: SchemaDetailsDto;
@Input('sqxContentSelectorItem')
public content: ContentDto;
public toggle() {
if (this.selectable) {
this.emitSelectedChange(!this.selected);
}
}
public emitSelectedChange(isSelected: boolean) {
this.selectedChange.emit(isSelected);
}
}

2
frontend/app/features/content/shared/content-status.component.scss

@ -1,5 +1,3 @@
@import '_vars';
@import '_mixins';
.content-status {
&.default {

73
frontend/app/features/content/shared/content-value-editor.component.ts

@ -1,73 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto } from '@app/shared';
@Component({
selector: 'sqx-content-value-editor',
template: `
<div [formGroup]="form">
<ng-container [ngSwitch]="field.properties.fieldType">
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControlName]="field.name" [maximumStars]="field.rawProperties.maxValue"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<ng-container class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="field.name" sqxIndeterminateValue />
</ng-container>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentValueEditorComponent {
@Input()
public field: FieldDto;
@Input()
public form: FormGroup;
}

42
frontend/app/features/content/shared/content-value.component.ts

@ -1,42 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { HtmlValue, Types } from '@app/shared';
@Component({
selector: 'sqx-content-value',
template: `
<ng-container *ngIf="isPlain; else html">
<span class="truncate">{{value}}</span>
</ng-container>
<ng-template #html>
<span class="html-value" [innerHTML]="value.html"></span>
</ng-template>
`,
styles: [`
.html-value {
position: relative;
}
::ng-deep .html-value img {
position: absolute;
min-height: 50px;
max-height: 50px;
margin-top: -25px;
}`
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentValueComponent {
@Input()
public value: any;
public get isPlain() {
return !Types.is(this.value, HtmlValue);
}
}

2
frontend/app/features/content/shared/due-time-selector.component.scss

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

13
frontend/app/features/content/shared/due-time-selector.component.ts

@ -16,25 +16,26 @@ import { DialogModel } from '@app/shared';
templateUrl: './due-time-selector.component.html'
})
export class DueTimeSelectorComponent {
private dueTimeResult: Subject<string | null>;
public dueTimeDialog = new DialogModel();
public dueTime: string | null = '';
public dueTimeFunction: Subject<string | null>;
public dueTimeAction: string | null = '';
public dueTimeMode = 'Immediately';
public selectDueTime(action: string): Observable<string | null> {
this.dueTimeAction = action;
this.dueTimeFunction = new Subject<string | null>();
this.dueTimeResult = new Subject<string | null>();
this.dueTimeDialog.show();
return this.dueTimeFunction;
return this.dueTimeResult;
}
public confirmStatusChange() {
const result = this.dueTimeMode === 'Immediately' ? null : this.dueTime;
this.dueTimeFunction.next(result);
this.dueTimeFunction.complete();
this.dueTimeResult.next(result);
this.dueTimeResult.complete();
this.cancelStatusChange();
}
@ -42,7 +43,7 @@ export class DueTimeSelectorComponent {
public cancelStatusChange() {
this.dueTimeMode = 'Immediately';
this.dueTimeDialog.hide();
this.dueTimeFunction = null!;
this.dueTimeResult = null!;
this.dueTime = null;
}
}

0
frontend/app/features/content/shared/array-editor.component.html → frontend/app/features/content/shared/forms/array-editor.component.html

3
frontend/app/features/content/shared/array-editor.component.scss → frontend/app/features/content/shared/forms/array-editor.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.array-container {
background: $color-border;
margin-bottom: 1rem;

0
frontend/app/features/content/shared/array-editor.component.ts → frontend/app/features/content/shared/forms/array-editor.component.ts

12
frontend/app/features/content/shared/array-item.component.html → frontend/app/features/content/shared/forms/array-item.component.html

@ -11,16 +11,16 @@
</div>
</div>
<div class="col-auto pr-4">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="emitMoveTop()">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveTop()">
<i class="icon-caret-top"></i>
</button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="emitMoveUp()">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveUp()">
<i class="icon-caret-up"></i>
</button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="emitMoveDown()">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveDown()">
<i class="icon-caret-down"></i>
</button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="emitMoveBottom()">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveBottom()">
<i class="icon-caret-bottom"></i>
</button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="!isHidden" (click)="expand()" title="Expand this item">
@ -31,11 +31,11 @@
</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled" (click)="emitClone()">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled" (click)="clone.emit()">
<i class="icon-clone"></i>
</button>
<button type="button" class="btn btn-text-danger" [disabled]="isDisabled" (click)="emitRemove()">
<button type="button" class="btn btn-text-danger" [disabled]="isDisabled" (click)="remove.emit()">
<i class="icon-bin2"></i>
</button>
</div>

3
frontend/app/features/content/shared/array-item.component.scss → frontend/app/features/content/shared/forms/array-item.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
:host ::ng-deep {
.ui-separator {
border-color: $color-border !important;

16
frontend/app/features/content/shared/array-item.component.ts → frontend/app/features/content/shared/forms/array-item.component.ts

@ -155,27 +155,19 @@ export class ArrayItemComponent implements OnChanges, OnDestroy {
this.changeDetector.markForCheck();
}
public emitClone() {
this.clone.emit();
}
public emitRemove() {
this.remove.emit();
}
public emitMoveTop() {
public moveTop() {
this.move.emit(0);
}
public emitMoveUp() {
public moveUp() {
this.move.emit(this.index - 1);
}
public emitMoveDown() {
public moveDown() {
this.move.emit(this.index + 1);
}
public emitMoveBottom() {
public moveBottom() {
this.move.emit(99999);
}

0
frontend/app/features/content/shared/assets-editor.component.html → frontend/app/features/content/shared/forms/assets-editor.component.html

3
frontend/app/features/content/shared/assets-editor.component.scss → frontend/app/features/content/shared/forms/assets-editor.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.disabled {
pointer-events: none;
}

9
frontend/app/features/content/shared/assets-editor.component.ts → frontend/app/features/content/shared/forms/assets-editor.component.ts

@ -45,11 +45,12 @@ interface State {
selector: 'sqx-assets-editor',
styleUrls: ['./assets-editor.component.scss'],
templateUrl: './assets-editor.component.html',
providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR],
providers: [
SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR
],
changeDetection: ChangeDetectionStrategy.OnPush
})
// tslint:disable-next-line: readonly-array
export class AssetsEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit {
export class AssetsEditorComponent extends StatefulControlComponent<State, ReadonlyArray<string>> implements OnInit {
@Input()
public isCompact = false;
@ -70,7 +71,7 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) {
if (!Types.isEquals(obj, this.snapshot.assets.map(x => x.id))) {
if (!Types.equals(obj, this.snapshot.assets.map(x => x.id))) {
const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)

0
frontend/app/features/content/shared/field-editor.component.html → frontend/app/features/content/shared/forms/field-editor.component.html

3
frontend/app/features/content/shared/field-editor.component.scss → frontend/app/features/content/shared/forms/field-editor.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
.field {
&-required {
color: $color-theme-error;

0
frontend/app/features/content/shared/field-editor.component.ts → frontend/app/features/content/shared/forms/field-editor.component.ts

0
frontend/app/features/content/shared/stock-photo-editor.component.html → frontend/app/features/content/shared/forms/stock-photo-editor.component.html

3
frontend/app/features/content/shared/stock-photo-editor.component.scss → frontend/app/features/content/shared/forms/stock-photo-editor.component.scss

@ -1,6 +1,3 @@
@import '_vars';
@import '_mixins';
$color-user-background: rgba(0, 0, 0, .5);
$color-background: #000;

4
frontend/app/features/content/shared/stock-photo-editor.component.ts → frontend/app/features/content/shared/forms/stock-photo-editor.component.ts

@ -30,7 +30,9 @@ export const SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-stock-photo-editor',
styleUrls: ['./stock-photo-editor.component.scss'],
templateUrl: './stock-photo-editor.component.html',
providers: [SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR],
providers: [
SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StockPhotoEditorComponent extends StatefulControlComponent<State, string> implements OnInit {

0
frontend/app/features/content/shared/content-list-cell.directive.ts → frontend/app/features/content/shared/list/content-list-cell.directive.ts

62
frontend/app/features/content/shared/list/content-list-field.component.html

@ -0,0 +1,62 @@
<ng-container [ngSwitch]="fieldName">
<ng-container *ngSwitchCase="metaFields.id">
<small class="truncate">{{content.id}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.created">
<small class="truncate">{{content.created | sqxFromNow}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByAvatar">
<img class="user-picture" title="{{content.createdBy | sqxUserNameRef}}" [src]="content.createdBy | sqxUserPictureRef" />
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByName">
<small class="truncate">{{content.createdBy | sqxUserNameRef}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModified">
<small class="truncate">{{content.lastModified | sqxFromNow}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [src]="content.lastModifiedBy | sqxUserPictureRef" />
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByName">
<small class="truncate">{{content.lastModifiedBy | sqxUserNameRef}}</small>
</ng-container>
<ng-container *ngSwitchCase="metaFields.status">
<span class="truncate">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
</sqx-content-status>
{{content.status}}
</span>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusNext">
<span class="truncate" *ngIf="content.scheduleJob">
{{content.scheduleJob.status}} at {{content.scheduleJob.dueTime | sqxShortDate}}
</span>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusColor">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
</sqx-content-status>
</ng-container>
<ng-container *ngSwitchCase="metaFields.version">
<small class="truncate">{{content.version.value}}</small>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-container *ngIf="isInlineEditable && patchAllowed; else displayTemplate">
<sqx-content-value-editor [form]="patchForm" [field]="field"></sqx-content-value-editor>
</ng-container>
<ng-template #displayTemplate>
<sqx-content-value [value]="value"></sqx-content-value>
</ng-template>
</ng-container>
</ng-container>

0
frontend/app/features/content/shared/list/content-list-field.component.scss

76
frontend/app/features/content/shared/list/content-list-field.component.ts

@ -0,0 +1,76 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
ContentDto,
getContentValue,
LanguageDto,
MetaFields,
RootFieldDto,
TableField,
Types
} from '@app/shared';
@Component({
selector: 'sqx-content-list-field',
styleUrls: ['./content-list-field.component.scss'],
templateUrl: './content-list-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentListFieldComponent implements OnChanges {
@Input()
public field: TableField;
@Input()
public content: ContentDto;
@Input()
public patchAllowed: boolean;
@Input()
public patchForm: FormGroup;
@Input()
public language: LanguageDto;
public value: any;
public ngOnChanges() {
this.reset();
}
public reset() {
if (Types.is(this.field, RootFieldDto)) {
const { value, formatted } = getContentValue(this.content, this.language, this.field);
if (this.patchForm) {
const formControl = this.patchForm.controls[this.field.name];
if (formControl) {
formControl.setValue(value);
}
}
this.value = formatted;
}
}
public get metaFields() {
return MetaFields;
}
public get isInlineEditable() {
return Types.is(this.field, RootFieldDto) ? this.field.isInlineEditable : false;
}
public get fieldName() {
return Types.is(this.field, RootFieldDto) ? this.field.name : this.field;
}
}

56
frontend/app/features/content/shared/list/content-list-header.component.html

@ -0,0 +1,56 @@
<ng-container [ngSwitch]="fieldName">
<ng-container *ngSwitchCase="metaFields.id">
<sqx-table-header text="Id"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.created">
<sqx-table-header text="Created"
[sortable]="true"
[fieldPath]="'created'"
[query]="query"
(queryChange)="queryChange.emit($event)"
[language]="language">
</sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByAvatar">
<sqx-table-header text="By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.createdByName">
<sqx-table-header text="Created By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModified">
<sqx-table-header text="Updated"
[sortable]="true"
[fieldPath]="'lastModified'"
[query]="query"
(queryChange)="queryChange.emit($event)"
[language]="language">
</sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar">
<sqx-table-header text="By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByName">
<sqx-table-header text="Modified By"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.status">
<sqx-table-header text="Status"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusNext">
<sqx-table-header text="Next Status"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.statusColor">
<sqx-table-header text="Status"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchCase="metaFields.version">
<sqx-table-header text="Version"></sqx-table-header>
</ng-container>
<ng-container *ngSwitchDefault>
<sqx-table-header [text]="fieldDisplayName"
[sortable]="isSortable"
[fieldPath]="fieldPath"
[query]="query"
(queryChange)="queryChange.emit($event)"
[language]="language">
</sqx-table-header>
</ng-container>
</ng-container>

0
frontend/app/features/content/shared/list/content-list-header.component.scss

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

Loading…
Cancel
Save