mirror of https://github.com/Squidex/squidex.git
47 changed files with 1302 additions and 575 deletions
@ -0,0 +1,104 @@ |
|||
// ==========================================================================
|
|||
// CachedGraphQLInvoker.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Read.Apps; |
|||
using Squidex.Domain.Apps.Read.Assets.Repositories; |
|||
using Squidex.Domain.Apps.Read.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Read.Schemas.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Domain.Apps.Read.Utils; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Squidex.Infrastructure.CQRS.Events; |
|||
using System; |
|||
using Squidex.Infrastructure.Tasks; |
|||
using Squidex.Domain.Apps.Events; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
|||
{ |
|||
public sealed class CachingGraphQLInvoker : CachingProviderBase, IGraphQLInvoker, IEventConsumer |
|||
{ |
|||
private readonly IContentRepository contentRepository; |
|||
private readonly IAssetRepository assetRepository; |
|||
private readonly ISchemaRepository schemaRepository; |
|||
|
|||
public string Name |
|||
{ |
|||
get { return GetType().Name; } |
|||
} |
|||
|
|||
public string EventsFilter |
|||
{ |
|||
get { return "^(schema-)|(apps-)"; } |
|||
} |
|||
|
|||
public CachingGraphQLInvoker(IMemoryCache cache, ISchemaRepository schemaRepository, IAssetRepository assetRepository, IContentRepository contentRepository) |
|||
: base(cache) |
|||
{ |
|||
Guard.NotNull(schemaRepository, nameof(schemaRepository)); |
|||
Guard.NotNull(assetRepository, nameof(assetRepository)); |
|||
Guard.NotNull(contentRepository, nameof(contentRepository)); |
|||
|
|||
this.schemaRepository = schemaRepository; |
|||
this.assetRepository = assetRepository; |
|||
this.contentRepository = contentRepository; |
|||
} |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
return TaskHelper.Done; |
|||
} |
|||
|
|||
public Task On(Envelope<IEvent> @event) |
|||
{ |
|||
if (@event.Payload is AppEvent appEvent) |
|||
{ |
|||
Cache.Remove(CreateCacheKey(appEvent.AppId.Id)); |
|||
} |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
|
|||
public async Task<object> QueryAsync(IAppEntity appEntity, GraphQLQuery query) |
|||
{ |
|||
Guard.NotNull(appEntity, nameof(appEntity)); |
|||
Guard.NotNull(query, nameof(query)); |
|||
|
|||
var modelContext = await GetModelAsync(appEntity); |
|||
var queryContext = new QueryContext(appEntity, contentRepository, assetRepository); |
|||
|
|||
return await modelContext.ExecuteAsync(queryContext, query); |
|||
} |
|||
|
|||
private async Task<GraphQLModel> GetModelAsync(IAppEntity appEntity) |
|||
{ |
|||
var cacheKey = CreateCacheKey(appEntity.Id); |
|||
|
|||
var modelContext = Cache.Get<GraphQLModel>(cacheKey); |
|||
|
|||
if (modelContext == null) |
|||
{ |
|||
var schemas = await schemaRepository.QueryAllAsync(appEntity.Id); |
|||
|
|||
modelContext = new GraphQLModel(appEntity, schemas.Where(x => x.IsPublished)); |
|||
|
|||
Cache.Set(cacheKey, modelContext); |
|||
} |
|||
|
|||
return modelContext; |
|||
} |
|||
|
|||
private static object CreateCacheKey(Guid appId) |
|||
{ |
|||
return $"GraphQLModel_{appId}"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,187 @@ |
|||
// ==========================================================================
|
|||
// GraphQLContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using GraphQL; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Read.Apps; |
|||
using Squidex.Domain.Apps.Read.Contents.GraphQL.Types; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using GraphQLSchema = GraphQL.Types.Schema; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
// ReSharper disable ParameterHidesMember
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
|||
{ |
|||
public sealed class GraphQLModel : IGraphQLContext |
|||
{ |
|||
private readonly Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> fieldInfos; |
|||
private readonly Dictionary<Guid, IGraphType> schemaTypes = new Dictionary<Guid, IGraphType>(); |
|||
private readonly Dictionary<Guid, ISchemaEntity> schemas; |
|||
private readonly PartitionResolver partitionResolver; |
|||
private readonly IGraphType assetType = new AssetGraphType(); |
|||
private readonly GraphQLSchema graphQLSchema; |
|||
|
|||
public GraphQLModel(IAppEntity appEntity, IEnumerable<ISchemaEntity> schemas) |
|||
{ |
|||
partitionResolver = appEntity.PartitionResolver; |
|||
|
|||
var defaultResolver = |
|||
new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName)); |
|||
|
|||
IGraphType assetListType = new ListGraphType(new NonNullGraphType(assetType)); |
|||
|
|||
var stringInfos = |
|||
(new StringGraphType(), defaultResolver); |
|||
|
|||
var booleanInfos = |
|||
(new BooleanGraphType(), defaultResolver); |
|||
|
|||
var numberInfos = |
|||
(new FloatGraphType(), defaultResolver); |
|||
|
|||
var dateTimeInfos = |
|||
(new DateGraphType(), defaultResolver); |
|||
|
|||
var jsonInfos = |
|||
(new ObjectGraphType(), defaultResolver); |
|||
|
|||
var geolocationInfos = |
|||
(new ObjectGraphType(), defaultResolver); |
|||
|
|||
fieldInfos = new Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> |
|||
{ |
|||
{ |
|||
typeof(StringField), |
|||
field => stringInfos |
|||
}, |
|||
{ |
|||
typeof(BooleanField), |
|||
field => booleanInfos |
|||
}, |
|||
{ |
|||
typeof(NumberField), |
|||
field => numberInfos |
|||
}, |
|||
{ |
|||
typeof(DateTimeField), |
|||
field => dateTimeInfos |
|||
}, |
|||
{ |
|||
typeof(JsonField), |
|||
field => jsonInfos |
|||
}, |
|||
{ |
|||
typeof(GeolocationField), |
|||
field => geolocationInfos |
|||
}, |
|||
{ |
|||
typeof(AssetsField), |
|||
field => |
|||
{ |
|||
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
|||
{ |
|||
var context = (QueryContext)c.UserContext; |
|||
var contentIds = c.Source.GetOrDefault(c.FieldName); |
|||
|
|||
return context.GetReferencedAssets(contentIds); |
|||
}); |
|||
|
|||
return (assetListType, resolver); |
|||
} |
|||
}, |
|||
{ |
|||
typeof(ReferencesField), |
|||
field => |
|||
{ |
|||
var schemaId = ((ReferencesField)field).Properties.SchemaId; |
|||
var schemaType = GetSchemaType(schemaId); |
|||
|
|||
if (schemaType == null) |
|||
{ |
|||
return (null, null); |
|||
} |
|||
|
|||
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
|||
{ |
|||
var context = (QueryContext)c.UserContext; |
|||
var contentIds = c.Source.GetOrDefault(c.FieldName); |
|||
|
|||
return context.GetReferencedContents(schemaId, contentIds); |
|||
}); |
|||
|
|||
var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); |
|||
|
|||
return (schemaFieldType, resolver); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
this.schemas = schemas.ToDictionary(x => x.Id); |
|||
|
|||
graphQLSchema = new GraphQLSchema { Query = new ContentQueryType(this, this.schemas.Values) }; |
|||
} |
|||
|
|||
public async Task<object> ExecuteAsync(QueryContext context, GraphQLQuery query) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
var result = await new DocumentExecuter().ExecuteAsync(options => |
|||
{ |
|||
options.Query = query.Query; |
|||
options.Schema = graphQLSchema; |
|||
options.Inputs = query.Variables?.ToInputs() ?? new Inputs(); |
|||
options.UserContext = context; |
|||
options.OperationName = query.OperationName; |
|||
}).ConfigureAwait(false); |
|||
|
|||
if (result.Errors != null && result.Errors.Count > 0) |
|||
{ |
|||
var errors = result.Errors.Select(x => new ValidationError(x.Message)).ToArray(); |
|||
|
|||
throw new ValidationException("Failed to execute GraphQL query.", errors); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public IFieldPartitioning ResolvePartition(Partitioning key) |
|||
{ |
|||
return partitionResolver(key); |
|||
} |
|||
|
|||
public IGraphType GetAssetType() |
|||
{ |
|||
return assetType; |
|||
} |
|||
|
|||
public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) |
|||
{ |
|||
return fieldInfos[field.GetType()](field); |
|||
} |
|||
|
|||
public IGraphType GetSchemaType(Guid schemaId) |
|||
{ |
|||
return schemaTypes.GetOrAdd(schemaId, k => |
|||
{ |
|||
var schemaEntity = schemas.GetOrDefault(k); |
|||
|
|||
return schemaEntity != null ? new ContentGraphType(schemaEntity.Schema, this) : null; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// GraphQLQuery.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
|||
{ |
|||
public class GraphQLQuery |
|||
{ |
|||
public string OperationName { get; set; } |
|||
|
|||
public string NamedQuery { get; set; } |
|||
|
|||
public string Query { get; set; } |
|||
|
|||
public string Variables { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// IGraphQLInvoker.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Read.Apps; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
|||
{ |
|||
public interface IGraphQLInvoker |
|||
{ |
|||
Task<object> QueryAsync(IAppEntity appEntity, GraphQLQuery query); |
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
// ==========================================================================
|
|||
// QueryContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Read.Contents.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using System.Collections.Concurrent; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Read.Apps; |
|||
using Squidex.Domain.Apps.Read.Assets; |
|||
using Squidex.Domain.Apps.Read.Assets.Repositories; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL |
|||
{ |
|||
public sealed class QueryContext |
|||
{ |
|||
private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>(); |
|||
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>(); |
|||
private readonly IContentRepository contentRepository; |
|||
private readonly IAssetRepository assetRepository; |
|||
private readonly IAppEntity appEntity; |
|||
|
|||
public QueryContext(IAppEntity appEntity, IContentRepository contentRepository, IAssetRepository assetRepository) |
|||
{ |
|||
Guard.NotNull(contentRepository, nameof(contentRepository)); |
|||
Guard.NotNull(assetRepository, nameof(assetRepository)); |
|||
Guard.NotNull(appEntity, nameof(appEntity)); |
|||
|
|||
this.contentRepository = contentRepository; |
|||
this.assetRepository = assetRepository; |
|||
|
|||
this.appEntity = appEntity; |
|||
} |
|||
|
|||
public async Task<IAssetEntity> FindAssetAsync(Guid id) |
|||
{ |
|||
var asset = cachedAssets.GetOrDefault(id); |
|||
|
|||
if (asset == null) |
|||
{ |
|||
asset = await assetRepository.FindAssetAsync(id); |
|||
|
|||
if (asset != null) |
|||
{ |
|||
cachedAssets[asset.Id] = asset; |
|||
} |
|||
} |
|||
|
|||
return asset; |
|||
} |
|||
|
|||
public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id) |
|||
{ |
|||
var content = cachedContents.GetOrDefault(id); |
|||
|
|||
if (content == null) |
|||
{ |
|||
content = await contentRepository.FindContentAsync(appEntity, schemaId, id); |
|||
|
|||
if (content != null) |
|||
{ |
|||
cachedContents[content.Id] = content; |
|||
} |
|||
} |
|||
|
|||
return content; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IAssetEntity>> QueryAssetsAsync(string query, int skip = 0, int take = 10) |
|||
{ |
|||
var assets = await assetRepository.QueryAsync(appEntity.Id, null, null, query, take, skip); |
|||
|
|||
foreach (var asset in assets) |
|||
{ |
|||
cachedAssets[asset.Id] = asset; |
|||
} |
|||
|
|||
return assets; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IContentEntity>> QueryContentsAsync(Guid schemaId, string query) |
|||
{ |
|||
var contents = await contentRepository.QueryAsync(appEntity, schemaId, false, null, query); |
|||
|
|||
foreach (var content in contents) |
|||
{ |
|||
cachedContents[content.Id] = content; |
|||
} |
|||
|
|||
return contents; |
|||
} |
|||
|
|||
public List<IAssetEntity> GetReferencedAssets(JToken value) |
|||
{ |
|||
var ids = ParseIds(value); |
|||
|
|||
return GetReferencedAssets(ids); |
|||
} |
|||
|
|||
public List<IAssetEntity> GetReferencedAssets(ICollection<Guid> ids) |
|||
{ |
|||
Guard.NotNull(ids, nameof(ids)); |
|||
|
|||
var notLoadedAssets = new HashSet<Guid>(ids.Where(id => !cachedAssets.ContainsKey(id))); |
|||
|
|||
if (notLoadedAssets.Count > 0) |
|||
{ |
|||
Task.Run(async () => |
|||
{ |
|||
var assets = await assetRepository.QueryAsync(appEntity.Id, null, notLoadedAssets, string.Empty, int.MaxValue).ConfigureAwait(false); |
|||
|
|||
foreach (var asset in assets) |
|||
{ |
|||
cachedAssets[asset.Id] = asset; |
|||
} |
|||
}).Wait(); |
|||
} |
|||
|
|||
return ids.Select(id => cachedAssets.GetOrDefault(id)).Where(x => x != null).ToList(); |
|||
} |
|||
|
|||
public List<IContentEntity> GetReferencedContents(Guid schemaId, JToken value) |
|||
{ |
|||
var ids = ParseIds(value); |
|||
|
|||
return GetReferencedContents(schemaId, ids); |
|||
} |
|||
|
|||
public List<IContentEntity> GetReferencedContents(Guid schemaId, ICollection<Guid> ids) |
|||
{ |
|||
Guard.NotNull(ids, nameof(ids)); |
|||
|
|||
var notLoadedContents = new HashSet<Guid>(ids.Where(id => !cachedContents.ContainsKey(id))); |
|||
|
|||
if (notLoadedContents.Count > 0) |
|||
{ |
|||
Task.Run(async () => |
|||
{ |
|||
var contents = await contentRepository.QueryAsync(appEntity, schemaId, false, notLoadedContents, string.Empty).ConfigureAwait(false); |
|||
|
|||
foreach (var content in contents) |
|||
{ |
|||
cachedContents[content.Id] = content; |
|||
} |
|||
}).Wait(); |
|||
} |
|||
|
|||
return ids.Select(id => cachedContents.GetOrDefault(id)).Where(x => x != null).ToList(); |
|||
} |
|||
|
|||
private static ICollection<Guid> ParseIds(JToken value) |
|||
{ |
|||
try |
|||
{ |
|||
var result = new List<Guid>(); |
|||
|
|||
if (value is JArray) |
|||
{ |
|||
foreach (var id in value) |
|||
{ |
|||
result.Add(Guid.Parse(id.ToString())); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
catch |
|||
{ |
|||
return new List<Guid>(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
// ==========================================================================
|
|||
// AssetGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Read.Assets; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity> |
|||
{ |
|||
public AssetGraphType() |
|||
{ |
|||
Name = "AssetDto"; |
|||
|
|||
Field("id", x => x.Id.ToString()) |
|||
.Description("The id of the asset."); |
|||
|
|||
Field("version", x => x.Version) |
|||
.Description("The version of the asset."); |
|||
|
|||
Field("created", x => x.Created.ToDateTimeUtc()) |
|||
.Description("The date and time when the asset has been created."); |
|||
|
|||
Field("createdBy", x => x.CreatedBy.ToString()) |
|||
.Description("The user that has created the asset."); |
|||
|
|||
Field("lastModified", x => x.LastModified.ToDateTimeUtc()) |
|||
.Description("The date and time when the asset has been modified last."); |
|||
|
|||
Field("lastModifiedBy", x => x.LastModifiedBy.ToString()) |
|||
.Description("The user that has updated the asset last."); |
|||
|
|||
Field("mimeType", x => x.MimeType) |
|||
.Description("The mime type."); |
|||
|
|||
Field("fileName", x => x.FileName) |
|||
.Description("The file name."); |
|||
|
|||
Field("fileSize", x => x.FileSize) |
|||
.Description("The size of the file in bytes."); |
|||
|
|||
Field("fileVersion", x => x.FileVersion) |
|||
.Description("The version of the file."); |
|||
|
|||
Field("isImage", x => x.IsImage) |
|||
.Description("Determines of the created file is an image."); |
|||
|
|||
Field("pixelWidth", x => x.PixelWidth, true) |
|||
.Description("The width of the image in pixels if the asset is an image."); |
|||
|
|||
Field("pixelHeight", x => x.PixelHeight, true) |
|||
.Description("The height of the image in pixels if the asset is an image."); |
|||
|
|||
Description = "An asset"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
// ==========================================================================
|
|||
// ContentDataGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
|||
|
|||
// ReSharper disable InvertIf
|
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> |
|||
{ |
|||
private static readonly IFieldResolver FieldResolver = |
|||
new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(c.FieldName)); |
|||
|
|||
public ContentDataGraphType(Schema schema, IGraphQLContext graphQLContext) |
|||
{ |
|||
var schemaName = schema.Properties.Label.WithFallback(schema.Name); |
|||
|
|||
Name = $"{schema.Name.ToPascalCase()}DataDto"; |
|||
|
|||
foreach (var field in schema.Fields) |
|||
{ |
|||
var fieldInfo = graphQLContext.GetGraphType(field); |
|||
|
|||
if (fieldInfo.ResolveType != null) |
|||
{ |
|||
var fieldName = field.RawProperties.Label.WithFallback(field.Name); |
|||
var fieldGraphType = new ObjectGraphType |
|||
{ |
|||
Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" |
|||
}; |
|||
|
|||
var partition = graphQLContext.ResolvePartition(field.Paritioning); |
|||
|
|||
foreach (var partitionItem in partition) |
|||
{ |
|||
fieldGraphType.AddField(new FieldType |
|||
{ |
|||
Name = partitionItem.Key, |
|||
Resolver = fieldInfo.Resolver, |
|||
ResolvedType = fieldInfo.ResolveType, |
|||
Description = field.RawProperties.Hints |
|||
}); |
|||
} |
|||
|
|||
fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type."; |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = field.Name, |
|||
Resolver = FieldResolver, |
|||
ResolvedType = fieldGraphType |
|||
}); |
|||
} |
|||
} |
|||
|
|||
Description = $"The structure of a {schemaName} content type."; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,192 @@ |
|||
// ==========================================================================
|
|||
// GraphModelType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Read.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class ContentQueryType : ObjectGraphType |
|||
{ |
|||
public ContentQueryType(IGraphQLContext graphQLContext, IEnumerable<ISchemaEntity> schemaEntities) |
|||
{ |
|||
AddAssetFind(graphQLContext); |
|||
AddAssetsQuery(graphQLContext); |
|||
|
|||
foreach (var schemaEntity in schemaEntities) |
|||
{ |
|||
var schemaName = schemaEntity.Schema.Properties.Label.WithFallback(schemaEntity.Schema.Name); |
|||
var schemaType = new ContentGraphType(schemaEntity.Schema, graphQLContext); |
|||
|
|||
AddContentFind(schemaEntity, schemaName, schemaType); |
|||
AddContentQuery(schemaEntity, schemaType, schemaName); |
|||
} |
|||
|
|||
Description = "The app queries."; |
|||
} |
|||
|
|||
private void AddAssetFind(IGraphQLContext graphQLContext) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = "findAsset", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "id", |
|||
Description = "The id of the asset.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = graphQLContext.GetAssetType(), |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (QueryContext)c.UserContext; |
|||
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
|||
|
|||
return context.FindAssetAsync(contentId); |
|||
}), |
|||
Description = "Find an asset by id." |
|||
}); |
|||
} |
|||
|
|||
private void AddContentFind(ISchemaEntity schemaEntity, string schemaName, IGraphType schemaType) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = $"find{schemaEntity.Name.ToPascalCase()}", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "id", |
|||
Description = $"The id of the {schemaName} content.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = schemaType, |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (QueryContext)c.UserContext; |
|||
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
|||
|
|||
return context.FindContentAsync(schemaEntity.Id, contentId); |
|||
}), |
|||
Description = $"Find an {schemaName} content by id." |
|||
}); |
|||
} |
|||
|
|||
private void AddAssetsQuery(IGraphQLContext graphQLContext) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = "queryAssets", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "top", |
|||
Description = "Optional number of assets to take.", |
|||
DefaultValue = 20 |
|||
}, |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "skip", |
|||
Description = "Optional number of assets to skip.", |
|||
DefaultValue = 0 |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "search", |
|||
Description = "Optional query.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = new ListGraphType(new NonNullGraphType(graphQLContext.GetAssetType())), |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (QueryContext)c.UserContext; |
|||
|
|||
var argTop = c.GetArgument("top", 20); |
|||
var argSkip = c.GetArgument("skip", 0); |
|||
var argQuery = c.GetArgument("query", string.Empty); |
|||
|
|||
return context.QueryAssetsAsync(argQuery, argSkip, argTop); |
|||
}), |
|||
Description = "Query assets items." |
|||
}); |
|||
} |
|||
|
|||
private void AddContentQuery(ISchemaEntity schemaEntity, IGraphType schemaType, string schemaName) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = $"query{schemaEntity.Name.ToPascalCase()}", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "top", |
|||
Description = "Optional number of contents to take.", |
|||
DefaultValue = 20 |
|||
}, |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "skip", |
|||
Description = "Optional number of contents to skip.", |
|||
DefaultValue = 0 |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "filter", |
|||
Description = "Optional OData filter.", |
|||
DefaultValue = string.Empty |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "search", |
|||
Description = "Optional OData full text search.", |
|||
DefaultValue = string.Empty |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "orderby", |
|||
Description = "Optional OData order definition.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (QueryContext)c.UserContext; |
|||
var contentQuery = BuildODataQuery(c); |
|||
|
|||
return context.QueryContentsAsync(schemaEntity.Id, contentQuery); |
|||
}), |
|||
Description = $"Query {schemaName} content items." |
|||
}); |
|||
} |
|||
|
|||
private static string BuildODataQuery(ResolveFieldContext c) |
|||
{ |
|||
var odataQuery = "?" + |
|||
string.Join("&", |
|||
c.Arguments |
|||
.Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
|||
.Select(x => $"${x.Key}={x.Value}")); |
|||
|
|||
return odataQuery; |
|||
} |
|||
} |
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
// ==========================================================================
|
|||
// SchemaGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.GraphQl |
|||
{ |
|||
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> |
|||
{ |
|||
private static readonly IFieldResolver FieldResolver = |
|||
new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(c.FieldName)); |
|||
|
|||
public ContentDataGraphType(Schema schema, IGraphQLContext context) |
|||
{ |
|||
foreach (var field in schema.Fields) |
|||
{ |
|||
var fieldName = field.Name; |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = fieldName, |
|||
Resolver = FieldResolver, |
|||
ResolvedType = new SchemaFieldGraphType(field, context), |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
// ==========================================================================
|
|||
// SchemaGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.GraphQl |
|||
{ |
|||
|
|||
public sealed class ContentFieldGraphType : ObjectGraphType<ContentFieldData> |
|||
{ |
|||
public ContentFieldGraphType(Field field, IGraphQLContext context) |
|||
{ |
|||
var partition = context.ResolvePartition(field.Paritioning); |
|||
|
|||
foreach (var partitionItem in partition) |
|||
{ |
|||
var fieldInfo = context.GetGraphType(field); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = partitionItem.Key, |
|||
Resolver = fieldInfo.Resolver, |
|||
ResolvedType = fieldInfo.ResolveType, |
|||
Description = field.RawProperties.Hints |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,94 +0,0 @@ |
|||
// ==========================================================================
|
|||
// GraphQLContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Read.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.GraphQl |
|||
{ |
|||
public sealed class GraphQLContext : IGraphQLContext |
|||
{ |
|||
private readonly PartitionResolver partitionResolver; |
|||
private readonly Dictionary<Type, (IGraphType ResolveType, IFieldResolver Resolver)> fieldInfos; |
|||
|
|||
public GraphQLContext(PartitionResolver partitionResolver) |
|||
{ |
|||
Guard.NotNull(partitionResolver, nameof(partitionResolver)); |
|||
|
|||
this.partitionResolver = partitionResolver; |
|||
|
|||
var defaultResolver = |
|||
new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName)); |
|||
|
|||
fieldInfos = new Dictionary<Type, (IGraphType ResolveType, IFieldResolver Resolver)> |
|||
{ |
|||
{ |
|||
typeof(StringField), |
|||
(new StringGraphType(), defaultResolver) |
|||
}, |
|||
{ |
|||
typeof(BooleanField), |
|||
(new BooleanGraphType(), defaultResolver) |
|||
}, |
|||
{ |
|||
typeof(NumberField), |
|||
(new FloatGraphType(), defaultResolver) |
|||
}, |
|||
{ |
|||
typeof(DateTimeField), |
|||
(new FloatGraphType(), defaultResolver) |
|||
}, |
|||
{ |
|||
typeof(JsonField), |
|||
(new ObjectGraphType(), defaultResolver) |
|||
}, |
|||
{ |
|||
typeof(GeolocationField), |
|||
(new ObjectGraphType(), defaultResolver) |
|||
}, |
|||
{ |
|||
typeof(AssetsField), |
|||
(new ListGraphType<StringGraphType>(), defaultResolver) |
|||
}, |
|||
{ |
|||
typeof(ReferencesField), |
|||
(new ListGraphType<StringGraphType>(), defaultResolver) |
|||
} |
|||
}; |
|||
} |
|||
|
|||
public IGraphType GetSchemaListType(Schema schema) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public IGraphType GetSchemaListType(Guid schemaId) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public IFieldPartitioning ResolvePartition(Partitioning key) |
|||
{ |
|||
return partitionResolver(key); |
|||
} |
|||
|
|||
public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) |
|||
{ |
|||
return fieldInfos[field.GetType()]; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<sqx-title message="{app} | Settings" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
|||
|
|||
<sqx-panel theme="dark" panelWidth="12rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<h3 class="panel-title">API</h3> |
|||
</div> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content"> |
|||
<ul class="nav flex-column nav-dark"> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> |
|||
GraphQL |
|||
<i class="icon-angle-right"></i> |
|||
</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link"> |
|||
REST |
|||
</a> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -0,0 +1,12 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.nav-link { |
|||
position: relative; |
|||
padding-top: .6rem; |
|||
padding-bottom: .6rem; |
|||
} |
|||
|
|||
.icon-angle-right { |
|||
@include absolute(14px, 2rem, auto, auto); |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component } from '@angular/core'; |
|||
|
|||
import { |
|||
AppComponentBase, |
|||
AppsStoreService, |
|||
NotificationService |
|||
} from 'shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-api-area', |
|||
styleUrls: ['./api-area.component.scss'], |
|||
templateUrl: './api-area.component.html' |
|||
}) |
|||
export class ApiAreaComponent extends AppComponentBase { |
|||
constructor(apps: AppsStoreService, notifications: NotificationService |
|||
) { |
|||
super(notifications, apps); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
export * from './pages/graphql/graphql-page.component'; |
|||
|
|||
export * from './api-area.component'; |
|||
@ -0,0 +1,9 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
export * from './declarations'; |
|||
export * from './module'; |
|||
@ -0,0 +1,50 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
import { DndModule } from 'ng2-dnd'; |
|||
|
|||
import { |
|||
SqxFrameworkModule, |
|||
SqxSharedModule |
|||
} from 'shared'; |
|||
|
|||
import { |
|||
ApiAreaComponent, |
|||
GraphQLPageComponent |
|||
} from './declarations'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
path: '', |
|||
component: ApiAreaComponent, |
|||
children: [ |
|||
{ |
|||
path: '' |
|||
}, |
|||
{ |
|||
path: 'graphql', |
|||
component: GraphQLPageComponent |
|||
} |
|||
] |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
DndModule, |
|||
SqxFrameworkModule, |
|||
SqxSharedModule, |
|||
RouterModule.forChild(routes) |
|||
], |
|||
declarations: [ |
|||
ApiAreaComponent, |
|||
GraphQLPageComponent |
|||
] |
|||
}) |
|||
export class SqxFeatureApiModule { } |
|||
@ -0,0 +1,5 @@ |
|||
<sqx-title message="{app} | API | GraphQL" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
|||
|
|||
<sqx-panel panelWidth="60rem" expand="true"> |
|||
<div #graphiQLContainer></div> |
|||
</sqx-panel> |
|||
@ -0,0 +1,12 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
@import '~graphiql/graphiql'; |
|||
|
|||
.graphiql-container { |
|||
@include absolute(0, 0, 0, 0); |
|||
} |
|||
|
|||
.graphiql-container > * { |
|||
box-sizing: content-box; |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; |
|||
|
|||
import * as React from 'react'; |
|||
import * as ReactDOM from 'react-dom'; |
|||
|
|||
const GraphiQL = require('graphiql'); |
|||
|
|||
import { |
|||
ApiUrlConfig, |
|||
AppComponentBase, |
|||
AppsStoreService, |
|||
AuthService, |
|||
NotificationService |
|||
} from 'shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-graphql-page', |
|||
styleUrls: ['./graphql-page.component.scss'], |
|||
templateUrl: './graphql-page.component.html', |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class GraphQLPageComponent extends AppComponentBase implements OnInit { |
|||
@ViewChild('graphiQLContainer') |
|||
public graphiQLContainer: ElementRef; |
|||
|
|||
constructor(apps: AppsStoreService, notifications: NotificationService, |
|||
private readonly authService: AuthService, |
|||
private readonly apiUrl: ApiUrlConfig |
|||
) { |
|||
super(notifications, apps); |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
ReactDOM.render( |
|||
React.createElement(GraphiQL, { |
|||
fetcher: (params: any) => this.request(params) |
|||
}), |
|||
this.graphiQLContainer.nativeElement |
|||
); |
|||
} |
|||
|
|||
private request(params: any) { |
|||
return this.appNameOnce() |
|||
.switchMap(app => this.authService.authPost(this.apiUrl.buildUrl(`api/content/${app}/graphql`), params).map(r => r.json())) |
|||
.toPromise(); |
|||
} |
|||
} |
|||
|
|||
@ -1,84 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { PanelService, PanelServiceFactory } from './../'; |
|||
|
|||
interface Styling { element: any; property: string; value: string; } |
|||
|
|||
describe('PanelService', () => { |
|||
it('should instantiate from factory', () => { |
|||
const panelService = PanelServiceFactory(); |
|||
|
|||
expect(panelService).toBeDefined(); |
|||
}); |
|||
|
|||
it('should instantiate', () => { |
|||
const panelService = new PanelService(); |
|||
|
|||
expect(panelService).toBeDefined(); |
|||
}); |
|||
|
|||
it('should update elements with renderer service', () => { |
|||
let styles: Styling[] = []; |
|||
|
|||
const renderer = { |
|||
setElementStyle: (element: any, property: string, value: string) => { |
|||
styles.push({element, property, value}); |
|||
} |
|||
}; |
|||
|
|||
const panelService = new PanelService(); |
|||
|
|||
const element1 = { |
|||
getBoundingClientRect: () => { |
|||
return { width: 100 }; |
|||
} |
|||
}; |
|||
|
|||
const element2 = { |
|||
getBoundingClientRect: () => { |
|||
return { width: 200 }; |
|||
} |
|||
}; |
|||
|
|||
const element3 = { |
|||
getBoundingClientRect: () => { |
|||
return { width: 300 }; |
|||
} |
|||
}; |
|||
|
|||
let numPublished = 0; |
|||
panelService.changed.subscribe(() => { |
|||
numPublished++; |
|||
}); |
|||
|
|||
panelService.push(element1); |
|||
panelService.push(element2); |
|||
panelService.push(element3); |
|||
|
|||
styles = []; |
|||
|
|||
panelService.pop(element3); |
|||
panelService.render(<any>renderer); |
|||
|
|||
expect(styles).toEqual([ |
|||
{ element: element1, property: 'top', value: '0px' }, |
|||
{ element: element1, property: 'left', value: '0px' }, |
|||
{ element: element1, property: 'bottom', value: '0px' }, |
|||
{ element: element1, property: 'position', value: 'absolute' }, |
|||
{ element: element1, property: 'z-index', value: '20' }, |
|||
|
|||
{ element: element2, property: 'top', value: '0px' }, |
|||
{ element: element2, property: 'left', value: '100px' }, |
|||
{ element: element2, property: 'bottom', value: '0px' }, |
|||
{ element: element2, property: 'position', value: 'absolute' }, |
|||
{ element: element2, property: 'z-index', value: '10' } |
|||
]); |
|||
|
|||
expect(numPublished).toBe(1); |
|||
}); |
|||
}); |
|||
@ -1,51 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Injectable, Renderer } from '@angular/core'; |
|||
import { Observable, Subject } from 'rxjs'; |
|||
|
|||
export const PanelServiceFactory = () => { |
|||
return new PanelService(); |
|||
}; |
|||
|
|||
@Injectable() |
|||
export class PanelService { |
|||
private readonly elements: any[] = []; |
|||
private readonly changed$ = new Subject<number>(); |
|||
|
|||
public get changed(): Observable<number> { |
|||
return this.changed$; |
|||
} |
|||
|
|||
public push(element: any) { |
|||
this.elements.push(element); |
|||
} |
|||
|
|||
public pop(element: any) { |
|||
this.elements.splice(-1, 1); |
|||
} |
|||
|
|||
public render(renderer: Renderer) { |
|||
let currentPosition = 0; |
|||
let currentLayer = this.elements.length * 10; |
|||
|
|||
for (let element of this.elements) { |
|||
const width = element.getBoundingClientRect().width; |
|||
|
|||
renderer.setElementStyle(element, 'top', '0px'); |
|||
renderer.setElementStyle(element, 'left', currentPosition + 'px'); |
|||
renderer.setElementStyle(element, 'bottom', '0px'); |
|||
renderer.setElementStyle(element, 'position', 'absolute'); |
|||
renderer.setElementStyle(element, 'z-index', currentLayer.toString()); |
|||
|
|||
currentPosition += width; |
|||
currentLayer -= 10; |
|||
} |
|||
|
|||
this.changed$.next(currentPosition); |
|||
} |
|||
} |
|||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue