Browse Source

Tags

pull/308/head
Sebastian 8 years ago
parent
commit
704bdd8aef
  1. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  2. 6
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  3. 40
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  4. 14
      src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs
  5. 5
      src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
  7. 3
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  8. 6
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  9. 6
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  10. 49
      src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs
  11. 22
      src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs
  12. 22
      src/Squidex.Domain.Apps.Entities/Tags/ITagService.cs
  13. 129
      src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs
  14. 17
      src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs
  15. 2
      src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  16. 5
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  17. 18
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs
  18. 4
      src/Squidex/Config/Domain/EntitiesServices.cs
  19. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  20. 6
      src/Squidex/app/features/settings/pages/clients/client.component.html
  21. 43
      src/Squidex/app/features/settings/pages/clients/client.component.scss
  22. 23
      src/Squidex/app/framework/angular/forms/tag-editor.component.html
  23. 46
      src/Squidex/app/framework/angular/forms/tag-editor.component.scss
  24. 59
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  25. 6
      src/Squidex/app/framework/angular/image-source.directive.ts
  26. 54
      src/Squidex/app/shared/components/asset.component.html
  27. 14
      src/Squidex/app/shared/components/asset.component.scss
  28. 35
      src/Squidex/app/shared/components/asset.component.ts
  29. 33
      src/Squidex/app/shared/services/assets.service.spec.ts
  30. 15
      src/Squidex/app/shared/services/assets.service.ts
  31. 26
      src/Squidex/app/shared/state/assets.forms.ts
  32. 8
      src/Squidex/app/shared/state/assets.state.spec.ts
  33. 58
      src/Squidex/app/theme/_forms.scss
  34. 2
      src/Squidex/app/theme/_vars.scss
  35. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  36. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  37. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs
  38. 2
      tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs
  39. 2
      tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs
  40. 2
      tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs

4
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -38,6 +38,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement] [BsonElement]
public string FileName { get; set; } public string FileName { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string[] Tags { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public long FileSize { get; set; } public long FileSize { get; set; }

6
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -72,14 +72,14 @@ namespace Squidex.Domain.Apps.Entities.Apps
}); });
case AssignContributor assigneContributor: case AssignContributor assigneContributor:
return UpdateReturnAsync(assigneContributor, (Func<AssignContributor, Task<object>>)(async c => return UpdateReturnAsync(assigneContributor, async c =>
{ {
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId)); await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
AssignContributor(c); AssignContributor(c);
return EntityCreatedResult.Create(c.ContributorId, (long)base.Version); return EntityCreatedResult.Create(c.ContributorId, (long)Version);
})); });
case RemoveContributor removeContributor: case RemoveContributor removeContributor:
return UpdateAsync(removeContributor, c => return UpdateAsync(removeContributor, c =>

40
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.Commands;
using Squidex.Domain.Apps.Entities.Assets.Guards; using Squidex.Domain.Apps.Entities.Assets.Guards;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -24,33 +25,40 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetGrain : SquidexDomainObjectGrainLogSnapshots<AssetState>, IAssetGrain public sealed class AssetGrain : SquidexDomainObjectGrainLogSnapshots<AssetState>, IAssetGrain
{ {
public AssetGrain(IStore<Guid> store, ISemanticLog log) private readonly ITagService tagService;
public AssetGrain(IStore<Guid> store, ITagService tagService, ISemanticLog log)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(tagService, nameof(tagService));
this.tagService = tagService;
} }
protected override Task<object> ExecuteAsync(IAggregateCommand command) protected override Task<object> ExecuteAsync(IAggregateCommand command)
{ {
VerifyNotDeleted();
switch (command) switch (command)
{ {
case CreateAsset createRule: case CreateAsset createRule:
return CreateReturnAsync(createRule, (Func<CreateAsset, object>)(c => return CreateReturnAsync(createRule, c =>
{ {
GuardAsset.CanCreate(c); GuardAsset.CanCreate(c);
Create(c); Create(c);
return new AssetSavedResult((long)base.Version, Snapshot.FileVersion); return new AssetSavedResult(Version, Snapshot.FileVersion);
})); });
case UpdateAsset updateRule: case UpdateAsset updateRule:
return UpdateReturnAsync(updateRule, (Func<UpdateAsset, object>)(c => return UpdateAsync(updateRule, c =>
{ {
GuardAsset.CanUpdate(c); GuardAsset.CanUpdate(c);
Update(c); Update(c);
return new AssetSavedResult((long)base.Version, Snapshot.FileVersion); return new AssetSavedResult(Version, Snapshot.FileVersion);
})); });
case RenameAsset renameAsset: case RenameAsset renameAsset:
return UpdateAsync(renameAsset, c => return UpdateAsync(renameAsset, c =>
{ {
@ -65,6 +73,15 @@ namespace Squidex.Domain.Apps.Entities.Assets
Delete(c); 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: default:
throw new NotSupportedException(); throw new NotSupportedException();
} }
@ -105,18 +122,19 @@ namespace Squidex.Domain.Apps.Entities.Assets
public void Delete(DeleteAsset command) public void Delete(DeleteAsset command)
{ {
VerifyNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize }));
} }
public void Rename(RenameAsset command) public void Rename(RenameAsset command)
{ {
VerifyNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); RaiseEvent(SimpleMapper.Map(command, new AssetRenamed()));
} }
public void Tag(TagAsset command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetTagged()));
}
private void RaiseEvent(AppEvent @event) private void RaiseEvent(AppEvent @event)
{ {
if (@event.AppId == null) if (@event.AppId == null)

14
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; }
}
}

5
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)); Guard.NotNull(command, nameof(command));
} }
public static void CanTag(TagAsset command)
{
Guard.NotNull(command, nameof(command));
}
public static void CanUpdate(UpdateAsset command) public static void CanUpdate(UpdateAsset command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));

2
src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs

@ -22,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
string MimeType { get; } string MimeType { get; }
string[] Tags { get; }
long FileVersion { get; } long FileVersion { get; }
} }
} }

3
src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs

@ -28,6 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[JsonProperty] [JsonProperty]
public string MimeType { get; set; } public string MimeType { get; set; }
[JsonProperty]
public string[] Tags { get; set; }
[JsonProperty] [JsonProperty]
public long FileVersion { get; set; } public long FileVersion { get; set; }

6
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
switch (command) switch (command)
{ {
case CreateContent createContent: case CreateContent createContent:
return CreateReturnAsync(createContent, (Func<CreateContent, Task<object>>)(async c => return CreateReturnAsync(createContent, async c =>
{ {
var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, () => "Failed to create content."); 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); Create(c);
return EntityCreatedResult.Create(c.Data, (long)base.Version); return EntityCreatedResult.Create(c.Data, (long)Version);
})); });
case UpdateContent updateContent: case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, c => return UpdateReturnAsync(updateContent, c =>

6
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
switch (command) switch (command)
{ {
case AddField addField: case AddField addField:
return UpdateReturnAsync(addField, (Func<AddField, object>)(c => return UpdateAsync(addField, c =>
{ {
GuardSchemaField.CanAdd(Snapshot.SchemaDef, 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; 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: case CreateSchema createSchema:
return CreateAsync(createSchema, async c => return CreateAsync(createSchema, async c =>

49
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<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}");
}
}
}

22
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<string[]> NormalizeTagsAsync(string[] names, string[] ids);
Task<Dictionary<string, string>> DenormalizeTagsAsync(string[] ids);
Task<Dictionary<string, int>> GetTagsAsync();
}
}

22
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<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);
}
}

129
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<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));
}
}
}

17
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; }
}
}

2
src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.Commands
return InvokeAsync(command, handler, true); return InvokeAsync(command, handler, true);
} }
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{ {
return InvokeAsync(command, handler?.ToAsync(), true); return InvokeAsync(command, handler?.ToAsync(), true);
} }

5
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -39,6 +39,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required] [Required]
public string FileType { get; set; } public string FileType { get; set; }
/// <summary>
/// The asset tags.
/// </summary>
public string[] Tags { get; set; }
/// <summary> /// <summary>
/// The size of the file in bytes. /// The size of the file in bytes.
/// </summary> /// </summary>

18
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs

@ -8,7 +8,6 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
{ {
@ -20,9 +19,22 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required] [Required]
public string FileName { get; set; } public string FileName { get; set; }
public RenameAsset ToCommand(Guid id) /// <summary>
/// The new asset tags.
/// </summary>
[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 };
}
} }
} }
} }

4
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.Rules.Indexes;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
@ -104,6 +105,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetCommandMiddleware>() services.AddSingletonAs<AssetCommandMiddleware>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<GrainTagService>()
.As<ITagService>();
services.AddSingletonAs<GrainCommandMiddleware<AppCommand, IAppGrain>>() services.AddSingletonAs<GrainCommandMiddleware<AppCommand, IAppGrain>>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();

2
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -2,7 +2,7 @@
<sqx-panel desiredWidth="60rem" [showSidebar]="true"> <sqx-panel desiredWidth="60rem" [showSidebar]="true">
<ng-container title> <ng-container title>
<i class="schema-edit icon-pencil" (click)="editSchemaDialog.show()"></i> {{schema.displayName}} <i class="schema-edit icon-pencil" (click)="editSchemaDialog.show()"></i> <span (dblclick)="editSchemaDialog.show()">{{schema.displayName}}</span>
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>

6
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -6,7 +6,7 @@
<div class="form-group mr-1"> <div class="form-group mr-1">
<sqx-control-errors for="name"></sqx-control-errors> <sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control client-name enabled" formControlName="name" maxlength="20" sqxFocusOnInit (keydown)="onKeyDown($event.keyCode)" /> <input type="text" class="form-control client-name form-underlined" formControlName="name" maxlength="20" sqxFocusOnInit (keydown)="onKeyDown($event.keyCode)" />
</div> </div>
<button type="submit" class="btn btn-primary" [disabled]="!renameForm.form.valid || !renameForm.form.dirty">Save</button> <button type="submit" class="btn btn-primary" [disabled]="!renameForm.form.valid || !renameForm.form.dirty">Save</button>
@ -17,8 +17,8 @@
</form> </form>
<ng-container *ngIf="!isRenaming"> <ng-container *ngIf="!isRenaming">
<h3 class="client-name"> <h3 class="client-name" (dblclick)="toggleRename()">
<span (dblclick)="toggleRename()">{{client.name}}</span> {{client.name}}
</h3> </h3>
<i class="client-edit icon-pencil" (click)="toggleRename()"></i> <i class="client-edit icon-pencil" (click)="toggleRename()"></i>

43
src/Squidex/app/features/settings/pages/clients/client.component.scss

