Browse Source

Merge branch 'master' into orleans3

# Conflicts:
#	src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
#	src/Squidex.Domain.Apps.Entities/AppProvider.cs
#	src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
#	src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
#	src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
#	src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
#	src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs
#	src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
#	src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
#	src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs
#	src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
#	src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
#	src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
#	src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
#	src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs
#	src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs
#	src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
#	src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs
#	src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs
#	src/Squidex.Infrastructure/States/StateFactory.cs
#	src/Squidex/Config/Domain/WriteServices.cs
#	src/Squidex/Squidex.csproj
#	tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
#	tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
#	tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
#	tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs
#	tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs
#	tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs
#	tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs
#	tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs
#	tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs
#	tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs
#	tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs
#	tools/Migrate_01/Migrations/AddPatterns.cs
#	tools/Migrate_01/Rebuilder.cs
pull/249/head
Sebastian Stehle 8 years ago
parent
commit
62f60e3414
  1. 10
      CHANGELOG.md
  2. 26
      Dockerfile
  3. 25
      libs/Dockerfile
  4. 0
      libs/keep
  5. 5
      src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  6. 31
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/ElasticSearchAction.cs
  7. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
  8. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  9. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  10. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  11. 22
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
  12. 180
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs
  13. 4
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs
  14. 4
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
  15. 1
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  16. 19
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  17. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  18. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  19. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
  20. 3
      src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs
  21. 60
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  22. 603
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs
  23. 17
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  24. 25
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  25. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
  26. 22
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs
  27. 21
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  28. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs
  29. 14
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs
  30. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  31. 11
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  32. 8
      src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs
  33. 25
      src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs
  34. 5
      src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs
  35. 41
      src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  36. 2
      src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs
  37. 34
      src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
  38. 10
      src/Squidex.Infrastructure/Orleans/Bootstrap.cs
  39. 27
      src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs
  40. 26
      src/Squidex.Infrastructure/Orleans/GrainOfString.cs
  41. 37
      src/Squidex.Infrastructure/Reflection/SimpleMapper.cs
  42. 2
      src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs
  43. 53
      src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/ElasticSearchActionDto.cs
  44. 5
      src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs
  45. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs
  46. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs
  47. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs
  48. 13
      src/Squidex/Config/Domain/ReadServices.cs
  49. 12
      src/Squidex/Config/Domain/WriteServices.cs
  50. 6
      src/Squidex/Config/Orleans/SiloWrapper.cs
  51. 23
      src/Squidex/app-config/webpack.run.prod.js
  52. 15
      src/Squidex/app/features/apps/pages/apps-page.component.html
  53. 188
      src/Squidex/app/features/content/pages/content/content-field.component.html
  54. 51
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  55. 2
      src/Squidex/app/features/content/pages/content/content-history.component.scss
  56. 74
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  57. 9
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  58. 23
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  59. 2
      src/Squidex/app/features/content/shared/assets-editor.component.html
  60. 2
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  61. 79
      src/Squidex/app/features/content/shared/content-item.component.html
  62. 4
      src/Squidex/app/features/content/shared/content-item.component.scss
  63. 106
      src/Squidex/app/features/content/shared/content-item.component.ts
  64. 2
      src/Squidex/app/features/content/shared/references-editor.component.html
  65. 2
      src/Squidex/app/features/content/shared/references-editor.component.scss
  66. 3
      src/Squidex/app/features/content/shared/references-editor.component.ts
  67. 2
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss
  68. 1
      src/Squidex/app/features/rules/declarations.ts
  69. 2
      src/Squidex/app/features/rules/module.ts
  70. 2
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss
  71. 2
      src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html
  72. 18
      src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts
  73. 2
      src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html
  74. 14
      src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts
  75. 73
      src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html
  76. 2
      src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.scss
  77. 70
      src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts
  78. 2
      src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html
  79. 12
      src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts
  80. 6
      src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts
  81. 6
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts
  82. 6
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  83. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  84. 4
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss
  85. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  86. 8
      src/Squidex/app/features/schemas/pages/schema/field.component.scss
  87. 19
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  88. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  89. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss
  90. 7
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  91. 10
      src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html
  92. 8
      src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts
  93. 13
      src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html
  94. 5
      src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts
  95. 5
      src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts
  96. 8
      src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts
  97. 2
      src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts
  98. 10
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html
  99. 34
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts
  100. 6
      src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts

10
CHANGELOG.md

@ -1,5 +1,15 @@
# Changelog
## v1.3.0 - 2018-02-17
### Features
* **Actions**: ElasticSearch action
### Refactorings
* **DomainObjects**: Refactored domain objects to be ready for Orleans.
## v1.2.0 - 2018-02-10
### Features

26
Dockerfile

@ -1,31 +1,7 @@
#
# Stage 1, Prebuild
#
FROM microsoft/aspnetcore-build:2.0.3-jessie as builder
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& mkdir /srv/var \
&& mkdir /tmp/phantomjs \
# Download Phantom JS
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
# Copy binaries only
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
# Create symbol link
# Clean up
&& apt-get autoremove -y \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version
FROM squidex/aspnetcore-build-phantomjs:2.0.3-jessie as builder
COPY src/Squidex/package.json /tmp/package.json

25
libs/Dockerfile

@ -0,0 +1,25 @@
FROM microsoft/aspnetcore-build:2.0.3-jessie
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& mkdir /srv/var \
&& mkdir /tmp/phantomjs \
# Download Phantom JS
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
# Copy binaries only
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
# Create symbol link
# Clean up
&& apt-get autoremove -y \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version

0
libs/keep

5
src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Core.Apps
public sealed class LanguagesConfig : IFieldPartitioning
{
public static readonly LanguagesConfig Empty = new LanguagesConfig(ImmutableDictionary<Language, LanguageConfig>.Empty, null, false);
public static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN);
public static readonly LanguagesConfig English = Build(Language.EN);
private readonly ImmutableDictionary<Language, LanguageConfig> languages;
private readonly LanguageConfig master;
@ -121,8 +121,7 @@ namespace Squidex.Domain.Apps.Core.Apps
.ToImmutableDictionary(x => x.Language);
var newMaster =
Master.Language != language ?
Master :
newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ??
newLanguages.Values.FirstOrDefault();
return new LanguagesConfig(newLanguages, newMaster);

31
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/ElasticSearchAction.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules.Actions
{
[TypeName(nameof(ElasticSearchAction))]
public sealed class ElasticSearchAction : RuleAction
{
public Uri Host { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string IndexName { get; set; }
public string IndexType { get; set; }
public override T Accept<T>(IRuleActionVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

2
src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Rules
T Visit(AzureQueueAction action);
T Visit(ElasticSearchAction action);
T Visit(FastlyAction action);
T Visit(SlackAction action);

2
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public bool? DefaultValue { get; set; }
public bool InlineEditable { get; set; }
public BooleanFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

2
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs

@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public double? DefaultValue { get; set; }
public bool InlineEditable { get; set; }
public NumberFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

2
src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxLength { get; set; }
public bool InlineEditable { get; set; }
public string DefaultValue { get; set; }
public string Pattern { get; set; }

22
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs

@ -22,7 +22,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class AlgoliaActionHandler : RuleActionHandler<AlgoliaAction>
{
private const string SchemaNamePlaceholder = "$SCHEMA_NAME";
private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients;
private readonly RuleEventFormatter formatter;
@ -62,6 +61,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
case ContentCreated created:
{
ruleDescription = $"Add entry to Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
new JProperty("id", contentEvent.ContentId),
new JProperty("created", timestamp),
@ -76,6 +76,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
case ContentUpdated updated:
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()),
@ -86,15 +87,20 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
case ContentStatusChanged statusChanged:
{
ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject(
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()),
new JProperty("status", statusChanged.Status.ToString()));
break;
}
case ContentDeleted deleted:
{
ruleDescription = $"Delete entry from Index: {action.IndexName}";
ruleDescription = $"Delete entry from Algolia index: {action.IndexName}";
ruleData["Content"] = new JObject();
ruleData["Operation"] = "Delete";
break;
}
}
@ -128,16 +134,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
content["objectID"] = contentId;
var resonse = await index.PartialUpdateObjectAsync(content);
var response = await index.PartialUpdateObjectAsync(content);
return (resonse.ToString(Formatting.Indented), null);
return (response.ToString(Formatting.Indented), null);
}
case "Delete":
{
var resonse = await index.DeleteObjectAsync(contentId);
var response = await index.DeleteObjectAsync(contentId);
return (resonse.ToString(Formatting.Indented), null);
return (response.ToString(Formatting.Indented), null);
}
default:
@ -148,10 +154,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
return (ex.Message, ex);
}
catch (Exception ex)
{
return (null, ex);
}
}
}
}

180
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/ElasticSearchActionHandler.cs

