mirror of https://github.com/Squidex/squidex.git
40 changed files with 642 additions and 158 deletions
@ -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; } |
|||
} |
|||
} |
|||
@ -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<string[]> NormalizeTagsAsync(Guid appId, string category, string[] names, string[] ids) |
|||
{ |
|||
return GetGrain(appId, category).NormalizeTagsAsync(names, ids); |
|||
} |
|||
|
|||
public Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string category, 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,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<string[]> NormalizeTagsAsync(string[] names, string[] ids); |
|||
|
|||
Task<Dictionary<string, string>> DenormalizeTagsAsync(string[] ids); |
|||
|
|||
Task<Dictionary<string, int>> GetTagsAsync(); |
|||
} |
|||
} |
|||
@ -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<string[]> NormalizeTagsAsync(Guid appId, string category, string[] names, string[] ids); |
|||
|
|||
Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string category, string[] ids); |
|||
|
|||
Task<Dictionary<string, int>> GetTagsAsync(Guid appId, string category); |
|||
} |
|||
} |
|||
@ -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<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<string[]> NormalizeTagsAsync(string[] names, string[] ids) |
|||
{ |
|||
var result = new List<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; |
|||
} |
|||
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<Dictionary<string, string>> DenormalizeTagsAsync(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,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; } |
|||
} |
|||
} |
|||
@ -1,12 +1,19 @@ |
|||
<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"> |
|||
<span class="items"> |
|||
<span class="item-container" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled"> |
|||
<span class="item"> |
|||
{{item}} <i class="icon-close" (click)="remove(i)"></i> |
|||
</span> |
|||
</span> |
|||
</span> |
|||
|
|||
<input type="text" class="blank" [attr.name]="inputName" (keydown)="onKeyDown($event)" #input |
|||
(focus)="focus()" |
|||
(blur)="markTouched()" |
|||
(input)="adjustSize($event.target)" |
|||
[formControl]="addInput" |
|||
autocomplete="off" |
|||
autocorrect="off" |
|||
autocapitalize="off" |
|||
placeholder="Press enter to add new item"> |
|||
|
|||
<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> |
|||
placeholder="+Tag"> |
|||
</div> |
|||
Loading…
Reference in new issue