diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 0691fb391..2e431430e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -38,6 +38,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement] public string FileName { get; set; } + [BsonIgnoreIfNull] + [BsonElement] + public string[] Tags { get; set; } + [BsonRequired] [BsonElement] public long FileSize { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index 4ebf19747..66feb04e3 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -72,14 +72,14 @@ namespace Squidex.Domain.Apps.Entities.Apps }); case AssignContributor assigneContributor: - return UpdateReturnAsync(assigneContributor, (Func>)(async c => + return UpdateReturnAsync(assigneContributor, async c => { await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId)); AssignContributor(c); - return EntityCreatedResult.Create(c.ContributorId, (long)base.Version); - })); + return EntityCreatedResult.Create(c.ContributorId, (long)Version); + }); case RemoveContributor removeContributor: return UpdateAsync(removeContributor, c => diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 15d059cf1..53d7bdb48 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Guards; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; @@ -24,33 +25,40 @@ namespace Squidex.Domain.Apps.Entities.Assets { public sealed class AssetGrain : SquidexDomainObjectGrainLogSnapshots, IAssetGrain { - public AssetGrain(IStore store, ISemanticLog log) + private readonly ITagService tagService; + + public AssetGrain(IStore store, ITagService tagService, ISemanticLog log) : base(store, log) { + Guard.NotNull(tagService, nameof(tagService)); + + this.tagService = tagService; } protected override Task ExecuteAsync(IAggregateCommand command) { + VerifyNotDeleted(); + switch (command) { case CreateAsset createRule: - return CreateReturnAsync(createRule, (Func)(c => + return CreateReturnAsync(createRule, c => { GuardAsset.CanCreate(c); Create(c); - return new AssetSavedResult((long)base.Version, Snapshot.FileVersion); - })); + return new AssetSavedResult(Version, Snapshot.FileVersion); + }); case UpdateAsset updateRule: - return UpdateReturnAsync(updateRule, (Func)(c => + return UpdateAsync(updateRule, c => { GuardAsset.CanUpdate(c); Update(c); - return new AssetSavedResult((long)base.Version, Snapshot.FileVersion); - })); + return new AssetSavedResult(Version, Snapshot.FileVersion); + }); case RenameAsset renameAsset: return UpdateAsync(renameAsset, c => { @@ -65,6 +73,15 @@ namespace Squidex.Domain.Apps.Entities.Assets Delete(c); }); + case TagAsset tagAsset: + return UpdateAsync(tagAsset, async c => + { + GuardAsset.CanTag(c); + + c.Tags = await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, "Assets", c.Tags, Snapshot.Tags); + + Tag(c); + }); default: throw new NotSupportedException(); } @@ -105,18 +122,19 @@ namespace Squidex.Domain.Apps.Entities.Assets public void Delete(DeleteAsset command) { - VerifyNotDeleted(); - RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); } public void Rename(RenameAsset command) { - VerifyNotDeleted(); - RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); } + public void Tag(TagAsset command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetTagged())); + } + private void RaiseEvent(AppEvent @event) { if (@event.AppId == null) diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs new file mode 100644 index 000000000..cda6173f5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class TagAsset : AssetCommand + { + public string[] Tags { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs index dc63eb46b..f9970aa1f 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs @@ -35,6 +35,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards Guard.NotNull(command, nameof(command)); } + public static void CanTag(TagAsset command) + { + Guard.NotNull(command, nameof(command)); + } + public static void CanUpdate(UpdateAsset command) { Guard.NotNull(command, nameof(command)); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs index c61c52cc0..c6afe8c57 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs @@ -22,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Assets string MimeType { get; } + string[] Tags { get; } + long FileVersion { get; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index d1cf6df53..333bf425b 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -28,6 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State [JsonProperty] public string MimeType { get; set; } + [JsonProperty] + public string[] Tags { get; set; } + [JsonProperty] public long FileVersion { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 16f9841ea..21d6c7450 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents switch (command) { case CreateContent createContent: - return CreateReturnAsync(createContent, (Func>)(async c => + return CreateReturnAsync(createContent, async c => { var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, () => "Failed to create content."); @@ -77,8 +77,8 @@ namespace Squidex.Domain.Apps.Entities.Contents Create(c); - return EntityCreatedResult.Create(c.Data, (long)base.Version); - })); + return EntityCreatedResult.Create(c.Data, (long)Version); + }); case UpdateContent updateContent: return UpdateReturnAsync(updateContent, c => diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index 2d6924531..c003d0f94 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas switch (command) { case AddField addField: - return UpdateReturnAsync(addField, (Func)(c => + return UpdateAsync(addField, c => { GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); @@ -64,8 +64,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id; } - return EntityCreatedResult.Create(id, (long)base.Version); - })); + return EntityCreatedResult.Create(id, (long)Version); + }); case CreateSchema createSchema: return CreateAsync(createSchema, async c => diff --git a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs new file mode 100644 index 000000000..922a2fb68 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class GrainTagService : ITagService + { + private readonly IGrainFactory grainFactory; + + public GrainTagService(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public Task NormalizeTagsAsync(Guid appId, string category, string[] names, string[] ids) + { + return GetGrain(appId, category).NormalizeTagsAsync(names, ids); + } + + public Task> DenormalizeTagsAsync(Guid appId, string category, string[] ids) + { + return GetGrain(appId, category).DenormalizeTagsAsync(ids); + } + + public Task> GetTagsAsync(Guid appId, string category) + { + return GetGrain(appId, category).GetTagsAsync(); + } + + private ITagGrain GetGrain(Guid appId, string category) + { + Guard.NotNullOrEmpty(category, nameof(category)); + + return grainFactory.GetGrain($"{appId}_{category}"); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs new file mode 100644 index 000000000..8dd82aa87 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public interface ITagGrain : IGrainWithStringKey + { + Task NormalizeTagsAsync(string[] names, string[] ids); + + Task> DenormalizeTagsAsync(string[] ids); + + Task> GetTagsAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs new file mode 100644 index 000000000..8a48be95f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public interface ITagService + { + Task NormalizeTagsAsync(Guid appId, string category, string[] names, string[] ids); + + Task> DenormalizeTagsAsync(Guid appId, string category, string[] ids); + + Task> GetTagsAsync(Guid appId, string category); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs new file mode 100644 index 000000000..6fda8c6dd --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class TagGrain : GrainOfString, ITagGrain + { + private readonly IStore store; + private IPersistence persistence; + private State state = new State(); + + [CollectionName("Index_Tags")] + public sealed class State + { + public Dictionary Tags { get; set; } = new Dictionary(); + } + + public sealed class TagInfo + { + public string Name { get; set; } + + public int Count { get; set; } = 1; + } + + public TagGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public override Task OnActivateAsync(string key) + { + persistence = store.WithSnapshots(key, s => + { + state = s; + }); + + return persistence.ReadAsync(); + } + + public async Task NormalizeTagsAsync(string[] names, string[] ids) + { + var result = new List(); + + if (names != null) + { + foreach (var tag in names) + { + if (!string.IsNullOrWhiteSpace(tag)) + { + var tagName = tag.ToLowerInvariant(); + var tagId = string.Empty; + + var found = state.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase)); + + if (found.Value != null) + { + tagId = found.Key; + } + else + { + tagId = Guid.NewGuid().ToString(); + + state.Tags.Add(tagId, new TagInfo { Name = tagName }); + } + + result.Add(tagId); + } + } + } + + if (ids != null) + { + foreach (var id in ids) + { + if (!result.Contains(id)) + { + if (state.Tags.TryGetValue(id, out var tagInfo)) + { + tagInfo.Count--; + + if (tagInfo.Count <= 0) + { + state.Tags.Remove(id); + } + } + } + } + } + + await persistence.WriteSnapshotAsync(state); + + return result.ToArray(); + } + + public Task> DenormalizeTagsAsync(string[] ids) + { + var result = new Dictionary(); + + foreach (var id in ids) + { + if (state.Tags.TryGetValue(id, out var tagInfo)) + { + result[id] = tagInfo.Name; + } + } + + return Task.FromResult(result); + } + + public Task> GetTagsAsync() + { + return Task.FromResult(state.Tags.Values.ToDictionary(x => x.Name, x => x.Count)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs new file mode 100644 index 000000000..579ce9eec --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetTagged))] + public sealed class AssetTagged : AssetEvent + { + public string[] Tags { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs index 042f6d4e5..a6b542645 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs @@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.Commands return InvokeAsync(command, handler, true); } - protected Task UpdateReturnAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand { return InvokeAsync(command, handler?.ToAsync(), true); } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index 0521a1f59..52fbe8dd6 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -39,6 +39,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Required] public string FileType { get; set; } + /// + /// The asset tags. + /// + public string[] Tags { get; set; } + /// /// The size of the file in bytes. /// diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs index ce03091f7..e4a834a11 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs @@ -8,7 +8,6 @@ using System; using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Assets.Models { @@ -20,9 +19,22 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Required] public string FileName { get; set; } - public RenameAsset ToCommand(Guid id) + /// + /// The new asset tags. + /// + [Required] + public string[] Tags { get; set; } + + public AssetCommand ToCommand(Guid id) { - return SimpleMapper.Map(this, new RenameAsset { AssetId = id }); + if (Tags != null) + { + return new TagAsset { AssetId = id, Tags = Tags }; + } + else + { + return new RenameAsset { AssetId = id, FileName = FileName }; + } } } } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 140a66cc6..e79f2a915 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -32,6 +32,7 @@ using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; @@ -104,6 +105,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs>() .As(); diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 722f32f07..4f89a5726 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -2,7 +2,7 @@ - {{schema.displayName}} + {{schema.displayName}} diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index ac0955aa2..39ce830d4 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -6,7 +6,7 @@
- +
@@ -17,8 +17,8 @@ -

- {{client.name}} +

+ {{client.name}}

diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.scss b/src/Squidex/app/features/settings/pages/clients/client.component.scss index 6dfdfcfa1..ec20b3cba 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.scss +++ b/src/Squidex/app/features/settings/pages/clients/client.component.scss @@ -30,31 +30,13 @@ $color-editor: #eceeef; } &-name { - & { - @include border-radius(.25rem); - margin: 0; - margin-left: -.6rem; - height: 2.5rem; - padding: 0 .6rem; - border: 0; - background: transparent; - font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - font-size: 1.2rem; - font-weight: normal; - display: inline-block; - line-height: 2.5rem; - } - - &.enabled, - &:hover { - & { - background: $color-editor; - } - } - - h3 { - font-size: 1.6rem; - } + padding: .375rem 0; + font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 1.2rem; + font-weight: normal; + line-height: 1.5rem; + display: inline-block; + margin: 0; } &-header { @@ -66,12 +48,15 @@ $color-editor: #eceeef; } } -.col-form-label { - text-align: left; +h3 { + &.client-name { + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + } } -.btn-cancel { - padding: .4rem; +.col-form-label { + text-align: left; } .form-check { diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index 860104663..17a4eb755 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -1,12 +1,19 @@ - + + + + {{item}} + + + + + - -
- - {{item}} - -
\ No newline at end of file + placeholder="+Tag"> + \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.scss b/src/Squidex/app/framework/angular/forms/tag-editor.component.scss index 024cc5ca8..2b5007e7e 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.scss +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.scss @@ -1,25 +1,57 @@ @import '_mixins'; @import '_vars'; +.form-control { + & { + cursor: text; + } + + &.focus { + @include box-shadow-raw(0 0 0 0.2rem rgba(51, 137, 255, 0.25)); + border-color: #b3d3ff; + } +} + +.blank { + & { + padding: 0; + border: 0; + background: transparent; + min-width: 40px; + } + + &:focus, + &.focus { + @include box-shadow-none; + outline: none; + } +} + +.icon-close { + font-size: .6rem; +} + .items { - margin-top: .4rem; - min-height: 1.6rem; + margin-left: -2px; } .item { & { - @include border-radius(.8rem); + @include border-radius(10px); display: inline-block; color: $color-dark-foreground; - margin-right: .4rem; - margin-bottom: .25rem; - min-height: 1.6rem; + cursor: default; + height: 20px; padding: 0 .6rem; background: $color-theme-blue; border: 0; font-size: .8rem; font-weight: normal; - line-height: 1.6rem; + line-height: 20px; + } + + &-container { + padding: 2px; } &.disabled { diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 6fd3fff59..7b692a6a7 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -5,12 +5,13 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, forwardRef, Input } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; -const KEY_ENTER = 13; +const KEY_SPACE = 32; +const KEY_DELETE = 8; export interface Converter { convert(input: string): any; @@ -81,9 +82,17 @@ export class TagEditorComponent implements ControlValueAccessor { @Input() public useDefaultValue = true; + @Input() + public class: string; + @Input() public inputName = 'tag-editor'; + @ViewChild('input') + public inputElement: ElementRef; + + public hasFocus = false; + public items: any[] = []; public addInput = new FormControl(); @@ -118,23 +127,57 @@ export class TagEditorComponent implements ControlValueAccessor { this.updateItems([...this.items.slice(0, index), ...this.items.splice(index + 1)]); } - public markTouched() { - this.callTouched(); + public focus() { + this.hasFocus = true; } private resetForm() { + this.adjustSize(); + this.addInput.reset(); } + public markTouched() { + this.callTouched(); + + this.hasFocus = false; + } + + public adjustSize() { + const style = window.getComputedStyle(this.inputElement.nativeElement); + + if (!canvas) { + canvas = document.createElement('canvas'); + } + + if (canvas) { + const ctx = canvas.getContext('2d'); + + if (ctx) { + ctx.font = `${style.getPropertyValue('font-size')} ${style.getPropertyValue('font-family')}`; + + this.inputElement.nativeElement.style.width = ((ctx.measureText(this.inputElement.nativeElement.value).width + 20) + 'px'); + } + } + } + public onKeyDown(event: KeyboardEvent) { - if (event.keyCode === KEY_ENTER) { + if (event.keyCode === KEY_SPACE) { const value = this.addInput.value; - if (this.converter.isValidInput(value)) { + if (value && this.converter.isValidInput(value)) { const converted = this.converter.convert(value); this.updateItems([...this.items, converted]); this.resetForm(); + return false; + } + } else if (event.keyCode === KEY_DELETE) { + const value = this.addInput.value; + + if (!value || value.length === 0) { + this.updateItems(this.items.slice(0, this.items.length - 2)); + return false; } } @@ -151,4 +194,6 @@ export class TagEditorComponent implements ControlValueAccessor { this.callChange(this.items); } } -} \ No newline at end of file +} + +let canvas: HTMLCanvasElement | null = null; \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/src/Squidex/app/framework/angular/image-source.directive.ts index 752584091..2bc972977 100644 --- a/src/Squidex/app/framework/angular/image-source.directive.ts +++ b/src/Squidex/app/framework/angular/image-source.directive.ts @@ -48,12 +48,12 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After this.parentResizeListener = this.renderer.listen(this.parent, 'resize', () => { - this.resize(this.parent); + this.resize(); }); } public ngAfterViewInit() { - this.resize(this.parent); + this.resize(); } public ngOnChanges() { @@ -75,7 +75,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After this.retryLoadingImage(); } - private resize(parent: any) { + private resize() { this.size = this.parent.getBoundingClientRect(); this.renderer.setStyle(this.element.nativeElement, 'display', 'inline-block'); diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 1a25cb889..1e28af161 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -1,4 +1,4 @@ -
+
@@ -16,17 +16,14 @@
@@ -44,8 +41,20 @@
\ No newline at end of file diff --git a/src/Squidex/app/shared/components/asset.component.scss b/src/Squidex/app/shared/components/asset.component.scss index 961b73bea..5beba1cc8 100644 --- a/src/Squidex/app/shared/components/asset.component.scss +++ b/src/Squidex/app/shared/components/asset.component.scss @@ -73,7 +73,6 @@ .card { & { @include overlay-container; - height: $asset-height; } &.selectable { @@ -82,6 +81,7 @@ &-body { position: relative; + height: 0.7 * $asset-height; } &-footer { @@ -89,7 +89,6 @@ background: transparent; padding: .8rem; padding-top: .4rem; - height: 70px; } } @@ -103,7 +102,8 @@ } &-image { - height: 100%; + min-height: 100%; + max-height: 100%; } &-preview { @@ -122,6 +122,10 @@ } } + &-info { + margin-top: .25rem; + } + &-user { @include absolute(auto, auto, 1.7rem, .5rem); } @@ -178,4 +182,8 @@ @include asset-type; } } +} + +.editable { + height: 2rem; } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index 583cd8e95..bc615fa29 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -14,12 +14,11 @@ import { AssetsService, AuthService, DateTime, - DialogModel, DialogService, fadeAnimation, + RenameAssetDto, RenameAssetForm, Types, - UpdateAssetDto, Versioned } from '@app/shared/internal'; @@ -68,7 +67,9 @@ export class AssetComponent implements OnInit { @Output() public failed = new EventEmitter(); - public renameDialog = new DialogModel(); + public renaming = false; + public isTagging = false; + public renameForm = new RenameAssetForm(this.formBuilder); public progress = 0; @@ -121,17 +122,16 @@ export class AssetComponent implements OnInit { } public renameAsset() { - const value = this.renameForm.submit(); + const value = this.renameForm.submit(this.asset); if (value) { - const requestDto = new UpdateAssetDto(value.name); + const requestDto = new RenameAssetDto(value.name); this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version) .subscribe(dto => { this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true); - this.renameForm.submitCompleted(); - this.renameDialog.hide(); + this.renameCancel(); }, error => { this.dialogs.notifyError(error); @@ -140,9 +140,22 @@ export class AssetComponent implements OnInit { } } - public cancelRenameAsset() { + public renameStart() { + if (!this.isDisabled) { + this.renameForm.load(this.asset); + this.renaming = true; + } + } + + public renameCancel() { this.renameForm.submitCompleted(); - this.renameDialog.hide(); + this.renaming = false; + } + + public startTagging() { + if (!this.isDisabled) { + this.isTagging = true; + } } private setProgress(progress = 0) { @@ -162,14 +175,14 @@ export class AssetComponent implements OnInit { } private updateAsset(asset: AssetDto, emitEvent: boolean) { - this.renameForm.load({ name: asset.fileName }); this.asset = asset; + this.progress = 0; if (emitEvent) { this.emitUpdated(asset); } - this.cancelRenameAsset(); + this.renameCancel(); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 3021e91f3..9855571bd 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -16,10 +16,11 @@ import { AssetsDto, AssetsService, DateTime, - UpdateAssetDto, + RenameAssetDto, Version, Versioned } from './../'; +import { TagAssetDto } from '@appshared/services/assets.service'; describe('AssetDto', () => { const creation = DateTime.today(); @@ -30,7 +31,7 @@ describe('AssetDto', () => { const newVersion = new Version('2'); it('should update name property and user info when renaming', () => { - const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'url', version); + const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, [], 'url', version); const asset_2 = asset_1.rename('new-name.png', modifier, newVersion, modified); expect(asset_2.fileName).toEqual('new-name.png'); @@ -42,7 +43,7 @@ describe('AssetDto', () => { it('should update file properties when uploading', () => { const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2); - const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'url', version); + const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, [], 'url', version); const asset_2 = asset_1.update(update, modifier, newVersion, modified); expect(asset_2.fileSize).toEqual(2); @@ -110,6 +111,7 @@ describe('AssetsService', () => { isImage: true, pixelWidth: 1024, pixelHeight: 2048, + tags: undefined, version: 11 }, { @@ -126,6 +128,7 @@ describe('AssetsService', () => { isImage: true, pixelWidth: 1024, pixelHeight: 2048, + tags: ['tag1', 'tag2'], version: 22 } ] @@ -145,6 +148,7 @@ describe('AssetsService', () => { true, 1024, 2048, + [], 'http://service/p/api/assets/id1', new Version('11')), new AssetDto('id2', 'Created2', 'LastModifiedBy2', @@ -158,6 +162,7 @@ describe('AssetsService', () => { true, 1024, 2048, + ['tag1', 'tag2'], 'http://service/p/api/assets/id2', new Version('22')) ])); @@ -190,7 +195,8 @@ describe('AssetsService', () => { mimeType: 'image/png', isImage: true, pixelWidth: 1024, - pixelHeight: 2048 + pixelHeight: 2048, + tags: ['tag1', 'tag2'] }, { headers: { etag: '2' @@ -210,6 +216,7 @@ describe('AssetsService', () => { true, 1024, 2048, + ['tag1', 'tag2'], 'http://service/p/api/assets/id1', new Version('2'))); })); @@ -284,6 +291,7 @@ describe('AssetsService', () => { true, 1024, 2048, + [], 'http://service/p/api/assets/id1', new Version('2'))); })); @@ -323,7 +331,22 @@ describe('AssetsService', () => { it('should make put request to update asset', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { - const dto = new UpdateAssetDto('My-Asset.pdf'); + const dto = new RenameAssetDto('My-Asset.pdf'); + + assetsService.putAsset('my-app', '123', dto, version).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toEqual(version.value); + + req.flush({}); + })); + + it('should make put request to update asset', + inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { + + const dto = new TagAssetDto(['tag1', 'tag2']); assetsService.putAsset('my-app', '123', dto, version).subscribe(); diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index b3601f6e3..a3a2577fa 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -50,6 +50,7 @@ export class AssetDto extends Model { public readonly isImage: boolean, public readonly pixelWidth: number | null, public readonly pixelHeight: number | null, + public readonly tags: string[], public readonly url: string, public readonly version: Version ) { @@ -79,13 +80,20 @@ export class AssetDto extends Model { } } -export class UpdateAssetDto { +export class RenameAssetDto { constructor( public readonly fileName: string ) { } } +export class TagAssetDto { + constructor( + public readonly tags: string[] + ) { + } +} + export class AssetReplacedDto { constructor( public readonly fileSize: number, @@ -151,6 +159,7 @@ export class AssetsService { item.isImage, item.pixelWidth, item.pixelHeight, + item.tags || [], assetUrl, new Version(item.version.toString())); })); @@ -194,6 +203,7 @@ export class AssetsService { response.isImage, response.pixelWidth, response.pixelHeight, + [], assetUrl, new Version(event.headers.get('etag')!)); @@ -231,6 +241,7 @@ export class AssetsService { body.isImage, body.pixelWidth, body.pixelHeight, + body.tags || [], assetUrl, response.version); }), @@ -288,7 +299,7 @@ export class AssetsService { pretifyError('Failed to delete asset. Please reload.')); } - public putAsset(appName: string, id: string, dto: UpdateAssetDto, version: Version): Observable> { + public putAsset(appName: string, id: string, dto: RenameAssetDto | TagAssetDto, version: Version): Observable> { const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); return HTTP.putVersioned(this.http, url, dto, version).pipe( diff --git a/src/Squidex/app/shared/state/assets.forms.ts b/src/Squidex/app/shared/state/assets.forms.ts index b6a23c236..3cc5426a2 100644 --- a/src/Squidex/app/shared/state/assets.forms.ts +++ b/src/Squidex/app/shared/state/assets.forms.ts @@ -9,6 +9,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Form } from '@app/framework'; +import { AssetDto } from './../services/assets.service'; + export class RenameAssetForm extends Form { constructor(formBuilder: FormBuilder) { super(formBuilder.group({ @@ -19,4 +21,28 @@ export class RenameAssetForm extends Form { ] })); } + + public submit(asset?: AssetDto) { + const result = super.submit(); + + if (asset) { + let index = asset.fileName.lastIndexOf('.'); + if (index > 0) { + result.name += asset.fileName.substr(index); + } + } + + return result; + } + + public load(asset: AssetDto) { + let name = asset.fileName; + + let index = name.lastIndexOf('.'); + if (index > 0) { + name = name.substr(0, index); + } + + super.load({ name }); + } } \ No newline at end of file diff --git a/src/Squidex/app/shared/state/assets.state.spec.ts b/src/Squidex/app/shared/state/assets.state.spec.ts index 08fc6fea5..ba6dbd88b 100644 --- a/src/Squidex/app/shared/state/assets.state.spec.ts +++ b/src/Squidex/app/shared/state/assets.state.spec.ts @@ -30,8 +30,8 @@ describe('AssetsState', () => { const newVersion = new Version('2'); const oldAssets = [ - new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, 'url1', version), - new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, 'url2', version) + new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, [], 'url1', version), + new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, [], 'url2', version) ]; let dialogs: IMock; @@ -77,7 +77,7 @@ describe('AssetsState', () => { }); it('should add asset to snapshot when created', () => { - const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'url3', version); + const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, [], 'url3', version); assetsState.add(newAsset); @@ -86,7 +86,7 @@ describe('AssetsState', () => { }); it('should update properties when updated', () => { - const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'url3', version); + const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, [], 'url3', version); assetsState.update(newAsset); diff --git a/src/Squidex/app/theme/_forms.scss b/src/Squidex/app/theme/_forms.scss index e69af6ba2..58443b065 100644 --- a/src/Squidex/app/theme/_forms.scss +++ b/src/Squidex/app/theme/_forms.scss @@ -4,16 +4,18 @@ // // Support for Angular validation states. // -.ng-invalid { - &.ng-dirty { - & { - border-color: $color-theme-error; - } - - &:hover, - &:focus { - @include box-shadow-colored(0, 0, .2rem, $color-theme-error); - border-color: $color-theme-error-dark; +.form-control { + &.ng-invalid { + &.ng-dirty { + & { + border-color: $color-theme-error; + } + + &:hover, + &:focus { + @include box-shadow-colored(0, 0, .2rem, $color-theme-error); + border-color: $color-theme-error-dark; + } } } } @@ -176,3 +178,39 @@ color: $color-dark2-focus-foreground; } } + +.form-underlined { + & { + @include border-radius(0); + padding-left: 0; + padding-right: 0; + border-color: transparent; + border-bottom: 1px solid $color-input-border; + } + + &:focus, + &.focus { + @include box-shadow-none; + background: transparent; + border-color: transparent; + border-bottom-color: $color-theme-blue; + outline: none; + } + + &.ng-invalid.ng-dirty { + & { + @include box-shadow-none; + background: transparent; + border-color: transparent; + border-bottom-color: $color-theme-error; + outline: none; + } + + &:hover, + &:focus { + @include box-shadow-none; + border-color: transparent; + border-bottom-color: $color-theme-error-dark; + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/theme/_vars.scss b/src/Squidex/app/theme/_vars.scss index c6e621401..0e5ef61ba 100644 --- a/src/Squidex/app/theme/_vars.scss +++ b/src/Squidex/app/theme/_vars.scss @@ -97,4 +97,4 @@ $panel-header: 5.4rem; $panel-sidebar: 3.75rem; $panel-light-background: #fff; -$asset-height: 13rem; \ No newline at end of file +$asset-height: 16rem; \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 5d8148151..098b0d3f1 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -13,6 +13,7 @@ using FakeItEasy; using Orleans; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; @@ -26,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly IAssetStore assetStore = A.Fake(); + private readonly ITagService tagService = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); private readonly Guid assetId = Guid.NewGuid(); private readonly Stream stream = new MemoryStream(); @@ -43,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { file = new AssetFile("my-image.png", "image/png", 1024, () => stream); - asset = new AssetGrain(Store, A.Dummy()); + asset = new AssetGrain(Store, tagService, A.Dummy()); asset.OnActivateAsync(Id).Wait(); A.CallTo(() => grainFactory.GetGrain(Id, null)) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs index ae0a44f82..069c8a8dc 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; @@ -23,6 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class AssetGrainTests : HandlerTestBase { + private readonly ITagService tagService = A.Fake(); private readonly ImageInfo image = new ImageInfo(2048, 2048); private readonly Guid assetId = Guid.NewGuid(); private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); @@ -35,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetGrainTests() { - sut = new AssetGrain(Store, A.Dummy()); + sut = new AssetGrain(Store, tagService, A.Dummy()); sut.OnActivateAsync(Id).Wait(); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs index 82a77eaef..340ce563d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs @@ -34,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData public string FileName { get; set; } + public string[] Tags { get; set; } + public long FileSize { get; set; } public long FileVersion { get; set; } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs index 487ae93f5..c74f56ee2 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -90,7 +90,7 @@ namespace Squidex.Infrastructure.Commands }); case UpdateCustom updateCustom: - return UpdateReturnAsync(updateCustom, c => + return UpdateAsync(updateCustom, c => { RaiseEvent(new ValueChanged { Value = c.Value }); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs index b224cdb82..86de1b367 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs @@ -85,7 +85,7 @@ namespace Squidex.Infrastructure.Commands }); case UpdateCustom updateCustom: - return UpdateReturnAsync(updateCustom, c => + return UpdateAsync(updateCustom, c => { RaiseEvent(new ValueChanged { Value = c.Value }); diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index 8c50065a5..e1ba8453f 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -72,7 +72,7 @@ namespace Squidex.Infrastructure.TestHelpers }); case UpdateCustom updateCustom: - return UpdateReturnAsync(updateCustom, c => + return UpdateAsync(updateCustom, c => { RaiseEvent(new ValueChanged { Value = c.Value });