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.Collections.Generic;
using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Schemas
{
@ -21,7 +23,22 @@ namespace Squidex.Domain.Apps.Core.Schemas
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)
{

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());
}
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)
{
return schema.Fields.OfType<IField<ReferencesFieldProperties>>()
.Where(x =>
x.Properties.SchemaId != Guid.Empty &&
x.Properties.ResolveReference &&
x.Properties.MaxItems == 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)
{
if (oldReferences.Contains(field.Properties.SchemaId))
if (oldReferences.Contains(field.Properties.SingleId()))
{
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();
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;

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>();
}
if (field.Properties.SchemaId != Guid.Empty)
{
yield return new ReferencesValidator(field.Properties.SchemaId);
}
yield return new ReferencesValidator(field.Properties.SchemaIds);
}
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
{
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);
@ -23,6 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private readonly Guid contentId;
private readonly Guid schemaId;
private readonly CheckContents checkContent;
private readonly CheckContentsByIds checkContentByIds;
private readonly CheckAssets checkAsset;
private readonly ImmutableQueue<string> propertyPath;
@ -47,8 +50,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
Guid contentId,
Guid schemaId,
CheckContents checkContent,
CheckContentsByIds checkContentsByIds,
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 schemaId,
CheckContents checkContent,
CheckContentsByIds checkContentByIds,
CheckAssets checkAsset,
ImmutableQueue<string> propertyPath,
bool isOptional)
{
Guard.NotNull(checkAsset, nameof(checkAsset));
Guard.NotNull(checkContent, nameof(checkAsset));
Guard.NotNull(checkContent, nameof(checkContent));
Guard.NotNull(checkContentByIds, nameof(checkContentByIds));
this.propertyPath = propertyPath;
this.checkContent = checkContent;
this.checkContentByIds = checkContentByIds;
this.checkAsset = checkAsset;
this.contentId = contentId;
@ -76,17 +83,40 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
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)
{
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)

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

@ -9,35 +9,37 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class ReferencesValidator : IValidator
{
private static readonly PropertyPath Path = "Id";
private readonly IEnumerable<Guid> schemaIds;
private readonly Guid schemaId;
public ReferencesValidator(Guid schemaId)
public ReferencesValidator(IEnumerable<Guid> schemaIds)
{
this.schemaId = schemaId;
this.schemaIds = schemaIds;
}
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is ICollection<Guid> contentIds)
{
var filter = ClrFilter.In(Path, contentIds.ToList());
var foundIds = await context.GetContentIdsAsync(schemaId, filter);
var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet());
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}'.");
}
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);
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.");
}

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 contentEntities =
await Collection.Find(filter).Only(x => x.Id)
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.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)

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>())
{
@ -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)
{
using (Profiler.TraceMethod<MongoContentRepository>())

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

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
@ -45,10 +46,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
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 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()
{
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)
@ -122,11 +125,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
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);
}
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)
{
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);
}
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);
}
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
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);
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return EmptyContents;
}
var dataLoader = GetContentsLoader(schemaId);
var dataLoader = GetContentsLoader();
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 =>
{
var result = await GetReferencedContentsAsync(schemaId, new List<Guid>(batch));
var result = await GetReferencedContentsAsync(new List<Guid>(batch));
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
{
private readonly Dictionary<ISchemaEntity, ContentGraphType> contentTypes = new Dictionary<ISchemaEntity, ContentGraphType>();
private readonly Dictionary<ISchemaEntity, ContentDataGraphType> contentDataTypes = new Dictionary<ISchemaEntity, ContentDataGraphType>();
private readonly Dictionary<Guid, ISchemaEntity> schemasById;
private readonly Dictionary<Guid, ContentGraphType> contentTypes = new Dictionary<Guid, ContentGraphType>();
private readonly PartitionResolver partitionResolver;
private readonly IAppEntity app;
private readonly IGraphType assetType;
@ -54,34 +52,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
assetType = new AssetGraphType(this);
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);
InitializeContentTypes();
}
private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets)
private void BuildSchemas(List<ISchemaEntity> allSchemas)
{
var schemas = model.schemasById.Values;
return new GraphQLSchema { Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) };
foreach (var schema in allSchemas)
{
contentTypes[schema.Id] = new ContentGraphType(schema);
}
}
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()
{
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)
{
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);
if (schema == null)
{
return null;
}
contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType());
return contentTypes.GetOrAdd(schema, s => new ContentGraphType());
return contentTypes.GetOrDefault(schemaId);
}
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);
IGraphType GetAssetType();
IObjectGraphType GetAssetType();
IGraphType GetContentType(Guid schemaId);
IObjectGraphType GetContentType(Guid schemaId);
(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);
AddContentFind(schemaId, schemaType, schemaName, contentType);
AddContentFind(schemaType, schemaName, contentType);
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
{
@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
var contentId = c.GetArgument<Guid>("id");
return e.FindContentAsync(schemaId, contentId);
return e.FindContentAsync(contentId);
}),
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)
{
Name = "AssetDto";
Name = "Asset";
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 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";
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 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();
var schemaName = schema.DisplayName();
this.schema = schema;
schemaType = schema.TypeName();
schemaName = schema.DisplayName();
Name = $"{schemaType}Dto";
Name = $"{schemaType}";
AddField(new FieldType
{
@ -86,6 +92,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
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
{
Name = "url",
@ -94,6 +114,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The url to the the {schemaName} content."
});
var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model);
if (contentDataType.Fields.Any())
{
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 structure of a {schemaName} content type.";
}
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.Collections.Generic;
using System.Linq;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
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)>
{
private static readonly ValueResolver NoopResolver = (value, c) => value;
private readonly Dictionary<Guid, ContentGraphType> schemaTypes;
private readonly ISchemaEntity schema;
private readonly Func<Guid, IGraphType> schemaResolver;
private readonly IGraphModel model;
private readonly IGraphType assetListType;
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.assetListType = assetListType;
this.schema = schema;
this.schemaResolver = schemaResolver;
this.schemaTypes = schemaTypes;
this.fieldName = fieldName;
}
@ -112,22 +119,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
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;
var contentType = schemaResolver(schemaId);
IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId());
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 context = (GraphQLExecutionContext)c.UserContext;
return context.GetReferencedContentsAsync(schemaId, value);
return context.GetReferencedContentsAsync(value);
});
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.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents
{
@ -15,6 +16,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
string StatusColor { get; }
string SchemaName { get; }
string SchemaDisplayName { get; }
RootField[] ReferenceFields { get; }
StatusInfo[] Nexts { 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())
{
var appVersion = context.App.Version.ToString();
var appVersion = context.App.Version;
var cache = new Dictionary<(Guid, Status), StatusInfo>();
@ -77,11 +77,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await ResolveCanUpdateAsync(content, result);
}
result.CacheDependencies = new HashSet<string>
{
appVersion
};
results.Add(result);
}
@ -89,16 +84,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString());
var schemaIdentity = schema.Id.ToString();
var schemaVersion = schema.Version.ToString();
foreach (var content in group)
{
content.CacheDependencies.Add(schemaIdentity);
content.CacheDependencies.Add(schemaVersion);
content.CacheDependencies = new HashSet<object>
{
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);
}
@ -129,12 +139,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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)
{
var fieldReference = content.ReferenceData[field.Name];
@ -151,9 +155,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (referencedContents.Count == 1)
{
var value =
formatted.GetOrAdd(referencedContents[0],
x => x.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig));
var reference = referencedContents[0];
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);
}
@ -165,9 +174,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
}
}
content.CacheDependencies.Add(schemaIdentity);
content.CacheDependencies.Add(schemaVersion);
}
}
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)
{
var text = $"{referencedContents.Count} Reference(s)";
@ -244,12 +255,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return info.Color;
}
private static bool ShouldEnrichWithSchema(Context context)
{
return context.IsFrontendClient;
}
private static bool ShouldEnrichWithStatuses(Context context)
{
return context.IsFrontendClient || context.IsResolveFlow();
}
private static bool ShouldEnrichWithReferences(Context context)
private static bool ShouldEnrich(Context context)
{
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();
}
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));
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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)
{

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

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

@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Entities
{
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.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Threading;
namespace Squidex.Infrastructure.Log.Internal
{
@ -18,7 +18,7 @@ namespace Squidex.Infrastructure.Log.Internal
private const int MaxQueuedMessages = 1024;
private readonly IConsole console;
private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages);
private readonly Task outputTask;
private readonly Thread outputThread;
public ConsoleLogProcessor()
{
@ -31,7 +31,11 @@ namespace Squidex.Infrastructure.Log.Internal
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)
@ -52,18 +56,25 @@ namespace Squidex.Infrastructure.Log.Internal
WriteMessage(message);
}
private static void ProcessLogQueue(object state)
{
var processor = (ConsoleLogProcessor)state;
processor.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
{
outputTask.Wait(1500);
outputThread.Join(1500);
}
catch (Exception ex)
{

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

@ -7,9 +7,10 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using NodaTime;
namespace Squidex.Infrastructure.Log.Internal
@ -19,7 +20,7 @@ namespace Squidex.Infrastructure.Log.Internal
private const int MaxQueuedMessages = 1024;
private const int Retries = 10;
private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages);
private readonly Task outputTask;
private readonly Thread outputThread;
private readonly string path;
private StreamWriter writer;
@ -27,31 +28,10 @@ namespace Squidex.Infrastructure.Log.Internal
{
this.path = path;
outputTask = Task.Factory.StartNew(ProcessLogQueue, this, TaskCreationOptions.LongRunning);
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
outputThread = new Thread(ProcessLogQueue)
{
messageQueue.CompleteAdding();
try
{
outputTask.Wait(1500);
}
catch (Exception ex)
{
if (!ex.Is<OperationCanceledException>())
{
throw;
}
}
finally
{
writer.Dispose();
}
}
IsBackground = true, Name = "Logging"
};
}
public void Initialize()
@ -72,6 +52,8 @@ namespace Squidex.Infrastructure.Log.Internal
};
writer.WriteLine($"--- Started Logging {SystemClock.Instance.GetCurrentInstant()} ---", 1);
outputThread.Start();
}
catch (Exception ex)
{
@ -84,35 +66,63 @@ namespace Squidex.Infrastructure.Log.Internal
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);
break;
}
catch (Exception ex)
{
await Task.Delay(i * 10);
if (i == Retries)
try
{
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.ComponentModel.DataAnnotations;
using System.Linq;
using NodaTime;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities;
@ -84,6 +86,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary>
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>
/// The version of the content.
/// </summary>
@ -104,6 +121,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
response.DataDraft = content.DataDraft;
}
if (content.ReferenceFields != null)
{
response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromField).ToArray();
}
if (content.ScheduleJob != null)
{
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]
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
{
@ -44,9 +45,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
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)
@ -58,18 +64,15 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
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))
{
AddPostLink("create", controller.Url<ContentsController>(x => nameof(x.PostContent), values));
if (controller.HasPermission(Permissions.AppContentsCreate, app, schema))
{
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;

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());
}
public FieldPropertiesDto Visit(AssetsFieldProperties properties)
{
var result = SimpleMapper.Map(properties, new AssetsFieldPropertiesDto());
result.AllowedExtensions = properties.AllowedExtensions?.ToArray();
return result;
}
public FieldPropertiesDto Visit(BooleanFieldProperties properties)
{
return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto());
@ -50,50 +59,45 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters
return SimpleMapper.Map(properties, new JsonFieldPropertiesDto());
}
public FieldPropertiesDto Visit(ReferencesFieldProperties properties)
{
return SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto());
}
public FieldPropertiesDto Visit(UIFieldProperties properties)
{
return SimpleMapper.Map(properties, new UIFieldPropertiesDto());
}
public FieldPropertiesDto Visit(TagsFieldProperties properties)
public FieldPropertiesDto Visit(NumberFieldProperties properties)
{
var result = SimpleMapper.Map(properties, new TagsFieldPropertiesDto());
var result = SimpleMapper.Map(properties, new NumberFieldPropertiesDto());
result.AllowedValues = properties.AllowedValues?.ToArray();
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;
}
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();
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();
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.ComponentModel.DataAnnotations;
using Squidex.Areas.Api.Controllers.Schemas.Models.Converters;
using Squidex.Areas.Api.Controllers.Schemas.Models.Fields;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Reflection;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
@ -58,6 +61,47 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
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)
{
allowUpdate = allowUpdate && !IsLocked;

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

@ -7,6 +7,7 @@
using System;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection;
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; }
/// <summary>
/// The id of the referenced schema.
/// The id of the referenced schemas.
/// </summary>
public Guid SchemaId { get; set; }
public Guid[] SchemaIds { get; set; }
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.ComponentModel.DataAnnotations;
using Squidex.Areas.Api.Controllers.Schemas.Models.Converters;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
@ -54,36 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
foreach (var field in schema.SchemaDef.Fields)
{
var fieldPropertiesDto = FieldPropertiesDtoFactory.Create(field.RawProperties);
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.Fields.Add(FieldDto.FromField(field));
}
result.CreateLinks(controller, app);

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

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

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

@ -22,7 +22,7 @@ export class EventConsumersDto {
public readonly _links: ResourceLinks;
constructor(
public readonly items: EventConsumerDto[], links?: ResourceLinks
public readonly items: ReadonlyArray<EventConsumerDto>, links?: ResourceLinks
) {
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 email: string,
public readonly displayName: string,
public readonly permissions: string[] = [],
public readonly permissions: ReadonlyArray<string> = [],
public readonly isLocked?: boolean
) {
this._links = links;
@ -50,14 +50,14 @@ export class UserDto {
export interface CreateUserDto {
readonly email: string;
readonly displayName: string;
readonly permissions: string[];
readonly permissions: ReadonlyArray<string>;
readonly password: string;
}
export interface UpdateUserDto {
readonly email: string;
readonly displayName: string;
readonly permissions: string[];
readonly permissions: ReadonlyArray<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();
expect(eventConsumersState.snapshot.eventConsumers.values).toEqual([eventConsumer1, eventConsumer2]);
expect(eventConsumersState.snapshot.eventConsumers).toEqual([eventConsumer1, eventConsumer2]);
expect(eventConsumersState.snapshot.isLoaded).toBeTruthy();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
@ -87,7 +87,7 @@ describe('EventConsumersState', () => {
eventConsumersState.start(eventConsumer2).subscribe();
const newConsumer2 = eventConsumersState.snapshot.eventConsumers.at(1);
const newConsumer2 = eventConsumersState.snapshot.eventConsumers[1];
expect(newConsumer2).toEqual(updated);
});
@ -100,7 +100,7 @@ describe('EventConsumersState', () => {
eventConsumersState.stop(eventConsumer2).subscribe();
const newConsumer2 = eventConsumersState.snapshot.eventConsumers.at(1);
const newConsumer2 = eventConsumersState.snapshot.eventConsumers[1];
expect(newConsumer2).toEqual(updated);
});
@ -113,7 +113,7 @@ describe('EventConsumersState', () => {
eventConsumersState.reset(eventConsumer2).subscribe();
const newConsumer2 = eventConsumersState.snapshot.eventConsumers.at(1);
const newConsumer2 = eventConsumersState.snapshot.eventConsumers[1];
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 {
DialogService,
ImmutableArray,
shareSubscribed,
State
} from '@app/shared';
@ -26,7 +25,7 @@ interface Snapshot {
isLoaded?: boolean;
}
type EventConsumersList = ImmutableArray<EventConsumerDto>;
type EventConsumersList = ReadonlyArray<EventConsumerDto>;
@Injectable()
export class EventConsumersState extends State<Snapshot> {
@ -40,7 +39,7 @@ export class EventConsumersState extends State<Snapshot> {
private readonly dialogs: DialogService,
private readonly eventConsumersService: EventConsumersService
) {
super({ eventConsumers: ImmutableArray.empty() });
super({ eventConsumers: [] });
}
public load(isReload = false, silent = false): Observable<any> {
@ -49,13 +48,11 @@ export class EventConsumersState extends State<Snapshot> {
}
return this.eventConsumersService.getEventConsumers().pipe(
tap(({ items }) => {
tap(({ items: eventConsumers }) => {
if (isReload && !silent) {
this.dialogs.notifyInfo('Event Consumers reloaded.');
}
const eventConsumers = ImmutableArray.of(items);
this.next(s => {
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();
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.isLoaded).toBeTruthy();
@ -178,7 +178,7 @@ describe('UsersState', () => {
usersState.select(user2.id).subscribe();
usersState.lock(user2).subscribe();
const user2New = usersState.snapshot.users.at(1);
const user2New = usersState.snapshot.users[1];
expect(user2New).toBe(usersState.snapshot.selectedUser!);
});
@ -192,7 +192,7 @@ describe('UsersState', () => {
usersState.select(user2.id).subscribe();
usersState.unlock(user2).subscribe();
const user2New = usersState.snapshot.users.at(1);
const user2New = usersState.snapshot.users[1];
expect(user2New).toEqual(updated);
expect(user2New).toBe(usersState.snapshot.selectedUser!);
@ -209,7 +209,7 @@ describe('UsersState', () => {
usersState.select(user2.id).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).toBe(usersState.snapshot.selectedUser!);
@ -223,7 +223,7 @@ describe('UsersState', () => {
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);
});
});

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

@ -13,7 +13,6 @@ import '@app/framework/utils/rxjs-extensions';
import {
DialogService,
ImmutableArray,
Pager,
shareSubscribed,
State
@ -46,7 +45,7 @@ interface Snapshot {
canCreate?: boolean;
}
export type UsersList = ImmutableArray<UserDto>;
export type UsersList = ReadonlyArray<UserDto>;
export type UsersResult = { total: number, users: UsersList };
@Injectable()
@ -70,7 +69,7 @@ export class UsersState extends State<Snapshot> {
private readonly dialogs: DialogService,
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> {
@ -110,14 +109,13 @@ export class UsersState extends State<Snapshot> {
this.snapshot.usersPager.pageSize,
this.snapshot.usersPager.skip,
this.snapshot.usersQuery).pipe(
tap(({ total, items, canCreate }) => {
tap(({ total, items: users, canCreate }) => {
if (isReload) {
this.dialogs.notifyInfo('Users reloaded.');
}
this.next(s => {
const usersPager = s.usersPager.setCount(total);
const users = ImmutableArray.of(items);
let selectedUser = s.selectedUser;
@ -141,7 +139,7 @@ export class UsersState extends State<Snapshot> {
return this.usersService.postUser(request).pipe(
tap(created => {
this.next(s => {
const users = s.users.pushFront(created);
const users = [created, ...s.users];
const usersPager = s.usersPager.incrementCount();
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 newsFeatures: FeatureDto[];
public newsFeatures: ReadonlyArray<FeatureDto>;
public newsDialog = new DialogModel();
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();
@Input()
public features: FeatureDto[];
public features: ReadonlyArray<FeatureDto>;
public emitClose() {
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);
}
public selectTags(tags: string[]) {
public selectTags(tags: ReadonlyArray<string>) {
this.assetsState.selectTags(tags);
}
@ -49,7 +49,7 @@ export class AssetsFiltersPageComponent {
this.assetsState.resetTags();
}
public trackByTag(tag: { name: string }) {
public trackByTag(index: number, tag: { name: string }) {
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);
}
public selectTags(tags: string[]) {
public selectTags(tags: ReadonlyArray<string>) {
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-value.component';
export * from './shared/content-value-editor.component';
export * from './shared/content-selector-item.component';
export * from './shared/contents-selector.component';
export * from './shared/due-time-selector.component';
export * from './shared/field-editor.component';
export * from './shared/preview-button.component';
export * from './shared/reference-item.component';
export * from './shared/references-editor.component';

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

@ -29,6 +29,7 @@ import {
ContentFieldComponent,
ContentHistoryPageComponent,
ContentPageComponent,
ContentSelectorItemComponent,
ContentsFiltersPageComponent,
ContentsPageComponent,
ContentsSelectorComponent,
@ -39,6 +40,7 @@ import {
FieldEditorComponent,
FieldLanguagesComponent,
PreviewButtonComponent,
ReferenceItemComponent,
ReferencesEditorComponent,
SchemasPageComponent
} from './declarations';
@ -110,10 +112,11 @@ const routes: Routes = [
ArrayItemComponent,
AssetsEditorComponent,
CommentsPageComponent,
ContentComponent,
ContentFieldComponent,
ContentHistoryPageComponent,
ContentComponent,
ContentPageComponent,
ContentSelectorItemComponent,
ContentsFiltersPageComponent,
ContentsPageComponent,
ContentsSelectorComponent,
@ -124,6 +127,7 @@ const routes: Routes = [
FieldEditorComponent,
FieldLanguagesComponent,
PreviewButtonComponent,
ReferenceItemComponent,
ReferencesEditorComponent,
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;
@Input()
public languages: AppLanguageDto[];
public languages: ReadonlyArray<AppLanguageDto>;
public selectedFormControl: AbstractControl;
public selectedFormControlCompare?: AbstractControl;
@ -145,7 +145,7 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
if (masterValue) {
if (this.showAllControls) {
for (let language of this.languages) {
for (const language of this.languages) {
if (!language.isMaster) {
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) {
const params = allParams(this.route);
for (let key in params) {
for (const key in params) {
if (params.hasOwnProperty(key)) {
const value = params[key];
@ -47,7 +47,7 @@ export class ContentHistoryPageComponent {
return channelPath;
}
public events: Observable<HistoryEventDto[]> =
public events: Observable<ReadonlyArray<HistoryEventDto>> =
merge(
timer(0, 10000),
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)"
[fieldFormCompare]="contentFormCompare?.form.get(field.name)"
[schema]="schema"
[languages]="languages.mutableValues"
[languages]="languages"
[(language)]="language">
</sqx-content-field>
</ng-container>

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

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

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

@ -48,5 +48,5 @@ export class FieldLanguagesComponent {
public language: AppLanguageDto;
@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>
</div>
<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 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">

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

@ -12,7 +12,6 @@ import {
AppLanguageDto,
ContentDto,
ContentsState,
ImmutableArray,
LanguagesState,
ModalModel,
Queries,
@ -42,10 +41,10 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public selectionCount = 0;
public selectionCanDelete = false;
public nextStatuses: string[] = [];
public nextStatuses: ReadonlyArray<string> = [];
public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>;
public languages: ReadonlyArray<AppLanguageDto>;
public queryModel: QueryModel;
public queries: Queries;
@ -96,7 +95,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.languagesState.languages
.subscribe(languages => {
this.languages = languages.map(x => x.language);
this.language = this.languages.at(0);
this.language = this.languages[0];
this.updateModel();
}));
@ -126,7 +125,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.contentsState.create(content.dataDraft, false);
}
private changeContentItems(contents: ContentDto[], action: string) {
private changeContentItems(contents: ReadonlyArray<ContentDto>, action: string) {
if (contents.length === 0) {
return;
}
@ -161,7 +160,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
}
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) {
@ -180,7 +179,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.selectedItems = {};
if (isSelected) {
for (let content of this.contentsState.snapshot.contents.values) {
for (let content of this.contentsState.snapshot.contents) {
this.selectedItems[content.id] = true;
}
}
@ -188,7 +187,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.updateSelectionSummary();
}
public trackByContent(content: ContentDto): string {
public trackByContent(index: number, content: ContentDto): string {
return content.id;
}
@ -199,17 +198,17 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
const allActions = {};
for (let content of this.contentsState.snapshot.contents.values) {
for (let info of content.statusUpdates) {
for (let content of this.contentsState.snapshot.contents) {
for (const info of content.statusUpdates) {
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]) {
this.selectionCount++;
for (let action in allActions) {
for (const action in allActions) {
if (!content.statusUpdates) {
delete allActions[action];
}
@ -234,7 +233,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
private updateModel() {
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;
@Input()
public languages: AppLanguageDto[];
public languages: ReadonlyArray<AppLanguageDto>;
@Input()
public arrayControl: FormArray;
@ -62,7 +62,7 @@ export class ArrayEditorComponent extends StatefulComponent<State> {
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++) {
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;
@Input()
public languages: AppLanguageDto[];
public languages: ReadonlyArray<AppLanguageDto>;
public isInvalid: Observable<boolean>;
public fieldControls: { field: FieldDto, control: AbstractControl }[];
public fieldControls: ReadonlyArray<{ field: FieldDto, control: AbstractControl }>;
public ngOnChanges(changes: SimpleChanges) {
if (changes['itemForm']) {

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

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

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

@ -13,7 +13,6 @@ import {
AssetDto,
AssetsService,
DialogModel,
ImmutableArray,
LocalStoreService,
MessageBus,
StatefulControlComponent,
@ -33,9 +32,9 @@ class AssetUpdated {
}
interface State {
assetFiles: ImmutableArray<File>;
assetFiles: ReadonlyArray<File>;
assets: ImmutableArray<AssetDto>;
assets: ReadonlyArray<AssetDto>;
isListView: boolean;
}
@ -47,6 +46,7 @@ interface State {
providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
// tslint:disable-next-line: readonly-array
export class AssetsEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit {
@Input()
public isCompact = false;
@ -60,30 +60,30 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
private readonly messageBus: MessageBus
) {
super(changeDetector, {
assets: ImmutableArray.empty(),
assetFiles: ImmutableArray.empty(),
assets: [],
assetFiles: [],
isListView: localStore.getBoolean('squidex.assets.list-view')
});
}
public writeValue(obj: any) {
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;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)
.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) {
this.updateValue();
}
}, () => {
this.setAssets(ImmutableArray.empty());
this.setAssets([]);
});
}
} 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 }));
}
public addFiles(files: File[]) {
for (let file of files) {
this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
public addFiles(files: ReadonlyArray<File>) {
for (const file of files) {
this.next(s => ({ ...s, assetFiles: [file, ...s.assetFiles] }));
}
}
public selectAssets(assets: AssetDto[]) {
this.setAssets(this.snapshot.assets.push(...assets));
public selectAssets(assets: ReadonlyArray<AssetDto>) {
this.setAssets([...this.snapshot.assets, ...assets]);
if (assets.length > 0) {
this.updateValue();
@ -125,17 +125,17 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
if (asset && file) {
this.next(s => ({
...s,
assetFiles: s.assetFiles.remove(file),
assets: s.assets.pushFront(asset)
assetFiles: s.assetFiles.removed(file),
assets: [asset, ...s.assets]
}));
this.updateValue();
}
}
public sortAssets(assets: AssetDto[]) {
public sortAssets(assets: ReadonlyArray<AssetDto>) {
if (assets) {
this.setAssets(ImmutableArray.of(assets));
this.setAssets(assets);
this.updateValue();
}
@ -143,14 +143,14 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
public removeLoadedAsset(asset: AssetDto) {
if (asset) {
this.setAssets(this.snapshot.assets.remove(asset));
this.setAssets(this.snapshot.assets.removed(asset));
this.updateValue();
}
}
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) {
@ -160,14 +160,15 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, strin
}
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) {
ids = null;
this.callChange(null);
} else {
this.callChange(ids);
}
this.callTouched();
this.callChange(ids);
}
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">
<ng-container [ngSwitch]="field.properties.fieldType">
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<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>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</ng-container>
@ -39,13 +39,13 @@ import { FieldDto } from '@app/shared';
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<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>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</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>
<ng-container *ngIf="!isReference; else referenceTemplate">
<input type="checkbox" class="form-check"
[disabled]="!selectable"
[ngModel]="selected || !selectable"
(ngModelChange)="emitSelectedChange($event)" />
</ng-container>
<ng-template #referenceTemplate>
<i class="icon-drag2 drag-handle"></i>
</ng-template>
<input type="checkbox" class="form-check"
[ngModel]="selected"
(ngModelChange)="emitSelectedChange($event)" />
</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">
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i>
@ -79,23 +72,5 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</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 class="spacer"></tr>

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

@ -1,24 +1,2 @@
@import '_vars';
@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;
}
}
@import '_mixins';

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

@ -47,9 +47,6 @@ export class ContentComponent implements OnChanges {
@Input()
public selected = false;
@Input()
public selectable = true;
@Input()
public language: AppLanguageDto;
@ -57,17 +54,11 @@ export class ContentComponent implements OnChanges {
public schema: SchemaDetailsDto;
@Input()
public schemaFields: RootFieldDto[];
public schemaFields: ReadonlyArray<RootFieldDto>;
@Input()
public canClone: boolean;
@Input()
public isReadOnly = false;
@Input()
public isReference = false;
@Input()
public isCompact = false;
@ -84,7 +75,7 @@ export class ContentComponent implements OnChanges {
public dropdown = new ModalModel();
public values: any[] = [];
public values: ReadonlyArray<any> = [];
public get isDirty() {
return this.patchForm && this.patchForm.form.dirty;
@ -99,7 +90,7 @@ export class ContentComponent implements OnChanges {
public ngOnChanges(changes: SimpleChanges) {
if (changes['content']) {
this.patchAllowed = !this.isReadOnly && this.content.canUpdate;
this.patchAllowed = this.content.canUpdate;
}
if (changes['schema'] || changes['language']) {
@ -157,12 +148,12 @@ export class ContentComponent implements OnChanges {
}
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);
this.values.push(formatted);
values.push(formatted);
if (this.patchForm) {
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;
}
}

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">
<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 tabs>
<div class="row no-gutters">
<div class="col-auto offset-lg-4">
<button type="button" class="btn btn-text-secondary" (click)="reload()">
<i class="icon-reset"></i> Refresh
</button>
</div>
<div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Search for content"
[query]="contentsState.contentsQuery | async"
[queryModel]="queryModel"
(queryChange)="search($event)">
</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>
<ng-container *ngIf="schema">
<div class="col-auto">
<button type="button" class="btn btn-text-secondary" (click)="reload()">
<i class="icon-reset"></i> Refresh
</button>
</div>
<div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Search for content"
[query]="contentsState.contentsQuery | async"
[queryModel]="queryModel"
(queryChange)="search($event)">
</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>
</ng-container>
</div>
</ng-container>
<ng-container content>
<div class="grid-header">
<table class="table table-items table-fixed" [style.minWidth]="minWidth" #header>
<thead>
<tr>
<th class="cell-select">
<input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-user">
<sqx-table-header text="By"></sqx-table-header>
</th>
<th class="cell-content" *ngFor="let field of schema.referenceFields">
<sqx-table-header [text]="field.displayName"
[sortable]="field.properties.isSortable"
[field]="field"
[query]="contentsState.contentsQuery | async"
(queryChange)="search($event)"
[language]="language">
</sqx-table-header>
</th>
<th class="cell-time">
<sqx-table-header text="Updated"
[sortable]="true"
[sortable]="field.properties.isSortable"
[field]="'lastModified'"
[query]="contentsState.contentsQuery | async"
(queryChange)="search($event)"
[language]="language">
</sqx-table-header>
</th>
</tr>
</thead>
</table>
</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>
<ng-container *ngIf="schema">
<div class="grid-header">
<table class="table table-items table-fixed" [style.minWidth]="minWidth" #header>
<thead>
<tr>
<th class="cell-select">
<input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-user">
<sqx-table-header text="By"></sqx-table-header>
</th>
<th class="cell-content" *ngFor="let field of schema.referenceFields">
<sqx-table-header [text]="field.displayName"
[sortable]="field.properties.isSortable"
[field]="field"
[query]="contentsState.contentsQuery | async"
(queryChange)="search($event)"
[language]="language">
</sqx-table-header>
</th>
<th class="cell-time">
<sqx-table-header text="Updated"
[sortable]="true"
[sortable]="field.properties.isSortable"
[field]="'lastModified'"
[query]="contentsState.contentsQuery | async"
(queryChange)="search($event)"
[language]="language">
</sqx-table-header>
</th>
</tr>
</thead>
</table>
</div>
</div>
<div class="grid-footer">
<sqx-pager [pager]="contentsState.contentsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager>
</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"
[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 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 {
display: inline-block;
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.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
ContentDto,
@ -15,7 +15,10 @@ import {
QueryModel,
queryModelFromSchema,
ResourceOwner,
SchemaDetailsDto
SchemaDetailsDto,
SchemaDto,
SchemasState,
Types
} from '@app/shared';
@Component({
@ -28,22 +31,25 @@ import {
})
export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
@Output()
public select = new EventEmitter<ContentDto[]>();
public select = new EventEmitter<ReadonlyArray<ContentDto>>();
@Input()
public schemaIds: ReadonlyArray<string>;
@Input()
public language: LanguageDto;
@Input()
public languages: LanguageDto[];
public languages: ReadonlyArray<LanguageDto>;
@Input()
public allowDuplicates: boolean;
@Input()
public alreadySelected: ContentDto[];
public alreadySelected: ReadonlyArray<ContentDto>;
@Input()
public schema: SchemaDetailsDto;
public schemas: ReadonlyArray<SchemaDto> = [];
public queryModel: QueryModel;
@ -54,22 +60,51 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
public minWidth: string;
constructor(
public readonly contentsState: ManualContentsState
public readonly contentsState: ManualContentsState,
public readonly schemasState: SchemasState,
private readonly changeDetector: ChangeDetectorRef
) {
super();
}
public ngOnInit() {
this.minWidth = `${200 + (200 * this.schema.referenceFields.length)}px`;
this.own(
this.contentsState.statuses
.subscribe(() => {
this.updateModel();
}));
this.contentsState.schema = this.schema;
this.contentsState.load();
this.schemas = this.schemasState.snapshot.schemas;
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() {
@ -112,8 +147,10 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
this.selectedItems = {};
if (isSelected) {
for (let content of this.contentsState.snapshot.contents.values) {
this.selectedItems[content.id] = content;
for (const content of this.contentsState.snapshot.contents) {
if (!this.isItemAlreadySelected(content)) {
this.selectedItems[content.id] = content;
}
}
}
@ -136,10 +173,12 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
}
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;
}
}

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>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="control" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</ng-container>
@ -44,7 +44,7 @@
</ng-container>
</ng-container>
<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 *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [isCompact]="isCompact" [formControl]="control"></sqx-geolocation-editor>
@ -53,21 +53,21 @@
<sqx-json-editor [formControl]="control"></sqx-json-editor>
</ng-container>
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="control" [placeholder]="field.displayPlaceholder" />
</ng-container>
<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 *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="control">
<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>
</ng-container>
<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" />
<label class="form-check-label">
{{value}}
@ -77,14 +77,14 @@
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'References'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'List'">
<sqx-references-editor
[formControl]="control"
[allowDuplicates]="field.properties['allowDuplicated']"
[allowDuplicates]="field.rawProperties.allowDuplicated"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']"
[schemaIds]="field.rawProperties.schemaIds"
[isCompact]="isCompact">
</sqx-references-editor>
</ng-container>
@ -92,13 +92,13 @@
<sqx-references-dropdown
[formControl]="control"
[language]="language"
[schemaId]="field.properties['schemaId']">
[schemaId]="field.rawProperties.singleId">
</sqx-references-dropdown>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="control" [placeholder]="field.displayPlaceholder" />
</ng-container>
@ -120,11 +120,11 @@
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="control">
<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>
</ng-container>
<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" />
<label class="form-check-label">
{{value}}
@ -137,16 +137,16 @@
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.properties['editor']">
<ng-container [ngSwitch]="field.rawProperties.editor">
<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 *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 *ngSwitchCase="'Dropdown'">
<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>
</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;
@Input()
public languages: AppLanguageDto[];
public languages: ReadonlyArray<AppLanguageDto>;
@Input()
public isCompact = false;

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

@ -20,7 +20,7 @@ import {
interface State {
selectedName?: string;
alternativeNames: string[];
alternativeNames: ReadonlyArray<string>;
}
@Component({
@ -77,9 +77,7 @@ export class PreviewButtonComponent extends StatefulComponent<State> implements
const keys = Object.keys(this.schema.previewUrls);
state.selectedName = selectedName;
state.alternativeNames = keys.filter(x => x !== s.selectedName);
state.alternativeNames.sort();
state.alternativeNames = keys.removed(s.selectedName).sorted();
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">
<ng-container *ngIf="snapshot.schema; let schema">
<ng-container>
<div class="drop-area-container">
<div class="drop-area" (click)="selectorDialog.show()">
Click here to link content items.
</div>
</div>
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="schema && snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems.mutableValues"
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems"
(sqxSort)="sort($event)">
<tbody *ngFor="let content of snapshot.contentItems; trackBy: trackByContent"
[sqxContent]="content"
[sqxReferenceItem]="content"
[language]="language"
[isReadOnly]="true"
[isReference]="true"
[isCompact]="isCompact"
[schema]="schema"
[schemaFields]="schema.referenceFields"
[columnCount]="snapshot.columnCount"
(delete)="remove(content)">
</tbody>
</table>
</ng-container>
<div class="invalid" *ngIf="snapshot.schemaInvalid">
Schema not found or not configured yet.
</div>
</div>
<ng-container *sqxModal="selectorDialog;closeAuto:false">
@ -33,7 +26,7 @@
[alreadySelected]="snapshot.contentItems"
[language]="language"
[languages]="languages"
[schema]="snapshot.schema"
[schemaIds]="schemaIds"
(select)="select($event)">
</sqx-contents-selector>
</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.
*/
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 {
@ -14,10 +14,6 @@ import {
ContentDto,
ContentsService,
DialogModel,
ImmutableArray,
MathHelper,
SchemaDetailsDto,
SchemasService,
StatefulControlComponent,
Types
} from '@app/shared';
@ -27,10 +23,9 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
};
interface State {
schema?: SchemaDetailsDto | null;
schemaInvalid: boolean;
contentItems: ReadonlyArray<ContentDto>;
contentItems: ImmutableArray<ContentDto>;
columnCount: number;
}
@Component({
@ -40,15 +35,16 @@ interface State {
providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR],
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()
public schemaId: string;
public schemaIds: ReadonlyArray<string>;
@Input()
public language: AppLanguageDto;
@Input()
public languages: AppLanguageDto[];
public languages: ReadonlyArray<AppLanguageDto>;
@Input()
public isCompact = false;
@ -56,64 +52,52 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, s
@Input()
public allowDuplicates = true;
@Input()
public columnCount = 0;
public selectorDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService
private readonly contentsService: ContentsService
) {
super(changeDetector, {
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 }));
});
super(changeDetector, { contentItems: [], columnCount: 0 });
}
public writeValue(obj: any) {
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;
this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
this.contentsService.getContentsByIds(this.appsState.appName, contentIds)
.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) {
this.updateValue();
}
}, () => {
this.setContentItems(ImmutableArray.empty());
this.setContentItems([]);
});
}
} else {
this.setContentItems(ImmutableArray.empty());
this.setContentItems([]);
}
}
public setContentItems(contentItems: ImmutableArray<ContentDto>) {
this.next(s => ({ ...s, contentItems }));
}
public setContentItems(contentItems: ReadonlyArray<ContentDto>) {
let columnCount = 1;
public select(contents: ContentDto[]) {
for (let content of contents) {
this.setContentItems(this.snapshot.contentItems.push(content));
for (const content of contentItems) {
columnCount = Math.max(columnCount, content.referenceFields.length);
}
this.next(s => ({ ...s, contentItems, columnCount }));
}
public select(contents: ReadonlyArray<ContentDto>) {
this.setContentItems([...this.snapshot.contentItems, ...contents]);
if (contents.length > 0) {
this.updateValue();
}
@ -123,29 +107,30 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, s
public remove(content: ContentDto) {
if (content) {
this.setContentItems(this.snapshot.contentItems.remove(content));
this.setContentItems(this.snapshot.contentItems.filter(x => x.id !== content.id));
this.updateValue();
}
}
public sort(contents: ContentDto[]) {
public sort(contents: ReadonlyArray<ContentDto>) {
if (contents) {
this.setContentItems(ImmutableArray.of(contents));
this.setContentItems(contents);
this.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) {
ids = null;
this.callChange(null);
} else {
this.callChange(ids);
}
this.callTouched();
this.callChange(ids);
}
public trackByContent(index: number, content: ContentDto) {

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

@ -19,7 +19,7 @@ import {
UsagesService
} from '@app/shared';
const COLORS = [
const COLORS: ReadonlyArray<string> = [
' 51, 137, 213',
'211, 50, 50',
'131, 211, 50',
@ -85,7 +85,7 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
maintainAspectRatio: false
};
public history: HistoryEventDto[] = [];
public history: ReadonlyArray<HistoryEventDto> = [];
public assetsCurrent = 0;
public assetsMax = 0;
@ -207,10 +207,10 @@ function label(category: string) {
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'));
}
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]]);
}

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 ngOnInit() {
for (let property of this.definition.properties) {
for (const property of this.definition.properties) {
const validators = [];
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 {
Form,
ImmutableArray,
RuleDto,
RuleElementDto,
RulesState,
@ -37,7 +36,7 @@ export class RuleWizardComponent implements AfterViewInit, OnInit {
public ruleTriggers: { [name: string]: RuleElementDto };
@Input()
public schemas: ImmutableArray<SchemaDto>;
public schemas: ReadonlyArray<SchemaDto>;
@Input()
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 { FormControl, FormGroup } from '@angular/forms';
import {
ImmutableArray,
SchemaDto,
Types
} from '@app/shared';
import { SchemaDto, Types } from '@app/shared';
export interface TriggerSchemaForm {
schema: SchemaDto;
@ -27,7 +23,7 @@ export interface TriggerSchemaForm {
})
export class ContentChangedTriggerComponent implements OnInit {
@Input()
public schemas: ImmutableArray<SchemaDto>;
public schemas: ReadonlyArray<SchemaDto>;
@Input()
public trigger: any;
@ -38,10 +34,10 @@ export class ContentChangedTriggerComponent implements OnInit {
@Input()
public triggerFormSubmitted = false;
public triggerSchemas: ImmutableArray<TriggerSchemaForm>;
public triggerSchemas: ReadonlyArray<TriggerSchemaForm>;
public schemaToAdd: SchemaDto;
public schemasToAdd: ImmutableArray<SchemaDto>;
public schemasToAdd: ReadonlyArray<SchemaDto>;
public get hasSchema() {
return !!this.schemaToAdd;
@ -57,7 +53,7 @@ export class ContentChangedTriggerComponent implements OnInit {
const schemas: TriggerSchemaForm[] = [];
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);
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();
}
public removeSchema(schemaForm: TriggerSchemaForm) {
this.triggerSchemas = this.triggerSchemas.remove(schemaForm);
this.triggerSchemas = this.triggerSchemas.removed(schemaForm);
this.updateValue();
this.updateSchemaToAdd();
}
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.updateSchemaToAdd();
@ -94,14 +90,14 @@ export class ContentChangedTriggerComponent implements OnInit {
}
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);
}
private updateSchemaToAdd() {
this.schemasToAdd = this.schemas.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortByStringAsc(x => x.name);
this.schemaToAdd = this.schemasToAdd.at(0);
this.schemasToAdd = this.schemas.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortedByString(x => x.name);
this.schemaToAdd = this.schemasToAdd[0];
}
public trackBySchema(index: number, schema: SchemaDto) {

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

@ -14,7 +14,6 @@ import {
DialogService,
EditFieldForm,
fadeAnimation,
ImmutableArray,
ModalModel,
NestedFieldDto,
PatternDto,
@ -42,7 +41,7 @@ export class FieldComponent implements OnChanges {
public parent: RootFieldDto;
@Input()
public patterns: ImmutableArray<PatternDto>;
public patterns: ReadonlyArray<PatternDto>;
public dropdown = new ModalModel();
@ -99,7 +98,7 @@ export class FieldComponent implements OnChanges {
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();
}

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 { FormGroup } from '@angular/forms';
import {
FieldDto,
ImmutableArray,
PatternDto
} from '@app/shared';
import { FieldDto, PatternDto } from '@app/shared';
@Component({
selector: 'sqx-field-form-validation',
@ -55,5 +51,5 @@ export class FieldFormValidationComponent {
public field: FieldDto;
@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 { FormGroup } from '@angular/forms';
import {
FieldDto,
ImmutableArray,
PatternDto
} from '@app/shared';
import { FieldDto, PatternDto } from '@app/shared';
@Component({
selector: 'sqx-field-form',
@ -63,7 +59,7 @@ export class FieldFormComponent implements AfterViewInit {
public editFormSubmitted: boolean;
@Input()
public patterns: ImmutableArray<PatternDto>;
public patterns: ReadonlyArray<PatternDto>;
@Input()
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();
}
public sortFields(fields: FieldDto[]) {
public sortFields(fields: ReadonlyArray<FieldDto>) {
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 showDefaultValue: Observable<boolean>;
public calculatedDefaultValues = ['Now', 'Today'];
public calculatedDefaultValues: ReadonlyArray<string> = ['Now', 'Today'];
public ngOnInit() {
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 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">
<select class="form-control" id="{{field.fieldId}}_fieldSchemaId" formControlName="schemaId">
<option *ngFor="let schema of schemasState.schemas | async" [ngValue]="schema.id">{{schema.displayName}}</option>
</select>
<sqx-tag-editor placeholder=", to add schema" formControlName="schemaIds"
[converter]="schemasSource" [suggestedValues]="schemasSource.suggestions">
</sqx-tag-editor>
</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 { 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({
selector: 'sqx-references-validation',
@ -26,7 +30,7 @@ export class ReferencesValidationComponent implements OnInit {
public properties: ReferencesFieldPropertiesDto;
constructor(
public readonly schemasState: SchemasState
public readonly schemasSource: SchemaTagConverter
) {
}
@ -40,9 +44,7 @@ export class ReferencesValidationComponent implements OnInit {
this.editForm.setControl('minItems',
new FormControl(this.properties.minItems));
this.editForm.setControl('schemaId',
new FormControl(this.properties.schemaId, [
Validators.required
]));
this.editForm.setControl('schemaIds',
new FormControl(this.properties.schemaIds));
}
}

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

@ -13,7 +13,6 @@ import {
fadeAnimation,
FieldDto,
hasNoValue$,
ImmutableArray,
ModalModel,
PatternDto,
ResourceOwner,
@ -41,7 +40,7 @@ export class StringValidationComponent extends ResourceOwner implements OnInit {
public properties: StringFieldPropertiesDto;
@Input()
public patterns: ImmutableArray<PatternDto>;
public patterns: ReadonlyArray<PatternDto>;
public showDefaultValue: Observable<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;
@Input()
public clientRoles: RoleDto[];
public clientRoles: ReadonlyArray<RoleDto>;
public connectToken: AccessTokenDto;
public connectDialog = new DialogModel();

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

@ -16,7 +16,6 @@ import {
ContributorsState,
DialogModel,
DialogService,
ImmutableArray,
RoleDto,
UsersService
} 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(
withLatestFrom(this.contributorsState.contributors, (users, contributors) => {
const results: any[] = [];
for (let user of users) {
for (const user of users) {
if (!contributors!.find(t => t.contributorId === user.id)) {
results.push(user);
}
@ -56,7 +55,7 @@ export class ContributorAddFormComponent implements OnInit {
private defaultValue: any;
@Input()
public roles: ImmutableArray<RoleDto>;
public roles: ReadonlyArray<RoleDto>;
public assignContributorForm = new AssignContributorForm(this.formBuilder);
@ -71,7 +70,7 @@ export class ContributorAddFormComponent implements OnInit {
}
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 });
}

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

@ -12,7 +12,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import {
ContributorDto,
ContributorsState,
ImmutableArray,
RoleDto
} from '@app/shared';
@ -45,7 +44,7 @@ import {
})
export class ContributorComponent {
@Input()
public roles: ImmutableArray<RoleDto>;
public roles: ReadonlyArray<RoleDto>;
@Input()
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);
}
public trackByContributor(contributor: ContributorDto) {
public trackByContributor(index: number, contributor: ContributorDto) {
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 {
ContributorsState,
ErrorDto,
ImmutableArray,
ImportContributorsForm,
RoleDto
} from '@app/shared';
interface ImportStatus {
type ImportStatus = {
email: string;
result: 'Pending' | 'Failed' | 'Success';
resultText: string;
role: string;
}
};
@Component({
selector: 'sqx-import-contributors-dialog',
@ -37,10 +36,10 @@ export class ImportContributorsDialogComponent {
public close = new EventEmitter();
@Input()
public roles: ImmutableArray<RoleDto>;
public roles: ReadonlyArray<RoleDto>;
public importForm = new ImportContributorsForm(this.formBuilder);
public importStatus: ImportStatus[] = [];
public importStatus: ReadonlyArray<ImportStatus> = [];
public importStage: 'Start' | 'Change' | 'Wait' = 'Start';
constructor(
@ -54,15 +53,13 @@ export class ImportContributorsDialogComponent {
const contributors = this.importForm.submit();
if (contributors && contributors.length > 0) {
for (let contributor of contributors) {
this.importStatus.push({
email: contributor.contributorId,
result: 'Pending',
resultText: 'Pending',
role: 'Developer'
});
}
if (contributors) {
this.importStatus = contributors.map(contributor => ({
email: contributor.contributorId,
result: 'Pending',
resultText: 'Pending',
role: 'Developer'
}));
}
}
@ -73,7 +70,7 @@ export class ImportContributorsDialogComponent {
mergeMap(s =>
this.contributorsState.assign(createRequest(s), { silent: true }).pipe(
tap(created => {
let status = this.importStatus.find(x => x.email === s.email);
const status = this.importStatus.find(x => x.email === s.email);
if (status) {
status.resultText = getSuccess(created);
@ -81,7 +78,7 @@ export class ImportContributorsDialogComponent {
}
}),
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) {
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 {
AddLanguageForm,
ImmutableArray,
LanguageDto,
LanguagesState
} from '@app/shared';
@ -36,7 +35,7 @@ import {
})
export class LanguageAddFormComponent implements OnChanges {
@Input()
public newLanguages: ImmutableArray<LanguageDto>;
public newLanguages: ReadonlyArray<LanguageDto>;
public addLanguageForm = new AddLanguageForm(this.formBuilder);
@ -48,7 +47,7 @@ export class LanguageAddFormComponent implements OnChanges {
public ngOnChanges() {
if (this.newLanguages.length > 0) {
const language = this.newLanguages.at(0);
const language = this.newLanguages[0];
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="fallback-languages"
[sqxSortModel]="fallbackLanguages.mutableValues"
[sqxSortModel]="fallbackLanguages"
[sqxSortDisabled]="!isEditable"
*ngIf="fallbackLanguages.length > 0">
<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,
EditLanguageForm,
fadeAnimation,
ImmutableArray,
LanguagesState
} from '@app/shared';
@ -29,10 +28,10 @@ export class LanguageComponent implements OnChanges {
public language: AppLanguageDto;
@Input()
public fallbackLanguages: ImmutableArray<AppLanguageDto>;
public fallbackLanguages: ReadonlyArray<AppLanguageDto>;
@Input()
public fallbackLanguagesNew: ImmutableArray<AppLanguageDto>;
public fallbackLanguagesNew: ReadonlyArray<AppLanguageDto>;
public otherLanguage: AppLanguageDto;
@ -53,7 +52,7 @@ export class LanguageComponent implements OnChanges {
this.editForm.load(this.language);
this.editForm.setEnabled(this.isEditable);
this.otherLanguage = this.fallbackLanguagesNew.at(0);
this.otherLanguage = this.fallbackLanguagesNew[0];
}
public toggleEditing() {
@ -72,7 +71,7 @@ export class LanguageComponent implements OnChanges {
const value = this.editForm.submit();
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)
.subscribe(() => {
@ -86,17 +85,17 @@ export class LanguageComponent implements OnChanges {
}
public removeFallbackLanguage(language: AppLanguageDto) {
this.fallbackLanguages = this.fallbackLanguages.remove(language);
this.fallbackLanguagesNew = this.fallbackLanguagesNew.push(language).sortByStringAsc(x => x.iso2Code);
this.fallbackLanguages = this.fallbackLanguages.removed(language);
this.fallbackLanguagesNew = [...this.fallbackLanguagesNew, language].sortedByString(x => x.iso2Code);
this.otherLanguage = this.fallbackLanguagesNew.at(0);
this.otherLanguage = this.fallbackLanguagesNew[0];
}
public addFallbackLanguage() {
this.fallbackLanguages = this.fallbackLanguages.push(this.otherLanguage);
this.fallbackLanguagesNew = this.fallbackLanguagesNew.remove(this.otherLanguage);
this.fallbackLanguages = [...this.fallbackLanguages, this.otherLanguage].sortedByString(x => x.iso2Code);
this.fallbackLanguagesNew = this.fallbackLanguagesNew.removed(this.otherLanguage);
this.otherLanguage = this.fallbackLanguagesNew.at(0);
this.otherLanguage = this.fallbackLanguagesNew[0];
}
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) {
return;
}

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

@ -17,13 +17,13 @@ import {
} from '@app/shared';
class PermissionsAutocomplete implements AutocompleteSource {
private permissions: string[] = [];
private permissions: ReadonlyArray<string> = [];
constructor(appsState: AppsState, rolesService: RolesService) {
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));
}
}
@ -51,7 +51,7 @@ export class RolesPageComponent implements OnInit {
this.rolesState.load(true);
}
public trackByRole(role: RoleDto) {
public trackByRole(index: number, role: RoleDto) {
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;
@Input()
public roles: RoleDto[];
public roles: ReadonlyArray<RoleDto>;
@Input()
public disabled: boolean;
public onBlur = { updateOn: 'blur' };
public openSteps: WorkflowStep[];
public openSteps: ReadonlyArray<WorkflowStep>;
public openStep: WorkflowStep;
public transitions: WorkflowTransitionView[];
public transitions: ReadonlyArray<WorkflowTransitionView>;
public ngOnChanges(changes: SimpleChanges) {
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;
@Input()
public roles: RoleDto[];
public roles: ReadonlyArray<RoleDto>;
@Input()
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.
*/
// tslint:disable: readonly-array
import { Component, Input, OnChanges } from '@angular/core';
import {
ErrorDto,
MathHelper,
RoleDto,
SchemaTagConverter,
WorkflowDto,
WorkflowsState,
WorkflowStep,
@ -19,8 +22,6 @@ import {
WorkflowTransitionValues
} from '@app/shared';
import { SchemaTagConverter } from './schema-tag-converter';
@Component({
selector: 'sqx-workflow',
styleUrls: ['./workflow.component.scss'],
@ -31,7 +32,7 @@ export class WorkflowComponent implements OnChanges {
public workflow: WorkflowDto;
@Input()
public roles: RoleDto[];
public roles: ReadonlyArray<RoleDto>;
@Input()
public schemasSource: SchemaTagConverter;
@ -74,7 +75,7 @@ export class WorkflowComponent implements OnChanges {
}
public addStep() {
let index = this.workflow.steps.length;
const index = this.workflow.steps.length;
for (let i = index; i < index + 100; 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.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import {
RolesState,
SchemasState,
SchemaTagConverter,
WorkflowDto,
WorkflowsState
} from '@app/shared';
import { SchemaTagConverter } from './schema-tag-converter';
@Component({
selector: 'sqx-workflows-page',
styleUrls: ['./workflows-page.component.scss'],
templateUrl: './workflows-page.component.html'
})
export class WorkflowsPageComponent implements OnInit, OnDestroy {
public schemasSource: SchemaTagConverter;
export class WorkflowsPageComponent implements OnInit {
constructor(
public readonly rolesState: RolesState,
public readonly schemasState: SchemasState,
public readonly schemasSource: SchemaTagConverter,
public readonly workflowsState: WorkflowsState
) {
}
@ -34,21 +30,14 @@ export class WorkflowsPageComponent implements OnInit, OnDestroy {
public ngOnInit() {
this.rolesState.load();
this.schemasSource = new SchemaTagConverter(this.schemasState);
this.schemasState.load();
this.workflowsState.load();
}
public ngOnDestroy() {
this.schemasSource.destroy();
}
public reload() {
this.workflowsState.load(true);
}
public trackByWorkflow(workflow: WorkflowDto) {
public trackByWorkflow(index: number, workflow: WorkflowDto) {
return workflow.id;
}
}

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

Loading…
Cancel
Save