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" |
[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> |
|
||||
Loading…
Reference in new issue