Browse Source

Feature/multi references (#422)

* Multiple referenced schemas.
pull/425/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
f4fe619739
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  2. 30
      src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs
  3. 2
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  4. 7
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs
  5. 5
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs
  6. 44
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  7. 22
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  8. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  9. 15
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  10. 10
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  11. 9
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  12. 12
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  13. 14
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  14. 56
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  15. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  16. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  17. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  18. 5
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  19. 32
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  20. 92
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs
  21. 60
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs
  22. 30
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  23. 7
      src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs
  24. 66
      src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  25. 4
      src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  26. 4
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  27. 2
      src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs
  28. 37
      src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs
  29. 94
      src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs
  30. 22
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  31. 27
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  32. 42
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs
  33. 44
      src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs
  34. 14
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs
  35. 33
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs
  36. 7
      src/Squidex/app/app.module.ts
  37. 2
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  38. 6
      src/Squidex/app/features/administration/services/users.service.ts
  39. 8
      src/Squidex/app/features/administration/state/event-consumers.state.spec.ts
  40. 9
      src/Squidex/app/features/administration/state/event-consumers.state.ts
  41. 10
      src/Squidex/app/features/administration/state/users.state.spec.ts
  42. 10
      src/Squidex/app/features/administration/state/users.state.ts
  43. 2
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  44. 2
      src/Squidex/app/features/apps/pages/news-dialog.component.ts
  45. 4
      src/Squidex/app/features/assets/pages/assets-filters-page.component.ts
  46. 2
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  47. 2
      src/Squidex/app/features/content/declarations.ts
  48. 6
      src/Squidex/app/features/content/module.ts
  49. 4
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  50. 4
      src/Squidex/app/features/content/pages/content/content-history-page.component.ts
  51. 2
      src/Squidex/app/features/content/pages/content/content-page.component.html
  52. 5
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  53. 2
      src/Squidex/app/features/content/pages/content/field-languages.component.ts
  54. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  55. 25
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  56. 4
      src/Squidex/app/features/content/shared/array-editor.component.ts
  57. 4
      src/Squidex/app/features/content/shared/array-item.component.ts
  58. 2
      src/Squidex/app/features/content/shared/assets-editor.component.html
  59. 49
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  60. 102
      src/Squidex/app/features/content/shared/content-selector-item.component.ts
  61. 10
      src/Squidex/app/features/content/shared/content-value-editor.component.ts
  62. 35
      src/Squidex/app/features/content/shared/content.component.html
  63. 24
      src/Squidex/app/features/content/shared/content.component.scss
  64. 25
      src/Squidex/app/features/content/shared/content.component.ts
  65. 148
      src/Squidex/app/features/content/shared/contents-selector.component.html
  66. 11
      src/Squidex/app/features/content/shared/contents-selector.component.scss
  67. 69
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  68. 34
      src/Squidex/app/features/content/shared/field-editor.component.html
  69. 2
      src/Squidex/app/features/content/shared/field-editor.component.ts
  70. 6
      src/Squidex/app/features/content/shared/preview-button.component.ts
  71. 28
      src/Squidex/app/features/content/shared/reference-item.component.scss
  72. 104
      src/Squidex/app/features/content/shared/reference-item.component.ts
  73. 19
      src/Squidex/app/features/content/shared/references-editor.component.html
  74. 83
      src/Squidex/app/features/content/shared/references-editor.component.ts
  75. 8
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  76. 2
      src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts
  77. 3
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  78. 26
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  79. 5
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  80. 8
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts
  81. 8
      src/Squidex/app/features/schemas/pages/schema/forms/field-form.component.ts
  82. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  83. 2
      src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts
  84. 8
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html
  85. 16
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts
  86. 3
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  87. 2
      src/Squidex/app/features/settings/pages/clients/client.component.ts
  88. 9
      src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.ts
  89. 3
      src/Squidex/app/features/settings/pages/contributors/contributor.component.ts
  90. 2
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  91. 29
      src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts
  92. 5
      src/Squidex/app/features/settings/pages/languages/language-add-form.component.ts
  93. 2
      src/Squidex/app/features/settings/pages/languages/language.component.html
  94. 21
      src/Squidex/app/features/settings/pages/languages/language.component.ts
  95. 2
      src/Squidex/app/features/settings/pages/more/more-page.component.ts
  96. 6
      src/Squidex/app/features/settings/pages/roles/roles-page.component.ts
  97. 6
      src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts
  98. 2
      src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.ts
  99. 9
      src/Squidex/app/features/settings/pages/workflows/workflow.component.ts
  100. 21
      src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts

19
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
@ -21,7 +23,22 @@ namespace Squidex.Domain.Apps.Core.Schemas
public ReferencesFieldEditor Editor { get; set; } public ReferencesFieldEditor Editor { get; set; }
public Guid SchemaId { get; set; } public ReadOnlyCollection<Guid> SchemaIds { get; set; }
public Guid SchemaId
{
set
{
if (value != default)
{
SchemaIds = new ReadOnlyCollection<Guid>(new List<Guid> { value });
}
else
{
SchemaIds = null;
}
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

30
src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs

@ -54,11 +54,39 @@ namespace Squidex.Domain.Apps.Core.Schemas
return schema.Properties.Label.WithFallback(schema.TypeName()); return schema.Properties.Label.WithFallback(schema.TypeName());
} }
public static string DisplayNameUnchanged(this Schema schema)
{
return schema.Properties.Label.WithFallback(schema.Name);
}
public static Guid SingleId(this ReferencesFieldProperties properties)
{
return properties.SchemaIds?.Count == 1 ? properties.SchemaIds[0] : Guid.Empty;
}
public static IEnumerable<RootField> ReferenceFields(this Schema schema)
{
var references = schema.Fields.Where(x => x.RawProperties.IsReferenceField);
if (references.Any())
{
return references;
}
references = schema.Fields.Where(x => x.RawProperties.IsListField);
if (references.Any())
{
return references;
}
return schema.Fields.Take(1);
}
public static IEnumerable<IField<ReferencesFieldProperties>> ResolvingReferences(this Schema schema) public static IEnumerable<IField<ReferencesFieldProperties>> ResolvingReferences(this Schema schema)
{ {
return schema.Fields.OfType<IField<ReferencesFieldProperties>>() return schema.Fields.OfType<IField<ReferencesFieldProperties>>()
.Where(x => .Where(x =>
x.Properties.SchemaId != Guid.Empty &&
x.Properties.ResolveReference && x.Properties.ResolveReference &&
x.Properties.MaxItems == 1 && x.Properties.MaxItems == 1 &&
(x.Properties.IsListField || schema.Fields.Count == 1)); (x.Properties.IsListField || schema.Fields.Count == 1));

2
src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs

@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
public IJsonValue Visit(IField<ReferencesFieldProperties> field) public IJsonValue Visit(IField<ReferencesFieldProperties> field)
{ {
if (oldReferences.Contains(field.Properties.SchemaId)) if (oldReferences.Contains(field.Properties.SingleId()))
{ {
return JsonValue.Array(); return JsonValue.Array();
} }

7
src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs

@ -62,9 +62,12 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{ {
var ids = value.ToGuidSet(); var ids = value.ToGuidSet();
if (strategy == Ids.All && field.Properties.SchemaId != Guid.Empty) if (strategy == Ids.All && field.Properties.SchemaIds != null)
{ {
ids.Add(field.Properties.SchemaId); foreach (var schemaId in field.Properties.SchemaIds)
{
ids.Add(schemaId);
}
} }
return ids; return ids;

5
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs

@ -134,10 +134,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new UniqueValuesValidator<Guid>(); yield return new UniqueValuesValidator<Guid>();
} }
if (field.Properties.SchemaId != Guid.Empty) yield return new ReferencesValidator(field.Properties.SchemaIds);
{
yield return new ReferencesValidator(field.Properties.SchemaId);
}
} }
public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field) public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field)

44
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs

@ -14,7 +14,9 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
public delegate Task<IReadOnlyList<Guid>> CheckContents(Guid schemaId, FilterNode<ClrValue> filter); public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckContents(Guid schemaId, FilterNode<ClrValue> filter);
public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckContentsByIds(HashSet<Guid> ids);
public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids); public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids);
@ -23,6 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private readonly Guid contentId; private readonly Guid contentId;
private readonly Guid schemaId; private readonly Guid schemaId;
private readonly CheckContents checkContent; private readonly CheckContents checkContent;
private readonly CheckContentsByIds checkContentByIds;
private readonly CheckAssets checkAsset; private readonly CheckAssets checkAsset;
private readonly ImmutableQueue<string> propertyPath; private readonly ImmutableQueue<string> propertyPath;
@ -47,8 +50,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
Guid contentId, Guid contentId,
Guid schemaId, Guid schemaId,
CheckContents checkContent, CheckContents checkContent,
CheckContentsByIds checkContentsByIds,
CheckAssets checkAsset) CheckAssets checkAsset)
: this(contentId, schemaId, checkContent, checkAsset, ImmutableQueue<string>.Empty, false) : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue<string>.Empty, false)
{ {
} }
@ -56,16 +60,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
Guid contentId, Guid contentId,
Guid schemaId, Guid schemaId,
CheckContents checkContent, CheckContents checkContent,
CheckContentsByIds checkContentByIds,
CheckAssets checkAsset, CheckAssets checkAsset,
ImmutableQueue<string> propertyPath, ImmutableQueue<string> propertyPath,
bool isOptional) bool isOptional)
{ {
Guard.NotNull(checkAsset, nameof(checkAsset)); Guard.NotNull(checkAsset, nameof(checkAsset));
Guard.NotNull(checkContent, nameof(checkAsset)); Guard.NotNull(checkContent, nameof(checkContent));
Guard.NotNull(checkContentByIds, nameof(checkContentByIds));
this.propertyPath = propertyPath; this.propertyPath = propertyPath;
this.checkContent = checkContent; this.checkContent = checkContent;
this.checkContentByIds = checkContentByIds;
this.checkAsset = checkAsset; this.checkAsset = checkAsset;
this.contentId = contentId; this.contentId = contentId;
@ -76,17 +83,40 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public ValidationContext Optional(bool isOptional) public ValidationContext Optional(bool isOptional)
{ {
return isOptional == IsOptional ? this : new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath, isOptional); return isOptional == IsOptional ? this : OptionalCore(isOptional);
}
private ValidationContext OptionalCore(bool isOptional)
{
return new ValidationContext(
contentId,
schemaId,
checkContent,
checkContentByIds,
checkAsset,
propertyPath,
isOptional);
} }
public ValidationContext Nested(string property) public ValidationContext Nested(string property)
{ {
return new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional); return new ValidationContext(
contentId, schemaId,
checkContent,
checkContentByIds,
checkAsset,
propertyPath.Enqueue(property),
IsOptional);
}
public Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> GetContentIdsAsync(HashSet<Guid> ids)
{
return checkContentByIds(ids);
} }
public Task<IReadOnlyList<Guid>> GetContentIdsAsync(Guid validatedSchemaId, FilterNode<ClrValue> filter) public Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> GetContentIdsAsync(Guid schemaId, FilterNode<ClrValue> filter)
{ {
return checkContent(validatedSchemaId, filter); return checkContent(schemaId, filter);
} }
public Task<IReadOnlyList<IAssetInfo>> GetAssetInfosAsync(IEnumerable<Guid> assetId) public Task<IReadOnlyList<IAssetInfo>> GetAssetInfosAsync(IEnumerable<Guid> assetId)

22
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs

@ -9,35 +9,37 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public sealed class ReferencesValidator : IValidator public sealed class ReferencesValidator : IValidator
{ {
private static readonly PropertyPath Path = "Id"; private readonly IEnumerable<Guid> schemaIds;
private readonly Guid schemaId; public ReferencesValidator(IEnumerable<Guid> schemaIds)
public ReferencesValidator(Guid schemaId)
{ {
this.schemaId = schemaId; this.schemaIds = schemaIds;
} }
public async Task ValidateAsync(object value, ValidationContext context, AddError addError) public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{ {
if (value is ICollection<Guid> contentIds) if (value is ICollection<Guid> contentIds)
{ {
var filter = ClrFilter.In(Path, contentIds.ToList()); var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet());
var foundIds = await context.GetContentIdsAsync(schemaId, filter);
foreach (var id in contentIds) foreach (var id in contentIds)
{ {
if (!foundIds.Contains(id)) var (schemaId, _) = foundIds.FirstOrDefault(x => x.Id == id);
if (schemaId == Guid.Empty)
{ {
addError(context.Path, $"Contains invalid reference '{id}'."); addError(context.Path, $"Contains invalid reference '{id}'.");
} }
else if (schemaIds?.Any() == true && !schemaIds.Contains(schemaId))
{
addError(context.Path, $"Contains reference '{id}' to invalid schema.");
}
} }
} }
} }

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
var found = await context.GetContentIdsAsync(context.SchemaId, filter); var found = await context.GetContentIdsAsync(context.SchemaId, filter);
if (found.Any(x => x != context.ContentId)) if (found.Any(x => x.Id != context.ContentId))
{ {
addError(context.Path, "Another content with the same value exists."); addError(context.Path, "Another content with the same value exists.");
} }

15
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -169,15 +169,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}); });
} }
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(ISchemaEntity schema, FilterNode<ClrValue> filterNode) public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(ISchemaEntity schema, FilterNode<ClrValue> filterNode)
{ {
var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id); var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id);
var contentEntities = var contentEntities =
await Collection.Find(filter).Only(x => x.Id) await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync(); .ToListAsync();
return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList();
}
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(HashSet<Guid> ids)
{
var contentEntities =
await Collection.Find(Filter.In(x => x.Id, ids)).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList();
} }
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId) public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId)

10
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode) public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode)
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
@ -124,6 +124,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await contents.QueryIdsAsync(ids);
}
}
public async Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback) public async Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback)
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())

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

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
@ -45,10 +46,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
public string StatusColor { get; set; } public string StatusColor { get; set; }
public string SchemaName { get; set; }
public string SchemaDisplayName { get; set; }
public RootField[] ReferenceFields { get; set; }
public bool CanUpdate { get; set; } public bool CanUpdate { get; set; }
public bool IsPending { get; set; } public bool IsPending { get; set; }
public HashSet<string> CacheDependencies { get; set; } public HashSet<object> CacheDependencies { get; set; }
} }
} }

12
src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -114,7 +114,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
private ValidationContext CreateValidationContext() private ValidationContext CreateValidationContext()
{ {
return new ValidationContext(command.ContentId, schemaId, QueryContentsAsync, QueryAssetsAsync); return new ValidationContext(command.ContentId, schemaId,
QueryContentsAsync,
QueryContentsAsync,
QueryAssetsAsync);
} }
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds) private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds)
@ -122,11 +125,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds)); return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds));
} }
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid filterSchemaId, FilterNode<ClrValue> filterNode) private async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryContentsAsync(Guid filterSchemaId, FilterNode<ClrValue> filterNode)
{ {
return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode);
} }
private async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryContentsAsync(HashSet<Guid> ids)
{
return await contentRepository.QueryIdsAsync(appEntity.Id, ids);
}
private string GetScript(Func<SchemaScripts, string> script) private string GetScript(Func<SchemaScripts, string> script)
{ {
return script(schemaEntity.SchemaDef.Scripts); return script(schemaEntity.SchemaDef.Scripts);

14
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -60,9 +60,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return dataLoader.LoadAsync(id); return dataLoader.LoadAsync(id);
} }
public override Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id) public Task<IContentEntity> FindContentAsync(Guid id)
{ {
var dataLoader = GetContentsLoader(schemaId); var dataLoader = GetContentsLoader();
return dataLoader.LoadAsync(id); return dataLoader.LoadAsync(id);
} }
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return await dataLoader.LoadManyAsync(ids); return await dataLoader.LoadManyAsync(ids);
} }
public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(IJsonValue value)
{ {
var ids = ParseIds(value); var ids = ParseIds(value);
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return EmptyContents; return EmptyContents;
} }
var dataLoader = GetContentsLoader(schemaId); var dataLoader = GetContentsLoader();
return await dataLoader.LoadManyAsync(ids); return await dataLoader.LoadManyAsync(ids);
} }
@ -106,12 +106,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}); });
} }
private IDataLoader<Guid, IContentEntity> GetContentsLoader(Guid schemaId) private IDataLoader<Guid, IContentEntity> GetContentsLoader()
{ {
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IContentEntity>($"Schema_{schemaId}", return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IContentEntity>($"References",
async batch => async batch =>
{ {
var result = await GetReferencedContentsAsync(schemaId, new List<Guid>(batch)); var result = await GetReferencedContentsAsync(new List<Guid>(batch));
return result.ToDictionary(x => x.Id); return result.ToDictionary(x => x.Id);
}); });

56
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -28,9 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class GraphQLModel : IGraphModel public sealed class GraphQLModel : IGraphModel
{ {
private readonly Dictionary<ISchemaEntity, ContentGraphType> contentTypes = new Dictionary<ISchemaEntity, ContentGraphType>(); private readonly Dictionary<Guid, ContentGraphType> contentTypes = new Dictionary<Guid, ContentGraphType>();
private readonly Dictionary<ISchemaEntity, ContentDataGraphType> contentDataTypes = new Dictionary<ISchemaEntity, ContentDataGraphType>();
private readonly Dictionary<Guid, ISchemaEntity> schemasById;
private readonly PartitionResolver partitionResolver; private readonly PartitionResolver partitionResolver;
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IGraphType assetType; private readonly IGraphType assetType;
@ -54,34 +52,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
assetType = new AssetGraphType(this); assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType)); assetListType = new ListGraphType(new NonNullGraphType(assetType));
schemasById = schemas.Where(x => x.SchemaDef.IsPublished).ToDictionary(x => x.Id); var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList();
graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets); BuildSchemas(allSchemas);
graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas);
graphQLSchema.RegisterValueConverter(JsonConverter.Instance); graphQLSchema.RegisterValueConverter(JsonConverter.Instance);
InitializeContentTypes(); InitializeContentTypes();
} }
private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets) private void BuildSchemas(List<ISchemaEntity> allSchemas)
{ {
var schemas = model.schemasById.Values; foreach (var schema in allSchemas)
{
return new GraphQLSchema { Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) }; contentTypes[schema.Id] = new ContentGraphType(schema);
}
} }
private void InitializeContentTypes() private void InitializeContentTypes()
{ {
foreach (var kvp in contentDataTypes) foreach (var contentType in contentTypes.Values)
{ {
kvp.Value.Initialize(this, kvp.Key); contentType.Initialize(this);
} }
foreach (var kvp in contentTypes) foreach (var contentType in contentTypes.Values)
{ {
kvp.Value.Initialize(this, kvp.Key, contentDataTypes[kvp.Key]); graphQLSchema.RegisterType(contentType);
} }
} }
private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List<ISchemaEntity> schemas)
{
var schema = new GraphQLSchema
{
Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas)
};
return schema;
}
public IFieldResolver ResolveAssetUrl() public IFieldResolver ResolveAssetUrl()
{ {
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => var resolver = new FuncFieldResolver<IAssetEntity, object>(c =>
@ -137,26 +148,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName)
{ {
return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType, fieldName)); return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName));
} }
public IGraphType GetAssetType() public IObjectGraphType GetAssetType()
{ {
return assetType; return assetType as IObjectGraphType;
} }
public IGraphType GetContentType(Guid schemaId) public IObjectGraphType GetContentType(Guid schemaId)
{ {
var schema = schemasById.GetOrDefault(schemaId); return contentTypes.GetOrDefault(schemaId);
if (schema == null)
{
return null;
}
contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType());
return contentTypes.GetOrAdd(schema, s => new ContentGraphType());
} }
public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query)

4
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs

@ -29,9 +29,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IFieldResolver ResolveContentUrl(ISchemaEntity schema); IFieldResolver ResolveContentUrl(ISchemaEntity schema);
IGraphType GetAssetType(); IObjectGraphType GetAssetType();
IGraphType GetContentType(Guid schemaId); IObjectGraphType GetContentType(Guid schemaId);
(IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName);
} }

6
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
var contentType = model.GetContentType(schema.Id); var contentType = model.GetContentType(schema.Id);
AddContentFind(schemaId, schemaType, schemaName, contentType); AddContentFind(schemaType, schemaName, contentType);
AddContentQueries(schemaId, schemaType, schemaName, contentType, pageSizeContents); AddContentQueries(schemaId, schemaType, schemaName, contentType, pageSizeContents);
} }
@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}); });
} }
private void AddContentFind(Guid schemaId, string schemaType, string schemaName, IGraphType contentType) private void AddContentFind(string schemaType, string schemaName, IGraphType contentType)
{ {
AddField(new FieldType AddField(new FieldType
{ {
@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
var contentId = c.GetArgument<Guid>("id"); var contentId = c.GetArgument<Guid>("id");
return e.FindContentAsync(schemaId, contentId); return e.FindContentAsync(contentId);
}), }),
Description = $"Find an {schemaName} content by id." Description = $"Find an {schemaName} content by id."
}); });

2
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public AssetGraphType(IGraphModel model) public AssetGraphType(IGraphModel model)
{ {
Name = "AssetDto"; Name = "Asset";
AddField(new FieldType AddField(new FieldType
{ {

5
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -18,11 +18,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData>
{ {
public void Initialize(IGraphModel model, ISchemaEntity schema) public ContentDataGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model)
{ {
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
Name = $"{schemaType}DataDto"; Name = $"{schemaType}DataDto";
foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields())

32
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs

@ -15,12 +15,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity> public sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity>
{ {
public void Initialize(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType) private readonly ISchemaEntity schema;
private readonly string schemaType;
private readonly string schemaName;
public ContentGraphType(ISchemaEntity schema)
{ {
var schemaType = schema.TypeName(); this.schema = schema;
var schemaName = schema.DisplayName();
schemaType = schema.TypeName();
schemaName = schema.DisplayName();
Name = $"{schemaType}Dto"; Name = $"{schemaType}";
AddField(new FieldType AddField(new FieldType
{ {
@ -86,6 +92,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The color status of the {schemaName} content." Description = $"The color status of the {schemaName} content."
}); });
Interface<ContentInterfaceGraphType>();
Description = $"The structure of a {schemaName} content type.";
IsTypeOf = CheckType;
}
private bool CheckType(object value)
{
return value is IContentEntity content && content.SchemaId?.Id == schema.Id;
}
public void Initialize(IGraphModel model)
{
AddField(new FieldType AddField(new FieldType
{ {
Name = "url", Name = "url",
@ -94,6 +114,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The url to the the {schemaName} content." Description = $"The url to the the {schemaName} content."
}); });
var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model);
if (contentDataType.Fields.Any()) if (contentDataType.Fields.Any())
{ {
AddField(new FieldType AddField(new FieldType
@ -112,8 +134,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The draft data of the {schemaName} content." Description = $"The draft data of the {schemaName} content."
}); });
} }
Description = $"The structure of a {schemaName} content type.";
} }
private static IFieldResolver Resolve(Func<IEnrichedContentEntity, object> action) private static IFieldResolver Resolve(Func<IEnrichedContentEntity, object> action)

92
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs

@ -0,0 +1,92 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentInterfaceGraphType : InterfaceGraphType<IContentEntity>
{
public ContentInterfaceGraphType()
{
Name = $"Content";
AddField(new FieldType
{
Name = "id",
ResolvedType = AllTypes.NonNullGuid,
Resolver = Resolve(x => x.Id),
Description = $"The id of the content."
});
AddField(new FieldType
{
Name = "version",
ResolvedType = AllTypes.NonNullInt,
Resolver = Resolve(x => x.Version),
Description = $"The version of the content."
});
AddField(new FieldType
{
Name = "created",
ResolvedType = AllTypes.NonNullDate,
Resolver = Resolve(x => x.Created),
Description = $"The date and time when the content has been created."
});
AddField(new FieldType
{
Name = "createdBy",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.CreatedBy.ToString()),
Description = $"The user that has created the content."
});
AddField(new FieldType
{
Name = "lastModified",
ResolvedType = AllTypes.NonNullDate,
Resolver = Resolve(x => x.LastModified),
Description = $"The date and time when the content has been modified last."
});
AddField(new FieldType
{
Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.LastModifiedBy.ToString()),
Description = $"The user that has updated the content last."
});
AddField(new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()),
Description = $"The the status of the content."
});
AddField(new FieldType
{
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.StatusColor),
Description = $"The color status of the content."
});
Description = $"The structure of all content types.";
}
private static IFieldResolver Resolve(Func<IEnrichedContentEntity, object> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, object>(c => action(c.Source));
}
}
}

60
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs

@ -0,0 +1,60 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using GraphQL.Types;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentUnionGraphType : UnionGraphType
{
private readonly Dictionary<Guid, IObjectGraphType> types = new Dictionary<Guid, IObjectGraphType>();
public ContentUnionGraphType(string fieldName, Dictionary<Guid, ContentGraphType> schemaTypes, IEnumerable<Guid> schemaIds)
{
Name = $"{fieldName}ReferenceUnionDto";
if (schemaIds?.Any() == true)
{
foreach (var schemaId in schemaIds)
{
var schemaType = schemaTypes.GetOrDefault(schemaId);
if (schemaType != null)
{
types[schemaId] = schemaType;
}
}
}
else
{
foreach (var schemaType in schemaTypes)
{
types[schemaType.Key] = schemaType.Value;
}
}
foreach (var type in types)
{
AddPossibleType(type.Value);
}
ResolveType = value =>
{
if (value is IContentEntity content)
{
return types.GetOrDefault(content.SchemaId.Id);
}
return null;
};
}
}
}

30
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs

@ -6,9 +6,12 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
@ -18,18 +21,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType ResolveType, ValueResolver Resolver)> public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType ResolveType, ValueResolver Resolver)>
{ {
private static readonly ValueResolver NoopResolver = (value, c) => value; private static readonly ValueResolver NoopResolver = (value, c) => value;
private readonly Dictionary<Guid, ContentGraphType> schemaTypes;
private readonly ISchemaEntity schema; private readonly ISchemaEntity schema;
private readonly Func<Guid, IGraphType> schemaResolver;
private readonly IGraphModel model; private readonly IGraphModel model;
private readonly IGraphType assetListType; private readonly IGraphType assetListType;
private readonly string fieldName; private readonly string fieldName;
public QueryGraphTypeVisitor(ISchemaEntity schema, Func<Guid, IGraphType> schemaResolver, IGraphModel model, IGraphType assetListType, string fieldName) public QueryGraphTypeVisitor(ISchemaEntity schema,
Dictionary<Guid, ContentGraphType> schemaTypes,
IGraphModel model,
IGraphType assetListType,
string fieldName)
{ {
this.model = model; this.model = model;
this.assetListType = assetListType; this.assetListType = assetListType;
this.schema = schema; this.schema = schema;
this.schemaResolver = schemaResolver; this.schemaTypes = schemaTypes;
this.fieldName = fieldName; this.fieldName = fieldName;
} }
@ -112,22 +119,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return (assetListType, resolver); return (assetListType, resolver);
} }
private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field) private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField<ReferencesFieldProperties> field)
{ {
var schemaId = ((ReferencesFieldProperties)field.RawProperties).SchemaId; IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId());
var contentType = schemaResolver(schemaId);
if (contentType == null) if (contentType == null)
{ {
return (null, null); var union = new ContentUnionGraphType(fieldName, schemaTypes, field.Properties.SchemaIds);
if (!union.PossibleTypes.Any())
{
return (null, null);
}
contentType = union;
} }
var resolver = new ValueResolver((value, c) => var resolver = new ValueResolver((value, c) =>
{ {
var context = (GraphQLExecutionContext)c.UserContext; var context = (GraphQLExecutionContext)c.UserContext;
return context.GetReferencedContentsAsync(schemaId, value); return context.GetReferencedContentsAsync(value);
}); });
var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType));

7
src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
@ -15,6 +16,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
string StatusColor { get; } string StatusColor { get; }
string SchemaName { get; }
string SchemaDisplayName { get; }
RootField[] ReferenceFields { get; }
StatusInfo[] Nexts { get; } StatusInfo[] Nexts { get; }
NamedContentData ReferenceData { get; } NamedContentData ReferenceData { get; }

66
src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (contents.Any()) if (contents.Any())
{ {
var appVersion = context.App.Version.ToString(); var appVersion = context.App.Version;
var cache = new Dictionary<(Guid, Status), StatusInfo>(); var cache = new Dictionary<(Guid, Status), StatusInfo>();
@ -77,11 +77,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await ResolveCanUpdateAsync(content, result); await ResolveCanUpdateAsync(content, result);
} }
result.CacheDependencies = new HashSet<string>
{
appVersion
};
results.Add(result); results.Add(result);
} }
@ -89,16 +84,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString());
var schemaIdentity = schema.Id.ToString();
var schemaVersion = schema.Version.ToString();
foreach (var content in group) foreach (var content in group)
{ {
content.CacheDependencies.Add(schemaIdentity); content.CacheDependencies = new HashSet<object>
content.CacheDependencies.Add(schemaVersion); {
schema.Id,
schema.Version
};
}
if (ShouldEnrichWithSchema(context))
{
var referenceFields = schema.SchemaDef.ReferenceFields().ToArray();
var schemaName = schema.SchemaDef.Name;
var schemaDisplayName = schema.SchemaDef.DisplayNameUnchanged();
foreach (var content in group)
{
content.ReferenceFields = referenceFields;
content.SchemaName = schemaName;
content.SchemaDisplayName = schemaDisplayName;
}
} }
if (ShouldEnrichWithReferences(context)) if (ShouldEnrich(context))
{ {
await ResolveReferencesAsync(schema, group, context); await ResolveReferencesAsync(schema, group, context);
} }
@ -129,12 +139,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
try try
{ {
var referencedSchemaId = field.Properties.SchemaId;
var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, referencedSchemaId.ToString());
var schemaIdentity = referencedSchema.Id.ToString();
var schemaVersion = referencedSchema.Version.ToString();
foreach (var content in contents) foreach (var content in contents)
{ {
var fieldReference = content.ReferenceData[field.Name]; var fieldReference = content.ReferenceData[field.Name];
@ -151,9 +155,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (referencedContents.Count == 1) if (referencedContents.Count == 1)
{ {
var value = var reference = referencedContents[0];
formatted.GetOrAdd(referencedContents[0],
x => x.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig)); var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString());
content.CacheDependencies.Add(referencedSchema.Id);
content.CacheDependencies.Add(referencedSchema.Version);
var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema));
fieldReference.AddJsonValue(partitionValue.Key, value); fieldReference.AddJsonValue(partitionValue.Key, value);
} }
@ -165,9 +174,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
} }
} }
content.CacheDependencies.Add(schemaIdentity);
content.CacheDependencies.Add(schemaVersion);
} }
} }
catch (DomainObjectNotFoundException) catch (DomainObjectNotFoundException)
@ -177,6 +183,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
} }
private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema)
{
return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig);
}
private static JsonObject CreateFallback(Context context, List<IEnrichedContentEntity> referencedContents) private static JsonObject CreateFallback(Context context, List<IEnrichedContentEntity> referencedContents)
{ {
var text = $"{referencedContents.Count} Reference(s)"; var text = $"{referencedContents.Count} Reference(s)";
@ -244,12 +255,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return info.Color; return info.Color;
} }
private static bool ShouldEnrichWithSchema(Context context)
{
return context.IsFrontendClient;
}
private static bool ShouldEnrichWithStatuses(Context context) private static bool ShouldEnrichWithStatuses(Context context)
{ {
return context.IsFrontendClient || context.IsResolveFlow(); return context.IsFrontendClient || context.IsResolveFlow();
} }
private static bool ShouldEnrichWithReferences(Context context) private static bool ShouldEnrich(Context context)
{ {
return context.IsFrontendClient && !context.IsNoEnrichment(); return context.IsFrontendClient && !context.IsNoEnrichment();
} }

4
src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList();
} }
public virtual async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids) public virtual async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(ICollection<Guid> ids)
{ {
Guard.NotNull(ids, nameof(ids)); Guard.NotNull(ids, nameof(ids));
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (notLoadedContents.Count > 0) if (notLoadedContents.Count > 0)
{ {
var result = await contentQuery.QueryAsync(context, schemaId.ToString(), Q.Empty.WithIds(notLoadedContents)); var result = await contentQuery.QueryAsync(context, notLoadedContents);
foreach (var content in result) foreach (var content in result)
{ {

4
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -25,7 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft);
Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode); Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode);
Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft); Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft);

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

@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Entities
{ {
public interface IEntityWithCacheDependencies public interface IEntityWithCacheDependencies
{ {
HashSet<string> CacheDependencies { get; } HashSet<object> CacheDependencies { get; }
} }
} }

37
src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs

@ -9,7 +9,7 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading;
namespace Squidex.Infrastructure.Log.Internal namespace Squidex.Infrastructure.Log.Internal
{ {
@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.Log.Internal
private const int MaxQueuedMessages = 1024; private const int MaxQueuedMessages = 1024;
private readonly IConsole console; private readonly IConsole console;
private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages); private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages);
private readonly Task outputTask; private readonly Thread outputThread;
public ConsoleLogProcessor() public ConsoleLogProcessor()
{ {
@ -31,7 +31,11 @@ namespace Squidex.Infrastructure.Log.Internal
console = new AnsiLogConsole(false); console = new AnsiLogConsole(false);
} }
outputTask = Task.Factory.StartNew(ProcessLogQueue, this, TaskCreationOptions.LongRunning); outputThread = new Thread(ProcessLogQueue)
{
IsBackground = true, Name = "Logging"
};
outputThread.Start();
} }
public void EnqueueMessage(LogMessageEntry message) public void EnqueueMessage(LogMessageEntry message)
@ -52,18 +56,25 @@ namespace Squidex.Infrastructure.Log.Internal
WriteMessage(message); WriteMessage(message);
} }
private static void ProcessLogQueue(object state)
{
var processor = (ConsoleLogProcessor)state;
processor.ProcessLogQueue();
}
private void ProcessLogQueue() private void ProcessLogQueue()
{ {
foreach (var entry in messageQueue.GetConsumingEnumerable()) try
{ {
WriteMessage(entry); foreach (var message in messageQueue.GetConsumingEnumerable())
{
WriteMessage(message);
}
}
catch
{
try
{
messageQueue.CompleteAdding();
}
catch
{
return;
}
} }
} }
@ -80,7 +91,7 @@ namespace Squidex.Infrastructure.Log.Internal
try try
{ {
outputTask.Wait(1500); outputThread.Join(1500);
} }
catch (Exception ex) catch (Exception ex)
{ {

94
src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs

@ -7,9 +7,10 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading;
using NodaTime; using NodaTime;
namespace Squidex.Infrastructure.Log.Internal namespace Squidex.Infrastructure.Log.Internal
@ -19,7 +20,7 @@ namespace Squidex.Infrastructure.Log.Internal
private const int MaxQueuedMessages = 1024; private const int MaxQueuedMessages = 1024;
private const int Retries = 10; private const int Retries = 10;
private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages); private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages);
private readonly Task outputTask; private readonly Thread outputThread;
private readonly string path; private readonly string path;
private StreamWriter writer; private StreamWriter writer;
@ -27,31 +28,10 @@ namespace Squidex.Infrastructure.Log.Internal
{ {
this.path = path; this.path = path;
outputTask = Task.Factory.StartNew(ProcessLogQueue, this, TaskCreationOptions.LongRunning); outputThread = new Thread(ProcessLogQueue)
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{ {
messageQueue.CompleteAdding(); IsBackground = true, Name = "Logging"
};
try
{
outputTask.Wait(1500);
}
catch (Exception ex)
{
if (!ex.Is<OperationCanceledException>())
{
throw;
}
}
finally
{
writer.Dispose();
}
}
} }
public void Initialize() public void Initialize()
@ -72,6 +52,8 @@ namespace Squidex.Infrastructure.Log.Internal
}; };
writer.WriteLine($"--- Started Logging {SystemClock.Instance.GetCurrentInstant()} ---", 1); writer.WriteLine($"--- Started Logging {SystemClock.Instance.GetCurrentInstant()} ---", 1);
outputThread.Start();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -84,35 +66,63 @@ namespace Squidex.Infrastructure.Log.Internal
messageQueue.Add(message); messageQueue.Add(message);
} }
private async Task ProcessLogQueue() private void ProcessLogQueue()
{ {
foreach (var entry in messageQueue.GetConsumingEnumerable()) try
{ {
for (var i = 1; i <= Retries; i++) foreach (var entry in messageQueue.GetConsumingEnumerable())
{ {
try for (var i = 1; i <= Retries; i++)
{ {
writer.WriteLine(entry.Message); try
break;
}
catch (Exception ex)
{
await Task.Delay(i * 10);
if (i == Retries)
{ {
Console.WriteLine($"Failed to write to log file '{path}': {ex}"); writer.WriteLine(entry.Message);
break;
}
catch (Exception ex)
{
Thread.Sleep(i * 10);
if (i == Retries)
{
Console.WriteLine($"Failed to write to log file '{path}': {ex}");
}
} }
} }
} }
} }
catch
{
try
{
messageQueue.CompleteAdding();
}
catch
{
return;
}
}
} }
private static Task ProcessLogQueue(object state) protected override void DisposeObject(bool disposing)
{ {
var processor = (FileLogProcessor)state; if (disposing)
{
messageQueue.CompleteAdding();
return processor.ProcessLogQueue(); try
{
outputThread.Join(1500);
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to shutdown log queue grateful: {ex}.");
}
finally
{
writer.Dispose();
}
}
} }
} }
} }

22
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -7,7 +7,9 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using NodaTime; using NodaTime;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
@ -84,6 +86,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary> /// </summary>
public string StatusColor { get; set; } public string StatusColor { get; set; }
/// <summary>
/// The name of the schema.
/// </summary>
public string SchemaName { get; set; }
/// <summary>
/// The display name of the schema.
/// </summary>
public string SchemaDisplayName { get; set; }
/// <summary>
/// The reference fields.
/// </summary>
public FieldDto[] ReferenceFields { get; set; }
/// <summary> /// <summary>
/// The version of the content. /// The version of the content.
/// </summary> /// </summary>
@ -104,6 +121,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
response.DataDraft = content.DataDraft; response.DataDraft = content.DataDraft;
} }
if (content.ReferenceFields != null)
{
response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromField).ToArray();
}
if (content.ScheduleJob != null) if (content.ScheduleJob != null)
{ {
response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto());

27
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -36,7 +36,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
[Required] [Required]
public StatusInfoDto[] Statuses { get; set; } public StatusInfoDto[] Statuses { get; set; }
public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents, Context context, ApiController controller, ISchemaEntity schema, IContentWorkflow workflow) public static async Task<ContentsDto> FromContentsAsync(IResultList<IEnrichedContentEntity> contents, Context context, ApiController controller,
ISchemaEntity schema, IContentWorkflow workflow)
{ {
var result = new ContentsDto var result = new ContentsDto
{ {
@ -44,9 +45,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray()
}; };
await result.AssignStatusesAsync(workflow, schema); if (schema != null)
{
await result.AssignStatusesAsync(workflow, schema);
result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name);
}
return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); return result;
} }
private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema)
@ -58,18 +64,15 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
private ContentsDto CreateLinks(ApiController controller, string app, string schema) private ContentsDto CreateLinks(ApiController controller, string app, string schema)
{ {
if (schema != null) var values = new { app, name = schema };
{
var values = new { app, name = schema };
AddSelfLink(controller.Url<ContentsController>(x => nameof(x.GetContents), values)); AddSelfLink(controller.Url<ContentsController>(x => nameof(x.GetContents), values));
if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) if (controller.HasPermission(Permissions.AppContentsCreate, app, schema))
{ {
AddPostLink("create", controller.Url<ContentsController>(x => nameof(x.PostContent), values)); AddPostLink("create", controller.Url<ContentsController>(x => nameof(x.PostContent), values));
AddPostLink("create/publish", controller.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true"); AddPostLink("create/publish", controller.Url<ContentsController>(x => nameof(x.PostContent), values) + "?publish=true");
}
} }
return this; return this;

42
src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs

@ -30,6 +30,15 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters
return SimpleMapper.Map(properties, new ArrayFieldPropertiesDto()); return SimpleMapper.Map(properties, new ArrayFieldPropertiesDto());
} }
public FieldPropertiesDto Visit(AssetsFieldProperties properties)
{
var result = SimpleMapper.Map(properties, new AssetsFieldPropertiesDto());
result.AllowedExtensions = properties.AllowedExtensions?.ToArray();
return result;
}
public FieldPropertiesDto Visit(BooleanFieldProperties properties) public FieldPropertiesDto Visit(BooleanFieldProperties properties)
{ {
return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto()); return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto());
@ -50,50 +59,45 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters
return SimpleMapper.Map(properties, new JsonFieldPropertiesDto()); return SimpleMapper.Map(properties, new JsonFieldPropertiesDto());
} }
public FieldPropertiesDto Visit(ReferencesFieldProperties properties) public FieldPropertiesDto Visit(NumberFieldProperties properties)
{
return SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto());
}
public FieldPropertiesDto Visit(UIFieldProperties properties)
{
return SimpleMapper.Map(properties, new UIFieldPropertiesDto());
}
public FieldPropertiesDto Visit(TagsFieldProperties properties)
{ {
var result = SimpleMapper.Map(properties, new TagsFieldPropertiesDto()); var result = SimpleMapper.Map(properties, new NumberFieldPropertiesDto());
result.AllowedValues = properties.AllowedValues?.ToArray(); result.AllowedValues = properties.AllowedValues?.ToArray();
return result; return result;
} }
public FieldPropertiesDto Visit(AssetsFieldProperties properties) public FieldPropertiesDto Visit(ReferencesFieldProperties properties)
{ {
var result = SimpleMapper.Map(properties, new AssetsFieldPropertiesDto()); var result = SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto());
result.AllowedExtensions = properties.AllowedExtensions?.ToArray(); result.SchemaIds = properties.SchemaIds?.ToArray();
return result; return result;
} }
public FieldPropertiesDto Visit(NumberFieldProperties properties) public FieldPropertiesDto Visit(StringFieldProperties properties)
{ {
var result = SimpleMapper.Map(properties, new NumberFieldPropertiesDto()); var result = SimpleMapper.Map(properties, new StringFieldPropertiesDto());
result.AllowedValues = properties.AllowedValues?.ToArray(); result.AllowedValues = properties.AllowedValues?.ToArray();
return result; return result;
} }
public FieldPropertiesDto Visit(StringFieldProperties properties) public FieldPropertiesDto Visit(TagsFieldProperties properties)
{ {
var result = SimpleMapper.Map(properties, new StringFieldPropertiesDto()); var result = SimpleMapper.Map(properties, new TagsFieldPropertiesDto());
result.AllowedValues = properties.AllowedValues?.ToArray(); result.AllowedValues = properties.AllowedValues?.ToArray();
return result; return result;
} }
public FieldPropertiesDto Visit(UIFieldProperties properties)
{
return SimpleMapper.Map(properties, new UIFieldPropertiesDto());
}
} }
} }

44
src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs

@ -7,7 +7,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Areas.Api.Controllers.Schemas.Models.Converters;
using Squidex.Areas.Api.Controllers.Schemas.Models.Fields; using Squidex.Areas.Api.Controllers.Schemas.Models.Fields;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Reflection;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -58,6 +61,47 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary> /// </summary>
public List<NestedFieldDto> Nested { get; set; } public List<NestedFieldDto> Nested { get; set; }
public static NestedFieldDto FromField(NestedField field)
{
var properties = FieldPropertiesDtoFactory.Create(field.RawProperties);
var result =
SimpleMapper.Map(field,
new NestedFieldDto
{
FieldId = field.Id,
Properties = properties
});
return result;
}
public static FieldDto FromField(RootField field)
{
var properties = FieldPropertiesDtoFactory.Create(field.RawProperties);
var result =
SimpleMapper.Map(field,
new FieldDto
{
FieldId = field.Id,
Properties = properties,
Partitioning = field.Partitioning.Key
});
if (field is IArrayField arrayField)
{
result.Nested = new List<NestedFieldDto>();
foreach (var nestedField in arrayField.Fields)
{
result.Nested.Add(FromField(nestedField));
}
}
return result;
}
public void CreateLinks(ApiController controller, string app, string schema, bool allowUpdate) public void CreateLinks(ApiController controller, string app, string schema, bool allowUpdate)
{ {
allowUpdate = allowUpdate && !IsLocked; allowUpdate = allowUpdate && !IsLocked;

14
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs

@ -7,6 +7,7 @@
using System; using System;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
@ -39,13 +40,20 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
public ReferencesFieldEditor Editor { get; set; } public ReferencesFieldEditor Editor { get; set; }
/// <summary> /// <summary>
/// The id of the referenced schema. /// The id of the referenced schemas.
/// </summary> /// </summary>
public Guid SchemaId { get; set; } public Guid[] SchemaIds { get; set; }
public override FieldProperties ToProperties() public override FieldProperties ToProperties()
{ {
return SimpleMapper.Map(this, new ReferencesFieldProperties()); var result = SimpleMapper.Map(this, new ReferencesFieldProperties());
if (SchemaIds != null)
{
result.SchemaIds = ReadOnlyCollection.Create(SchemaIds);
}
return result;
} }
} }
} }

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

@ -7,8 +7,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Areas.Api.Controllers.Schemas.Models.Converters;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Shared; using Squidex.Shared;
@ -54,36 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
foreach (var field in schema.SchemaDef.Fields) foreach (var field in schema.SchemaDef.Fields)
{ {
var fieldPropertiesDto = FieldPropertiesDtoFactory.Create(field.RawProperties); result.Fields.Add(FieldDto.FromField(field));
var fieldDto =
SimpleMapper.Map(field,
new FieldDto
{
FieldId = field.Id,
Properties = fieldPropertiesDto,
Partitioning = field.Partitioning.Key
});
if (field is IArrayField arrayField)
{
fieldDto.Nested = new List<NestedFieldDto>();
foreach (var nestedField in arrayField.Fields)
{
var nestedFieldPropertiesDto = FieldPropertiesDtoFactory.Create(nestedField.RawProperties);
var nestedFieldDto =
SimpleMapper.Map(nestedField,
new NestedFieldDto
{
FieldId = nestedField.Id,
Properties = nestedFieldPropertiesDto
});
fieldDto.Nested.Add(nestedFieldDto);
}
}
result.Fields.Add(fieldDto);
} }
result.CreateLinks(controller, app); result.CreateLinks(controller, app);

7
src/Squidex/app/app.module.ts

