mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
94 changed files with 2078 additions and 556 deletions
@ -0,0 +1,105 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Domain.Apps.Entities.Assets.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Tags; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public sealed class AssetQueryService : IAssetQueryService |
||||
|
{ |
||||
|
private readonly ITagService tagService; |
||||
|
private readonly IAssetRepository assetRepository; |
||||
|
|
||||
|
public AssetQueryService(ITagService tagService, IAssetRepository assetRepository) |
||||
|
{ |
||||
|
Guard.NotNull(tagService, nameof(tagService)); |
||||
|
Guard.NotNull(assetRepository, nameof(assetRepository)); |
||||
|
|
||||
|
this.tagService = tagService; |
||||
|
|
||||
|
this.assetRepository = assetRepository; |
||||
|
} |
||||
|
|
||||
|
public async Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
|
||||
|
var asset = await assetRepository.FindAssetAsync(id); |
||||
|
|
||||
|
if (asset != null) |
||||
|
{ |
||||
|
await DenormalizeTagsAsync(context.App.Id, Enumerable.Repeat(asset, 1)); |
||||
|
} |
||||
|
|
||||
|
return asset; |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Query query) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
Guard.NotNull(query, nameof(query)); |
||||
|
|
||||
|
IResultList<IAssetEntity> assets; |
||||
|
|
||||
|
if (query.Ids != null) |
||||
|
{ |
||||
|
assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<Guid>(query.Ids)); |
||||
|
assets = Sort(assets, query.Ids); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
assets = await assetRepository.QueryAsync(context.App.Id, query.ODataQuery); |
||||
|
} |
||||
|
|
||||
|
await DenormalizeTagsAsync(context.App.Id, assets); |
||||
|
|
||||
|
return assets; |
||||
|
} |
||||
|
|
||||
|
private IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IList<Guid> ids) |
||||
|
{ |
||||
|
var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null); |
||||
|
|
||||
|
return ResultList.Create(assets.Total, sorted); |
||||
|
} |
||||
|
|
||||
|
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets) |
||||
|
{ |
||||
|
var tags = new HashSet<string>(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?.Count > 0) |
||||
|
{ |
||||
|
var tagNames = asset.Tags.ToList(); |
||||
|
|
||||
|
asset.Tags.Clear(); |
||||
|
|
||||
|
foreach (var id in tagNames) |
||||
|
{ |
||||
|
if (tagsById.TryGetValue(id, out var name)) |
||||
|
{ |
||||
|
asset.Tags.Add(name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
asset.Tags?.Clear(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// 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 HashSet<string> Tags { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public interface IAssetQueryService |
||||
|
{ |
||||
|
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Query query); |
||||
|
|
||||
|
Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class ContentQueryContext : Cloneable<ContentQueryContext> |
||||
|
{ |
||||
|
public string SchemaIdOrName { get; private set; } |
||||
|
|
||||
|
public QueryContext Base { get; private set; } |
||||
|
|
||||
|
public ContentQueryContext(QueryContext @base) |
||||
|
{ |
||||
|
Guard.NotNull(@base, nameof(@base)); |
||||
|
|
||||
|
Base = @base; |
||||
|
} |
||||
|
|
||||
|
public ContentQueryContext WithSchemaName(string name) |
||||
|
{ |
||||
|
return Clone(c => c.SchemaIdOrName = name); |
||||
|
} |
||||
|
|
||||
|
public ContentQueryContext WithArchived(bool archived) |
||||
|
{ |
||||
|
return Clone(c => c.Base = c.Base.WithArchived(archived)); |
||||
|
} |
||||
|
|
||||
|
public ContentQueryContext WithFlatten(bool flatten) |
||||
|
{ |
||||
|
return Clone(c => c.Base = c.Base.WithFlatten(flatten)); |
||||
|
} |
||||
|
|
||||
|
public ContentQueryContext WithSchemaId(Guid id) |
||||
|
{ |
||||
|
return Clone(c => c.SchemaIdOrName = id.ToString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<string> Tags { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities |
||||
|
{ |
||||
|
public sealed class Query : Cloneable<Query> |
||||
|
{ |
||||
|
public static readonly Query Empty = new Query(); |
||||
|
|
||||
|
public List<Guid> Ids { get; private set; } |
||||
|
|
||||
|
public string ODataQuery { get; private set; } |
||||
|
|
||||
|
public Query WithODataQuery(string odataQuery) |
||||
|
{ |
||||
|
return Clone(c => c.ODataQuery = odataQuery); |
||||
|
} |
||||
|
|
||||
|
public Query WithIds(IEnumerable<Guid> ids) |
||||
|
{ |
||||
|
return Clone(c => c.Ids = ids.ToList()); |
||||
|
} |
||||
|
|
||||
|
public Query WithIds(string ids) |
||||
|
{ |
||||
|
if (!string.IsNullOrEmpty(ids)) |
||||
|
{ |
||||
|
return Clone(c => |
||||
|
{ |
||||
|
c.Ids = new List<Guid>(); |
||||
|
|
||||
|
foreach (var id in ids.Split(',')) |
||||
|
{ |
||||
|
if (Guid.TryParse(id, out var guid)) |
||||
|
{ |
||||
|
c.Ids.Add(guid); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return this; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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<HashSet<string>> NormalizeTagsAsync(Guid appId, string category, HashSet<string> names, HashSet<string> ids) |
||||
|
{ |
||||
|
return GetGrain(appId, category).NormalizeTagsAsync(names, ids); |
||||
|
} |
||||
|
|
||||
|
public Task<HashSet<string>> GetTagIdsAsync(Guid appId, string category, HashSet<string> names) |
||||
|
{ |
||||
|
return GetGrain(appId, category).GetTagIdsAsync(names); |
||||
|
} |
||||
|
|
||||
|
public Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string category, HashSet<string> ids) |
||||
|
{ |
||||
|
return GetGrain(appId, category).DenormalizeTagsAsync(ids); |
||||
|
} |
||||
|
|
||||
|
public Task<Dictionary<string, int>> 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<ITagGrain>($"{appId}_{category}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids); |
||||
|
|
||||
|
Task<HashSet<string>> GetTagIdsAsync(HashSet<string> names); |
||||
|
|
||||
|
Task<Dictionary<string, string>> DenormalizeTagsAsync(HashSet<string> ids); |
||||
|
|
||||
|
Task<Dictionary<string, int>> GetTagsAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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<HashSet<string>> NormalizeTagsAsync(Guid appId, string category, HashSet<string> names, HashSet<string> ids); |
||||
|
|
||||
|
Task<HashSet<string>> GetTagIdsAsync(Guid appId, string category, HashSet<string> names); |
||||
|
|
||||
|
Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string category, HashSet<string> ids); |
||||
|
|
||||
|
Task<Dictionary<string, int>> GetTagsAsync(Guid appId, string category); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,151 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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<string> store; |
||||
|
private IPersistence<State> persistence; |
||||
|
private State state = new State(); |
||||
|
|
||||
|
[CollectionName("Index_Tags")] |
||||
|
public sealed class State |
||||
|
{ |
||||
|
public Dictionary<string, TagInfo> Tags { get; set; } = new Dictionary<string, TagInfo>(); |
||||
|
} |
||||
|
|
||||
|
public sealed class TagInfo |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public int Count { get; set; } = 1; |
||||
|
} |
||||
|
|
||||
|
public TagGrain(IStore<string> store) |
||||
|
{ |
||||
|
Guard.NotNull(store, nameof(store)); |
||||
|
|
||||
|
this.store = store; |
||||
|
} |
||||
|
|
||||
|
public override Task OnActivateAsync(string key) |
||||
|
{ |
||||
|
persistence = store.WithSnapshots<TagGrain, State, string>(key, s => |
||||
|
{ |
||||
|
state = s; |
||||
|
}); |
||||
|
|
||||
|
return persistence.ReadAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task<HashSet<string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids) |
||||
|
{ |
||||
|
var result = new HashSet<string>(); |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
if (ids == null || !ids.Contains(tagId)) |
||||
|
{ |
||||
|
found.Value.Count++; |
||||
|
} |
||||
|
} |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
public Task<HashSet<string>> GetTagIdsAsync(HashSet<string> names) |
||||
|
{ |
||||
|
var result = new HashSet<string>(); |
||||
|
|
||||
|
foreach (var name in names) |
||||
|
{ |
||||
|
var id = state.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(id)) |
||||
|
{ |
||||
|
result.Add(id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
public Task<Dictionary<string, string>> DenormalizeTagsAsync(HashSet<string> ids) |
||||
|
{ |
||||
|
var result = new Dictionary<string, string>(); |
||||
|
|
||||
|
foreach (var id in ids) |
||||
|
{ |
||||
|
if (state.Tags.TryGetValue(id, out var tagInfo)) |
||||
|
{ |
||||
|
result[id] = tagInfo.Name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
public Task<Dictionary<string, int>> GetTagsAsync() |
||||
|
{ |
||||
|
return Task.FromResult(state.Tags.Values.ToDictionary(x => x.Name, x => x.Count)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Tags |
||||
|
{ |
||||
|
public static class TagGroups |
||||
|
{ |
||||
|
public const string Assets = "Assets"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Assets |
||||
|
{ |
||||
|
[EventType(nameof(AssetTagged))] |
||||
|
public sealed class AssetTagged : AssetEvent |
||||
|
{ |
||||
|
public HashSet<string> Tags { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.MongoDb.OData |
||||
|
{ |
||||
|
public delegate object ConvertValue(string field, object value); |
||||
|
|
||||
|
public static class ValueConversion |
||||
|
{ |
||||
|
public static object Convert(string field, object value, ConvertValue converter = null) |
||||
|
{ |
||||
|
if (converter == null) |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
return converter(field, value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Squidex.Infrastructure |
||||
|
{ |
||||
|
public static class HashSet |
||||
|
{ |
||||
|
public static HashSet<T> Of<T>(params T[] items) |
||||
|
{ |
||||
|
return new HashSet<T>(items); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,2 +1,26 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@import '_mixins'; |
||||
|
|
||||
|
.section { |
||||
|
border-top: 1px solid $color-border; |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
.tag { |
||||
|
& { |
||||
|
padding: .25rem 0; |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
&.active, |
||||
|
&:hover { |
||||
|
background: $color-background; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
a.tag { |
||||
|
cursor: pointer !important; |
||||
|
} |
||||
@ -1,12 +1,15 @@ |
|||||
<input type="text" class="form-control" [attr.name]="inputName" (keydown)="onKeyDown($event)" (blur)="markTouched()" |
<div class="form-control {{class}}" (click)="input.focus()" [class.focus]="hasFocus" [class.disabled]="addInput.disabled"> |
||||
|
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled"> |
||||
|
{{item}} <i class="icon-close" (click)="remove(i)"></i> |
||||
|
</span> |
||||
|
|
||||
|
<input type="text" class="blank" [attr.name]="inputName" (keydown)="onKeyDown($event)" [class.hidden]="addInput.disabled" #input |
||||
|
(focus)="focus()" |
||||
|
(blur)="markTouched()" |
||||
|
(input)="adjustSize()" |
||||
[formControl]="addInput" |
[formControl]="addInput" |
||||
autocomplete="off" |
autocomplete="off" |
||||
autocorrect="off" |
autocorrect="off" |
||||
autocapitalize="off" |
autocapitalize="off" |
||||
placeholder="Press enter to add new item"> |
placeholder="+Tag"> |
||||
|
</div> |
||||
<div class="items"> |
|
||||
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled"> |
|
||||
{{item}} <i class="icon-close" (click)="remove(i)"></i> |
|
||||
</span> |
|
||||
</div> |
|
||||
@ -0,0 +1,117 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Tags; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public class AssetQueryServiceTests |
||||
|
{ |
||||
|
private readonly ITagService tagService = A.Fake<ITagService>(); |
||||
|
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); |
||||
|
private readonly IAppEntity app = A.Fake<IAppEntity>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly string appName = "my-app"; |
||||
|
private readonly ClaimsPrincipal user; |
||||
|
private readonly ClaimsIdentity identity = new ClaimsIdentity(); |
||||
|
private readonly QueryContext context; |
||||
|
private readonly AssetQueryService sut; |
||||
|
|
||||
|
public AssetQueryServiceTests() |
||||
|
{ |
||||
|
user = new ClaimsPrincipal(identity); |
||||
|
|
||||
|
A.CallTo(() => app.Id).Returns(appId); |
||||
|
A.CallTo(() => app.Name).Returns(appName); |
||||
|
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.English); |
||||
|
|
||||
|
context = QueryContext.Create(app, user); |
||||
|
|
||||
|
A.CallTo(() => tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, A<HashSet<string>>.That.IsSameSequenceAs("id1", "id2", "id3"))) |
||||
|
.Returns(new Dictionary<string, string> |
||||
|
{ |
||||
|
["id1"] = "name1", |
||||
|
["id2"] = "name2", |
||||
|
["id3"] = "name3" |
||||
|
}); |
||||
|
|
||||
|
sut = new AssetQueryService(tagService, assetRepository); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_find_asset_by_id_and_resolve_tags() |
||||
|
{ |
||||
|
var id = Guid.NewGuid(); |
||||
|
|
||||
|
A.CallTo(() => assetRepository.FindAssetAsync(id)) |
||||
|
.Returns(CreateAsset(id, "id1", "id2", "id3")); |
||||
|
|
||||
|
var result = await sut.FindAssetAsync(context, id); |
||||
|
|
||||
|
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_load_assets_from_ids_and_resolve_tags() |
||||
|
{ |
||||
|
var id1 = Guid.NewGuid(); |
||||
|
var id2 = Guid.NewGuid(); |
||||
|
|
||||
|
var ids = HashSet.Of(id1, id2); |
||||
|
|
||||
|
A.CallTo(() => assetRepository.QueryAsync(appId, A<HashSet<Guid>>.That.IsSameSequenceAs(ids))) |
||||
|
.Returns(ResultList.Create(8, |
||||
|
CreateAsset(id1, "id1", "id2", "id3"), |
||||
|
CreateAsset(id2))); |
||||
|
|
||||
|
var result = await sut.QueryAsync(context, Query.Empty.WithIds(ids)); |
||||
|
|
||||
|
Assert.Equal(8, result.Total); |
||||
|
Assert.Equal(2, result.Count); |
||||
|
|
||||
|
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result[0].Tags); |
||||
|
Assert.Empty(result[1].Tags); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_load_assets_with_query_and_resolve_tags() |
||||
|
{ |
||||
|
A.CallTo(() => assetRepository.QueryAsync(appId, "my-query")) |
||||
|
.Returns(ResultList.Create(8, |
||||
|
CreateAsset(Guid.NewGuid(), "id1", "id2"), |
||||
|
CreateAsset(Guid.NewGuid(), "id2", "id3"))); |
||||
|
|
||||
|
var result = await sut.QueryAsync(context, Query.Empty.WithODataQuery("my-query")); |
||||
|
|
||||
|
Assert.Equal(8, result.Total); |
||||
|
Assert.Equal(2, result.Count); |
||||
|
|
||||
|
Assert.Equal(HashSet.Of("name1", "name2"), result[0].Tags); |
||||
|
Assert.Equal(HashSet.Of("name2", "name3"), result[1].Tags); |
||||
|
} |
||||
|
|
||||
|
private IAssetEntity CreateAsset(Guid id, params string[] tags) |
||||
|
{ |
||||
|
var asset = A.Fake<IAssetEntity>(); |
||||
|
|
||||
|
A.CallTo(() => asset.Id).Returns(id); |
||||
|
A.CallTo(() => asset.Tags).Returns(HashSet.Of(tags)); |
||||
|
|
||||
|
return asset; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 FakeItEasy; |
||||
|
using Orleans; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Tags |
||||
|
{ |
||||
|
public class GrainTagServiceTests |
||||
|
{ |
||||
|
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); |
||||
|
private readonly ITagGrain grain = A.Fake<ITagGrain>(); |
||||
|
private readonly Guid appId = Guid.NewGuid(); |
||||
|
private readonly GrainTagService sut; |
||||
|
|
||||
|
public GrainTagServiceTests() |
||||
|
{ |
||||
|
A.CallTo(() => grainFactory.GetGrain<ITagGrain>($"{appId}_Assets", null)) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
sut = new GrainTagService(grainFactory); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_when_retrieving_tas() |
||||
|
{ |
||||
|
await sut.GetTagsAsync(appId, TagGroups.Assets); |
||||
|
|
||||
|
A.CallTo(() => grain.GetTagsAsync()) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_when_resolving_tag_ids() |
||||
|
{ |
||||
|
var tagNames = new HashSet<string>(); |
||||
|
|
||||
|
await sut.GetTagIdsAsync(appId, TagGroups.Assets, tagNames); |
||||
|
|
||||
|
A.CallTo(() => grain.GetTagIdsAsync(tagNames)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_when_denormalizing_tags() |
||||
|
{ |
||||
|
var tagIds = new HashSet<string>(); |
||||
|
|
||||
|
await sut.DenormalizeTagsAsync(appId, TagGroups.Assets, tagIds); |
||||
|
|
||||
|
A.CallTo(() => grain.DenormalizeTagsAsync(tagIds)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_call_grain_when_normalizing_tags() |
||||
|
{ |
||||
|
var tagIds = new HashSet<string>(); |
||||
|
var tagNames = new HashSet<string>(); |
||||
|
|
||||
|
await sut.NormalizeTagsAsync(appId, TagGroups.Assets, tagNames, tagIds); |
||||
|
|
||||
|
A.CallTo(() => grain.NormalizeTagsAsync(tagNames, tagIds)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 FakeItEasy; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Tags |
||||
|
{ |
||||
|
public class TagGrainTests |
||||
|
{ |
||||
|
private readonly IStore<string> store = A.Fake<IStore<string>>(); |
||||
|
private readonly IPersistence<TagGrain.State> persistence = A.Fake<IPersistence<TagGrain.State>>(); |
||||
|
private readonly TagGrain sut; |
||||
|
|
||||
|
public TagGrainTests() |
||||
|
{ |
||||
|
A.CallTo(() => store.WithSnapshots(A<Type>.Ignored, A<string>.Ignored, A<Func<TagGrain.State, Task>>.Ignored)) |
||||
|
.Returns(persistence); |
||||
|
|
||||
|
sut = new TagGrain(store); |
||||
|
sut.OnActivateAsync(string.Empty).Wait(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_tags_to_grain() |
||||
|
{ |
||||
|
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
||||
|
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag2", "tag3"), null); |
||||
|
|
||||
|
var allTags = await sut.GetTagsAsync(); |
||||
|
|
||||
|
Assert.Equal(new Dictionary<string, int> |
||||
|
{ |
||||
|
["tag1"] = 1, |
||||
|
["tag2"] = 2, |
||||
|
["tag3"] = 1 |
||||
|
}, allTags); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_add_tags_if_already_added() |
||||
|
{ |
||||
|
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
||||
|
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2", "tag3"), result1); |
||||
|
|
||||
|
var allTags = await sut.GetTagsAsync(); |
||||
|
|
||||
|
Assert.Equal(new Dictionary<string, int> |
||||
|
{ |
||||
|
["tag1"] = 1, |
||||
|
["tag2"] = 1, |
||||
|
["tag3"] = 1 |
||||
|
}, allTags); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_remove_tags_from_grain() |
||||
|
{ |
||||
|
var result1 = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
||||
|
var result2 = await sut.NormalizeTagsAsync(HashSet.Of("tag2", "tag3"), null); |
||||
|
|
||||
|
await sut.NormalizeTagsAsync(null, result1); |
||||
|
|
||||
|
var allTags = await sut.GetTagsAsync(); |
||||
|
|
||||
|
Assert.Equal(new Dictionary<string, int> |
||||
|
{ |
||||
|
["tag2"] = 1, |
||||
|
["tag3"] = 1 |
||||
|
}, allTags); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_resolve_tag_names() |
||||
|
{ |
||||
|
var tagIds = await sut.NormalizeTagsAsync(HashSet.Of("tag1", "tag2"), null); |
||||
|
var tagNames = await sut.GetTagIdsAsync(HashSet.Of("tag1", "tag2", "invalid1")); |
||||
|
|
||||
|
Assert.Equal(tagIds, tagNames); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue