diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs index 099b67053..11e567a6b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs @@ -11,6 +11,7 @@ using Fluid.Values; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Templates; using Squidex.Infrastructure; +using Squidex.Text; namespace Squidex.Domain.Apps.Core.HandleRules.Extensions { @@ -29,21 +30,34 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions { TemplateContext.GlobalFilters.AddFilter("contentUrl", ContentUrl); TemplateContext.GlobalFilters.AddFilter("assetContentUrl", AssetContentUrl); + TemplateContext.GlobalFilters.AddFilter("assetContentAppUrl", AssetContentAppUrl); + TemplateContext.GlobalFilters.AddFilter("assetContentSlugUrl", AssetContentSlugUrl); } private FluidValue ContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) { - if (input is ObjectValue objectValue) + var value = input.ToObjectValue(); + + switch (value) { - if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent) - { - if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty) + case Guid guid when guid != Guid.Empty: { - var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, guid); + if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent) + { + var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, guid); + + return new StringValue(result); + } + + break; + } + + case EnrichedContentEvent contentEvent: + { + var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); return new StringValue(result); } - } } return NilValue.Empty; @@ -51,14 +65,81 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions private FluidValue AssetContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) { - if (input is ObjectValue objectValue) + var value = input.ToObjectValue(); + + switch (value) { - if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty) - { - var result = urlGenerator.AssetContent(guid); + case Guid guid when guid != Guid.Empty: + { + var result = urlGenerator.AssetContent(guid); + + return new StringValue(result); + } + + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.Id); + + return new StringValue(result); + } + } + + return NilValue.Empty; + } + + private FluidValue AssetContentAppUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); + + switch (value) + { + case Guid guid when guid != Guid.Empty: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) + { + var result = urlGenerator.AssetContent(assetEvent.AppId, guid.ToString()); - return new StringValue(result); - } + return new StringValue(result); + } + + break; + } + + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + + return new StringValue(result); + } + } + + return NilValue.Empty; + } + + private FluidValue AssetContentSlugUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); + + switch (value) + { + case string s: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) + { + var result = urlGenerator.AssetContent(assetEvent.AppId, s.Slugify()); + + return new StringValue(result); + } + + break; + } + + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + + return new StringValue(result); + } } return NilValue.Empty; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs index 14d91d048..9fc63df23 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs @@ -9,6 +9,7 @@ using Jint.Native; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; +using Squidex.Text; namespace Squidex.Domain.Apps.Core.HandleRules.Extensions { @@ -55,6 +56,26 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions return JsValue.Null; })); + + context.Engine.SetValue("assetContentAppUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + } + + return JsValue.Null; + })); + + context.Engine.SetValue("assetContentSlugUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + } + + return JsValue.Null; + })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs index f2877da5f..1c50966c6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs @@ -11,6 +11,7 @@ using System.Globalization; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure; using Squidex.Shared.Users; +using Squidex.Text; namespace Squidex.Domain.Apps.Core.HandleRules { @@ -28,6 +29,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules AddPattern("APP_ID", AppId); AddPattern("APP_NAME", AppName); AddPattern("ASSET_CONTENT_URL", AssetContentUrl); + AddPattern("ASSET_CONTENT_APP_URL", AssetContentAppUrl); + AddPattern("ASSET_CONTENT_SLUG_URL", AssetContentSlugUrl); AddPattern("CONTENT_ACTION", ContentAction); AddPattern("CONTENT_URL", ContentUrl); AddPattern("MENTIONED_ID", MentionedId); @@ -124,6 +127,26 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } + private string? AssetContentAppUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + } + + return null; + } + + private string? AssetContentSlugUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + } + + return null; + } + private string? ContentUrl(EnrichedEvent @event) { if (@event is EnrichedContentEvent contentEvent) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs index 1e987251d..4b96939f0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs @@ -27,6 +27,8 @@ namespace Squidex.Domain.Apps.Core string AssetContent(Guid assetId); + string AssetContent(NamedId appId, string idOrSlug); + string BackupsUI(NamedId appId); string ClientsUI(NamedId appId); diff --git a/backend/src/Squidex.Web/Services/UrlGenerator.cs b/backend/src/Squidex.Web/Services/UrlGenerator.cs index 4ae668c10..135eee58b 100644 --- a/backend/src/Squidex.Web/Services/UrlGenerator.cs +++ b/backend/src/Squidex.Web/Services/UrlGenerator.cs @@ -53,6 +53,11 @@ namespace Squidex.Web.Services return urlsOptions.BuildUrl($"api/assets/{assetId}"); } + public string AssetContent(NamedId appId, string idOrSlug) + { + return urlsOptions.BuildUrl($"assets/{appId.Name}/{idOrSlug}"); + } + public string? AssetSource(Guid assetId, long fileVersion) { return assetFileStore.GeneratePublicUrl(assetId, fileVersion); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index 7936c857e..2ad6092c7 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs @@ -70,6 +70,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => urlGenerator.AssetContent(assetId)) .Returns("asset-content-url"); + A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString())) + .Returns("asset-content-app-url"); + + A.CallTo(() => urlGenerator.AssetContent(appId, "file-name")) + .Returns("asset-content-slug-url"); + A.CallTo(() => user.Id) .Returns("user123"); @@ -301,6 +307,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules "Download at ${assetContentUrl()}", "Download at {{event.id | assetContentUrl}}" )] + [InlineData("Liquid(Download at {{event | assetContentUrl}})")] public async Task Should_format_asset_content_url_from_event(string script) { var @event = new EnrichedAssetEvent { Id = assetId }; @@ -317,6 +324,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules "Download at ${assetContentUrl()}", "Download at {{event.id | assetContentUrl | default: 'null'}}" )] + [InlineData("Liquid(Download at {{event | assetContentUrl | default: 'null'}})")] public async Task Should_return_null_when_asset_content_url_not_found(string script) { var @event = new EnrichedContentEvent(); @@ -326,6 +334,74 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal("Download at null", result); } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_APP_URL", + null, + "Download at ${assetContentAppUrl()}", + "Download at {{event.id | assetContentAppUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] + public async Task Should_format_asset_content_app_url_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at asset-content-app-url", result); + } + + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_APP_URL", + null, + "Download at ${assetContentAppUrl()}", + "Download at {{event.id | assetContentAppUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] + public async Task Should_return_null_when_asset_content_app_url_not_found(string script) + { + var @event = new EnrichedContentEvent(); + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at null", result); + } + + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_SLUG_URL", + null, + "Download at ${assetContentSlugUrl()}", + "Download at {{event.fileName | assetContentSlugUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] + public async Task Should_format_asset_content_slug_url_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at asset-content-slug-url", result); + } + + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_SLUG_URL", + null, + "Download at ${assetContentSlugUrl()}", + "Download at {{event.id | assetContentSlugUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] + public async Task Should_return_null_when_asset_content_slug_url_not_found(string script) + { + var @event = new EnrichedContentEvent(); + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at null", result); + } + [Theory] [Expressions( "Go to $CONTENT_URL", diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs index ef8d64e00..cad98469d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs @@ -31,6 +31,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData return $"assets/{assetId}"; } + public string AssetContent(NamedId appId, string idOrSlug) + { + return $"assets/{appId.Name}/{idOrSlug}"; + } + public string ContentUI(NamedId appId, NamedId schemaId, Guid contentId) { return $"contents/{schemaId.Name}/{contentId}"; diff --git a/frontend/app/features/content/shared/forms/array-editor.component.html b/frontend/app/features/content/shared/forms/array-editor.component.html index 864b63810..9c760a428 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.html +++ b/frontend/app/features/content/shared/forms/array-editor.component.html @@ -1,43 +1,47 @@ -
-
- - - + +
+
+ + + +
-
-
-
- +
+
+ +
+ +
+ + +
+ -
- - -
-
- - - {{ 'contents.arrayNoFields' | sqxTranslate }} - \ No newline at end of file + + + {{ 'contents.arrayNoFields' | sqxTranslate }} + + \ No newline at end of file diff --git a/frontend/app/features/content/shared/forms/array-editor.component.ts b/frontend/app/features/content/shared/forms/array-editor.component.ts index f81c6b3af..1e59e0f22 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.ts +++ b/frontend/app/features/content/shared/forms/array-editor.component.ts @@ -6,8 +6,8 @@ */ import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core'; -import { AppLanguageDto, EditContentForm, FieldArrayForm, FieldArrayItemForm, sorted } from '@app/shared'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; +import { AppLanguageDto, ArrayFieldPropertiesDto, EditContentForm, FieldArrayForm, FieldArrayItemForm, sorted } from '@app/shared'; import { ArrayItemComponent } from './array-item.component'; @Component({ @@ -16,7 +16,7 @@ import { ArrayItemComponent } from './array-item.component'; templateUrl: './array-editor.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class ArrayEditorComponent { +export class ArrayEditorComponent implements OnChanges { @Input() public form: EditContentForm; @@ -38,10 +38,28 @@ export class ArrayEditorComponent { @ViewChildren(ArrayItemComponent) public children: QueryList; + public maxItems: number; + public get field() { return this.formModel.field; } + public get hasFields() { + return this.field.nested.length > 0; + } + + public get canAdd() { + return this.formModel.items.length < this.maxItems; + } + + public ngOnChanges(changes: SimpleChanges) { + if (changes['formModel']) { + const properties = this.field.properties as ArrayFieldPropertiesDto; + + this.maxItems = properties.maxItems || Number.MAX_VALUE; + } + } + public itemRemove(index: number) { this.formModel.removeItemAt(index); }