@ -30,31 +30,13 @@ $color-editor: #eceeef;
} }
&-name { &-name {
& { padding: .375rem 0;
@include border-radius(.25rem); font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
margin: 0; font-size: 1.2rem;
margin-left: -.6rem; font-weight: normal;
height: 2.5rem; line-height: 1.5rem;
padding: 0 .6rem; display: inline-block;
border: 0; margin: 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;
}
} }
&-header { &-header {
@ -66,12 +48,15 @@ $color-editor: #eceeef;
} }
} }
.col-form-label { h3 {
text-align: left; &.client-name {
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
}
} }
.btn-cancel { .col-form-label {
padding: .4rem; text-align: left;
} }
.form-check { .form-check {

23
src/Squidex/app/framework/angular/forms/tag-editor.component.html

@ -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>

46
src/Squidex/app/framework/angular/forms/tag-editor.component.scss

@ -1,25 +1,57 @@
@import '_mixins'; @import '_mixins';
@import '_vars'; @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 { .items {
margin-top: .4rem; margin-left: -2px;
min-height: 1.6rem;
} }
.item { .item {
& { & {
@include border-radius(.8rem); @include border-radius(10px);
display: inline-block; display: inline-block;
color: $color-dark-foreground; color: $color-dark-foreground;
margin-right: .4rem; cursor: default;
margin-bottom: .25rem; height: 20px;
min-height: 1.6rem;
padding: 0 .6rem; padding: 0 .6rem;
background: $color-theme-blue; background: $color-theme-blue;
border: 0; border: 0;
font-size: .8rem; font-size: .8rem;
font-weight: normal; font-weight: normal;
line-height: 1.6rem; line-height: 20px;
}
&-container {
padding: 2px;
} }
&.disabled { &.disabled {

59
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -5,12 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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 { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
const KEY_ENTER = 13; const KEY_SPACE = 32;
const KEY_DELETE = 8;
export interface Converter { export interface Converter {
convert(input: string): any; convert(input: string): any;
@ -81,9 +82,17 @@ export class TagEditorComponent implements ControlValueAccessor {
@Input() @Input()
public useDefaultValue = true; public useDefaultValue = true;
@Input()
public class: string;
@Input() @Input()
public inputName = 'tag-editor'; public inputName = 'tag-editor';
@ViewChild('input')
public inputElement: ElementRef;
public hasFocus = false;
public items: any[] = []; public items: any[] = [];
public addInput = new FormControl(); 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)]); this.updateItems([...this.items.slice(0, index), ...this.items.splice(index + 1)]);
} }
public markTouched() { public focus() {
this.callTouched(); this.hasFocus = true;
} }
private resetForm() { private resetForm() {
this.adjustSize();
this.addInput.reset(); 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 = <any>((ctx.measureText(this.inputElement.nativeElement.value).width + 20) + 'px');
}
}
}
public onKeyDown(event: KeyboardEvent) { public onKeyDown(event: KeyboardEvent) {
if (event.keyCode === KEY_ENTER) { if (event.keyCode === KEY_SPACE) {
const value = <string>this.addInput.value; const value = <string>this.addInput.value;
if (this.converter.isValidInput(value)) { if (value && this.converter.isValidInput(value)) {
const converted = this.converter.convert(value); const converted = this.converter.convert(value);
this.updateItems([...this.items, converted]); this.updateItems([...this.items, converted]);
this.resetForm(); this.resetForm();
return false;
}
} else if (event.keyCode === KEY_DELETE) {
const value = <string>this.addInput.value;
if (!value || value.length === 0) {
this.updateItems(this.items.slice(0, this.items.length - 2));
return false; return false;
} }
} }
@ -151,4 +194,6 @@ export class TagEditorComponent implements ControlValueAccessor {
this.callChange(this.items); this.callChange(this.items);
} }
} }
} }
let canvas: HTMLCanvasElement | null = null;

6
src/Squidex/app/framework/angular/image-source.directive.ts

@ -48,12 +48,12 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
this.parentResizeListener = this.parentResizeListener =
this.renderer.listen(this.parent, 'resize', () => { this.renderer.listen(this.parent, 'resize', () => {
this.resize(this.parent); this.resize();
}); });
} }
public ngAfterViewInit() { public ngAfterViewInit() {
this.resize(this.parent); this.resize();
} }
public ngOnChanges() { public ngOnChanges() {
@ -75,7 +75,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
this.retryLoadingImage(); this.retryLoadingImage();
} }
private resize(parent: any) { private resize() {
this.size = this.parent.getBoundingClientRect(); this.size = this.parent.getBoundingClientRect();
this.renderer.setStyle(this.element.nativeElement, 'display', 'inline-block'); this.renderer.setStyle(this.element.nativeElement, 'display', 'inline-block');

54
src/Squidex/app/shared/components/asset.component.html

@ -1,4 +1,4 @@
<div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)"> <div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)">
<div class="card-body"> <div class="card-body">
<div class="file-preview" *ngIf="asset && progress == 0" @fade> <div class="file-preview" *ngIf="asset && progress == 0" @fade>
<span class="file-type" *ngIf="asset.fileType"> <span class="file-type" *ngIf="asset.fileType">
@ -16,17 +16,14 @@
<div class="file-overlay-background"></div> <div class="file-overlay-background"></div>
<div class="file-menu"> <div class="file-menu">
<a class="file-edit ml-1" *ngIf="!isDisabled" (click)="renameDialog.show()"> <a class="file-download" [attr.href]="asset | sqxAssetUrl" target="_blank" (click)="$event.stopPropagation()">
<i class="icon-pencil"></i>
</a>
<a class="file-download ml-1" [attr.href]="asset | sqxAssetUrl" target="_blank" (click)="$event.stopPropagation()">
<i class="icon-download"></i> <i class="icon-download"></i>
</a> </a>
<a class="file-delete ml-1" (click)="deleting.emit(asset); $event.stopPropagation()" *ngIf="!isDisabled && !removeMode"> <a class="file-delete ml-2" (click)="deleting.emit(asset); $event.stopPropagation()" *ngIf="!isDisabled && !removeMode">
<i class="icon-delete"></i> <i class="icon-delete"></i>
</a> </a>
<a class="file-delete ml-1" (click)="removing.emit(asset); $event.stopPropagation()" *ngIf="removeMode"> <a class="file-delete ml-2" (click)="removing.emit(asset); $event.stopPropagation()" *ngIf="removeMode">
<i class="icon-close"></i> <i class="icon-close"></i>
</a> </a>
</div> </div>
@ -44,8 +41,20 @@
</div> </div>
</div> </div>
<div class="card-footer" *ngIf="asset && progress == 0"> <div class="card-footer" *ngIf="asset && progress == 0">
<div class="file-name" [attr.title]="asset.fileName"> <div>
{{asset.fileName}} <div *ngIf="!renaming" class="file-name editable" [attr.title]="asset.fileName" (dblclick)="renameStart()">
{{asset.fileName}}
</div>
<div *ngIf="renaming">
<form [formGroup]="renameForm.form" (ngSubmit)="renameAsset()">
<sqx-control-errors for="name" [submitted]="renameForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control form-underlined editable" id="assetName" formControlName="name" autocomplete="off" sqxFocusOnInit (blur)="renameCancel()" />
</form>
</div>
</div>
<div>
<sqx-tag-editor class="blank"></sqx-tag-editor>
</div> </div>
<div class="file-info"> <div class="file-info">
<ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}} <ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}
@ -61,29 +70,4 @@
<span class="drop-overlay-text">Drop to update</span> <span class="drop-overlay-text">Drop to update</span>
</div> </div>
</div> </div>
<ng-container *sqxModalView="renameDialog;onRoot:true">
<form [formGroup]="renameForm.form" (ngSubmit)="renameAsset()">
<sqx-modal-dialog (closed)="cancelRenameAsset()">
<ng-container title>
Rename asset
</ng-container>
<ng-container content>
<div class="form-group">
<label for="assetName">Name</label>
<sqx-control-errors for="name" [submitted]="renameForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="assetName" formControlName="name" autocomplete="off" sqxFocusOnInit />
</div>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="cancelRenameAsset()">Cancel</button>
<button type="submit" class="float-right btn btn-success">Rename</button>
</ng-container>
</sqx-modal-dialog>
</form>
</ng-container>

14
src/Squidex/app/shared/components/asset.component.scss

@ -73,7 +73,6 @@
.card { .card {
& { & {
@include overlay-container; @include overlay-container;
height: $asset-height;
} }
&.selectable { &.selectable {
@ -82,6 +81,7 @@
&-body { &-body {
position: relative; position: relative;
height: 0.7 * $asset-height;
} }
&-footer { &-footer {
@ -89,7 +89,6 @@
background: transparent; background: transparent;
padding: .8rem; padding: .8rem;
padding-top: .4rem; padding-top: .4rem;
height: 70px;
} }
} }
@ -103,7 +102,8 @@
} }
&-image { &-image {
height: 100%; min-height: 100%;
max-height: 100%;
} }
&-preview { &-preview {
@ -122,6 +122,10 @@
} }
} }
&-info {
margin-top: .25rem;
}
&-user { &-user {
@include absolute(auto, auto, 1.7rem, .5rem); @include absolute(auto, auto, 1.7rem, .5rem);
} }
@ -178,4 +182,8 @@
@include asset-type; @include asset-type;
} }
} }
}
.editable {
height: 2rem;
} }

35
src/Squidex/app/shared/components/asset.component.ts

@ -14,12 +14,11 @@ import {
AssetsService, AssetsService,
AuthService, AuthService,
DateTime, DateTime,
DialogModel,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
RenameAssetDto,
RenameAssetForm, RenameAssetForm,
Types, Types,
UpdateAssetDto,
Versioned Versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -68,7 +67,9 @@ export class AssetComponent implements OnInit {
@Output() @Output()
public failed = new EventEmitter(); public failed = new EventEmitter();
public renameDialog = new DialogModel(); public renaming = false;
public isTagging = false;
public renameForm = new RenameAssetForm(this.formBuilder); public renameForm = new RenameAssetForm(this.formBuilder);
public progress = 0; public progress = 0;
@ -121,17 +122,16 @@ export class AssetComponent implements OnInit {
} }
public renameAsset() { public renameAsset() {
const value = this.renameForm.submit(); const value = this.renameForm.submit(this.asset);
if (value) { 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) this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version)
.subscribe(dto => { .subscribe(dto => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true); this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true);
this.renameForm.submitCompleted(); this.renameCancel();
this.renameDialog.hide();
}, error => { }, error => {
this.dialogs.notifyError(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.renameForm.submitCompleted();
this.renameDialog.hide(); this.renaming = false;
}
public startTagging() {
if (!this.isDisabled) {
this.isTagging = true;
}
} }
private setProgress(progress = 0) { private setProgress(progress = 0) {
@ -162,14 +175,14 @@ export class AssetComponent implements OnInit {
} }
private updateAsset(asset: AssetDto, emitEvent: boolean) { private updateAsset(asset: AssetDto, emitEvent: boolean) {
this.renameForm.load({ name: asset.fileName });
this.asset = asset; this.asset = asset;
this.progress = 0; this.progress = 0;
if (emitEvent) { if (emitEvent) {
this.emitUpdated(asset); this.emitUpdated(asset);
} }
this.cancelRenameAsset(); this.renameCancel();
} }
} }

33
src/Squidex/app/shared/services/assets.service.spec.ts

@ -16,10 +16,11 @@ import {
AssetsDto, AssetsDto,
AssetsService, AssetsService,
DateTime, DateTime,
UpdateAssetDto, RenameAssetDto,
Version, Version,
Versioned Versioned
} from './../'; } from './../';
import { TagAssetDto } from '@appshared/services/assets.service';
describe('AssetDto', () => { describe('AssetDto', () => {
const creation = DateTime.today(); const creation = DateTime.today();
@ -30,7 +31,7 @@ describe('AssetDto', () => {
const newVersion = new Version('2'); const newVersion = new Version('2');
it('should update name property and user info when renaming', () => { 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); const asset_2 = asset_1.rename('new-name.png', modifier, newVersion, modified);
expect(asset_2.fileName).toEqual('new-name.png'); expect(asset_2.fileName).toEqual('new-name.png');
@ -42,7 +43,7 @@ describe('AssetDto', () => {
it('should update file properties when uploading', () => { it('should update file properties when uploading', () => {
const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2); 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); const asset_2 = asset_1.update(update, modifier, newVersion, modified);
expect(asset_2.fileSize).toEqual(2); expect(asset_2.fileSize).toEqual(2);
@ -110,6 +111,7 @@ describe('AssetsService', () => {
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
tags: undefined,
version: 11 version: 11
}, },
{ {
@ -126,6 +128,7 @@ describe('AssetsService', () => {
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
tags: ['tag1', 'tag2'],
version: 22 version: 22
} }
] ]
@ -145,6 +148,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
[],
'http://service/p/api/assets/id1', 'http://service/p/api/assets/id1',
new Version('11')), new Version('11')),
new AssetDto('id2', 'Created2', 'LastModifiedBy2', new AssetDto('id2', 'Created2', 'LastModifiedBy2',
@ -158,6 +162,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
['tag1', 'tag2'],
'http://service/p/api/assets/id2', 'http://service/p/api/assets/id2',
new Version('22')) new Version('22'))
])); ]));
@ -190,7 +195,8 @@ describe('AssetsService', () => {
mimeType: 'image/png', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048 pixelHeight: 2048,
tags: ['tag1', 'tag2']
}, { }, {
headers: { headers: {
etag: '2' etag: '2'
@ -210,6 +216,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
['tag1', 'tag2'],
'http://service/p/api/assets/id1', 'http://service/p/api/assets/id1',
new Version('2'))); new Version('2')));
})); }));
@ -284,6 +291,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
[],
'http://service/p/api/assets/id1', 'http://service/p/api/assets/id1',
new Version('2'))); new Version('2')));
})); }));
@ -323,7 +331,22 @@ describe('AssetsService', () => {
it('should make put request to update asset', it('should make put request to update asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { 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(); assetsService.putAsset('my-app', '123', dto, version).subscribe();

15
src/Squidex/app/shared/services/assets.service.ts

@ -50,6 +50,7 @@ export class AssetDto extends Model {
public readonly isImage: boolean, public readonly isImage: boolean,
public readonly pixelWidth: number | null, public readonly pixelWidth: number | null,
public readonly pixelHeight: number | null, public readonly pixelHeight: number | null,
public readonly tags: string[],
public readonly url: string, public readonly url: string,
public readonly version: Version public readonly version: Version
) { ) {
@ -79,13 +80,20 @@ export class AssetDto extends Model {
} }
} }
export class UpdateAssetDto { export class RenameAssetDto {
constructor( constructor(
public readonly fileName: string public readonly fileName: string
) { ) {
} }
} }
export class TagAssetDto {
constructor(
public readonly tags: string[]
) {
}
}
export class AssetReplacedDto { export class AssetReplacedDto {
constructor( constructor(
public readonly fileSize: number, public readonly fileSize: number,
@ -151,6 +159,7 @@ export class AssetsService {
item.isImage, item.isImage,
item.pixelWidth, item.pixelWidth,
item.pixelHeight, item.pixelHeight,
item.tags || [],
assetUrl, assetUrl,
new Version(item.version.toString())); new Version(item.version.toString()));
})); }));
@ -194,6 +203,7 @@ export class AssetsService {
response.isImage, response.isImage,
response.pixelWidth, response.pixelWidth,
response.pixelHeight, response.pixelHeight,
[],
assetUrl, assetUrl,
new Version(event.headers.get('etag')!)); new Version(event.headers.get('etag')!));
@ -231,6 +241,7 @@ export class AssetsService {
body.isImage, body.isImage,
body.pixelWidth, body.pixelWidth,
body.pixelHeight, body.pixelHeight,
body.tags || [],
assetUrl, assetUrl,
response.version); response.version);
}), }),
@ -288,7 +299,7 @@ export class AssetsService {
pretifyError('Failed to delete asset. Please reload.')); pretifyError('Failed to delete asset. Please reload.'));
} }
public putAsset(appName: string, id: string, dto: UpdateAssetDto, version: Version): Observable<Versioned<any>> { public putAsset(appName: string, id: string, dto: RenameAssetDto | TagAssetDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`);
return HTTP.putVersioned(this.http, url, dto, version).pipe( return HTTP.putVersioned(this.http, url, dto, version).pipe(

26
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 { Form } from '@app/framework';
import { AssetDto } from './../services/assets.service';
export class RenameAssetForm extends Form<FormGroup> { export class RenameAssetForm extends Form<FormGroup> {
constructor(formBuilder: FormBuilder) { constructor(formBuilder: FormBuilder) {
super(formBuilder.group({ super(formBuilder.group({
@ -19,4 +21,28 @@ export class RenameAssetForm extends Form<FormGroup> {
] ]
})); }));
} }
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 });
}
} }

8
src/Squidex/app/shared/state/assets.state.spec.ts

@ -30,8 +30,8 @@ describe('AssetsState', () => {
const newVersion = new Version('2'); const newVersion = new Version('2');
const oldAssets = [ const oldAssets = [
new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, 'url1', 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) new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, [], 'url2', version)
]; ];
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
@ -77,7 +77,7 @@ describe('AssetsState', () => {
}); });
it('should add asset to snapshot when created', () => { 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); assetsState.add(newAsset);
@ -86,7 +86,7 @@ describe('AssetsState', () => {
}); });
it('should update properties when updated', () => { 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); assetsState.update(newAsset);

58
src/Squidex/app/theme/_forms.scss

@ -4,16 +4,18 @@
// //
// Support for Angular validation states. // Support for Angular validation states.
// //
.ng-invalid { .form-control {
&.ng-dirty { &.ng-invalid {
& { &.ng-dirty {
border-color: $color-theme-error; & {
} border-color: $color-theme-error;
}
&:hover,
&:focus { &:hover,
@include box-shadow-colored(0, 0, .2rem, $color-theme-error); &:focus {
border-color: $color-theme-error-dark; @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; 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;
}
}
}

2
src/Squidex/app/theme/_vars.scss

@ -97,4 +97,4 @@ $panel-header: 5.4rem;
$panel-sidebar: 3.75rem; $panel-sidebar: 3.75rem;
$panel-light-background: #fff; $panel-light-background: #fff;
$asset-height: 13rem; $asset-height: 16rem;

4
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -13,6 +13,7 @@ using FakeItEasy;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -26,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>(); private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetStore assetStore = A.Fake<IAssetStore>(); private readonly IAssetStore assetStore = A.Fake<IAssetStore>();
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly Guid assetId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid();
private readonly Stream stream = new MemoryStream(); 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); file = new AssetFile("my-image.png", "image/png", 1024, () => stream);
asset = new AssetGrain(Store, A.Dummy<ISemanticLog>()); asset = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>());
asset.OnActivateAsync(Id).Wait(); asset.OnActivateAsync(Id).Wait();
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))

4
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -23,6 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public class AssetGrainTests : HandlerTestBase<AssetGrain, AssetState> public class AssetGrainTests : HandlerTestBase<AssetGrain, AssetState>
{ {
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly ImageInfo image = new ImageInfo(2048, 2048); private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly Guid assetId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid();
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); 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() public AssetGrainTests()
{ {
sut = new AssetGrain(Store, A.Dummy<ISemanticLog>()); sut = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>());
sut.OnActivateAsync(Id).Wait(); sut.OnActivateAsync(Id).Wait();
} }

2
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 FileName { get; set; }
public string[] Tags { get; set; }
public long FileSize { get; set; } public long FileSize { get; set; }
public long FileVersion { get; set; } public long FileVersion { get; set; }

2
tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs

@ -90,7 +90,7 @@ namespace Squidex.Infrastructure.Commands
}); });
case UpdateCustom updateCustom: case UpdateCustom updateCustom:
return UpdateReturnAsync(updateCustom, c => return UpdateAsync(updateCustom, c =>
{ {
RaiseEvent(new ValueChanged { Value = c.Value }); RaiseEvent(new ValueChanged { Value = c.Value });

2
tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectGrainTests.cs

@ -85,7 +85,7 @@ namespace Squidex.Infrastructure.Commands
}); });
case UpdateCustom updateCustom: case UpdateCustom updateCustom:
return UpdateReturnAsync(updateCustom, c => return UpdateAsync(updateCustom, c =>
{ {
RaiseEvent(new ValueChanged { Value = c.Value }); RaiseEvent(new ValueChanged { Value = c.Value });

2
tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs

@ -72,7 +72,7 @@ namespace Squidex.Infrastructure.TestHelpers
}); });
case UpdateCustom updateCustom: case UpdateCustom updateCustom:
return UpdateReturnAsync(updateCustom, c => return UpdateAsync(updateCustom, c =>
{ {
RaiseEvent(new ValueChanged { Value = c.Value }); RaiseEvent(new ValueChanged { Value = c.Value });

Loading…
Cancel
Save