Browse Source

Merge branch 'next' into workflow-ui

pull/372/head
Sebastian Stehle 7 years ago
parent
commit
c95b38e3b2
  1. 28
      CHANGELOG.md
  2. 12
      Dockerfile
  3. 12
      Dockerfile.build
  4. 4
      README.md
  5. 42
      src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs
  6. 53
      src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  7. 46
      src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs
  8. 5
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
  9. 37
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs
  10. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs
  11. 7
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  12. 9
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  13. 1
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  14. 8
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs
  15. 64
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  16. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  17. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs
  18. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  19. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  20. 20
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  21. 7
      src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  22. 21
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  23. 38
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  24. 13
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  25. 54
      src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  26. 41
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  27. 99
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  28. 18
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  29. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  30. 42
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs
  31. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  32. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  33. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  34. 9
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs
  35. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  36. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  37. 35
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  38. 5
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  39. 10
      src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  40. 12
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  41. 8
      src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs
  42. 5
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  43. 5
      src/Squidex.Domain.Apps.Entities/Q.cs
  44. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs
  45. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs
  46. 8
      src/Squidex.Infrastructure/CollectionExtensions.cs
  47. 36
      src/Squidex/.vscode/settings.json
  48. 4
      src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
  49. 6
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  50. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  51. 29
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  52. 2
      src/Squidex/Areas/Api/Controllers/Contents/Helper.cs
  53. 26
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  54. 36
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  55. 10
      src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs
  56. 5
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs
  57. 6
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  58. 27
      src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
  59. 17
      src/Squidex/Config/Domain/EntitiesServices.cs
  60. 2
      src/Squidex/Config/Domain/SerializationServices.cs
  61. 25
      src/Squidex/app-config/helpers.js
  62. 3
      src/Squidex/app-config/karma-test-shim.js
  63. 6
      src/Squidex/app-config/karma.conf.js
  64. 6
      src/Squidex/app-config/karma.coverage.conf.js
  65. 446
      src/Squidex/app-config/webpack.config.js
  66. 35
      src/Squidex/app-config/webpack.run.base.js
  67. 68
      src/Squidex/app-config/webpack.run.dev.js
  68. 137
      src/Squidex/app-config/webpack.run.prod.js
  69. 37
      src/Squidex/app-config/webpack.test.coverage.js
  70. 16
      src/Squidex/app-config/webpack.test.js
  71. 18
      src/Squidex/app/app.routes.ts
  72. 2
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts
  73. 2
      src/Squidex/app/features/apps/pages/apps-page.component.scss
  74. 2
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  75. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  76. 4
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  77. 10
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  78. 8
      src/Squidex/app/features/rules/pages/rules/rule-element.component.scss
  79. 8
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  80. 3
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  81. 1
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  82. 2
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  83. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  84. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  85. 2
      src/Squidex/app/features/settings/pages/roles/role.component.ts
  86. 4
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  87. 2
      src/Squidex/app/framework/angular/forms/code-editor.component.ts
  88. 2
      src/Squidex/app/framework/angular/forms/date-time-editor.component.ts
  89. 2
      src/Squidex/app/framework/angular/forms/iframe-editor.component.ts
  90. 2
      src/Squidex/app/framework/angular/forms/json-editor.component.ts
  91. 4
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  92. 4
      src/Squidex/app/framework/angular/modals/modal-dialog.component.ts
  93. 2
      src/Squidex/app/framework/angular/modals/root-view.component.ts
  94. 2
      src/Squidex/app/framework/angular/panel.component.ts
  95. 4
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  96. 6
      src/Squidex/app/shared/components/markdown-editor.component.ts
  97. 2
      src/Squidex/app/shared/components/rich-editor.component.ts
  98. 24
      src/Squidex/app/shared/components/schema-category.component.html
  99. 68
      src/Squidex/app/shared/components/schema-category.component.ts
  100. 2
      src/Squidex/app/shared/services/assets.service.ts

28
CHANGELOG.md

@ -1,5 +1,33 @@
# Changelog
## v2.1.0 - 2019-06-22
## Features
* **Assets**: Parameter to prevent download in Browser.
* **Assets**: FTP asset store.
* **GraphQL**: Logging for field resolvers
* **GraphQL**: Performance optimizations for asset fields and references with DataLoader.
* **MongoDB**: Performance optimizations.
* **MongoDB**: Support for AWS DocumentDB.
* **Schemas**: Separator field.
* **Schemas**: Setting to prevent duplicate references.
* **UI**: Improved styling of DateTime editor.
* **UI**: Custom Editors: Provide all values.
* **UI**: Custom Editors: Provide context with user information and auth token.
* **UI**: Filter by status.
* **UI**: Dropdown field for references.
* **Users**: Email notifications when contributors is added.
## Bugfixes
* **Contents**: Fix for scheduled publishing.
* **GraphQL**: Fix query parameters for assets.
* **GraphQL**: Fix for duplicate field names in GraphQL.
* **GraphQL**: Fix for invalid field names.
* **Plans**: Fix when plans reset and extra events.
* **UI**: Unify slugify in Frontend and Backend.
## v2.0.5 - 2019-04-21
## Features

12
Dockerfile

@ -5,11 +5,20 @@ FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
WORKDIR /src
# Copy Node project files.
COPY src/Squidex/package*.json /tmp/
# Install Node packages
RUN cd /tmp && npm install --loglevel=error
# Copy nuget project files.
COPY /**/**/*.csproj /tmp/
# Copy nuget.config for package sources.
COPY NuGet.Config /tmp/
# Install nuget packages
RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd'
COPY . .
# Build Frontend
@ -19,8 +28,7 @@ RUN cp -a /tmp/node_modules src/Squidex/ \
&& npm run build
# Test Backend
RUN dotnet restore \
&& dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \
&& dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \

12
Dockerfile.build

@ -2,11 +2,20 @@ FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
WORKDIR /src
# Copy Node project files.
COPY src/Squidex/package*.json /tmp/
# Install Node packages
RUN cd /tmp && npm install --loglevel=error
# Copy Dotnet project files.
COPY /**/**/*.csproj /tmp/
# Copy nuget.config for package sources.
COPY NuGet.Config /tmp/
# Install Dotnet packages
RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd'
COPY . .
# Build Frontend
@ -16,8 +25,7 @@ RUN cp -a /tmp/node_modules src/Squidex/ \
&& npm run build
# Test Backend
RUN dotnet restore \
&& dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \
&& dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \

4
README.md

@ -16,7 +16,7 @@ Please join our community forum: https://support.squidex.io
## Status
Current Version v2.0.4. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
Current Version v2.1.0. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
## Prerequisites
@ -50,4 +50,4 @@ Although Squidex is free it is also available as a Saas version on [https://clou
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FSquidex%2Fsquidex.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FSquidex%2Fsquidex?ref=badge_large)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FSquidex%2Fsquidex.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FSquidex%2Fsquidex?ref=badge_large)

42
src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs

@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
using System;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Contents.Json
{
public sealed class StatusConverter : JsonConverter, ISupportedTypes
{
public IEnumerable<Type> SupportedTypes
{
get { yield return typeof(Status); }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.String)
{
throw new JsonException($"Expected String, but got {reader.TokenType}.");
}
return new Status(reader.Value.ToString());
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Status);
}
}
}

53
src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -5,12 +5,57 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Contents
{
public enum Status
public struct Status : IEquatable<Status>
{
Draft,
Archived,
Published
public static readonly Status Archived = new Status("Archived");
public static readonly Status Draft = new Status("Draft");
public static readonly Status Published = new Status("Published");
private readonly string name;
public string Name
{
get { return name ?? "Unknown"; }
}
public Status(string name)
{
this.name = name;
}
public override bool Equals(object obj)
{
return obj is Status status && Equals(status);
}
public bool Equals(Status other)
{
return string.Equals(name, other.name);
}
public override int GetHashCode()
{
return name?.GetHashCode() ?? 0;
}
public override string ToString()
{
return name;
}
public static bool operator ==(Status lhs, Status rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(Status lhs, Status rhs)
{
return !lhs.Equals(rhs);
}
}
}

46
src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs

@ -1,46 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Contents
{
public struct Status2 : IEquatable<Status2>
{
public static readonly Status2 Published = new Status2("Published");
public string Name { get; }
public Status2(string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
Name = name;
}
public override bool Equals(object obj)
{
return obj is Status2 status && Equals(status);
}
public bool Equals(Status2 other)
{
return Name.Equals(other.Name);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public override string ToString()
{
return Name;
}
}
}

5
src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs

@ -1,7 +1,7 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
@ -9,9 +9,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
public enum StatusChange
{
Archived,
Change,
Published,
Restored,
Unpublished
}
}

37
src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs

@ -1,37 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusFlow
{
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[Status.Draft] = new[] { Status.Published, Status.Archived },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
};
public static bool Exists(Status status)
{
return Flow.ContainsKey(status);
}
public static bool CanChange(Status status, Status toStatus)
{
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus);
}
public static IEnumerable<Status> Next(Status status)
{
return Flow.TryGetValue(status, out var result) ? result : Enumerable.Empty<Status>();
}
}
}

5
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs

@ -9,12 +9,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
public enum EnrichedContentEventType
{
Archived,
Created,
Deleted,
Published,
Restored,
StatusChanged,
Updated,
Unpublished,
Updated
}
}

7
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -119,12 +119,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified);
var assetItems = find.ToListAsync();
var assetCount = find.CountDocumentsAsync();
var assetItems = await find.ToListAsync();
await Task.WhenAll(assetItems, assetCount);
return ResultList.Create(assetCount.Result, assetItems.Result.OfType<IAssetEntity>());
return ResultList.Create(assetItems.Count, assetItems.OfType<IAssetEntity>());
}
}

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

@ -137,17 +137,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status));
var contentItems = find.WithoutDraft(includeDraft).ToListAsync();
var contentCount = find.CountDocumentsAsync();
await Task.WhenAll(contentItems, contentCount);
var contentItems = await find.WithoutDraft(includeDraft).ToListAsync();
foreach (var entity in contentItems.Result)
foreach (var entity in contentItems)
{
entity.ParseData(schema.SchemaDef, serializer);
}
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result);
return ResultList.Create<IContentEntity>(contentItems.Count, contentItems);
}
public async Task<IContentEntity> FindContentAsync(ISchemaEntity schema, Guid id, Status[] status, bool includeDraft)

1
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -51,7 +51,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonRequired]
[BsonElement("ss")]
[BsonRepresentation(BsonType.String)]
public Status Status { get; set; }
[BsonIgnoreIfNull]

8
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs

@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public sealed class StatusSerializer : SerializerBase<Status2>
public sealed class StatusSerializer : SerializerBase<Status>
{
private static volatile int isRegistered;
@ -24,14 +24,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public override Status2 Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
public override Status Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var value = context.Reader.ReadString();
return new Status2(value);
return new Status(value);
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status2 value)
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status value)
{
context.Writer.WriteString(value.Name);
}

64
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
@ -24,25 +25,28 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
IAssetQueryService assetQuery,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators,
ITagService tagService)
: base(grainFactory)
{
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators));
Guard.NotNull(tagService, nameof(tagService));
this.assetStore = assetStore;
this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators;
this.tagService = tagService;
}
public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -56,9 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.Tags = new HashSet<string>();
}
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead());
createAsset.FileHash = await UploadAsync(context, createAsset.File);
await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, context);
try
{
@ -70,7 +73,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
if (IsDuplicate(createAsset, existing))
{
result = new AssetCreatedResult(existing, true);
var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags);
result = new AssetCreatedResult(existing, true, new HashSet<string>(denormalizedTags.Values));
}
break;
@ -85,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset);
result = new AssetCreatedResult(asset, false);
result = new AssetCreatedResult(asset, false, createAsset.Tags);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
}
@ -102,16 +107,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
case UpdateAsset updateAsset:
{
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead());
await EnrichWithImageInfosAsync(updateAsset);
await EnrichWithHashAndUploadAsync(updateAsset, context);
updateAsset.FileHash = await UploadAsync(context, updateAsset.File);
try
{
var result = (IAssetEntity)await ExecuteCommandAsync(updateAsset);
var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset);
context.Complete(result);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null);
}
finally
{
@ -121,29 +126,54 @@ namespace Squidex.Domain.Apps.Entities.Assets
break;
}
case AssetCommand command:
{
var result = await ExecuteAndAdjustTagsAsync(command);
context.Complete(result);
break;
}
default:
await base.HandleAsync(context, next);
break;
}
}
private async Task<object> ExecuteAndAdjustTagsAsync(AssetCommand command)
{
var result = await ExecuteCommandAsync(command);
if (result is IAssetEntity asset)
{
var denormalizedTags = await tagService.DenormalizeTagsAsync(asset.AppId.Id, TagGroups.Assets, asset.Tags);
return new AssetResult(asset, new HashSet<string>(denormalizedTags.Values));
}
return result;
}
private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset)
{
return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize;
}
private async Task<string> UploadAsync(CommandContext context, AssetFile file)
private async Task EnrichWithImageInfosAsync(UploadAssetCommand command)
{
string hash;
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
}
using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256))
private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, CommandContext context)
{
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
{
await assetStore.UploadAsync(context.ContextId.ToString(), hashStream);
hash = $"{hashStream.GetHashStringAndReset()}{file.FileName}{file.FileSize}".Sha256Base64();
command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64();
}
return hash;
}
}
}

11
src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs

@ -5,18 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetCreatedResult
public sealed class AssetCreatedResult : AssetResult
{
public IAssetEntity Asset { get; }
public bool IsDuplicate { get; }
public AssetCreatedResult(IAssetEntity asset, bool isDuplicate)
public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet<string> tags)
: base(asset, tags)
{
Asset = asset;
IsDuplicate = isDuplicate;
}
}

25
src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetResult
{
public IAssetEntity Asset { get; }
public HashSet<string> Tags { get; }
public AssetResult(IAssetEntity asset, HashSet<string> tags)
{
Asset = asset;
Tags = tags;
}
}
}

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -8,22 +8,15 @@
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class CreateAsset : AssetCommand, IAppCommand
public sealed class CreateAsset : UploadAssetCommand, IAppCommand
{
public NamedId<Guid> AppId { get; set; }
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset()
{
AssetId = Guid.NewGuid();

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs

@ -5,16 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class UpdateAsset : AssetCommand
public sealed class UpdateAsset : UploadAssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

20
src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public abstract class UploadAssetCommand : AssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

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

@ -69,11 +69,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
case StatusChange.Unpublished:
result.Type = EnrichedContentEventType.Unpublished;
break;
case StatusChange.Archived:
result.Type = EnrichedContentEventType.Archived;
break;
case StatusChange.Restored:
result.Type = EnrichedContentEventType.Restored;
default:
result.Type = EnrichedContentEventType.StatusChanged;
break;
}

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

@ -8,9 +8,7 @@
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents
{
@ -41,24 +39,5 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Status Status { get; set; }
public bool IsPending { get; set; }
public static ContentEntity Create(CreateContent command, EntityCreatedResult<NamedContentData> result)
{
var now = SystemClock.Instance.GetCurrentInstant();
var response = new ContentEntity
{
Id = command.ContentId,
Data = result.IdOrValue,
Version = result.Version,
Created = now,
CreatedBy = command.Actor,
LastModified = now,
LastModifiedBy = command.Actor,
Status = command.Publish ? Status.Published : Status.Draft
};
return response;
}
}
}

38
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -32,6 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
private readonly IScriptEngine scriptEngine;
private readonly IContentWorkflow contentWorkflow;
public ContentGrain(
IStore<Guid> store,
@ -39,17 +40,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository)
: base(store, log)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentWorkflow, nameof(contentWorkflow));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository;
}
@ -79,25 +83,27 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data);
}
Create(c);
var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
Create(c, status);
return Snapshot;
});
case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, c =>
return UpdateReturnAsync(updateContent, async c =>
{
GuardContent.CanUpdate(c);
await GuardContent.CanUpdate(Snapshot, contentWorkflow, c);
return UpdateAsync(c, x => c.Data, false);
return await UpdateAsync(c, x => c.Data, false);
});
case PatchContent patchContent:
return UpdateReturnAsync(patchContent, c =>
return UpdateReturnAsync(patchContent, async c =>
{
GuardContent.CanPatch(c);
await GuardContent.CanPatch(Snapshot, contentWorkflow, c);
return UpdateAsync(c, c.Data.MergeInto, true);
return await UpdateAsync(c, c.Data.MergeInto, true);
});
case ChangeContentStatus changeContentStatus:
@ -107,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to change content.");
GuardContent.CanChangeContentStatus(ctx.Schema, Snapshot.IsPending, Snapshot.Status, c);
await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c);
if (c.DueTime.HasValue)
{
@ -127,17 +133,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
reason = StatusChange.Published;
}
else if (c.Status == Status.Archived)
{
reason = StatusChange.Archived;
}
else if (Snapshot.Status == Status.Published)
{
reason = StatusChange.Unpublished;
}
else
{
reason = StatusChange.Restored;
reason = StatusChange.Change;
}
await ctx.ExecuteScriptAsync(s => s.Change, reason, c, Snapshot.Data);
@ -227,13 +229,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Snapshot;
}
public void Create(CreateContent command)
public void Create(CreateContent command, Status status)
{
RaiseEvent(SimpleMapper.Map(command, new ContentCreated()));
RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status }));
if (command.Publish)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
}
}
@ -272,9 +274,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value }));
}
public void ChangeStatus(ChangeContentStatus command, StatusChange reason)
public void ChangeStatus(ChangeContentStatus command, StatusChange change)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = reason }));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change }));
}
private void RaiseEvent(SchemaEvent @event)

13
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -73,16 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.scriptEngine = scriptEngine;
}
public Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName)
{
return GetSchemaAsync(context, schemaIdOrName);
}
public async Task<IContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1)
{
Guard.NotNull(context, nameof(context));
var schema = await GetSchemaAsync(context, schemaIdOrName);
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context.User, schema);
@ -110,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
Guard.NotNull(context, nameof(context));
var schema = await GetSchemaAsync(context, schemaIdOrName);
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context.User, schema);
@ -136,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public async Task<IList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids)
public async Task<IReadOnlyList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids)
{
Guard.NotNull(context, nameof(context));
@ -295,7 +290,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public async Task<ISchemaEntity> GetSchemaAsync(QueryContext context, string schemaIdOrName)
public async Task<ISchemaEntity> GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName)
{
ISchemaEntity schema = null;

54
src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class DefaultContentWorkflow : IContentWorkflow
{
private static readonly Status[] All = { Status.Archived, Status.Draft, Status.Published };
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[Status.Draft] = new[] { Status.Archived, Status.Published },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
};
public Task<Status> GetInitialStatusAsync(ISchemaEntity schema)
{
return Task.FromResult(Status.Draft);
}
public Task<bool> CanMoveToAsync(IContentEntity content, Status next)
{
return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next));
}
public Task<bool> CanUpdateAsync(IContentEntity content)
{
return Task.FromResult(content.Status != Status.Archived);
}
public Task<Status[]> GetNextsAsync(IContentEntity content)
{
return Task.FromResult(Flow.TryGetValue(content.Status, out var result) ? result : Array.Empty<Status>());
}
public Task<Status[]> GetAllAsync(ISchemaEntity schema)
{
return Task.FromResult(All);
}
}
}

41
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -8,6 +8,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using GraphQL;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
@ -18,28 +19,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetQueryService assetQuery;
private readonly IAppProvider appProvider;
public CachingGraphQLService(
IMemoryCache cache,
IAppProvider appProvider,
IAssetQueryService assetQuery,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
private readonly IDependencyResolver resolver;
public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver)
: base(cache)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider;
this.assetQuery = assetQuery;
this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator;
Guard.NotNull(resolver, nameof(resolver));
this.resolver = resolver;
}
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries)
@ -49,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var model = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator);
var ctx = new GraphQLExecutionContext(context, resolver);
var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q)));
@ -63,14 +50,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var model = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator);
var ctx = new GraphQLExecutionContext(context, resolver);
var result = await QueryInternalAsync(model, ctx, query);
return result;
}
private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query)
private async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query)
{
if (string.IsNullOrWhiteSpace(query.Query))
{
@ -97,9 +84,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
var allSchemas = await appProvider.GetSchemasAsync(app.Id);
var allSchemas = await resolver.Resolve<IAppProvider>().GetSchemasAsync(app.Id);
return new GraphQLModel(app, allSchemas, contentQuery.DefaultPageSizeGraphQl, assetQuery.DefaultPageSizeGraphQl, urlGenerator);
return new GraphQLModel(app,
allSchemas,
resolver.Resolve<IContentQueryService>().DefaultPageSizeGraphQl,
resolver.Resolve<IAssetQueryService>().DefaultPageSizeGraphQl,
resolver.Resolve<IGraphQLUrlGenerator>());
});
}

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

@ -7,37 +7,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.DataLoader;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class GraphQLExecutionContext : QueryExecutionContext
{
private static readonly List<IAssetEntity> EmptyAssets = new List<IAssetEntity>();
private static readonly List<IContentEntity> EmptyContents = new List<IContentEntity>();
private readonly IDataLoaderContextAccessor dataLoaderContextAccessor;
private readonly IDependencyResolver resolver;
public IGraphQLUrlGenerator UrlGenerator { get; }
public GraphQLExecutionContext(QueryContext context,
IAssetQueryService assetQuery,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(context, assetQuery, contentQuery)
public ISemanticLog Log { get; }
public GraphQLExecutionContext(QueryContext context, IDependencyResolver resolver)
: base(context,
resolver.Resolve<IAssetQueryService>(),
resolver.Resolve<IContentQueryService>())
{
UrlGenerator = resolver.Resolve<IGraphQLUrlGenerator>();
dataLoaderContextAccessor = resolver.Resolve<IDataLoaderContextAccessor>();
this.resolver = resolver;
}
public void Setup(ExecutionOptions execution)
{
var loader = resolver.Resolve<DataLoaderDocumentListener>();
var logger = LoggingMiddleware.Create(resolver.Resolve<ISemanticLog>());
execution.Listeners.Add(loader);
execution.FieldMiddleware.Use(logger);
execution.UserContext = this;
}
public override Task<IAssetEntity> FindAssetAsync(Guid id)
{
UrlGenerator = urlGenerator;
var dataLoader = GetAssetsLoader();
return dataLoader.LoadAsync(id);
}
public override Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id)
{
var dataLoader = GetContentsLoader(schemaId);
return dataLoader.LoadAsync(id);
}
public Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
{
var ids = ParseIds(value);
return GetReferencedAssetsAsync(ids);
if (ids == null)
{
return EmptyAssets;
}
var dataLoader = GetAssetsLoader();
return await dataLoader.LoadManyAsync(ids);
}
public Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, IJsonValue value)
public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, IJsonValue value)
{
var ids = ParseIds(value);
return GetReferencedContentsAsync(schemaId, ids);
if (ids == null)
{
return EmptyContents;
}
var dataLoader = GetContentsLoader(schemaId);
return await dataLoader.LoadManyAsync(ids);
}
private IDataLoader<Guid, IAssetEntity> GetAssetsLoader()
{
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IAssetEntity>("Assets",
async batch =>
{
var result = await GetReferencedAssetsAsync(new List<Guid>(batch));
return result.ToDictionary(x => x.Id);
});
}
private IDataLoader<Guid, IContentEntity> GetContentsLoader(Guid schemaId)
{
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IContentEntity>($"Schema_{schemaId}",
async batch =>
{
var result = await GetReferencedContentsAsync(schemaId, new List<Guid>(batch));
return result.ToDictionary(x => x.Id);
});
}
private static ICollection<Guid> ParseIds(IJsonValue value)
@ -58,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
catch
{
return new List<Guid>();
return null;
}
}
}

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

@ -135,9 +135,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key);
}
public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field)
public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName)
{
return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType));
return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType, fieldName));
}
public IGraphType GetAssetType()
@ -175,15 +175,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
Guard.NotNull(context, nameof(context));
var inputs = query.Variables?.ToInputs();
var result = await new DocumentExecuter().ExecuteAsync(options =>
var result = await new DocumentExecuter().ExecuteAsync(execution =>
{
options.OperationName = query.OperationName;
options.UserContext = context;
options.Schema = graphQLSchema;
options.Inputs = inputs;
options.Query = query.Query;
context.Setup(execution);
execution.Schema = graphQLSchema;
execution.Inputs = query.Variables?.ToInputs();
execution.Query = query.Query;
}).ConfigureAwait(false);
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray());

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

@ -35,6 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IGraphType GetContentDataType(Guid schemaId);
(IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field);
(IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName);
}
}

42
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs

@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL.Instrumentation;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public static class LoggingMiddleware
{
public static Func<FieldMiddlewareDelegate, FieldMiddlewareDelegate> Create(ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
return new Func<FieldMiddlewareDelegate, FieldMiddlewareDelegate>(next =>
{
return async context =>
{
try
{
return await next(context);
}
catch (Exception ex)
{
log.LogWarning(ex, w => w
.WriteProperty("action", "reolveField")
.WriteProperty("status", "failed")
.WriteProperty("field", context.FieldName));
throw ex;
}
};
});
}
}
}

4
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs

@ -28,8 +28,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType Float = new FloatGraphType();
public static readonly IGraphType Status = new EnumerationGraphType<Status>();
public static readonly IGraphType String = new StringGraphType();
public static readonly IGraphType Boolean = new BooleanGraphType();
@ -46,8 +44,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean);
public static readonly IGraphType NonNullStatusType = new NonNullGraphType(Status);
public static readonly IGraphType NoopDate = new NoopGraphType(Date);
public static readonly IGraphType NoopJson = new NoopGraphType(Json);

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

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields())
{
var (resolvedType, valueResolver) = model.GetGraphType(schema, field);
var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName);
if (valueResolver != null)
{

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

@ -73,8 +73,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullStatusType,
Resolver = Resolve(x => x.Status),
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()),
Description = $"The the status of the {schemaName} content."
});

9
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs

@ -7,6 +7,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GraphQL.DataLoader;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
@ -37,5 +39,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return value;
}
public static async Task<IReadOnlyList<T>> LoadManyAsync<TKey, T>(this IDataLoader<TKey, T> dataLoader, ICollection<TKey> keys) where T : class
{
var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x)));
return contents.Where(x => x != null).ToList();
}
}
}

10
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs

@ -16,18 +16,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class NestedGraphType : ObjectGraphType<JsonObject>
{
public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field)
public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
var fieldName = field.DisplayName();
var fieldDisplayName = field.DisplayName();
Name = $"{schemaType}{fieldName}ChildDto";
foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields())
{
var fieldInfo = model.GetGraphType(schema, nestedField);
var fieldInfo = model.GetGraphType(schema, nestedField, nestedName);
if (fieldInfo.ResolveType != null)
{
@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = nestedName,
Resolver = resolver,
ResolvedType = fieldInfo.ResolveType,
Description = $"The {fieldName}/{nestedField.DisplayName()} nested field."
Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field."
});
}
}
Description = $"The structure of the {schemaName}.{fieldName} nested schema.";
Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema.";
}
private static FuncFieldResolver<object> ValueResolver(NestedField nestedField, (IGraphType ResolveType, ValueResolver Resolver) fieldInfo)

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

@ -22,13 +22,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
private readonly Func<Guid, IGraphType> schemaResolver;
private readonly IGraphModel model;
private readonly IGraphType assetListType;
private readonly string fieldName;
public QueryGraphTypeVisitor(ISchemaEntity schema, Func<Guid, IGraphType> schemaResolver, IGraphModel model, IGraphType assetListType)
public QueryGraphTypeVisitor(ISchemaEntity schema, Func<Guid, IGraphType> schemaResolver, IGraphModel model, IGraphType assetListType, string fieldName)
{
this.model = model;
this.assetListType = assetListType;
this.schema = schema;
this.schemaResolver = schemaResolver;
this.fieldName = fieldName;
}
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IArrayField field)
@ -93,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
private (IGraphType ResolveType, ValueResolver Resolver) ResolveNested(IArrayField field)
{
var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field)));
var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, this.fieldName)));
return (schemaFieldType, NoopResolver);
}

35
src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
@ -30,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
}
}
public static void CanUpdate(UpdateContent command)
public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command)
{
Guard.NotNull(command, nameof(command));
@ -38,9 +39,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
ValidateData(command, e);
});
await ValidateCanUpdate(content, contentWorkflow);
}
public static void CanPatch(PatchContent command)
public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command)
{
Guard.NotNull(command, nameof(command));
@ -48,6 +51,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
ValidateData(command, e);
});
await ValidateCanUpdate(content, contentWorkflow);
}
public static void CanDiscardChanges(bool isPending, DiscardChanges command)
@ -60,33 +65,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
}
}
public static void CanChangeContentStatus(ISchemaEntity schema, bool isPending, Status status, ChangeContentStatus command)
public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command)
{
Guard.NotNull(command, nameof(command));
if (schema.SchemaDef.IsSingleton && command.Status != Status.Published)
{
throw new DomainException("Singleton content archived or unpublished.");
throw new DomainException("Singleton content cannot be changed.");
}
Validate.It(() => "Cannot change status.", e =>
return Validate.It(() => "Cannot change status.", async e =>
{
if (!StatusFlow.Exists(command.Status))
if (!await contentWorkflow.CanMoveToAsync(content, command.Status))
{
e(Not.Valid("Status"), nameof(command.Status));
}
else if (!StatusFlow.CanChange(status, command.Status))
{
if (status == command.Status && status == Status.Published)
if (content.Status == command.Status && content.Status == Status.Published)
{
if (!isPending)
if (!content.IsPending)
{
e("Content has no changes to publish.", nameof(command.Status));
}
}
else
{
e($"Cannot change status from {status} to {command.Status}.", nameof(command.Status));
e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status));
}
}
@ -114,5 +115,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
e(Not.Defined("Data"), nameof(command.Data));
}
}
private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow)
{
if (!await contentWorkflow.CanUpdateAsync(content))
{
throw new DomainException($"The workflow does not allow updates at status {content.Status}");
}
}
}
}

5
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
@ -16,12 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
int DefaultPageSizeGraphQl { get; }
Task<IList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids);
Task<IReadOnlyList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query);
Task<IContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any);
Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName);
Task<ISchemaEntity> GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName);
}
}

10
src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -13,12 +13,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentWorkflow
{
Task<Status2> GetInitialStatusAsync(ISchemaEntity schema);
Task<Status> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> IsValidNextStatus(IContentEntity content, Status2 next);
Task<bool> CanMoveToAsync(IContentEntity content, Status next);
Task<Status2[]> GetNextsAsync(IContentEntity content);
Task<bool> CanUpdateAsync(IContentEntity content);
Task<Status2[]> GetAllAsync(ISchemaEntity schema);
Task<Status[]> GetNextsAsync(IContentEntity content);
Task<Status[]> GetAllAsync(ISchemaEntity schema);
}
}

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

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.context = context;
}
public async Task<IAssetEntity> FindAssetAsync(Guid id)
public virtual async Task<IAssetEntity> FindAssetAsync(Guid id)
{
var asset = cachedAssets.GetOrDefault(id);
@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return asset;
}
public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id)
public virtual async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id)
{
var content = cachedContents.GetOrDefault(id);
@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return content;
}
public async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
public virtual async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
{
var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query));
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return assets;
}
public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
public virtual async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
{
var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query));
@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return result;
}
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids)
public virtual async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids)
{
Guard.NotNull(ids, nameof(ids));
@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList();
}
public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids)
public virtual async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids)
{
Guard.NotNull(ids, nameof(ids));

8
src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs

@ -22,17 +22,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Instant DueTime { get; }
public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant due)
public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime)
{
Id = id;
ScheduledBy = scheduledBy;
Status = status;
DueTime = due;
DueTime = dueTime;
}
public static ScheduleJob Build(Status status, RefToken by, Instant due)
public static ScheduleJob Build(Status status, RefToken scheduledBy, Instant dueTime)
{
return new ScheduleJob(Guid.NewGuid(), status, by, due);
return new ScheduleJob(Guid.NewGuid(), status, scheduledBy, dueTime);
}
}
}

5
src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -50,6 +50,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
SimpleMapper.Map(@event, this);
UpdateData(null, @event.Data, false);
if (Status == default)
{
Status = Status.Draft;
}
}
protected void On(ContentChangesPublished @event)

5
src/Squidex.Domain.Apps.Entities/Q.cs

@ -25,6 +25,11 @@ namespace Squidex.Domain.Apps.Entities
return Clone(c => c.ODataQuery = odataQuery);
}
public Q WithIds(params Guid[] ids)
{
return Clone(c => c.Ids = ids.ToList());
}
public Q WithIds(IEnumerable<Guid> ids)
{
return Clone(c => c.Ids = ids.ToList());

2
src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs

@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Events.Contents
[EventType(nameof(ContentCreated))]
public sealed class ContentCreated : ContentEvent
{
public Status Status { get; set; }
public NamedContentData Data { get; set; }
}
}

2
src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Events.Contents
[EventType(nameof(ContentStatusChanged))]
public sealed class ContentStatusChanged : ContentEvent
{
public StatusChange? Change { get; set; }
public StatusChange Change { get; set; }
public Status Status { get; set; }
}

8
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -13,6 +13,14 @@ namespace Squidex.Infrastructure
{
public static class CollectionExtensions
{
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
{
foreach (var value in source)
{
target.Add(value);
}
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{
var random = new Random();

36
src/Squidex/.vscode/settings.json

@ -1,36 +0,0 @@
{
// When opening a file, `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.
"editor.detectIndentation": false,
// Typescript version from local package to be consistent
"typescript.tsdk": "node_modules/typescript/lib",
// Configure glob patterns for excluding files and folders.
"files.exclude": {
"**/node_modules": true,
"**/Assets": true,
"**/artifacts": true,
"**/build": true,
"**/logs": true,
"**/out": true,
"**/obj": true,
"**/bin": true,
"**/*.lock.json": true,
"**/*.bat": true,
"**/*.sln": true,
"**/*.sln.DotSettings": true,
"**/*.user": true,
"**/*.xproj": true,
"**/*.gitattributes": true,
"appsetttings.Development.json": true,
"appsetttings.Production.json": true,
".awcache": true,
".vs:": true,
".vscode:": true
},
"coverage-gutters.coverageFileNames": [
"_test-output/coverage/lcov.info"
],
"coverage-gutters.showLineCoverage": true
}

4
src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs

@ -14,6 +14,7 @@ using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.Processors;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Config.Swagger
@ -76,7 +77,8 @@ namespace Squidex.Areas.Api.Config.Swagger
}),
new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String),
new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String)
new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String),
new PrimitiveTypeMapper(typeof(Status), s => s.Type = JsonObjectType.String),
};
}
}

6
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -182,7 +182,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetCreatedResult>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.IsDuplicate);
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags, result.IsDuplicate);
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
}
@ -267,8 +267,8 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAssetEntity>();
var response = AssetDto.FromAsset(result, this, app);
var result = context.Result<AssetResult>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags);
return response;
}

7
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -118,10 +118,15 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[JsonProperty("_meta")]
public AssetMetadata Metadata { get; set; }
public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, bool isDuplicate = false)
public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, HashSet<string> tags = null, bool isDuplicate = false)
{
var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
if (tags != null)
{
response.Tags = tags;
}
if (isDuplicate)
{
response.Metadata = new AssetMetadata { IsDuplicate = "true" };

29
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
@ -26,15 +27,18 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
private readonly IOptions<MyContentsControllerOptions> controllerOptions;
private readonly IContentQueryService contentQuery;
private readonly IContentWorkflow contentWorkflow;
private readonly IGraphQLService graphQl;
public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery,
IContentWorkflow contentWorkflow,
IGraphQLService graphQl,
IOptions<MyContentsControllerOptions> controllerOptions)
: base(commandBus)
{
this.contentQuery = contentQuery;
this.contentWorkflow = contentWorkflow;
this.controllerOptions = controllerOptions;
this.graphQl = graphQl;
@ -123,8 +127,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
var context = Context();
var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids);
var contentsList = ResultList.Create<IContentEntity>(contents.Count, contents);
var response = ContentsDto.FromContents(contents.Count, contents, context, this, app, null);
var response = await ContentsDto.FromContentsAsync(contentsList, context, this, null, contentWorkflow);
if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys)
{
@ -159,7 +164,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context();
var contents = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));
var response = ContentsDto.FromContents(contents.Total, contents, context, this, app, name);
var schema = await contentQuery.GetSchemaOrThrowAsync(context, name);
var response = await ContentsDto.FromContentsAsync(contents, context, this, schema, contentWorkflow);
if (ShouldProvideSurrogateKeys(response))
{
@ -194,7 +201,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id);
var response = ContentDto.FromContent(content, context, this, app, name);
var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -230,7 +237,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = Context();
var content = await contentQuery.FindContentAsync(context, name, id, version);
var response = ContentDto.FromContent(content, context, this, app, name);
var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this);
if (controllerOptions.Value.EnableSurrogateKeys)
{
@ -264,7 +271,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
if (publish && !this.HasPermission(Helper.StatusPermission(app, name, Status.Published)))
{
@ -301,7 +308,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft };
@ -333,7 +340,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft };
@ -364,7 +371,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> PutContentStatus(string app, string name, Guid id, ChangeStatusDto request)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published)))
{
@ -399,7 +406,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> DiscardDraft(string app, string name, Guid id)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
var command = new DiscardChanges { ContentId = id };
@ -427,7 +434,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, Guid id)
{
await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name);
await contentQuery.GetSchemaOrThrowAsync(Context(), name);
var command = new DeleteContent { ContentId = id };
@ -441,7 +448,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IContentEntity>();
var response = ContentDto.FromContent(result, null, this, app, schema);
var response = await ContentDto.FromContentAsync(null, result, contentWorkflow, this);
return response;
}

2
src/Squidex/Areas/Api/Controllers/Contents/Helper.cs

@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
public static Permission StatusPermission(string app, string schema, Status status)
{
var id = Permissions.AppContentsStatus.Replace("{status}", status.ToString());
var id = Permissions.AppContentsStatus.Replace("{status}", status.Name);
return Permissions.ForApp(id, app, schema);
}

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

@ -7,6 +7,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
@ -79,7 +80,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary>
public long Version { get; set; }
public static ContentDto FromContent(IContentEntity content, QueryContext context, ApiController controller, string app, string schema)
public static ValueTask<ContentDto> FromContentAsync(
QueryContext context,
IContentEntity content,
IContentWorkflow contentWorkflow,
ApiController controller)
{
var response = SimpleMapper.Map(content, new ContentDto());
@ -99,10 +104,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto());
}
return response.CreateLinks(controller, app, schema);
return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name, contentWorkflow);
}
private ContentDto CreateLinks(ApiController controller, string app, string schema)
private async ValueTask<ContentDto> CreateLinksAsync(IContentEntity content,
ApiController controller,
string app,
string schema,
IContentWorkflow contentWorkflow)
{
var values = new { app, name = schema, id = Id };
@ -130,11 +139,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema))
{
AddPutLink("update", controller.Url<ContentsController>(x => nameof(x.PutContent), values));
if (await contentWorkflow.CanUpdateAsync(content))
{
AddPutLink("update", controller.Url<ContentsController>(x => nameof(x.PutContent), values));
}
if (Status == Status.Published)
{
AddPutLink("draft/propose", controller.Url<ContentsController>(x => nameof(x.PutContent), values) + "?asDraft=true");
AddPutLink("draft/propose", controller.Url((ContentsController x) => nameof(x.PutContent), values) + "?asDraft=true");
}
AddPatchLink("patch", controller.Url<ContentsController>(x => nameof(x.PatchContent), values));
@ -145,7 +157,9 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
AddDeleteLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values));
}
foreach (var next in StatusFlow.Next(Status))
var nextStatuses = await contentWorkflow.GetNextsAsync(content);
foreach (var next in nextStatuses)
{
if (controller.HasPermission(Helper.StatusPermission(app, schema, next)))
{

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

@ -5,12 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
@ -34,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// The possible statuses.
/// </summary>
[Required]
public string[] Statuses { get; set; }
public Status[] Statuses { get; set; }
public string ToEtag()
{
@ -46,20 +47,37 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
return Items.ToSurrogateKeys();
}
public static ContentsDto FromContents(long total, IEnumerable<IContentEntity> contents, QueryContext context,
public static async Task<ContentsDto> FromContentsAsync(IResultList<IContentEntity> contents, QueryContext context,
ApiController controller,
string app,
string schema)
ISchemaEntity schema,
IContentWorkflow contentWorkflow)
{
var result = new ContentsDto
{
Total = total,
Items = contents.Select(x => ContentDto.FromContent(x, context, controller, app, schema)).ToArray(),
Total = contents.Total,
Items = new ContentDto[contents.Count]
};
result.Statuses = new string[] { "Archived", "Draft", "Published" };
await Task.WhenAll(
result.AssignContentsAsync(contentWorkflow, contents, context, controller),
result.AssignStatusesAsync(contentWorkflow, schema));
return result.CreateLinks(controller, app, schema);
return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name);
}
private async Task AssignStatusesAsync(IContentWorkflow contentWorkflow, ISchemaEntity schema)
{
var allStatuses = await contentWorkflow.GetAllAsync(schema);
Statuses = allStatuses.ToArray();
}
private async Task AssignContentsAsync(IContentWorkflow contentWorkflow, IResultList<IContentEntity> contents, QueryContext context, ApiController controller)
{
for (var i = 0; i < Items.Length; i++)
{
Items[i] = await ContentDto.FromContentAsync(context, contents[i], contentWorkflow, controller);
}
}
private ContentsDto CreateLinks(ApiController controller, string app, string schema)

10
src/Squidex/Areas/Api/Controllers/Contents/Models/ScheduleJobDto.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
@ -25,13 +26,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
public Status Status { get; set; }
/// <summary>
/// The user who schedule the content.
/// The target date and time when the content should be scheduled.
/// </summary>
public RefToken ScheduledBy { get; set; }
public Instant DueTime { get; set; }
/// <summary>
/// The target date and time when the content should be scheduled.
/// The user who schedule the content.
/// </summary>
public Instant DueTime { get; set; }
[Required]
public RefToken ScheduledBy { get; set; }
}
}

5
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs

@ -80,7 +80,10 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutEvent), values));
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values));
if (NextAttempt.HasValue)
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values));
}
return this;
}

6
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -513,9 +513,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiCosts(1)]
public async Task<IActionResult> DeleteNestedField(string app, string name, long parentId, long id)
{
await CommandBus.PublishAsync(new DeleteField { ParentFieldId = parentId, FieldId = id });
var command = new DeleteField { ParentFieldId = parentId, FieldId = id };
return NoContent();
var response = await InvokeCommandAsync(app, command);
return Ok(response);
}
private async Task<SchemaDetailsDto> InvokeCommandAsync(string app, ICommand command)

27
src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml

@ -53,22 +53,22 @@
{
if (Model.IsLogin)
{
if (Model.IsFailed)
{
<div class="form-alert form-alert-error">Email or password not correct.</div>
}
if (Model.IsFailed)
{
<div class="form-alert form-alert-error">Email or password not correct.</div>
}
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model.ReturnUrl" method="post">
<div class="form-group">
<input type="email" class="form-control" name="email" id="email" placeholder="Enter Email" />
</div>
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model.ReturnUrl" method="post">
<div class="form-group">
<input type="email" class="form-control" name="email" id="email" placeholder="Enter Email" />
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" id="password" placeholder="Enter Password" />
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" id="password" placeholder="Enter Password" />
</div>
<button type="submit" class="btn btn-block btn-primary">@type</button>
</form>
<button type="submit" class="btn btn-block btn-primary">@type</button>
</form>
}
else
{
@ -95,7 +95,6 @@ else
var redirectButtons = document.getElementsByClassName("redirect-button");
if (redirectButtons.length === 1) {
debugger;
redirectButtons[0].click();
}
</script>

17
src/Squidex/Config/Domain/EntitiesServices.cs

@ -6,6 +6,8 @@
// ==========================================================================
using System;
using GraphQL;
using GraphQL.DataLoader;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -74,6 +76,18 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetUsageTracker>()
.As<IEventConsumer>().As<IAssetUsageTracker>();
services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t)))
.As<IDependencyResolver>();
services.AddSingletonAs<DataLoaderContextAccessor>()
.As<IDataLoaderContextAccessor>();
services.AddSingletonAs<DataLoaderDocumentListener>()
.AsSelf();
services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>();
services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>();
@ -101,6 +115,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
services.AddSingletonAs<DefaultContentWorkflow>()
.AsOptional<IContentWorkflow>();
services.AddSingletonAs<RolePermissionsProvider>()
.AsSelf();

2
src/Squidex/Config/Domain/SerializationServices.cs

@ -11,6 +11,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps.Json;
using Squidex.Domain.Apps.Core.Contents.Json;
using Squidex.Domain.Apps.Core.Rules.Json;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Schemas.Json;
@ -44,6 +45,7 @@ namespace Squidex.Config.Domain
new RolesConverter(),
new RuleConverter(),
new SchemaConverter(),
new StatusConverter(),
new StringEnumConverter());
settings.NullValueHandling = NullValueHandling.Ignore;

25
src/Squidex/app-config/helpers.js

@ -1,25 +0,0 @@
var path = require('path');
var appRoot = path.resolve(__dirname, '..');
exports.root = function () {
var newArgs = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [appRoot].concat(newArgs));
};
exports.removeLoaders = function (config, extensions) {
var rules = config.module.rules;
for (var i = 0; i < rules.length; i += 1) {
var rule = rules[i];
for (var j = 0; j < extensions.length; j += 1) {
if (rule.test.source.indexOf(extensions[j]) >= 0) {
rules.splice(i, 1);
i--;
break;
}
}
}
}

3
src/Squidex/app-config/karma-test-shim.js

@ -1,7 +1,6 @@
Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
require('core-js/proposals/reflect-metadata');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');

6
src/Squidex/app-config/karma.conf.js

