diff --git a/Squidex.sln b/Squidex.sln index d365dc2c8..8ba487e78 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -54,7 +54,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users.MongoDb", "src\Squidex.Domain.Users.MongoDb\Squidex.Domain.Users.MongoDb.csproj", "{27CF800D-890F-4882-BF05-44EC3233537D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Users.Tests", "tests\Squidex.Domain.Users.Tests\Squidex.Domain.Users.Tests.csproj", "{42184546-E3CB-4D4F-9495-43979B9C63B9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users.Tests", "tests\Squidex.Domain.Users.Tests\Squidex.Domain.Users.Tests.csproj", "{42184546-E3CB-4D4F-9495-43979B9C63B9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs b/src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs index 0ba34c117..a227a7691 100644 --- a/src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs +++ b/src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { private T properties; - protected T Properties + public T Properties { get { return properties; } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs index 02198b288..b8525b493 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets await Collection.Find(x => assetIds.Contains(x.Id) && x.AppId == appId).Project(Project.Include(x => x.Id)) .ToListAsync(); - return assetIds.Except(assetEntities.Select(x => x["Id"].AsGuid)).ToList(); + return assetIds.Except(assetEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); } public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs index 2f06a7928..9c711ddfb 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents this.modelBuilder = modelBuilder; } - public async Task> QueryAsync(Guid schemaId, bool nonPublished, HashSet ids, string odataQuery, IAppEntity appEntity) + public async Task> QueryAsync(IAppEntity appEntity, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery) { var contentEntities = (List)null; @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents return contentEntities; } - public async Task CountAsync(Guid schemaId, bool nonPublished, HashSet ids, string odataQuery, IAppEntity appEntity) + public async Task CountAsync(IAppEntity appEntity, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery) { var contentsCount = 0L; @@ -166,10 +166,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents .ToListAsync(); }); - return contentIds.Except(contentEntities.Select(x => x["Id"].AsGuid)).ToList(); + return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); } - public async Task FindContentAsync(Guid schemaId, Guid id, IAppEntity appEntity) + public async Task FindContentAsync(IAppEntity appEntity, Guid schemaId, Guid id) { var contentEntity = (MongoContentEntity)null; diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLInvoker.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLInvoker.cs new file mode 100644 index 000000000..82f58575f --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLInvoker.cs @@ -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 @event) + { + if (@event.Payload is AppEvent appEvent) + { + Cache.Remove(CreateCacheKey(appEvent.AppId.Id)); + } + + return TaskHelper.Done; + } + + public async Task 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 GetModelAsync(IAppEntity appEntity) + { + var cacheKey = CreateCacheKey(appEntity.Id); + + var modelContext = Cache.Get(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}"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs new file mode 100644 index 000000000..b7046e38f --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs @@ -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> fieldInfos; + private readonly Dictionary schemaTypes = new Dictionary(); + private readonly Dictionary schemas; + private readonly PartitionResolver partitionResolver; + private readonly IGraphType assetType = new AssetGraphType(); + private readonly GraphQLSchema graphQLSchema; + + public GraphQLModel(IAppEntity appEntity, IEnumerable schemas) + { + partitionResolver = appEntity.PartitionResolver; + + var defaultResolver = + new FuncFieldResolver(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> + { + { + 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(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(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 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; + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs new file mode 100644 index 000000000..d8ee25be8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs @@ -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; } + } +} diff --git a/src/Squidex.Domain.Apps.Read/GraphQl/IGraphQLContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs similarity index 81% rename from src/Squidex.Domain.Apps.Read/GraphQl/IGraphQLContext.cs rename to src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs index 6d7faa1c2..955ebca91 100644 --- a/src/Squidex.Domain.Apps.Read/GraphQl/IGraphQLContext.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs @@ -6,20 +6,22 @@ // All rights reserved. // ========================================================================== +using System; using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; -using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; -namespace Squidex.Domain.Apps.Read.GraphQl +namespace Squidex.Domain.Apps.Read.Contents.GraphQL { public interface IGraphQLContext { - IGraphType GetSchemaListType(Schema schema); - IFieldPartitioning ResolvePartition(Partitioning key); + IGraphType GetAssetType(); + + IGraphType GetSchemaType(Guid schemaId); + (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field); } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLInvoker.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLInvoker.cs new file mode 100644 index 000000000..47bb9eab6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLInvoker.cs @@ -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 QueryAsync(IAppEntity appEntity, GraphQLQuery query); + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs new file mode 100644 index 000000000..be29037c3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs @@ -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 cachedContents = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); + 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 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 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> 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> 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 GetReferencedAssets(JToken value) + { + var ids = ParseIds(value); + + return GetReferencedAssets(ids); + } + + public List GetReferencedAssets(ICollection ids) + { + Guard.NotNull(ids, nameof(ids)); + + var notLoadedAssets = new HashSet(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 GetReferencedContents(Guid schemaId, JToken value) + { + var ids = ParseIds(value); + + return GetReferencedContents(schemaId, ids); + } + + public List GetReferencedContents(Guid schemaId, ICollection ids) + { + Guard.NotNull(ids, nameof(ids)); + + var notLoadedContents = new HashSet(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 ParseIds(JToken value) + { + try + { + var result = new List(); + + if (value is JArray) + { + foreach (var id in value) + { + result.Add(Guid.Parse(id.ToString())); + } + } + + return result; + } + catch + { + return new List(); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs new file mode 100644 index 000000000..ae49d1f8e --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs @@ -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 + { + 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"; + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs new file mode 100644 index 000000000..cc345b87b --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -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 + { + private static readonly IFieldResolver FieldResolver = + new FuncFieldResolver(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."; + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/GraphQl/ContentGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs similarity index 69% rename from src/Squidex.Domain.Apps.Read/GraphQl/ContentGraphType.cs rename to src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs index b35cfe045..a21cab2e2 100644 --- a/src/Squidex.Domain.Apps.Read/GraphQl/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs @@ -9,46 +9,49 @@ using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Read.Contents; using Squidex.Infrastructure; using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; -namespace Squidex.Domain.Apps.Read.GraphQl +namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types { public sealed class ContentGraphType : ObjectGraphType { private static readonly IFieldResolver DataResolver = new FuncFieldResolver(c => c.Source.Data); - public ContentGraphType(Schema schema, IGraphQLContext context) + public ContentGraphType(Schema schema, IGraphQLContext graphQLContext) { var schemaName = schema.Properties.Label.WithFallback(schema.Name); - Field("id", x => x.Id) + Name = $"{schema.Name.ToPascalCase()}Dto"; + + Field("id", x => x.Id.ToString()) .Description($"The id of the {schemaName} content."); Field("version", x => x.Version) .Description($"The version of the {schemaName} content."); - Field("created", x => x.Created) + Field("created", x => x.Created.ToDateTimeUtc()) .Description($"The date and time when the {schemaName} content has been created."); Field("createdBy", x => x.CreatedBy.ToString()) .Description($"The user that has created the {schemaName} content."); - Field("lastModified", x => x.LastModified.ToString()) + Field("lastModified", x => x.LastModified.ToDateTimeUtc()) .Description($"The date and time when the {schemaName} content has been modified last."); - Field("lastModified", x => x.LastModified.ToString()) + Field("lastModifiedBy", x => x.LastModifiedBy.ToString()) .Description($"The user that has updated the {schemaName} content last."); - + AddField(new FieldType { Name = "data", Resolver = DataResolver, - ResolvedType = new SchemaDataGraphType(schema, context), - Description = $"The version of the {schemaName} content." + ResolvedType = new NonNullGraphType(new ContentDataGraphType(schema, graphQLContext)), + Description = $"The data of the {schemaName} content." }); + + Description = $"The structure of a {schemaName} content type."; } } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryType.cs new file mode 100644 index 000000000..0189b9a93 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryType.cs @@ -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 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(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(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(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(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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs index 8e9f5bbc0..ddb27c1e2 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/Repositories/IContentRepository.cs @@ -15,12 +15,12 @@ namespace Squidex.Domain.Apps.Read.Contents.Repositories { public interface IContentRepository { - Task> QueryAsync(Guid schemaId, bool nonPublished, HashSet ids, string odataQuery, IAppEntity appEntity); + Task> QueryAsync(IAppEntity appEntity, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery); Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds); - Task CountAsync(Guid schemaId, bool nonPublished, HashSet ids, string odataQuery, IAppEntity appEntity); + Task CountAsync(IAppEntity appEntity, Guid schemaId, bool nonPublished, HashSet ids, string odataQuery); - Task FindContentAsync(Guid schemaId, Guid id, IAppEntity appEntity); + Task FindContentAsync(IAppEntity appEntity, Guid schemaId, Guid id); } } diff --git a/src/Squidex.Domain.Apps.Read/GraphQl/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Read/GraphQl/ContentDataGraphType.cs deleted file mode 100644 index 610ce4a6f..000000000 --- a/src/Squidex.Domain.Apps.Read/GraphQl/ContentDataGraphType.cs +++ /dev/null @@ -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 - { - private static readonly IFieldResolver FieldResolver = - new FuncFieldResolver(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), - }); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/GraphQl/ContentFieldGraphType.cs b/src/Squidex.Domain.Apps.Read/GraphQl/ContentFieldGraphType.cs deleted file mode 100644 index 22af68b94..000000000 --- a/src/Squidex.Domain.Apps.Read/GraphQl/ContentFieldGraphType.cs +++ /dev/null @@ -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 - { - 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 - }); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/GraphQl/GraphQLContext.cs b/src/Squidex.Domain.Apps.Read/GraphQl/GraphQLContext.cs deleted file mode 100644 index 3bf18433c..000000000 --- a/src/Squidex.Domain.Apps.Read/GraphQl/GraphQLContext.cs +++ /dev/null @@ -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 fieldInfos; - - public GraphQLContext(PartitionResolver partitionResolver) - { - Guard.NotNull(partitionResolver, nameof(partitionResolver)); - - this.partitionResolver = partitionResolver; - - var defaultResolver = - new FuncFieldResolver(c => c.Source.GetOrDefault(c.FieldName)); - - fieldInfos = new Dictionary - { - { - 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(), defaultResolver) - }, - { - typeof(ReferencesField), - (new ListGraphType(), 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()]; - } - } -} diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index 2e81391da..787568d99 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Apps.Services.Implementations; using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents.Edm; +using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Read.History; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas.Services; @@ -87,6 +88,11 @@ namespace Squidex.Config.Domain builder.RegisterType() .AsSelf() .SingleInstance(); + + builder.RegisterType() + .As() + .AsSelf() + .InstancePerDependency(); } } } diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index 903d9dd27..07015185a 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -15,6 +15,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Read.Apps.Repositories; using Squidex.Domain.Apps.Read.Apps.Services.Implementations; using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.History.Repositories; using Squidex.Domain.Apps.Read.MongoDb.Apps; @@ -170,6 +171,7 @@ namespace Squidex.Config.Domain builder.Register(c => new CompoundEventConsumer( c.Resolve(), + c.Resolve(), c.Resolve())) .As() .AsSelf() diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index e456e6343..5a4b90320 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -15,8 +15,10 @@ using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Controllers.ContentApi.Models; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Repositories; using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Write.Contents.Commands; using Squidex.Infrastructure.CQRS.Commands; @@ -33,15 +35,40 @@ namespace Squidex.Controllers.ContentApi { private readonly ISchemaProvider schemas; private readonly IContentRepository contentRepository; + private readonly IGraphQLInvoker graphQL; - public ContentsController(ICommandBus commandBus, ISchemaProvider schemas, IContentRepository contentRepository) + public ContentsController( + ICommandBus commandBus, + ISchemaProvider schemas, + IContentRepository contentRepository, + IGraphQLInvoker graphQL) : base(commandBus) { + this.graphQL = graphQL; this.schemas = schemas; - this.contentRepository = contentRepository; } + [HttpGet] + [Route("content/{app}/graphql")] + [ApiCosts(2)] + public async Task GetGraphQL([FromQuery] GraphQLQuery query) + { + var result = await graphQL.QueryAsync(App, query); + + return Ok(result); + } + + [HttpPost] + [Route("content/{app}/graphql")] + [ApiCosts(2)] + public async Task PostGraphQL([FromBody] GraphQLQuery query) + { + var result = await graphQL.QueryAsync(App, query); + + return Ok(result); + } + [HttpGet] [Route("content/{app}/{name}")] [ApiCosts(2)] @@ -64,8 +91,8 @@ namespace Squidex.Controllers.ContentApi var query = Request.QueryString.ToString(); - var taskForItems = contentRepository.QueryAsync(schemaEntity.Id, nonPublished, idsList, query, App); - var taskForCount = contentRepository.CountAsync(schemaEntity.Id, nonPublished, idsList, query, App); + var taskForItems = contentRepository.QueryAsync(App, schemaEntity.Id, nonPublished, idsList, query); + var taskForCount = contentRepository.CountAsync(App, schemaEntity.Id, nonPublished, idsList, query); await Task.WhenAll(taskForItems, taskForCount); @@ -100,7 +127,7 @@ namespace Squidex.Controllers.ContentApi return NotFound(); } - var entity = await contentRepository.FindContentAsync(schemaEntity.Id, id, App); + var entity = await contentRepository.FindContentAsync(App, schemaEntity.Id, id); if (entity == null) { diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index ebf192b20..3b4e8fea7 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -70,6 +70,10 @@ export const routes: Routes = [ { path: 'settings', loadChildren: './features/settings/module#SqxFeatureSettingsModule' + }, + { + path: 'api', + loadChildren: './features/api/module#SqxFeatureApiModule' } ] } diff --git a/src/Squidex/app/features/api/api-area.component.html b/src/Squidex/app/features/api/api-area.component.html new file mode 100644 index 000000000..abf56b677 --- /dev/null +++ b/src/Squidex/app/features/api/api-area.component.html @@ -0,0 +1,33 @@ + + + +
+
+

API

+
+ + + + +
+ +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/src/Squidex/app/features/api/api-area.component.scss b/src/Squidex/app/features/api/api-area.component.scss new file mode 100644 index 000000000..3bda24ee9 --- /dev/null +++ b/src/Squidex/app/features/api/api-area.component.scss @@ -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); +} \ No newline at end of file diff --git a/src/Squidex/app/features/api/api-area.component.ts b/src/Squidex/app/features/api/api-area.component.ts new file mode 100644 index 000000000..3351cbb63 --- /dev/null +++ b/src/Squidex/app/features/api/api-area.component.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/api/declarations.ts b/src/Squidex/app/features/api/declarations.ts new file mode 100644 index 000000000..9a0ad2ccb --- /dev/null +++ b/src/Squidex/app/features/api/declarations.ts @@ -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'; \ No newline at end of file diff --git a/src/Squidex/app/features/api/index.ts b/src/Squidex/app/features/api/index.ts new file mode 100644 index 000000000..9c0a4f6ee --- /dev/null +++ b/src/Squidex/app/features/api/index.ts @@ -0,0 +1,9 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export * from './declarations'; +export * from './module'; \ No newline at end of file diff --git a/src/Squidex/app/features/api/module.ts b/src/Squidex/app/features/api/module.ts new file mode 100644 index 000000000..49268e04b --- /dev/null +++ b/src/Squidex/app/features/api/module.ts @@ -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 { } \ No newline at end of file diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html new file mode 100644 index 000000000..7dedbeb1b --- /dev/null +++ b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html @@ -0,0 +1,5 @@ + + + +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss new file mode 100644 index 000000000..00a5cbf4a --- /dev/null +++ b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss @@ -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; +} diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts new file mode 100644 index 000000000..6feba81e2 --- /dev/null +++ b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts @@ -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(); + } +} + diff --git a/src/Squidex/app/framework/angular/panel-container.directive.ts b/src/Squidex/app/framework/angular/panel-container.directive.ts index 3931f34ca..a9539078f 100644 --- a/src/Squidex/app/framework/angular/panel-container.directive.ts +++ b/src/Squidex/app/framework/angular/panel-container.directive.ts @@ -5,50 +5,84 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Directive, ElementRef, HostListener, OnDestroy, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { AfterViewInit, Directive, ElementRef, HostListener, OnDestroy, Renderer } from '@angular/core'; -import { PanelService } from './../services/panel.service'; +import { PanelComponent } from './panel.component'; @Directive({ selector: '.panel-container' }) -export class PanelContainerDirective implements OnInit, OnDestroy { - private subscription: Subscription; - private panelsSize: number | null = null; +export class PanelContainerDirective implements AfterViewInit, OnDestroy { + private readonly panels: PanelComponent[] = []; + private isInit = false; constructor( private readonly element: ElementRef, - private readonly panels: PanelService + private readonly renderer: Renderer ) { } @HostListener('window:resize') public onResize() { - this.resize(); + this.invalidate(); + } + + public ngAfterViewInit() { + this.invalidate(true); } public ngOnDestroy() { - this.subscription.unsubscribe(); + this.isInit = true; + } + + public push(panel: PanelComponent) { + this.panels.push(panel); + + this.invalidate(); } - public ngOnInit() { - this.subscription = - this.panels.changed.subscribe(width => { - this.panelsSize = width; + public pop() { + this.panels.splice(-1, 1); - this.resize(); - }); + this.invalidate(); } - private resize() { - if (!this.panelsSize) { + public invalidate(force = false) { + this.isInit = this.isInit || force; + + if (!this.isInit) { return; } - const currentWidth = this.element.nativeElement.getBoundingClientRect().width; + const containerWidth = this.element.nativeElement.getBoundingClientRect().width; + + let currentPosition = 0; + let currentLayer = this.panels.length * 10; + + const last = this.panels[this.panels.length - 1]; + + for (let panel of this.panels) { + const panelRoot = panel.panel.nativeElement; + + let width = panelRoot.getBoundingClientRect().width; + + if (panel.expand && panel === last) { + width = containerWidth - currentPosition; + + panel.panelWidth = width + 'px'; + } + + this.renderer.setElementStyle(panelRoot, 'top', '0px'); + this.renderer.setElementStyle(panelRoot, 'left', currentPosition + 'px'); + this.renderer.setElementStyle(panelRoot, 'bottom', '0px'); + this.renderer.setElementStyle(panelRoot, 'position', 'absolute'); + this.renderer.setElementStyle(panelRoot, 'z-index', currentLayer.toString()); + + currentPosition += width; + currentLayer -= 10; + } - const diff = this.panelsSize - currentWidth; + const diff = currentPosition - containerWidth; if (diff > 0) { this.element.nativeElement.scrollLeft = diff; diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index be1089b55..54d869546 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -5,16 +5,16 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { AfterViewInit, Component, ElementRef, Input, OnDestroy, Renderer, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { slideRightAnimation } from './animations'; -import { PanelService } from './../services/panel.service'; +import { PanelContainerDirective } from './panel-container.directive'; @Component({ selector: 'sqx-panel', template: ` -
+
@@ -23,30 +23,33 @@ import { PanelService } from './../services/panel.service'; slideRightAnimation ] }) -export class PanelComponent implements OnDestroy, AfterViewInit { +export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { @Input() public theme = 'light'; @Input() public panelWidth = '10rem'; + @Input() + public expand = false; + @ViewChild('panel') public panel: ElementRef; constructor( - private readonly renderer: Renderer, - private readonly panels: PanelService + private readonly container: PanelContainerDirective ) { } public ngOnDestroy() { - this.panels.pop(this.panel.nativeElement); - this.panels.render(this.renderer); - } - public ngAfterViewInit() { - this.panels.render(this.renderer); + this.container.pop(); } + public ngOnInit() { - this.panels.push(this.panel.nativeElement); + this.container.push(this); + } + + public ngAfterViewInit() { + this.container.invalidate(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 6ca5cb097..07c55c91e 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -52,7 +52,6 @@ export * from './services/clipboard.service'; export * from './services/local-store.service'; export * from './services/message-bus'; export * from './services/notification.service'; -export * from './services/panel.service'; export * from './services/resource-loader.service'; export * from './services/shortcut.service'; export * from './services/title.service'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 3fff587a2..871b766c4 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -43,7 +43,6 @@ import { NotificationService, PanelContainerDirective, PanelComponent, - PanelService, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, @@ -177,7 +176,6 @@ export class SqxFrameworkModule { LocalStoreService, MessageBus, NotificationService, - PanelService, ResourceLoaderService, ShortcutService, TitleService diff --git a/src/Squidex/app/framework/services/panel.service.spec.ts b/src/Squidex/app/framework/services/panel.service.spec.ts deleted file mode 100644 index bee103b9e..000000000 --- a/src/Squidex/app/framework/services/panel.service.spec.ts +++ /dev/null @@ -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(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); - }); -}); \ No newline at end of file diff --git a/src/Squidex/app/framework/services/panel.service.ts b/src/Squidex/app/framework/services/panel.service.ts deleted file mode 100644 index 15a0e3e4a..000000000 --- a/src/Squidex/app/framework/services/panel.service.ts +++ /dev/null @@ -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(); - - public get changed(): Observable { - 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); - } -} \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/app/left-menu.component.html b/src/Squidex/app/shell/pages/app/left-menu.component.html index 44b65b629..5627fbac8 100644 --- a/src/Squidex/app/shell/pages/app/left-menu.component.html +++ b/src/Squidex/app/shell/pages/app/left-menu.component.html @@ -25,5 +25,10 @@ +
\ No newline at end of file diff --git a/src/Squidex/app/theme/icomoon/demo.html b/src/Squidex/app/theme/icomoon/demo.html index 0b3f476a8..285de7858 100644 --- a/src/Squidex/app/theme/icomoon/demo.html +++ b/src/Squidex/app/theme/icomoon/demo.html @@ -9,56 +9,24 @@
-

