diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 977cb3a09..38158c0cb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -12,12 +12,15 @@ using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Identity; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.HandleRules @@ -27,8 +30,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules private const string Fallback = "null"; private const string ScriptSuffix = ")"; private const string ScriptPrefix = "Script("; - private static readonly Regex RegexPatternOld = new Regex(@"^(?[^_]*)_(?.*)", RegexOptions.Compiled); - private static readonly Regex RegexPatternNew = new Regex(@"^\{(?[^_]*)_(?.*)\}", RegexOptions.Compiled); + private static readonly Regex RegexPatternOld = new Regex(@"^(?[^_]*)_(?[^\s]*)", RegexOptions.Compiled); + private static readonly Regex RegexPatternNew = new Regex(@"^\{(?[^_]*)_(?[^\s]*)\}", RegexOptions.Compiled); private readonly List<(char[] Pattern, Func Replacer)> patterns = new List<(char[] Pattern, Func Replacer)>(); private readonly IJsonSerializer jsonSerializer; private readonly IUrlGenerator urlGenerator; @@ -324,6 +327,24 @@ namespace Squidex.Domain.Apps.Core.HandleRules } else if (current != null) { + if (current is IUser user) + { + var type = segment; + + if (string.Equals(type, "Name", StringComparison.OrdinalIgnoreCase)) + { + type = SquidexClaimTypes.DisplayName; + } + + var claim = user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); + + if (claim != null) + { + current = claim.Value; + continue; + } + } + const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.Public | diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs index 095edc9c0..b891d86f1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppLimitsPlan.cs @@ -15,10 +15,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans public string Costs { get; set; } + public string? ConfirmText { get; set; } + public string? YearlyCosts { get; set; } public string? YearlyId { get; set; } + public string? YearlyConfirmText { get; set; } + public long BlockingApiCalls { get; set; } public long MaxApiCalls { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs index 1f8245480..e6a349c95 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppLimitsPlan.cs @@ -15,10 +15,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans string Costs { get; } + string? ConfirmText { get; } + string? YearlyCosts { get; } string? YearlyId { get; } + string? YearlyConfirmText { get; } + long BlockingApiCalls { get; } long MaxApiCalls { get; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs index f413c5147..afacb232c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs @@ -31,6 +31,16 @@ namespace Squidex.Areas.Api.Controllers.Plans.Models [Required] public string Costs { get; set; } + /// + /// An optional confirm text for the monthly subscription. + /// + public string? ConfirmText { get; set; } + + /// + /// An optional confirm text for the yearly subscription. + /// + public string? YearlyConfirmText { get; set; } + /// /// The yearly costs of the plan. /// diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 620dc4572..3021e7dc3 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -97,6 +97,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Theory] [InlineData("Name $APP_NAME has id $APP_ID")] + [InlineData("Name ${$EVENT_APPID.NAME} has id ${EVENT_APPID.ID}")] [InlineData("Script(`Name ${event.appId.name} has id ${event.appId.id}`)")] public void Should_format_app_information_from_event(string script) { @@ -120,19 +121,32 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Theory] - [InlineData("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME")] - [InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}, Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")] + [InlineData("Full: $TIMESTAMP_DATETIME")] + [InlineData("Script(`Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")] public void Should_format_timestamp_information_from_event(string script) { var @event = new EnrichedContentEvent { Timestamp = now }; var result = sut.Format(script, @event); - Assert.Equal($"Date: {now:yyyy-MM-dd}, Full: {now:yyyy-MM-dd-hh-mm-ss}", result); + Assert.Equal($"Full: {now:yyyy-MM-dd-hh-mm-ss}", result); + } + + [Theory] + [InlineData("Date: $TIMESTAMP_DATE")] + [InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}`)")] + public void Should_format_timestamp_date_information_from_event(string script) + { + var @event = new EnrichedContentEvent { Timestamp = now }; + + var result = sut.Format(script, @event); + + Assert.Equal($"Date: {now:yyyy-MM-dd}", result); } [Theory] [InlineData("From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)")] + [InlineData("From ${COMMENT_MENTIONEDUSER.NAME} (${COMMENT_MENTIONEDUSER.EMAIL}, ${COMMENT_MENTIONEDUSER.ID})")] [InlineData("Script(`From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})`)")] public void Should_format_email_and_display_name_from_mentioned_user(string script) { diff --git a/frontend/app/features/settings/pages/plans/plan.component.html b/frontend/app/features/settings/pages/plans/plan.component.html index 39cb13ae8..fd2505c50 100644 --- a/frontend/app/features/settings/pages/plans/plan.component.html +++ b/frontend/app/features/settings/pages/plans/plan.component.html @@ -22,7 +22,11 @@ ✓ Selected - @@ -37,7 +41,11 @@ ✓ Selected - diff --git a/frontend/app/shared/services/plans.service.spec.ts b/frontend/app/shared/services/plans.service.spec.ts index 4c4cd4792..9e1ef2264 100644 --- a/frontend/app/shared/services/plans.service.spec.ts +++ b/frontend/app/shared/services/plans.service.spec.ts @@ -60,18 +60,22 @@ describe('PlansService', () => { id: 'free', name: 'Free', costs: '14 €', + confirmText: 'Change for 14 € per month?', yearlyId: 'free_yearly', - yearlyCosts: '12 €', + yearlyCosts: '120 €', + yearlyConfirmText: 'Change for 120 € per year?', maxApiCalls: 1000, maxAssetSize: 1500, maxContributors: 2500 }, { - id: 'prof', - name: 'Prof', + id: 'professional', + name: 'Professional', costs: '18 €', - yearlyId: 'prof_yearly', - yearlyCosts: '16 €', + confirmText: 'Change for 18 € per month?', + yearlyId: 'professional_yearly', + yearlyCosts: '160 €', + yearlyConfirmText: 'Change for 160 € per year?', maxApiCalls: 4000, maxAssetSize: 5500, maxContributors: 6500 @@ -89,8 +93,18 @@ describe('PlansService', () => { currentPlanId: '123', planOwner: '456', plans: [ - new PlanDto('free', 'Free', '14 €', 'free_yearly', '12 €', 1000, 1500, 2500), - new PlanDto('prof', 'Prof', '18 €', 'prof_yearly', '16 €', 4000, 5500, 6500) + new PlanDto( + 'free', 'Free', '14 €', + 'Change for 14 € per month?', + 'free_yearly', '120 €', + 'Change for 120 € per year?', + 1000, 1500, 2500), + new PlanDto( + 'professional', 'Professional', '18 €', + 'Change for 18 € per month?', + 'professional_yearly', '160 €', + 'Change for 160 € per year?', + 4000, 5500, 6500) ], hasPortal: true }, diff --git a/frontend/app/shared/services/plans.service.ts b/frontend/app/shared/services/plans.service.ts index f2b3d0df9..6b9e157c2 100644 --- a/frontend/app/shared/services/plans.service.ts +++ b/frontend/app/shared/services/plans.service.ts @@ -32,8 +32,10 @@ export class PlanDto { public readonly id: string, public readonly name: string, public readonly costs: string, + public readonly confirmText: string | undefined, public readonly yearlyId: string, public readonly yearlyCosts: string, + public readonly yearlyConfirmText: string | undefined, public readonly maxApiCalls: number, public readonly maxAssetSize: number, public readonly maxContributors: number @@ -75,8 +77,10 @@ export class PlansService { item.id, item.name, item.costs, + item.confirmText, item.yearlyId, item.yearlyCosts, + item.yearlyConfirmText, item.maxApiCalls, item.maxAssetSize, item.maxContributors)), diff --git a/frontend/app/shared/state/plans.state.spec.ts b/frontend/app/shared/state/plans.state.spec.ts index ca8377aec..cb2f3f4f6 100644 --- a/frontend/app/shared/state/plans.state.spec.ts +++ b/frontend/app/shared/state/plans.state.spec.ts @@ -33,8 +33,8 @@ describe('PlansState', () => { currentPlanId: 'id1', planOwner: creator, plans: [ - new PlanDto('id1', 'name1', '100€', 'id1_yearly', '200€', 1, 1, 1), - new PlanDto('id2', 'name2', '400€', 'id2_yearly', '800€', 2, 2, 2) + new PlanDto('id1', 'name1', '100€', undefined, 'id1_yearly', '200€', undefined, 1, 1, 1), + new PlanDto('id2', 'name2', '400€', undefined, 'id2_yearly', '800€', undefined, 2, 2, 2) ], hasPortal: true };