From 4a43948b6d9d699d933f22fece5a36df2a3efd2f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 15 Jul 2018 19:59:32 +0200 Subject: [PATCH] Tags --- .../Assets/MongoAssetEntity.cs | 9 +++-- .../Assets/Visitors/FindExtensions.cs | 9 ++--- .../Assets/AssetQueryService.cs | 18 +++++---- .../Assets/Commands/TagAsset.cs | 4 +- .../Assets/IAssetEntity.cs | 3 +- .../Assets/State/AssetState.cs | 7 ++-- .../IEntityWithTags.cs | 16 ++++++++ .../Tags/GrainTagService.cs | 6 +-- .../Tags/ITagGrain.cs | 6 +-- .../Tags/ITagService.cs | 6 +-- .../Tags/TagGrain.cs | 14 +++---- .../Assets/AssetTagged.cs | 3 +- .../Api/Controllers/Assets/Models/AssetDto.cs | 3 +- .../Assets/Models/AssetUpdateDto.cs | 3 +- .../angular/forms/tag-editor.component.html | 8 +--- .../angular/forms/tag-editor.component.scss | 12 ++++-- .../angular/forms/tag-editor.component.ts | 4 +- .../shared/components/asset.component.html | 2 +- .../components/assets-list.component.html | 1 + .../components/assets-list.component.ts | 4 ++ .../app/shared/state/assets.state.spec.ts | 4 +- src/Squidex/app/shared/state/assets.state.ts | 38 ++++++++++++++++++- .../Assets/OData/ODataQueryTests.cs | 5 ++- .../Contents/TestData/FakeAssetEntity.cs | 5 ++- 24 files changed, 128 insertions(+), 62 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 2e431430e..5d4e69f60 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using Squidex.Domain.Apps.Core.ValidateContent; @@ -38,10 +39,6 @@ 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; } @@ -74,6 +71,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement] public RefToken LastModifiedBy { get; set; } + [BsonIgnoreIfNull] + [BsonElement] + public HashSet Tags { get; set; } + [BsonElement] public bool IsDeleted { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index 455543d76..bc1fa6201 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.OData.UriParser; using MongoDB.Bson; @@ -93,12 +94,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors { if (string.Equals(field, nameof(MongoAssetEntity.Tags), StringComparison.OrdinalIgnoreCase)) { - var tags = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, new[] { value.ToString() })).Result; + var tagIds = new HashSet(new[] { value.ToString() }); + var tagNames = Task.Run(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, tagIds)).Result; - if (tags.Length == 1) - { - return tags[0] ?? value; - } + return tagNames?.FirstOrDefault() ?? value; } return value; diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index 1d18f7f60..4faf3dffc 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs @@ -75,25 +75,29 @@ namespace Squidex.Domain.Apps.Entities.Assets private async Task DenormalizeTagsAsync(Guid appId, IEnumerable assets) { - var tags = assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct().ToArray(); + var tags = new HashSet(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct()); var tagsById = await tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, tags); foreach (var asset in assets) { - if (asset.Tags?.Length > 0) + if (asset.Tags?.Count > 0) { - var tagNames = new List(); + var tagNames = asset.Tags.ToList(); - foreach (var id in asset.Tags) + asset.Tags.Clear(); + + foreach (var id in tagNames) { if (tagsById.TryGetValue(id, out var name)) { - tagNames.Add(name); + asset.Tags.Add(name); } } - - asset.Tags = tagNames.ToArray(); + } + else + { + asset.Tags?.Clear(); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs index cda6173f5..7ca591f53 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs @@ -5,10 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; + namespace Squidex.Domain.Apps.Entities.Assets.Commands { public sealed class TagAsset : AssetCommand { - public string[] Tags { get; set; } + public HashSet Tags { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs index 2f7bffca0..02890b87e 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs @@ -16,14 +16,13 @@ namespace Squidex.Domain.Apps.Entities.Assets IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion, + IEntityWithTags, IAssetInfo { NamedId AppId { get; } string MimeType { get; } - string[] Tags { get; set; } - 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 4f468f65b..defa18436 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Events; @@ -28,9 +29,6 @@ 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; } @@ -52,6 +50,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State [JsonProperty] public bool IsDeleted { get; set; } + [JsonProperty] + public HashSet Tags { get; set; } + Guid IAssetInfo.AssetId { get { return Id; } diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs new file mode 100644 index 000000000..1049ae23d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithTags.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithTags + { + HashSet Tags { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs index 7cfeccd85..ff869cf80 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs @@ -24,17 +24,17 @@ namespace Squidex.Domain.Apps.Entities.Tags this.grainFactory = grainFactory; } - public Task NormalizeTagsAsync(Guid appId, string category, string[] names, string[] ids) + public Task> NormalizeTagsAsync(Guid appId, string category, HashSet names, HashSet ids) { return GetGrain(appId, category).NormalizeTagsAsync(names, ids); } - public Task GetTagIdsAsync(Guid appId, string category, string[] names) + public Task> GetTagIdsAsync(Guid appId, string category, HashSet names) { return GetGrain(appId, category).GetTagIdsAsync(names); } - public Task> DenormalizeTagsAsync(Guid appId, string category, string[] ids) + public Task> DenormalizeTagsAsync(Guid appId, string category, HashSet ids) { return GetGrain(appId, category).DenormalizeTagsAsync(ids); } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs index 8639fc51b..a37bf83cb 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs @@ -13,11 +13,11 @@ namespace Squidex.Domain.Apps.Entities.Tags { public interface ITagGrain : IGrainWithStringKey { - Task NormalizeTagsAsync(string[] names, string[] ids); + Task> NormalizeTagsAsync(HashSet names, HashSet ids); - Task GetTagIdsAsync(string[] names); + Task> GetTagIdsAsync(HashSet names); - Task> DenormalizeTagsAsync(string[] ids); + Task> DenormalizeTagsAsync(HashSet ids); Task> GetTagsAsync(); } diff --git a/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs index d045e1e5c..52dae1dc4 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs @@ -13,11 +13,11 @@ namespace Squidex.Domain.Apps.Entities.Tags { public interface ITagService { - Task NormalizeTagsAsync(Guid appId, string category, string[] names, string[] ids); + Task> NormalizeTagsAsync(Guid appId, string category, HashSet names, HashSet ids); - Task GetTagIdsAsync(Guid appId, string category, string[] names); + Task> GetTagIdsAsync(Guid appId, string category, HashSet names); - Task> DenormalizeTagsAsync(Guid appId, string category, string[] ids); + Task> DenormalizeTagsAsync(Guid appId, string category, HashSet 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 index 3387b428d..e25bc319f 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs @@ -51,9 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Tags return persistence.ReadAsync(); } - public async Task NormalizeTagsAsync(string[] names, string[] ids) + public async Task> NormalizeTagsAsync(HashSet names, HashSet ids) { - var result = new List(); + var result = new HashSet(); if (names != null) { @@ -108,22 +108,22 @@ namespace Squidex.Domain.Apps.Entities.Tags await persistence.WriteSnapshotAsync(state); - return result.ToArray(); + return result; } - public Task GetTagIdsAsync(string[] names) + public Task> GetTagIdsAsync(HashSet names) { - var result = new List(); + var result = new HashSet(); foreach (var name in names) { result.Add(state.Tags.FirstOrDefault(x => x.Value.Name == name).Key); } - return Task.FromResult(result.ToArray()); + return Task.FromResult(result); } - public Task> DenormalizeTagsAsync(string[] ids) + public Task> DenormalizeTagsAsync(HashSet ids) { var result = new Dictionary(); diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs index 579ce9eec..fb555b515 100644 --- a/src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs +++ b/src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Assets @@ -12,6 +13,6 @@ namespace Squidex.Domain.Apps.Events.Assets [EventType(nameof(AssetTagged))] public sealed class AssetTagged : AssetEvent { - public string[] Tags { get; set; } + public HashSet Tags { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index 52fbe8dd6..0b3f722de 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Domain.Apps.Entities.Assets; @@ -42,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// /// The asset tags. /// - public string[] Tags { get; set; } + public HashSet 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 e4a834a11..61b4e956b 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Entities.Assets.Commands; @@ -23,7 +24,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// The new asset tags. /// [Required] - public string[] Tags { get; set; } + public HashSet Tags { get; set; } public AssetCommand ToCommand(Guid id) { 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 17a4eb755..8ecdb31a8 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -1,10 +1,6 @@
- - - - {{item}} - - + + {{item}}
- +
{{asset.pixelWidth}}x{{asset.pixelHeight}}px, {{asset.fileSize | sqxFileSize}} diff --git a/src/Squidex/app/shared/components/assets-list.component.html b/src/Squidex/app/shared/components/assets-list.component.html index e5d941ca5..f18981c91 100644 --- a/src/Squidex/app/shared/components/assets-list.component.html +++ b/src/Squidex/app/shared/components/assets-list.component.html @@ -25,6 +25,7 @@ [isDisabled]="isDisabled" [isSelectable]="selectedIds" [isSelected]="isSelected(asset)" + (updated)="update($event)" (selected)="select($event)" (deleting)="delete($event)"> diff --git a/src/Squidex/app/shared/components/assets-list.component.ts b/src/Squidex/app/shared/components/assets-list.component.ts index e7dd32ce6..d8fce26de 100644 --- a/src/Squidex/app/shared/components/assets-list.component.ts +++ b/src/Squidex/app/shared/components/assets-list.component.ts @@ -58,6 +58,10 @@ export class AssetsListComponent { this.state.goPrev().pipe(onErrorResumeNext()).subscribe(); } + public update(asset: AssetDto) { + this.state.update(asset); + } + public trackByAsset(index: number, asset: AssetDto) { return asset.id; } diff --git a/src/Squidex/app/shared/state/assets.state.spec.ts b/src/Squidex/app/shared/state/assets.state.spec.ts index ba6dbd88b..0520135c2 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, ['tag1', 'shared'], 'url1', version), + new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, ['tag2', 'shared'], 'url2', version) ]; let dialogs: IMock; diff --git a/src/Squidex/app/shared/state/assets.state.ts b/src/Squidex/app/shared/state/assets.state.ts index 0fe1eafbe..1fd7ee671 100644 --- a/src/Squidex/app/shared/state/assets.state.ts +++ b/src/Squidex/app/shared/state/assets.state.ts @@ -105,7 +105,17 @@ export class AssetsState extends State { const assets = s.assets.filter(x => x.id !== asset.id); const assetsPager = s.assetsPager.decrementCount(); - return { ...s, assets, assetsPager }; + const tags = { ...s.tags }; + + for (let tag of asset.tags) { + if (tags[tag] === 1) { + delete tags[tag]; + } else { + tags[tag]--; + } + } + + return { ...s, assets, assetsPager, tags }; }); }), notify(this.dialogs)); @@ -113,9 +123,33 @@ export class AssetsState extends State { public update(asset: AssetDto) { this.next(s => { + const previous = s.assets.find(x => x.id === asset.id); + + const tags = { ...s.tags }; + + if (previous) { + for (let tag of previous.tags) { + if (tags[tag] === 1) { + delete tags[tag]; + } else { + tags[tag]--; + } + } + } + + if (asset) { + for (let tag of asset.tags) { + if (tags[tag]) { + tags[tag]++; + } else { + tags[tag] = 1; + } + } + } + const assets = s.assets.replaceBy('id', asset); - return { ...s, assets }; + return { ...s, assets, tags }; }); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs index cfe1a5b51..54b495133 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/OData/ODataQueryTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using FakeItEasy; using Microsoft.OData.Edm; using MongoDB.Bson.Serialization; @@ -36,8 +37,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.OData public ODataQueryTests() { - A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A.That.Contains("tag1"))) - .Returns(new[] { "normalized1" }); + A.CallTo(() => tagService.GetTagIdsAsync(appId, TagGroups.Assets, A>.That.Contains("tag1"))) + .Returns(new HashSet(new[] { "normalized1" })); valueConverter = FindExtensions.CreateValueConverter(appId, tagService); } 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 340ce563d..aaabf1630 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using NodaTime; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; @@ -28,14 +29,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData public RefToken LastModifiedBy { get; set; } + public HashSet Tags { get; set; } + public long Version { get; set; } public string MimeType { get; set; } public string FileName { get; set; } - public string[] Tags { get; set; } - public long FileSize { get; set; } public long FileVersion { get; set; }