From 11d1eedf885437a400c48a79aca6951037f46133 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 24 Aug 2018 14:29:57 +0200 Subject: [PATCH] Started with twitter integration. --- .../Rules/Actions/TweetAction.cs | 24 + .../Rules/Actions/TwitterOptions.cs | 21 + .../Rules/IRuleActionVisitor.cs | 2 + .../HandleRules/Actions/TweetActionHandler.cs | 74 + .../HandleRules/ClientPool.cs | 15 +- ...Squidex.Domain.Apps.Core.Operations.csproj | 1 + .../Rules/Guards/RuleActionValidator.cs | 12 + src/Squidex/AppServices.cs | 7 +- .../Contents/ContentsController.cs | 4 +- ...ions.cs => MyContentsControllerOptions.cs} | 2 +- .../Rules/Models/Actions/TweetActionDto.cs | 35 + .../Models/Converters/RuleActionDtoFactory.cs | 5 + .../Controllers/Rules/TwitterController.cs | 33 + .../Controllers/UI/Models/UISettingsDto.cs | 5 + .../Areas/Api/Controllers/UI/UIController.cs | 9 +- src/Squidex/Config/Domain/RuleServices.cs | 3 + .../app/features/rules/declarations.ts | 3 + src/Squidex/app/features/rules/module.ts | 2 + .../rules/actions/tweet-action.component.html | 31 + .../rules/actions/tweet-action.component.scss | 2 + .../rules/actions/tweet-action.component.ts | 37 + .../pages/rules/rule-wizard.component.html | 7 + .../app/shared/services/rules.service.ts | 3 + src/Squidex/app/theme/_rules.scss | 5 + .../app/theme/icomoon/demo-files/demo.css | 8 +- src/Squidex/app/theme/icomoon/demo.html | 1230 ++++++---- .../app/theme/icomoon/fonts/icomoon.eot | Bin 25320 -> 25568 bytes .../app/theme/icomoon/fonts/icomoon.svg | 1 + .../app/theme/icomoon/fonts/icomoon.ttf | Bin 25156 -> 25404 bytes .../app/theme/icomoon/fonts/icomoon.woff | Bin 25232 -> 25480 bytes src/Squidex/app/theme/icomoon/selection.json | 2174 +++++++++-------- src/Squidex/app/theme/icomoon/style.css | 157 +- src/Squidex/appsettings.json | 15 +- .../Rules/Guards/Actions/TweetActionTests.cs | 43 + 34 files changed, 2353 insertions(+), 1617 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TwitterOptions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs rename src/Squidex/Areas/Api/Controllers/Contents/{ContentsControllerOptions.cs => MyContentsControllerOptions.cs} (91%) create mode 100644 src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs create mode 100644 src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss create mode 100644 src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/TweetActionTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs new file mode 100644 index 000000000..749f62b96 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Actions +{ + [TypeName(nameof(TweetAction))] + public sealed class TweetAction : RuleAction + { + public string PinCode { get; set; } + + public string Text { get; set; } + + public override T Accept(IRuleActionVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TwitterOptions.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TwitterOptions.cs new file mode 100644 index 000000000..d2059d36f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TwitterOptions.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Rules.Actions +{ + public sealed class TwitterOptions + { + public string ClientId { get; set; } + + public string ClientSecret { get; set; } + + public bool IsConfigured() + { + return !string.IsNullOrWhiteSpace(ClientId) && !string.IsNullOrWhiteSpace(ClientSecret); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs index dc1929cb7..c331d0938 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Core.Rules T Visit(SlackAction action); + T Visit(TweetAction action); + T Visit(WebhookAction action); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs new file mode 100644 index 000000000..d056a39e6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using CoreTweet; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Infrastructure; + +#pragma warning disable SA1649 // File name must match first type name + +namespace Squidex.Domain.Apps.Core.HandleRules.Actions +{ + public sealed class TweetJob + { + public string PinCode { get; set; } + + public string Text { get; set; } + } + + public sealed class TweetActionHandler : RuleActionHandler + { + private const string Description = "Send a tweet"; + + private readonly RuleEventFormatter formatter; + private readonly ClientPool tokenPool; + + public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions) + { + Guard.NotNull(formatter, nameof(formatter)); + Guard.NotNull(twitterOptions, nameof(twitterOptions)); + + this.formatter = formatter; + + tokenPool = new ClientPool(async key => + { + var session = await OAuth.AuthorizeAsync(twitterOptions.Value.ClientId, twitterOptions.Value.ClientSecret); + + return await session.GetTokensAsync(key); + }); + } + + protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) + { + var text = formatter.Format(action.Text, @event); + + var ruleJob = new TweetJob { Text = text, PinCode = action.PinCode }; + + return (Description, ruleJob); + } + + protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(TweetJob job) + { + try + { + var tokens = await tokenPool.GetClientAsync(job.PinCode); + + var response = await tokens.Statuses.UpdateAsync(x => job.Text); + + return ($"Tweeted: {job.Text}", null); + } + catch (Exception ex) + { + return (ex.Message, ex); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs index b126e4bf3..b93a45a25 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -17,18 +18,28 @@ namespace Squidex.Domain.Apps.Core.HandleRules { private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30); private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly Func factory; + private readonly Func> factory; public ClientPool(Func factory) + { + this.factory = x => Task.FromResult(factory(x)); + } + + public ClientPool(Func> factory) { this.factory = factory; } public TClient GetClient(TKey key) + { + return GetClientAsync(key).Result; + } + + public async Task GetClientAsync(TKey key) { if (!memoryCache.TryGetValue(key, out var client)) { - client = factory(key); + client = await factory(key); memoryCache.Set(key, client, TTL); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 84cb6b779..62d82c371 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs index 3aadcc635..7c42f042f 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs @@ -129,6 +129,18 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards return Task.FromResult>(errors); } + public Task> Visit(TweetAction action) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(action.PinCode)) + { + errors.Add(new ValidationError("Pin Code is required.", nameof(action.PinCode))); + } + + return Task.FromResult>(errors); + } + public Task> Visit(SlackAction action) { var errors = new List(); diff --git a/src/Squidex/AppServices.cs b/src/Squidex/AppServices.cs index 300255c57..380c4df53 100644 --- a/src/Squidex/AppServices.cs +++ b/src/Squidex/AppServices.cs @@ -14,6 +14,7 @@ using Squidex.Config; using Squidex.Config.Authentication; using Squidex.Config.Domain; using Squidex.Config.Web; +using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Infrastructure.Commands; namespace Squidex @@ -44,7 +45,11 @@ namespace Squidex services.Configure( config.GetSection("mode")); - services.Configure( + + services.Configure( + config.GetSection("twitter")); + + services.Configure( config.GetSection("contentsController")); services.Configure( config.GetSection("urls")); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index bc88ae58b..90ffe3948 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -30,14 +30,14 @@ namespace Squidex.Areas.Api.Controllers.Contents [SwaggerIgnore] public sealed class ContentsController : ApiController { - private readonly IOptions controllerOptions; + private readonly IOptions controllerOptions; private readonly IContentQueryService contentQuery; private readonly IGraphQLService graphQl; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IGraphQLService graphQl, - IOptions controllerOptions) + IOptions controllerOptions) : base(commandBus) { this.contentQuery = contentQuery; diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsControllerOptions.cs b/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs similarity index 91% rename from src/Squidex/Areas/Api/Controllers/Contents/ContentsControllerOptions.cs rename to src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs index 95d210a18..bf0cd13af 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsControllerOptions.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs @@ -7,7 +7,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { - public sealed class ContentsControllerOptions + public sealed class MyContentsControllerOptions { public bool EnableSurrogateKeys { get; set; } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs new file mode 100644 index 000000000..4f252d962 --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +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("Tweet")] + public sealed class TweetActionDto : RuleActionDto + { + /// + /// The pin code. + /// + [Required] + public string PinCode { get; set; } + + /// + /// The text that is sent as tweet to twitter. + /// + public string Text { get; set; } + + public override RuleAction ToAction() + { + return SimpleMapper.Map(this, new TweetAction()); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs index 664d3f352..4207aed35 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs @@ -55,6 +55,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters return SimpleMapper.Map(action, new SlackActionDto()); } + public RuleActionDto Visit(TweetAction action) + { + return SimpleMapper.Map(action, new TweetActionDto()); + } + public RuleActionDto Visit(WebhookAction action) { return SimpleMapper.Map(action, new WebhookActionDto()); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs b/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs new file mode 100644 index 000000000..32061a55e --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using CoreTweet; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Rules.Actions; + +namespace Squidex.Areas.Api.Controllers.Rules +{ + public sealed class TwitterController : Controller + { + private readonly TwitterOptions twitterOptions; + + public TwitterController(IOptions twitterOptions) + { + this.twitterOptions = twitterOptions.Value; + } + + [Route("rules/twitter/auth")] + public async Task Auth() + { + var session = await OAuth.AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret); + + return Redirect(session.AuthorizeUri.ToString()); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs index c2770c86b..a78188b4e 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs @@ -22,5 +22,10 @@ namespace Squidex.Areas.Api.Controllers.UI.Models /// [Required] public string MapKey { get; set; } + + /// + /// Indicates whether twitter actions are supported. + /// + public bool SupportsTwitterActions { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 2c87e8624..daa152b9c 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Options; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.UI.Models; using Squidex.Config; +using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; @@ -23,10 +24,13 @@ namespace Squidex.Areas.Api.Controllers.UI public sealed class UIController : ApiController { private readonly MyUIOptions uiOptions; + private readonly TwitterOptions twitterOptions; - public UIController(ICommandBus commandBus, IOptions uiOptions) + public UIController(ICommandBus commandBus, IOptions uiOptions, IOptions twitterOptions) : base(commandBus) { + this.twitterOptions = twitterOptions.Value; + this.uiOptions = uiOptions.Value; } @@ -42,7 +46,8 @@ namespace Squidex.Areas.Api.Controllers.UI var dto = new UISettingsDto { MapType = uiOptions.Map?.Type ?? "OSM", - MapKey = uiOptions.Map?.GoogleMaps?.Key + MapKey = uiOptions.Map?.GoogleMaps?.Key, + SupportsTwitterActions = twitterOptions.IsConfigured() }; return Ok(dto); diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index 3a9ae24d1..90cdfb1a1 100644 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ b/src/Squidex/Config/Domain/RuleServices.cs @@ -42,6 +42,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index d3e0a1722..fcfbc44d8 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -11,9 +11,12 @@ export * from './pages/rules/actions/elastic-search-action.component'; export * from './pages/rules/actions/fastly-action.component'; export * from './pages/rules/actions/medium-action.component'; export * from './pages/rules/actions/slack-action.component'; +export * from './pages/rules/actions/tweet-action.component'; export * from './pages/rules/actions/webhook-action.component'; + export * from './pages/rules/triggers/asset-changed-trigger.component'; export * from './pages/rules/triggers/content-changed-trigger.component'; + export * from './pages/rules/rule-wizard.component'; export * from './pages/rules/rules-page.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 3111c2490..fb61fa0d7 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -27,6 +27,7 @@ import { RulesPageComponent, RuleWizardComponent, SlackActionComponent, + TweetActionComponent, WebhookActionComponent } from './declarations'; @@ -69,6 +70,7 @@ const routes: Routes = [ RulesPageComponent, RuleWizardComponent, SlackActionComponent, + TweetActionComponent, WebhookActionComponent ] }) diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html new file mode 100644 index 000000000..37d7ff054 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html @@ -0,0 +1,31 @@ +

Tweet a status update to your twitter feed

+ +
+
+ + +
+ + + + + + The pin code to authorize. Follow this link and add the pin code: Login to twitter. + +
+
+ +
+ + +
+ + + + + + The text to tweet. Read the help section for information about advanced formatting. + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts new file mode 100644 index 000000000..947239970 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts @@ -0,0 +1,37 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +@Component({ + selector: 'sqx-tweet-action', + styleUrls: ['./tweet-action.component.scss'], + templateUrl: './tweet-action.component.html' +}) +export class TweetActionComponent implements OnInit { + @Input() + public action: any; + + @Input() + public actionForm: FormGroup; + + @Input() + public actionFormSubmitted = false; + + public ngOnInit() { + this.actionForm.setControl('pinCode', + new FormControl(this.action.pinCode || '', [ + Validators.required + ])); + + this.actionForm.setControl('text', + new FormControl(this.action.text || '', [ + Validators.required + ])); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html index d0b979c1f..201ef2672 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -114,6 +114,13 @@ [actionFormSubmitted]="actionForm.submitted | async"> + + + +
-

Font Name: icomoon (Glyphs: 95)

+

Font Name: icomoon (Glyphs: 96)

-

Grid Size: 24

+

Grid Size: 16

- + - icon-backup + icon-twitter
- - + +
liga: @@ -31,14 +31,14 @@
- + - icon-support + icon-action-Tweet
- - + +
liga: @@ -47,14 +47,14 @@
- + - icon-control-RichText + icon-hour-glass
- - + +
liga: @@ -63,257 +63,257 @@
- + - icon-download + icon-spinner
- - + +
liga:
-
-
-

Grid Size: Unknown

-
+
- + - icon-action-Medium + icon-clock
- - + +
liga:
-
+
- + - icon-circle + icon-bin2
- - + +
liga:
-
+
- + - icon-action-Fastly + icon-earth
- - + +
liga: - +
-
+
- + - icon-control-Slug + icon-elapsed
- - + +
liga:
-
+
- + - icon-action-Algolia + icon-google
- - + +
liga:
-
+
- + - icon-type-Tags + icon-lock
- - + +
liga:
-
+
- + - icon-activity + icon-microsoft
- - + +
liga:
-
+
- + - icon-history + icon-action-AzureQueue
- - + +
liga:
-
+
- + - icon-time + icon-pause
- - + +
liga:
-
+
- + - icon-add + icon-play
- - + +
liga:
-
+
- + - icon-plus + icon-reset
- - + +
liga:
-
+
- + - icon-check-circle + icon-settings2
- - + +
liga:
-
+
- + - icon-check-circle-filled + icon-timeout
- - + +
liga:
-
+
- + - icon-close + icon-unlocked
- - + +
liga:
+
+
+

Grid Size: 24

- + - icon-type-References + icon-backup
- - + +
liga: @@ -322,14 +322,14 @@
- + - icon-control-Checkbox + icon-support
- - + +
liga: @@ -338,14 +338,14 @@
- + - icon-control-Dropdown + icon-control-RichText
- - + +
liga: @@ -354,14 +354,14 @@
- + - icon-control-Input + icon-download
- - + +
liga: @@ -370,14 +370,14 @@
- + - icon-control-Radio + icon-action-Algolia
- - + +
liga: @@ -386,14 +386,14 @@
- + - icon-control-TextArea + icon-type-Tags
- - + +
liga: @@ -402,14 +402,14 @@
- + - icon-control-Toggle + icon-activity
- - + +
liga: @@ -418,14 +418,14 @@
- + - icon-copy + icon-history
- - + +
liga: @@ -434,14 +434,14 @@
- + - icon-dashboard + icon-time
- - + +
liga: @@ -450,14 +450,14 @@
- + - icon-delete + icon-add
- - + +
liga: @@ -466,14 +466,14 @@
- + - icon-bin + icon-plus
- - + +
liga: @@ -482,14 +482,14 @@
- + - icon-delete-filled + icon-check-circle
- - + +
liga: @@ -498,14 +498,14 @@
- + - icon-document-delete + icon-check-circle-filled
- - + +
liga: @@ -514,14 +514,14 @@
- + - icon-document-disable + icon-close
- - + +
liga: @@ -530,14 +530,14 @@
- + - icon-document-publish + icon-type-References
- - + +
liga: @@ -546,14 +546,14 @@
- + - icon-drag + icon-control-Checkbox
- - + +
liga: @@ -562,14 +562,14 @@
- + - icon-filter + icon-control-Dropdown
- - + +
liga: @@ -578,14 +578,14 @@
- + - icon-github + icon-control-Input
- - + +
liga: @@ -594,369 +594,641 @@
- + - icon-help + icon-control-Radio
- - + +
liga:
-
+
+
+

Grid Size: Unknown

+
- + - icon-location + icon-action-Medium
- - + +
liga:
-
+
- + - icon-control-Map + icon-circle
- - + +
liga:
-
+
- + - icon-type-Geolocation + icon-action-Fastly
- - + +
liga:
-
+
-
- - + +
liga:
-
+
- + - icon-media + icon-action-Algolia
- - + +
liga:
-
+
- + - icon-type-Assets + icon-type-Tags
- - + +
liga:
-
+
- + - icon-trigger-AssetChanged + icon-activity
- - + +
liga:
-
+
- + - icon-more + icon-history
- - + +
liga:
-
+
- + - icon-dots + icon-time
- - + +
liga:
-
+
- + - icon-pencil + icon-add
- - + +
liga:
-
+
- + - icon-reference + icon-plus
- - + + +
+
+ liga: + +
+
+
+
+ + + + icon-check-circle +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-check-circle-filled +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-close +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-References +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Checkbox +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Dropdown +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Input +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Radio +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-TextArea +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Toggle +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-copy +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-dashboard +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-delete +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-bin +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-delete-filled +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-document-delete +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-document-disable +
+
+ +
liga:
-
+
- + - icon-schemas + icon-document-publish
- - + +
liga:
-
+
- + - icon-search + icon-drag
- - + +
liga:
-
+
- + - icon-settings + icon-filter
- - + +
liga:
-
+
- + - icon-type-Boolean + icon-github
- - + +
liga:
-
+
- + - icon-type-DateTime + icon-help
- - + +
liga:
-
+
- + - icon-type-Json + icon-location
- - + +
liga:
-
+
- + - icon-json + icon-control-Map
- - + +
liga:
-
+
- + - icon-type-Number + icon-type-Geolocation
- - + +
liga:
-
+
- + - icon-type-String + icon-logo
- - + +
liga:
-
+
- + - icon-user + icon-media
- - + +
liga:
-
-
-

Grid Size: 14

- + - icon-single-content + icon-type-Assets
- - + +
liga: @@ -965,14 +1237,14 @@
- + - icon-multiple-content + icon-trigger-AssetChanged
- - + +
liga: @@ -981,14 +1253,14 @@
- + - icon-type-Array + icon-more
- - + +
liga: @@ -997,14 +1269,14 @@
- + - icon-exclamation + icon-dots
- - + +
liga: @@ -1013,14 +1285,14 @@
- + - icon-action-ElasticSearch + icon-pencil
- - + +
liga: @@ -1029,14 +1301,14 @@
- + - icon-action-Slack + icon-reference
- - + +
liga: @@ -1045,14 +1317,14 @@
- + - icon-orleans + icon-schemas
- - + +
liga: @@ -1061,14 +1333,14 @@
- + - icon-document-lock + icon-search
- - + +
liga: @@ -1077,14 +1349,14 @@
- + - icon-document-unpublish + icon-settings
- - + +
liga: @@ -1093,14 +1365,14 @@
- + - icon-angle-down + icon-type-Boolean
- - + +
liga: @@ -1109,14 +1381,14 @@
- + - icon-angle-left + icon-type-DateTime
- - + +
liga: @@ -1125,14 +1397,14 @@
- + - icon-angle-right + icon-type-Json
- - + +
liga: @@ -1141,14 +1413,14 @@
- + - icon-angle-up + icon-json
- - + +
liga: @@ -1157,14 +1429,14 @@
- + - icon-api + icon-type-Number
- - + +
liga: @@ -1173,14 +1445,14 @@
- + - icon-assets + icon-type-String
- - + +
liga: @@ -1189,257 +1461,257 @@
- + - icon-bug + icon-user
- - + +
liga:
-
+
+
+

Grid Size: 14

+
- + - icon-caret-down + icon-single-content
- - + +
liga:
-
+
- + - icon-caret-left + icon-multiple-content
-
- - +
+ +
liga:
-
+
- + - icon-caret-right + icon-type-Array
- - + +
liga:
-
+
- + - icon-caret-up + icon-exclamation
- - + +
liga:
-
+
- + - icon-contents + icon-action-ElasticSearch
- - + +
liga:
-
+
- + - icon-trigger-ContentChanged + icon-action-Slack
- - + +
liga:
-
+
- + - icon-control-Date + icon-orleans
- - + +
liga:
-
+
- + - icon-control-DateTime + icon-document-lock
- - + +
liga:
-
+
- + - icon-control-Markdown + icon-document-unpublish
- - + +
liga:
-
+
- + - icon-grid + icon-angle-down
- - + +
liga:
-
+
- + - icon-list + icon-angle-left
- - + +
liga:
-
+
- + - icon-user-o + icon-angle-right
- - + +
liga:
-
+
- + - icon-rules + icon-angle-up
- - + +
liga:
-
+
- + - icon-action-Webhook + icon-api
- - + +
liga:
-
-
-

Grid Size: 16

- + - icon-hour-glass + icon-assets
- - + +
liga: @@ -1448,14 +1720,14 @@
- + - icon-spinner + icon-bug
- - + +
liga: @@ -1464,14 +1736,14 @@
- + - icon-clock + icon-caret-down
- - + +
liga: @@ -1480,14 +1752,14 @@
- + - icon-bin2 + icon-caret-left
- - + +
liga: @@ -1496,30 +1768,30 @@
- + - icon-earth + icon-caret-right
- - + +
liga: - +
- + - icon-elapsed + icon-caret-up
- - + +
liga: @@ -1528,14 +1800,14 @@
- + - icon-google + icon-contents
- - + +
liga: @@ -1544,14 +1816,14 @@
- + - icon-lock + icon-trigger-ContentChanged
- - + +
liga: @@ -1560,14 +1832,14 @@
- + - icon-microsoft + icon-control-Date
- - + +
liga: @@ -1576,14 +1848,14 @@
- + - icon-action-AzureQueue + icon-control-DateTime
- - + +
liga: @@ -1592,14 +1864,14 @@
- + - icon-pause + icon-control-Markdown
- - + +
liga: @@ -1608,14 +1880,14 @@
- + - icon-play + icon-grid
- - + +
liga: @@ -1624,14 +1896,14 @@
- + - icon-reset + icon-list
- - + +
liga: @@ -1640,14 +1912,14 @@
- + - icon-settings2 + icon-user-o
- - + +
liga: @@ -1656,14 +1928,14 @@
- + - icon-timeout + icon-rules
- - + +
liga: @@ -1672,14 +1944,14 @@
- + - icon-unlocked + icon-action-Webhook
- - + +
liga: diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot index 7d637f8990f194cefdf7fb70f9311affd3c82407..8b2611dd795e5672f0adbfcd3298a64dff484e61 100644 GIT binary patch delta 541 zcmaEHl<~oFMz#mZ3=B5O6WPpI`2YKOPBu&wnHZ8RxSN52VGj@|BUxaoYU+H9AP$fz17`36m8hx9F^WSNV&Xuin6Z&L&;T=26E#yH z&B7pKtDtDWBk!yt6QHZDu4XA9$jszuYnNwWpw7#0tR-1*D4OWOE5pwv%q5cTYS!w- zE6Jm*rWUBJturydV1~Lnqr9mo(>-26W?>suAt`n_6#+p@H8mX`2_9xaK}&UY193B1 zvlv-MJ|0F!MxPQs7G@?!yC5bl113f>AwdatHZ$`zS=kxvyzFA;3Lw8SF);i;0QB$y z25E+3hRw#&uFNu?KL7dS`E9;3aI=7vFkD}_Rc1ImNKE`>3IK@6mpfobx^B;n1@iAxy)C=zv- delta 263 zcmaEGobkm`Mz$A83=A$w6WPpIZWko&pXg97xQl^-VGj@|BMc&ODgh~inIXv8bCcg zIr+(n{wGp<7#QNW0Oe2QCRP+MR53;Yt*8O=74i~uQ)hZ{{s!{r0M)k@6 zl2kzQ49v`o$0w&Sb~8p#{=lfrI!z#)f97ThCf>-+qA|hDGW(MXZpZW6d}ZJUS + diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf index c486ad6d3b49d3af49fe608f9197c9cd64652a75..8e3ee27d3ace193f8c178055637906dff213fbf9 100644 GIT binary patch delta 561 zcmX?dgmKR?#(D-u1_lOhh6V;^1_S?KeItG$-rYcvJwTk0oSRs1+x*rW1_nkMAU`KP zvA6(83jp~YK$;^xr!p<$2Om&9;{*nVSe=a2#1!G>Q+pX0;z0V%GJpb{OsuIu{udx$ zB_p?_qG+i|3j;&E1V~R#ezId?omA>1pj|6~3QpxFRunK)Ge$8mB$fdA3VDgSsWZJe ze*^hbfEwBg@{3D=4h4c_kb@K$n3+#ZJkZS;GuekxnRU8A1plnfC5*h0^&n3%urM$? zF!%%Y@iDU7F`5HOJw|0Eb|B3rDk3JXrmn}RuBOh%2;u;lGGGQDP>Gtl9HTgdAtnxF ziWwW30}U`UHBmDK(ku)zwhD>{Jo3&eG6A~U>S~q(g3L^gwsv_22I{>0##)l~hN6ic zyfXY;!dxQRu4b)XyplZ1YHESn+By^S3udURGs>HaGTq}9WEQqj6_R3?QxOofR8!O8 zk>Ft#6tq-VHxM_IHH(pDx<;pDM>GPjIp5Nvx12+pu3B&azE81Z6{1vb7{owo8JH#;CJAqzpRkk>0OFT(*#H0l delta 287 zcmdmUjPb}3#(D-u1_lOhh6V;^1_S?KeItG$-d#YEJwTk0oSRs1+w9g$1_nkMAU`KP zvA6(83jp~YK$;^xr!wt9?Y+G~{sabw7@LgL#1!Fdfkp;~*fT(RvkagBClhN514A4O zkgt-FTT+p?RHOyS*8uA2$;nT4O!PmI+QYyQzXhn^L~de50YepI6wvM(AYUObF*kLl z7w2yve-2PXTS0zt3DBWHkfZ{VXJBSNKJh>|WAtPnMrGD%0^$5KHfY-gE(`>mD9bYCkGCUUxSBC4FEKY2 zDE0uTu?>W0dU5_P$S*Dd`UK=?h&pBW*}k=>5b97yUhDl4%AX*N+2F>y6@Jw|mkbv{NA2gsBGGx&f?)YRn| z#UTtaaUfI7*vK4cfSIX@nkkTGVUV#^P&D9?cUF-J(A8E~vlI|yW^%N(%QG-g=jAun zlB_orP4wWE;pY? zfS{$CnhuWy53``4rMkL-xS6b3j4UG`4(D|S{O)p4q8c-S(b}5_BB^+V|0Ck*qV*mgE delta 343 zcmeA;&N$&Hqe!{Gn;Qco0}v!lVBiMRE|UwB#V2Zu)bC2pO)OwwV9Wpt=Ya5Svs*9I z6N^D&SAcvDC>BW1sZ0Zk-Cs)+wfFXBq$Z{?FvLj!)tG^>aJE2W22c1gO6Ug!7h)wB+O`1J(H@0M#&@0Ac?VsXe)g6+nv~k!)iDEE%*@9p^DuTZMo*r>sLVP|Ae?{Z<^znp zk(>8M2Q$m;Pb#<_&u{aUftv-WmVx1V8_+vYda_QuIb-tXl=u$D$sZDqiRsGe-qMo; X2U#*>1cMk@Armmjgf~A + { + new ValidationError("Pin Code is required.", "PinCode") + }); + } + + [Fact] + public async Task Should_not_add_error_if_pin_code_is_absolute() + { + var action = new TweetAction { PinCode = "123" }; + + var errors = await RuleActionValidator.ValidateAsync(action); + + Assert.Empty(errors); + } + } +}