Browse Source

Merge branch 'hateaos' of github.com:Squidex/squidex into hateaos

# Conflicts:
#	src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs
pull/364/head
Sebastian 7 years ago
parent
commit
f08bfe1b87
  1. 64
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  2. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  3. 25
      src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs
  4. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  5. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  6. 20
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  7. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs
  8. 8
      src/Squidex.Infrastructure/CollectionExtensions.cs
  9. 9
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  10. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  11. 2
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  12. 2
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
  13. 5
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs
  14. 8
      src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs
  15. 2
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
  16. 6
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  17. 2
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  18. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  19. 8
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  20. 3
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  21. 1
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  22. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  23. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  24. 2
      src/Squidex/app/features/settings/pages/patterns/pattern.component.ts
  25. 10
      src/Squidex/app/shared/components/asset.component.html
  26. 19
      src/Squidex/app/shared/components/assets-list.component.ts
  27. 8
      src/Squidex/app/shared/components/markdown-editor.component.ts
  28. 4
      src/Squidex/app/shared/components/rich-editor.component.ts
  29. 2
      src/Squidex/app/shared/components/schema-category.component.ts
  30. 2
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  31. 2
      src/Squidex/app/shared/services/app-languages.service.ts
  32. 6
      src/Squidex/app/shared/services/assets.service.ts
  33. 67
      src/Squidex/app/shared/services/rules.service.spec.ts
  34. 14
      src/Squidex/app/shared/services/rules.service.ts
  35. 6
      src/Squidex/app/shared/services/schemas.service.spec.ts
  36. 10
      src/Squidex/app/shared/services/schemas.service.ts
  37. 2
      src/Squidex/app/shared/state/asset-uploader.state.ts
  38. 8
      src/Squidex/app/shared/state/rule-events.state.spec.ts
  39. 29
      src/Squidex/app/shared/state/schemas.state.spec.ts
  40. 10
      src/Squidex/app/shared/state/schemas.state.ts
  41. 84
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

64
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -24,25 +25,28 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators; private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly ITagService tagService;
public AssetCommandMiddleware( public AssetCommandMiddleware(
IGrainFactory grainFactory, IGrainFactory grainFactory,
IAssetQueryService assetQuery, IAssetQueryService assetQuery,
IAssetStore assetStore, IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator, IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators) IEnumerable<ITagGenerator<CreateAsset>> tagGenerators,
ITagService tagService)
: base(grainFactory) : base(grainFactory)
{ {
Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators)); Guard.NotNull(tagGenerators, nameof(tagGenerators));
Guard.NotNull(tagService, nameof(tagService));
this.assetStore = assetStore; this.assetStore = assetStore;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators; this.tagGenerators = tagGenerators;
this.tagService = tagService;
} }
public override async Task HandleAsync(CommandContext context, Func<Task> next) public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -56,9 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.Tags = new HashSet<string>(); createAsset.Tags = new HashSet<string>();
} }
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead()); await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, context);
createAsset.FileHash = await UploadAsync(context, createAsset.File);
try try
{ {
@ -70,7 +73,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
if (IsDuplicate(createAsset, existing)) if (IsDuplicate(createAsset, existing))
{ {
result = new AssetCreatedResult(existing, true); var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags);
result = new AssetCreatedResult(existing, true, new HashSet<string>(denormalizedTags.Values));
} }
break; break;
@ -85,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset); var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset);
result = new AssetCreatedResult(asset, false); result = new AssetCreatedResult(asset, false, createAsset.Tags);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null); await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
} }
@ -102,16 +107,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
case UpdateAsset updateAsset: case UpdateAsset updateAsset:
{ {
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead()); await EnrichWithImageInfosAsync(updateAsset);
await EnrichWithHashAndUploadAsync(updateAsset, context);
updateAsset.FileHash = await UploadAsync(context, updateAsset.File);
try try
{ {
var result = (IAssetEntity)await ExecuteCommandAsync(updateAsset); var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset);
context.Complete(result); context.Complete(result);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null); await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null);
} }
finally finally
{ {
@ -121,29 +126,54 @@ namespace Squidex.Domain.Apps.Entities.Assets
break; break;
} }
case AssetCommand command:
{
var result = await ExecuteAndAdjustTagsAsync(command);
context.Complete(result);
break;
}
default: default:
await base.HandleAsync(context, next); await base.HandleAsync(context, next);
break; break;
} }
} }
private async Task<object> ExecuteAndAdjustTagsAsync(AssetCommand command)
{
var result = await ExecuteCommandAsync(command);
if (result is IAssetEntity asset)
{
var denormalizedTags = await tagService.DenormalizeTagsAsync(asset.AppId.Id, TagGroups.Assets, asset.Tags);
return new AssetResult(asset, new HashSet<string>(denormalizedTags.Values));
}
return result;
}
private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset) private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset)
{ {
return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize; return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize;
} }
private async Task<string> UploadAsync(CommandContext context, AssetFile file) private async Task EnrichWithImageInfosAsync(UploadAssetCommand command)
{ {
string hash; command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
}
using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256)) private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, CommandContext context)
{
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
{ {
await assetStore.UploadAsync(context.ContextId.ToString(), hashStream); await assetStore.UploadAsync(context.ContextId.ToString(), hashStream);
hash = $"{hashStream.GetHashStringAndReset()}{file.FileName}{file.FileSize}".Sha256Base64(); command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64();
} }
return hash;
} }
} }
} }

11
src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs

@ -5,18 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetCreatedResult public sealed class AssetCreatedResult : AssetResult
{ {
public IAssetEntity Asset { get; }
public bool IsDuplicate { get; } public bool IsDuplicate { get; }
public AssetCreatedResult(IAssetEntity asset, bool isDuplicate) public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet<string> tags)
: base(asset, tags)
{ {
Asset = asset;
IsDuplicate = isDuplicate; IsDuplicate = isDuplicate;
} }
} }

25
src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs

@ -0,0 +1,25 @@
// ==========================================================================
// 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.Assets
{
public class AssetResult
{
public IAssetEntity Asset { get; }
public HashSet<string> Tags { get; }
public AssetResult(IAssetEntity asset, HashSet<string> tags)
{
Asset = asset;
Tags = tags;
}
}
}

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -8,22 +8,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class CreateAsset : AssetCommand, IAppCommand public sealed class CreateAsset : UploadAssetCommand, IAppCommand
{ {
public NamedId<Guid> AppId { get; set; } public NamedId<Guid> AppId { get; set; }
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset() public CreateAsset()
{ {
AssetId = Guid.NewGuid(); AssetId = Guid.NewGuid();

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs

@ -5,16 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class UpdateAsset : AssetCommand public sealed class UpdateAsset : UploadAssetCommand
{ {
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
} }
} }

20
src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public abstract class UploadAssetCommand : AssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs

@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
throw new DomainException("Schema field is already disabled."); throw new DomainException("Schema field is already disabled.");
} }
if (!field.IsForApi()) if (!field.IsForApi(true))
{ {
throw new DomainException("UI field cannot be disabled."); throw new DomainException("UI field cannot be disabled.");
} }

8
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -13,6 +13,14 @@ namespace Squidex.Infrastructure
{ {
public static class CollectionExtensions public static class CollectionExtensions
{ {
public static void AddRange<T>(this ICollection<T> target, IEnumerable<T> source)
{
foreach (var value in source)
{
target.Add(value);
}
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable) public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{ {
var random = new Random(); var random = new Random();

9
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -179,7 +179,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
var command = new CreateAsset { File = assetFile }; var command = new CreateAsset { File = assetFile };
var response = await InvokeCommandAsync(app, command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetCreatedResult>();
var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags, result.IsDuplicate);
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
} }
@ -264,8 +267,8 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAssetEntity>(); var result = context.Result<AssetResult>();
var response = AssetDto.FromAsset(result, this, app); var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags);
return response; return response;
} }

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

@ -118,10 +118,15 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[JsonProperty("_meta")] [JsonProperty("_meta")]
public AssetMetadata Metadata { get; set; } public AssetMetadata Metadata { get; set; }
public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, bool isDuplicate = false) public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, HashSet<string> tags = null, bool isDuplicate = false)
{ {
var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
if (tags != null)
{
response.Tags = tags;
}
if (isDuplicate) if (isDuplicate)
{ {
response.Metadata = new AssetMetadata { IsDuplicate = "true" }; response.Metadata = new AssetMetadata { IsDuplicate = "true" };

2
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -142,7 +142,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
if (controller.HasPermission(Permissions.AppContentsDelete, app, schema)) if (controller.HasPermission(Permissions.AppContentsDelete, app, schema))
{ {
AddPutLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values)); AddDeleteLink("delete", controller.Url<ContentsController>(x => nameof(x.DeleteContent), values));
} }
foreach (var next in StatusFlow.Next(Status)) foreach (var next in StatusFlow.Next(Status))

2
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs

@ -109,7 +109,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
if (controller.HasPermission(Permissions.AppRulesDelete)) if (controller.HasPermission(Permissions.AppRulesDelete))
{ {
AddPutLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteRule), values)); AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteRule), values));
} }
return this; return this;

5
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs

@ -80,7 +80,10 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutEvent), values)); AddPutLink("update", controller.Url<RulesController>(x => nameof(x.PutEvent), values));
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values)); if (NextAttempt.HasValue)
{
AddDeleteLink("delete", controller.Url<RulesController>(x => nameof(x.DeleteEvent), values));
}
return this; return this;
} }

8
src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs

@ -83,14 +83,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
} }
else else
{ {
AddPutLink("show", controller.Url<SchemaFieldsController>(x => nameof(x.DisableField), values)); AddPutLink("disable", controller.Url<SchemaFieldsController>(x => nameof(x.DisableField), values));
} }
if (Properties is ArrayFieldPropertiesDto) if (Properties is ArrayFieldPropertiesDto)
{ {
AddPostLink("fields/add", controller.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), values)); var parentValues = new { app, name = schema, parentId = FieldId };
AddPutLink("order", controller.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), values)); AddPostLink("fields/add", controller.Url<SchemaFieldsController>(x => nameof(x.PostNestedField), parentValues));
AddPutLink("fields/order", controller.Url<SchemaFieldsController>(x => nameof(x.PutNestedFieldOrdering), parentValues));
} }
AddPutLink("lock", controller.Url<SchemaFieldsController>(x => nameof(x.LockField), values)); AddPutLink("lock", controller.Url<SchemaFieldsController>(x => nameof(x.LockField), values));

2
src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs

@ -119,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
if (allowUpdate) if (allowUpdate)
{ {
AddPutLink("order", controller.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values)); AddPutLink("fields/order", controller.Url<SchemaFieldsController>(x => nameof(x.PutSchemaFieldOrdering), values));
AddPutLink("update", controller.Url<SchemasController>(x => nameof(x.PutSchema), values)); AddPutLink("update", controller.Url<SchemasController>(x => nameof(x.PutSchema), values));
AddPutLink("update/category", controller.Url<SchemasController>(x => nameof(x.PutCategory), values)); AddPutLink("update/category", controller.Url<SchemasController>(x => nameof(x.PutCategory), values));

6
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -513,9 +513,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteNestedField(string app, string name, long parentId, long id) public async Task<IActionResult> DeleteNestedField(string app, string name, long parentId, long id)
{ {
await CommandBus.PublishAsync(new DeleteField { ParentFieldId = parentId, FieldId = id }); var command = new DeleteField { ParentFieldId = parentId, FieldId = id };
return NoContent(); var response = await InvokeCommandAsync(app, command);
return Ok(response);
} }
private async Task<SchemaDetailsDto> InvokeCommandAsync(string app, ICommand command) private async Task<SchemaDetailsDto> InvokeCommandAsync(string app, ICommand command)

2
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -183,7 +183,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
private loadContent(data: any) { private loadContent(data: any) {
this.contentForm.loadContent(data); this.contentForm.loadContent(data);
this.contentForm.setEnabled(this.content && !this.content.canUpdate); this.contentForm.setEnabled(!this.content || this.content.canUpdate);
} }
public discardChanges() { public discardChanges() {

2
src/Squidex/app/features/content/shared/content-item.component.html

@ -11,7 +11,7 @@
</ng-template> </ng-template>
</td> </td>
<td class="cell-auto" *ngFor="let field of schema.listFields; let i = index; trackBy: trackByField.bind(this)" [sqxStopClick]="isDirty || field.isInlineEditable"> <td class="cell-auto" *ngFor="let field of schema.listFields; let i = index; trackBy: trackByField.bind(this)" [sqxStopClick]="isDirty || (field.isInlineEditable && patchAllowed)">
<ng-container *ngIf="field.isInlineEditable && patchAllowed; else displayTemplate"> <ng-container *ngIf="field.isInlineEditable && patchAllowed; else displayTemplate">
<sqx-content-item-editor [form]="patchForm.form" [field]="field"></sqx-content-item-editor> <sqx-content-item-editor [form]="patchForm.form" [field]="field"></sqx-content-item-editor>
</ng-container> </ng-container>

8
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -22,13 +22,13 @@
</td> </td>
<td class="text-center"> <td class="text-center">
<input type="text" class="form-control code" placeholder="Optional condition as javascript expression" <input type="text" class="form-control code" placeholder="Optional condition as javascript expression"
[disabled]="!isEditable" [disabled]="triggerForm.disabled"
[ngModelOptions]="{ updateOn: 'blur' }" [ngModelOptions]="{ updateOn: 'blur' }"
[ngModel]="triggerSchema.condition" [ngModel]="triggerSchema.condition"
(ngModelChange)="updateCondition(triggerSchema.schema, $event)" /> (ngModelChange)="updateCondition(triggerSchema.schema, $event)" />
</td> </td>
<td class="text-center"> <td class="text-center">
<button type="button" class="btn btn-text-secondary" (click)="removeSchema(triggerSchema)" [disabled]="!isEditable"> <button type="button" class="btn btn-text-secondary" (click)="removeSchema(triggerSchema)" [disabled]="triggerForm.disabled">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</td> </td>
@ -38,12 +38,12 @@
<div class="section" *ngIf="schemasToAdd.length > 0"> <div class="section" *ngIf="schemasToAdd.length > 0">
<form class="form-inline" (ngSubmit)="addSchema()"> <form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1"> <div class="form-group mr-1">
<select class="form-control schemas-control" [disabled]="!isEditable" [(ngModel)]="schemaToAdd" name="schema"> <select class="form-control schemas-control" [disabled]="triggerForm.disabled" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd; trackBy: trackBySchema" [ngValue]="schema">{{schema.displayName}}</option> <option *ngFor="let schema of schemasToAdd; trackBy: trackBySchema" [ngValue]="schema">{{schema.displayName}}</option>
</select> </select>
</div> </div>
<button type="submit" class="btn btn-success" [disabled]="!isEditable">Add Schema</button> <button type="submit" class="btn btn-success" [disabled]="triggerForm.disabled">Add Schema</button>
</form> </form>
</div> </div>

3
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -29,9 +29,6 @@ export class ContentChangedTriggerComponent implements OnInit {
@Input() @Input()
public schemas: ImmutableArray<SchemaDto>; public schemas: ImmutableArray<SchemaDto>;
@Input()
public isEditable: boolean;
@Input() @Input()
public trigger: any; public trigger: any;

1
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -65,6 +65,7 @@
<ng-template #notEditing> <ng-template #notEditing>
<form [formGroup]="editForm.form" class="edit-form" (ngSubmit)="save()"> <form [formGroup]="editForm.form" class="edit-form" (ngSubmit)="save()">
<sqx-field-form <sqx-field-form
[isEditable]="true"
[patterns]="patternsState.patterns | async" [patterns]="patternsState.patterns | async"
[editForm]="editForm.form" [editForm]="editForm.form"
[editFormSubmitted]="editForm.submitted | async" [editFormSubmitted]="editForm.submitted | async"

2
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -95,7 +95,7 @@ export class FieldComponent implements OnChanges {
} }
public sortFields(fields: NestedFieldDto[]) { public sortFields(fields: NestedFieldDto[]) {
this.schemasState.sortFields(this.schema, fields, <any>this.field).subscribe(); this.schemasState.orderFields(this.schema, fields, <any>this.field).subscribe();
} }
public lockField() { public lockField() {

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

@ -87,7 +87,7 @@ export class SchemaPageComponent extends ResourceOwner implements OnInit {
} }
public sortFields(fields: FieldDto[]) { public sortFields(fields: FieldDto[]) {
this.schemasState.sortFields(this.schema, fields).subscribe(); this.schemasState.orderFields(this.schema, fields).subscribe();
} }
public trackByField(index: number, field: FieldDto) { public trackByField(index: number, field: FieldDto) {

2
src/Squidex/app/features/settings/pages/patterns/pattern.component.ts

@ -25,7 +25,7 @@ export class PatternComponent implements OnChanges {
public editForm = new EditPatternForm(this.formBuilder); public editForm = new EditPatternForm(this.formBuilder);
public isEditable = false; public isEditable = true;
public isDeletable = false; public isDeletable = false;
constructor( constructor(

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

@ -26,7 +26,10 @@
<a class="file-download ml-2" [href]="asset | sqxAssetUrl" sqxStopClick sqxExternalLink="noicon"> <a class="file-download ml-2" [href]="asset | sqxAssetUrl" sqxStopClick sqxExternalLink="noicon">
<i class="icon-download"></i> <i class="icon-download"></i>
</a> </a>
<a class="file-delete ml-2" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode && asset.canDelete"> <a class="file-delete ml-2" *ngIf="!isDisabled && !removeMode && asset.canDelete"
(sqxConfirmClick)="emitDelete()"
confirmTitle="Delete asset"
confirmText="Do you really want to delete the asset?">
<i class="icon-delete"></i> <i class="icon-delete"></i>
</a> </a>
<a class="file-delete ml-2" (click)="emitRemove()" *ngIf="removeMode"> <a class="file-delete ml-2" (click)="emitRemove()" *ngIf="removeMode">
@ -109,7 +112,10 @@
</a> </a>
</td> </td>
<td class="col-actions text-right" *ngIf="!isDisabled || removeMode"> <td class="col-actions text-right" *ngIf="!isDisabled || removeMode">
<button type="button" class="btn btn-text-danger" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode && asset.canDelete"> <button type="button" class="btn btn-text-danger" *ngIf="!isDisabled && !removeMode && asset.canDelete"
(sqxConfirmClick)="emitDelete()"
confirmTitle="Delete asset"
confirmText="Do you really want to delete the asset?">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" (click)="emitRemove()" *ngIf="removeMode"> <button type="button" class="btn btn-text-secondary" (click)="emitRemove()" *ngIf="removeMode">

19
src/Squidex/app/shared/components/assets-list.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
import { import {
@ -38,10 +38,23 @@ export class AssetsListComponent {
@Output() @Output()
public select = new EventEmitter<AssetDto>(); public select = new EventEmitter<AssetDto>();
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public add(file: File, asset: AssetDto) { public add(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file); if (asset.isDuplicate) {
setTimeout(() => {
this.newFiles = this.newFiles.remove(file);
this.state.add(asset); this.changeDetector.detectChanges();
}, 2000);
} else {
this.newFiles = this.newFiles.remove(file);
this.state.add(asset);
}
} }
public search() { public search() {

8
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -78,6 +78,10 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
} }
private showSelector = () => { private showSelector = () => {
if (this.isDisabled) {
return;
}
this.assetsDialog.show(); this.assetsDialog.show();
} }
@ -220,6 +224,10 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
} }
private uploadFile(doc: any, file: File) { private uploadFile(doc: any, file: File) {
if (this.isDisabled) {
return;
}
const uploadCursor = doc.getCursor(); const uploadCursor = doc.getCursor();
const uploadText = `![Uploading file...${new Date()}]()`; const uploadText = `![Uploading file...${new Date()}]()`;

4
src/Squidex/app/shared/components/rich-editor.component.ts

@ -78,6 +78,10 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
} }
private showSelector = () => { private showSelector = () => {
if (this.isDisabled) {
return;
}
this.assetsDialog.show(); this.assetsDialog.show();
} }

2
src/Squidex/app/shared/components/schema-category.component.ts

@ -96,7 +96,7 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
if (query) { if (query) {
isOpen = true; isOpen = true;
} else { } else {
isOpen = this.localStore.get(`schema-category.${this.name}`) !== 'false'; isOpen = !this.localStore.getBoolean(this.configKey());
} }
this.next(s => ({ ...s, isOpen, schemasFiltered, schemasForCategory })); this.next(s => ({ ...s, isOpen, schemasFiltered, schemasForCategory }));

2
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -124,7 +124,7 @@ describe('AppLanguagesService', () => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
update: { method: 'DELETE', href: 'api/apps/my-app/languages/de' } delete: { method: 'DELETE', href: 'api/apps/my-app/languages/de' }
} }
}; };

2
src/Squidex/app/shared/services/app-languages.service.ts

@ -109,7 +109,7 @@ export class AppLanguagesService {
} }
public deleteLanguage(appName: string, resource: Resource, version: Version): Observable<AppLanguagesDto> { public deleteLanguage(appName: string, resource: Resource, version: Version): Observable<AppLanguagesDto> {
const link = resource._links['update']; const link = resource._links['delete'];
const url = this.apiUrl.buildUrl(link.href); const url = this.apiUrl.buildUrl(link.href);

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

@ -43,6 +43,10 @@ export class AssetDto {
public readonly canUpdate: boolean; public readonly canUpdate: boolean;
public readonly canUpload: boolean; public readonly canUpload: boolean;
public get isDuplicate() {
return this._meta && this._meta['isDuplicate'] === 'true';
}
public get contentUrl() { public get contentUrl() {
return this._links['content'].href; return this._links['content'].href;
} }
@ -238,7 +242,7 @@ export class AssetsService {
tap(() => { tap(() => {
this.analytics.trackEvent('Analytics', 'Updated', appName); this.analytics.trackEvent('Analytics', 'Updated', appName);
}), }),
pretifyError('Failed to delete asset. Please reload.')); pretifyError('Failed to update asset. Please reload.'));
} }
public deleteAsset(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> { public deleteAsset(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> {

67
src/Squidex/app/shared/services/rules.service.spec.ts

@ -288,41 +288,15 @@ describe('RulesService', () => {
req.flush({ req.flush({
total: 20, total: 20,
items: [ items: [
{ ruleEventResponse(1),
id: 'id1', ruleEventResponse(2)
created: '2017-12-12T10:10',
eventName: 'event1',
nextAttempt: '2017-12-12T12:10',
jobResult: 'Failed',
lastDump: 'dump1',
numCalls: 1,
description: 'url1',
result: 'Failed'
},
{
id: 'id2',
created: '2017-12-13T10:10',
eventName: 'event2',
nextAttempt: '2017-12-13T12:10',
jobResult: 'Failed',
lastDump: 'dump2',
numCalls: 2,
description: 'url2',
result: 'Failed'
}
] ]
}); });
expect(rules!).toEqual( expect(rules!).toEqual(
new RuleEventsDto(20, [ new RuleEventsDto(20, [
new RuleEventDto('id1', createRuleEvent(1),
DateTime.parseISO_UTC('2017-12-12T10:10'), createRuleEvent(2)
DateTime.parseISO_UTC('2017-12-12T12:10'),
'event1', 'url1', 'dump1', 'Failed', 'Failed', 1),
new RuleEventDto('id2',
DateTime.parseISO_UTC('2017-12-13T10:10'),
DateTime.parseISO_UTC('2017-12-13T12:10'),
'event2', 'url2', 'dump2', 'Failed', 'Failed', 2)
])); ]));
})); }));
@ -364,6 +338,23 @@ describe('RulesService', () => {
req.flush({}); req.flush({});
})); }));
function ruleEventResponse(id: number, suffix = '') {
return {
id: `id${id}`,
created: `${id % 1000 + 2000}-12-12T10:10:00`,
eventName: `event${id}${suffix}`,
nextAttempt: `${id % 1000 + 2000}-11-11T10:10`,
jobResult: `Failed${id}${suffix}`,
lastDump: `dump${id}${suffix}`,
numCalls: id,
description: `url${id}${suffix}`,
result: `Failed${id}${suffix}`,
_links: {
update: { method: 'PUT', href: `/rules/events/${id}` }
}
};
}
function ruleResponse(id: number, suffix = '') { function ruleResponse(id: number, suffix = '') {
return { return {
id: `id${id}`, id: `id${id}`,
@ -390,6 +381,22 @@ describe('RulesService', () => {
} }
}); });
export function createRuleEvent(id: number, suffix = '') {
const links: ResourceLinks = {
update: { method: 'PUT', href: `/rules/events/${id}` }
};
return new RuleEventDto(links, `id${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`),
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`),
`event${id}${suffix}`,
`url${id}${suffix}`,
`dump${id}${suffix}`,
`Failed${id}${suffix}`,
`Failed${id}${suffix}`,
id);
}
export function createRule(id: number, suffix = '') { export function createRule(id: number, suffix = '') {
const links: ResourceLinks = { const links: ResourceLinks = {
update: { method: 'PUT', href: `/rules/${id}` } update: { method: 'PUT', href: `/rules/${id}` }

14
src/Squidex/app/shared/services/rules.service.ts

@ -111,6 +111,8 @@ export class RuleDto {
public readonly action: any, public readonly action: any,
public readonly actionType: string public readonly actionType: string
) { ) {
this._links = links;
this.canDelete = hasAnyLink(links, 'delete'); this.canDelete = hasAnyLink(links, 'delete');
this.canDisable = hasAnyLink(links, 'disable'); this.canDisable = hasAnyLink(links, 'disable');
this.canEnable = hasAnyLink(links, 'enable'); this.canEnable = hasAnyLink(links, 'enable');
@ -125,7 +127,10 @@ export class RuleEventsDto extends ResultSet<RuleEventDto> {
export class RuleEventDto extends Model<RuleEventDto> { export class RuleEventDto extends Model<RuleEventDto> {
public readonly _links: ResourceLinks; public readonly _links: ResourceLinks;
constructor( public readonly canDelete: boolean;
public readonly canUpdate: boolean;
constructor(links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly created: DateTime, public readonly created: DateTime,
public readonly nextAttempt: DateTime | null, public readonly nextAttempt: DateTime | null,
@ -137,6 +142,11 @@ export class RuleEventDto extends Model<RuleEventDto> {
public readonly numCalls: number public readonly numCalls: number
) { ) {
super(); super();
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
this.canUpdate = hasAnyLink(links, 'update');
} }
} }
@ -285,7 +295,7 @@ export class RulesService {
const items: any[] = body.items; const items: any[] = body.items;
const ruleEvents = new RuleEventsDto(body.total, items.map(item => const ruleEvents = new RuleEventsDto(body.total, items.map(item =>
new RuleEventDto( new RuleEventDto(item._links,
item.id, item.id,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null,

6
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -205,7 +205,7 @@ describe('SchemasService', () => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
updateCategory: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/category' } ['update/category']: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/category' }
} }
}; };
@ -236,7 +236,7 @@ describe('SchemasService', () => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
updateUrls: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/preview-urls' } ['update/urls']: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/preview-urls' }
} }
}; };
@ -387,7 +387,7 @@ describe('SchemasService', () => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
order: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/fields/ordering' } ['fields/order']: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/fields/ordering' }
} }
}; };

10
src/Squidex/app/shared/services/schemas.service.ts

@ -213,7 +213,7 @@ export class SchemaPropertiesDto {
export interface AddFieldDto { export interface AddFieldDto {
readonly name: string; readonly name: string;
readonly partitioning: string; readonly partitioning?: string;
readonly properties: FieldPropertiesDto; readonly properties: FieldPropertiesDto;
} }
@ -310,7 +310,7 @@ export class SchemasService {
} }
public putCategory(appName: string, resource: Resource, dto: UpdateSchemaCategoryDto, version: Version): Observable<SchemaDetailsDto> { public putCategory(appName: string, resource: Resource, dto: UpdateSchemaCategoryDto, version: Version): Observable<SchemaDetailsDto> {
const link = resource._links['updateCategory']; const link = resource._links['update/category'];
const url = this.apiUrl.buildUrl(link.href); const url = this.apiUrl.buildUrl(link.href);
@ -325,7 +325,7 @@ export class SchemasService {
} }
public putPreviewUrls(appName: string, resource: Resource, dto: {}, version: Version): Observable<SchemaDetailsDto> { public putPreviewUrls(appName: string, resource: Resource, dto: {}, version: Version): Observable<SchemaDetailsDto> {
const link = resource._links['updateUrls']; const link = resource._links['update/urls'];
const url = this.apiUrl.buildUrl(link.href); const url = this.apiUrl.buildUrl(link.href);
@ -385,11 +385,11 @@ export class SchemasService {
} }
public putFieldOrdering(appName: string, resource: Resource, dto: number[], version: Version): Observable<SchemaDetailsDto> { public putFieldOrdering(appName: string, resource: Resource, dto: number[], version: Version): Observable<SchemaDetailsDto> {
const link = resource._links['order']; const link = resource._links['fields/order'];
const url = this.apiUrl.buildUrl(link.href); const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( return HTTP.requestVersioned(this.http, link.method, url, version, { fieldIds: dto }).pipe(
map(({ version: newVersion, payload }) => { map(({ version: newVersion, payload }) => {
return parseSchemaWithDetails(payload.body, newVersion); return parseSchemaWithDetails(payload.body, newVersion);
}), }),

2
src/Squidex/app/shared/state/asset-uploader.state.ts

@ -75,7 +75,7 @@ export class AssetUploaderState extends State<Snapshot> {
const stream = this.assetsService.uploadFile(this.appName, file); const stream = this.assetsService.uploadFile(this.appName, file);
return this.upload(stream, MathHelper.guid(), file, asset => { return this.upload(stream, MathHelper.guid(), file, asset => {
if (asset._meta && asset._meta['isDuplicate'] === 'true') { if (asset.isDuplicate) {
this.dialogs.notifyError('Asset has already been uploaded.'); this.dialogs.notifyError('Asset has already been uploaded.');
} else if (target) { } else if (target) {
target.add(asset); target.add(asset);

8
src/Squidex/app/shared/state/rule-events.state.spec.ts

@ -9,14 +9,14 @@ import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DateTime,
DialogService, DialogService,
RuleEventDto,
RuleEventsDto, RuleEventsDto,
RuleEventsState, RuleEventsState,
RulesService RulesService
} from '@app/shared/internal'; } from '@app/shared/internal';
import { createRuleEvent } from '../services/rules.service.spec';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
describe('RuleEventsState', () => { describe('RuleEventsState', () => {
@ -26,8 +26,8 @@ describe('RuleEventsState', () => {
} = TestValues; } = TestValues;
const oldRuleEvents = [ const oldRuleEvents = [
new RuleEventDto('id1', DateTime.now(), null, 'event1', 'description', 'dump1', 'result1', 'result1', 1), createRuleEvent(1),
new RuleEventDto('id2', DateTime.now(), null, 'event2', 'description', 'dump2', 'result2', 'result2', 2) createRuleEvent(2)
]; ];
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;

29
src/Squidex/app/shared/state/schemas.state.spec.ts

@ -13,6 +13,7 @@ import { SchemasState } from './schemas.state';
import { import {
DialogService, DialogService,
FieldDto,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
UpdateSchemaCategoryDto, UpdateSchemaCategoryDto,
@ -69,7 +70,7 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items); expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items);
expect(schemasState.snapshot.isLoaded).toBeTruthy(); expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, '': true }); expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false });
schemasService.verifyAll(); schemasService.verifyAll();
}); });
@ -83,7 +84,7 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items); expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items);
expect(schemasState.snapshot.isLoaded).toBeTruthy(); expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true, '': true }); expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true });
schemasService.verifyAll(); schemasService.verifyAll();
}); });
@ -111,13 +112,13 @@ describe('SchemasState', () => {
it('should add category', () => { it('should add category', () => {
schemasState.addCategory('category3'); schemasState.addCategory('category3');
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true, '': true }); expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true });
}); });
it('should remove category', () => { it('should remove category', () => {
schemasState.removeCategory('category1'); schemasState.removeCategory('category1');
expect(schemasState.snapshot.categories).toEqual({ 'category2': false, '': true }); expect(schemasState.snapshot.categories).toEqual({ 'category2': false });
}); });
it('should return schema on select and reload when already loaded', () => { it('should return schema on select and reload when already loaded', () => {
@ -329,28 +330,38 @@ describe('SchemasState', () => {
schemasService.setup(x => x.postField(app, schema1, It.isAny(), version)) schemasService.setup(x => x.postField(app, schema1, It.isAny(), version))
.returns(() => of(updated)).verifiable(); .returns(() => of(updated)).verifiable();
schemasState.addField(schema1, request).subscribe(); let newField: FieldDto;
schemasState.addField(schema1, request).subscribe(result => {
newField = result;
});
const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0); const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0);
expect(schema1New).toEqual(updated); expect(schema1New).toEqual(updated);
expect(schemasState.snapshot.selectedSchema).toEqual(updated); expect(schemasState.snapshot.selectedSchema).toEqual(updated);
expect(newField!).toBeDefined();
}); });
it('should update schema and selected schema when nested field added', () => { it('should update schema and selected schema when nested field added', () => {
const request = { ...schema.fields[0] }; const request = { ...schema.fields[0].nested[0] };
const updated = createSchemaDetails(1, newVersion, '-new'); const updated = createSchemaDetails(1, newVersion, '-new');
schemasService.setup(x => x.postField(app, schema.fields[0], It.isAny(), version)) schemasService.setup(x => x.postField(app, schema.fields[0], It.isAny(), version))
.returns(() => of(updated)).verifiable(); .returns(() => of(updated)).verifiable();
schemasState.addField(schema1, request, schema.fields[0]).subscribe(); let newField: FieldDto;
schemasState.addField(schema1, request, schema.fields[0]).subscribe(result => {
newField = result;
});
const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0); const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0);
expect(schema1New).toEqual(updated); expect(schema1New).toEqual(updated);
expect(schemasState.snapshot.selectedSchema).toEqual(updated); expect(schemasState.snapshot.selectedSchema).toEqual(updated);
expect(newField!).toBeDefined();
}); });
it('should update schema and selected schema when field removed', () => { it('should update schema and selected schema when field removed', () => {
@ -373,7 +384,7 @@ describe('SchemasState', () => {
schemasService.setup(x => x.putFieldOrdering(app, schema1, [schema.fields[1].fieldId, schema.fields[2].fieldId], version)) schemasService.setup(x => x.putFieldOrdering(app, schema1, [schema.fields[1].fieldId, schema.fields[2].fieldId], version))
.returns(() => of(updated)).verifiable(); .returns(() => of(updated)).verifiable();
schemasState.sortFields(schema1, [schema.fields[1], schema.fields[2]]).subscribe(); schemasState.orderFields(schema1, [schema.fields[1], schema.fields[2]]).subscribe();
const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0); const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0);
@ -387,7 +398,7 @@ describe('SchemasState', () => {
schemasService.setup(x => x.putFieldOrdering(app, schema.fields[0], [schema.fields[1].fieldId, schema.fields[2].fieldId], version)) schemasService.setup(x => x.putFieldOrdering(app, schema.fields[0], [schema.fields[1].fieldId, schema.fields[2].fieldId], version))
.returns(() => of(updated)).verifiable(); .returns(() => of(updated)).verifiable();
schemasState.sortFields(schema1, [schema.fields[1], schema.fields[2]], schema.fields[0]).subscribe(); schemasState.orderFields(schema1, [schema.fields[1], schema.fields[2]], schema.fields[0]).subscribe();
const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0); const schema1New = <SchemaDetailsDto>schemasState.snapshot.schemas.at(0);

10
src/Squidex/app/shared/state/schemas.state.ts

@ -140,7 +140,9 @@ export class SchemasState extends State<Snapshot> {
this.next(s => { this.next(s => {
const schemas = s.schemas.push(created).sortByStringAsc(x => x.displayName); const schemas = s.schemas.push(created).sortByStringAsc(x => x.displayName);
return { ...s, schemas }; const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, categories };
}); });
}), }),
shareSubscribed(this.dialogs, { silent: true })); shareSubscribed(this.dialogs, { silent: true }));
@ -231,7 +233,7 @@ export class SchemasState extends State<Snapshot> {
shareMapSubscribed(this.dialogs, x => getField(x, request, parent), { silent: true })); shareMapSubscribed(this.dialogs, x => getField(x, request, parent), { silent: true }));
} }
public sortFields(schema: SchemaDto, fields: any[], parent?: RootFieldDto): Observable<SchemaDetailsDto> { public orderFields(schema: SchemaDto, fields: any[], parent?: RootFieldDto): Observable<SchemaDetailsDto> {
return this.schemasService.putFieldOrdering(this.appName, parent || schema, fields.map(t => t.fieldId), schema.version).pipe( return this.schemasService.putFieldOrdering(this.appName, parent || schema, fields.map(t => t.fieldId), schema.version).pipe(
tap(updated => { tap(updated => {
this.replaceSchema(updated); this.replaceSchema(updated);
@ -320,7 +322,7 @@ export class SchemasState extends State<Snapshot> {
function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto): FieldDto { function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto): FieldDto {
if (parent) { if (parent) {
return parent.nested.find(f => f.name === request.name)!; return x.fields.find(f => f.fieldId === parent.fieldId)!.nested.find(f => f.name === request.name)!;
} else { } else {
return x.fields.find(f => f.name === request.name)!; return x.fields.find(f => f.name === request.name)!;
} }
@ -343,8 +345,6 @@ function buildCategories(categories: { [name: string]: boolean }, schemas?: Sche
} }
} }
categories[''] = true;
return categories; return categories;
} }

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

@ -54,19 +54,23 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored)) A.CallTo(() => assetQuery.QueryByHashAsync(AppId, A<string>.Ignored))
.Returns(new List<IAssetEntity>()); .Returns(new List<IAssetEntity>());
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored)) A.CallTo(() => tagService.DenormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>()); .Returns(new Dictionary<string, string>
{
["1"] = "foundTag1",
["2"] = "foundTag2"
});
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id, null))
.Returns(asset); .Returns(asset);
sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }); sut = new AssetCommandMiddleware(grainFactory, assetQuery, assetStore, assetThumbnailGenerator, new[] { tagGenerator }, tagService);
} }
[Fact] [Fact]
public async Task Create_should_create_domain_object() public async Task Create_should_create_domain_object()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupTags(command); SetupTags(command);
@ -80,6 +84,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Contains("tag1", command.Tags); Assert.Contains("tag1", command.Tags);
Assert.Contains("tag2", command.Tags); Assert.Contains("tag2", command.Tags);
Assert.Equal(new HashSet<string> { "tag1", "tag2" }, result.Tags);
AssertAssetHasBeenUploaded(0, context.ContextId); AssertAssetHasBeenUploaded(0, context.ContextId);
AssertAssetImageChecked(); AssertAssetImageChecked();
} }
@ -87,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Create_should_calculate_hash() public async Task Create_should_calculate_hash()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -100,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _); SetupSameHashAsset(file.FileName, file.FileSize, out _);
@ -108,13 +114,15 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.HandleAsync(context); await sut.HandleAsync(context);
Assert.True(context.Result<AssetCreatedResult>().IsDuplicate); var result = context.Result<AssetCreatedResult>();
Assert.True(result.IsDuplicate);
} }
[Fact] [Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found() public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupSameHashAsset("other-name", file.FileSize, out _); SetupSameHashAsset("other-name", file.FileSize, out _);
@ -122,13 +130,31 @@ namespace Squidex.Domain.Apps.Entities.Assets
await sut.HandleAsync(context); await sut.HandleAsync(context);
Assert.False(context.Result<AssetCreatedResult>().IsDuplicate); var result = context.Result<AssetCreatedResult>();
Assert.False(result.IsDuplicate);
}
[Fact]
public async Task Create_should_resolve_tag_names_for_duplicate()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, file.FileSize, out _);
SetupImageInfo();
await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
} }
[Fact] [Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found() public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found()
{ {
var command = new CreateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, 12345, out _); SetupSameHashAsset(file.FileName, 12345, out _);
@ -142,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Update_should_update_domain_object() public async Task Update_should_update_domain_object()
{ {
var command = new UpdateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -158,7 +184,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact] [Fact]
public async Task Update_should_calculate_hash() public async Task Update_should_calculate_hash()
{ {
var command = new UpdateAsset { AssetId = assetId, File = file }; var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command); var context = CreateContextForCommand(command);
SetupImageInfo(); SetupImageInfo();
@ -170,6 +196,40 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.True(command.FileHash.Length > 10); Assert.True(command.FileHash.Length > 10);
} }
[Fact]
public async Task Update_should_resolve_tags()
{
var command = CreateCommand(new UpdateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
var result = context.Result<AssetResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
}
[Fact]
public async Task AnnotateAsset_should_resolve_tags()
{
var command = CreateCommand(new AnnotateAsset { AssetId = assetId, FileName = "newName" });
var context = CreateContextForCommand(command);
SetupImageInfo();
await ExecuteCreateAsync();
await sut.HandleAsync(context);
var result = context.Result<AssetResult>();
Assert.Equal(new HashSet<string> { "foundTag1", "foundTag2" }, result.Tags);
}
private Task ExecuteCreateAsync() private Task ExecuteCreateAsync()
{ {
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file })); return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file }));

Loading…
Cancel
Save