@ -0,0 +1,180 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Elasticsearch.Net;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class ElasticSearchActionHandler : RuleActionHandler<ElasticSearchAction>
{
private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients;
private readonly RuleEventFormatter formatter;
public ElasticSearchActionHandler(RuleEventFormatter formatter)
{
Guard.NotNull(formatter, nameof(formatter));
this.formatter = formatter;
clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key =>
{
var config = new ConnectionConfiguration(key.Host);
if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password))
{
config = config.BasicAuthentication(key.Username, key.Password);
}
return new ElasticLowLevelClient(config);
});
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, ElasticSearchAction action)
{
var ruleDescription = string.Empty;
var ruleData = new RuleJobData
{
["Host"] = action.Host,
["Username"] = action.Username,
["Password"] = action.Password
};
if (@event.Payload is ContentEvent contentEvent)
{
ruleData["ContentId"] = contentEvent.ContentId.ToString();
ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event);
ruleData["IndexType"] = formatter.FormatString(action.IndexType, @event);
var timestamp = @event.Headers.Timestamp().ToString();
switch (@event.Payload)
{
case ContentCreated created:
{
ruleDescription = $"Add entry to ES index: {action.IndexName}";
ruleData["Operation"] = "Create";
ruleData["Content"] = new JObject(
new JProperty("id", contentEvent.ContentId),
new JProperty("created", timestamp),
new JProperty("createdBy", created.Actor.ToString()),
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", created.Actor.ToString()),
new JProperty("status", Status.Draft.ToString()),
new JProperty("data", formatter.ToRouteData(created.Data)));
break;
}
case ContentUpdated updated:
{
ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update";
ruleData["Content"] = new JObject(
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", updated.Actor.ToString()),
new JProperty("data", formatter.ToRouteData(updated.Data)));
break;
}
case ContentStatusChanged statusChanged:
{
ruleDescription = $"Update entry in ES index: {action.IndexName}";
ruleData["Operation"] = "Update";
ruleData["Content"] = new JObject(
new JProperty("lastModified", timestamp),
new JProperty("lastModifiedBy", statusChanged.Actor.ToString()),
new JProperty("status", statusChanged.Status.ToString()));
break;
}
case ContentDeleted deleted:
{
ruleDescription = $"Delete entry from ES index: {action.IndexName}";
ruleData["Operation"] = "Delete";
ruleData["Content"] = new JObject();
break;
}
}
}
return (ruleDescription, ruleData);
}
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
{
if (!job.TryGetValue("Operation", out var operationToken))
{
return (null, new InvalidOperationException("The action cannot handle this event."));
}
var host = new Uri(job["Host"].Value<string>(), UriKind.Absolute);
var username = job["Username"].Value<string>();
var password = job["Password"].Value<string>();
var client = clients.GetClient((host, username, password));
var indexName = job["IndexName"].Value<string>();
var indexType = job["IndexType"].Value<string>();
var operation = operationToken.Value<string>();
var content = job["Content"].Value<JObject>();
var contentId = job["ContentId"].Value<string>();
try
{
switch (operation)
{
case "Create":
{
var doc = content.ToString();
var response = await client.IndexAsync<StringResponse>(indexName, indexType, contentId, doc);
return (response.Body, response.OriginalException);
}
case "Update":
{
var doc = new JObject(new JProperty("doc", content)).ToString();
var response = await client.UpdateAsync<StringResponse>(indexName, indexType, contentId, doc);
return (response.Body, response.OriginalException);
}
case "Delete":
{
var response = await client.DeleteAsync<StringResponse>(indexName, indexType, contentId);
return (response.Body, response.OriginalException);
}
default:
return (null, null);
}
}
catch (ElasticsearchClientException ex)
{
return (ex.Message, ex);
}
}
}
}

4
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs

@ -77,9 +77,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
}
else
{
var requestDump = ex.ToString();
return (requestDump, ex);
throw;
}
}
}

4
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs

@ -75,9 +75,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
}
else
{
var requestDump = ex.ToString();
return (requestDump, ex);
throw;
}
}
}

1
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="4.2.2" />
<PackageReference Include="Elasticsearch.Net" Version="6.0.1" />
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.1" />

19
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -8,8 +8,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Guards;
@ -42,20 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver)
: this(initialPatterns, store, appProvider, appPlansProvider, appPlansBillingManager, userResolver, null, null)
{
}
protected AppGrain(
InitialPatterns initialPatterns,
IStore<Guid> store,
IAppProvider appProvider,
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver,
IGrainIdentity identity,
IGrainRuntime runtime)
: base(store, identity, runtime)
: base(store)
{
Guard.NotNull(initialPatterns, nameof(initialPatterns));
Guard.NotNull(appProvider, nameof(appProvider));
@ -70,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
this.initialPatterns = initialPatterns;
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
protected override Task<object> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs

@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (!clients.TryGetValue(id, out var client))
{
throw new DomainObjectNotFoundException(id, "Clients", typeof(AppGrain));
throw new DomainObjectNotFoundException(id, "Clients", typeof(IAppEntity));
}
return client;

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (!contributors.ContainsKey(command.ContributorId))
{
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(AppGrain));
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity));
}
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs

@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (!languages.TryGetConfig(language, out var languageConfig))
{
throw new DomainObjectNotFoundException(language, "Languages", typeof(AppGrain));
throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity));
}
return languageConfig;

3
src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs

@ -9,8 +9,5 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services
{
public sealed class PlanChangeAsyncResult : IChangePlanResult
{
public PlanChangeAsyncResult()
{
}
}
}

60
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs

@ -8,7 +8,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -16,7 +15,6 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
@ -36,13 +34,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
var appId = new NamedId<Guid>(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task>(command =>
{
if (command is IAppCommand appCommand)
{
appCommand.AppId = appId;
}
return context.CommandBus.PublishAsync(command);
});
return Task.WhenAll(
CreatePagesAsync(context.CommandBus, appId),
CreatePostsAsync(context.CommandBus, appId),
CreateClientAsync(context.CommandBus, appId));
CreatePagesAsync(publish),
CreatePostsAsync(publish),
CreateClientAsync(publish, appId.Id));
}
return TaskHelper.Done;
return next();
}
private static bool IsRightTemplate(CreateApp createApp)
@ -50,16 +58,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
}
private static async Task CreateClientAsync(ICommandBus bus, NamedId<Guid> appId)
private static async Task CreateClientAsync(Func<ICommand, Task> publish, Guid appId)
{
await bus.PublishAsync(new AttachClient { Id = "sample-client" });
await publish(new AttachClient { Id = "sample-client", AppId = appId });
}
private async Task CreatePostsAsync(ICommandBus bus, NamedId<Guid> appId)
private async Task CreatePostsAsync(Func<ICommand, Task> publish)
{
var postsId = await CreatePostsSchema(bus, appId);
var postsId = await CreatePostsSchemaAsync(publish);
await bus.PublishAsync(new CreateContent
await publish(new CreateContent
{
SchemaId = postsId,
Data =
@ -74,11 +82,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
});
}
private async Task CreatePagesAsync(ICommandBus bus, NamedId<Guid> appId)
private async Task CreatePagesAsync(Func<ICommand, Task> publish)
{
var pagesId = await CreatePagesSchema(bus, appId);
var pagesId = await CreatePagesSchemaAsync(publish);
await bus.PublishAsync(new CreateContent
await publish(new CreateContent
{
SchemaId = pagesId,
Data =
@ -93,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
});
}
private async Task<NamedId<Guid>> CreatePostsSchema(ICommandBus bus, NamedId<Guid> appId)
private async Task<NamedId<Guid>> CreatePostsSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
@ -108,7 +116,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
new CreateSchemaField
{
Name = "title",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
@ -122,7 +129,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
new CreateSchemaField
{
Name = "slug",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Slug,
@ -137,7 +143,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
new CreateSchemaField
{
Name = "text",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.RichText,
@ -146,15 +151,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
Label = "Text"
}
}
},
AppId = appId
}
};
await bus.PublishAsync(command);
await publish(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name);
await bus.PublishAsync(new ConfigureScripts
await publish(new ConfigureScripts
{
SchemaId = schemaId.Id,
ScriptCreate = SlugScript,
@ -164,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
return schemaId;
}
private async Task<NamedId<Guid>> CreatePagesSchema(ICommandBus bus, NamedId<Guid> appId)
private async Task<NamedId<Guid>> CreatePagesSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
@ -178,7 +182,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
new CreateSchemaField
{
Name = "title",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
@ -192,7 +195,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
new CreateSchemaField
{
Name = "slug",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Slug,
@ -207,7 +209,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
new CreateSchemaField
{
Name = "text",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.RichText,
@ -216,15 +217,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
Label = "Text"
}
}
},
AppId = appId
}
};
await bus.PublishAsync(command);
await publish(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name);
await bus.PublishAsync(new ConfigureScripts
await publish(new ConfigureScripts
{
SchemaId = schemaId.Id,
ScriptCreate = SlugScript,

603
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs

@ -0,0 +1,603 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public sealed class CreateProfileCommandMiddleware : ICommandMiddleware
{
private const string TemplateName = "Profile";
public Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = new NamedId<Guid>(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task>(command =>
{
if (command is IAppCommand appCommand)
{
appCommand.AppId = appId;
}
return context.CommandBus.PublishAsync(command);
});
return Task.WhenAll(
CreateBasicsAsync(publish),
CreateProjectsSchemaAsync(publish),
CreateExperienceSchemaAsync(publish),
CreateSkillsSchemaAsync(publish),
CreateEducationSchemaAsync(publish),
CreatePublicationsSchemaAsync(publish),
CreateClientAsync(publish, appId.Id));
}
return next();
}
private static bool IsRightTemplate(CreateApp createApp)
{
return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
}
private static async Task CreateClientAsync(Func<ICommand, Task> publish, Guid appId)
{
await publish(new AttachClient { Id = "sample-client", AppId = appId });
}
private async Task CreateBasicsAsync(Func<ICommand, Task> publish)
{
var postsId = await CreateBasicsSchemaAsync(publish);
await publish(new CreateContent
{
SchemaId = postsId,
Data =
new NamedContentData()
.AddField("firstName",
new ContentFieldData()
.AddValue("iv", "John"))
.AddField("lastName",
new ContentFieldData()
.AddValue("iv", "Doe"))
.AddField("profession",
new ContentFieldData()
.AddValue("iv", "Software Developer")),
Publish = true,
});
}
private async Task<NamedId<Guid>> CreateBasicsSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
Name = "basics",
Properties = new SchemaProperties
{
Label = "Basics"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "firstName",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "First Name",
Hints = "Your first name"
}
},
new CreateSchemaField
{
Name = "lastName",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "Last Name",
Hints = "Your last name"
}
},
new CreateSchemaField
{
Name = "profession",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.TextArea,
IsRequired = true,
IsListField = false,
Label = "Profession",
Hints = "Define your profession"
}
},
new CreateSchemaField
{
Name = "image",
Properties = new AssetsFieldProperties
{
IsRequired = false,
IsListField = false,
MustBeImage = true,
Label = "Image",
Hints = "Your image"
}
},
new CreateSchemaField
{
Name = "summary",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.TextArea,
IsRequired = false,
IsListField = false,
Label = "Summary",
Hints = "Write a short summary about yourself"
}
},
new CreateSchemaField
{
Name = "githubLink",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "Github",
Hints = "An optional link to your Github account"
}
},
new CreateSchemaField
{
Name = "blogLink",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "Blog",
Hints = "An optional link to your blog"
}
},
new CreateSchemaField
{
Name = "twitterLink",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "Twitter",
Hints = "An optional link to your twitter account"
}
},
new CreateSchemaField
{
Name = "linkedInLink",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "LinkedIn",
Hints = "An optional link to your LinkedIn account"
}
},
new CreateSchemaField
{
Name = "emailAddress",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "Email Address",
Hints = "An optional email address to contact you"
}
},
new CreateSchemaField
{
Name = "legalTerms",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.TextArea,
IsRequired = false,
IsListField = false,
Label = "Legal terms",
Hints = "The terms to fulfill legal requirements"
}
}
},
Publish = true
};
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateProjectsSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
Name = "projects",
Properties = new SchemaProperties
{
Label = "Projects"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "name",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "Name",
Hints = "The name of the projection"
}
},
new CreateSchemaField
{
Name = "description",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.TextArea,
IsRequired = true,
IsListField = false,
Label = "Description",
Hints = "Describe your project"
}
},
new CreateSchemaField
{
Name = "image",
Properties = new AssetsFieldProperties
{
IsRequired = true,
IsListField = false,
MustBeImage = true,
Label = "Image",
Hints = "An image or screenshot for your project"
}
},
new CreateSchemaField
{
Name = "label",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "Label",
Hints = "An optional label to categorize your project, e.g. 'Open Source'"
}
},
new CreateSchemaField
{
Name = "link",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "link",
Hints = "The logo of the company or organization you worked for"
}
},
new CreateSchemaField
{
Name = "year",
Properties = new NumberFieldProperties
{
IsRequired = false,
IsListField = false,
Label = "Year",
Hints = "The year, when you realized the project, used for sorting only"
}
}
},
Publish = true
};
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateExperienceSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
Name = "experience",
Properties = new SchemaProperties
{
Label = "Experience"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "position",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "Position",
Hints = "Your position in this job"
}
},
new CreateSchemaField
{
Name = "company",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "Company",
Hints = "The company or organization you worked for"
}
},
new CreateSchemaField
{
Name = "logo",
Properties = new AssetsFieldProperties
{
IsRequired = false,
IsListField = false,
MustBeImage = true,
Label = "Logo",
Hints = "The logo of the company or organization you worked for"
}
},
new CreateSchemaField
{
Name = "from",
Properties = new DateTimeFieldProperties
{
Editor = DateTimeFieldEditor.Date,
IsRequired = true,
IsListField = false,
Label = "Start Date",
Hints = "The start date"
}
},
new CreateSchemaField
{
Name = "to",
Properties = new DateTimeFieldProperties
{
Editor = DateTimeFieldEditor.Date,
IsRequired = false,
IsListField = false,
Label = "End Date",
Hints = "The end date, keep empty if you still work there"
}
}
},
Publish = true
};
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateEducationSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
Name = "education",
Properties = new SchemaProperties
{
Label = "Education"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "degree",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "Degree",
Hints = "The degree you got or achieved"
}
},
new CreateSchemaField
{
Name = "school",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "School",
Hints = "The school or university"
}
},
new CreateSchemaField
{
Name = "logo",
Properties = new AssetsFieldProperties
{
IsRequired = false,
IsListField = false,
MustBeImage = true,
Label = "Logo",
Hints = "The logo of the school"
}
},
new CreateSchemaField
{
Name = "from",
Properties = new DateTimeFieldProperties
{
Editor = DateTimeFieldEditor.Date,
IsRequired = true,
IsListField = false,
Label = "Start Date",
Hints = "The start date"
}
},
new CreateSchemaField
{
Name = "to",
Properties = new DateTimeFieldProperties
{
Editor = DateTimeFieldEditor.Date,
IsRequired = false,
IsListField = false,
Label = "End Date",
Hints = "The end date, keep empty if you still study there"
}
}
},
Publish = true
};
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreatePublicationsSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
Name = "publications",
Properties = new SchemaProperties
{
Label = "Publications"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "name",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "Name",
Hints = "The name or title of your publication"
}
},
new CreateSchemaField
{
Name = "description",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.TextArea,
IsRequired = false,
IsListField = false,
Label = "Description",
Hints = "Describe the content of your publication"
}
},
new CreateSchemaField
{
Name = "cover",
Properties = new AssetsFieldProperties
{
IsRequired = true,
IsListField = false,
MustBeImage = true,
Label = "Cover",
Hints = "The cover of your publication"
}
},
new CreateSchemaField
{
Name = "link",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = false,
IsListField = false,
Label = "Link",
Hints = "An optional link to your publication"
}
}
},
Publish = true
};
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateSkillsSchemaAsync(Func<ICommand, Task> publish)
{
var command = new CreateSchema
{
Name = "skills",
Properties = new SchemaProperties
{
Label = "Skills"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "name",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
Label = "Name",
Hints = "The name for your skill"
}
},
new CreateSchemaField
{
Name = "experience",
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Dropdown,
IsRequired = true,
IsListField = true,
AllowedValues = ImmutableList.Create("Beginner", "Advanced", "Professional", "Expert"),
Label = "Experience",
Hints = "The level of experience"
}
}
},
Publish = true
};
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
}
}
}

17
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -7,8 +7,6 @@
using System;
using System.Threading.Tasks;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Guards;
using Squidex.Domain.Apps.Entities.Assets.State;
@ -17,6 +15,7 @@ using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
@ -25,16 +24,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
public class AssetGrain : DomainObjectGrain<AssetState>, IAssetGrain
{
public AssetGrain(IStore<Guid> store)
: this(store, null, null)
: base(store)
{
}
protected AssetGrain(IStore<Guid> store, IGrainIdentity identity, IGrainRuntime runtime)
: base(store, identity, runtime)
{
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
protected override Task<object> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{
@ -144,5 +138,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
ApplySnapshot(Snapshot.Apply(@event));
}
public Task<J<IAssetEntity>> GetStateAsync()
{
return Task.FromResult(new J<IAssetEntity>(Snapshot));
}
}
}

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

@ -7,8 +7,6 @@
using System;
using System.Threading.Tasks;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
@ -39,19 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentRepository contentRepository)
: this(store, appProvider, assetRepository, scriptEngine, contentRepository, null, null)
{
}
protected ContentGrain(
IStore<Guid> store,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentRepository contentRepository,
IGrainIdentity identity,
IGrainRuntime runtime)
: base(store, identity, runtime)
: base(store)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
@ -64,14 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.contentRepository = contentRepository;
}
public override Task OnActivateAsync()
{
DelayDeactivation(TimeSpan.FromMinutes(10));
return base.OnActivateAsync();
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
protected override Task<object> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();

2
src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
: base(typeNameRegistry)
{
AddEventMessage<ContentCreated>(
"created {[Schema]} content item to.");
"created {[Schema]} content item.");
AddEventMessage<ContentUpdated>(
"updated {[Schema]} content item.");

22
src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs

@ -68,6 +68,28 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
return Task.FromResult<IEnumerable<ValidationError>>(errors);
}
public Task<IEnumerable<ValidationError>> Visit(ElasticSearchAction action)
{
var errors = new List<ValidationError>();
if (action.Host == null || !action.Host.IsAbsoluteUri)
{
errors.Add(new ValidationError("Host is required and must be an absolute URL.", nameof(action.Host)));
}
if (string.IsNullOrWhiteSpace(action.IndexType))
{
errors.Add(new ValidationError("TypeName is required.", nameof(action.IndexType)));
}
if (string.IsNullOrWhiteSpace(action.IndexName))
{
errors.Add(new ValidationError("IndexName is required.", nameof(action.IndexName)));
}
return Task.FromResult<IEnumerable<ValidationError>>(errors);
}
public Task<IEnumerable<ValidationError>> Visit(FastlyAction action)
{
var errors = new List<ValidationError>();

21
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -7,8 +7,6 @@
using System;
using System.Threading.Tasks;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Guards;
using Squidex.Domain.Apps.Entities.Rules.State;
@ -23,40 +21,35 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class RuleGrain : DomainObjectGrain<RuleState>, IRuleGrain
public sealed class RuleGrain : DomainObjectGrain<RuleState>, IRuleGrain
{
private readonly IAppProvider appProvider;
public RuleGrain(IStore<Guid> store, IAppProvider appProvider)
: this(store, appProvider, null, null)
{
}
protected RuleGrain(IStore<Guid> store, IAppProvider appProvider, IGrainIdentity identity, IGrainRuntime runtime)
: base(store, identity, runtime)
: base(store)
{
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
protected override Task<object> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();
switch (command)
{
case CreateRule createRule:
return CreateAsync(createRule, c =>
return CreateAsync(createRule, async c =>
{
GuardRule.CanCreate(c, appProvider);
await GuardRule.CanCreate(c, appProvider);
Create(c);
});
case UpdateRule updateRule:
return UpdateAsync(updateRule, c =>
return UpdateAsync(updateRule, async c =>
{
GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider);
await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider);
Update(c);
});

2
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs

@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class CreateSchemaField
{
public string Partitioning { get; set; }
public string Partitioning { get; set; } = Core.Partitioning.Invariant.Key;
public string Name { get; set; }

14
src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs

@ -170,6 +170,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.MinValue),
nameof(properties.MaxValue));
}
if (properties.InlineEditable && properties.Editor != NumberFieldEditor.Input && properties.Editor != NumberFieldEditor.Dropdown)
{
yield return new ValidationError("Inline editing is only allowed for dropdowns and input fields.",
nameof(properties.InlineEditable),
nameof(properties.Editor));
}
}
public IEnumerable<ValidationError> Visit(ReferencesFieldProperties properties)
@ -216,6 +223,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.MinLength),
nameof(properties.MaxLength));
}
if (properties.InlineEditable && properties.Editor != StringFieldEditor.Dropdown && properties.Editor != StringFieldEditor.Input && properties.Editor != StringFieldEditor.Slug)
{
yield return new ValidationError("Inline editing is only allowed for dropdowns, slugs and input fields.",
nameof(properties.InlineEditable),
nameof(properties.Editor));
}
}
public IEnumerable<ValidationError> Visit(TagsFieldProperties properties)

2
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
error(new ValidationError("Field ids is required.", nameof(command.FieldIds)));
}
if (command.FieldIds.Count != schema.Fields.Count || command.FieldIds.Any(x => !schema.FieldsById.ContainsKey(x)))
if (command.FieldIds != null && (command.FieldIds.Count != schema.Fields.Count || command.FieldIds.Any(x => !schema.FieldsById.ContainsKey(x))))
{
error(new ValidationError("Ids must cover all fields.", nameof(command.FieldIds)));
}

11
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -9,8 +9,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Guards;
@ -32,12 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
private readonly FieldRegistry registry;
public SchemaGrain(IStore<Guid> store, IAppProvider appProvider, FieldRegistry registry)
: this(store, appProvider, registry, null, null)
{
}
protected SchemaGrain(IStore<Guid> store, IAppProvider appProvider, FieldRegistry registry, IGrainIdentity identity, IGrainRuntime runtime)
: base(store, identity, runtime)
: base(store)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(registry, nameof(registry));
@ -47,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
this.registry = registry;
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
protected override Task<object> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();