@ -32,11 +32,12 @@ import { SqxShellModule } from './shell';
import { routing } from './app.routes'; import { routing } from './app.routes';
export function configApiUrl() { export function configApiUrl() {
let bases = document.getElementsByTagName('base'); const baseElements = document.getElementsByTagName('base');
let baseHref = null; let baseHref = null;
if (bases.length > 0) { if (baseElements.length > 0) {
baseHref = bases[0].href; baseHref = baseElements[0].href;
} }
if (!baseHref) { if (!baseHref) {

2
src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -22,7 +22,7 @@ export class EventConsumersDto {
public readonly _links: ResourceLinks; public readonly _links: ResourceLinks;
constructor( constructor(
public readonly items: EventConsumerDto[], links?: ResourceLinks public readonly items: ReadonlyArray<EventConsumerDto>, links?: ResourceLinks
) { ) {
this._links = links || {}; this._links = links || {};
} }

6
src/Squidex/app/features/administration/services/users.service.ts

@ -36,7 +36,7 @@ export class UserDto {
public readonly id: string, public readonly id: string,
public readonly email: string, public readonly email: string,
public readonly displayName: string, public readonly displayName: string,
public readonly permissions: string[] = [], public readonly permissions: ReadonlyArray<string> = [],
public readonly isLocked?: boolean public readonly isLocked?: boolean
) { ) {
this._links = links; this._links = links;
@ -50,14 +50,14 @@ export class UserDto {
export interface CreateUserDto { export interface CreateUserDto {
readonly email: string; readonly email: string;
readonly displayName: string; readonly displayName: string;
readonly permissions: string[]; readonly permissions: ReadonlyArray<string>;
readonly password: string; readonly password: string;
} }
export interface UpdateUserDto { export interface UpdateUserDto {
readonly email: string; readonly email: string;
readonly displayName: string; readonly displayName: string;
readonly permissions: string[]; readonly permissions: ReadonlyArray<string>;
readonly password?: string; readonly password?: string;
} }

8
src/Squidex/app/features/administration/state/event-consumers.state.spec.ts

@ -42,7 +42,7 @@ describe('EventConsumersState', () => {
eventConsumersState.load().subscribe(); eventConsumersState.load().subscribe();
expect(eventConsumersState.snapshot.eventConsumers.values).toEqual([eventConsumer1, eventConsumer2]); expect(eventConsumersState.snapshot.eventConsumers).toEqual([eventConsumer1, eventConsumer2]);
expect(eventConsumersState.snapshot.isLoaded).toBeTruthy(); expect(eventConsumersState.snapshot.isLoaded).toBeTruthy();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
@ -87,7 +87,7 @@ describe('EventConsumersState', () => {
eventConsumersState.start(eventConsumer2).subscribe(); eventConsumersState.start(eventConsumer2).subscribe();
const newConsumer2 = eventConsumersState.snapshot.eventConsumers.at(1); const newConsumer2 = eventConsumersState.snapshot.eventConsumers[1];
expect(newConsumer2).toEqual(updated); expect(newConsumer2).toEqual(updated);
}); });
@ -100,7 +100,7 @@ describe('EventConsumersState', () => {
eventConsumersState.stop(eventConsumer2).subscribe(); eventConsumersState.stop(eventConsumer2).subscribe();
const newConsumer2 = eventConsumersState.snapshot.eventConsumers.at(1); const newConsumer2 = eventConsumersState.snapshot.eventConsumers[1];
expect(newConsumer2).toEqual(updated); expect(newConsumer2).toEqual(updated);
}); });
@ -113,7 +113,7 @@ describe('EventConsumersState', () => {
eventConsumersState.reset(eventConsumer2).subscribe(); eventConsumersState.reset(eventConsumer2).subscribe();
const newConsumer2 = eventConsumersState.snapshot.eventConsumers.at(1); const newConsumer2 = eventConsumersState.snapshot.eventConsumers[1];
expect(newConsumer2).toEqual(updated); expect(newConsumer2).toEqual(updated);
}); });

9
src/Squidex/app/features/administration/state/event-consumers.state.ts

@ -11,7 +11,6 @@ import { tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray,
shareSubscribed, shareSubscribed,
State State
} from '@app/shared'; } from '@app/shared';
@ -26,7 +25,7 @@ interface Snapshot {
isLoaded?: boolean; isLoaded?: boolean;
} }
type EventConsumersList = ImmutableArray<EventConsumerDto>; type EventConsumersList = ReadonlyArray<EventConsumerDto>;
@Injectable() @Injectable()
export class EventConsumersState extends State<Snapshot> { export class EventConsumersState extends State<Snapshot> {
@ -40,7 +39,7 @@ export class EventConsumersState extends State<Snapshot> {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly eventConsumersService: EventConsumersService private readonly eventConsumersService: EventConsumersService
) { ) {
super({ eventConsumers: ImmutableArray.empty() }); super({ eventConsumers: [] });
} }
public load(isReload = false, silent = false): Observable<any> { public load(isReload = false, silent = false): Observable<any> {
@ -49,13 +48,11 @@ export class EventConsumersState extends State<Snapshot> {
} }
return this.eventConsumersService.getEventConsumers().pipe( return this.eventConsumersService.getEventConsumers().pipe(
tap(({ items }) => { tap(({ items: eventConsumers }) => {
if (isReload && !silent) { if (isReload && !silent) {
this.dialogs.notifyInfo('Event Consumers reloaded.'); this.dialogs.notifyInfo('Event Consumers reloaded.');
} }
const eventConsumers = ImmutableArray.of(items);
this.next(s => { this.next(s => {
return { ...s, eventConsumers, isLoaded: true }; return { ...s, eventConsumers, isLoaded: true };
}); });

10
src/Squidex/app/features/administration/state/users.state.spec.ts

@ -50,7 +50,7 @@ describe('UsersState', () => {
usersState.load().subscribe(); usersState.load().subscribe();
expect(usersState.snapshot.users.values).toEqual([user1, user2]); expect(usersState.snapshot.users).toEqual([user1, user2]);
expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200); expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200);
expect(usersState.snapshot.isLoaded).toBeTruthy(); expect(usersState.snapshot.isLoaded).toBeTruthy();
@ -178,7 +178,7 @@ describe('UsersState', () => {
usersState.select(user2.id).subscribe(); usersState.select(user2.id).subscribe();
usersState.lock(user2).subscribe(); usersState.lock(user2).subscribe();
const user2New = usersState.snapshot.users.at(1); const user2New = usersState.snapshot.users[1];
expect(user2New).toBe(usersState.snapshot.selectedUser!); expect(user2New).toBe(usersState.snapshot.selectedUser!);
}); });
@ -192,7 +192,7 @@ describe('UsersState', () => {
usersState.select(user2.id).subscribe(); usersState.select(user2.id).subscribe();
usersState.unlock(user2).subscribe(); usersState.unlock(user2).subscribe();
const user2New = usersState.snapshot.users.at(1); const user2New = usersState.snapshot.users[1];
expect(user2New).toEqual(updated); expect(user2New).toEqual(updated);
expect(user2New).toBe(usersState.snapshot.selectedUser!); expect(user2New).toBe(usersState.snapshot.selectedUser!);
@ -209,7 +209,7 @@ describe('UsersState', () => {
usersState.select(user2.id).subscribe(); usersState.select(user2.id).subscribe();
usersState.update(user2, request).subscribe(); usersState.update(user2, request).subscribe();
const user2New = usersState.snapshot.users.at(1); const user2New = usersState.snapshot.users[1];
expect(user2New).toEqual(updated); expect(user2New).toEqual(updated);
expect(user2New).toBe(usersState.snapshot.selectedUser!); expect(user2New).toBe(usersState.snapshot.selectedUser!);
@ -223,7 +223,7 @@ describe('UsersState', () => {
usersState.create(request).subscribe(); usersState.create(request).subscribe();
expect(usersState.snapshot.users.values).toEqual([newUser, user1, user2]); expect(usersState.snapshot.users).toEqual([newUser, user1, user2]);
expect(usersState.snapshot.usersPager.numberOfItems).toBe(201); expect(usersState.snapshot.usersPager.numberOfItems).toBe(201);
}); });
}); });

10
src/Squidex/app/features/administration/state/users.state.ts

@ -13,7 +13,6 @@ import '@app/framework/utils/rxjs-extensions';
import { import {
DialogService, DialogService,
ImmutableArray,
Pager, Pager,
shareSubscribed, shareSubscribed,
State State
@ -46,7 +45,7 @@ interface Snapshot {
canCreate?: boolean; canCreate?: boolean;
} }
export type UsersList = ImmutableArray<UserDto>; export type UsersList = ReadonlyArray<UserDto>;
export type UsersResult = { total: number, users: UsersList }; export type UsersResult = { total: number, users: UsersList };
@Injectable() @Injectable()
@ -70,7 +69,7 @@ export class UsersState extends State<Snapshot> {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly usersService: UsersService private readonly usersService: UsersService
) { ) {
super({ users: ImmutableArray.empty(), usersPager: new Pager(0) }); super({ users: [], usersPager: new Pager(0) });
} }
public select(id: string | null): Observable<UserDto | null> { public select(id: string | null): Observable<UserDto | null> {
@ -110,14 +109,13 @@ export class UsersState extends State<Snapshot> {
this.snapshot.usersPager.pageSize, this.snapshot.usersPager.pageSize,
this.snapshot.usersPager.skip, this.snapshot.usersPager.skip,
this.snapshot.usersQuery).pipe( this.snapshot.usersQuery).pipe(
tap(({ total, items, canCreate }) => { tap(({ total, items: users, canCreate }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Users reloaded.'); this.dialogs.notifyInfo('Users reloaded.');
} }
this.next(s => { this.next(s => {
const usersPager = s.usersPager.setCount(total); const usersPager = s.usersPager.setCount(total);
const users = ImmutableArray.of(items);
let selectedUser = s.selectedUser; let selectedUser = s.selectedUser;
@ -141,7 +139,7 @@ export class UsersState extends State<Snapshot> {
return this.usersService.postUser(request).pipe( return this.usersService.postUser(request).pipe(
tap(created => { tap(created => {
this.next(s => { this.next(s => {
const users = s.users.pushFront(created); const users = [created, ...s.users];
const usersPager = s.usersPager.incrementCount(); const usersPager = s.usersPager.incrementCount();
return { ...s, users, usersPager }; return { ...s, users, usersPager };

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

@ -32,7 +32,7 @@ export class AppsPageComponent implements OnInit {
public onboardingDialog = new DialogModel(); public onboardingDialog = new DialogModel();
public newsFeatures: FeatureDto[]; public newsFeatures: ReadonlyArray<FeatureDto>;
public newsDialog = new DialogModel(); public newsDialog = new DialogModel();
public info: string; public info: string;

2
src/Squidex/app/features/apps/pages/news-dialog.component.ts

@ -19,7 +19,7 @@ export class NewsDialogComponent {
public close = new EventEmitter(); public close = new EventEmitter();
@Input() @Input()
public features: FeatureDto[]; public features: ReadonlyArray<FeatureDto>;
public emitClose() { public emitClose() {
this.close.emit(); this.close.emit();

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

@ -37,7 +37,7 @@ export class AssetsFiltersPageComponent {
this.assetsState.search(query); this.assetsState.search(query);
} }
public selectTags(tags: string[]) { public selectTags(tags: ReadonlyArray<string>) {
this.assetsState.selectTags(tags); this.assetsState.selectTags(tags);
} }
@ -49,7 +49,7 @@ export class AssetsFiltersPageComponent {
this.assetsState.resetTags(); this.assetsState.resetTags();
} }
public trackByTag(tag: { name: string }) { public trackByTag(index: number, tag: { name: string }) {
return tag.name; return tag.name;
} }
} }

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

@ -51,7 +51,7 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
this.assetsState.search(query); this.assetsState.search(query);
} }
public selectTags(tags: string[]) { public selectTags(tags: ReadonlyArray<string>) {
this.assetsState.selectTags(tags); this.assetsState.selectTags(tags);
} }

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

@ -21,8 +21,10 @@ export * from './shared/content.component';
export * from './shared/content-status.component'; export * from './shared/content-status.component';
export * from './shared/content-value.component'; export * from './shared/content-value.component';
export * from './shared/content-value-editor.component'; export * from './shared/content-value-editor.component';
export * from './shared/content-selector-item.component';
export * from './shared/contents-selector.component'; export * from './shared/contents-selector.component';
export * from './shared/due-time-selector.component'; export * from './shared/due-time-selector.component';
export * from './shared/field-editor.component'; export * from './shared/field-editor.component';
export * from './shared/preview-button.component'; export * from './shared/preview-button.component';
export * from './shared/reference-item.component';
export * from './shared/references-editor.component'; export * from './shared/references-editor.component';

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

@ -29,6 +29,7 @@ import {
ContentFieldComponent, ContentFieldComponent,
ContentHistoryPageComponent, ContentHistoryPageComponent,
ContentPageComponent, ContentPageComponent,
ContentSelectorItemComponent,
ContentsFiltersPageComponent, ContentsFiltersPageComponent,
ContentsPageComponent, ContentsPageComponent,
ContentsSelectorComponent, ContentsSelectorComponent,
@ -39,6 +40,7 @@ import {
FieldEditorComponent, FieldEditorComponent,
FieldLanguagesComponent, FieldLanguagesComponent,
PreviewButtonComponent, PreviewButtonComponent,
ReferenceItemComponent,
ReferencesEditorComponent, ReferencesEditorComponent,
SchemasPageComponent SchemasPageComponent
} from './declarations'; } from './declarations';
@ -110,10 +112,11 @@ const routes: Routes = [
ArrayItemComponent, ArrayItemComponent,
AssetsEditorComponent, AssetsEditorComponent,
CommentsPageComponent, CommentsPageComponent,
ContentComponent,
ContentFieldComponent, ContentFieldComponent,
ContentHistoryPageComponent, ContentHistoryPageComponent,
ContentComponent,
ContentPageComponent, ContentPageComponent,
ContentSelectorItemComponent,
ContentsFiltersPageComponent, ContentsFiltersPageComponent,
ContentsPageComponent, ContentsPageComponent,
ContentsSelectorComponent, ContentsSelectorComponent,
@ -124,6 +127,7 @@ const routes: Routes = [
FieldEditorComponent, FieldEditorComponent,
FieldLanguagesComponent, FieldLanguagesComponent,
PreviewButtonComponent, PreviewButtonComponent,
ReferenceItemComponent,
ReferencesEditorComponent, ReferencesEditorComponent,
SchemasPageComponent SchemasPageComponent
] ]

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

@ -55,7 +55,7 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: AppLanguageDto[]; public languages: ReadonlyArray<AppLanguageDto>;
public selectedFormControl: AbstractControl; public selectedFormControl: AbstractControl;
public selectedFormControlCompare?: AbstractControl; public selectedFormControlCompare?: AbstractControl;
@ -145,7 +145,7 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
if (masterValue) { if (masterValue) {
if (this.showAllControls) { if (this.showAllControls) {
for (let language of this.languages) { for (const language of this.languages) {
if (!language.isMaster) { if (!language.isMaster) {
this.translateValue(masterValue, masterCode, language.iso2Code); this.translateValue(masterValue, masterCode, language.iso2Code);
} }

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

@ -35,7 +35,7 @@ export class ContentHistoryPageComponent {
if (channelPath) { if (channelPath) {
const params = allParams(this.route); const params = allParams(this.route);
for (let key in params) { for (const key in params) {
if (params.hasOwnProperty(key)) { if (params.hasOwnProperty(key)) {
const value = params[key]; const value = params[key];
@ -47,7 +47,7 @@ export class ContentHistoryPageComponent {
return channelPath; return channelPath;
} }
public events: Observable<HistoryEventDto[]> = public events: Observable<ReadonlyArray<HistoryEventDto>> =
merge( merge(
timer(0, 10000), timer(0, 10000),
this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)) this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000))

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

@ -112,7 +112,7 @@
[fieldForm]="contentForm.form.get(field.name)" [fieldForm]="contentForm.form.get(field.name)"
[fieldFormCompare]="contentFormCompare?.form.get(field.name)" [fieldFormCompare]="contentFormCompare?.form.get(field.name)"
[schema]="schema" [schema]="schema"
[languages]="languages.mutableValues" [languages]="languages"
[(language)]="language"> [(language)]="language">
</sqx-content-field> </sqx-content-field>
</ng-container> </ng-container>

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

@ -25,7 +25,6 @@ import {
EditContentForm, EditContentForm,
fadeAnimation, fadeAnimation,
FieldDto, FieldDto,
ImmutableArray,
LanguagesState, LanguagesState,
MessageBus, MessageBus,
ModalModel, ModalModel,
@ -61,7 +60,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
public dropdown = new ModalModel(); public dropdown = new ModalModel();
public language: AppLanguageDto; public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>; public languages: ReadonlyArray<AppLanguageDto>;
public trackByFieldFn: Function; public trackByFieldFn: Function;
@ -92,7 +91,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
this.languagesState.languages this.languagesState.languages
.subscribe(languages => { .subscribe(languages => {
this.languages = languages.map(x => x.language); this.languages = languages.map(x => x.language);
this.language = this.languages.at(0); this.language = this.languages[0];
})); }));
this.own( this.own(

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

@ -48,5 +48,5 @@ export class FieldLanguagesComponent {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: AppLanguageDto[]; public languages: ReadonlyArray<AppLanguageDto>;
} }

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

@ -24,7 +24,7 @@
</sqx-search-form> </sqx-search-form>
</div> </div>
<div class="col-auto pl-1" *ngIf="languages.length > 1"> <div class="col-auto pl-1" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.mutableValues"></sqx-language-selector> <sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div> </div>
<div class="col-auto pl-1"> <div class="col-auto pl-1">
<button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)" [disabled]="(contentsState.canCreateAny | async) === false"> <button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)" [disabled]="(contentsState.canCreateAny | async) === false">

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

@ -12,7 +12,6 @@ import {
AppLanguageDto, AppLanguageDto,
ContentDto, ContentDto,
ContentsState, ContentsState,
ImmutableArray,
LanguagesState, LanguagesState,
ModalModel, ModalModel,
Queries, Queries,
@ -42,10 +41,10 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public selectionCount = 0; public selectionCount = 0;
public selectionCanDelete = false; public selectionCanDelete = false;
public nextStatuses: string[] = []; public nextStatuses: ReadonlyArray<string> = [];
public language: AppLanguageDto; public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>; public languages: ReadonlyArray<AppLanguageDto>;
public queryModel: QueryModel; public queryModel: QueryModel;
public queries: Queries; public queries: Queries;
@ -96,7 +95,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.languagesState.languages this.languagesState.languages
.subscribe(languages => { .subscribe(languages => {
this.languages = languages.map(x => x.language); this.languages = languages.map(x => x.language);
this.language = this.languages.at(0); this.language = this.languages[0];
this.updateModel(); this.updateModel();
})); }));
@ -126,7 +125,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.contentsState.create(content.dataDraft, false); this.contentsState.create(content.dataDraft, false);
} }
private changeContentItems(contents: ContentDto[], action: string) { private changeContentItems(contents: ReadonlyArray<ContentDto>, action: string) {
if (contents.length === 0) { if (contents.length === 0) {
return; return;
} }
@ -161,7 +160,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
} }
private selectItems(predicate?: (content: ContentDto) => boolean) { private selectItems(predicate?: (content: ContentDto) => boolean) {
return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c))); return this.contentsState.snapshot.contents.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
} }
public selectItem(content: ContentDto, isSelected: boolean) { public selectItem(content: ContentDto, isSelected: boolean) {
@ -180,7 +179,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.selectedItems = {}; this.selectedItems = {};
if (isSelected) { if (isSelected) {
for (let content of this.contentsState.snapshot.contents.values) { for (let content of this.contentsState.snapshot.contents) {
this.selectedItems[content.id] = true; this.selectedItems[content.id] = true;
} }
} }
@ -188,7 +187,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.updateSelectionSummary(); this.updateSelectionSummary();
} }
public trackByContent(content: ContentDto): string { public trackByContent(index: number, content: ContentDto): string {
return content.id; return content.id;
} }
@ -199,17 +198,17 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
const allActions = {}; const allActions = {};
for (let content of this.contentsState.snapshot.contents.values) { for (let content of this.contentsState.snapshot.contents) {
for (let info of content.statusUpdates) { for (const info of content.statusUpdates) {
allActions[info.status] = info.color; allActions[info.status] = info.color;
} }
} }
for (let content of this.contentsState.snapshot.contents.values) { for (let content of this.contentsState.snapshot.contents) {
if (this.selectedItems[content.id]) { if (this.selectedItems[content.id]) {
this.selectionCount++; this.selectionCount++;
for (let action in allActions) { for (const action in allActions) {
if (!content.statusUpdates) { if (!content.statusUpdates) {
delete allActions[action]; delete allActions[action];
} }
@ -234,7 +233,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
private updateModel() { private updateModel() {
if (this.schema && this.languages) { if (this.schema && this.languages) {
this.queryModel = queryModelFromSchema(this.schema, this.languages.values, this.contentsState.snapshot.statuses); this.queryModel = queryModelFromSchema(this.schema, this.languages, this.contentsState.snapshot.statuses);
} }
} }
} }

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

@ -39,7 +39,7 @@ export class ArrayEditorComponent extends StatefulComponent<State> {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: AppLanguageDto[]; public languages: ReadonlyArray<AppLanguageDto>;
@Input() @Input()
public arrayControl: FormArray; public arrayControl: FormArray;
@ -62,7 +62,7 @@ export class ArrayEditorComponent extends StatefulComponent<State> {
this.form.arrayItemInsert(this.field, this.language, value); this.form.arrayItemInsert(this.field, this.language, value);
} }
public sort(controls: AbstractControl[]) { public sort(controls: ReadonlyArray<AbstractControl>) {
for (let i = 0; i < controls.length; i++) { for (let i = 0; i < controls.length; i++) {
this.arrayControl.setControl(i, controls[i]); this.arrayControl.setControl(i, controls[i]);
} }

4
src/Squidex/app/features/content/shared/array-item.component.ts

@ -67,11 +67,11 @@ export class ArrayItemComponent implements OnChanges {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: AppLanguageDto[]; public languages: ReadonlyArray<AppLanguageDto>;
public isInvalid: Observable<boolean>; public isInvalid: Observable<boolean>;
public fieldControls: { field: FieldDto, control: AbstractControl }[]; public fieldControls: ReadonlyArray<{ field: FieldDto, control: AbstractControl }>;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['itemForm']) { if (changes['itemForm']) {

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

@ -42,7 +42,7 @@
</sqx-asset> </sqx-asset>
<div <div
[sqxSortModel]="snapshot.assets.mutableValues" [sqxSortModel]="snapshot.assets"
(sqxSort)="sortAssets($event)"> (sqxSort)="sortAssets($event)">
<div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset"> <div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset">
<sqx-asset [asset]="asset" removeMode="true" <sqx-asset [asset]="asset" removeMode="true"

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

@ -13,7 +13,6 @@ import {
AssetDto, AssetDto,
AssetsService, AssetsService,
DialogModel, DialogModel,
ImmutableArray,
LocalStoreService, LocalStoreService,
MessageBus, MessageBus,
StatefulControlComponent, StatefulControlComponent,
@ -33,9 +32,9 @@ class AssetUpdated {
} }
interface State { interface State {
assetFiles: ImmutableArray<File>; assetFiles: ReadonlyArray<File>;
assets: ImmutableArray<AssetDto>; assets: ReadonlyArray<AssetDto>;
isListView: boolean; isListView: boolean;
} }
@ -47,6 +46,7 @@ interface State {
providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR], providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
// tslint:disable-next-line: readonly-array
export class AssetsEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit { export class AssetsEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit {
@Input() @Input()
public isCompact = false; public isCompact = false;
@ -60,30 +60,30 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
private readonly messageBus: MessageBus private readonly messageBus: MessageBus
) { ) {
super(changeDetector, { super(changeDetector, {
assets: ImmutableArray.empty(), assets: [],
assetFiles: ImmutableArray.empty(), assetFiles: [],
isListView: localStore.getBoolean('squidex.assets.list-view') isListView: localStore.getBoolean('squidex.assets.list-view')
}); });
} }
public writeValue(obj: any) { public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) { if (Types.isArrayOfString(obj)) {
if (!Types.isEquals(obj, this.snapshot.assets.map(x => x.id).values)) { if (!Types.isEquals(obj, this.snapshot.assets.map(x => x.id))) {
const assetIds: string[] = obj; const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj) this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)
.subscribe(dtos => { .subscribe(dtos => {
this.setAssets(ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a))); this.setAssets(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a));
if (this.snapshot.assets.length !== assetIds.length) { if (this.snapshot.assets.length !== assetIds.length) {
this.updateValue(); this.updateValue();
} }
}, () => { }, () => {
this.setAssets(ImmutableArray.empty()); this.setAssets([]);
}); });
} }
} else { } else {
this.setAssets(ImmutableArray.empty()); this.setAssets([]);
} }
} }
@ -101,18 +101,18 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
})); }));
} }
public setAssets(assets: ImmutableArray<AssetDto>) { public setAssets(assets: ReadonlyArray<AssetDto>) {
this.next(s => ({ ...s, assets })); this.next(s => ({ ...s, assets }));
} }
public addFiles(files: File[]) { public addFiles(files: ReadonlyArray<File>) {
for (let file of files) { for (const file of files) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) })); this.next(s => ({ ...s, assetFiles: [file, ...s.assetFiles] }));
} }
} }
public selectAssets(assets: AssetDto[]) { public selectAssets(assets: ReadonlyArray<AssetDto>) {
this.setAssets(this.snapshot.assets.push(...assets)); this.setAssets([...this.snapshot.assets, ...assets]);
if (assets.length > 0) { if (assets.length > 0) {
this.updateValue(); this.updateValue();
@ -125,17 +125,17 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
if (asset && file) { if (asset && file) {
this.next(s => ({ this.next(s => ({
...s, ...s,
assetFiles: s.assetFiles.remove(file), assetFiles: s.assetFiles.removed(file),
assets: s.assets.pushFront(asset) assets: [asset, ...s.assets]
})); }));
this.updateValue(); this.updateValue();
} }
} }
public sortAssets(assets: AssetDto[]) { public sortAssets(assets: ReadonlyArray<AssetDto>) {
if (assets) { if (assets) {
this.setAssets(ImmutableArray.of(assets)); this.setAssets(assets);
this.updateValue(); this.updateValue();
} }
@ -143,14 +143,14 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
public removeLoadedAsset(asset: AssetDto) { public removeLoadedAsset(asset: AssetDto) {
if (asset) { if (asset) {
this.setAssets(this.snapshot.assets.remove(asset)); this.setAssets(this.snapshot.assets.removed(asset));
this.updateValue(); this.updateValue();
} }
} }
public removeLoadingAsset(file: File) { public removeLoadingAsset(file: File) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.remove(file) })); this.next(s => ({ ...s, assetFiles: s.assetFiles.removed(file) }));
} }
public changeView(isListView: boolean) { public changeView(isListView: boolean) {
@ -160,14 +160,15 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
} }
private updateValue() { private updateValue() {
let ids: string[] | null = this.snapshot.assets.values.map(x => x.id); const ids = this.snapshot.assets.map(x => x.id);
if (ids.length === 0) { if (ids.length === 0) {
ids = null; this.callChange(null);
} else {
this.callChange(ids);
} }
this.callTouched(); this.callTouched();
this.callChange(ids);
} }
public trackByAsset(index: number, asset: AssetDto) { public trackByAsset(index: number, asset: AssetDto) {

102
src/Squidex/app/features/content/shared/content-selector-item.component.ts

@ -0,0 +1,102 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import {
AppLanguageDto,
ContentDto,
getContentValue,
RootFieldDto
} 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 class="cell-user">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-auto cell-content" *ngFor="let value of values">
<sqx-content-value [value]="value"></sqx-content-value>
</td>
<td class="cell-time">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
</sqx-content-status>
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
</tr>
<tr class="spacer"></tr>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentSelectorItemComponent implements OnChanges {
@Output()
public selectedChange = new EventEmitter<boolean>();
@Input()
public selected = false;
@Input()
public selectable = true;
@Input()
public language: AppLanguageDto;
@Input()
public fields: ReadonlyArray<RootFieldDto>;
@Input('sqxContentSelectorItem')
public content: ContentDto;
public values: ReadonlyArray<any> = [];
public ngOnChanges(changes: SimpleChanges) {
if (changes['content'] || changes['language']) {
this.updateValues();
}
}
public toggle() {
if (this.selectable) {
this.emitSelectedChange(!this.selected);
}
}
public emitSelectedChange(isSelected: boolean) {
this.selectedChange.emit(isSelected);
}
private updateValues() {
const values = [];
for (const field of this.fields) {
const { formatted } = getContentValue(this.content, this.language, field);
values.push(formatted);
}
this.values = values;
}
}

10
src/Squidex/app/features/content/shared/content-value-editor.component.ts

@ -16,20 +16,20 @@ import { FieldDto } from '@app/shared';
<div [formGroup]="form"> <div [formGroup]="form">
<ng-container [ngSwitch]="field.properties.fieldType"> <ng-container [ngSwitch]="field.properties.fieldType">
<ng-container *ngSwitchCase="'Number'"> <ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'"> <ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name"> <select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select> </select>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'String'"> <ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'"> <ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</ng-container> </ng-container>
@ -39,13 +39,13 @@ import { FieldDto } from '@app/shared';
<ng-container *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name"> <select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select> </select>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Boolean'"> <ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Toggle'"> <ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name" [threeStates]="!field.properties.isRequired"></sqx-toggle> <sqx-toggle [formControlName]="field.name" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</ng-container> </ng-container>

35
src/Squidex/app/features/content/shared/content.component.html

@ -1,18 +1,11 @@
<tr [routerLink]="link" routerLinkActive="active"> <tr [routerLink]="link">
<td class="cell-select" sqxStopClick> <td class="cell-select" sqxStopClick>
<ng-container *ngIf="!isReference; else referenceTemplate"> <input type="checkbox" class="form-check"
<input type="checkbox" class="form-check" [ngModel]="selected"
[disabled]="!selectable" (ngModelChange)="emitSelectedChange($event)" />
[ngModel]="selected || !selectable"
(ngModelChange)="emitSelectedChange($event)" />
</ng-container>
<ng-template #referenceTemplate>
<i class="icon-drag2 drag-handle"></i>
</ng-template>
</td> </td>
<td class="cell-actions cell-actions-left" *ngIf="!isReadOnly && !isDirty" sqxStopClick> <td class="cell-actions cell-actions-left" *ngIf="!isDirty" sqxStopClick>
<div class="dropdown dropdown-options" *ngIf="content"> <div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions> <button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i> <i class="icon-dots"></i>
@ -79,23 +72,5 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small> <small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td> </td>
<td class="cell-actions" *ngIf="isReference" [sqxStopClick]="isDirty">
<div class="reference-edit">
<button type="button" class="btn btn-text-secondary">
<i class="icon-dots"></i>
</button>
<div class="reference-menu">
<a class="btn btn-text-secondary" [routerLink]="['../..', schema.name, content.id]">
<i class="icon-pencil"></i>
</a>
<button type="button" class="btn btn-text-secondary" (click)="emitDelete()">
<i class="icon-close"></i>
</button>
</div>
</div>
</td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>

24
src/Squidex/app/features/content/shared/content.component.scss

@ -1,24 +1,2 @@
@import '_vars'; @import '_vars';
@import '_mixins'; @import '_mixins';
.reference-edit {
& {
position: relative;
}
&:hover {
.reference-menu {
display: block;
}
}
.reference-menu {
@include absolute(0, -.25rem, auto, auto);
display: none;
padding-left: 2rem;
min-height: 2.4rem;
max-height: 2.4rem;
white-space: nowrap;
background: $color-table-background;
}
}

25
src/Squidex/app/features/content/shared/content.component.ts

@ -47,9 +47,6 @@ export class ContentComponent implements OnChanges {
@Input() @Input()
public selected = false; public selected = false;
@Input()
public selectable = true;
@Input() @Input()
public language: AppLanguageDto; public language: AppLanguageDto;
@ -57,17 +54,11 @@ export class ContentComponent implements OnChanges {
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@Input() @Input()
public schemaFields: RootFieldDto[]; public schemaFields: ReadonlyArray<RootFieldDto>;
@Input() @Input()
public canClone: boolean; public canClone: boolean;
@Input()
public isReadOnly = false;
@Input()
public isReference = false;
@Input() @Input()
public isCompact = false; public isCompact = false;
@ -84,7 +75,7 @@ export class ContentComponent implements OnChanges {
public dropdown = new ModalModel(); public dropdown = new ModalModel();
public values: any[] = []; public values: ReadonlyArray<any> = [];
public get isDirty() { public get isDirty() {
return this.patchForm && this.patchForm.form.dirty; return this.patchForm && this.patchForm.form.dirty;
@ -99,7 +90,7 @@ export class ContentComponent implements OnChanges {
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['content']) { if (changes['content']) {
this.patchAllowed = !this.isReadOnly && this.content.canUpdate; this.patchAllowed = this.content.canUpdate;
} }
if (changes['schema'] || changes['language']) { if (changes['schema'] || changes['language']) {
@ -157,12 +148,12 @@ export class ContentComponent implements OnChanges {
} }
private updateValues() { private updateValues() {
this.values = []; const values = [];
for (let field of this.schemaFields) { for (const field of this.schemaFields) {
const { value, formatted } = getContentValue(this.content, this.language, field); const { value, formatted } = getContentValue(this.content, this.language, field);
this.values.push(formatted); values.push(formatted);
if (this.patchForm) { if (this.patchForm) {
const formControl = this.patchForm.form.controls[field.name]; const formControl = this.patchForm.form.controls[field.name];
@ -172,9 +163,11 @@ export class ContentComponent implements OnChanges {
} }
} }
} }
this.values = values;
} }
public trackByField(field: FieldDto) { public trackByField(index: number, field: FieldDto) {
return field.fieldId + this.schema.id; return field.fieldId + this.schema.id;
} }
} }

148
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -1,84 +1,96 @@
<sqx-modal-dialog (close)="emitComplete()" large="true" fullHeight="true" contentClass="grid"> <sqx-modal-dialog (close)="emitComplete()" large="true" fullHeight="true" contentClass="grid">
<ng-container title> <ng-container title>
Select contents <div class="row">
<div class="col-selector">
<select class="form-control form-control-dark"
[ngModel]="schema?.id"
(ngModelChange)="selectSchema($event)">
<option *ngFor="let schema of schemas" [ngValue]="schema.id">
Select {{schema.displayName}}
</option>
</select>
</div>
</div>
</ng-container> </ng-container>
<ng-container tabs> <ng-container tabs>
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-auto offset-lg-4"> <ng-container *ngIf="schema">
<button type="button" class="btn btn-text-secondary" (click)="reload()"> <div class="col-auto">
<i class="icon-reset"></i> Refresh <button type="button" class="btn btn-text-secondary" (click)="reload()">
</button> <i class="icon-reset"></i> Refresh
</div> </button>
<div class="col pl-1"> </div>
<sqx-search-form formClass="form" placeholder="Search for content" <div class="col pl-1">
[query]="contentsState.contentsQuery | async" <sqx-search-form formClass="form" placeholder="Search for content"
[queryModel]="queryModel" [query]="contentsState.contentsQuery | async"
(queryChange)="search($event)"> [queryModel]="queryModel"
</sqx-search-form> (queryChange)="search($event)">
</div> </sqx-search-form>
</div>
<div class="-auto pl-1" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages"></sqx-language-selector> <div class="-auto pl-1" *ngIf="languages.length > 1">
</div> <sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
</ng-container>
</div> </div>
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="grid-header"> <ng-container *ngIf="schema">
<table class="table table-items table-fixed" [style.minWidth]="minWidth" #header> <div class="grid-header">
<thead> <table class="table table-items table-fixed" [style.minWidth]="minWidth" #header>
<tr> <thead>
<th class="cell-select"> <tr>
<input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)" /> <th class="cell-select">
</th> <input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)" />
<th class="cell-user"> </th>
<sqx-table-header text="By"></sqx-table-header> <th class="cell-user">
</th> <sqx-table-header text="By"></sqx-table-header>
<th class="cell-content" *ngFor="let field of schema.referenceFields"> </th>
<sqx-table-header [text]="field.displayName" <th class="cell-content" *ngFor="let field of schema.referenceFields">
[sortable]="field.properties.isSortable" <sqx-table-header [text]="field.displayName"
[field]="field" [sortable]="field.properties.isSortable"
[query]="contentsState.contentsQuery | async" [field]="field"
(queryChange)="search($event)" [query]="contentsState.contentsQuery | async"
[language]="language"> (queryChange)="search($event)"
</sqx-table-header> [language]="language">
</th> </sqx-table-header>
<th class="cell-time"> </th>
<sqx-table-header text="Updated" <th class="cell-time">
[sortable]="true" <sqx-table-header text="Updated"
[sortable]="field.properties.isSortable" [sortable]="true"
[field]="'lastModified'" [sortable]="field.properties.isSortable"
[query]="contentsState.contentsQuery | async" [field]="'lastModified'"
(queryChange)="search($event)" [query]="contentsState.contentsQuery | async"
[language]="language"> (queryChange)="search($event)"
</sqx-table-header> [language]="language">
</th> </sqx-table-header>
</tr> </th>
</thead> </tr>
</table> </thead>
</div>
<div class="grid-content" [sqxSyncScrolling]="header">
<div class="table-container" sqxIgnoreScrollbar>
<table class="table table-items table-fixed" [style.minWidth]="minWidth" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"
[sqxContent]="content"
[selectable]="!isItemAlreadySelected(content)"
[selected]="isItemSelected(content)"
(selectedChange)="selectContent(content)"
[language]="language"
[schema]="schema"
[schemaFields]="schema.referenceFields"
isReadOnly="true">
</tbody>
</table> </table>
</div> </div>
</div>
<div class="grid-footer"> <div class="grid-content" [sqxSyncScrolling]="header">
<sqx-pager [pager]="contentsState.contentsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager> <div class="table-container" sqxIgnoreScrollbar>
</div> <table class="table table-items table-fixed" [style.minWidth]="minWidth" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"
[sqxContentSelectorItem]="content"
[fields]="schema.referenceFields"
[selectable]="!isItemAlreadySelected(content)"
[selected]="isItemSelected(content)"
(selectedChange)="selectContent(content)"
[language]="language">
</tbody>
</table>
</div>
</div>
<div class="grid-footer">
<sqx-pager [pager]="contentsState.contentsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager>
</div>
</ng-container>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>

11
src/Squidex/app/features/content/shared/contents-selector.component.scss

@ -11,6 +11,17 @@
} }
} }
.col-selector {
position: relative;
.form-control {
@include absolute(-.4rem, auto, auto, 0);
max-width: 300px;
min-width: 300px;
color: $color-dark-foreground;
}
}
.table-container { .table-container {
display: inline-block; display: inline-block;
padding: 0; padding: 0;

69
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { import {
ContentDto, ContentDto,
@ -15,7 +15,10 @@ import {
QueryModel, QueryModel,
queryModelFromSchema, queryModelFromSchema,
ResourceOwner, ResourceOwner,
SchemaDetailsDto SchemaDetailsDto,
SchemaDto,
SchemasState,
Types
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -28,22 +31,25 @@ import {
}) })
export class ContentsSelectorComponent extends ResourceOwner implements OnInit { export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
@Output() @Output()
public select = new EventEmitter<ContentDto[]>(); public select = new EventEmitter<ReadonlyArray<ContentDto>>();
@Input()
public schemaIds: ReadonlyArray<string>;
@Input() @Input()
public language: LanguageDto; public language: LanguageDto;
@Input() @Input()
public languages: LanguageDto[]; public languages: ReadonlyArray<LanguageDto>;
@Input() @Input()
public allowDuplicates: boolean; public allowDuplicates: boolean;
@Input() @Input()
public alreadySelected: ContentDto[]; public alreadySelected: ReadonlyArray<ContentDto>;
@Input()
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public schemas: ReadonlyArray<SchemaDto> = [];
public queryModel: QueryModel; public queryModel: QueryModel;
@ -54,22 +60,51 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
public minWidth: string; public minWidth: string;
constructor( constructor(
public readonly contentsState: ManualContentsState public readonly contentsState: ManualContentsState,
public readonly schemasState: SchemasState,
private readonly changeDetector: ChangeDetectorRef
) { ) {
super(); super();
} }
public ngOnInit() { public ngOnInit() {
this.minWidth = `${200 + (200 * this.schema.referenceFields.length)}px`;
this.own( this.own(
this.contentsState.statuses this.contentsState.statuses
.subscribe(() => { .subscribe(() => {
this.updateModel(); this.updateModel();
})); }));
this.contentsState.schema = this.schema; this.schemas = this.schemasState.snapshot.schemas;
this.contentsState.load();
if (this.schemaIds && this.schemaIds.length > 0) {
this.schemas = this.schemas.filter(x => this.schemaIds.indexOf(x.id) >= 0);
}
this.selectSchema(this.schemas[0]);
this.changeDetector.detectChanges();
}
public selectSchema(selected: string | SchemaDto) {
if (Types.is(selected, SchemaDto)) {
selected = selected.id;
}
this.schemasState.loadSchema(selected, true)
.subscribe(schema => {
if (schema) {
this.schema = schema;
this.minWidth = `${200 + (200 * schema.referenceFields.length)}px`;
this.contentsState.schema = schema;
this.contentsState.load();
this.updateModel();
this.changeDetector.detectChanges();
}
});
} }
public reload() { public reload() {
@ -112,8 +147,10 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
this.selectedItems = {}; this.selectedItems = {};
if (isSelected) { if (isSelected) {
for (let content of this.contentsState.snapshot.contents.values) { for (const content of this.contentsState.snapshot.contents) {
this.selectedItems[content.id] = content; if (!this.isItemAlreadySelected(content)) {
this.selectedItems[content.id] = content;
}
} }
} }
@ -136,10 +173,12 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
} }
private updateModel() { private updateModel() {
this.queryModel = queryModelFromSchema(this.schema, this.languages, this.contentsState.snapshot.statuses); if (this.schema) {
this.queryModel = queryModelFromSchema(this.schema, this.languages, this.contentsState.snapshot.statuses);
}
} }
public trackByContent(content: ContentDto): string { public trackByContent(index: number, content: ContentDto): string {
return content.id; return content.id;
} }
} }

34
src/Squidex/app/features/content/shared/field-editor.component.html

@ -34,7 +34,7 @@
<sqx-assets-editor [formControl]="control" [isCompact]="isCompact"></sqx-assets-editor> <sqx-assets-editor [formControl]="control" [isCompact]="isCompact"></sqx-assets-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Boolean'"> <ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Toggle'"> <ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="control" [threeStates]="!field.properties.isRequired"></sqx-toggle> <sqx-toggle [formControl]="control" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</ng-container> </ng-container>
@ -44,7 +44,7 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'DateTime'"> <ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="control"></sqx-date-time-editor> <sqx-date-time-editor enforceTime="true" [mode]="field.rawProperties.editor" [formControl]="control"></sqx-date-time-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Geolocation'"> <ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [isCompact]="isCompact" [formControl]="control"></sqx-geolocation-editor> <sqx-geolocation-editor [isCompact]="isCompact" [formControl]="control"></sqx-geolocation-editor>
@ -53,21 +53,21 @@
<sqx-json-editor [formControl]="control"></sqx-json-editor> <sqx-json-editor [formControl]="control"></sqx-json-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Number'"> <ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'"> <ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="control" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="number" [formControl]="control" [placeholder]="field.displayPlaceholder" />
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Stars'"> <ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="control" [maximumStars]="field.properties['maxValue']"></sqx-stars> <sqx-stars [formControl]="control" [maximumStars]="field.rawProperties.maxValue"></sqx-stars>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="control"> <select class="form-control" [formControl]="control">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select> </select>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Radio'"> <ng-container *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']"> <div class="form-check form-check-inline" *ngFor="let value of field.rawProperties.allowedValues">
<input class="form-check-input" type="radio" [value]="value" [formControl]="control" [name]="uniqueId" /> <input class="form-check-input" type="radio" [value]="value" [formControl]="control" [name]="uniqueId" />
<label class="form-check-label"> <label class="form-check-label">
{{value}} {{value}}
@ -77,14 +77,14 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'References'"> <ng-container *ngSwitchCase="'References'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'List'"> <ng-container *ngSwitchCase="'List'">
<sqx-references-editor <sqx-references-editor
[formControl]="control" [formControl]="control"
[allowDuplicates]="field.properties['allowDuplicated']" [allowDuplicates]="field.rawProperties.allowDuplicated"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schemaId]="field.properties['schemaId']" [schemaIds]="field.rawProperties.schemaIds"
[isCompact]="isCompact"> [isCompact]="isCompact">
</sqx-references-editor> </sqx-references-editor>
</ng-container> </ng-container>
@ -92,13 +92,13 @@
<sqx-references-dropdown <sqx-references-dropdown
[formControl]="control" [formControl]="control"
[language]="language" [language]="language"
[schemaId]="field.properties['schemaId']"> [schemaId]="field.rawProperties.singleId">
</sqx-references-dropdown> </sqx-references-dropdown>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'String'"> <ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'"> <ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="control" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="text" [formControl]="control" [placeholder]="field.displayPlaceholder" />
</ng-container> </ng-container>
@ -120,11 +120,11 @@
<ng-container *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="control"> <select class="form-control" [formControl]="control">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select> </select>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Radio'"> <ng-container *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']"> <div class="form-check form-check-inline" *ngFor="let value of field.rawProperties.allowedValues">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="control" [name]="uniqueId" /> <input class="form-check-input" type="radio" value="{{value}}" [formControl]="control" [name]="uniqueId" />
<label class="form-check-label"> <label class="form-check-label">
{{value}} {{value}}
@ -137,16 +137,16 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Tags'"> <ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Tags'"> <ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="control" [placeholder]="field.displayPlaceholder" [suggestions]="field.properties['allowedValues']"></sqx-tag-editor> <sqx-tag-editor [formControl]="control" [placeholder]="field.displayPlaceholder" [suggestions]="field.rawProperties.allowedValues"></sqx-tag-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Checkboxes'"> <ng-container *ngSwitchCase="'Checkboxes'">
<sqx-checkbox-group [formControl]="control" [values]="field.properties['allowedValues']"></sqx-checkbox-group> <sqx-checkbox-group [formControl]="control" [values]="field.rawProperties.allowedValues"></sqx-checkbox-group>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'Dropdown'">
<select multiple class="form-control" [formControl]="control"> <select multiple class="form-control" [formControl]="control">
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select> </select>
</ng-container> </ng-container>
</ng-container> </ng-container>

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

@ -37,7 +37,7 @@ export class FieldEditorComponent {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: AppLanguageDto[]; public languages: ReadonlyArray<AppLanguageDto>;
@Input() @Input()
public isCompact = false; public isCompact = false;

6
src/Squidex/app/features/content/shared/preview-button.component.ts

@ -20,7 +20,7 @@ import {
interface State { interface State {
selectedName?: string; selectedName?: string;
alternativeNames: string[]; alternativeNames: ReadonlyArray<string>;
} }
@Component({ @Component({
@ -77,9 +77,7 @@ export class PreviewButtonComponent extends StatefulComponent<State> implements
const keys = Object.keys(this.schema.previewUrls); const keys = Object.keys(this.schema.previewUrls);
state.selectedName = selectedName; state.selectedName = selectedName;
state.alternativeNames = keys.removed(s.selectedName).sorted();
state.alternativeNames = keys.filter(x => x !== s.selectedName);
state.alternativeNames.sort();
this.localStore.set(this.configKey(), selectedName); this.localStore.set(this.configKey(), selectedName);

28
src/Squidex/app/features/content/shared/reference-item.component.scss

@ -0,0 +1,28 @@
@import '_vars';
@import '_mixins';
.reference-edit {
& {
position: relative;
}
&:hover {
.reference-menu {
display: block;
}
}
.reference-menu {
@include absolute(0, -.25rem, auto, auto);
display: none;
min-height: 2.4rem;
max-height: 2.4rem;
white-space: nowrap;
background: $color-table-background;
}
}
.badge {
@include truncate;
display: inline-block !important;
}

104
src/Squidex/app/features/content/shared/reference-item.component.ts

@ -0,0 +1,104 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import {
AppLanguageDto,
ContentDto,
getContentValue
} from '@app/shared';
/* tslint:disable:component-selector */
@Component({
selector: '[sqxReferenceItem]',
styleUrls: ['./reference-item.component.scss'],
template: `
<tr>
<td class="cell-select">
<i class="icon-drag2 drag-handle"></i>
</td>
<td class="cell-user" *ngIf="!isCompact">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-auto cell-content" *ngFor="let value of values">
<sqx-content-value [value]="value"></sqx-content-value>
</td>
<td class="cell-label" *ngIf="!isCompact">
<span class="badge badge-pill truncate-inline badge-primary">{{content.schemaDisplayName}}</span>
</td>
<td class="cell-actions">
<div class="reference-edit">
<button type="button" class="btn btn-text-secondary">
<i class="icon-dots"></i>
</button>
<div class="reference-menu">
<a class="btn btn-text-secondary" [routerLink]="['../..', content.schemaName, content.id]">
<i class="icon-pencil"></i>
</a>
<button type="button" class="btn btn-text-secondary" (click)="emitDelete()">
<i class="icon-close"></i>
</button>
</div>
</div>
</td>
</tr>
<tr class="spacer"></tr>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReferenceItemComponent implements OnChanges {
@Output()
public delete = new EventEmitter();
@Input()
public language: AppLanguageDto;
@Input()
public isCompact = false;
@Input()
public columnCount = 0;
@Input('sqxReferenceItem')
public content: ContentDto;
public values: ReadonlyArray<any> = [];
public ngOnChanges(changes: SimpleChanges) {
this.updateValues();
}
public emitDelete() {
this.delete.emit();
}
private updateValues() {
const values = [];
for (let i = 0; i < this.columnCount; i++) {
const field = this.content.referenceFields[i];
if (field) {
const { formatted } = getContentValue(this.content, this.language, field);
values.push(formatted);
} else {
values.push('');
}
}
this.values = values;
}
}

19
src/Squidex/app/features/content/shared/references-editor.component.html

@ -1,30 +1,23 @@
<div class="references-container" [class.disabled]="snapshot.isDisabled"> <div class="references-container" [class.disabled]="snapshot.isDisabled">
<ng-container *ngIf="snapshot.schema; let schema"> <ng-container>
<div class="drop-area-container"> <div class="drop-area-container">
<div class="drop-area" (click)="selectorDialog.show()"> <div class="drop-area" (click)="selectorDialog.show()">
Click here to link content items. Click here to link content items.
</div> </div>
</div> </div>
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="schema && snapshot.contentItems && snapshot.contentItems.length > 0" <table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems.mutableValues" [sqxSortModel]="snapshot.contentItems"
(sqxSort)="sort($event)"> (sqxSort)="sort($event)">
<tbody *ngFor="let content of snapshot.contentItems; trackBy: trackByContent" <tbody *ngFor="let content of snapshot.contentItems; trackBy: trackByContent"
[sqxContent]="content" [sqxReferenceItem]="content"
[language]="language" [language]="language"
[isReadOnly]="true"
[isReference]="true"
[isCompact]="isCompact" [isCompact]="isCompact"
[schema]="schema" [columnCount]="snapshot.columnCount"
[schemaFields]="schema.referenceFields"
(delete)="remove(content)"> (delete)="remove(content)">
</tbody> </tbody>
</table> </table>
</ng-container> </ng-container>
<div class="invalid" *ngIf="snapshot.schemaInvalid">
Schema not found or not configured yet.
</div>
</div> </div>
<ng-container *sqxModal="selectorDialog;closeAuto:false"> <ng-container *sqxModal="selectorDialog;closeAuto:false">
@ -33,7 +26,7 @@
[alreadySelected]="snapshot.contentItems" [alreadySelected]="snapshot.contentItems"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schema]="snapshot.schema" [schemaIds]="schemaIds"
(select)="select($event)"> (select)="select($event)">
</sqx-contents-selector> </sqx-contents-selector>
</ng-container> </ng-container>

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
@ -14,10 +14,6 @@ import {
ContentDto, ContentDto,
ContentsService, ContentsService,
DialogModel, DialogModel,
ImmutableArray,
MathHelper,
SchemaDetailsDto,
SchemasService,
StatefulControlComponent, StatefulControlComponent,
Types Types
} from '@app/shared'; } from '@app/shared';
@ -27,10 +23,9 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
}; };
interface State { interface State {
schema?: SchemaDetailsDto | null; contentItems: ReadonlyArray<ContentDto>;
schemaInvalid: boolean;
contentItems: ImmutableArray<ContentDto>; columnCount: number;
} }
@Component({ @Component({
@ -40,15 +35,16 @@ interface State {
providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR], providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReferencesEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit { // tslint:disable-next-line: readonly-array
export class ReferencesEditorComponent extends StatefulControlComponent<State, string[]> {
@Input() @Input()
public schemaId: string; public schemaIds: ReadonlyArray<string>;
@Input() @Input()
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: AppLanguageDto[]; public languages: ReadonlyArray<AppLanguageDto>;
@Input() @Input()
public isCompact = false; public isCompact = false;
@ -56,64 +52,52 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, s
@Input() @Input()
public allowDuplicates = true; public allowDuplicates = true;
@Input()
public columnCount = 0;
public selectorDialog = new DialogModel(); public selectorDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService
private readonly schemasService: SchemasService
) { ) {
super(changeDetector, { super(changeDetector, { contentItems: [], columnCount: 0 });
schemaInvalid: false,
schema: null,
contentItems: ImmutableArray.empty()
});
}
public ngOnInit() {
if (this.schemaId === MathHelper.EMPTY_GUID) {
this.next(s => ({ ...s, schemaInvalid: true }));
return;
}
this.schemasService.getSchema(this.appsState.appName, this.schemaId)
.subscribe(schema => {
this.next(s => ({ ...s, schema }));
}, () => {
this.next(s => ({ ...s, schemaInvalid: true }));
});
} }
public writeValue(obj: any) { public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) { if (Types.isArrayOfString(obj)) {
if (!Types.isEquals(obj, this.snapshot.contentItems.map(x => x.id).values)) { if (!Types.isEquals(obj, this.snapshot.contentItems.map(x => x.id))) {
const contentIds: string[] = obj; const contentIds: string[] = obj;
this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds) this.contentsService.getContentsByIds(this.appsState.appName, contentIds)
.subscribe(dtos => { .subscribe(dtos => {
this.setContentItems(ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r))); this.setContentItems(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r));
if (this.snapshot.contentItems.length !== contentIds.length) { if (this.snapshot.contentItems.length !== contentIds.length) {
this.updateValue(); this.updateValue();
} }
}, () => { }, () => {
this.setContentItems(ImmutableArray.empty()); this.setContentItems([]);
}); });
} }
} else { } else {
this.setContentItems(ImmutableArray.empty()); this.setContentItems([]);
} }
} }
public setContentItems(contentItems: ImmutableArray<ContentDto>) { public setContentItems(contentItems: ReadonlyArray<ContentDto>) {
this.next(s => ({ ...s, contentItems })); let columnCount = 1;
}
public select(contents: ContentDto[]) { for (const content of contentItems) {
for (let content of contents) { columnCount = Math.max(columnCount, content.referenceFields.length);
this.setContentItems(this.snapshot.contentItems.push(content));
} }
this.next(s => ({ ...s, contentItems, columnCount }));
}
public select(contents: ReadonlyArray<ContentDto>) {
this.setContentItems([...this.snapshot.contentItems, ...contents]);
if (contents.length > 0) { if (contents.length > 0) {
this.updateValue(); this.updateValue();
} }
@ -123,29 +107,30 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, s
public remove(content: ContentDto) { public remove(content: ContentDto) {
if (content) { if (content) {
this.setContentItems(this.snapshot.contentItems.remove(content)); this.setContentItems(this.snapshot.contentItems.filter(x => x.id !== content.id));
this.updateValue(); this.updateValue();
} }
} }
public sort(contents: ContentDto[]) { public sort(contents: ReadonlyArray<ContentDto>) {
if (contents) { if (contents) {
this.setContentItems(ImmutableArray.of(contents)); this.setContentItems(contents);
this.updateValue(); this.updateValue();
} }
} }
private updateValue() { private updateValue() {
let ids: string[] | null = this.snapshot.contentItems.values.map(x => x.id); const ids = this.snapshot.contentItems.map(x => x.id);
if (ids.length === 0) { if (ids.length === 0) {
ids = null; this.callChange(null);
} else {
this.callChange(ids);
} }
this.callTouched(); this.callTouched();
this.callChange(ids);
} }
public trackByContent(index: number, content: ContentDto) { public trackByContent(index: number, content: ContentDto) {

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

@ -19,7 +19,7 @@ import {
UsagesService UsagesService
} from '@app/shared'; } from '@app/shared';
const COLORS = [ const COLORS: ReadonlyArray<string> = [
' 51, 137, 213', ' 51, 137, 213',
'211, 50, 50', '211, 50, 50',
'131, 211, 50', '131, 211, 50',
@ -85,7 +85,7 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
maintainAspectRatio: false maintainAspectRatio: false
}; };
public history: HistoryEventDto[] = []; public history: ReadonlyArray<HistoryEventDto> = [];
public assetsCurrent = 0; public assetsCurrent = 0;
public assetsMax = 0; public assetsMax = 0;
@ -207,10 +207,10 @@ function label(category: string) {
return category === '*' ? 'anonymous' : category; return category === '*' ? 'anonymous' : category;
} }
function createLabels(dtos: { date: DateTime }[]): string[] { function createLabels(dtos: ReadonlyArray<{ date: DateTime }>): ReadonlyArray<string> {
return dtos.map(d => d.date.toStringFormat('M-DD')); return dtos.map(d => d.date.toStringFormat('M-DD'));
} }
function createLabelsFromSet(dtos: { [category: string]: { date: DateTime }[] }): string[] { function createLabelsFromSet(dtos: { [category: string]: ReadonlyArray<{ date: DateTime }> }): ReadonlyArray<string> {
return createLabels(dtos[Object.keys(dtos)[0]]); return createLabels(dtos[Object.keys(dtos)[0]]);
} }

2
src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts

@ -29,7 +29,7 @@ export class GenericActionComponent implements OnInit {
public actionFormSubmitted = false; public actionFormSubmitted = false;
public ngOnInit() { public ngOnInit() {
for (let property of this.definition.properties) { for (const property of this.definition.properties) {
const validators = []; const validators = [];
if (property.isRequired) { if (property.isRequired) {

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

@ -10,7 +10,6 @@ import { FormGroup } from '@angular/forms';
import { import {
Form, Form,
ImmutableArray,
RuleDto, RuleDto,
RuleElementDto, RuleElementDto,
RulesState, RulesState,
@ -37,7 +36,7 @@ export class RuleWizardComponent implements AfterViewInit, OnInit {
public ruleTriggers: { [name: string]: RuleElementDto }; public ruleTriggers: { [name: string]: RuleElementDto };
@Input() @Input()
public schemas: ImmutableArray<SchemaDto>; public schemas: ReadonlyArray<SchemaDto>;
@Input() @Input()
public rule: RuleDto; public rule: RuleDto;

26
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -8,11 +8,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { import { SchemaDto, Types } from '@app/shared';
ImmutableArray,
SchemaDto,
Types
} from '@app/shared';
export interface TriggerSchemaForm { export interface TriggerSchemaForm {
schema: SchemaDto; schema: SchemaDto;
@ -27,7 +23,7 @@ export interface TriggerSchemaForm {
}) })
export class ContentChangedTriggerComponent implements OnInit { export class ContentChangedTriggerComponent implements OnInit {
@Input() @Input()
public schemas: ImmutableArray<SchemaDto>; public schemas: ReadonlyArray<SchemaDto>;
@Input() @Input()
public trigger: any; public trigger: any;
@ -38,10 +34,10 @@ export class ContentChangedTriggerComponent implements OnInit {
@Input() @Input()
public triggerFormSubmitted = false; public triggerFormSubmitted = false;
public triggerSchemas: ImmutableArray<TriggerSchemaForm>; public triggerSchemas: ReadonlyArray<TriggerSchemaForm>;
public schemaToAdd: SchemaDto; public schemaToAdd: SchemaDto;
public schemasToAdd: ImmutableArray<SchemaDto>; public schemasToAdd: ReadonlyArray<SchemaDto>;
public get hasSchema() { public get hasSchema() {
return !!this.schemaToAdd; return !!this.schemaToAdd;
@ -57,7 +53,7 @@ export class ContentChangedTriggerComponent implements OnInit {
const schemas: TriggerSchemaForm[] = []; const schemas: TriggerSchemaForm[] = [];
if (this.trigger.schemas) { if (this.trigger.schemas) {
for (let triggerSchema of this.trigger.schemas) { for (const triggerSchema of this.trigger.schemas) {
const schema = this.schemas.find(s => s.id === triggerSchema.schemaId); const schema = this.schemas.find(s => s.id === triggerSchema.schemaId);
if (schema) { if (schema) {
@ -68,20 +64,20 @@ export class ContentChangedTriggerComponent implements OnInit {
} }
} }
this.triggerSchemas = ImmutableArray.of(schemas).sortByStringAsc(s => s.schema.name); this.triggerSchemas = schemas.sortedByString(s => s.schema.name);
this.updateSchemaToAdd(); this.updateSchemaToAdd();
} }
public removeSchema(schemaForm: TriggerSchemaForm) { public removeSchema(schemaForm: TriggerSchemaForm) {
this.triggerSchemas = this.triggerSchemas.remove(schemaForm); this.triggerSchemas = this.triggerSchemas.removed(schemaForm);
this.updateValue(); this.updateValue();
this.updateSchemaToAdd(); this.updateSchemaToAdd();
} }
public addSchema() { public addSchema() {
this.triggerSchemas = this.triggerSchemas.push({ schema: this.schemaToAdd }).sortByStringAsc(x => x.schema.name); this.triggerSchemas = [{ schema: this.schemaToAdd }, ...this.triggerSchemas].sortedByString(x => x.schema.name);
this.updateValue(); this.updateValue();
this.updateSchemaToAdd(); this.updateSchemaToAdd();
@ -94,14 +90,14 @@ export class ContentChangedTriggerComponent implements OnInit {
} }
public updateValue() { public updateValue() {
const schemas = this.triggerSchemas.values.map(s => ({ schemaId: s.schema.id, condition: s.condition })); const schemas = this.triggerSchemas.map(s => ({ schemaId: s.schema.id, condition: s.condition }));
this.triggerForm.controls['schemas'].setValue(schemas); this.triggerForm.controls['schemas'].setValue(schemas);
} }
private updateSchemaToAdd() { private updateSchemaToAdd() {
this.schemasToAdd = this.schemas.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortByStringAsc(x => x.name); this.schemasToAdd = this.schemas.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortedByString(x => x.name);
this.schemaToAdd = this.schemasToAdd.at(0); this.schemaToAdd = this.schemasToAdd[0];
} }
public trackBySchema(index: number, schema: SchemaDto) { public trackBySchema(index: number, schema: SchemaDto) {

5
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -14,7 +14,6 @@ import {
DialogService, DialogService,
EditFieldForm, EditFieldForm,
fadeAnimation, fadeAnimation,
ImmutableArray,
ModalModel, ModalModel,
NestedFieldDto, NestedFieldDto,
PatternDto, PatternDto,
@ -42,7 +41,7 @@ export class FieldComponent implements OnChanges {
public parent: RootFieldDto; public parent: RootFieldDto;
@Input() @Input()
public patterns: ImmutableArray<PatternDto>; public patterns: ReadonlyArray<PatternDto>;
public dropdown = new ModalModel(); public dropdown = new ModalModel();
@ -99,7 +98,7 @@ export class FieldComponent implements OnChanges {
this.schemasState.hideField(this.schema, this.field); this.schemasState.hideField(this.schema, this.field);
} }
public sortFields(fields: NestedFieldDto[]) { public sortFields(fields: ReadonlyArray<NestedFieldDto>) {
this.schemasState.orderFields(this.schema, fields, <any>this.field).subscribe(); this.schemasState.orderFields(this.schema, fields, <any>this.field).subscribe();
} }

8
src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts

@ -8,11 +8,7 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { import { FieldDto, PatternDto } from '@app/shared';
FieldDto,
ImmutableArray,
PatternDto
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-field-form-validation', selector: 'sqx-field-form-validation',
@ -55,5 +51,5 @@ export class FieldFormValidationComponent {
public field: FieldDto; public field: FieldDto;
@Input() @Input()
public patterns: ImmutableArray<PatternDto>; public patterns: ReadonlyArray<PatternDto>;
} }

8
src/Squidex/app/features/schemas/pages/schema/forms/field-form.component.ts

@ -8,11 +8,7 @@
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core'; import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { import { FieldDto, PatternDto } from '@app/shared';
FieldDto,
ImmutableArray,
PatternDto
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-field-form', selector: 'sqx-field-form',
@ -63,7 +59,7 @@ export class FieldFormComponent implements AfterViewInit {
public editFormSubmitted: boolean; public editFormSubmitted: boolean;
@Input() @Input()
public patterns: ImmutableArray<PatternDto>; public patterns: ReadonlyArray<PatternDto>;
@Input() @Input()
public field: FieldDto; public field: FieldDto;

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

@ -79,7 +79,7 @@ export class SchemaPageComponent extends ResourceOwner implements OnInit {
this.schemasState.unpublish(this.schema).subscribe(); this.schemasState.unpublish(this.schema).subscribe();
} }
public sortFields(fields: FieldDto[]) { public sortFields(fields: ReadonlyArray<FieldDto>) {
this.schemasState.orderFields(this.schema, fields).subscribe(); this.schemasState.orderFields(this.schema, fields).subscribe();
} }

2
src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts

@ -34,7 +34,7 @@ export class DateTimeValidationComponent implements OnInit {
public showDefaultValues: Observable<boolean>; public showDefaultValues: Observable<boolean>;
public showDefaultValue: Observable<boolean>; public showDefaultValue: Observable<boolean>;
public calculatedDefaultValues = ['Now', 'Today']; public calculatedDefaultValues: ReadonlyArray<string> = ['Now', 'Today'];
public ngOnInit() { public ngOnInit() {
this.editForm.setControl('calculatedDefaultValue', this.editForm.setControl('calculatedDefaultValue',

8
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html

@ -1,11 +1,11 @@
<div [formGroup]="editForm"> <div [formGroup]="editForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldSchemaId">Schema</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldSchemaId">Schemas</label>
<div class="col-6"> <div class="col-6">
<select class="form-control" id="{{field.fieldId}}_fieldSchemaId" formControlName="schemaId"> <sqx-tag-editor placeholder=", to add schema" formControlName="schemaIds"
<option *ngFor="let schema of schemasState.schemas | async" [ngValue]="schema.id">{{schema.displayName}}</option> [converter]="schemasSource" [suggestedValues]="schemasSource.suggestions">
</select> </sqx-tag-editor>
</div> </div>
</div> </div>

16
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.ts

@ -6,9 +6,13 @@
*/ */
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { FieldDto, ReferencesFieldPropertiesDto, SchemasState } from '@app/shared'; import {
FieldDto,
ReferencesFieldPropertiesDto,
SchemaTagConverter
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-references-validation', selector: 'sqx-references-validation',
@ -26,7 +30,7 @@ export class ReferencesValidationComponent implements OnInit {
public properties: ReferencesFieldPropertiesDto; public properties: ReferencesFieldPropertiesDto;
constructor( constructor(
public readonly schemasState: SchemasState public readonly schemasSource: SchemaTagConverter
) { ) {
} }
@ -40,9 +44,7 @@ export class ReferencesValidationComponent implements OnInit {
this.editForm.setControl('minItems', this.editForm.setControl('minItems',
new FormControl(this.properties.minItems)); new FormControl(this.properties.minItems));
this.editForm.setControl('schemaId', this.editForm.setControl('schemaIds',
new FormControl(this.properties.schemaId, [ new FormControl(this.properties.schemaIds));
Validators.required
]));
} }
} }

3
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -13,7 +13,6 @@ import {
fadeAnimation, fadeAnimation,
FieldDto, FieldDto,
hasNoValue$, hasNoValue$,
ImmutableArray,
ModalModel, ModalModel,
PatternDto, PatternDto,
ResourceOwner, ResourceOwner,
@ -41,7 +40,7 @@ export class StringValidationComponent extends ResourceOwner implements OnInit {
public properties: StringFieldPropertiesDto; public properties: StringFieldPropertiesDto;
@Input() @Input()
public patterns: ImmutableArray<PatternDto>; public patterns: ReadonlyArray<PatternDto>;
public showDefaultValue: Observable<boolean>; public showDefaultValue: Observable<boolean>;
public showPatternMessage: boolean; public showPatternMessage: boolean;

2
src/Squidex/app/features/settings/pages/clients/client.component.ts

@ -34,7 +34,7 @@ export class ClientComponent implements OnChanges {
public client: ClientDto; public client: ClientDto;
@Input() @Input()
public clientRoles: RoleDto[]; public clientRoles: ReadonlyArray<RoleDto>;
public connectToken: AccessTokenDto; public connectToken: AccessTokenDto;
public connectDialog = new DialogModel(); public connectDialog = new DialogModel();

9
src/Squidex/app/features/settings/pages/contributors/contributor-add-form.component.ts

@ -16,7 +16,6 @@ import {
ContributorsState, ContributorsState,
DialogModel, DialogModel,
DialogService, DialogService,
ImmutableArray,
RoleDto, RoleDto,
UsersService UsersService
} from '@app/shared'; } from '@app/shared';
@ -29,12 +28,12 @@ export class UsersDataSource implements AutocompleteSource {
) { ) {
} }
public find(query: string): Observable<any[]> { public find(query: string): Observable<ReadonlyArray<any>> {
return this.usersService.getUsers(query).pipe( return this.usersService.getUsers(query).pipe(
withLatestFrom(this.contributorsState.contributors, (users, contributors) => { withLatestFrom(this.contributorsState.contributors, (users, contributors) => {
const results: any[] = []; const results: any[] = [];
for (let user of users) { for (const user of users) {
if (!contributors!.find(t => t.contributorId === user.id)) { if (!contributors!.find(t => t.contributorId === user.id)) {
results.push(user); results.push(user);
} }
@ -56,7 +55,7 @@ export class ContributorAddFormComponent implements OnInit {
private defaultValue: any; private defaultValue: any;
@Input() @Input()
public roles: ImmutableArray<RoleDto>; public roles: ReadonlyArray<RoleDto>;
public assignContributorForm = new AssignContributorForm(this.formBuilder); public assignContributorForm = new AssignContributorForm(this.formBuilder);
@ -71,7 +70,7 @@ export class ContributorAddFormComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.defaultValue = { role: this.roles.at(0).name, contributorId: '' }; this.defaultValue = { role: this.roles[0].name, contributorId: '' };
this.assignContributorForm.submitCompleted({ newValue: this.defaultValue }); this.assignContributorForm.submitCompleted({ newValue: this.defaultValue });
} }

3
src/Squidex/app/features/settings/pages/contributors/contributor.component.ts

@ -12,7 +12,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { import {
ContributorDto, ContributorDto,
ContributorsState, ContributorsState,
ImmutableArray,
RoleDto RoleDto
} from '@app/shared'; } from '@app/shared';
@ -45,7 +44,7 @@ import {
}) })
export class ContributorComponent { export class ContributorComponent {
@Input() @Input()
public roles: ImmutableArray<RoleDto>; public roles: ReadonlyArray<RoleDto>;
@Input() @Input()
public search: string; public search: string;

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

@ -50,7 +50,7 @@ export class ContributorsPageComponent implements OnInit {
this.contributorsState.search(query); this.contributorsState.search(query);
} }
public trackByContributor(contributor: ContributorDto) { public trackByContributor(index: number, contributor: ContributorDto) {
return contributor.contributorId; return contributor.contributorId;
} }
} }

29
src/Squidex/app/features/settings/pages/contributors/import-contributors-dialog.component.ts

@ -13,17 +13,16 @@ import { catchError, mergeMap, tap } from 'rxjs/operators';
import { import {
ContributorsState, ContributorsState,
ErrorDto, ErrorDto,
ImmutableArray,
ImportContributorsForm, ImportContributorsForm,
RoleDto RoleDto
} from '@app/shared'; } from '@app/shared';
interface ImportStatus { type ImportStatus = {
email: string; email: string;
result: 'Pending' | 'Failed' | 'Success'; result: 'Pending' | 'Failed' | 'Success';
resultText: string; resultText: string;
role: string; role: string;
} };
@Component({ @Component({
selector: 'sqx-import-contributors-dialog', selector: 'sqx-import-contributors-dialog',
@ -37,10 +36,10 @@ export class ImportContributorsDialogComponent {
public close = new EventEmitter(); public close = new EventEmitter();
@Input() @Input()
public roles: ImmutableArray<RoleDto>; public roles: ReadonlyArray<RoleDto>;
public importForm = new ImportContributorsForm(this.formBuilder); public importForm = new ImportContributorsForm(this.formBuilder);
public importStatus: ImportStatus[] = []; public importStatus: ReadonlyArray<ImportStatus> = [];
public importStage: 'Start' | 'Change' | 'Wait' = 'Start'; public importStage: 'Start' | 'Change' | 'Wait' = 'Start';
constructor( constructor(
@ -54,15 +53,13 @@ export class ImportContributorsDialogComponent {
const contributors = this.importForm.submit(); const contributors = this.importForm.submit();
if (contributors && contributors.length > 0) { if (contributors) {
for (let contributor of contributors) { this.importStatus = contributors.map(contributor => ({
this.importStatus.push({ email: contributor.contributorId,
email: contributor.contributorId, result: 'Pending',
result: 'Pending', resultText: 'Pending',
resultText: 'Pending', role: 'Developer'
role: 'Developer' }));
});
}
} }
} }
@ -73,7 +70,7 @@ export class ImportContributorsDialogComponent {
mergeMap(s => mergeMap(s =>
this.contributorsState.assign(createRequest(s), { silent: true }).pipe( this.contributorsState.assign(createRequest(s), { silent: true }).pipe(
tap(created => { tap(created => {
let status = this.importStatus.find(x => x.email === s.email); const status = this.importStatus.find(x => x.email === s.email);
if (status) { if (status) {
status.resultText = getSuccess(created); status.resultText = getSuccess(created);
@ -81,7 +78,7 @@ export class ImportContributorsDialogComponent {
} }
}), }),
catchError((error: ErrorDto) => { catchError((error: ErrorDto) => {
let status = this.importStatus.find(x => x.email === s.email); const status = this.importStatus.find(x => x.email === s.email);
if (status) { if (status) {
status.resultText = getError(error); status.resultText = getError(error);

5
src/Squidex/app/features/settings/pages/languages/language-add-form.component.ts

@ -10,7 +10,6 @@ import { FormBuilder } from '@angular/forms';
import { import {
AddLanguageForm, AddLanguageForm,
ImmutableArray,
LanguageDto, LanguageDto,
LanguagesState LanguagesState
} from '@app/shared'; } from '@app/shared';
@ -36,7 +35,7 @@ import {
}) })
export class LanguageAddFormComponent implements OnChanges { export class LanguageAddFormComponent implements OnChanges {
@Input() @Input()
public newLanguages: ImmutableArray<LanguageDto>; public newLanguages: ReadonlyArray<LanguageDto>;
public addLanguageForm = new AddLanguageForm(this.formBuilder); public addLanguageForm = new AddLanguageForm(this.formBuilder);
@ -48,7 +47,7 @@ export class LanguageAddFormComponent implements OnChanges {
public ngOnChanges() { public ngOnChanges() {
if (this.newLanguages.length > 0) { if (this.newLanguages.length > 0) {
const language = this.newLanguages.at(0); const language = this.newLanguages[0];
this.addLanguageForm.load({ language }); this.addLanguageForm.load({ language });
} }

2
src/Squidex/app/features/settings/pages/languages/language.component.html

@ -40,7 +40,7 @@
<div class="col-9"> <div class="col-9">
<div class="fallback-languages" <div class="fallback-languages"
[sqxSortModel]="fallbackLanguages.mutableValues" [sqxSortModel]="fallbackLanguages"
[sqxSortDisabled]="!isEditable" [sqxSortDisabled]="!isEditable"
*ngIf="fallbackLanguages.length > 0"> *ngIf="fallbackLanguages.length > 0">
<div class="fallback-language" *ngFor="let language of fallbackLanguages"> <div class="fallback-language" *ngFor="let language of fallbackLanguages">

21
src/Squidex/app/features/settings/pages/languages/language.component.ts

@ -12,7 +12,6 @@ import {
AppLanguageDto, AppLanguageDto,
EditLanguageForm, EditLanguageForm,
fadeAnimation, fadeAnimation,
ImmutableArray,
LanguagesState LanguagesState
} from '@app/shared'; } from '@app/shared';
@ -29,10 +28,10 @@ export class LanguageComponent implements OnChanges {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public fallbackLanguages: ImmutableArray<AppLanguageDto>; public fallbackLanguages: ReadonlyArray<AppLanguageDto>;
@Input() @Input()
public fallbackLanguagesNew: ImmutableArray<AppLanguageDto>; public fallbackLanguagesNew: ReadonlyArray<AppLanguageDto>;
public otherLanguage: AppLanguageDto; public otherLanguage: AppLanguageDto;
@ -53,7 +52,7 @@ export class LanguageComponent implements OnChanges {
this.editForm.load(this.language); this.editForm.load(this.language);
this.editForm.setEnabled(this.isEditable); this.editForm.setEnabled(this.isEditable);
this.otherLanguage = this.fallbackLanguagesNew.at(0); this.otherLanguage = this.fallbackLanguagesNew[0];
} }
public toggleEditing() { public toggleEditing() {
@ -72,7 +71,7 @@ export class LanguageComponent implements OnChanges {
const value = this.editForm.submit(); const value = this.editForm.submit();
if (value) { if (value) {
const request = { ...value, fallback: this.fallbackLanguages.map(x => x.iso2Code).values }; const request = { ...value, fallback: this.fallbackLanguages.map(x => x.iso2Code) };
this.languagesState.update(this.language, request) this.languagesState.update(this.language, request)
.subscribe(() => { .subscribe(() => {
@ -86,17 +85,17 @@ export class LanguageComponent implements OnChanges {
} }
public removeFallbackLanguage(language: AppLanguageDto) { public removeFallbackLanguage(language: AppLanguageDto) {
this.fallbackLanguages = this.fallbackLanguages.remove(language); this.fallbackLanguages = this.fallbackLanguages.removed(language);
this.fallbackLanguagesNew = this.fallbackLanguagesNew.push(language).sortByStringAsc(x => x.iso2Code); this.fallbackLanguagesNew = [...this.fallbackLanguagesNew, language].sortedByString(x => x.iso2Code);
this.otherLanguage = this.fallbackLanguagesNew.at(0); this.otherLanguage = this.fallbackLanguagesNew[0];
} }
public addFallbackLanguage() { public addFallbackLanguage() {
this.fallbackLanguages = this.fallbackLanguages.push(this.otherLanguage); this.fallbackLanguages = [...this.fallbackLanguages, this.otherLanguage].sortedByString(x => x.iso2Code);
this.fallbackLanguagesNew = this.fallbackLanguagesNew.remove(this.otherLanguage); this.fallbackLanguagesNew = this.fallbackLanguagesNew.removed(this.otherLanguage);
this.otherLanguage = this.fallbackLanguagesNew.at(0); this.otherLanguage = this.fallbackLanguagesNew[0];
} }
public trackByLanguage(index: number, language: AppLanguageDto) { public trackByLanguage(index: number, language: AppLanguageDto) {

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

@ -74,7 +74,7 @@ export class MorePageComponent extends ResourceOwner implements OnInit {
} }
} }
public uploadImage(file: File[]) { public uploadImage(file: ReadonlyArray<File>) {
if (!this.isImageEditable) { if (!this.isImageEditable) {
return; return;
} }

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

@ -17,13 +17,13 @@ import {
} from '@app/shared'; } from '@app/shared';
class PermissionsAutocomplete implements AutocompleteSource { class PermissionsAutocomplete implements AutocompleteSource {
private permissions: string[] = []; private permissions: ReadonlyArray<string> = [];
constructor(appsState: AppsState, rolesService: RolesService) { constructor(appsState: AppsState, rolesService: RolesService) {
rolesService.getPermissions(appsState.appName).subscribe(x => this.permissions = x); rolesService.getPermissions(appsState.appName).subscribe(x => this.permissions = x);
} }
public find(query: string): Observable<any[]> { public find(query: string): Observable<ReadonlyArray<any>> {
return of(this.permissions.filter(y => y.indexOf(query) === 0)); return of(this.permissions.filter(y => y.indexOf(query) === 0));
} }
} }
@ -51,7 +51,7 @@ export class RolesPageComponent implements OnInit {
this.rolesState.load(true); this.rolesState.load(true);
} }
public trackByRole(role: RoleDto) { public trackByRole(index: number, role: RoleDto) {
return role.name; return role.name;
} }
} }

6
src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts

@ -51,17 +51,17 @@ export class WorkflowStepComponent implements OnChanges {
public step: WorkflowStep; public step: WorkflowStep;
@Input() @Input()
public roles: RoleDto[]; public roles: ReadonlyArray<RoleDto>;
@Input() @Input()
public disabled: boolean; public disabled: boolean;
public onBlur = { updateOn: 'blur' }; public onBlur = { updateOn: 'blur' };
public openSteps: WorkflowStep[]; public openSteps: ReadonlyArray<WorkflowStep>;
public openStep: WorkflowStep; public openStep: WorkflowStep;
public transitions: WorkflowTransitionView[]; public transitions: ReadonlyArray<WorkflowTransitionView>;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['workflow'] || changes['step'] || false) { if (changes['workflow'] || changes['step'] || false) {

2
src/Squidex/app/features/settings/pages/workflows/workflow-transition.component.ts

@ -29,7 +29,7 @@ export class WorkflowTransitionComponent {
public transition: WorkflowTransitionView; public transition: WorkflowTransitionView;
@Input() @Input()
public roles: RoleDto[]; public roles: ReadonlyArray<RoleDto>;
@Input() @Input()
public disabled: boolean; public disabled: boolean;

9
src/Squidex/app/features/settings/pages/workflows/workflow.component.ts

@ -5,12 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable: readonly-array
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { import {
ErrorDto, ErrorDto,
MathHelper, MathHelper,
RoleDto, RoleDto,
SchemaTagConverter,
WorkflowDto, WorkflowDto,
WorkflowsState, WorkflowsState,
WorkflowStep, WorkflowStep,
@ -19,8 +22,6 @@ import {
WorkflowTransitionValues WorkflowTransitionValues
} from '@app/shared'; } from '@app/shared';
import { SchemaTagConverter } from './schema-tag-converter';
@Component({ @Component({
selector: 'sqx-workflow', selector: 'sqx-workflow',
styleUrls: ['./workflow.component.scss'], styleUrls: ['./workflow.component.scss'],
@ -31,7 +32,7 @@ export class WorkflowComponent implements OnChanges {
public workflow: WorkflowDto; public workflow: WorkflowDto;
@Input() @Input()
public roles: RoleDto[]; public roles: ReadonlyArray<RoleDto>;
@Input() @Input()
public schemasSource: SchemaTagConverter; public schemasSource: SchemaTagConverter;
@ -74,7 +75,7 @@ export class WorkflowComponent implements OnChanges {
} }
public addStep() { public addStep() {
let index = this.workflow.steps.length; const index = this.workflow.steps.length;
for (let i = index; i < index + 100; i++) { for (let i = index; i < index + 100; i++) {
const name = `Step${i}`; const name = `Step${i}`;

21
src/Squidex/app/features/settings/pages/workflows/workflows-page.component.ts

@ -5,28 +5,24 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { import {
RolesState, RolesState,
SchemasState, SchemaTagConverter,
WorkflowDto, WorkflowDto,
WorkflowsState WorkflowsState
} from '@app/shared'; } from '@app/shared';
import { SchemaTagConverter } from './schema-tag-converter';
@Component({ @Component({
selector: 'sqx-workflows-page', selector: 'sqx-workflows-page',
styleUrls: ['./workflows-page.component.scss'], styleUrls: ['./workflows-page.component.scss'],
templateUrl: './workflows-page.component.html' templateUrl: './workflows-page.component.html'
}) })
export class WorkflowsPageComponent implements OnInit, OnDestroy { export class WorkflowsPageComponent implements OnInit {
public schemasSource: SchemaTagConverter;
constructor( constructor(
public readonly rolesState: RolesState, public readonly rolesState: RolesState,
public readonly schemasState: SchemasState, public readonly schemasSource: SchemaTagConverter,
public readonly workflowsState: WorkflowsState public readonly workflowsState: WorkflowsState
) { ) {
} }
@ -34,21 +30,14 @@ export class WorkflowsPageComponent implements OnInit, OnDestroy {
public ngOnInit() { public ngOnInit() {
this.rolesState.load(); this.rolesState.load();
this.schemasSource = new SchemaTagConverter(this.schemasState);
this.schemasState.load();
this.workflowsState.load(); this.workflowsState.load();
} }
public ngOnDestroy() {
this.schemasSource.destroy();
}
public reload() { public reload() {
this.workflowsState.load(true); this.workflowsState.load(true);
} }
public trackByWorkflow(workflow: WorkflowDto) { public trackByWorkflow(index: number, workflow: WorkflowDto) {
return workflow.id; return workflow.id;
} }
} }

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

Loading…
Cancel
Save