Font Name: icomoon (Glyphs: 69)

+

Font Name: icomoon (Glyphs: 67)

Grid Size: 16

- + - icon-elapsed + icon-earth
- - + +
liga: - -
-
-
-
- - - - icon-timeout -
-
- - -
-
- liga: - -
-
-
-
- - - - icon-checkmark -
-
- - -
-
- liga: - +
diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot index b33df0050..351d6b2e7 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot and b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot differ diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg index 05aebbad4..70d7b5467 100644 --- a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg +++ b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg @@ -73,7 +73,5 @@ - - - + \ No newline at end of file diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf index 06cd7e4f2..387899839 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf and b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf differ diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff b/src/Squidex/app/theme/icomoon/fonts/icomoon.woff index b7fafc67b..2a114b73b 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff and b/src/Squidex/app/theme/icomoon/fonts/icomoon.woff differ diff --git a/src/Squidex/app/theme/icomoon/selection.json b/src/Squidex/app/theme/icomoon/selection.json index 8819b5684..30f23b4ab 100644 --- a/src/Squidex/app/theme/icomoon/selection.json +++ b/src/Squidex/app/theme/icomoon/selection.json @@ -4,102 +4,35 @@ { "icon": { "paths": [ - "M512.002 193.212v-65.212h128v-64c0-35.346-28.654-64-64.002-64h-191.998c-35.346 0-64 28.654-64 64v64h128v65.212c-214.798 16.338-384 195.802-384 414.788 0 229.75 186.25 416 416 416s416-186.25 416-416c0-218.984-169.202-398.448-384-414.788zM706.276 834.274c-60.442 60.44-140.798 93.726-226.274 93.726s-165.834-33.286-226.274-93.726c-60.44-60.44-93.726-140.8-93.726-226.274s33.286-165.834 93.726-226.274c58.040-58.038 134.448-91.018 216.114-93.548l-21.678 314.020c-1.86 26.29 12.464 37.802 31.836 37.802s33.698-11.512 31.836-37.802l-21.676-314.022c81.666 2.532 158.076 35.512 216.116 93.55 60.44 60.44 93.726 140.8 93.726 226.274s-33.286 165.834-93.726 226.274z" - ], - "attrs": [ - {} + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 960.002c-62.958 0-122.872-13.012-177.23-36.452l233.148-262.29c5.206-5.858 8.082-13.422 8.082-21.26v-96c0-17.674-14.326-32-32-32-112.99 0-232.204-117.462-233.374-118.626-6-6.002-14.14-9.374-22.626-9.374h-128c-17.672 0-32 14.328-32 32v192c0 12.122 6.848 23.202 17.69 28.622l110.31 55.156v187.886c-116.052-80.956-192-215.432-192-367.664 0-68.714 15.49-133.806 43.138-192h116.862c8.488 0 16.626-3.372 22.628-9.372l128-128c6-6.002 9.372-14.14 9.372-22.628v-77.412c40.562-12.074 83.518-18.588 128-18.588 70.406 0 137.004 16.26 196.282 45.2-4.144 3.502-8.176 7.164-12.046 11.036-36.266 36.264-56.236 84.478-56.236 135.764s19.97 99.5 56.236 135.764c36.434 36.432 85.218 56.264 135.634 56.26 3.166 0 6.342-0.080 9.518-0.236 13.814 51.802 38.752 186.656-8.404 372.334-0.444 1.744-0.696 3.488-0.842 5.224-81.324 83.080-194.7 134.656-320.142 134.656z" ], + "attrs": [], "isMulticolor": false, "isMulticolor2": false, "tags": [ - "stopwatch", - "time", - "speed", - "meter", - "chronometer" + "earth", + "globe", + "language", + "web", + "internet", + "sphere", + "planet" ], + "defaultCode": 59850, "grid": 16 }, - "attrs": [ - {} - ], + "attrs": [], "properties": { - "order": 1, - "id": 2, + "ligatures": "earth, globe2", + "name": "earth", + "id": 202, + "order": 91, "prevSize": 32, - "code": 59715, - "name": "elapsed" + "code": 59850 }, - "setIdx": 1, - "setId": 2, - "iconIdx": 0 - }, - { - "icon": { - "paths": [ - "M512 128c-247.424 0-448 200.576-448 448s200.576 448 448 448 448-200.576 448-448-200.576-448-448-448zM512 936c-198.824 0-360-161.178-360-360 0-198.824 161.176-360 360-360 198.822 0 360 161.176 360 360 0 198.822-161.178 360-360 360zM934.784 287.174c16.042-28.052 25.216-60.542 25.216-95.174 0-106.040-85.96-192-192-192-61.818 0-116.802 29.222-151.92 74.596 131.884 27.236 245.206 105.198 318.704 212.578v0zM407.92 74.596c-35.116-45.374-90.102-74.596-151.92-74.596-106.040 0-192 85.96-192 192 0 34.632 9.174 67.122 25.216 95.174 73.5-107.38 186.822-185.342 318.704-212.578z", - "M512 576v-256h-64v320h256v-64z" - ], - "attrs": [ - {}, - {} - ], - "isMulticolor": false, - "isMulticolor2": false, - "tags": [ - "alarm", - "time", - "clock" - ], - "grid": 16 - }, - "attrs": [ - {}, - {} - ], - "properties": { - "order": 2, - "id": 1, - "prevSize": 32, - "code": 59716, - "name": "timeout" - }, - "setIdx": 1, - "setId": 2, - "iconIdx": 1 - }, - { - "icon": { - "paths": [ - "M864 128l-480 480-224-224-160 160 384 384 640-640z" - ], - "attrs": [ - {} - ], - "isMulticolor": false, - "isMulticolor2": false, - "tags": [ - "checkmark", - "tick", - "correct", - "accept", - "ok" - ], - "grid": 16 - }, - "attrs": [ - {} - ], - "properties": { - "order": 1, - "id": 0, - "name": "checkmark", - "prevSize": 32, - "code": 59714 - }, - "setIdx": 1, - "setId": 2, - "iconIdx": 2 + "setIdx": 0, + "setId": 4, + "iconIdx": 202 }, { "icon": { @@ -128,7 +61,7 @@ "code": 59712, "name": "microsoft" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 39 }, @@ -158,7 +91,7 @@ "code": 59707, "name": "google" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 40 }, @@ -188,7 +121,7 @@ "code": 59699, "name": "unlocked" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 41 }, @@ -220,7 +153,7 @@ "code": 59700, "name": "lock" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 42 }, @@ -259,7 +192,7 @@ "code": 59694, "name": "reset" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 43 }, @@ -289,7 +222,7 @@ "code": 59695, "name": "pause" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 44 }, @@ -319,7 +252,7 @@ "code": 59696, "name": "play" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 45 }, @@ -354,7 +287,7 @@ "code": 59693, "name": "settings2" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 46 }, @@ -391,7 +324,7 @@ "prevSize": 32, "code": 59650 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 47 }, @@ -420,7 +353,7 @@ "prevSize": 32, "code": 59711 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 0 }, @@ -450,7 +383,7 @@ "prevSize": 32, "code": 59713 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 1 }, @@ -479,7 +412,7 @@ "prevSize": 32, "code": 59652 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 2 }, @@ -508,7 +441,7 @@ "prevSize": 32, "code": 59653 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 3 }, @@ -537,7 +470,7 @@ "prevSize": 32, "code": 59654 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 4 }, @@ -566,7 +499,7 @@ "prevSize": 32, "code": 59655 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 5 }, @@ -595,7 +528,7 @@ "prevSize": 32, "code": 59656 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 6 }, @@ -624,7 +557,7 @@ "prevSize": 32, "code": 59657 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 7 }, @@ -653,7 +586,7 @@ "prevSize": 32, "code": 59658 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 8 }, @@ -682,7 +615,7 @@ "prevSize": 32, "code": 59659 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 9 }, @@ -711,7 +644,7 @@ "prevSize": 32, "code": 59660 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 10 }, @@ -740,7 +673,7 @@ "prevSize": 32, "code": 59661 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 11 }, @@ -769,7 +702,7 @@ "prevSize": 32, "code": 59662 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 12 }, @@ -798,7 +731,7 @@ "prevSize": 32, "code": 59663 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 13 }, @@ -827,7 +760,7 @@ "prevSize": 32, "code": 59664 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 14 }, @@ -856,7 +789,7 @@ "prevSize": 32, "code": 59665 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 15 }, @@ -885,7 +818,7 @@ "prevSize": 32, "code": 59666 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 16 }, @@ -914,7 +847,7 @@ "prevSize": 32, "code": 59667 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 17 }, @@ -943,7 +876,7 @@ "prevSize": 32, "code": 59668 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 18 }, @@ -972,7 +905,7 @@ "prevSize": 32, "code": 59669 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 19 }, @@ -1001,7 +934,7 @@ "prevSize": 32, "code": 59670 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 20 }, @@ -1030,7 +963,7 @@ "prevSize": 32, "code": 59671 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 21 }, @@ -1059,7 +992,7 @@ "prevSize": 32, "code": 59672 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 22 }, @@ -1088,7 +1021,7 @@ "prevSize": 32, "code": 59673 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 23 }, @@ -1117,7 +1050,7 @@ "prevSize": 32, "code": 59674 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 24 }, @@ -1146,7 +1079,7 @@ "prevSize": 32, "code": 59675 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 25 }, @@ -1175,7 +1108,7 @@ "prevSize": 32, "code": 59676 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 26 }, @@ -1204,7 +1137,7 @@ "prevSize": 32, "code": 59677 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 27 }, @@ -1233,7 +1166,7 @@ "prevSize": 32, "code": 59678 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 28 }, @@ -1262,7 +1195,7 @@ "prevSize": 32, "code": 59679 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 29 }, @@ -1291,7 +1224,7 @@ "prevSize": 32, "code": 59680 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 30 }, @@ -1320,7 +1253,7 @@ "prevSize": 32, "code": 59681 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 31 }, @@ -1349,7 +1282,7 @@ "prevSize": 32, "code": 59682 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 32 }, @@ -1378,7 +1311,7 @@ "prevSize": 32, "code": 59683 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 33 }, @@ -1407,7 +1340,7 @@ "prevSize": 32, "code": 59684 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 34 }, @@ -1436,7 +1369,7 @@ "prevSize": 32, "code": 59685 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 35 }, @@ -1465,7 +1398,7 @@ "prevSize": 32, "code": 59686 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 36 }, @@ -1494,7 +1427,7 @@ "prevSize": 32, "code": 59687 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 37 }, @@ -1523,7 +1456,7 @@ "prevSize": 32, "code": 59688 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 38 }, @@ -1552,7 +1485,7 @@ "code": 59710, "name": "download" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 48 }, @@ -1581,7 +1514,7 @@ "code": 59705, "name": "control-RichText" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 49 }, @@ -1611,7 +1544,7 @@ "code": 59709, "name": "bug" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 50 }, @@ -1640,7 +1573,7 @@ "prevSize": 28, "code": 59704 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 51 }, @@ -1669,7 +1602,7 @@ "prevSize": 28, "code": 59702 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 52 }, @@ -1698,7 +1631,7 @@ "prevSize": 28, "code": 59703 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 53 }, @@ -1728,7 +1661,7 @@ "code": 59697, "name": "angle-right" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 54 }, @@ -1757,7 +1690,7 @@ "prevSize": 28, "code": 59698 }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 55 }, @@ -1787,7 +1720,7 @@ "code": 59689, "name": "caret-right" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 56 }, @@ -1817,7 +1750,7 @@ "code": 59690, "name": "caret-left" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 57 }, @@ -1847,7 +1780,7 @@ "code": 59691, "name": "caret-up" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 58 }, @@ -1877,7 +1810,7 @@ "code": 59692, "name": "caret-down" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 59 }, @@ -1907,7 +1840,7 @@ "code": 59651, "name": "angle-up" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 60 }, @@ -1937,7 +1870,7 @@ "code": 59648, "name": "angle-down" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 61 }, @@ -1967,7 +1900,7 @@ "code": 59649, "name": "angle-left" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 62 }, @@ -1996,7 +1929,7 @@ "code": 59708, "name": "info" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 63 }, @@ -2026,7 +1959,7 @@ "code": 59706, "name": "control-Stars" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 64 }, @@ -2059,7 +1992,7 @@ "code": 59701, "name": "browser" }, - "setIdx": 2, + "setIdx": 3, "setId": 1, "iconIdx": 65 } diff --git a/src/Squidex/app/theme/icomoon/style.css b/src/Squidex/app/theme/icomoon/style.css index dd99dbad7..1f6bce96a 100644 --- a/src/Squidex/app/theme/icomoon/style.css +++ b/src/Squidex/app/theme/icomoon/style.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?3aocbn'); - src: url('fonts/icomoon.eot?3aocbn#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?3aocbn') format('truetype'), - url('fonts/icomoon.woff?3aocbn') format('woff'), - url('fonts/icomoon.svg?3aocbn#icomoon') format('svg'); + src: url('fonts/icomoon.eot?oi12nb'); + src: url('fonts/icomoon.eot?oi12nb#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?oi12nb') format('truetype'), + url('fonts/icomoon.woff?oi12nb') format('woff'), + url('fonts/icomoon.svg?oi12nb#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -24,14 +24,8 @@ -moz-osx-font-smoothing: grayscale; } -.icon-elapsed:before { - content: "\e943"; -} -.icon-timeout:before { - content: "\e944"; -} -.icon-checkmark:before { - content: "\e942"; +.icon-earth:before { + content: "\e9ca"; } .icon-microsoft:before { content: "\e940"; diff --git a/src/Squidex/app/theme/theme.scss b/src/Squidex/app/theme/theme.scss index 55e4f8af4..72aa22ddc 100644 --- a/src/Squidex/app/theme/theme.scss +++ b/src/Squidex/app/theme/theme.scss @@ -1,13 +1,13 @@ @import '_bootstrap-vars.scss'; // Bootstrap -@import './../../node_modules/bootstrap/scss/bootstrap.scss'; +@import '~bootstrap/scss/bootstrap.scss'; // Pikaday -@import './../../node_modules/pikaday/css/pikaday.css'; +@import '~pikaday/css/pikaday.css'; // Drag and Drop -@import './../../node_modules/ng2-dnd/bundles/style.css'; +@import '~ng2-dnd/bundles/style.css'; // Bootstrap Overrides @import '_bootstrap.scss'; diff --git a/src/Squidex/package.json b/src/Squidex/package.json index 08af4ad46..d8399bf1c 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -24,17 +24,20 @@ "@angular/platform-browser": "4.2.5", "@angular/platform-browser-dynamic": "4.2.5", "@angular/router": "4.2.5", - "angular2-chartjs": "^0.2.0", "angular-progress-http": "0.5.1", + "angular2-chartjs": "^0.2.0", "babel-polyfill": "6.23.0", "bootstrap": "4.0.0-alpha.6", "core-js": "2.4.1", + "graphiql": "^0.11.2", "moment": "2.18.1", "mousetrap": "1.6.1", "ng2-dnd": "4.2.0", "oidc-client": "1.3.0", "pikaday": "1.6.1", "progressbar.js": "1.0.1", + "react": "^15.6.1", + "react-dom": "^15.6.1", "redoc": "1.16.1", "rxjs": "5.4.2", "zone.js": "0.8.12" @@ -47,6 +50,8 @@ "@types/jasmine": "2.5.43", "@types/mousetrap": "1.5.33", "@types/node": "7.0.5", + "@types/react": "^15.0.35", + "@types/react-dom": "^15.5.1", "angular2-router-loader": "0.3.5", "angular2-template-loader": "0.6.2", "awesome-typescript-loader": "3.2.1",