8
src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs

@ -11,19 +11,19 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.EventSourcing
{
public class MongoEvent
public sealed class MongoEvent
{
[BsonElement]
[BsonRequired]
public string Payload { get; set; }
public string Type { get; set; }
[BsonJson]
[BsonRequired]
public JToken Metadata { get; set; }
public string Payload { get; set; }
[BsonElement]
[BsonRequired]
public string Type { get; set; }
public JToken Metadata { get; set; }
public static MongoEvent FromEventData(EventData data)
{

25
src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs

@ -81,16 +81,31 @@ namespace Squidex.Infrastructure.MongoDb
return BsonNull.Value;
case JTokenType.Undefined:
return BsonUndefined.Value;
case JTokenType.Date:
return BsonValue.Create(((JValue)source).Value.ToString());
case JTokenType.Bytes:
return BsonValue.Create(((JValue)source).Value);
case JTokenType.Guid:
return BsonValue.Create(((JValue)source).Value.ToString());
return BsonValue.Create(((JValue)source).ToString());
case JTokenType.Uri:
return BsonValue.Create(((JValue)source).Value.ToString());
return BsonValue.Create(((JValue)source).ToString());
case JTokenType.TimeSpan:
return BsonValue.Create(((JValue)source).Value.ToString());
return BsonValue.Create(((JValue)source).ToString());
case JTokenType.Date:
{
var value = ((JValue)source).Value;
if (value is DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-ddTHH:mm:ssK");
}
else if (value is DateTimeOffset dateTimeOffset)
{
return dateTimeOffset.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK");
}
else
{
return value.ToString();
}
}
}
throw new NotSupportedException($"Cannot convert {source.GetType()} to Bson.");

5
src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Globalization;
using MongoDB.Bson.IO;
using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter;
@ -134,12 +135,12 @@ namespace Squidex.Infrastructure.MongoDb
public override void WriteValue(DateTime value)
{
bsonWriter.WriteString(value.ToString());
bsonWriter.WriteString(value.ToString(CultureInfo.InvariantCulture));
}
public override void WriteValue(DateTimeOffset value)
{
bsonWriter.WriteString(value.ToString());
bsonWriter.WriteString(value.ToString(CultureInfo.InvariantCulture));
}
public override void WriteValue(byte[] value)

41
src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs

@ -8,9 +8,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
@ -18,13 +15,19 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Commands
{
public abstract class DomainObjectGrain<T> : Grain, IDomainObjectGrain where T : IDomainState, new()
public abstract class DomainObjectGrain<T> : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private readonly IStore<Guid> store;
private Guid id;
private T snapshot = new T { Version = EtagVersion.Empty };
private IPersistence<T> persistence;
public Guid Id
{
get { return id; }
}
public long Version
{
get { return snapshot.Version; }
@ -41,21 +44,17 @@ namespace Squidex.Infrastructure.Commands
}
protected DomainObjectGrain(IStore<Guid> store)
: this(store, null, null)
{
}
protected DomainObjectGrain(IStore<Guid> store, IGrainIdentity identity, IGrainRuntime runtime)
: base(identity, runtime)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public override Task OnActivateAsync()
public override Task OnActivateAsync(Guid key)
{
persistence = store.WithSnapshotsAndEventSourcing<T, Guid>(GetType(), this.GetPrimaryKey(), ApplySnapshot, ApplyEvent);
id = key;
persistence = store.WithSnapshotsAndEventSourcing<T, Guid>(GetType(), id, ApplySnapshot, ApplyEvent);
return persistence.ReadAsync();
}
@ -69,7 +68,7 @@ namespace Squidex.Infrastructure.Commands
{
Guard.NotNull(@event, nameof(@event));
@event.SetAggregateId(this.GetPrimaryKey());
@event.SetAggregateId(id);
ApplyEvent(@event);
@ -148,14 +147,20 @@ namespace Squidex.Infrastructure.Commands
if (command.ExpectedVersion != EtagVersion.Any && command.ExpectedVersion != Version)
{
throw new DomainObjectVersionException(this.GetPrimaryKey().ToString(), GetType(), Version, command.ExpectedVersion);
throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion);
}
if (isUpdate && Version < 0)
{
DeactivateOnIdle();
try
{
DeactivateOnIdle();
}
catch (InvalidOperationException)
{
}
throw new DomainObjectNotFoundException(this.GetPrimaryKey().ToString(), GetType());
throw new DomainObjectNotFoundException(id.ToString(), GetType());
}
else if (!isUpdate && Version >= 0)
{
@ -185,7 +190,7 @@ namespace Squidex.Infrastructure.Commands
}
else
{
result = EntityCreatedResult.Create(this.GetPrimaryKey(), Version);
result = EntityCreatedResult.Create(id, Version);
}
}
@ -210,6 +215,6 @@ namespace Squidex.Infrastructure.Commands
return result.AsJ();
}
public abstract Task<object> ExecuteAsync(IAggregateCommand command);
protected abstract Task<object> ExecuteAsync(IAggregateCommand command);
}
}

2
src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs

@ -29,7 +29,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
var eventType = typeNameRegistry.GetType(eventData.Type);
var headers = eventData.Metadata.ToObject<EnvelopeHeaders>();
var headers = eventData.Metadata.ToObject<EnvelopeHeaders>(serializer);
var content = eventData.Payload.ToObject(eventType, serializer) as IEvent;
if (migrate && content is IMigratedEvent migratedEvent)

34
src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs

@ -10,15 +10,14 @@ using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Orleans;
using Orleans.Concurrency;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.EventSourcing.Grains
{
public class EventConsumerGrain : Grain, IEventConsumerGrain
public class EventConsumerGrain : GrainOfString, IEventConsumerGrain
{
private readonly EventConsumerFactory eventConsumerFactory;
private readonly IStore<string> store;
@ -37,19 +36,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISemanticLog log)
: this (eventConsumerFactory, store, eventStore, eventDataFormatter, null, null, log)
{
}
protected EventConsumerGrain(
EventConsumerFactory eventConsumerFactory,
IStore<string> store,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
IGrainIdentity identity,
IGrainRuntime runtime,
ISemanticLog log)
: base(identity, runtime)
{
Guard.NotNull(log, nameof(log));
Guard.NotNull(store, nameof(store));
@ -58,19 +44,17 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
Guard.NotNull(eventConsumerFactory, nameof(eventConsumerFactory));
this.log = log;
this.store = store;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.eventConsumerFactory = eventConsumerFactory;
this.store = store;
}
public override Task OnActivateAsync()
public override Task OnActivateAsync(string key)
{
scheduler = TaskScheduler.Current;
eventConsumer = eventConsumerFactory(this.GetPrimaryKeyString());
eventConsumer = eventConsumerFactory(key);
persistence = store.WithSnapshots<EventConsumerState, string>(GetType(), eventConsumer.Name, s => state = s);
@ -263,7 +247,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
if (currentSubscription == null)
{
currentSubscription?.StopAsync().Forget();
currentSubscription = CreateSubscription(eventStore, eventConsumer.EventsFilter, position);
currentSubscription = CreateSubscription(eventConsumer.EventsFilter, position);
}
else
{
@ -295,12 +279,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
return this.AsReference<IEventConsumerGrain>();
}
protected virtual IEventSubscription CreateSubscription(IEventStore eventStore, IEventSubscriber subscriber, string streamFilter, string position)
protected virtual IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string position)
{
return new RetrySubscription(eventStore, subscriber, streamFilter, position);
return new RetrySubscription(store, subscriber, streamFilter, position);
}
private IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position)
private IEventSubscription CreateSubscription(string streamFilter, string position)
{
return CreateSubscription(eventStore, new WrapperSubscription(GetSelf(), scheduler), streamFilter, position);
}

10
src/Squidex.Infrastructure/Orleans/Bootstrap.cs

@ -5,12 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure.Tasks;
using Orleans.Runtime;
namespace Squidex.Infrastructure.Orleans
{
public sealed class Bootstrap<T> : IRunnable where T : IBackgroundGrain
public sealed class Bootstrap<T> : IStartupTask where T : IBackgroundGrain
{
private readonly IGrainFactory grainFactory;
@ -21,11 +23,11 @@ namespace Squidex.Infrastructure.Orleans
this.grainFactory = grainFactory;
}
public void Run()
public Task Execute(CancellationToken cancellationToken)
{
var grain = grainFactory.GetGrain<T>("Default");
grain.ActivateAsync().Forget();
return grain.ActivateAsync();
}
}
}

27
src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Orleans
{
public abstract class GrainOfGuid : Grain
{
public override Task OnActivateAsync()
{
return OnActivateAsync(this.GetPrimaryKey());
}
public virtual Task OnActivateAsync(Guid key)
{
return TaskHelper.Done;
}
}
}

26
src/Squidex.Infrastructure/Orleans/GrainOfString.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Orleans
{
public abstract class GrainOfString : Grain
{
public override Task OnActivateAsync()
{
return OnActivateAsync(this.GetPrimaryKeyString());
}
public virtual Task OnActivateAsync(string key)
{
return TaskHelper.Done;
}
}
}

37
src/Squidex.Infrastructure/Reflection/SimpleMapper.cs

@ -16,6 +16,23 @@ namespace Squidex.Infrastructure.Reflection
{
public static class SimpleMapper
{
private sealed class StringConversionPropertyMapper : PropertyMapper
{
public StringConversionPropertyMapper(
IPropertyAccessor sourceAccessor,
IPropertyAccessor targetAccessor)
: base(sourceAccessor, targetAccessor)
{
}
public override void MapProperty(object source, object target, CultureInfo culture)
{
var value = GetValue(source);
SetValue(target, value?.ToString());
}
}
private sealed class ConversionPropertyMapper : PropertyMapper
{
private readonly Type targetType;
@ -38,21 +55,15 @@ namespace Squidex.Infrastructure.Reflection
return;
}
object converted;
try
{
converted = Convert.ChangeType(value, targetType, culture);
var converted = Convert.ChangeType(value, targetType, culture);
SetValue(target, converted);
}
catch (InvalidCastException)
catch
{
if (targetType == typeof(string))
{
converted = value.ToString();
SetValue(target, converted);
}
return;
}
}
}
@ -120,7 +131,13 @@ namespace Squidex.Infrastructure.Reflection
new PropertyAccessor(sourceClassType, sourceProperty),
new PropertyAccessor(targetClassType, targetProperty)));
}
else if (targetType.Implements<IConvertible>())
else if (targetType == typeof(string))
{
Mappers.Add(new StringConversionPropertyMapper(
new PropertyAccessor(sourceClassType, sourceProperty),
new PropertyAccessor(targetClassType, targetProperty)));
}
else if (sourceType.Implements<IConvertible>() || targetType.Implements<IConvertible>())
{
Mappers.Add(new ConversionPropertyMapper(
new PropertyAccessor(sourceClassType, sourceProperty),

2
src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs

@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States
{
public sealed class DefaultStreamNameResolver : IStreamNameResolver
{
private static readonly string[] Suffixes = new string[] { "DomainObject", "Grain" };
private static readonly string[] Suffixes = { "Grain", "DomainObject" };
public string GetStreamName(Type aggregateType, string id)
{

53
src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/ElasticSearchActionDto.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NJsonSchema.Annotations;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Rules.Models.Actions
{
[JsonSchema("ElasticSearch")]
public sealed class ElasticSearchActionDto : RuleActionDto
{
/// <summary>
/// The host to the elastic search instance.
/// </summary>
[Required]
public Uri Host { get; set; }
/// <summary>
/// The name of the index.
/// </summary>
[Required]
public string IndexName { get; set; }
/// <summary>
/// The name of the index type.
/// </summary>
[Required]
public string IndexType { get; set; }
/// <summary>
/// The optional username for authentication.
/// </summary>
public string Username { get; set; }
/// <summary>
/// The optional password for authentication.
/// </summary>
public string Password { get; set; }
public override RuleAction ToAction()
{
return SimpleMapper.Map(this, new ElasticSearchAction());
}
}
}

5
src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs

@ -35,6 +35,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters
return SimpleMapper.Map(action, new AzureQueueActionDto());
}
public RuleActionDto Visit(ElasticSearchAction action)
{
return SimpleMapper.Map(action, new ElasticSearchActionDto());
}
public RuleActionDto Visit(FastlyAction action)
{
return SimpleMapper.Map(action, new FastlyActionDto());

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs

@ -21,6 +21,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public bool? DefaultValue { get; set; }
/// <summary>
/// Indicates that the inline editor is enabled for this field.
/// </summary>
public bool InlineEditable { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs

@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public double[] AllowedValues { get; set; }
/// <summary>
/// Indicates that the inline editor is enabled for this field.
/// </summary>
public bool InlineEditable { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs

@ -47,6 +47,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public string[] AllowedValues { get; set; }
/// <summary>
/// Indicates that the inline editor is enabled for this field.
/// </summary>
public bool InlineEditable { get; set; }
/// <summary>
/// The editor that is used to manage this field.
/// </summary>

13
src/Squidex/Config/Domain/ReadServices.cs

@ -27,7 +27,6 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline;
namespace Squidex.Config.Domain
@ -92,6 +91,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AzureQueueActionHandler>()
.As<IRuleActionHandler>();
services.AddSingletonAs<ElasticSearchActionHandler>()
.As<IRuleActionHandler>();
services.AddSingletonAs<FastlyActionHandler>()
.As<IRuleActionHandler>();
@ -101,15 +103,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<WebhookActionHandler>()
.As<IRuleActionHandler>();
services.AddSingletonAs<Bootstrap<IRuleDequeuerGrain>>()
.As<IRunnable>();
services.AddSingletonAs<Bootstrap<IContentSchedulerGrain>>()
.As<IRunnable>();
services.AddSingletonAs<Bootstrap<IEventConsumerManagerGrain>>()
.As<IRunnable>();
services.AddSingletonAs<RuleEnqueuer>()
.As<IEventConsumer>();

12
src/Squidex/Config/Domain/WriteServices.cs

@ -72,6 +72,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<CreateBlogCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<CreateProfileCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<MigrationPath>()
.As<IMigrationPath>();
@ -96,9 +99,18 @@ namespace Squidex.Config.Domain
services.AddTransientAs<AppGrain>()
.AsSelf();
services.AddTransientAs<AssetGrain>()
.AsSelf();
services.AddTransientAs<ContentGrain>()
.AsSelf();
services.AddTransientAs<RuleGrain>()
.AsSelf();
services.AddTransientAs<SchemaGrain>()
.AsSelf();
services.AddSingleton(c =>
{
var config = c.GetRequiredService<IOptions<MyUIOptions>>();

6
src/Squidex/Config/Orleans/SiloWrapper.cs

@ -15,7 +15,10 @@ using Orleans.Configuration;
using Orleans.Hosting;
using Squidex.Config.Domain;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Log.Adapter;
using Squidex.Infrastructure.Orleans;
@ -46,6 +49,9 @@ namespace Squidex.Config.Orleans
silo = new SiloHostBuilder()
.UseDashboard(options => options.HostSelf = true)
.AddStartupTask<Bootstrap<IContentSchedulerGrain>>()
.AddStartupTask<Bootstrap<IEventConsumerManagerGrain>>()
.AddStartupTask<Bootstrap<IRuleDequeuerGrain>>()
.ConfigureEndpoints(Dns.GetHostName(), 11111, 40000, listenOnAllHostAddresses: true)
.Configure(options =>
{

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

@ -1,7 +1,6 @@
 var webpack = require('webpack'),
webpackMerge = require('webpack-merge'),
ExtractTextPlugin = require('extract-text-webpack-plugin'),
UglifyJsPlugin = require('uglifyjs-webpack-plugin'),
ngToolsWebpack = require('@ngtools/webpack'),
runConfig = require('./webpack.run.base.js'),
helpers = require('./helpers');
@ -96,19 +95,15 @@ module.exports = webpackMerge(runConfig, {
*/
new ExtractTextPlugin('[name].css'),
new UglifyJsPlugin({
cache: true,
uglifyOptions: {
mangle: {
safari10: true,
},
compress: {
pure_getters: true, passes: 3
},
output: {
ascii_only: true, comments: false,webkit: true,
}
}
new webpack.optimize.UglifyJsPlugin({
beautify: false,
mangle: {
screw_ie8: true, keep_fnames: true
},
compress: {
screw_ie8: true, warnings: false
},
comments: false
}),
new ngToolsWebpack.AngularCompilerPlugin({

15
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -53,6 +53,21 @@
</div>
</div>
</div>
<div class="card card-template card-href" (click)="createNewApp('Profile')">
<div class="card-body">
<div class="card-image">
<img src="/images/add-profile.png" />
</div>
<h4 class="card-title">New Profile Sample</h4>
<div class="card-text">
<div>Create your profile page.</div>
<div>Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Profile" (click)="$event.stopPropagation()" target="_blank">ASP.NET Core</a></div>
</div>
</div>
</div>
</div>
<div class="modal" *sqxModalView="addAppDialog;onRoot:true" @fade>

188
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -5,112 +5,106 @@
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
<div [formGroup]="fieldForm">
<div *ngIf="field.partitioning === 'language' && languages.length > 1">
<div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
<div *ngIf="field.isLocalizable && languages.length > 1">
<div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
<div *ngFor="let partition of fieldPartitions">
<div *ngIf="partition == fieldPartition">
<sqx-control-errors [for]="partition" fieldName="{{field.displayName}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</div>
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" />
</div>
<div *ngSwitchCase="'Stars'">
<sqx-stars [formControlName]="partition" [maximumStars]="field.properties.maxValue"></sqx-stars>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="partition">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" [value]="value" [formControlName]="partition" />
<label class="form-check-label">
{{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControlName]="partition" rows="5" [placeholder]="field.properties.placeholder || ''"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControlName]="partition" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControlName]="partition" (assetPluginClicked)="assetPluginClicked()"></sqx-markdown-editor>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="partition">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" value="{{value}}" [formControlName]="partition" />
<label class="form-check-label">
{{value}}
</label>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="partition"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="partition" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties.editor" [formControlName]="partition"></sqx-date-time-editor>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControlName]="partition"></sqx-geolocation-editor>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-editor [formControlName]="partition"></sqx-json-editor>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControlName]="partition"></sqx-assets-editor>
<sqx-control-errors [for]="selectedFormControl" [fieldName]="field.displayName" [submitted]="contentFormSubmitted"></sqx-control-errors>
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties.maxValue"></sqx-stars>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControlName]="partition"></sqx-tag-editor>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [id]="selectedFormName"[formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-markdown-editor>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" />
<label class="form-check-label">
{{value}}
</label>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-editor [formControlName]="partition" [languageCode]="selectFieldLanguage(partition)" [schemaId]="field.properties.schemaId"></sqx-references-editor>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="selectedFormControl"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControl]="selectedFormControl" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties.editor" [formControl]="selectedFormControl"></sqx-date-time-editor>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-editor [formControl]="selectedFormControl"></sqx-json-editor>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="selectedFormControl"></sqx-assets-editor>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="selectedFormControl"></sqx-tag-editor>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-editor [formControl]="selectedFormControl" [language]="selectedLanguage" [schemaId]="field.properties.schemaId"></sqx-references-editor>
</div>
</div>
<small class="form-text text-muted" *ngIf="field.properties.hints && field.properties.hints.length > 0">

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

@ -6,21 +6,21 @@
*/
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppLanguageDto, FieldDto } from 'shared';
import { AbstractControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
AppLanguageDto,
FieldDto,
fieldInvariant
} from 'shared';
@Component({
selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html'
})
export class ContentFieldComponent implements OnInit {
constructor(private readonly router: Router, private readonly route: ActivatedRoute) {
}
private masterLanguageCode: string;
@Input()
public field: FieldDto;
@ -33,35 +33,36 @@ export class ContentFieldComponent implements OnInit {
@Input()
public contentFormSubmitted: boolean;
public fieldPartitions: string[];
public fieldPartition: string;
public selectedFormControl: AbstractControl;
public selectedLanguage: AppLanguageDto;
public selectLanguage(language: AppLanguageDto) {
this.fieldPartition = language.iso2Code;
constructor(
private readonly router: Router,
private readonly route: ActivatedRoute
) {
}
public ngOnInit() {
this.masterLanguageCode = this.languages.find(l => l.isMaster)!.iso2Code;
if (this.field.isDisabled) {
this.fieldForm.disable();
}
const masterLanguage = this.languages[0];
if (this.field.partitioning === 'language') {
this.fieldPartitions = this.languages.map(t => t.iso2Code);
this.fieldPartition = this.fieldPartitions[0];
if (this.field.isLocalizable) {
this.selectedFormControl = this.fieldForm.controls[masterLanguage.iso2Code];
} else {
this.fieldPartitions = ['iv'];
this.fieldPartition = 'iv';
this.selectedFormControl = this.fieldForm.controls[fieldInvariant];
}
this.selectedLanguage = masterLanguage;
}
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
public selectLanguage(language: AppLanguageDto) {
this.selectedFormControl['_clearChangeFns']();
this.selectedFormControl = this.fieldForm.controls[language.iso2Code];
this.selectedLanguage = language;
}
public selectFieldLanguage(partition: string) {
return partition === 'iv' ? this.masterLanguageCode : partition;
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
}
}

2
src/Squidex/app/features/content/pages/content/content-history.component.scss

@ -28,7 +28,7 @@
&-left {
min-width: 2.8rem;
max-width: 2.8rem;
margin-top: .3rem;
margin-top: .25rem;
}
&-created {

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

@ -25,6 +25,7 @@ import {
CanComponentDeactivate,
ContentDto,
ContentsService,
fieldInvariant,
SchemaDetailsDto,
Version
} from 'shared';
@ -40,6 +41,7 @@ import {
export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit {
private contentStatusChangedSubscription: Subscription;
private contentDeletedSubscription: Subscription;
private contentUpdatedSubscription: Subscription;
private contentVersionSelectedSubscription: Subscription;
public schema: SchemaDetailsDto;
@ -62,6 +64,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
public ngOnDestroy() {
this.contentVersionSelectedSubscription.unsubscribe();
this.contentStatusChangedSubscription.unsubscribe();
this.contentUpdatedSubscription.unsubscribe();
this.contentDeletedSubscription.unsubscribe();
}
@ -80,6 +83,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
}
});
this.contentUpdatedSubscription =
this.ctx.bus.of(ContentUpdated)
.subscribe(message => {
if (this.content && message.content.id === this.content.id) {
this.reloadContentForm(message.content);
}
});
this.contentStatusChangedSubscription =
this.ctx.bus.of(ContentStatusChanged)
.subscribe(message => {
@ -96,15 +107,12 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
const routeData = allData(this.ctx.route);
this.languages = routeData.appLanguages;
this.setupLanguages(routeData);
this.setupContentForm(routeData.schema);
this.ctx.route.data.map(d => d.content)
.subscribe((content: ContentDto) => {
this.content = content;
this.populateContentForm();
this.reloadContentForm(content);
});
}
@ -122,7 +130,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.contentOld = null;
this.emitContentUpdated(this.content);
this.populateContentForm();
this.reloadContentForm(this.content);
}
}
@ -159,13 +167,13 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
} else {
this.contentsService.putContent(this.ctx.appName, this.schema.name, this.content.id, requestDto, this.content.version)
.subscribe(dto => {
this.content = this.content.update(dto.payload, this.ctx.userToken, dto.version);
const content = this.content.update(dto.payload, this.ctx.userToken, dto.version);
this.ctx.notifyInfo('Content saved successfully.');
this.emitContentUpdated(this.content);
this.emitContentUpdated(content);
this.enableContentForm();
this.populateContentForm();
this.reloadContentForm(content);
}, error => {
this.ctx.notifyError(error);
@ -187,12 +195,9 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.contentOld = null;
}
this.content = this.content.setData(dto);
this.ctx.notifyInfo('Content version loaded successfully.');
this.emitContentUpdated(this.content);
this.populateContentForm();
this.reloadContentForm(this.content.setData(dto));
}, error => {
this.ctx.notifyError(error);
});
@ -221,35 +226,48 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
if (this.schema.fields.length === 0) {
this.contentForm.enable();
} else {
for (const field of this.schema.fields.filter(f => !f.isDisabled)) {
this.contentForm.controls[field.name].enable();
for (const field of this.schema.fields) {
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.isDisabled) {
fieldForm.disable();
} else {
fieldForm.enable();
}
}
}
}
private setupLanguages(routeData: { [name: string]: any; }) {
this.languages = routeData.appLanguages;
}
private setupContentForm(schema: SchemaDetailsDto) {
this.schema = schema;
const controls: { [key: string]: AbstractControl } = {};
for (const field of schema.fields) {
const group = new FormGroup({});
const fieldForm = new FormGroup({});
if (field.partitioning === 'language') {
if (field.isLocalizable) {
for (let language of this.languages) {
group.addControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional)));
fieldForm.setControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional)));
}
} else {
group.addControl('iv', new FormControl(undefined, field.createValidators(false)));
fieldForm.setControl(fieldInvariant, new FormControl(undefined, field.createValidators(false)));
}
controls[field.name] = group;
controls[field.name] = fieldForm;
}
this.contentForm = new FormGroup(controls);
this.enableContentForm();
}
private populateContentForm() {
private reloadContentForm(content: ContentDto) {
this.content = content;
this.contentForm.markAsPristine();
this.isNewMode = !this.content;
@ -257,14 +275,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
if (!this.isNewMode) {
for (const field of this.schema.fields) {
const fieldValue = this.content.data[field.name] || {};
const fieldForm = <FormGroup>this.contentForm.get(field.name);
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.partitioning === 'language') {
if (field.isLocalizable) {
for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]);
}
} else {
fieldForm.controls['iv'].setValue(fieldValue['iv'] === undefined ? null : fieldValue['iv']);
fieldForm.controls[fieldInvariant].setValue(fieldValue[fieldInvariant] === undefined ? null : fieldValue[fieldInvariant]);
}
}
if (this.content.status === 'Archived') {
@ -273,14 +291,16 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
} else {
for (const field of this.schema.fields) {
const defaultValue = field.defaultValue();
if (defaultValue) {
const fieldForm = <FormGroup>this.contentForm.get(field.name);
if (field.partitioning === 'language') {
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.isLocalizable) {
for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(defaultValue);
}
} else {
fieldForm.controls['iv'].setValue(defaultValue);
fieldForm.controls[fieldInvariant].setValue(defaultValue);
}
}
}

9
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -122,9 +122,9 @@
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="contentItems" >
<tbody *ngIf="!isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackBy">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[languageCode]="languageSelected.iso2Code"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
[selected]="isItemSelected(content)"
@ -133,7 +133,8 @@
(publishing)="publishContent(content)"
(archiving)="archiveContent(content)"
(restoring)="restoreContent(content)"
(deleting)="deleteContent(content)"></tr>
(deleting)="deleteContent(content)"
(saved)="onContentSaved(content, $event)"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
@ -141,7 +142,7 @@
<tbody *ngIf="isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)"
[languageCode]="languageSelected.iso2Code"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
isReadOnly="true"></tr>

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

@ -27,7 +27,8 @@ import {
ImmutableArray,
ModalView,
Pager,
SchemaDetailsDto
SchemaDetailsDto,
Versioned
} from 'shared';
@Component({
@ -203,7 +204,9 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
DateTime.parseISO_UTC(dueTime) :
null;
this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, dt, this.ctx.userToken, dto.version));
content = content.changeStatus(status, dt, this.ctx.userToken, dto.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentStatusChanged(content);
}
@ -240,6 +243,14 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
});
}
public onContentSaved(content: ContentDto, update: Versioned<any>) {
content = content.update(update.payload, this.ctx.userToken, update.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentUpdated(content);
}
public load(showInfo = false) {
this.contentsService.getContents(this.ctx.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, this.isArchive)
.finally(() => {
@ -342,10 +353,18 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.ctx.bus.emit(new ContentStatusChanged(content));
}
private emitContentUpdated(content: ContentDto) {
this.ctx.bus.emit(new ContentUpdated(content));
}
private emitContentRemoved(content: ContentDto) {
this.ctx.bus.emit(new ContentRemoved(content));
}
public trackBy(content: ContentDto): string {
return content.id;
}
private resetContents() {
this.contentItems = ImmutableArray.empty<ContentDto>();
this.contentsQuery = '';

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

@ -1,6 +1,6 @@
<div class="assets-container" [class.disabled]="isDisabled">
<div class="row">
<div class="col-4 drop-area-container" *ngIf="!isDisabled">
<div class="col-4 drop-area-container">
<div class="drop-area" dnd-droppable (onDropSuccess)="onAssetDropped($event.dragData)" [allowDrop]="canDrop()" (sqxFileDrop)="addFiles($event)" routerLink="assets">
Drop files or assets here to add them.
</div>

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

@ -97,7 +97,7 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, O
const component = this;
return (dragData: any) => {
return dragData instanceof AssetDto && !component.oldAssets.find(a => a.id === dragData.id);
return !component.isDisabled && dragData instanceof AssetDto && !component.oldAssets.find(a => a.id === dragData.id);
};
}

79
src/Squidex/app/features/content/shared/content-item.component.html

@ -1,15 +1,61 @@
<td class="cell-select" *ngIf="!isReadOnly">
<input type="checkbox" class="form-control"
<td class="cell-select" *ngIf="!isReadOnly" (click)="shouldStop($event)">
<input type="checkbox" class="form-control"
[ngModel]="selected"
(ngModelChange)="selectedChange.emit($event);"
(click)="$event.stopPropagation()" />
</td>
<td class="cell-auto" *ngFor="let value of values">
<span class="table-cell">
{{value}}
</span>
<td class="cell-auto" *ngFor="let field of schemaFields; let i = index" (click)="shouldStop($event)">
<div *ngIf="field.properties.inlineEditable && !isReadOnly" [formGroup]="form" (click)="$event.stopPropagation()">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="field.name" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!field.properties.inlineEditable || isReadOnly" class="truncate">
{{values[i]}}
</div>
</td>
<td class="cell-time">
<td class="cell-time" (click)="shouldStop($event)">
<span *ngIf="!content.scheduledTo">
<span class="content-status content-status-{{content.status | lowercase}}" #statusIcon>
<i class="icon-circle"></i>
@ -28,10 +74,23 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
<td class="cell-user">
<td class="cell-user" *ngIf="form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-success" (click)="save(); $event.stopPropagation()">
<i class="icon-checkmark"></i>
</button>
</td>
<td class="cell-actions" *ngIf="form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-link btn-secondary btn-cancel" (click)="save(); $event.stopPropagation()">
<i class="icon-close"></i>
</button>
</td>
<td class="cell-user" *ngIf="form.pristine" (click)="shouldStop($event)">
<img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-actions" *ngIf="!isReadOnly">
<td class="cell-actions" *ngIf="!isReadOnly && form.pristine" (click)="shouldStop($event)">
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-link btn-secondary" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
@ -58,7 +117,7 @@
</div>
</div>
</td>
<td class="cell-actions" *ngIf="isReference">
<td class="cell-actions" *ngIf="isReference" (click)="shouldStop($event)">
<button type="button" class="btn btn-link btn-secondary" (click)="deleting.emit(); $event.stopPropagation()">
<i class="icon-close"></i>
</button>

4
src/Squidex/app/features/content/shared/content-item.component.scss

@ -1,6 +1,10 @@
@import '_vars';
@import '_mixins';
.truncate {
@include truncate;
}
.content-status {
& {
vertical-align: middle;

106
src/Squidex/app/features/content/shared/content-item.component.ts

@ -5,15 +5,21 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import {
AppContext,
AppLanguageDto,
ContentDto,
ContentsService,
fadeAnimation,
FieldDto,
fieldInvariant,
ModalView,
SchemaDto
SchemaDto,
Types,
Versioned
} from 'shared';
/* tslint:disable:component-selector */
@ -27,8 +33,7 @@ import {
],
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
]
})
export class ContentItemComponent implements OnInit, OnChanges {
@Output()
@ -46,6 +51,9 @@ export class ContentItemComponent implements OnInit, OnChanges {
@Output()
public deleting = new EventEmitter();
@Output()
public saved = new EventEmitter<Versioned<any>>();
@Output()
public selectedChange = new EventEmitter();
@ -53,10 +61,7 @@ export class ContentItemComponent implements OnInit, OnChanges {
public selected = false;
@Input()
public columnWidth: number;
@Input()
public languageCode: string;
public language: AppLanguageDto;
@Input()
public schemaFields: FieldDto[];
@ -73,11 +78,15 @@ export class ContentItemComponent implements OnInit, OnChanges {
@Input('sqxContent')
public content: ContentDto;
public formSubmitted = false;
public form: FormGroup = new FormGroup({});
public dropdown = new ModalView(false, true);
public values: any[] = [];
constructor(public readonly ctx: AppContext
constructor(public readonly ctx: AppContext,
private readonly contentsService: ContentsService
) {
}
@ -86,31 +95,96 @@ export class ContentItemComponent implements OnInit, OnChanges {
}
public ngOnInit() {
for (let field of this.schemaFields) {
if (field.properties['inlineEditable']) {
this.form.setControl(field.name, new FormControl(undefined, field.createValidators(this.language.isOptional)));
}
}
this.updateValues();
}
public shouldStop(event: Event) {
if (this.form.dirty) {
event.stopPropagation();
event.stopImmediatePropagation();
}
}
public save() {
this.formSubmitted = true;
if (this.form.dirty && this.form.valid) {
this.form.disable();
const request = {};
for (let field of this.schemaFields) {
if (field.properties['inlineEditable']) {
const value = this.form.controls[field.name].value;
if (field.isLocalizable) {
request[field.name] = { [this.language.iso2Code]: value };
} else {
request[field.name] = { iv: value };
}
}
}
this.contentsService.patchContent(this.ctx.appName, this.schema.name, this.content.id, request, this.content.version)
.finally(() => {
this.form.enable();
})
.subscribe(dto => {
this.form.markAsPristine();
this.emitSaved(dto);
}, error => {
this.ctx.notifyError(error);
});
}
}
private emitSaved(data: Versioned<any>) {
this.saved.emit(data);
}
private updateValues() {
this.values = [];
if (this.schemaFields) {
for (let field of this.schemaFields) {
this.values.push(this.getValue(field));
const value = this.getRawValue(field);
if (Types.isUndefined(value)) {
this.values.push('');
} else {
this.values.push(field.formatValue(value));
}
if (this.form) {
const formControl = this.form.controls[field.name];
if (formControl) {
formControl.setValue(value);
}
}
}
}
}
private getValue(field: FieldDto): any {
private getRawValue(field: FieldDto): any {
const contentField = this.content.data[field.name];
if (contentField) {
if (field.partitioning === 'language') {
return field.formatValue(contentField[this.languageCode]);
if (field.isLocalizable) {
return contentField[this.language.iso2Code];
} else {
return field.formatValue(contentField['iv']);
return contentField[fieldInvariant];
}
} else {
return '';
}
return undefined;
}
}

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

@ -13,7 +13,7 @@
<tbody dnd-sortable-container [sortableData]="contentItems.mutableValues">
<ng-template ngFor let-content let-i="index" [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="onContentsSorted($event)"
[languageCode]="languageCode"
[language]="language"
[schemaFields]="contentFields"
[schema]="schema"
(deleting)="onContentRemoving(content)"

2
src/Squidex/app/features/content/shared/references-editor.component.scss

@ -53,7 +53,7 @@
.table {
& {
margin-bottom: -.3rem;
margin-bottom: -.25rem;
margin-top: 1rem;
}

3
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -12,6 +12,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AppContext,
AppLanguageDto,
ContentDto,
ContentsService,
FieldDto,
@ -43,7 +44,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public schemaId: string;
@Input()
public languageCode: string;
public language: AppLanguageDto;
public schema: SchemaDetailsDto;

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

@ -136,7 +136,7 @@
&-left {
min-width: 2.8rem;
max-width: 2.8rem;
margin-top: .3rem;
margin-top: .25rem;
}
&-created {

1
src/Squidex/app/features/rules/declarations.ts

@ -7,6 +7,7 @@
export * from './pages/rules/actions/algolia-action.component';
export * from './pages/rules/actions/azure-queue-action.component';
export * from './pages/rules/actions/elastic-search-action.component';
export * from './pages/rules/actions/fastly-action.component';
export * from './pages/rules/actions/slack-action.component';
export * from './pages/rules/actions/webhook-action.component';

2
src/Squidex/app/features/rules/module.ts

@ -19,6 +19,7 @@ import {
AssetChangedTriggerComponent,
AzureQueueActionComponent,
ContentChangedTriggerComponent,
ElasticSearchActionComponent,
FastlyActionComponent,
RuleEventsPageComponent,
RulesPageComponent,
@ -58,6 +59,7 @@ const routes: Routes = [
AssetChangedTriggerComponent,
AzureQueueActionComponent,
ContentChangedTriggerComponent,
ElasticSearchActionComponent,
FastlyActionComponent,
RuleEventsPageComponent,
RulesPageComponent,

2
src/Squidex/app/features/rules/pages/events/rule-events-page.component.scss

@ -35,7 +35,7 @@ h3 {
&::before {
@include caret-top($color-border);
@include absolute(-1.1rem, 2.5rem, auto, auto);
@include absolute(-1.1rem, 1.8rem, auto, auto);
}
h3 {

2
src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html

@ -5,7 +5,7 @@
<label class="col col-3 col-form-label" for="appId">App ID</label>
<div class="col col-9">
<sqx-control-errors for="text" [submitted]="actionFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="appId" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="appId" formControlName="appId" />

18
src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts

@ -24,17 +24,17 @@ export class AlgoliaActionComponent implements OnInit {
public actionForm =
this.formBuilder.group({
appId: ['',
[
Validators.required
]],
[
Validators.required
]],
apiKey: ['',
[
Validators.required
]],
[
Validators.required
]],
indexName: ['$SCHEMA_NAME',
[
Validators.required
]]
[
Validators.required
]]
});
constructor(

2
src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html

@ -5,7 +5,7 @@
<label class="col col-3 col-form-label" for="connectionString">Connection String</label>
<div class="col col-9">
<sqx-control-errors for="text" [submitted]="actionFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="connectionString" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="connectionString" formControlName="connectionString" />

14
src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts

@ -26,14 +26,14 @@ export class AzureQueueActionComponent implements OnInit {
public actionForm =
this.formBuilder.group({
connectionString: ['',
[
Validators.required
]],
[
Validators.required
]],
queue: ['squidex',
[
Validators.required,
ValidatorsEx.pattern('[a-z][a-z0-9]{2,}(\-[a-z0-9]+)*', 'Name must be a valid azure queue name.')
]]
[
Validators.required,
ValidatorsEx.pattern('[a-z][a-z0-9]{2,}(\-[a-z0-9]+)*', 'Name must be a valid azure queue name.')
]]
});
constructor(

73
src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html

@ -0,0 +1,73 @@
<h3 class="rule-title">Populate index in ElasticSearch with content</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">
<label class="col col-3 col-form-label" for="host">Host</label>
<div class="col col-9">
<sqx-control-errors for="host" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="host" formControlName="host" placeholder="http://localhost:9200" />
<small class="form-text text-muted">
The url to your elastic search instance.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="username">Username</label>
<div class="col col-9">
<sqx-control-errors for="username" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="username" formControlName="username" />
<small class="form-text text-muted">
The username for authentication. Highly recommended.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="username">Password</label>
<div class="col col-9">
<sqx-control-errors for="password" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="password" formControlName="password" />
<small class="form-text text-muted">
The password for authentication. Highly recommended.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="sharedSecret">Index Name</label>
<div class="col col-9">
<sqx-control-errors for="indexName" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="indexName" formControlName="indexName" />
<small class="form-text text-muted">
The name of the index. You can use advanced formatting (read help section).
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="sharedSecret">Type Name</label>
<div class="col col-9">
<sqx-control-errors for="indexType" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="indexType" formControlName="indexType" />
<small class="form-text text-muted">
The name of the type. You can use advanced formatting (read help section).
</small>
</div>
</div>
</form>

2
src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

70
src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts

@ -0,0 +1,70 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'sqx-elastic-search-action',
styleUrls: ['./elastic-search-action.component.scss'],
templateUrl: './elastic-search-action.component.html'
})
export class ElasticSearchActionComponent implements OnInit {
@Input()
public action: any;
@Output()
public actionChanged = new EventEmitter<object>();
public actionFormSubmitted = false;
public actionForm =
this.formBuilder.group({
host: ['',
[
Validators.required
]],
indexName: ['$APP_NAME',
[
Validators.required
]],
indexType: ['$SCHEMA_NAME',
[
// Validators.required
]],
username: '',
password: ''
});
constructor(
private readonly formBuilder: FormBuilder
) {
}
public ngOnInit() {
this.action = Object.assign({}, {
host: '',
indexName: '$APP_NAME',
indexType: '$SCHEMA_NAME',
username: '',
password: ''
}, this.action || {});
this.actionFormSubmitted = false;
this.actionForm.reset();
this.actionForm.setValue(this.action);
}
public save() {
this.actionFormSubmitted = true;
if (this.actionForm.valid) {
const action = this.actionForm.value;
this.actionChanged.emit(action);
}
}
}

2
src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html

@ -5,7 +5,7 @@
<label class="col col-3 col-form-label" for="serviceId">Service ID</label>
<div class="col col-9">
<sqx-control-errors for="text" [submitted]="actionFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="serviceId" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="serviceId" formControlName="serviceId" />

12
src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts

@ -24,13 +24,13 @@ export class FastlyActionComponent implements OnInit {
public actionForm =
this.formBuilder.group({
serviceId: ['',
[
Validators.required
]],
[
Validators.required
]],
apiKey: ['',
[
Validators.required
]]
[
Validators.required
]]
});
constructor(

6
src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts

@ -24,9 +24,9 @@ export class SlackActionComponent implements OnInit {
public actionForm =
this.formBuilder.group({
webhookUrl: ['',
[
Validators.required
]],
[
Validators.required
]],
text: ['']
});

6
src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts

@ -24,9 +24,9 @@ export class WebhookActionComponent implements OnInit {
public actionForm =
this.formBuilder.group({
url: ['',
[
Validators.required
]],
[
Validators.required
]],
sharedSecret: ['']
});

6
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -80,6 +80,12 @@
(actionChanged)="selectAction($event)">
</sqx-azure-queue-action>
</div>
<div *ngSwitchCase="'ElasticSearch'">
<sqx-elastic-search-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-elastic-search-action>
</div>
<div *ngSwitchCase="'Fastly'">
<sqx-fastly-action #actionControl
[action]="action"

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

@ -60,7 +60,7 @@
</tr>
</table>
<div class="section" *ngIf="schemasToAdd.length > 0">
<div class="section" *ngIf="!handleAll && schemasToAdd.length > 0">
<form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1">
<select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema">

4
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.scss

@ -7,6 +7,10 @@
padding-bottom: 0;
}
.form-check {
margin-top: 1rem;
}
.rotated-label {
@include rotate(-45deg);
}

2
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -6,7 +6,7 @@
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i>
<span [class.field-hidden]="field.isHidden" [attr.title]="field.isHidden ? 'Hidden Field' : 'Visible Field'">{{displayName}}</span>
<span class="field-partitioning" *ngIf="field.partitioning === 'language'">localizable</span>
<span class="field-partitioning" *ngIf="field.isLocalizable">localizable</span>
</span>
</div>
<div class="col col-tags">

8
src/Squidex/app/features/schemas/pages/schema/field.component.scss

@ -4,7 +4,13 @@
$field-header: #e7ebef;
.table-items-row-details {
cursor: default;
& {
cursor: default;
}
&::before {
right: 4.6rem;
}
}
.col {

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

@ -13,6 +13,7 @@ import {
createProperties,
fadeAnimation,
FieldDto,
FieldPropertiesDto,
ModalView,
SchemaDto
} from 'shared';
@ -45,7 +46,7 @@ export class FieldComponent implements OnInit {
public showing = new EventEmitter();
@Output()
public saving = new EventEmitter();
public saving = new EventEmitter<FieldPropertiesDto>();
@Output()
public enabling = new EventEmitter();
@ -107,22 +108,12 @@ export class FieldComponent implements OnInit {
if (this.editForm.valid) {
const properties = createProperties(this.field.properties['fieldType'], this.editForm.value);
const field =
new FieldDto(
this.field.fieldId,
this.field.name,
this.field.isLocked,
this.field.isHidden,
this.field.isHidden,
this.field.partitioning,
properties);
this.emitSaving(field);
this.emitSaving(properties);
}
}
private emitSaving(field: FieldDto) {
this.saving.emit(field);
private emitSaving(properties: FieldPropertiesDto) {
this.saving.emit(properties);
}
private resetEditForm() {

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

@ -69,7 +69,7 @@
(locking)="lockField(field)"
(showing)="showField(field)"
(hiding)="hideField(field)"
(saving)="saveField($event)"></sqx-field>
(saving)="saveField(field, $event)"></sqx-field>
</div>
<div class="table-items-footer">

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

@ -25,7 +25,7 @@
color: $color-border-dark;
font-size: .9rem;
font-weight: normal;
padding: .6rem .3rem;
padding: .6rem .25rem;
border: 0;
background: transparent;
vertical-align: baseline;

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

@ -18,6 +18,7 @@ import {
createProperties,
fadeAnimation,
FieldDto,
FieldPropertiesDto,
fieldTypes,
HistoryChannelUpdated,
ImmutableArray,
@ -213,12 +214,12 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
});
}
public saveField(field: FieldDto) {
const requestDto = new UpdateFieldDto(field.properties);
public saveField(field: FieldDto, properties: FieldPropertiesDto) {
const requestDto = new UpdateFieldDto(properties);
this.schemasService.putField(this.ctx.appName, this.schema.name, field.fieldId, requestDto, this.schema.version)
.subscribe(dto => {
this.updateSchema(this.schema.updateField(field, this.ctx.userToken, dto.version));
this.updateSchema(this.schema.updateField(field.update(properties), this.ctx.userToken, dto.version));
}, error => {
this.ctx.notifyError(error);
});

10
src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html

@ -30,4 +30,14 @@
</label>
</div>
</div>
<div class="form-group row">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldInlineEditable" formControlName="inlineEditable" />
<label class="form-check-label" for="fieldInlineEditable">
Inline Editable
</label>
</div>
</div>
</div>
</div>

8
src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts

@ -23,13 +23,17 @@ export class BooleanUIComponent implements OnInit {
public properties: BooleanFieldPropertiesDto;
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
this.editForm.addControl('placeholder',
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable));
}
}

13
src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html

@ -10,11 +10,14 @@
</div>
</div>
<div class="form-group row" *ngIf="showDefaultValue | async">
<label class="col col-3 col-form-checkbox-label" for="fieldDefaultValue">Default Value</label>
<div class="col col-6">
<input class="form-check-input" type="checkbox" id="fieldDefaultValue" formControlName="defaultValue" sqxIndeterminateValue />
<div class="form-group row">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldDefaultValue" formControlName="defaultValue" sqxIndeterminateValue />
<label class="form-check-label" for="fieldDefaultValue">
Default Value
</label>
</div>
</div>
</div>
</div>

5
src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts

@ -26,9 +26,12 @@ export class BooleanValidationComponent implements OnInit {
public showDefaultValue: Observable<boolean>;
public ngOnInit() {
this.editForm.addControl('defaultValue',
this.editForm.setControl('defaultValue',
new FormControl(this.properties.defaultValue));
this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable));
this.showDefaultValue =
this.editForm.controls['isRequired'].valueChanges
.startWith(this.properties.isRequired)

5
src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts

@ -28,11 +28,12 @@ export class DateTimeUIComponent implements OnInit {
public hideAllowedValues: Observable<boolean>;
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
this.editForm.addControl('placeholder',
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));

8
src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts

@ -29,20 +29,20 @@ export class DateTimeValidationComponent implements OnInit {
public calculatedDefaultValues = ['Now', 'Today'];
public ngOnInit() {
this.editForm.addControl('calculatedDefaultValue',
this.editForm.setControl('calculatedDefaultValue',
new FormControl(this.properties.calculatedDefaultValue));
this.editForm.addControl('maxValue',
this.editForm.setControl('maxValue',
new FormControl(this.properties.maxValue, [
ValidatorsEx.validDateTime()
]));
this.editForm.addControl('minValue',
this.editForm.setControl('minValue',
new FormControl(this.properties.minValue, [
ValidatorsEx.validDateTime()
]));
this.editForm.addControl('defaultValue',
this.editForm.setControl('defaultValue',
new FormControl(this.properties.defaultValue, [
ValidatorsEx.validDateTime()
]));

2
src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts

@ -23,7 +23,7 @@ export class GeolocationUIComponent implements OnInit {
public properties: GeolocationFieldPropertiesDto;
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));

10
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html

@ -51,4 +51,14 @@
<sqx-tag-editor formControlName="allowedValues" [converter]="converter"></sqx-tag-editor>
</div>
</div>
<div class="form-group row" [class.hidden]="hideInlineEditable | async">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldInlineEditable" formControlName="inlineEditable" />
<label class="form-check-label" for="fieldInlineEditable">
Inline Editable
</label>
</div>
</div>
</div>
</div>

34
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts

@ -17,7 +17,8 @@ import { FloatConverter, NumberFieldPropertiesDto } from 'shared';
templateUrl: 'number-ui.component.html'
})
export class NumberUIComponent implements OnDestroy, OnInit {
private editorSubscription: Subscription;
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
@Input()
public editForm: FormGroup;
@ -28,33 +29,52 @@ export class NumberUIComponent implements OnDestroy, OnInit {
public converter = new FloatConverter();
public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.editorSubscription.unsubscribe();
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() {
this.editForm.addControl('editor',
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
Validators.required
]));
this.editForm.addControl('placeholder',
this.editForm.setControl('placeholder',
new FormControl(this.properties.placeholder, [
Validators.maxLength(100)
]));
this.editForm.addControl('allowedValues',
this.editForm.setControl('allowedValues',
new FormControl(this.properties.allowedValues, []));
this.editForm.setControl('inlineEditable',
new FormControl(this.properties.inlineEditable));
this.hideAllowedValues =
this.editForm.controls['editor'].valueChanges
.startWith(this.properties.editor)
.map(x => !x || x === 'Input' || x === 'Textarea');
.map(x => !(x && (x === 'Radio' || x === 'Dropdown')));
this.hideInlineEditable =
this.editForm.controls['editor'].valueChanges
.startWith(this.properties.editor)
.map(x => !(x && (x === 'Input' || x === 'Dropdown')));
this.editorSubscription =
this.hideAllowedValuesSubscription =
this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
this.hideInlineEditableSubscription =
this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false);
}
});
}
}

6
src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts

@ -26,13 +26,13 @@ export class NumberValidationComponent implements OnInit {
public showDefaultValue: Observable<boolean>;
public ngOnInit() {
this.editForm.addControl('maxValue',
this.editForm.setControl('maxValue',
new FormControl(this.properties.maxValue));
this.editForm.addControl('minValue',
this.editForm.setControl('minValue',
new FormControl(this.properties.minValue));
this.editForm.addControl('defaultValue',
this.editForm.setControl('defaultValue',
new FormControl(this.properties.defaultValue));
this.showDefaultValue =

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

Loading…
Cancel
Save