diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs index 3f6013f9e..bca51a0cc 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ b/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 SchemaIds { get; set; } + + public Guid SchemaId + { + set + { + if (value != default) + { + SchemaIds = new ReadOnlyCollection(new List { value }); + } + else + { + SchemaIds = null; + } + } + } public override T Accept(IFieldPropertiesVisitor visitor) { diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs index 0e2274691..71d912b26 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs +++ b/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 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> ResolvingReferences(this Schema schema) { return schema.Fields.OfType>() .Where(x => - x.Properties.SchemaId != Guid.Empty && x.Properties.ResolveReference && x.Properties.MaxItems == 1 && (x.Properties.IsListField || schema.Fields.Count == 1)); diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs index b8c0e33e9..240dffb99 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds public IJsonValue Visit(IField field) { - if (oldReferences.Contains(field.Properties.SchemaId)) + if (oldReferences.Contains(field.Properties.SingleId())) { return JsonValue.Array(); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs index df024acc0..90fdb52a6 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ b/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; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs index a73183637..95e179558 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs @@ -134,10 +134,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new UniqueValuesValidator(); } - if (field.Properties.SchemaId != Guid.Empty) - { - yield return new ReferencesValidator(field.Properties.SchemaId); - } + yield return new ReferencesValidator(field.Properties.SchemaIds); } public IEnumerable Visit(IField field) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index bf52c5824..ec4740c39 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/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> CheckContents(Guid schemaId, FilterNode filter); + public delegate Task> CheckContents(Guid schemaId, FilterNode filter); + + public delegate Task> CheckContentsByIds(HashSet ids); public delegate Task> CheckAssets(IEnumerable 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 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.Empty, false) + : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false) { } @@ -56,16 +60,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent Guid contentId, Guid schemaId, CheckContents checkContent, + CheckContentsByIds checkContentByIds, CheckAssets checkAsset, ImmutableQueue 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> GetContentIdsAsync(HashSet ids) + { + return checkContentByIds(ids); } - public Task> GetContentIdsAsync(Guid validatedSchemaId, FilterNode filter) + public Task> GetContentIdsAsync(Guid schemaId, FilterNode filter) { - return checkContent(validatedSchemaId, filter); + return checkContent(schemaId, filter); } public Task> GetAssetInfosAsync(IEnumerable assetId) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index 91066eab2..62ad9a34c 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/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 schemaIds; - private readonly Guid schemaId; - - public ReferencesValidator(Guid schemaId) + public ReferencesValidator(IEnumerable schemaIds) { - this.schemaId = schemaId; + this.schemaIds = schemaIds; } public async Task ValidateAsync(object value, ValidationContext context, AddError addError) { if (value is ICollection 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."); + } } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs index 6717f242b..6fad491b9 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ b/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."); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 47638300c..7988b72a7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -169,15 +169,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents }); } - public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) + public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode 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> QueryIdsAsync(HashSet 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> QueryIdsAsync(Guid appId) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 2e78ae822..c7d13da72 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) + public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) { using (Profiler.TraceMethod()) { @@ -124,6 +124,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } + public async Task> QueryIdsAsync(Guid appId, HashSet ids) + { + using (Profiler.TraceMethod()) + { + return await contents.QueryIdsAsync(ids); + } + } + public async Task QueryScheduledWithoutDataAsync(Instant now, Func callback) { using (Profiler.TraceMethod()) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 5f91f13ea..974094d57 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/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 CacheDependencies { get; set; } + public HashSet CacheDependencies { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index 28d9b75e9..d5752543a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/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> QueryAssetsAsync(IEnumerable assetIds) @@ -122,11 +125,16 @@ namespace Squidex.Domain.Apps.Entities.Contents return await assetRepository.QueryAsync(appEntity.Id, new HashSet(assetIds)); } - private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) + private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) { return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); } + private async Task> QueryContentsAsync(HashSet ids) + { + return await contentRepository.QueryIdsAsync(appEntity.Id, ids); + } + private string GetScript(Func script) { return script(schemaEntity.SchemaDef.Scripts); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index b128aabae..f5a27ffd9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/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 FindContentAsync(Guid schemaId, Guid id) + public Task 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> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) + public async Task> 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 GetContentsLoader(Guid schemaId) + private IDataLoader GetContentsLoader() { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"Schema_{schemaId}", + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"References", async batch => { - var result = await GetReferencedContentsAsync(schemaId, new List(batch)); + var result = await GetReferencedContentsAsync(new List(batch)); return result.ToDictionary(x => x.Id); }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index cf4adb1be..151264b20 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/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 contentTypes = new Dictionary(); - private readonly Dictionary contentDataTypes = new Dictionary(); - private readonly Dictionary schemasById; + private readonly Dictionary contentTypes = new Dictionary(); 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 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 schemas) + { + var schema = new GraphQLSchema + { + Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) + }; + + return schema; + } + public IFieldResolver ResolveAssetUrl() { var resolver = new FuncFieldResolver(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) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs index c953fc801..d945c3a83 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ b/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); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index bdb563a2b..128fd9eb5 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/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("id"); - return e.FindContentAsync(schemaId, contentId); + return e.FindContentAsync(contentId); }), Description = $"Find an {schemaName} content by id." }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs index 801f44c81..639ee6d55 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ b/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 { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index eb6ef19f5..517776b3f 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/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 { - 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()) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index 63f571c1a..d07ee4b82 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/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 { - 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(); + + 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 action) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs new file mode 100644 index 000000000..b1d0c4615 --- /dev/null +++ b/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 + { + 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 action) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs new file mode 100644 index 000000000..523b58032 --- /dev/null +++ b/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 types = new Dictionary(); + + public ContentUnionGraphType(string fieldName, Dictionary schemaTypes, IEnumerable 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; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs index 8279825fe..e038b0432 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ b/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 schemaTypes; private readonly ISchemaEntity schema; - private readonly Func schemaResolver; private readonly IGraphModel model; private readonly IGraphType assetListType; private readonly string fieldName; - public QueryGraphTypeVisitor(ISchemaEntity schema, Func schemaResolver, IGraphModel model, IGraphType assetListType, string fieldName) + public QueryGraphTypeVisitor(ISchemaEntity schema, + Dictionary 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 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)); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs index 66d125dd9..b8e45f2eb 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs +++ b/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; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 74db6ab52..612539c41 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/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 - { - 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 + { + 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 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(); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 7e68202c9..9f743e85e 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/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> GetReferencedContentsAsync(Guid schemaId, ICollection ids) + public virtual async Task> GetReferencedContentsAsync(ICollection 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) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index ca183aad1..5b06f2dc4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -25,7 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft); - Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); + Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); + + Task> QueryIdsAsync(Guid appId, HashSet ids); Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft); diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs index 512a2f4c6..1a6b5af96 100644 --- a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs @@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Entities { public interface IEntityWithCacheDependencies { - HashSet CacheDependencies { get; } + HashSet CacheDependencies { get; } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs b/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs index 06c76e40e..2957ceb63 100644 --- a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs +++ b/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 messageQueue = new BlockingCollection(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) { diff --git a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs b/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs index 80ad22c26..6171731af 100644 --- a/src/Squidex.Infrastructure/Log/Internal/FileLogProcessor.cs +++ b/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 messageQueue = new BlockingCollection(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()) - { - 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(); + } + } } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index dede3c34d..8399e0edd 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/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 /// public string StatusColor { get; set; } + /// + /// The name of the schema. + /// + public string SchemaName { get; set; } + + /// + /// The display name of the schema. + /// + public string SchemaDisplayName { get; set; } + + /// + /// The reference fields. + /// + public FieldDto[] ReferenceFields { get; set; } + /// /// The version of the content. /// @@ -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()); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 83ed3a14b..cd02ddaa7 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/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 FromContentsAsync(IResultList contents, Context context, ApiController controller, ISchemaEntity schema, IContentWorkflow workflow) + public static async Task FromContentsAsync(IResultList 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(x => nameof(x.GetContents), values)); + AddSelfLink(controller.Url(x => nameof(x.GetContents), values)); - if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) - { - AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); + if (controller.HasPermission(Permissions.AppContentsCreate, app, schema)) + { + AddPostLink("create", controller.Url(x => nameof(x.PostContent), values)); - AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); - } + AddPostLink("create/publish", controller.Url(x => nameof(x.PostContent), values) + "?publish=true"); } return this; diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs index c2cacdb89..9909d8435 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs +++ b/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()); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs index bc583bb03..b222fc132 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs +++ b/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 /// public List 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(); + + 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; diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index 8f144931c..d85708d51 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/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; } /// - /// The id of the referenced schema. + /// The id of the referenced schemas. /// - 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; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs index 716dc5ad0..d2b1402a3 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs +++ b/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(); - - 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); diff --git a/src/Squidex/app/app.module.ts b/src/Squidex/app/app.module.ts index bb9e56f10..425390ef0 100644 --- a/src/Squidex/app/app.module.ts +++ b/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) { diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.ts b/src/Squidex/app/features/administration/services/event-consumers.service.ts index 5fa97ebfb..10287e55a 100644 --- a/src/Squidex/app/features/administration/services/event-consumers.service.ts +++ b/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, links?: ResourceLinks ) { this._links = links || {}; } diff --git a/src/Squidex/app/features/administration/services/users.service.ts b/src/Squidex/app/features/administration/services/users.service.ts index 3d6cc1a05..9858cb832 100644 --- a/src/Squidex/app/features/administration/services/users.service.ts +++ b/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 = [], 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; readonly password: string; } export interface UpdateUserDto { readonly email: string; readonly displayName: string; - readonly permissions: string[]; + readonly permissions: ReadonlyArray; readonly password?: string; } diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts index 2a4c32c14..9849635be 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts +++ b/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); }); diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.ts b/src/Squidex/app/features/administration/state/event-consumers.state.ts index 4d686eed5..4d5668fa8 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.ts +++ b/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; +type EventConsumersList = ReadonlyArray; @Injectable() export class EventConsumersState extends State { @@ -40,7 +39,7 @@ export class EventConsumersState extends State { private readonly dialogs: DialogService, private readonly eventConsumersService: EventConsumersService ) { - super({ eventConsumers: ImmutableArray.empty() }); + super({ eventConsumers: [] }); } public load(isReload = false, silent = false): Observable { @@ -49,13 +48,11 @@ export class EventConsumersState extends State { } 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 }; }); diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/src/Squidex/app/features/administration/state/users.state.spec.ts index 90fd5321d..3242ab8fd 100644 --- a/src/Squidex/app/features/administration/state/users.state.spec.ts +++ b/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); }); }); diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index 8d2122a29..39239bdbd 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/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; +export type UsersList = ReadonlyArray; export type UsersResult = { total: number, users: UsersList }; @Injectable() @@ -70,7 +69,7 @@ export class UsersState extends State { 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 { @@ -110,14 +109,13 @@ export class UsersState extends State { 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 { 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 }; diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/src/Squidex/app/features/apps/pages/apps-page.component.ts index 1c73760fd..0519c76a0 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.ts +++ b/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; public newsDialog = new DialogModel(); public info: string; diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.ts b/src/Squidex/app/features/apps/pages/news-dialog.component.ts index b7dcd9889..5683be87b 100644 --- a/src/Squidex/app/features/apps/pages/news-dialog.component.ts +++ b/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; public emitClose() { this.close.emit(); diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts b/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts index 58991e740..a1f6c576c 100644 --- a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts +++ b/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) { 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; } } \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.ts b/src/Squidex/app/features/assets/pages/assets-page.component.ts index 9a5138018..fd2224127 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.ts +++ b/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) { this.assetsState.selectTags(tags); } diff --git a/src/Squidex/app/features/content/declarations.ts b/src/Squidex/app/features/content/declarations.ts index e6ac799a9..952e04938 100644 --- a/src/Squidex/app/features/content/declarations.ts +++ b/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'; \ No newline at end of file diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index e22b6b3c3..4fef616d1 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/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 ] diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/src/Squidex/app/features/content/pages/content/content-field.component.ts index 40f587cdf..cb6dec4b6 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.ts +++ b/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; 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); } diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.ts b/src/Squidex/app/features/content/pages/content/content-history-page.component.ts index 74410e37d..cac64e1c2 100644 --- a/src/Squidex/app/features/content/pages/content/content-history-page.component.ts +++ b/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 = + public events: Observable> = merge( timer(0, 10000), this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)) diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 969dddf9e..2a298404e 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/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"> diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 9234a177b..1eace2005 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/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; + public languages: ReadonlyArray; 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( diff --git a/src/Squidex/app/features/content/pages/content/field-languages.component.ts b/src/Squidex/app/features/content/pages/content/field-languages.component.ts index 22405ace2..06f9d0202 100644 --- a/src/Squidex/app/features/content/pages/content/field-languages.component.ts +++ b/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; } \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index 313bb0044..97fb041ec 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -24,7 +24,7 @@
- +
- -
- - - - - -
-
- \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content.component.scss b/src/Squidex/app/features/content/shared/content.component.scss index d56691f00..fbb752506 100644 --- a/src/Squidex/app/features/content/shared/content.component.scss +++ b/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; - } -} \ No newline at end of file +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content.component.ts b/src/Squidex/app/features/content/shared/content.component.ts index 56ad23dd7..58fe946fa 100644 --- a/src/Squidex/app/features/content/shared/content.component.ts +++ b/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; @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 = []; 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; } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.html b/src/Squidex/app/features/content/shared/contents-selector.component.html index c6e2958fe..f7859a974 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.html +++ b/src/Squidex/app/features/content/shared/contents-selector.component.html @@ -1,84 +1,96 @@ - Select contents +
+
+ +
+
-
- -
-
- - -
- -
- -
+ +
+ +
+
+ + +
+ +
+ +
+
-
- - - - - - - - - -
- - - - - - - - - -
-
- -
-
- - - + +
+
+ + + + + + + +
+ + + + + + + + + +
-
- +
+
+ + + +
+
+
+ + +
diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.scss b/src/Squidex/app/features/content/shared/contents-selector.component.scss index 0d2c43170..dc1189cf8 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.scss +++ b/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; diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.ts b/src/Squidex/app/features/content/shared/contents-selector.component.ts index 02a97c5f9..28e14086c 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.ts +++ b/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(); + public select = new EventEmitter>(); + + @Input() + public schemaIds: ReadonlyArray; @Input() public language: LanguageDto; @Input() - public languages: LanguageDto[]; + public languages: ReadonlyArray; @Input() public allowDuplicates: boolean; @Input() - public alreadySelected: ContentDto[]; + public alreadySelected: ReadonlyArray; - @Input() public schema: SchemaDetailsDto; + public schemas: ReadonlyArray = []; 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; } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 3d81911cf..1b89955ae 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -34,7 +34,7 @@ - + @@ -44,7 +44,7 @@ - + @@ -53,21 +53,21 @@ - + - + -
+