@ -1,4 +1,4 @@
var webpackConfig = require('./webpack.test');
const webpackConfig = require('./webpack.config');
module.exports = function (config) {
var _config = {
@ -10,7 +10,7 @@ module.exports = function (config) {
frameworks: ['jasmine'],
/**
* Load additional test shim to setup angular2 for testing.
* Load additional test shim to setup angular for testing.
*/
files: [
{ pattern: './app-config/karma-test-shim.js', watched: false }
@ -23,7 +23,7 @@ module.exports = function (config) {
/**
* Load the files with webpack and use test configuration for it.
*/
webpack: webpackConfig,
webpack: webpackConfig({ target: 'tests', jit: true }),
webpackMiddleware: {
stats: 'errors-only'

6
src/Squidex/app-config/karma.coverage.conf.js

@ -1,4 +1,4 @@
var webpackConfig = require('./webpack.test.coverage');
const webpackConfig = require('./webpack.config');
module.exports = function (config) {
var _config = {
@ -10,7 +10,7 @@ module.exports = function (config) {
frameworks: ['jasmine'],
/**
* Load additional test shim to setup angular2 for testing.
* Load additional test shim to setup angular for testing.
*/
files: [
{ pattern: './app-config/karma-test-shim.js', watched: false }
@ -23,7 +23,7 @@ module.exports = function (config) {
/**
* Load the files with webpack and use test configuration for it.
*/
webpack: webpackConfig,
webpack: webpackConfig({ target: 'tests', coverage: true, jit: true }),
webpackMiddleware: {
stats: 'errors-only'

446
src/Squidex/app-config/webpack.config.js

@ -1,6 +1,13 @@
const webpack = require('webpack'),
path = require('path'),
helpers = require('./helpers');
path = require('path');
const appRoot = path.resolve(__dirname, '..');
function root() {
var newArgs = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [appRoot].concat(newArgs));
};
const plugins = {
// https://github.com/webpack-contrib/mini-css-extract-plugin
@ -8,147 +15,356 @@ const plugins = {
// https://github.com/dividab/tsconfig-paths-webpack-plugin
TsconfigPathsPlugin: require('tsconfig-paths-webpack-plugin'),
// https://github.com/aackerman/circular-dependency-plugin
CircularDependencyPlugin: require('circular-dependency-plugin')
CircularDependencyPlugin: require('circular-dependency-plugin'),
// https://github.com/jantimon/html-webpack-plugin
HtmlWebpackPlugin: require('html-webpack-plugin'),
// https://github.com/mishoo/UglifyJS2/tree/harmony
UglifyJsPlugin: require('uglifyjs-webpack-plugin'),
// https://www.npmjs.com/package/@ngtools/webpack
NgToolsWebpack: require('@ngtools/webpack'),
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
OptimizeCSSAssetsPlugin: require("optimize-css-assets-webpack-plugin"),
// https://github.com/jrparish/tslint-webpack-plugin
TsLintPlugin: require('tslint-webpack-plugin')
};
const isDevServer = path.basename(require.main.filename) === 'webpack-dev-server.js';
module.exports = function(env) {
const isDevServer = path.basename(require.main.filename) === 'webpack-dev-server.js';
const isProduction = env && env.production;
const isTesting = env && env.target === 'tests';
const isCoverage = env && env.coverage;
const isJit = env && env.jit;
const config = {
mode: isProduction ? 'production' : 'development',
module.exports = {
/**
* Options affecting the resolving of modules.
*
* See: https://webpack.js.org/configuration/resolve/
*/
resolve: {
/**
* An array of extensions that should be used to resolve modules.
* Source map for Karma from the help of karma-sourcemap-loader & karma-webpack.
*
* See: https://webpack.js.org/configuration/resolve/#resolve-extensions
* See: https://webpack.js.org/configuration/devtool/
*/
extensions: ['.js', '.mjs', '.ts', '.css', '.scss'],
modules: [
helpers.root('app'),
helpers.root('app', 'theme'),
helpers.root('node_modules')
],
devtool: isProduction ? undefined : (isTesting ? 'inline-source-map' : 'source-map'),
/**
* Options affecting the resolving of modules.
*
* See: https://webpack.js.org/configuration/resolve/
*/
resolve: {
/**
* An array of extensions that should be used to resolve modules.
*
* See: https://webpack.js.org/configuration/resolve/#resolve-extensions
*/
extensions: ['.js', '.mjs', '.ts', '.css', '.scss'],
modules: [
root('app'),
root('app', 'theme'),
root('node_modules')
],
plugins: [
new plugins.TsconfigPathsPlugin()
]
},
/**
* Options affecting the normal modules.
*
* See: https://webpack.js.org/configuration/module/
*/
module: {
/**
* An array of Rules which are matched to requests when modules are created.
*
* See: https://webpack.js.org/configuration/module/#module-rules
*/
rules: [{
test: /\.mjs$/,
type: "javascript/auto",
include: [/node_modules/]
}, {
test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/,
parser: { system: true },
include: [/node_modules/]
}, {
test: /\.js\.flow$/,
use: [{
loader: 'ignore-loader'
}],
include: [/node_modules/]
}, {
test: /\.html$/,
use: [{
loader: 'raw-loader'
}]
}, {
test: /\.(woff|woff2|ttf|eot)(\?.*$|$)/,
use: [{
loader: 'file-loader?name=[name].[hash].[ext]',
options: {
outputPath: 'assets',
/*
* Use custom public path as ./ is not supported by fonts.
*/
publicPath: isDevServer ? undefined : 'assets'
}
}]
}, {
test: /\.(png|jpe?g|gif|svg|ico)(\?.*$|$)/,
use: [{
loader: 'file-loader?name=[name].[hash].[ext]',
options: {
outputPath: 'assets'
}
}]
}, {
test: /\.css$/,
use: [
plugins.MiniCssExtractPlugin.loader,
{
loader: 'css-loader'
}]
}, {
test: /\.scss$/,
use: [{
loader: 'raw-loader'
}, {
loader: 'sass-loader', options: { includePaths: [root('app', 'theme')] }
}],
exclude: root('app', 'theme')
}]
},
plugins: [
new plugins.TsconfigPathsPlugin()
]
},
/**
* Options affecting the normal modules.
*
* See: https://webpack.js.org/configuration/module/
*/
module: {
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/, root('./app'), {}),
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
/**
* Puts each bundle into a file and appends the hash of the file to the path.
*
* See: https://github.com/webpack-contrib/mini-css-extract-plugin
*/
new plugins.MiniCssExtractPlugin('[name].css'),
new webpack.LoaderOptionsPlugin({
options: {
htmlLoader: {
/**
* Define the root for images, so that we can use absolute urls.
*
* See: https://github.com/webpack/html-loader#Advanced_Options
*/
root: root('app', 'images')
},
context: '/'
}
}),
/**
* Detect circular dependencies in app.
*
* See: https://github.com/aackerman/circular-dependency-plugin
*/
new plugins.CircularDependencyPlugin({
exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/,
// Add errors to webpack instead of warnings
failOnError: true
}),
],
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
},
historyApiFallback: true
}
};
if (!isTesting) {
/**
* An array of Rules which are matched to requests when modules are created.
* The entry point for the bundle. Our Angular app.
*
* See: https://webpack.js.org/configuration/module/#module-rules
* See: https://webpack.js.org/configuration/entry-context/
*/
rules: [{
test: /\.mjs$/,
type: "javascript/auto",
include: [/node_modules/]
}, {
test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/,
parser: { system: true },
include: [/node_modules/]
}, {
config.entry = {
'shims': './app/shims.ts',
'app': './app/app.ts'
};
if (isProduction) {
config.output = {
/**
* The output directory as absolute path (required).
*
* See: https://webpack.js.org/configuration/output/#output-path
*/
path: root('wwwroot/build/'),
publicPath: './build/',
/**
* Specifies the name of each output file on disk.
*
* See: https://webpack.js.org/configuration/output/#output-filename
*/
filename: '[name].js',
/**
* The filename of non-entry chunks as relative path inside the output.path directory.
*
* See: https://webpack.js.org/configuration/output/#output-chunkfilename
*/
chunkFilename: '[id].[hash].chunk.js'
};
} else {
config.output = {
filename: '[name].js',
/**
* Set the public path, because we are running the website from another port (5000).
*/
publicPath: 'http://localhost:3000/'
};
}
config.plugins.push(
new plugins.HtmlWebpackPlugin({
hash: true,
chunks: ['shims', 'app'],
chunksSortMode: 'manual',
template: 'wwwroot/index.html'
})
);
config.plugins.push(
new plugins.HtmlWebpackPlugin({
template: 'wwwroot/_theme.html', hash: true, chunksSortMode: 'none', filename: 'theme.html'
})
);
config.plugins.push(
new plugins.TsLintPlugin({
files: ['./app/**/*.ts'],
/**
* Path to a configuration file.
*/
config: root('tslint.json'),
/**
* Wait for linting and fail the build when linting error occur.
*/
waitForLinting: isProduction
})
);
}
if (isProduction) {
config.optimization = {
minimizer: [
new plugins.UglifyJsPlugin({
uglifyOptions: {
compress: false,
ecma: 6,
mangle: true,
output: {
comments: false
}
},
extractComments: true
}),
new plugins.OptimizeCSSAssetsPlugin({})
]
};
config.performance = {
hints: false
};
}
if (!isCoverage) {
config.module.rules.push({
test: /\.ts$/,
use: [{
loader: 'awesome-typescript-loader'
}, {
loader: 'angular-router-loader'
}, {
loader: 'angular2-template-loader'
}],
exclude: [/node_modules/]
}, {
})
} else {
config.module.rules.push({
test: /\.ts$/,
use: [{
loader: 'awesome-typescript-loader'
loader: 'ts-loader'
}],
include: [/node_modules/]
}, {
test: /\.js\.flow$/,
include: [/\.(e2e|spec)\.ts$/],
});
// Use instrument loader for all normal builds.
config.module.rules.push({
test: /\.ts$/,
use: [{
loader: 'ignore-loader'
loader: 'istanbul-instrumenter-loader'
}, {
loader: 'ts-loader'
}],
include: [/node_modules/]
}, {
test: /\.html$/,
use: [{
loader: 'raw-loader'
}]
}, {
test: /\.(woff|woff2|ttf|eot)(\?.*$|$)/,
use: [{
loader: 'file-loader?name=[name].[hash].[ext]',
options: {
outputPath: 'assets',
/*
* Use custom public path as ./ is not supported by fonts.
*/
publicPath: isDevServer ? undefined : 'assets'
}
}]
}, {
test: /\.(png|jpe?g|gif|svg|ico)(\?.*$|$)/,
use: [{
loader: 'file-loader?name=[name].[hash].[ext]',
options: {
outputPath: 'assets'
}
}]
}, {
test: /\.css$/,
exclude: [/\.(e2e|spec)\.ts$/]
});
}
if (isProduction) {
config.module.rules.push({
test: /\.scss$/,
/*
* Extract the content from a bundle to a file.
*
* See: https://github.com/webpack-contrib/extract-text-webpack-plugin
*/
use: [
plugins.MiniCssExtractPlugin.loader,
{
loader: 'css-loader'
}]
}, {
}, {
loader: 'sass-loader'
}],
/*
* Do not include component styles.
*/
include: root('app', 'theme'),
});
} else {
config.module.rules.push({
test: /\.scss$/,
use: [{
loader: 'raw-loader'
loader: 'style-loader'
}, {
loader: 'sass-loader', options: { includePaths: [helpers.root('app', 'theme')] }
loader: 'css-loader'
}, {
loader: 'sass-loader?sourceMap'
}],
exclude: helpers.root('app', 'theme')
}]
},
/*
* Do not include component styles.
*/
include: root('app', 'theme')
});
}
plugins: [
/**
* Puts each bundle into a file and appends the hash of the file to the path.
*
* See: https://github.com/webpack-contrib/mini-css-extract-plugin
*/
new plugins.MiniCssExtractPlugin('[name].css'),
new webpack.LoaderOptionsPlugin({
options: {
htmlLoader: {
/**
* Define the root for images, so that we can use absolute urls.
*
* See: https://github.com/webpack/html-loader#Advanced_Options
*/
root: helpers.root('app', 'images')
},
context: '/'
}
}),
new plugins.CircularDependencyPlugin({
exclude: /([\\\/]node_modules[\\\/])|(ngfactory\.js$)/,
// Add errors to webpack instead of warnings
failOnError: true
}),
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/)
]
if (!isJit) {
config.module.rules.push({
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
use: [{
loader: '@ngtools/webpack'
}]
});
config.plugins.push(
new plugins.NgToolsWebpack.AngularCompilerPlugin({
entryModule: 'app/app.module#AppModule',
sourceMap: !isProduction,
skipSourceGeneration: false,
tsConfigPath: './tsconfig.json'
})
);
}
return config;
};

35
src/Squidex/app-config/webpack.run.base.js

@ -1,35 +0,0 @@
const webpack = require('webpack'),
webpackMerge = require('webpack-merge'),
path = require('path'),
helpers = require('./helpers'),
commonConfig = require('./webpack.config.js');
const plugins = {
// https://github.com/jantimon/html-webpack-plugin
HtmlWebpackPlugin: require('html-webpack-plugin')
};
module.exports = webpackMerge(commonConfig, {
/**
* The entry point for the bundle. Our Angular app.
*
* See: https://webpack.js.org/configuration/entry-context/
*/
entry: {
'shims': './app/shims.ts',
'app': './app/app.ts'
},
plugins: [
new plugins.HtmlWebpackPlugin({
hash: true,
chunks: ['shims', 'app'],
chunksSortMode: 'manual',
template: 'wwwroot/index.html'
}),
new plugins.HtmlWebpackPlugin({
template: 'wwwroot/_theme.html', hash: true, chunksSortMode: 'none', filename: 'theme.html'
})
]
});

68
src/Squidex/app-config/webpack.run.dev.js

@ -1,68 +0,0 @@
const webpack = require('webpack'),
webpackMerge = require('webpack-merge'),
path = require('path'),
helpers = require('./helpers'),
runConfig = require('./webpack.run.base.js');
const plugins = {
// https://github.com/jrparish/tslint-webpack-plugin
TsLintPlugin: require('tslint-webpack-plugin')
};
module.exports = webpackMerge(runConfig, {
mode: 'development',
devtool: 'source-map',
output: {
filename: '[name].js',
/**
* Set the public path, because we are running the website from another port (5000).
*/
publicPath: 'http://localhost:3000/'
},
/*
* Options affecting the normal modules.
*
* See: https://webpack.js.org/configuration/module/
*/
module: {
/**
* An array of Rules which are matched to requests when modules are created.
*
* See: https://webpack.js.org/configuration/module/#module-rules
*/
rules: [{
test: /\.scss$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'sass-loader?sourceMap', options: { includePaths: [helpers.root('app', 'theme')] }
}],
include: helpers.root('app', 'theme')
}]
},
plugins: [
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)fesm5/, helpers.root('./src'), {}),
new plugins.TsLintPlugin({
files: ['./app/**/*.ts'],
/**
* Path to a configuration file.
*/
config: helpers.root('tslint.json')
})
],
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
},
historyApiFallback: true
}
});

137
src/Squidex/app-config/webpack.run.prod.js

@ -1,137 +0,0 @@
const webpack = require('webpack'),
webpackMerge = require('webpack-merge'),
path = require('path'),
helpers = require('./helpers'),
runConfig = require('./webpack.run.base.js');
const plugins = {
// https://github.com/mishoo/UglifyJS2/tree/harmony
UglifyJsPlugin: require('uglifyjs-webpack-plugin'),
// https://www.npmjs.com/package/@ngtools/webpack
NgToolsWebpack: require('@ngtools/webpack'),
// https://github.com/webpack-contrib/mini-css-extract-plugin
MiniCssExtractPlugin: require('mini-css-extract-plugin'),
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
OptimizeCSSAssetsPlugin: require("optimize-css-assets-webpack-plugin"),
// https://github.com/jrparish/tslint-webpack-plugin
TsLintPlugin: require('tslint-webpack-plugin')
};
helpers.removeLoaders(runConfig, ['scss', 'ts']);
module.exports = webpackMerge(runConfig, {
mode: 'production',
output: {
/**
* The output directory as absolute path (required).
*
* See: https://webpack.js.org/configuration/output/#output-path
*/
path: helpers.root('wwwroot/build/'),
publicPath: './build/',
/**
* Specifies the name of each output file on disk.
*
* See: https://webpack.js.org/configuration/output/#output-filename
*/
filename: '[name].js',
/**
* The filename of non-entry chunks as relative path inside the output.path directory.
*
* See: https://webpack.js.org/configuration/output/#output-chunkfilename
*/
chunkFilename: '[id].[hash].chunk.js'
},
/*
* Options affecting the normal modules.
*
* See: https://webpack.js.org/configuration/module/
*/
module: {
/**
* An array of Rules which are matched to requests when modules are created.
*
* See: https://webpack.js.org/configuration/module/#module-rules
*/
rules: [{
test: /\.scss$/,
/*
* Extract the content from a bundle to a file.
*
* See: https://github.com/webpack-contrib/extract-text-webpack-plugin
*/
use: [
plugins.MiniCssExtractPlugin.loader,
{
loader: 'css-loader'
}, {
loader: 'sass-loader'
}],
/*
* Do not include component styles.
*/
include: helpers.root('app', 'theme'),
}, {
test: /\.scss$/,
use: [{
loader: 'raw-loader'
}, {
loader: 'sass-loader', options: { includePaths: [helpers.root('app', 'theme')] }
}],
exclude: helpers.root('app', 'theme'),
}, {
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
use: [{
loader: '@ngtools/webpack'
}]
}]
},
plugins: [
new plugins.NgToolsWebpack.AngularCompilerPlugin({
entryModule: 'app/app.module#AppModule',
sourceMap: false,
skipSourceGeneration: false,
tsConfigPath: './tsconfig.json'
}),
new plugins.TsLintPlugin({
files: ['./app/**/*.ts'],
/**
* Path to a configuration file.
*/
config: helpers.root('tslint.json'),
/**
* Wait for linting and fail the build when linting error occur.
*/
waitForLinting: true
})
],
optimization: {
minimizer: [
new plugins.UglifyJsPlugin({
uglifyOptions: {
compress: false,
ecma: 6,
mangle: true,
output: {
comments: false
}
},
extractComments: true
}),
new plugins.OptimizeCSSAssetsPlugin({})
]
},
performance: {
hints: false
}
});

37
src/Squidex/app-config/webpack.test.coverage.js

@ -1,37 +0,0 @@
const webpack = require('webpack'),
webpackMerge = require('webpack-merge'),
path = require('path'),
helpers = require('./helpers'),
testConfig = require('./webpack.test.js');
helpers.removeLoaders(testConfig, ['ts']);
module.exports = webpackMerge(testConfig, {
module: {
/**
* An array of Rules which are matched to requests when modules are created.
*
* See: https://webpack.js.org/configuration/module/#module-rules
*/
rules: [{
test: /\.ts$/,
use: [{
loader: 'ts-loader'
}],
include: [/\.(e2e|spec)\.ts$/],
}, {
test: /\.ts$/,
use: [{
loader: 'istanbul-instrumenter-loader'
}, {
loader: 'ts-loader'
}, {
loader: 'angular-router-loader'
}, {
loader: 'angular2-template-loader'
}],
exclude: [/\.(e2e|spec)\.ts$/]
}]
}
});

16
src/Squidex/app-config/webpack.test.js

@ -1,16 +0,0 @@
const webpack = require('webpack'),
webpackMerge = require('webpack-merge'),
path = require('path'),
helpers = require('./helpers'),
commonConfig = require('./webpack.config.js');
module.exports = webpackMerge(commonConfig, {
mode: 'development',
/**
* Source map for Karma from the help of karma-sourcemap-loader & karma-webpack.
*
* See: https://webpack.js.org/configuration/devtool/
*/
devtool: 'inline-source-map'
});

18
src/Squidex/app/app.routes.ts

@ -38,12 +38,12 @@ export const routes: Routes = [
children: [
{
path: '',
loadChildren: './features/apps/module#SqxFeatureAppsModule',
loadChildren: () => import('./features/apps/module').then(m => m.SqxFeatureAppsModule),
canActivate: [UnsetAppGuard]
},
{
path: 'administration',
loadChildren: './features/administration/module#SqxFeatureAdministrationModule',
loadChildren: () => import('./features/administration/module').then(m => m.SqxFeatureAdministrationModule),
canActivate: [UnsetAppGuard]
},
{
@ -53,31 +53,31 @@ export const routes: Routes = [
children: [
{
path: '',
loadChildren: './features/dashboard/module#SqxFeatureDashboardModule'
loadChildren: () => import('./features/dashboard/module').then(m => m.SqxFeatureDashboardModule)
},
{
path: 'content',
loadChildren: './features/content/module#SqxFeatureContentModule'
loadChildren: () => import('./features/content/module').then(m => m.SqxFeatureContentModule)
},
{
path: 'schemas',
loadChildren: './features/schemas/module#SqxFeatureSchemasModule'
loadChildren: () => import('./features/schemas/module').then(m => m.SqxFeatureSchemasModule)
},
{
path: 'assets',
loadChildren: './features/assets/module#SqxFeatureAssetsModule'
loadChildren: () => import('./features/assets/module').then(m => m.SqxFeatureAssetsModule)
},
{
path: 'rules',
loadChildren: './features/rules/module#SqxFeatureRulesModule'
loadChildren: () => import('./features/rules/module').then(m => m.SqxFeatureRulesModule)
},
{
path: 'settings',
loadChildren: './features/settings/module#SqxFeatureSettingsModule'
loadChildren: () => import('./features/settings/module').then(m => m.SqxFeatureSettingsModule)
},
{
path: 'api',
loadChildren: './features/api/module#SqxFeatureApiModule'
loadChildren: () => import('./features/api/module').then(m => m.SqxFeatureApiModule)
}
]
}

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

@ -22,7 +22,7 @@ import { AppsState, GraphQlService } from '@app/shared';
templateUrl: './graphql-page.component.html'
})
export class GraphQLPageComponent implements AfterViewInit {
@ViewChild('graphiQLContainer')
@ViewChild('graphiQLContainer', { static: false })
public graphiQLContainer: ElementRef;
constructor(

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

@ -66,7 +66,7 @@
}
&:hover {
@include box-shadow(0, 3px, 16px, .2px);
@include box-shadow(0, 3px, 16px, .2);
}
&:focus {

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

@ -59,7 +59,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>;
@ViewChild('dueTimeSelector')
@ViewChild('dueTimeSelector', { static: false })
public dueTimeSelector: DueTimeSelectorComponent;
constructor(apiUrl: ApiUrlConfig, authService: AuthService,

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

@ -52,7 +52,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public isAllSelected = false;
@ViewChild('dueTimeSelector')
@ViewChild('dueTimeSelector', { static: false })
public dueTimeSelector: DueTimeSelectorComponent;
constructor(

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

@ -18,10 +18,8 @@
<ng-container content>
<ng-container *ngIf="schemasState.publishedSchemas | async; let schemas">
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[name]="category"
[schemas]="schemas"
[schemaCategory]="category"
[schemasFilter]="schemasFilter.valueChanges | async"
[routeSingletonToContent]="true"
[forContent]="true">
</sqx-schema-category>
</ng-container>

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

@ -8,7 +8,11 @@
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { AppsState, SchemasState } from '@app/shared';
import {
AppsState,
SchemaCategory,
SchemasState
} from '@app/shared';
@Component({
selector: 'sqx-schemas-page',
@ -28,8 +32,8 @@ export class SchemasPageComponent implements OnInit {
this.schemasState.load();
}
public trackByCategory(index: number, category: string) {
return category;
public trackByCategory(index: number, category: SchemaCategory) {
return category.name;
}
}

8
src/Squidex/app/features/rules/pages/rules/rule-element.component.scss

@ -82,7 +82,9 @@
display: inline-block;
}
/deep/ svg {
fill: $color-dark-foreground;
display: block;
::ng-deep {
svg {
fill: $color-dark-foreground;
display: block;
}
}

8
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -22,13 +22,13 @@
</td>
<td class="text-center">
<input type="text" class="form-control code" placeholder="Optional condition as javascript expression"
[disabled]="!isEditable"
[disabled]="triggerForm.disabled"
[ngModelOptions]="{ updateOn: 'blur' }"
[ngModel]="triggerSchema.condition"
(ngModelChange)="updateCondition(triggerSchema.schema, $event)" />
</td>
<td class="text-center">
<button type="button" class="btn btn-text-secondary" (click)="removeSchema(triggerSchema)" [disabled]="!isEditable">
<button type="button" class="btn btn-text-secondary" (click)="removeSchema(triggerSchema)" [disabled]="triggerForm.disabled">
<i class="icon-close"></i>
</button>
</td>
@ -38,12 +38,12 @@
<div class="section" *ngIf="schemasToAdd.length > 0">
<form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1">
<select class="form-control schemas-control" [disabled]="!isEditable" [(ngModel)]="schemaToAdd" name="schema">
<select class="form-control schemas-control" [disabled]="triggerForm.disabled" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd; trackBy: trackBySchema" [ngValue]="schema">{{schema.displayName}}</option>
</select>
</div>
<button type="submit" class="btn btn-success" [disabled]="!isEditable">Add Schema</button>
<button type="submit" class="btn btn-success" [disabled]="triggerForm.disabled">Add Schema</button>
</form>
</div>

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

@ -29,9 +29,6 @@ export class ContentChangedTriggerComponent implements OnInit {
@Input()
public schemas: ImmutableArray<SchemaDto>;
@Input()
public isEditable: boolean;
@Input()
public trigger: any;

1
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -65,6 +65,7 @@
<ng-template #notEditing>
<form [formGroup]="editForm.form" class="edit-form" (ngSubmit)="save()">
<sqx-field-form
[isEditable]="true"
[patterns]="patternsState.patterns | async"
[editForm]="editForm.form"
[editFormSubmitted]="editForm.submitted | async"

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

@ -29,7 +29,7 @@ const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createP
templateUrl: './field-wizard.component.html'
})
export class FieldWizardComponent implements OnInit {
@ViewChild('nameInput')
@ViewChild('nameInput', { static: false })
public nameInput: ElementRef<HTMLElement>;
@Input()

5
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html

@ -27,10 +27,9 @@
<ng-container content>
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[name]="category"
[schemas]="schemasState.schemas | async"
[schemaCategory]="category"
[schemasFilter]="schemasFilter.valueChanges | async"
(remove)="removeCategory(category)">
(remove)="removeCategory(category.name)">
</sqx-schema-category>
<form [formGroup]="addCategoryForm.form" (ngSubmit)="addCategory()">

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

@ -16,6 +16,7 @@ import {
DialogModel,
MessageBus,
ResourceOwner,
SchemaCategory,
SchemaDto,
SchemasState
} from '@app/shared';
@ -94,8 +95,8 @@ export class SchemasPageComponent extends ResourceOwner implements OnInit {
this.addSchemaDialog.show();
}
public trackByCategory(index: number, category: string) {
return category;
public trackByCategory(index: number, category: SchemaCategory) {
return category.name;
}
}

2
src/Squidex/app/features/settings/pages/roles/role.component.ts

@ -33,7 +33,7 @@ export class RoleComponent implements OnChanges {
@Input()
public allPermissions: AutocompleteSource;
@ViewChild('addInput')
@ViewChild('addInput', { static: false })
public addPermissionInput: AutocompleteComponent;
public isEditing = false;

4
src/Squidex/app/framework/angular/forms/autocomplete.component.ts

@ -52,10 +52,10 @@ export class AutocompleteComponent extends StatefulControlComponent<State, any[]
@Input()
public placeholder = '';
@ContentChild(TemplateRef)
@ContentChild(TemplateRef, { static: false })
public itemTemplate: TemplateRef<any>;
@ViewChild('input')
@ViewChild('input', { static: false })
public inputControl: ElementRef<HTMLInputElement>;
public queryInput = new FormControl();

2
src/Squidex/app/framework/angular/forms/code-editor.component.ts

@ -35,7 +35,7 @@ export class CodeEditorComponent extends ExternalControlComponent<string> implem
private value: string;
private isDisabled = false;
@ViewChild('editor')
@ViewChild('editor', { static: false })
public editor: ElementRef;
@Input()

2
src/Squidex/app/framework/angular/forms/date-time-editor.component.ts

@ -41,7 +41,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
@Input()
public hideClear: boolean;
@ViewChild('dateInput')
@ViewChild('dateInput', { static: false })
public dateInput: ElementRef;
public timeControl = new FormControl();

2
src/Squidex/app/framework/angular/forms/iframe-editor.component.ts

@ -26,7 +26,7 @@ export class IFrameEditorComponent extends ExternalControlComponent<any> impleme
private isDisabled = false;
private isInitialized = false;
@ViewChild('iframe')
@ViewChild('iframe', { static: false })
public iframe: ElementRef<HTMLIFrameElement>;
@Input()

2
src/Squidex/app/framework/angular/forms/json-editor.component.ts

@ -32,7 +32,7 @@ export class JsonEditorComponent extends ExternalControlComponent<string> implem
private valueString: string;
private isDisabled = false;
@ViewChild('editor')
@ViewChild('editor', { static: false })
public editor: ElementRef<HTMLDivElement>;
constructor(changeDetector: ChangeDetectorRef,

4
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -93,10 +93,10 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagEditorComponent extends StatefulControlComponent<State, any[]> implements AfterViewInit, OnInit {
@ViewChild('form')
@ViewChild('form', { static: false })
public formElement: ElementRef<HTMLElement>;
@ViewChild('input')
@ViewChild('input', { static: false })
public inputElement: ElementRef<HTMLInputElement>;
@Input()

4
src/Squidex/app/framework/angular/modals/modal-dialog.component.ts

@ -51,10 +51,10 @@ export class ModalDialogComponent extends StatefulComponent<State> implements Af
@Output()
public close = new EventEmitter();
@ViewChild('tabsElement')
@ViewChild('tabsElement', { static: false })
public tabsElement: ElementRef<ParentNode>;
@ViewChild('footerElement')
@ViewChild('footerElement', { static: false })
public footerElement: ElementRef<ParentNode>;
constructor(changeDetector: ChangeDetectorRef) {

2
src/Squidex/app/framework/angular/modals/root-view.component.ts

@ -14,6 +14,6 @@ import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RootViewComponent {
@ViewChild('element', { read: ViewContainerRef })
@ViewChild('element', { read: ViewContainerRef, static: false })
public viewContainer: ViewContainerRef;
}

2
src/Squidex/app/framework/angular/panel.component.ts

@ -64,7 +64,7 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
@Input()
public sidebarClass = '';
@ViewChild('panel')
@ViewChild('panel', { static: false })
public panel: ElementRef<HTMLElement>;
constructor(

4
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -64,10 +64,10 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
]
});
@ViewChild('editor')
@ViewChild('editor', { static: false })
public editor: ElementRef<HTMLElement>;
@ViewChild('searchBox')
@ViewChild('searchBox', { static: false })
public searchBoxInput: ElementRef<HTMLInputElement>;
constructor(changeDetector: ChangeDetectorRef,

6
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -40,13 +40,13 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
private value: string;
private isDisabled = false;
@ViewChild('editor')
@ViewChild('editor', { static: false })
public editor: ElementRef;
@ViewChild('container')
@ViewChild('container', { static: false })
public container: ElementRef;
@ViewChild('inner')
@ViewChild('inner', { static: false })
public inner: ElementRef;
public assetsDialog = new DialogModel();

2
src/Squidex/app/shared/components/rich-editor.component.ts

@ -46,7 +46,7 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
private value: string;
private isDisabled = false;
@ViewChild('editor')
@ViewChild('editor', { static: false })
public editor: ElementRef;
@Output()

24
src/Squidex/app/shared/components/schema-category.component.html

@ -1,4 +1,4 @@
<div *ngIf="!forContent || snapshot.schemasFiltered.length > 0" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div *ngIf="!forContent || snapshot.filtered.length > 0" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div class="drop-indicator"></div>
<div class="header clearfix">
@ -6,18 +6,18 @@
<i [class.icon-caret-right]="!snapshot.isOpen" [class.icon-caret-down]="snapshot.isOpen"></i>
</button>
<h3>{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})</h3>
<h3>{{schemaCategory.name}} ({{snapshot.filtered.length}})</h3>
<button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="snapshot.schemasForCategory.length === 0 && !forContent" (click)="emitRemove()">
<button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="schemaCategory.schemas.length === 0 && !forContent" (click)="emitRemove()">
<i class="icon-bin2"></i>
</button>
</div>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade>
<ng-container *ngFor="let schema of snapshot.schemasFiltered; trackBy: trackBySchema">
<li class="nav-item" dnd-draggable [dragEnabled]="!forContent && schema.canUpdateCategory" [dragData]="schema">
<ng-container *ngIf="!forContent; else simpleMode">
<li *ngFor="let schema of snapshot.filtered; trackBy: trackBySchema" class="nav-item" dnd-draggable [dragEnabled]="schema.canUpdateCategory" [dragData]="schema">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<div class="row" *ngIf="!forContent; else simpleMode">
<div class="row">
<div class="col-4">
<span class="schema-name schema-name-accent">{{schema.displayName}}</span>
</div>
@ -32,12 +32,16 @@
<span class="item-published" [class.unpublished]="!schema.isPublished"></span>
</div>
</div>
<ng-template #simpleMode>
<span class="schema-name" *ngIf="forContent">{{schema.displayName}}</span>
</ng-template>
</a>
</li>
</ng-container>
<ng-template #simpleMode>
<li *ngFor="let schema of snapshot.filtered; trackBy: trackBySchema" class="nav-item">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<span class="schema-name" *ngIf="forContent">{{schema.displayName}}</span>
</a>
</li>
</ng-template>
</ul>
</div>

68
src/Squidex/app/shared/components/schema-category.component.ts

@ -10,7 +10,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, In
import {
fadeAnimation,
ImmutableArray,
isSameCategory,
LocalStoreService,
SchemaCategory,
SchemaDetailsDto,
SchemaDto,
SchemasState,
@ -19,12 +21,9 @@ import {
} from '@app/shared/internal';
interface State {
displayName?: string;
filtered: ImmutableArray<SchemaDto>;
schemasFiltered: ImmutableArray<SchemaDto>;
schemasForCategory: ImmutableArray<SchemaDto>;
isOpen: boolean;
isOpen?: boolean;
}
@Component({
@ -38,36 +37,26 @@ interface State {
})
export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges {
@Input()
public name: string;
@Input()
public forContent: boolean;
@Input()
public routeSingletonToContent = false;
public schemaCategory: SchemaCategory;
@Input()
public schemasFilter: string;
@Input()
public schemas: ImmutableArray<SchemaDto>;
public forContent: boolean;
@Output()
public remove = new EventEmitter();
public allowDrop = (schema: any) => {
return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !this.isSameCategory(schema);
return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !isSameCategory(this.schemaCategory.name, schema);
}
constructor(changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState
) {
super(changeDetector, {
schemasFiltered: ImmutableArray.empty(),
schemasForCategory: ImmutableArray.empty(),
isOpen: true
});
super(changeDetector, { filtered: ImmutableArray.empty(), isOpen: true });
}
public ngOnInit() {
@ -81,52 +70,37 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes['schemas'] || changes['schemasFilter']) {
const isSameCategory = (schema: SchemaDto) => {
return (!this.name && !schema.category) || schema.category === this.name;
};
if (changes['schemaCategory'] || changes['schemasFilter']) {
let filtered = this.schemaCategory.schemas;
const query = this.schemasFilter;
const schemasForCategory = this.schemas.filter(x => isSameCategory(x));
const schemasFiltered = schemasForCategory.filter(x => !query || x.name.indexOf(query) >= 0);
if (this.forContent) {
filtered = filtered.filter(x => x.canReadContents);
}
let isOpen = false;
if (query) {
if (this.schemasFilter) {
filtered = filtered.filter(x => x.name.indexOf(this.schemasFilter) >= 0);
isOpen = true;
} else {
isOpen = this.localStore.get(`schema-category.${this.name}`) !== 'false';
}
this.next(s => ({ ...s, isOpen, schemasFiltered, schemasForCategory }));
}
if (changes['name']) {
let displayName = 'Schemas';
if (this.name && this.name.length > 0) {
displayName = this.name;
isOpen = this.localStore.get(`schema-category.${this.schemaCategory.name}`) !== 'false';
}
this.next(s => ({ ...s, displayName }));
this.next(s => ({ ...s, isOpen, filtered }));
}
}
public schemaRoute(schema: SchemaDto) {
if (schema.isSingleton && this.routeSingletonToContent) {
if (schema.isSingleton && this.forContent) {
return [schema.name, schema.id];
} else {
return [schema.name];
}
}
private isSameCategory(schema: SchemaDto): boolean {
return ((!this.name && !schema.category) || schema.category === this.name) && (!this.forContent || schema.canReadContents);
}
public changeCategory(schema: SchemaDto) {
this.schemasState.changeCategory(schema, this.name);
this.schemasState.changeCategory(schema, this.schemaCategory.name);
}
public emitRemove() {
@ -138,6 +112,6 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
}
private configKey(): string {
return `squidex.schema.category.${this.name}.closed`;
return `squidex.schema.category.${this.schemaCategory.name}.closed`;
}
}

2
src/Squidex/app/shared/services/assets.service.ts

@ -242,7 +242,7 @@ export class AssetsService {
tap(() => {
this.analytics.trackEvent('Analytics', 'Updated', appName);
}),
pretifyError('Failed to delete asset. Please reload.'));
pretifyError('Failed to update asset. Please reload.'));
}
public deleteAsset(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> {

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

Loading…
Cancel
Save