Browse Source

Asset dialog.

pull/352/head
Sebastian Stehle 7 years ago
parent
commit
8bed01fb1d
  1. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs
  3. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  4. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  5. 4
      src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  6. 34
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  7. 6
      src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs
  8. 14
      src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs
  9. 20
      src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  10. 35
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  11. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  12. 8
      src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs
  13. 2
      src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs
  14. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  15. 17
      src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs
  16. 6
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs
  17. 4
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  18. 2
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  19. 6
      src/Squidex/app/features/apps/pages/apps-page.component.html
  20. 2
      src/Squidex/app/features/apps/pages/news-dialog.component.html
  21. 6
      src/Squidex/app/features/apps/pages/news-dialog.component.ts
  22. 2
      src/Squidex/app/features/apps/pages/onboarding-dialog.component.html
  23. 6
      src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts
  24. 2
      src/Squidex/app/features/assets/pages/assets-page.component.html
  25. 16
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  26. 8
      src/Squidex/app/features/content/shared/array-editor.component.html
  27. 16
      src/Squidex/app/features/content/shared/array-item.component.html
  28. 34
      src/Squidex/app/features/content/shared/array-item.component.ts
  29. 14
      src/Squidex/app/features/content/shared/assets-editor.component.html
  30. 14
      src/Squidex/app/features/content/shared/content-item.component.html
  31. 36
      src/Squidex/app/features/content/shared/content-item.component.ts
  32. 8
      src/Squidex/app/features/content/shared/contents-selector.component.html
  33. 10
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  34. 2
      src/Squidex/app/features/content/shared/due-time-selector.component.html
  35. 6
      src/Squidex/app/features/content/shared/references-editor.component.html
  36. 8
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  37. 12
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  38. 2
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  39. 4
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  40. 10
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  41. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  42. 4
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html
  43. 8
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  44. 12
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  45. 4
      src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html
  46. 8
      src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts
  47. 4
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html
  48. 8
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  49. 4
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  50. 14
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  51. 6
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  52. 2
      src/Squidex/app/features/settings/pages/clients/client.component.html
  53. 15
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  54. 4
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.html
  55. 4
      src/Squidex/app/framework/angular/modals/modal-dialog.component.html
  56. 6
      src/Squidex/app/framework/angular/modals/modal-dialog.component.ts
  57. 4
      src/Squidex/app/framework/angular/pager.component.html
  58. 8
      src/Squidex/app/framework/angular/pager.component.ts
  59. 6
      src/Squidex/app/framework/angular/sorted.directive.ts
  60. 14
      src/Squidex/app/framework/state.ts
  61. 4
      src/Squidex/app/shared/components/app-form.component.html
  62. 8
      src/Squidex/app/shared/components/app-form.component.ts
  63. 40
      src/Squidex/app/shared/components/asset-dialog.component.html
  64. 2
      src/Squidex/app/shared/components/asset-dialog.component.scss
  65. 78
      src/Squidex/app/shared/components/asset-dialog.component.ts
  66. 56
      src/Squidex/app/shared/components/asset.component.html
  67. 120
      src/Squidex/app/shared/components/asset.component.ts
  68. 10
      src/Squidex/app/shared/components/assets-list.component.html
  69. 6
      src/Squidex/app/shared/components/assets-list.component.ts
  70. 10
      src/Squidex/app/shared/components/assets-selector.component.html
  71. 10
      src/Squidex/app/shared/components/assets-selector.component.ts
  72. 2
      src/Squidex/app/shared/components/comment.component.html
  73. 8
      src/Squidex/app/shared/components/comment.component.ts
  74. 4
      src/Squidex/app/shared/components/comments.component.html
  75. 2
      src/Squidex/app/shared/components/markdown-editor.component.html
  76. 2
      src/Squidex/app/shared/components/rich-editor.component.html
  77. 2
      src/Squidex/app/shared/components/rich-editor.component.ts
  78. 2
      src/Squidex/app/shared/components/schema-category.component.html
  79. 10
      src/Squidex/app/shared/components/schema-category.component.ts
  80. 2
      src/Squidex/app/shared/components/search-form.component.html
  81. 8
      src/Squidex/app/shared/components/search-form.component.ts
  82. 1
      src/Squidex/app/shared/declarations.ts
  83. 3
      src/Squidex/app/shared/module.ts
  84. 68
      src/Squidex/app/shared/services/assets.service.spec.ts
  85. 38
      src/Squidex/app/shared/services/assets.service.ts
  86. 44
      src/Squidex/app/shared/state/assets.forms.ts
  87. 8
      src/Squidex/app/shared/state/assets.state.spec.ts
  88. 2
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  89. 2
      src/Squidex/appsettings.json
  90. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  91. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  92. 35
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  93. 29
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs
  94. 18
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  95. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  96. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs
  97. 9
      tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs
  98. 2
      tools/Migrate_01/Migrations/CreateAssetSlugs.cs
  99. 13
      tools/Migrate_01/OldEvents/AssetRenamed.cs
  100. 27
      tools/Migrate_01/OldEvents/AssetTagged.cs

2
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEventType.cs

@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{ {
Created, Created,
Deleted, Deleted,
Renamed, Annotated,
Updated Updated
} }
} }

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs

@ -23,6 +23,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
string FileName { get; } string FileName { get; }
string FileNameSlug { get; } string Slug { get; }
} }
} }

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

@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonIgnoreIfDefault] [BsonIgnoreIfDefault]
[BsonElement] [BsonElement]
public string FileNameSlug { get; set; } public string Slug { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]

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

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.Ascending(x => x.Tags) .Ascending(x => x.Tags)
.Descending(x => x.LastModified)), .Descending(x => x.LastModified)),
new CreateIndexModel<MongoAssetEntity>( new CreateIndexModel<MongoAssetEntity>(
Index.Ascending(x => x.FileNameSlug)) Index.Ascending(x => x.Slug))
}, },
ct); ct);
} }
@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
using (Profiler.TraceMethod<MongoAssetRepository>()) using (Profiler.TraceMethod<MongoAssetRepository>())
{ {
var assetEntity = var assetEntity =
await Collection.Find(x => x.FileNameSlug == slug) await Collection.Find(x => x.Slug == slug)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
return assetEntity; return assetEntity;

4
src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -49,8 +49,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
case AssetCreated _: case AssetCreated _:
result.Type = EnrichedAssetEventType.Created; result.Type = EnrichedAssetEventType.Created;
break; break;
case AssetRenamed _: case AssetAnnotated _:
result.Type = EnrichedAssetEventType.Renamed; result.Type = EnrichedAssetEventType.Annotated;
break; break;
case AssetUpdated _: case AssetUpdated _:
result.Type = EnrichedAssetEventType.Updated; result.Type = EnrichedAssetEventType.Updated;

34
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -62,15 +62,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
return new AssetSavedResult(Version, Snapshot.FileVersion); return new AssetSavedResult(Version, Snapshot.FileVersion);
}); });
case TagAsset tagAsset:
return UpdateAsync(tagAsset, async c =>
{
GuardAsset.CanTag(c);
c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Tag(c);
});
case DeleteAsset deleteAsset: case DeleteAsset deleteAsset:
return UpdateAsync(deleteAsset, async c => return UpdateAsync(deleteAsset, async c =>
{ {
@ -80,12 +71,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
Delete(c); Delete(c);
}); });
case RenameAsset renameAsset: case AnnotateAsset annotateAsset:
return UpdateAsync(renameAsset, c => return UpdateAsync(annotateAsset, async c =>
{ {
GuardAsset.CanRename(c, Snapshot.FileName); GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
if (c.Tags != null)
{
c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
}
Rename(c); Annotate(c);
}); });
default: default:
throw new NotSupportedException(); throw new NotSupportedException();
@ -109,7 +105,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
MimeType = command.File.MimeType, MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth, PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight, PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null IsImage = command.ImageInfo != null,
Slug = command.File.FileName.ToAssetSlug()
}); });
RaiseEvent(@event); RaiseEvent(@event);
@ -137,14 +134,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
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 Annotate(AnnotateAsset command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetRenamed()));
}
public void Tag(TagAsset command)
{ {
RaiseEvent(SimpleMapper.Map(command, new AssetTagged())); RaiseEvent(SimpleMapper.Map(command, new AssetAnnotated()));
} }
private void RaiseEvent(AppEvent @event) private void RaiseEvent(AppEvent @event)

6
src/Squidex.Domain.Apps.Entities/Assets/Commands/TagAsset.cs → src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs

@ -9,8 +9,12 @@ using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class TagAsset : AssetCommand public sealed class AnnotateAsset : AssetCommand
{ {
public string FileName { get; set; }
public string Slug { get; set; }
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; set; }
} }
} }

14
src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs

@ -1,14 +0,0 @@
// ==========================================================================
// 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 RenameAsset : AssetCommand
{
public string FileName { get; set; }
}
}

20
src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs

@ -12,21 +12,28 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{ {
public static class GuardAsset public static class GuardAsset
{ {
public static void CanRename(RenameAsset command, string oldName) public static void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot rename asset.", e => Validate.It(() => "Cannot rename asset.", e =>
{ {
if (string.IsNullOrWhiteSpace(command.FileName)) if (string.IsNullOrWhiteSpace(command.FileName) &&
string.IsNullOrWhiteSpace(command.Slug) &&
command.Tags == null)
{ {
e(Not.Defined("Name"), nameof(command.FileName)); e("Either file name, slug or tags must be defined.", nameof(command.FileName), nameof(command.Slug), nameof(command.Tags));
} }
if (string.Equals(command.FileName, oldName)) if (!string.IsNullOrWhiteSpace(command.FileName) && string.Equals(command.FileName, oldFileName))
{ {
e(Not.New("Asset", "name"), nameof(command.FileName)); e(Not.New("Asset", "name"), nameof(command.FileName));
} }
if (!string.IsNullOrWhiteSpace(command.Slug) && string.Equals(command.Slug, oldSlug))
{
e(Not.New("Asset", "slug"), nameof(command.Slug));
}
}); });
} }
@ -35,11 +42,6 @@ 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));

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

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
public string MimeType { get; set; } public string MimeType { get; set; }
[DataMember] [DataMember]
public string FileNameSlug { get; set; } public string Slug { get; set; }
[DataMember] [DataMember]
public long FileVersion { get; set; } public long FileVersion { get; set; }
@ -66,7 +66,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
SimpleMapper.Map(@event, this); SimpleMapper.Map(@event, this);
FileName = @event.FileName; FileName = @event.FileName;
FileNameSlug = @event.FileName.ToAssetSlug();
if (string.IsNullOrWhiteSpace(@event.Slug))
{
Slug = @event.FileName.ToAssetSlug();
}
else
{
Slug = @event.Slug;
}
TotalSize += @event.FileSize; TotalSize += @event.FileSize;
} }
@ -78,15 +86,22 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
TotalSize += @event.FileSize; TotalSize += @event.FileSize;
} }
protected void On(AssetRenamed @event) protected void On(AssetAnnotated @event)
{
FileName = @event.FileName;
FileNameSlug = @event.FileName.ToAssetSlug();
}
protected void On(AssetTagged @event)
{ {
Tags = @event.Tags; if (!string.IsNullOrWhiteSpace(@event.FileName))
{
FileName = @event.FileName;
}
if (!string.IsNullOrWhiteSpace(@event.Slug))
{
Slug = @event.Slug;
}
if (@event.Tags != null)
{
Tags = @event.Tags;
}
} }
protected void On(AssetDeleted @event) protected void On(AssetDeleted @event)

4
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -101,9 +101,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = "fileNameSlug", Name = "slug",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileNameSlug), Resolver = Resolve(x => x.Slug),
Description = "The file name as slug." Description = "The file name as slug."
}); });

8
src/Squidex.Domain.Apps.Events/Assets/AssetTagged.cs → src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs

@ -10,9 +10,13 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets namespace Squidex.Domain.Apps.Events.Assets
{ {
[EventType(nameof(AssetTagged))] [EventType(nameof(AssetAnnotated))]
public sealed class AssetTagged : AssetEvent public sealed class AssetAnnotated : AssetEvent
{ {
public string FileName { get; set; }
public string Slug { get; set; }
public HashSet<string> Tags { get; set; } public HashSet<string> Tags { get; set; }
} }
} }

2
src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Events.Assets
public string MimeType { get; set; } public string MimeType { get; set; }
public string Slug { get; set; }
public long FileVersion { get; set; } public long FileVersion { get; set; }
public long FileSize { get; set; } public long FileSize { get; set; }

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

@ -237,7 +237,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[AssetRequestSizeLimit] [AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)] [ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] UpdateAssetDto request) public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request)
{ {
await CommandBus.PublishAsync(request.ToCommand(id)); await CommandBus.PublishAsync(request.ToCommand(id));

17
src/Squidex/Areas/Api/Controllers/Assets/Models/UpdateAssetDto.cs → src/Squidex/Areas/Api/Controllers/Assets/Models/AnnotateAssetDto.cs

@ -8,16 +8,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
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
{ {
public sealed class UpdateAssetDto public sealed class AnnotateAssetDto
{ {
/// <summary> /// <summary>
/// The new name of the asset. /// The new name of the asset.
/// </summary> /// </summary>
public string FileName { get; set; } public string FileName { get; set; }
/// <summary>
/// The new slug of the asset.
/// </summary>
public string Slug { get; set; }
/// <summary> /// <summary>
/// The new asset tags. /// The new asset tags.
/// </summary> /// </summary>
@ -25,14 +31,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
public AssetCommand ToCommand(Guid id) public AssetCommand ToCommand(Guid id)
{ {
if (Tags != null) return SimpleMapper.Map(this, new AnnotateAsset { AssetId = id });
{
return new TagAsset { AssetId = id, Tags = Tags };
}
else
{
return new RenameAsset { AssetId = id, FileName = FileName };
}
} }
} }
} }

6
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs

@ -33,6 +33,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required] [Required]
public string FileName { get; set; } public string FileName { get; set; }
/// <summary>
/// The slug.
/// </summary>
[Required]
public string Slug { get; set; }
/// <summary> /// <summary>
/// The mime type. /// The mime type.
/// </summary> /// </summary>

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

@ -30,10 +30,10 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
public string FileName { get; set; } public string FileName { get; set; }
/// <summary> /// <summary>
/// The file name as a slug. /// The slug.
/// </summary> /// </summary>
[Required] [Required]
public string FileNameSlug { get; set; } public string Slug { get; set; }
/// <summary> /// <summary>
/// The mime type. /// The mime type.

2
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -59,7 +59,7 @@
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>
<sqx-modal-dialog *sqxModalView="eventConsumerErrorDialog;onRoot:true" (closed)="eventConsumerErrorDialog.hide()"> <sqx-modal-dialog *sqxModalView="eventConsumerErrorDialog;onRoot:true" (close)="eventConsumerErrorDialog.hide()">
<ng-container title> <ng-container title>
Error Error
</ng-container> </ng-container>

6
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -95,18 +95,18 @@
<ng-container *sqxModalView="addAppDialog;onRoot:true"> <ng-container *sqxModalView="addAppDialog;onRoot:true">
<sqx-app-form [template]="addAppTemplate" <sqx-app-form [template]="addAppTemplate"
(completed)="addAppDialog.hide()"> (complete)="addAppDialog.hide()">
</sqx-app-form> </sqx-app-form>
</ng-container> </ng-container>
<ng-container *sqxModalView="onboardingDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="onboardingDialog;onRoot:true;closeAuto:false">
<sqx-onboarding-dialog <sqx-onboarding-dialog
(closed)="onboardingDialog.hide()"> (close)="onboardingDialog.hide()">
</sqx-onboarding-dialog> </sqx-onboarding-dialog>
</ng-container> </ng-container>
<ng-container *sqxModalView="newsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="newsDialog;onRoot:true;closeAuto:false">
<sqx-news-dialog [features]="newsFeatures" <sqx-news-dialog [features]="newsFeatures"
(closed)="newsDialog.hide()"> (close)="newsDialog.hide()">
</sqx-news-dialog> </sqx-news-dialog>
</ng-container> </ng-container>

2
src/Squidex/app/features/apps/pages/news-dialog.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog large="true" (closed)="closed.emit()"> <sqx-modal-dialog large="true" (close)="emitClose()">
<ng-container title> <ng-container title>
New Features New Features
</ng-container> </ng-container>

6
src/Squidex/app/features/apps/pages/news-dialog.component.ts

@ -19,9 +19,9 @@ export class NewsDialogComponent {
public features: FeatureDto[]; public features: FeatureDto[];
@Output() @Output()
public closed = new EventEmitter(); public close = new EventEmitter();
public close() { public emitClose() {
this.closed.emit(); this.close.emit();
} }
} }

2
src/Squidex/app/features/apps/pages/onboarding-dialog.component.html

@ -1,6 +1,6 @@
<sqx-modal-dialog [showHeader]="false"> <sqx-modal-dialog [showHeader]="false">
<ng-container content> <ng-container content>
<a class="header-right modal-close" (click)="close()">Skip Tour</a> <a class="header-right modal-close" (click)="emitClose()">Skip Tour</a>
<div class="onboarding-step" *ngIf="step === 0"> <div class="onboarding-step" *ngIf="step === 0">
<img @fade class="header-left" src="/images/logo-white-small.png" /> <img @fade class="header-left" src="/images/logo-white-small.png" />

6
src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts

@ -21,10 +21,10 @@ export class OnboardingDialogComponent {
public step = 0; public step = 0;
@Output() @Output()
public closed = new EventEmitter(); public close = new EventEmitter();
public close() { public emitClose() {
this.closed.emit(); this.close.emit();
} }
public next() { public next() {

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

@ -26,7 +26,7 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize" <sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize"
(queryChanged)="search($event)" (queryChange)="search($event)"
[query]="assetsState.assetsQuery | async" [query]="assetsState.assetsQuery | async"
[queries]="queries" [queries]="queries"
enableShortcut="true"> enableShortcut="true">

16
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -22,10 +22,10 @@
</div> </div>
<div class="col pl-1"> <div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv" <sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv"
(queryChanged)="search($event)" (queryChange)="search($event)"
[query]="contentsState.contentsQuery | async" [query]="contentsState.contentsQuery | async"
[queries]="schemaQueries" [queries]="schemaQueries"
(archivedChanged)="goArchive($event)" (archivedChange)="goArchive($event)"
[archived]="contentsState.isArchive | async" [archived]="contentsState.isArchive | async"
expandable="true" expandable="true"
enableArchive="true" enableArchive="true"
@ -106,12 +106,12 @@
[schema]="schema" [schema]="schema"
[selected]="isItemSelected(content)" [selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)" (selectedChange)="selectItem(content, $event)"
(unpublishing)="unpublish(content)" (unpublishe)="unpublish(content)"
(publishing)="publish(content)" (publishe)="publish(content)"
(archiving)="archive(content)" (archive)="archive(content)"
(restoring)="restore(content)" (restore)="restore(content)"
(deleting)="delete(content)" (delete)="delete(content)"
(cloning)="clone(content)"> (clone)="clone(content)">
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</tbody> </tbody>

8
src/Squidex/app/features/content/shared/array-editor.component.html

@ -1,6 +1,6 @@
<div class="array-container" *ngIf="arrayControl.controls.length > 0" <div class="array-container" *ngIf="arrayControl.controls.length > 0"
[sqxSortModel]="arrayControl.controls" [sqxSortModel]="arrayControl.controls"
(sqxSorted)="sort($event)"> (sqxSort)="sort($event)">
<div class="item" *ngFor="let itemForm of arrayControl.controls; let i = index"> <div class="item" *ngFor="let itemForm of arrayControl.controls; let i = index">
<sqx-array-item <sqx-array-item
[form]="form" [form]="form"
@ -13,9 +13,9 @@
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
(toggle)="hide($event)" (toggle)="hide($event)"
(moving)="move(itemForm, $event)" (move)="move(itemForm, $event)"
(cloning)="addItem(itemForm)" (clone)="addItem(itemForm)"
(removing)="removeItem(i)"> (remove)="removeItem(i)">
</sqx-array-item> </sqx-array-item>
</div> </div>
</div> </div>

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

@ -5,32 +5,32 @@
<span class="header-text text-decent">Item #{{index + 1}}</span> <span class="header-text text-decent">Item #{{index + 1}}</span>
<button type="button" class="btn btn-text-secondary" [disabled]="isFirst" (click)="moveTop()"> <button type="button" class="btn btn-text-secondary" [disabled]="isFirst" (click)="emitMoveTop()">
<i class="icon-caret-top"></i> <i class="icon-caret-top"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isFirst" (click)="moveUp()"> <button type="button" class="btn btn-text-secondary" [disabled]="isFirst" (click)="emitMoveUp()">
<i class="icon-caret-up"></i> <i class="icon-caret-up"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isLast" (click)="moveDown()"> <button type="button" class="btn btn-text-secondary" [disabled]="isLast" (click)="emitMoveDown()">
<i class="icon-caret-down"></i> <i class="icon-caret-down"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isLast" (click)="moveBottom()"> <button type="button" class="btn btn-text-secondary" [disabled]="isLast" (click)="emitMoveBottom()">
<i class="icon-caret-bottom"></i> <i class="icon-caret-bottom"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="!isHidden" (click)="toggle.emit(false)" title="Open all items"> <button type="button" class="btn btn-text-secondary" [class.hidden]="!isHidden" (click)="emitToggle(false)" title="Open all items">
<i class="icon-plus-square"></i> <i class="icon-plus-square"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="isHidden" (click)="toggle.emit(true)" title="Close all items"> <button type="button" class="btn btn-text-secondary" [class.hidden]="isHidden" (click)="emitToggle(true)" title="Close all items">
<i class="icon-minus-square"></i> <i class="icon-minus-square"></i>
</button> </button>
</span> </span>
<span class="float-right"> <span class="float-right">
<button type="button" class="btn btn-text-secondary" (click)="cloning.emit()"> <button type="button" class="btn btn-text-secondary" (click)="emitClone()">
<i class="icon-clone"></i> <i class="icon-clone"></i>
</button> </button>
<button type="button" class="btn btn-text-danger" (click)="removing.emit()"> <button type="button" class="btn btn-text-danger" (click)="emitRemove()">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>

34
src/Squidex/app/features/content/shared/array-item.component.ts

@ -25,13 +25,13 @@ import {
}) })
export class ArrayItemComponent implements OnChanges { export class ArrayItemComponent implements OnChanges {
@Output() @Output()
public removing = new EventEmitter(); public remove = new EventEmitter();
@Output() @Output()
public moving = new EventEmitter<number>(); public move = new EventEmitter<number>();
@Output() @Output()
public cloning = new EventEmitter(); public clone = new EventEmitter();
@Output() @Output()
public toggle = new EventEmitter<boolean>(); public toggle = new EventEmitter<boolean>();
@ -77,19 +77,31 @@ export class ArrayItemComponent implements OnChanges {
} }
} }
public moveTop() { public emitToggle(value: boolean) {
this.moving.emit(0); this.toggle.emit(value);
} }
public moveUp() { public emitClone() {
this.moving.emit(this.index - 1); this.clone.emit();
} }
public moveDown() { public emitRemove() {
this.moving.emit(this.index + 1); this.remove.emit();
} }
public moveBottom() { public emitMoveTop() {
this.moving.emit(99999); this.move.emit(0);
}
public emitMoveUp() {
this.move.emit(this.index - 1);
}
public emitMoveDown() {
this.move.emit(this.index + 1);
}
public emitMoveBottom() {
this.move.emit(99999);
} }
} }

14
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -24,10 +24,10 @@
<ng-container *ngIf="!snapshot.isListView; else listTemplate"> <ng-container *ngIf="!snapshot.isListView; else listTemplate">
<div class="row no-gutters"> <div class="row no-gutters">
<sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file" <sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file"
(failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)"> (faile)="removeLoadingAsset(file)" (load)="addAsset(file, $event)">
</sqx-asset> </sqx-asset>
<sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" [asset]="asset" [isCompact]="isCompact" removeMode="true" <sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" [asset]="asset" [isCompact]="isCompact" removeMode="true"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)"> (update)="notifyOthers($event)" (remove)="removeLoadedAsset($event)">
</sqx-asset> </sqx-asset>
</div> </div>
</ng-container> </ng-container>
@ -35,17 +35,19 @@
<ng-template #listTemplate> <ng-template #listTemplate>
<div class="list-view"> <div class="list-view">
<sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file" <sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file"
[isListView]="true" (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)"> [isListView]="true"
(loadError)="removeLoadingAsset(file)"
(load)="addAsset(file, $event)">
</sqx-asset> </sqx-asset>
<div <div
[sqxSortModel]="snapshot.assets.mutableValues" [sqxSortModel]="snapshot.assets.mutableValues"
(sqxSorted)="sortAssets($event)"> (sqxSort)="sortAssets($event)">
<div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset"> <div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset">
<sqx-asset [asset]="asset" removeMode="true" <sqx-asset [asset]="asset" removeMode="true"
[isListView]="true" [isListView]="true"
[isCompact]="isCompact" [isCompact]="isCompact"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)"> (update)="notifyOthers($event)" (remove)="removeLoadedAsset($event)">
</sqx-asset> </sqx-asset>
</div> </div>
</div> </div>
@ -56,6 +58,6 @@
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">
<sqx-assets-selector <sqx-assets-selector
(selected)="selectAssets($event)"> (select)="selectAssets($event)">
</sqx-assets-selector> </sqx-assets-selector>
</ng-container> </ng-container>

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

@ -54,26 +54,26 @@
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="publishing.emit()" *ngIf="content.status === 'Draft'"> <a class="dropdown-item" (click)="emitPublish()" *ngIf="content.status === 'Draft'">
Publish Publish
</a> </a>
<a class="dropdown-item" (click)="unpublishing.emit()" *ngIf="content.status === 'Published'"> <a class="dropdown-item" (click)="emitUnpublish()" *ngIf="content.status === 'Published'">
Unpublish Unpublish
</a> </a>
<a class="dropdown-item" (click)="archiving.emit()" *ngIf="content.status !== 'Archived'"> <a class="dropdown-item" (click)="emitArchive()" *ngIf="content.status !== 'Archived'">
Archive Archive
</a> </a>
<a class="dropdown-item" (click)="restoring.emit()" *ngIf="content.status === 'Archived'"> <a class="dropdown-item" (click)="emitRestore()" *ngIf="content.status === 'Archived'">
Restore Restore
</a> </a>
<a class="dropdown-item" (click)="cloning.emit(); dropdown.hide()" *ngIf="content.status !== 'Archived'"> <a class="dropdown-item" (click)="emitClone(); dropdown.hide()" *ngIf="content.status !== 'Archived'">
Clone Clone
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="deleting.emit()" (sqxConfirmClick)="emitDelete()"
confirmTitle="Delete content" confirmTitle="Delete content"
confirmText="Do you really want to delete the content?"> confirmText="Do you really want to delete the content?">
Delete Delete
@ -82,7 +82,7 @@
</div> </div>
</td> </td>
<td class="cell-actions" *ngIf="isReference" (click)="shouldStop($event)"> <td class="cell-actions" *ngIf="isReference" (click)="shouldStop($event)">
<button type="button" class="btn btn-text-secondary" (click)="deleting.emit()"> <button type="button" class="btn btn-text-secondary" (click)="emitDelete()">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</td> </td>

36
src/Squidex/app/features/content/shared/content-item.component.ts

@ -35,22 +35,22 @@ import {
}) })
export class ContentItemComponent implements OnChanges { export class ContentItemComponent implements OnChanges {
@Output() @Output()
public cloning = new EventEmitter(); public clone = new EventEmitter();
@Output() @Output()
public deleting = new EventEmitter(); public delete = new EventEmitter();
@Output() @Output()
public archiving = new EventEmitter(); public archive = new EventEmitter();
@Output() @Output()
public restoring = new EventEmitter(); public restore = new EventEmitter();
@Output() @Output()
public publishing = new EventEmitter(); public publish = new EventEmitter();
@Output() @Output()
public unpublishing = new EventEmitter(); public unpublish = new EventEmitter();
@Output() @Output()
public selectedChange = new EventEmitter(); public selectedChange = new EventEmitter();
@ -128,6 +128,30 @@ export class ContentItemComponent implements OnChanges {
this.updateValues(); this.updateValues();
} }
public emitDelete() {
this.delete.emit();
}
public emitPublish() {
this.publish.emit();
}
public emitUnpublish() {
this.unpublish.emit();
}
public emitArchive() {
this.archive.emit();
}
public emitRestore() {
this.unpublish.emit();
}
public emitClone() {
this.clone.emit();
}
private updateValues() { private updateValues() {
this.values = []; this.values = [];

8
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog (closed)="complete()" large="true" fullHeight="true" contentClass="grid"> <sqx-modal-dialog (close)="emitComplete()" large="true" fullHeight="true" contentClass="grid">
<ng-container title> <ng-container title>
Select contents Select contents
</ng-container> </ng-container>
@ -13,7 +13,7 @@
<div class="col pl-1"> <div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv" <sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv"
[query]="contentsState.contentsQuery | async" [query]="contentsState.contentsQuery | async"
(queryChanged)="search($event)" (queryChange)="search($event)"
expandable="true"> expandable="true">
</sqx-search-form> </sqx-search-form>
</div> </div>
@ -68,7 +68,7 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<button type="submit" class="float-right btn btn-success" (click)="select()" [disabled]="selectionCount === 0">Link selected contents ({{selectionCount}})</button> <button type="submit" class="float-right btn btn-success" (click)="emitSelect()" [disabled]="selectionCount === 0">Link selected contents ({{selectionCount}})</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

10
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -35,7 +35,7 @@ export class ContentsSelectorComponent implements OnInit {
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@Output() @Output()
public selected = new EventEmitter<ContentDto[]>(); public select = new EventEmitter<ContentDto[]>();
public searchModal = new ModalModel(); public searchModal = new ModalModel();
@ -75,12 +75,12 @@ export class ContentsSelectorComponent implements OnInit {
return this.selectedItems[content.id]; return this.selectedItems[content.id];
} }
public complete() { public emitComplete() {
this.selected.emit([]); this.select.emit([]);
} }
public select() { public emitSelect() {
this.selected.emit(Object.values(this.selectedItems)); this.select.emit(Object.values(this.selectedItems));
} }
public selectLanguage(language: LanguageDto) { public selectLanguage(language: LanguageDto) {

2
src/Squidex/app/features/content/shared/due-time-selector.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog *sqxModalView="dueTimeDialog;onRoot:true" (closed)="cancelStatusChange()"> <sqx-modal-dialog *sqxModalView="dueTimeDialog;onRoot:true" (close)="cancelStatusChange()">
<ng-container title> <ng-container title>
{{dueTimeAction}} content item(s) {{dueTimeAction}} content item(s)
</ng-container> </ng-container>

6
src/Squidex/app/features/content/shared/references-editor.component.html

@ -8,7 +8,7 @@
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.schema && snapshot.contentItems && snapshot.contentItems.length > 0" <table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.schema && snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems.mutableValues" [sqxSortModel]="snapshot.contentItems.mutableValues"
(sqxSorted)="sort($event)"> (sqxSort)="sort($event)">
<tbody *ngFor="let content of snapshot.contentItems"> <tbody *ngFor="let content of snapshot.contentItems">
<tr [sqxContent]="content" <tr [sqxContent]="content"
[language]="language" [language]="language"
@ -16,7 +16,7 @@
[isReference]="true" [isReference]="true"
[isCompact]="isCompact" [isCompact]="isCompact"
[schema]="snapshot.schema" [schema]="snapshot.schema"
(deleting)="remove(content)"></tr> (delete)="remove(content)"></tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>
@ -32,6 +32,6 @@
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schema]="snapshot.schema" [schema]="snapshot.schema"
(selected)="select($event)"> (select)="select($event)">
</sqx-contents-selector> </sqx-contents-selector>
</ng-container> </ng-container>

8
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog large="true" fullHeight="true" (closed)="complete()" [showFooter]="step === 2 || step === 4"> <sqx-modal-dialog large="true" fullHeight="true" (close)="emitComplete()" [showFooter]="step === 2 || step === 4">
<ng-container title> <ng-container title>
<ng-container *ngIf="mode === 'EditTrigger'"> <ng-container *ngIf="mode === 'EditTrigger'">
Edit Trigger Edit Trigger
@ -92,17 +92,17 @@
<ng-container footer> <ng-container footer>
<div> <div>
<ng-container *ngIf="mode === 'Wizard' && step === 2"> <ng-container *ngIf="mode === 'Wizard' && step === 2">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="saveTrigger()">Next</button> <button type="submit" class="float-right btn btn-primary" (click)="saveTrigger()">Next</button>
</ng-container> </ng-container>
<ng-container *ngIf="mode !== 'Wizard' && step === 2"> <ng-container *ngIf="mode !== 'Wizard' && step === 2">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="saveTrigger()">Save</button> <button type="submit" class="float-right btn btn-primary" (click)="saveTrigger()">Save</button>
</ng-container> </ng-container>
<ng-container *ngIf="step === 4"> <ng-container *ngIf="step === 4">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="saveAction()">Save</button> <button type="submit" class="float-right btn btn-primary" (click)="saveAction()">Save</button>
</ng-container> </ng-container>
</div> </div>

12
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts

@ -39,7 +39,7 @@ export class RuleWizardComponent implements OnInit {
public step = 1; public step = 1;
@Output() @Output()
public completed = new EventEmitter(); public complete = new EventEmitter();
@Input() @Input()
public ruleActions: { [name: string]: RuleElementDto }; public ruleActions: { [name: string]: RuleElementDto };
@ -75,8 +75,8 @@ export class RuleWizardComponent implements OnInit {
} }
} }
public complete() { public emitComplete() {
this.completed.emit(); this.complete.emit();
} }
public selectTriggerType(type: string) { public selectTriggerType(type: string) {
@ -122,7 +122,7 @@ export class RuleWizardComponent implements OnInit {
this.rulesState.create(requestDto) this.rulesState.create(requestDto)
.subscribe(() => { .subscribe(() => {
this.complete(); this.emitComplete();
this.actionForm.submitCompleted(); this.actionForm.submitCompleted();
this.triggerForm.submitCompleted(); this.triggerForm.submitCompleted();
@ -135,7 +135,7 @@ export class RuleWizardComponent implements OnInit {
private updateTrigger() { private updateTrigger() {
this.rulesState.updateTrigger(this.rule, this.trigger) this.rulesState.updateTrigger(this.rule, this.trigger)
.subscribe(() => { .subscribe(() => {
this.complete(); this.emitComplete();
this.triggerForm.submitCompleted(); this.triggerForm.submitCompleted();
}, error => { }, error => {
@ -146,7 +146,7 @@ export class RuleWizardComponent implements OnInit {
private updateAction() { private updateAction() {
this.rulesState.updateAction(this.rule, this.action) this.rulesState.updateAction(this.rule, this.action)
.subscribe(() => { .subscribe(() => {
this.complete(); this.emitComplete();
this.actionForm.submitCompleted(); this.actionForm.submitCompleted();
}, error => { }, error => {

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

@ -70,7 +70,7 @@
[ruleActions]="ruleActions" [ruleActions]="ruleActions"
[ruleTriggers]="ruleTriggers" [ruleTriggers]="ruleTriggers"
[mode]="wizardMode" [mode]="wizardMode"
(completed)="addRuleDialog.hide()"> (complete)="addRuleDialog.hide()">
</sqx-rule-wizard> </sqx-rule-wizard>
</ng-container> </ng-container>
</ng-container> </ng-container>

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

@ -1,4 +1,4 @@
<sqx-modal-dialog (closed)="complete()" large="true"> <sqx-modal-dialog (close)="emitComplete()" large="true">
<ng-container title> <ng-container title>
<ng-container *ngIf="parent; else noParent"> <ng-container *ngIf="parent; else noParent">
Add Nested Field Add Nested Field
@ -91,7 +91,7 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<div class="float-right" *ngIf="!isEditing"> <div class="float-right" *ngIf="!isEditing">
<button type="button" class="btn btn-outline-success mr-1" (click)="addField(false, false)">Create and close</button> <button type="button" class="btn btn-outline-success mr-1" (click)="addField(false, false)">Create and close</button>

10
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts

@ -38,7 +38,7 @@ export class FieldWizardComponent implements OnInit {
public parent: RootFieldDto; public parent: RootFieldDto;
@Output() @Output()
public completed = new EventEmitter(); public complete = new EventEmitter();
public fieldTypes = fieldTypes; public fieldTypes = fieldTypes;
public field: FieldDto; public field: FieldDto;
@ -62,8 +62,8 @@ export class FieldWizardComponent implements OnInit {
} }
} }
public complete() { public emitComplete() {
this.completed.emit(); this.complete.emit();
} }
public addField(addNew: boolean, edit: boolean) { public addField(addNew: boolean, edit: boolean) {
@ -85,7 +85,7 @@ export class FieldWizardComponent implements OnInit {
this.isEditing = true; this.isEditing = true;
} else { } else {
this.complete(); this.emitComplete();
} }
}, error => { }, error => {
this.addFieldForm.submitFailed(error); this.addFieldForm.submitFailed(error);
@ -110,7 +110,7 @@ export class FieldWizardComponent implements OnInit {
if (addNew) { if (addNew) {
this.isEditing = false; this.isEditing = false;
} else { } else {
this.complete(); this.emitComplete();
} }
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);

4
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -107,7 +107,7 @@
<ng-container *ngIf="field['nested']; let nested"> <ng-container *ngIf="field['nested']; let nested">
<span class="nested-field-line-v"></span> <span class="nested-field-line-v"></span>
<div [sqxSortModel]="nested" (sqxSorted)="sortFields($event)"> <div [sqxSortModel]="nested" (sqxSort)="sortFields($event)">
<div class="nested-field" *ngFor="let nested of nested; trackBy: trackByField.bind(this)"> <div class="nested-field" *ngFor="let nested of nested; trackBy: trackByField.bind(this)">
<span class="nested-field-line-h"></span> <span class="nested-field-line-h"></span>
@ -125,7 +125,7 @@
<ng-container *sqxModalView="addFieldDialog;onRoot:true;closeAuto:false;closeAlways:true"> <ng-container *sqxModalView="addFieldDialog;onRoot:true;closeAuto:false;closeAlways:true">
<sqx-field-wizard [schema]="schema" [parent]="field" <sqx-field-wizard [schema]="schema" [parent]="field"
(completed)="addFieldDialog.hide()"> (complete)="addFieldDialog.hide()">
</sqx-field-wizard> </sqx-field-wizard>
</ng-container> </ng-container>
</ng-container> </ng-container>

4
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html

@ -1,5 +1,5 @@
<form [formGroup]="editForm.form" (ngSubmit)="saveSchema()"> <form [formGroup]="editForm.form" (ngSubmit)="saveSchema()">
<sqx-modal-dialog (closed)="complete()"> <sqx-modal-dialog (close)="emitComplete()">
<ng-container title> <ng-container title>
Edit Schema Edit Schema
</ng-container> </ng-container>
@ -29,7 +29,7 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button> <button type="submit" class="float-right btn btn-primary">Save</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

8
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts

@ -21,7 +21,7 @@ import {
}) })
export class SchemaEditFormComponent implements OnInit { export class SchemaEditFormComponent implements OnInit {
@Output() @Output()
public completed = new EventEmitter(); public complete = new EventEmitter();
@Input() @Input()
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@ -38,8 +38,8 @@ export class SchemaEditFormComponent implements OnInit {
this.editForm.load(this.schema.properties); this.editForm.load(this.schema.properties);
} }
public complete() { public emitComplete() {
this.completed.emit(); this.complete.emit();
} }
public saveSchema() { public saveSchema() {
@ -48,7 +48,7 @@ export class SchemaEditFormComponent implements OnInit {
if (value) { if (value) {
this.schemasState.update(this.schema, value) this.schemasState.update(this.schema, value)
.subscribe(() => { .subscribe(() => {
this.complete(); this.emitComplete();
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);
}); });

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

@ -69,7 +69,7 @@
<ng-container *ngIf="patternsState.patterns | async; let patterns"> <ng-container *ngIf="patternsState.patterns | async; let patterns">
<div class="schemas" <div class="schemas"
[sqxSortModel]="schema.fields" [sqxSortModel]="schema.fields"
(sqxSorted)="sortFields($event)"> (sqxSort)="sortFields($event)">
<div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)"> <div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)">
<sqx-field [field]="field" [schema]="schema" [patterns]="patterns"></sqx-field> <sqx-field [field]="field" [schema]="schema" [patterns]="patterns"></sqx-field>
</div> </div>
@ -94,30 +94,30 @@
<ng-container *sqxModalView="editSchemaDialog;onRoot:true"> <ng-container *sqxModalView="editSchemaDialog;onRoot:true">
<sqx-schema-edit-form [schema]="schema" <sqx-schema-edit-form [schema]="schema"
(completed)="editSchemaDialog.hide()"> (complete)="editSchemaDialog.hide()">
</sqx-schema-edit-form> </sqx-schema-edit-form>
</ng-container> </ng-container>
<ng-container *sqxModalView="addFieldDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="addFieldDialog;onRoot:true;closeAuto:false">
<sqx-field-wizard [schema]="schema" <sqx-field-wizard [schema]="schema"
(completed)="addFieldDialog.hide()"> (complete)="addFieldDialog.hide()">
</sqx-field-wizard> </sqx-field-wizard>
</ng-container> </ng-container>
<ng-container *sqxModalView="configureScriptsDialog;onRoot:true"> <ng-container *sqxModalView="configureScriptsDialog;onRoot:true">
<sqx-schema-scripts-form [schema]="schema" <sqx-schema-scripts-form [schema]="schema"
(completed)="configureScriptsDialog.hide()"> (complete)="configureScriptsDialog.hide()">
</sqx-schema-scripts-form> </sqx-schema-scripts-form>
</ng-container> </ng-container>
<ng-container *sqxModalView="configurePreviewUrlsDialog;onRoot:true"> <ng-container *sqxModalView="configurePreviewUrlsDialog;onRoot:true">
<sqx-schema-preview-urls-form [schema]="schema" <sqx-schema-preview-urls-form [schema]="schema"
(completed)="configurePreviewUrlsDialog.hide()"> (complete)="configurePreviewUrlsDialog.hide()">
</sqx-schema-preview-urls-form> </sqx-schema-preview-urls-form>
</ng-container> </ng-container>
<ng-container *sqxModalView="exportSchemaDialog;onRoot:true"> <ng-container *sqxModalView="exportSchemaDialog;onRoot:true">
<sqx-modal-dialog (closed)="exportSchemaDialog.hide()" large="true"> <sqx-modal-dialog (close)="exportSchemaDialog.hide()" large="true">
<ng-container title> <ng-container title>
Export Schema Export Schema
</ng-container> </ng-container>

4
src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html

@ -1,5 +1,5 @@
<form [formGroup]="editForm.form" (ngSubmit)="saveSchema()"> <form [formGroup]="editForm.form" (ngSubmit)="saveSchema()">
<sqx-modal-dialog (closed)="complete()" large="true"> <sqx-modal-dialog (close)="emitComplete()" large="true">
<ng-container title> <ng-container title>
Preview Urls Preview Urls
</ng-container> </ng-container>
@ -58,7 +58,7 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()" [disabled]="editForm.submitted | async">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()" [disabled]="editForm.submitted | async">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button> <button type="submit" class="float-right btn btn-primary">Save</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

8
src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts

@ -22,7 +22,7 @@ import {
}) })
export class SchemaPreviewUrlsFormComponent implements OnInit { export class SchemaPreviewUrlsFormComponent implements OnInit {
@Output() @Output()
public completed = new EventEmitter(); public complete = new EventEmitter();
@Input() @Input()
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@ -41,8 +41,8 @@ export class SchemaPreviewUrlsFormComponent implements OnInit {
this.editForm.load(this.schema.previewUrls); this.editForm.load(this.schema.previewUrls);
} }
public complete() { public emitComplete() {
this.completed.emit(); this.complete.emit();
} }
public cancelAdd() { public cancelAdd() {
@ -65,7 +65,7 @@ export class SchemaPreviewUrlsFormComponent implements OnInit {
if (value) { if (value) {
this.schemasState.configurePreviewUrls(this.schema, value) this.schemasState.configurePreviewUrls(this.schema, value)
.subscribe(() => { .subscribe(() => {
this.complete(); this.emitComplete();
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);
}); });

4
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html

@ -1,5 +1,5 @@
<form [formGroup]="editForm.form" (ngSubmit)="saveSchema()"> <form [formGroup]="editForm.form" (ngSubmit)="saveSchema()">
<sqx-modal-dialog (closed)="complete()" large="true"> <sqx-modal-dialog (close)="emitComplete()" large="true">
<ng-container title> <ng-container title>
Scripts Scripts
</ng-container> </ng-container>
@ -23,7 +23,7 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()" [disabled]="editForm.submitted | async">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()" [disabled]="editForm.submitted | async">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button> <button type="submit" class="float-right btn btn-primary">Save</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

8
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts

@ -21,7 +21,7 @@ import {
}) })
export class SchemaScriptsFormComponent implements OnInit { export class SchemaScriptsFormComponent implements OnInit {
@Output() @Output()
public completed = new EventEmitter(); public complete = new EventEmitter();
@Input() @Input()
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@ -40,8 +40,8 @@ export class SchemaScriptsFormComponent implements OnInit {
this.editForm.load(this.schema.scripts); this.editForm.load(this.schema.scripts);
} }
public complete() { public emitComplete() {
this.completed.emit(); this.complete.emit();
} }
public selectField(field: string) { public selectField(field: string) {
@ -54,7 +54,7 @@ export class SchemaScriptsFormComponent implements OnInit {
if (value) { if (value) {
this.schemasState.configureScripts(this.schema, value) this.schemasState.configureScripts(this.schema, value)
.subscribe(() => { .subscribe(() => {
this.complete(); this.emitComplete();
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);
}); });

4
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -1,5 +1,5 @@
<form [formGroup]="createForm.form" (ngSubmit)="createSchema()"> <form [formGroup]="createForm.form" (ngSubmit)="createSchema()">
<sqx-modal-dialog (closed)="cancel()" [large]="false"> <sqx-modal-dialog (close)="emitCancel()" [large]="false">
<ng-container title> <ng-container title>
<ng-container *ngIf="import; else noImport"> <ng-container *ngIf="import; else noImport">
Clone Schema Clone Schema
@ -84,7 +84,7 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitCancel()">Cancel</button>
<button type="submit" class="float-right btn btn-success">Create</button> <button type="submit" class="float-right btn btn-success">Create</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

14
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts

@ -23,10 +23,10 @@ import {
}) })
export class SchemaFormComponent implements OnInit { export class SchemaFormComponent implements OnInit {
@Output() @Output()
public created = new EventEmitter<SchemaDto>(); public complete = new EventEmitter<SchemaDto>();
@Output() @Output()
public cancelled = new EventEmitter(); public cancel = new EventEmitter();
@Input() @Input()
public import: any; public import: any;
@ -55,12 +55,12 @@ export class SchemaFormComponent implements OnInit {
return false; return false;
} }
public complete(schema: SchemaDto) { public emitComplete(value: SchemaDto) {
this.created.emit(schema); this.complete.emit(value);
} }
public cancel() { public emitCancel() {
this.cancelled.emit(); this.cancel.emit();
} }
public createSchema() { public createSchema() {
@ -71,7 +71,7 @@ export class SchemaFormComponent implements OnInit {
this.schemasState.create(schemaDto) this.schemasState.create(schemaDto)
.subscribe(dto => { .subscribe(dto => {
this.complete(dto); this.emitComplete(dto);
}, error => { }, error => {
this.createForm.submitFailed(error); this.createForm.submitFailed(error);
}); });

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

@ -25,7 +25,7 @@
[name]="category" [name]="category"
[schemas]="schemasState.schemas | async" [schemas]="schemasState.schemas | async"
[schemasFilter]="schemasFilter.valueChanges | async" [schemasFilter]="schemasFilter.valueChanges | async"
(removing)="removeCategory(category)"> (remove)="removeCategory(category)">
</sqx-schema-category> </sqx-schema-category>
<form [formGroup]="addCategoryForm.form" (ngSubmit)="addCategory()"> <form [formGroup]="addCategoryForm.form" (ngSubmit)="addCategory()">
@ -36,8 +36,8 @@
<ng-container *sqxModalView="addSchemaDialog;onRoot:true"> <ng-container *sqxModalView="addSchemaDialog;onRoot:true">
<sqx-schema-form [import]="import" <sqx-schema-form [import]="import"
(cancelled)="addSchemaDialog.hide()" (cancel)="addSchemaDialog.hide()"
(created)="redirectSchema($event)"> (create)="redirectSchema($event)">
</sqx-schema-form> </sqx-schema-form>
</ng-container> </ng-container>

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

@ -80,7 +80,7 @@
</div> </div>
</div> </div>
<sqx-modal-dialog *sqxModalView="connectDialog;onRoot:true" large="true" (closed)="connectDialog.hide()"> <sqx-modal-dialog *sqxModalView="connectDialog;onRoot:true" large="true" (close)="connectDialog.hide()">
<ng-container title> <ng-container title>
Connect Connect
</ng-container> </ng-container>

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

@ -91,6 +91,12 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TagEditorComponent extends StatefulControlComponent<State, any[]> implements AfterViewInit, OnInit { export class TagEditorComponent extends StatefulControlComponent<State, any[]> implements AfterViewInit, OnInit {
@ViewChild('form')
public formElement: ElementRef<HTMLElement>;
@ViewChild('input')
public inputElement: ElementRef<HTMLInputElement>;
@Input() @Input()
public converter: Converter = new StringConverter(); public converter: Converter = new StringConverter();
@ -118,11 +124,10 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
@Input() @Input()
public inputName = 'tag-editor'; public inputName = 'tag-editor';
@ViewChild('form') @Input()
public formElement: ElementRef<HTMLElement>; public set disabled(value: boolean) {
this.setDisabledState(value);
@ViewChild('input') }
public inputElement: ElementRef<HTMLInputElement>;
public addInput = new FormControl(); public addInput = new FormControl();

4
src/Squidex/app/framework/angular/modals/dialog-renderer.component.html

@ -1,6 +1,6 @@
<ng-content></ng-content> <ng-content></ng-content>
<sqx-modal-dialog *sqxModalView="dialogView;onRoot:true" showClose="false" (closed)="cancel()"> <sqx-modal-dialog *sqxModalView="dialogView;onRoot:true" showClose="false" (close)="cancel()">
<ng-container title> <ng-container title>
{{snapshot.dialogRequest?.title}} {{snapshot.dialogRequest?.title}}
</ng-container> </ng-container>
@ -17,7 +17,7 @@
<div class="notification-container notification-container-bottom-right"> <div class="notification-container notification-container-bottom-right">
<div class="alert alert-dismissible alert-{{notification.messageType}}" *ngFor="let notification of snapshot.notifications" (click)="close(notification)" @fade> <div class="alert alert-dismissible alert-{{notification.messageType}}" *ngFor="let notification of snapshot.notifications" (click)="close(notification)" @fade>
<button type="button" class="close" data-dismiss="alert" (closed)="close(notification)">&times;</button> <button type="button" class="close" data-dismiss="alert" (close)="close(notification)">&times;</button>
<span [innerHTML]="notification.message"></span> <span [innerHTML]="notification.message"></span>
</div> </div>

4
src/Squidex/app/framework/angular/modals/modal-dialog.component.html

@ -8,7 +8,7 @@
<ng-content select="[title]"></ng-content> <ng-content select="[title]"></ng-content>
</h4> </h4>
<button type="button" class="close" (click)="closed.emit()"> <button type="button" class="close" (click)="close()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
@ -30,4 +30,4 @@
</div> </div>
</div> </div>
<sqx-shortcut keys="escape" (trigger)="closed.emit()"></sqx-shortcut> <sqx-shortcut keys="escape" (trigger)="emitClose()"></sqx-shortcut>

6
src/Squidex/app/framework/angular/modals/modal-dialog.component.ts

@ -49,7 +49,7 @@ export class ModalDialogComponent extends StatefulComponent<State> implements Af
public contentClass = ''; public contentClass = '';
@Output() @Output()
public closed = new EventEmitter(); public close = new EventEmitter();
@ViewChild('tabsElement') @ViewChild('tabsElement')
public tabsElement: ElementRef<ParentNode>; public tabsElement: ElementRef<ParentNode>;
@ -70,4 +70,8 @@ export class ModalDialogComponent extends StatefulComponent<State> implements Af
this.next(() => ({ hasTabs, hasFooter })); this.next(() => ({ hasTabs, hasFooter }));
} }
public emitClose() {
this.close.emit();
}
} }

4
src/Squidex/app/framework/angular/pager.component.html

@ -2,10 +2,10 @@
<div class="float-right pagination"> <div class="float-right pagination">
<span class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span> <span class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span>
<button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="prevPage.emit()"> <button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="emitPrev()">
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="nextPage.emit()"> <button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="emitNext()">
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</button> </button>
</div> </div>

8
src/Squidex/app/framework/angular/pager.component.ts

@ -27,4 +27,12 @@ export class PagerComponent {
@Input() @Input()
public hideWhenButtonsDisabled = false; public hideWhenButtonsDisabled = false;
public emitNext() {
this.nextPage.emit();
}
public emitPrev() {
this.prevPage.emit();
}
} }

6
src/Squidex/app/framework/angular/sorted.directive.ts

@ -21,8 +21,8 @@ export class SortedDirective implements OnDestroy, OnInit {
@Input('sqxSortModel') @Input('sqxSortModel')
public sortModel: any[]; public sortModel: any[];
@Output('sqxSorted') @Output('sqxSort')
public sorted = new EventEmitter<any[]>(); public sort = new EventEmitter<any[]>();
constructor( constructor(
private readonly elementRef: ElementRef private readonly elementRef: ElementRef
@ -49,7 +49,7 @@ export class SortedDirective implements OnDestroy, OnInit {
newModel.splice(event.oldIndex, 1); newModel.splice(event.oldIndex, 1);
newModel.splice(event.newIndex, 0, item); newModel.splice(event.newIndex, 0, item);
this.sorted.emit(newModel); this.sort.emit(newModel);
} }
}, },

14
src/Squidex/app/framework/state.ts

@ -98,7 +98,7 @@ export class Form<T extends AbstractControl> {
} }
export class Model { export class Model {
protected clone(update: ((v: any) => object) | object): any { protected clone(update: ((v: any) => object) | object, validOnly = false): any {
let values: object; let values: object;
if (Types.isFunction(update)) { if (Types.isFunction(update)) {
values = update(<any>this); values = update(<any>this);
@ -106,7 +106,17 @@ export class Model {
values = update; values = update;
} }
const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this, values); const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
for (let key in values) {
if (values.hasOwnProperty(key)) {
let value = values[key];
if (value || validOnly) {
clone[key] = value;
}
}
}
if (Types.isFunction(clone.onCloned)) { if (Types.isFunction(clone.onCloned)) {
clone.onCloned(); clone.onCloned();

4
src/Squidex/app/shared/components/app-form.component.html

@ -1,5 +1,5 @@
<form [formGroup]="createForm.form" (ngSubmit)="createApp()"> <form [formGroup]="createForm.form" (ngSubmit)="createApp()">
<sqx-modal-dialog (closed)="complete()"> <sqx-modal-dialog (close)="emitComplete()">
<ng-container title> <ng-container title>
<ng-container *ngIf="template; else noTemplate"> <ng-container *ngIf="template; else noTemplate">
Create {{template}} Sample Create {{template}} Sample
@ -33,7 +33,7 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()" [disabled]="createForm.submitted | async">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()" [disabled]="createForm.submitted | async">Cancel</button>
<button type="submit" class="float-right btn btn-success">Create</button> <button type="submit" class="float-right btn btn-success">Create</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

8
src/Squidex/app/shared/components/app-form.component.ts

@ -23,7 +23,7 @@ import {
}) })
export class AppFormComponent { export class AppFormComponent {
@Output() @Output()
public completed = new EventEmitter(); public complete = new EventEmitter();
@Input() @Input()
public template = ''; public template = '';
@ -37,8 +37,8 @@ export class AppFormComponent {
) { ) {
} }
public complete() { public emitComplete() {
this.completed.emit(); this.complete.emit();
} }
public createApp() { public createApp() {
@ -49,7 +49,7 @@ export class AppFormComponent {
this.appsStore.create(request) this.appsStore.create(request)
.subscribe(() => { .subscribe(() => {
this.complete(); this.emitComplete();
}, error => { }, error => {
this.createForm.submitFailed(error); this.createForm.submitFailed(error);
}); });

40
src/Squidex/app/shared/components/asset-dialog.component.html

@ -0,0 +1,40 @@
<form [formGroup]="annotateForm.form" (ngSubmit)="annotateAsset()">
<sqx-modal-dialog (close)="emitCancel()">
<ng-container title>
Update Asset
</ng-container>
<ng-container content>
<sqx-form-error [error]="annotateForm.error | async"></sqx-form-error>
<div class="form-group">
<label for="fileName">Name</label>
<sqx-control-errors for="fileName" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="fileName" formControlName="fileName" />
</div>
<div class="form-group">
<label for="slug">Slug</label>
<sqx-control-errors for="slug" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="slug" formControlName="slug" sqxTransformInput="Slugify" />
</div>
<div class="form-group">
<label for="tags">Tags</label>
<sqx-control-errors for="tags" [submitted]="annotateForm.submitted | async"></sqx-control-errors>
<sqx-tag-editor [suggestions]="allTags" [allowDuplicates]="false" [undefinedWhenEmpty]="false" formControlName="tags"></sqx-tag-editor>
</div>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="emitCancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button>
</ng-container>
</sqx-modal-dialog>
</form>

2
src/Squidex/app/shared/components/asset-dialog.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

78
src/Squidex/app/shared/components/asset-dialog.component.ts

@ -0,0 +1,78 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import {
AnnotateAssetForm,
AppsState,
AssetDto,
AssetsService,
AuthService,
StatefulComponent
} from '@app/shared/internal';
@Component({
selector: 'sqx-asset-dialog',
styleUrls: ['./asset-dialog.component.scss'],
templateUrl: './asset-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetDialogComponent extends StatefulComponent implements OnInit {
@Input()
public asset: AssetDto;
@Input()
public allTags: string[];
@Output()
public cancel = new EventEmitter();
@Output()
public complete = new EventEmitter<AssetDto>();
public annotateForm = new AnnotateAssetForm(this.formBuilder);
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly formBuilder: FormBuilder
) {
super(changeDetector, {
isRenaming: false,
isTagging: false,
progress: 0
});
}
public ngOnInit() {
this.annotateForm.load(this.asset);
}
public emitCancel() {
this.cancel.emit();
}
public emitComplete(asset: AssetDto) {
this.complete.emit(asset);
}
public annotateAsset() {
const value = this.annotateForm.submit(this.asset);
if (value) {
this.assetsService.putAsset(this.appsState.appName, this.asset.id, value, this.asset.version)
.subscribe(dto => {
this.emitComplete(this.asset.annnotate(value, this.authState.user!.token, dto.version));
}, error => {
this.annotateForm.submitFailed(error);
});
}
}
}

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

@ -1,5 +1,5 @@
<ng-container *ngIf="!isListView; else listTemplate"> <ng-container *ngIf="!isListView; else listTemplate">
<div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)" [noDrop]="true"> <div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="emitSelect()" (sqxFileDrop)="updateFile($event)" [noDrop]="true">
<div class="card-body"> <div class="card-body">
<div class="file-preview" *ngIf="asset && snapshot.progress === 0" @fade> <div class="file-preview" *ngIf="asset && snapshot.progress === 0" @fade>
<span class="file-type" *ngIf="asset.fileType"> <span class="file-type" *ngIf="asset.fileType">
@ -17,14 +17,16 @@
<div class="overlay-background"></div> <div class="overlay-background"></div>
<div class="overlay-menu"> <div class="overlay-menu">
<a class="file-download" [href]="asset | sqxAssetUrl" sqxExternalLink="noicon" (click)="$event.stopPropagation()"> <a class="file-edit ml-2" (click)="edit()" *ngIf="!isDisabled">
<i class="icon-pencil"></i>
</a>
<a class="file-download ml-2" [href]="asset | sqxAssetUrl" sqxExternalLink="noicon" (click)="$event.stopPropagation()">
<i class="icon-download"></i> <i class="icon-download"></i>
</a> </a>
<a class="file-delete ml-2" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode">
<a class="file-delete ml-2" (click)="deleting.emit(asset)" *ngIf="!isDisabled && !removeMode">
<i class="icon-delete"></i> <i class="icon-delete"></i>
</a> </a>
<a class="file-delete ml-2" (click)="removing.emit(asset)" *ngIf="removeMode"> <a class="file-delete ml-2" (click)="emitRemove()" *ngIf="removeMode">
<i class="icon-close"></i> <i class="icon-close"></i>
</a> </a>
</div> </div>
@ -53,28 +55,15 @@
<div class="drop-overlay-text">Drop to update</div> <div class="drop-overlay-text">Drop to update</div>
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer" (dblclick)="edit()">
<ng-container *ngIf="asset"> <ng-container *ngIf="asset">
<div> <div>
<div *ngIf="!snapshot.isRenaming" class="file-name editable" (dblclick)="renameStart()"> <div class="file-name editable" (click)="edit()">
{{asset.fileName}} {{asset.fileName}}
</div> </div>
<div *ngIf="snapshot.isRenaming">
<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" spellcheck="false" sqxFocusOnInit (blur)="renameCancel()" />
</form>
</div>
</div> </div>
<div class="file-tags tags"> <div class="file-tags tags">
<sqx-tag-editor <sqx-tag-editor [disabled]="true" [ngModel]="asset.tags" class="blank"></sqx-tag-editor>
[suggestions]="allTags"
[acceptEnter]="true"
[allowDuplicates]="false"
[undefinedWhenEmpty]="false"
[formControl]="tagInput" class="blank" placeholder="+Tag">
</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}}
@ -85,7 +74,7 @@
</ng-container> </ng-container>
<ng-template #listTemplate> <ng-template #listTemplate>
<div class="table-items-row" [class.selectable]="isSelectable" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)" [noDrop]="true"> <div class="table-items-row" [class.selectable]="isSelectable" (click)="emitSelect()" (sqxFileDrop)="updateFile($event)" [noDrop]="true">
<div class="left-border" [class.hidden]="!isSelectable" [class.selected]="isSelected" ></div> <div class="left-border" [class.hidden]="!isSelectable" [class.selected]="isSelected" ></div>
<div *ngIf="asset && asset.canPreview && snapshot.progress === 0" class="image drag-handle" [class.image-left]="!isSelectable" @fade> <div *ngIf="asset && asset.canPreview && snapshot.progress === 0" class="image drag-handle" [class.image-left]="!isSelectable" @fade>
@ -98,16 +87,9 @@
<table class="table-fixed" *ngIf="asset && snapshot.progress === 0" @fade> <table class="table-fixed" *ngIf="asset && snapshot.progress === 0" @fade>
<tr> <tr>
<td class="col-name"> <td class="col-name">
<div *ngIf="!snapshot.isRenaming" class="file-name editable" (dblclick)="renameStart()"> <div class="file-name editable" (click)="edit()">
{{asset.fileName}} {{asset.fileName}}
</div> </div>
<div *ngIf="snapshot.isRenaming">
<form [formGroup]="renameForm.form" (ngSubmit)="renameAsset()">
<sqx-control-errors for="name" [submitted]="renameForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control editable form-underlined" id="assetName" formControlName="name" autocomplete="off" spellcheck="false" sqxFocusOnInit (blur)="renameCancel()" />
</form>
</div>
</td> </td>
<td class="col-info" *ngIf="!isCompact"> <td class="col-info" *ngIf="!isCompact">
<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}}
@ -121,10 +103,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)="deleting.emit(asset)" *ngIf="!isDisabled && !removeMode"> <button type="button" class="btn btn-text-danger" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" (click)="removing.emit(asset)" *ngIf="removeMode"> <button type="button" class="btn btn-text-secondary" (click)="emitRemove()" *ngIf="removeMode">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</td> </td>
@ -140,4 +122,12 @@
<div class="drop-overlay-text">Drop to update</div> <div class="drop-overlay-text">Drop to update</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>
<ng-container *ngIf="asset">
<sqx-asset-dialog *sqxModalView="editDialog;onRoot:true"
[asset]="asset" [allTags]="allTags"
(cancel)="cancelEdit()"
(complete)="updateAsset($event, true)">
</sqx-asset-dialog>
</ng-container>

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

@ -5,9 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -15,20 +13,15 @@ import {
AssetsService, AssetsService,
AuthService, AuthService,
DateTime, DateTime,
DialogModel,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
RenameAssetDto,
RenameAssetForm,
StatefulComponent, StatefulComponent,
TagAssetDto,
Types, Types,
Versioned Versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
interface State { interface State {
isTagging: boolean;
isRenaming: boolean;
progress: number; progress: number;
} }
@ -41,7 +34,7 @@ interface State {
], ],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetComponent extends StatefulComponent<State> implements OnChanges, OnInit { export class AssetComponent extends StatefulComponent<State> implements OnInit {
@Input() @Input()
public initFile: File; public initFile: File;
@ -70,37 +63,32 @@ export class AssetComponent extends StatefulComponent<State> implements OnChange
public allTags: string[]; public allTags: string[];
@Output() @Output()
public loaded = new EventEmitter<AssetDto>(); public load = new EventEmitter<AssetDto>();
@Output() @Output()
public removing = new EventEmitter<AssetDto>(); public loadError = new EventEmitter();
@Output() @Output()
public updated = new EventEmitter<AssetDto>(); public remove = new EventEmitter<AssetDto>();
@Output() @Output()
public deleting = new EventEmitter<AssetDto>(); public update = new EventEmitter<AssetDto>();
@Output() @Output()
public selected = new EventEmitter<AssetDto>(); public delete = new EventEmitter<AssetDto>();
@Output() @Output()
public failed = new EventEmitter(); public select = new EventEmitter<AssetDto>();
public renameForm = new RenameAssetForm(this.formBuilder);
public tagInput = new FormControl(); public editDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly authState: AuthService, private readonly authState: AuthService,
private readonly dialogs: DialogService, private readonly dialogs: DialogService
private readonly formBuilder: FormBuilder
) { ) {
super(changeDetector, { super(changeDetector, {
isRenaming: false,
isTagging: false,
progress: 0 progress: 0
}); });
} }
@ -114,30 +102,16 @@ export class AssetComponent extends StatefulComponent<State> implements OnChange
this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now()) this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now())
.subscribe(dto => { .subscribe(dto => {
if (Types.is(dto, AssetDto)) { if (Types.is(dto, AssetDto)) {
this.emitLoaded(dto); this.emitLoad(dto);
} else { } else {
this.setProgress(dto); this.setProgress(dto);
} }
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
this.emitFailed(error); this.emitLoadError(error);
}); });
} }
this.own(
this.tagInput.valueChanges.pipe(
distinctUntilChanged(),
debounceTime(2000)
).subscribe(tags => {
this.tagAsset(tags);
}));
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['asset'] && this.asset) {
this.tagInput.setValue(this.asset.tags, { emitEvent: false });
}
} }
public updateFile(files: FileList) { public updateFile(files: FileList) {
@ -159,77 +133,53 @@ export class AssetComponent extends StatefulComponent<State> implements OnChange
} }
} }
public renameAsset() { public edit() {
const value = this.renameForm.submit(this.asset); if (!this.isDisabled) {
this.editDialog.show();
if (value) {
const requestDto = new RenameAssetDto(value.name);
this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version)
.subscribe(dto => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true);
}, error => {
this.dialogs.notifyError(error);
this.renameForm.submitFailed(error);
});
} }
} }
public tagAsset(tags: string[]) { public cancelEdit() {
if (tags) { this.editDialog.hide();
const requestDto = new TagAssetDto(tags);
this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version)
.subscribe(dto => {
this.updateAsset(this.asset.tag(tags, this.authState.user!.token, dto.version), true);
}, error => {
this.dialogs.notifyError(error);
});
}
} }
public renameStart() { public emitSelect() {
if (!this.isDisabled) { this.select.emit(this.asset);
this.renameForm.load(this.asset);
this.next(s => ({ ...s, isRenaming: true }));
}
} }
public renameCancel() { public emitDelete() {
this.renameForm.submitCompleted(); this.delete.emit(this.asset);
}
this.next(s => ({ ...s, isRenaming: false })); public emitLoad(asset: AssetDto) {
this.load.emit(asset);
} }
private emitFailed(error: any) { public emitLoadError(error: any) {
this.failed.emit(error); this.loadError.emit(error);
} }
private emitLoaded(asset: AssetDto) { public emitUpdate() {
this.loaded.emit(asset); this.update.emit(this.asset);
} }
private emitUpdated(asset: AssetDto) { public emitRemove() {
this.updated.emit(asset); this.remove.emit(this.asset);
} }
private setProgress(progress: number) { private setProgress(progress: number) {
this.next(s => ({ ...s, progress })); this.next(s => ({ ...s, progress }));
} }
private updateAsset(asset: AssetDto, emitEvent: boolean) { public updateAsset(asset: AssetDto, emitEvent: boolean) {
this.asset = asset; this.asset = asset;
this.tagInput.setValue(asset.tags, { emitEvent: false });
if (emitEvent) { if (emitEvent) {
this.emitUpdated(asset); this.emitUpdate();
} }
this.renameCancel();
this.next(s => ({ ...s, progress: 0 })); this.next(s => ({ ...s, progress: 0 }));
this.cancelEdit();
} }
} }

10
src/Squidex/app/shared/components/assets-list.component.html

@ -17,8 +17,8 @@
<div class="row assets" [class.unrow]="isListView" *ngIf="state.tagsNames | async; let tags" (paste)="addFiles($event)"> <div class="row assets" [class.unrow]="isListView" *ngIf="state.tagsNames | async; let tags" (paste)="addFiles($event)">
<sqx-asset *ngFor="let file of newFiles" [initFile]="file" <sqx-asset *ngFor="let file of newFiles" [initFile]="file"
[isListView]="isListView" [isListView]="isListView"
(failed)="remove(file)" (loadError)="remove(file)"
(loaded)="add(file, $event)"> (load)="add(file, $event)">
</sqx-asset> </sqx-asset>
<ng-container *ngIf="state.assets | async; let assets"> <ng-container *ngIf="state.assets | async; let assets">
@ -28,9 +28,9 @@
[isSelectable]="selectedIds" [isSelectable]="selectedIds"
[isSelected]="isSelected(asset)" [isSelected]="isSelected(asset)"
[allTags]="tags" [allTags]="tags"
(updated)="update($event)" (update)="update($event)"
(selected)="select($event)" (select)="emitSelect($event)"
(deleting)="delete($event)"> (delete)="delete($event)">
</sqx-asset> </sqx-asset>
</ng-container> </ng-container>
</div> </div>

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

@ -36,7 +36,7 @@ export class AssetsListComponent {
public selectedIds: object; public selectedIds: object;
@Output() @Output()
public selected = new EventEmitter<AssetDto>(); public select = new EventEmitter<AssetDto>();
public add(file: File, asset: AssetDto) { public add(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file); this.newFiles = this.newFiles.remove(file);
@ -64,8 +64,8 @@ export class AssetsListComponent {
this.state.update(asset); this.state.update(asset);
} }
public select(asset: AssetDto) { public emitSelect(asset: AssetDto) {
this.selected.emit(asset); this.select.emit(asset);
} }
public isSelected(asset: AssetDto) { public isSelected(asset: AssetDto) {

10
src/Squidex/app/shared/components/assets-selector.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog (closed)="complete()" large="true" fullHeight="true"> <sqx-modal-dialog (close)="emitComplete()" large="true" fullHeight="true">
<ng-container title> <ng-container title>
Select assets Select assets
</ng-container> </ng-container>
@ -22,7 +22,7 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize" <sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize"
(queryChanged)="search($event)" (queryChange)="search($event)"
[query]="assetsState.assetsQuery | async" [query]="assetsState.assetsQuery | async"
enableShortcut="true"> enableShortcut="true">
</sqx-search-form> </sqx-search-form>
@ -45,14 +45,14 @@
<ng-container content> <ng-container content>
<sqx-assets-list <sqx-assets-list
[isListView]="snapshot.isListView" [isListView]="snapshot.isListView"
(selected)="selectAsset($event)" (select)="selectAsset($event)"
[selectedIds]="snapshot.selectedAssets" [selectedIds]="snapshot.selectedAssets"
[state]="assetsState" isDisabled="true"> [state]="assetsState" isDisabled="true">
</sqx-assets-list> </sqx-assets-list>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button>
<button type="submit" class="float-right btn btn-success" (click)="select()" [disabled]="snapshot.selectionCount === 0">Link selected assets ({{snapshot.selectionCount}})</button> <button type="submit" class="float-right btn btn-success" (click)="emitSelect()" [disabled]="snapshot.selectionCount === 0">Link selected assets ({{snapshot.selectionCount}})</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

10
src/Squidex/app/shared/components/assets-selector.component.ts

@ -34,7 +34,7 @@ interface State {
}) })
export class AssetsSelectorComponent extends StatefulComponent<State> implements OnInit { export class AssetsSelectorComponent extends StatefulComponent<State> implements OnInit {
@Output() @Output()
public selected = new EventEmitter<AssetDto[]>(); public select = new EventEmitter<AssetDto[]>();
constructor(changeDector: ChangeDetectorRef, constructor(changeDector: ChangeDetectorRef,
public readonly assetsState: AssetsDialogState, public readonly assetsState: AssetsDialogState,
@ -59,12 +59,12 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
this.assetsState.search(query).pipe(onErrorResumeNext()).subscribe(); this.assetsState.search(query).pipe(onErrorResumeNext()).subscribe();
} }
public complete() { public emitComplete() {
this.selected.emit([]); this.select.emit([]);
} }
public select() { public emitSelect() {
this.selected.emit(Object.values(this.snapshot.selectedAssets)); this.select.emit(Object.values(this.snapshot.selectedAssets));
} }
public selectTags(tags: string[]) { public selectTags(tags: string[]) {

2
src/Squidex/app/shared/components/comment.component.html

@ -7,7 +7,7 @@
<div class="user-row"> <div class="user-row">
<div class="user-ref">{{comment.user | sqxUserNameRef}}</div> <div class="user-ref">{{comment.user | sqxUserNameRef}}</div>
<button *ngIf="comment.user === userId" type="button" class="btn btn-sm btn-text-danger item-remove" (click)="deleting.emit()!"> <button *ngIf="comment.user === userId" type="button" class="btn btn-sm btn-text-danger item-remove" (click)="emitDelete()!">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

8
src/Squidex/app/shared/components/comment.component.ts

@ -26,13 +26,17 @@ export class CommentComponent {
public userId: string; public userId: string;
@Output() @Output()
public deleting = new EventEmitter(); public delete = new EventEmitter();
@Output() @Output()
public updated = new EventEmitter<string>(); public update = new EventEmitter<string>();
constructor( constructor(
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
} }
public emitDelete() {
this.delete.emit();
}
} }

4
src/Squidex/app/shared/components/comments.component.html

@ -8,8 +8,8 @@
<sqx-comment *ngFor="let comment of state.comments | async; trackBy: trackByComment" <sqx-comment *ngFor="let comment of state.comments | async; trackBy: trackByComment"
[comment]="comment" [comment]="comment"
[userId]="userId" [userId]="userId"
(updated)="update(comment, $event)" (update)="update(comment, $event)"
(deleting)="delete(comment)"> (delete)="delete(comment)">
</sqx-comment> </sqx-comment>
</div> </div>

2
src/Squidex/app/shared/components/markdown-editor.component.html

@ -6,6 +6,6 @@
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">
<sqx-assets-selector <sqx-assets-selector
(selected)="insertAssets($event)"> (select)="insertAssets($event)">
</sqx-assets-selector> </sqx-assets-selector>
</ng-container> </ng-container>

2
src/Squidex/app/shared/components/rich-editor.component.html

@ -4,6 +4,6 @@
<ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="assetsDialog;onRoot:true;closeAuto:false">
<sqx-assets-selector <sqx-assets-selector
(selected)="insertAssets($event)"> (select)="insertAssets($event)">
</sqx-assets-selector> </sqx-assets-selector>
</ng-container> </ng-container>

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

@ -52,7 +52,7 @@ export class RichEditorComponent extends StatefulControlComponent<any, string> i
public editor: ElementRef; public editor: ElementRef;
@Output() @Output()
public assetPluginClicked = new EventEmitter<any>(); public assetPluginClick = new EventEmitter<any>();
public assetsDialog = new DialogModel(); public assetsDialog = new DialogModel();

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

@ -8,7 +8,7 @@
<h3>{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})</h3> <h3>{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})</h3>
<button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="snapshot.schemasForCategory.length === 0 && !isReadonly" (click)="removing.emit()"> <button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="snapshot.schemasForCategory.length === 0 && !isReadonly" (click)="emitRemove()">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

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

@ -38,9 +38,6 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges { export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges {
@Output()
public removing = new EventEmitter();
@Input() @Input()
public name: string; public name: string;
@ -56,6 +53,9 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
@Input() @Input()
public schemas: ImmutableArray<SchemaDto>; public schemas: ImmutableArray<SchemaDto>;
@Output()
public remove = new EventEmitter();
public allowDrop = (schema: any) => { public allowDrop = (schema: any) => {
return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !this.isSameCategory(schema); return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !this.isSameCategory(schema);
} }
@ -130,6 +130,10 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe(); this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe();
} }
public emitRemove() {
this.remove.emit();
}
public trackBySchema(index: number, schema: SchemaDto) { public trackBySchema(index: number, schema: SchemaDto) {
return schema.id; return schema.id;
} }

2
src/Squidex/app/shared/components/search-form.component.html

@ -77,7 +77,7 @@
<ng-container *sqxModalView="saveQueryDialog;onRoot:true"> <ng-container *sqxModalView="saveQueryDialog;onRoot:true">
<form [formGroup]="saveQueryForm.form" (ngSubmit)="saveQueryComplete()"> <form [formGroup]="saveQueryForm.form" (ngSubmit)="saveQueryComplete()">
<sqx-modal-dialog (closed)="saveQueryDialog.hide()"> <sqx-modal-dialog (close)="saveQueryDialog.hide()">
<ng-container title> <ng-container title>
Name your query Name your query
</ng-container> </ng-container>

8
src/Squidex/app/shared/components/search-form.component.ts

@ -40,13 +40,13 @@ export class SearchFormComponent implements OnChanges, OnInit {
public query = ''; public query = '';
@Output() @Output()
public queryChanged = new EventEmitter<string>(); public queryChange = new EventEmitter<string>();
@Input() @Input()
public archived = false; public archived = false;
@Output() @Output()
public archivedChanged = new EventEmitter<boolean>(); public archivedChange = new EventEmitter<boolean>();
@Input() @Input()
public schemaName = ''; public schemaName = '';
@ -113,7 +113,7 @@ export class SearchFormComponent implements OnChanges, OnInit {
public search() { public search() {
this.invalidate(this.contentsFilter.value); this.invalidate(this.contentsFilter.value);
this.queryChanged.emit(this.contentsFilter.value); this.queryChange.emit(this.contentsFilter.value);
} }
private invalidate(query: string) { private invalidate(query: string) {
@ -186,7 +186,7 @@ export class SearchFormComponent implements OnChanges, OnInit {
} }
if (query !== this.query) { if (query !== this.query) {
this.queryChanged.emit(query); this.queryChange.emit(query);
} }
this.contentsFilter.setValue(query); this.contentsFilter.setValue(query);

1
src/Squidex/app/shared/declarations.ts

@ -6,6 +6,7 @@
*/ */
export * from './components/app-form.component'; export * from './components/app-form.component';
export * from './components/asset-dialog.component';
export * from './components/asset.component'; export * from './components/asset.component';
export * from './components/assets-list.component'; export * from './components/assets-list.component';
export * from './components/assets-selector.component'; export * from './components/assets-selector.component';

3
src/Squidex/app/shared/module.ts

@ -23,6 +23,7 @@ import {
AppsService, AppsService,
AppsState, AppsState,
AssetComponent, AssetComponent,
AssetDialogComponent,
AssetPreviewUrlPipe, AssetPreviewUrlPipe,
AssetsDialogState, AssetsDialogState,
AssetsListComponent, AssetsListComponent,
@ -102,6 +103,7 @@ import {
declarations: [ declarations: [
AppFormComponent, AppFormComponent,
AssetComponent, AssetComponent,
AssetDialogComponent,
AssetPreviewUrlPipe, AssetPreviewUrlPipe,
AssetUrlPipe, AssetUrlPipe,
AssetsListComponent, AssetsListComponent,
@ -131,6 +133,7 @@ import {
exports: [ exports: [
AppFormComponent, AppFormComponent,
AssetComponent, AssetComponent,
AssetDialogComponent,
AssetPreviewUrlPipe, AssetPreviewUrlPipe,
AssetUrlPipe, AssetUrlPipe,
AssetsListComponent, AssetsListComponent,

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

@ -10,6 +10,7 @@ import { inject, TestBed } from '@angular/core/testing';
import { import {
AnalyticsService, AnalyticsService,
AnnotateAssetDto,
ApiUrlConfig, ApiUrlConfig,
AssetDto, AssetDto,
AssetReplacedDto, AssetReplacedDto,
@ -17,8 +18,6 @@ import {
AssetsService, AssetsService,
DateTime, DateTime,
ErrorDto, ErrorDto,
RenameAssetDto,
TagAssetDto,
Version, Version,
Versioned Versioned
} from './../'; } from './../';
@ -31,21 +30,15 @@ describe('AssetDto', () => {
const version = new Version('1'); const version = new Version('1');
const newVersion = new Version('2'); const newVersion = new Version('2');
it('should update name property and user info when renaming', () => { it('should update tag property and user info when annnoting', () => {
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, [], 'url', version); const update = new AnnotateAssetDto('NewName.png', null, null);
const asset_2 = asset_1.rename('new-name.png', modifier, newVersion, modified);
expect(asset_2.fileName).toEqual('new-name.png'); const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'name.png', [], 'url', version);
expect(asset_2.lastModified).toEqual(modified); const asset_2 = asset_1.annnotate(update, modifier, newVersion, modified);
expect(asset_2.lastModifiedBy).toEqual(modifier);
expect(asset_2.version).toEqual(newVersion);
});
it('should update tag property and user info when tagged', () => { expect(asset_2.fileName).toEqual('NewName.png');
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, [], 'url', version); expect(asset_2.tags).toEqual([]);
const asset_2 = asset_1.tag(['tag1', 'tag2'], modifier, newVersion, modified); expect(asset_2.slug).toEqual(asset_1.slug);
expect(asset_2.tags).toEqual(['tag1', 'tag2']);
expect(asset_2.lastModified).toEqual(modified); expect(asset_2.lastModified).toEqual(modified);
expect(asset_2.lastModifiedBy).toEqual(modifier); expect(asset_2.lastModifiedBy).toEqual(modifier);
expect(asset_2.version).toEqual(newVersion); expect(asset_2.version).toEqual(newVersion);
@ -54,7 +47,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, 'name.png', [], '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);
@ -139,7 +132,7 @@ describe('AssetsService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
fileName: 'my-asset1.png', fileName: 'My Asset1.png',
fileType: 'png', fileType: 'png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2000, fileVersion: 2000,
@ -147,6 +140,7 @@ describe('AssetsService', () => {
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
slug: 'my-asset1.png',
tags: undefined, tags: undefined,
version: 11 version: 11
}, },
@ -156,7 +150,7 @@ describe('AssetsService', () => {
createdBy: 'Created2', createdBy: 'Created2',
lastModified: '2017-10-12T10:10', lastModified: '2017-10-12T10:10',
lastModifiedBy: 'LastModifiedBy2', lastModifiedBy: 'LastModifiedBy2',
fileName: 'my-asset2.png', fileName: 'My Asset2.png',
fileType: 'png', fileType: 'png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2000, fileVersion: 2000,
@ -164,6 +158,7 @@ describe('AssetsService', () => {
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
slug: 'my-asset2.png',
tags: ['tag1', 'tag2'], tags: ['tag1', 'tag2'],
version: 22 version: 22
} }
@ -176,7 +171,7 @@ describe('AssetsService', () => {
'id1', 'Created1', 'LastModifiedBy1', 'id1', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
'my-asset1.png', 'My Asset1.png',
'png', 'png',
1024, 1024,
2000, 2000,
@ -184,13 +179,14 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
'my-asset1.png',
[], [],
'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',
DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2016-10-12T10:10'),
DateTime.parseISO_UTC('2017-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'),
'my-asset2.png', 'My Asset2.png',
'png', 'png',
1024, 1024,
2000, 2000,
@ -198,6 +194,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
'my-asset2.png',
['tag1', 'tag2'], ['tag1', 'tag2'],
'http://service/p/api/assets/id2', 'http://service/p/api/assets/id2',
new Version('22')) new Version('22'))
@ -224,7 +221,7 @@ describe('AssetsService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
fileName: 'my-asset1.png', fileName: 'My Asset1.png',
fileType: 'png', fileType: 'png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2000, fileVersion: 2000,
@ -232,6 +229,7 @@ describe('AssetsService', () => {
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
slug: 'my-asset1.png',
tags: ['tag1', 'tag2'] tags: ['tag1', 'tag2']
}, { }, {
headers: { headers: {
@ -244,7 +242,7 @@ describe('AssetsService', () => {
'id1', 'Created1', 'LastModifiedBy1', 'id1', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
'my-asset1.png', 'My Asset1.png',
'png', 'png',
1024, 1024,
2000, 2000,
@ -252,6 +250,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
'my-asset1.png',
['tag1', 'tag2'], ['tag1', 'tag2'],
'http://service/p/api/assets/id1', 'http://service/p/api/assets/id1',
new Version('2'))); new Version('2')));
@ -312,7 +311,7 @@ describe('AssetsService', () => {
req.flush({ req.flush({
id: 'id1', id: 'id1',
fileName: 'my-asset1.png', fileName: 'My Asset1.png',
fileType: 'png', fileType: 'png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2, fileVersion: 2,
@ -320,6 +319,7 @@ describe('AssetsService', () => {
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
slug: 'my-asset1.png',
tags: ['tag1', 'tag2'] tags: ['tag1', 'tag2']
}, { }, {
headers: { headers: {
@ -334,13 +334,14 @@ describe('AssetsService', () => {
user, user,
now, now,
now, now,
'my-asset1.png', 'My Asset1.png',
'png', 'png',
1024, 2, 1024, 2,
'image/png', 'image/png',
true, true,
1024, 1024,
2048, 2048,
'my-asset1.png',
['tag1', 'tag2'], ['tag1', 'tag2'],
'http://service/p/api/assets/id1', 'http://service/p/api/assets/id1',
new Version('2'))); new Version('2')));
@ -424,25 +425,10 @@ describe('AssetsService', () => {
expect(error!).toEqual(new ErrorDto(413, 'Asset is too big.')); expect(error!).toEqual(new ErrorDto(413, 'Asset is too big.'));
})); }));
it('should make put request to update asset', it('should make put request to annotate asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
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) => { inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
const dto = new TagAssetDto(['tag1', 'tag2']); const dto = new AnnotateAssetDto('My Asset.pdf', 'my-asset.pdf', ['tag1', 'tag2']);
assetsService.putAsset('my-app', '123', dto, version).subscribe(); assetsService.putAsset('my-app', '123', dto, version).subscribe();

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

@ -51,6 +51,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 slug: string,
public readonly tags: string[], public readonly tags: string[],
public readonly url: string, public readonly url: string,
public readonly version: Version public readonly version: Version
@ -58,8 +59,8 @@ export class AssetDto extends Model {
super(); super();
} }
public with(value: Partial<AssetDto>): AssetDto { public with(value: Partial<AssetDto>, validOnly = false): AssetDto {
return this.clone(value); return this.clone(value, validOnly);
} }
public update(update: AssetReplacedDto, user: string, version: Version, now?: DateTime): AssetDto { public update(update: AssetReplacedDto, user: string, version: Version, now?: DateTime): AssetDto {
@ -71,35 +72,21 @@ export class AssetDto extends Model {
}); });
} }
public tag(tags: string[], user: string, version: Version, now?: DateTime): AssetDto { public annnotate(update: AnnotateAssetDto, user: string, version: Version, now?: DateTime): AssetDto {
return this.with({ return this.with({
tags, ...<any>update,
lastModified: now || DateTime.now(), lastModified: now || DateTime.now(),
lastModifiedBy: user, lastModifiedBy: user,
version version
}); }, true);
}
public rename(fileName: string, user: string, version: Version, now?: DateTime): AssetDto {
return this.with({
fileName,
lastModified: now || DateTime.now(),
lastModifiedBy: user,
version
});
}
}
export class RenameAssetDto {
constructor(
public readonly fileName: string
) {
} }
} }
export class TagAssetDto { export class AnnotateAssetDto {
constructor( constructor(
public readonly tags: string[] public readonly fileName: string | null,
public readonly slug: string | null,
public readonly tags: string[] | null
) { ) {
} }
} }
@ -189,6 +176,7 @@ export class AssetsService {
item.isImage, item.isImage,
item.pixelWidth, item.pixelWidth,
item.pixelHeight, item.pixelHeight,
item.slug,
item.tags || [], item.tags || [],
assetUrl, assetUrl,
new Version(item.version.toString())); new Version(item.version.toString()));
@ -231,6 +219,7 @@ export class AssetsService {
response.isImage, response.isImage,
response.pixelWidth, response.pixelWidth,
response.pixelHeight, response.pixelHeight,
response.slug,
response.tags || [], response.tags || [],
assetUrl, assetUrl,
new Version(event.headers.get('etag')!)); new Version(event.headers.get('etag')!));
@ -278,6 +267,7 @@ export class AssetsService {
body.isImage, body.isImage,
body.pixelWidth, body.pixelWidth,
body.pixelHeight, body.pixelHeight,
body.slug,
body.tags || [], body.tags || [],
assetUrl, assetUrl,
response.version); response.version);
@ -340,7 +330,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: RenameAssetDto | TagAssetDto, version: Version): Observable<Versioned<any>> { public putAsset(appName: string, id: string, dto: AnnotateAssetDto, 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(

44
src/Squidex/app/shared/state/assets.forms.ts

@ -7,14 +7,24 @@
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form } from '@app/framework'; import { Form, Types } from '@app/framework';
import { AssetDto } from './../services/assets.service'; import { AssetDto } from './../services/assets.service';
export class RenameAssetForm extends Form<FormGroup> { export class AnnotateAssetForm extends Form<FormGroup> {
constructor(formBuilder: FormBuilder) { constructor(formBuilder: FormBuilder) {
super(formBuilder.group({ super(formBuilder.group({
name: ['', fileName: ['',
[
Validators.required
]
],
slug: ['',
[
Validators.required
]
],
tags: ['',
[ [
Validators.required Validators.required
] ]
@ -27,8 +37,25 @@ export class RenameAssetForm extends Form<FormGroup> {
if (asset) { if (asset) {
let index = asset.fileName.lastIndexOf('.'); let index = asset.fileName.lastIndexOf('.');
if (index > 0) { if (index > 0) {
result.name += asset.fileName.substr(index); result.fileName += asset.fileName.substr(index);
}
if (result.fileName === asset.fileName) {
delete result.fileName;
}
if (result.slug === asset.slug) {
delete result.slug;
}
if (Types.jsJsonEquals(result.tags, asset.tags)) {
delete result.tags;
}
if (Object.keys(result).length === 0) {
return null;
} }
} }
@ -36,13 +63,14 @@ export class RenameAssetForm extends Form<FormGroup> {
} }
public load(asset: AssetDto) { public load(asset: AssetDto) {
let name = asset.fileName; let fileName = asset.fileName;
let index = fileName.lastIndexOf('.');
let index = name.lastIndexOf('.');
if (index > 0) { if (index > 0) {
name = name.substr(0, index); fileName = fileName.substr(0, index);
} }
super.load({ name }); super.load({ fileName, slug: asset.slug, tags: asset.tags });
} }
} }

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, ['tag1', 'shared'], 'url1', version), new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, 'slug1', ['tag1', 'shared'], 'url1', version),
new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, ['tag2', 'shared'], 'url2', version) new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, 'slug2', ['tag2', 'shared'], 'url2', version)
]; ];
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
@ -81,7 +81,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, 'slug2', [], 'url3', version);
assetsState.add(newAsset); assetsState.add(newAsset);
@ -90,7 +90,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, ['new'], 'url3', version); const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'slug3', ['new'], 'url3', version);
assetsState.update(newAsset); assetsState.update(newAsset);

2
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -57,6 +57,6 @@
<ng-container *sqxModalView="addAppDialog;onRoot:true"> <ng-container *sqxModalView="addAppDialog;onRoot:true">
<sqx-app-form <sqx-app-form
(completed)="addAppDialog.hide()"> (complete)="addAppDialog.hide()">
</sqx-app-form> </sqx-app-form>
</ng-container> </ng-container>

2
src/Squidex/appsettings.json

@ -189,7 +189,7 @@
* *
* Supported: MongoDB, Development * Supported: MongoDB, Development
*/ */
"clustering": "MongoDb", "clustering": "Development",
/* /*
* The port is used to share messages between all cluster members. Must be accessible within your cluster or network. * The port is used to share messages between all cluster members. Must be accessible within your cluster or network.
*/ */

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
public string FileName { get; set; } public string FileName { get; set; }
public string FileNameSlug { get; set; } public string Slug { get; set; }
public long FileSize { get; set; } public long FileSize { get; set; }

2
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
new object[] { new AssetCreated(), EnrichedAssetEventType.Created }, new object[] { new AssetCreated(), EnrichedAssetEventType.Created },
new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated }, new object[] { new AssetUpdated(), EnrichedAssetEventType.Updated },
new object[] { new AssetRenamed(), EnrichedAssetEventType.Renamed }, new object[] { new AssetAnnotated(), EnrichedAssetEventType.Annotated },
new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted } new object[] { new AssetDeleted(), EnrichedAssetEventType.Deleted }
}; };

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

@ -76,7 +76,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
MimeType = file.MimeType, MimeType = file.MimeType,
PixelWidth = image.PixelWidth, PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight, PixelHeight = image.PixelHeight,
Tags = new HashSet<string>() Tags = new HashSet<string>(),
Slug = file.FileName.ToAssetSlug()
}) })
); );
} }
@ -109,9 +110,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
} }
[Fact] [Fact]
public async Task Rename_should_create_events() public async Task AnnotateName_should_create_events()
{ {
var command = new RenameAsset { FileName = "My New Image.png" }; var command = new AnnotateAsset { FileName = "My New Image.png" };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
@ -120,18 +121,36 @@ namespace Squidex.Domain.Apps.Entities.Assets
result.ShouldBeEquivalent(new EntitySavedResult(1)); result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Equal("My New Image.png", sut.Snapshot.FileName); Assert.Equal("My New Image.png", sut.Snapshot.FileName);
Assert.Equal("my-new-image.png", sut.Snapshot.FileNameSlug);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetRenamed { FileName = "My New Image.png" }) CreateAssetEvent(new AssetAnnotated { FileName = "My New Image.png" })
); );
} }
[Fact] [Fact]
public async Task Tag_should_create_events() public async Task AnnotateSlug_should_create_events()
{ {
var command = new TagAsset(); var command = new AnnotateAsset { Slug = "my-new-image.png" };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Equal("my-new-image.png", sut.Snapshot.Slug);
LastEvents
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { Slug = "my-new-image.png" })
);
}
[Fact]
public async Task AnnotateTag_should_create_events()
{
var command = new AnnotateAsset { Tags = new HashSet<string>() };
await ExecuteCreateAsync(); await ExecuteCreateAsync();
@ -141,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetTagged { Tags = new HashSet<string>() }) CreateAssetEvent(new AssetAnnotated { Tags = new HashSet<string>() })
); );
} }

29
tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs

@ -15,29 +15,38 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
public class GuardAssetTests public class GuardAssetTests
{ {
[Fact] [Fact]
public void CanRename_should_throw_exception_if_name_not_defined() public void CanAnnotate_should_throw_exception_if_nothing_defined()
{ {
var command = new RenameAsset(); var command = new AnnotateAsset();
ValidationAssert.Throws(() => GuardAsset.CanRename(command, "asset-name"), ValidationAssert.Throws(() => GuardAsset.CanAnnotate(command, "asset-name", "asset-slug"),
new ValidationError("Name is required.", "FileName")); new ValidationError("Either file name, slug or tags must be defined.", "FileName", "Slug", "Tags"));
} }
[Fact] [Fact]
public void CanRename_should_throw_exception_if_name_are_the_same() public void CanAnnotate_should_throw_exception_if_names_are_the_same()
{ {
var command = new RenameAsset { FileName = "asset-name" }; var command = new AnnotateAsset { FileName = "asset-name" };
ValidationAssert.Throws(() => GuardAsset.CanRename(command, "asset-name"), ValidationAssert.Throws(() => GuardAsset.CanAnnotate(command, "asset-name", "asset-slug"),
new ValidationError("Asset has already this name.", "FileName")); new ValidationError("Asset has already this name.", "FileName"));
} }
[Fact] [Fact]
public void CanRename_should_not_throw_exception_if_name_are_different() public void CanAnnotate_should_throw_exception_if_slugs_are_the_same()
{ {
var command = new RenameAsset { FileName = "new-name" }; var command = new AnnotateAsset { Slug = "asset-slug" };
GuardAsset.CanRename(command, "asset-name"); ValidationAssert.Throws(() => GuardAsset.CanAnnotate(command, "asset-name", "asset-slug"),
new ValidationError("Asset has already this slug.", "Slug"));
}
[Fact]
public void CanAnnotate_should_not_throw_exception_if_names_are_different()
{
var command = new AnnotateAsset { FileName = "new-name", Slug = "new-slug" };
GuardAsset.CanAnnotate(command, "asset-name", "asset-slug");
} }
[Fact] [Fact]

18
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -51,12 +51,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl sourceUrl
mimeType mimeType
fileName fileName
fileNameSlug
fileSize fileSize
fileVersion fileVersion
isImage isImage
pixelWidth pixelWidth
pixelHeight pixelHeight
slug
} }
}"; }";
@ -86,12 +86,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}", sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png", mimeType = "image/png",
fileName = "MyFile.png", fileName = "MyFile.png",
fileNameSlug = "myfile.png",
fileSize = 1024, fileSize = 1024,
fileVersion = 123, fileVersion = 123,
isImage = true, isImage = true,
pixelWidth = 800, pixelWidth = 800,
pixelHeight = 600 pixelHeight = 600,
slug = "myfile.png"
} }
} }
} }
@ -119,12 +119,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl sourceUrl
mimeType mimeType
fileName fileName
fileNameSlug
fileSize fileSize
fileVersion fileVersion
isImage isImage
pixelWidth pixelWidth
pixelHeight pixelHeight
slug
} }
} }
}"; }";
@ -158,12 +158,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}", sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png", mimeType = "image/png",
fileName = "MyFile.png", fileName = "MyFile.png",
fileNameSlug = "myfile.png",
fileSize = 1024, fileSize = 1024,
fileVersion = 123, fileVersion = 123,
isImage = true, isImage = true,
pixelWidth = 800, pixelWidth = 800,
pixelHeight = 600 pixelHeight = 600,
slug = "myfile.png"
} }
} }
} }
@ -193,12 +193,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl sourceUrl
mimeType mimeType
fileName fileName
fileNameSlug
fileSize fileSize
fileVersion fileVersion
isImage isImage
pixelWidth pixelWidth
pixelHeight pixelHeight
slug
}} }}
}}"; }}";
@ -224,12 +224,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}", sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png", mimeType = "image/png",
fileName = "MyFile.png", fileName = "MyFile.png",
fileNameSlug = "myfile.png",
fileSize = 1024, fileSize = 1024,
fileVersion = 123, fileVersion = 123,
isImage = true, isImage = true,
pixelWidth = 800, pixelWidth = 800,
pixelHeight = 600 pixelHeight = 600,
slug = "myfile.png"
} }
} }
}; };

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -170,7 +170,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
LastModified = now, LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
FileName = "MyFile.png", FileName = "MyFile.png",
FileNameSlug = "myfile.png", Slug = "myfile.png",
FileSize = 1024, FileSize = 1024,
FileVersion = 123, FileVersion = 123,
MimeType = "image/png", MimeType = "image/png",

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs

@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
public string FileName { get; set; } public string FileName { get; set; }
public string FileNameSlug { get; set; } public string Slug { get; set; }
public long FileSize { get; set; } public long FileSize { get; set; }

9
tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs

@ -355,6 +355,15 @@ namespace Squidex.Infrastructure.Queries
Assert.Equal(o, i); Assert.Equal(o, i);
} }
[Fact]
public void Should_parse_filter_with_full_text_numbers()
{
var i = Q("$search=\"33k\"");
var o = C("FullText: '33k'");
Assert.Equal(o, i);
}
[Fact] [Fact]
public void Should_parse_filter_with_full_text() public void Should_parse_filter_with_full_text()
{ {

2
tools/Migrate_01/Migrations/CreateAssetSlugs.cs

@ -27,7 +27,7 @@ namespace Migrate_01.Migrations
{ {
return stateForAssets.ReadAllAsync(async (state, version) => return stateForAssets.ReadAllAsync(async (state, version) =>
{ {
state.FileNameSlug = state.FileName.ToAssetSlug(); state.Slug = state.FileName.ToAssetSlug();
await stateForAssets.WriteAsync(state.Id, state, version, version); await stateForAssets.WriteAsync(state.Id, state, version, version);
}); });

13
src/Squidex.Domain.Apps.Events/Assets/AssetRenamed.cs → tools/Migrate_01/OldEvents/AssetRenamed.cs

@ -5,13 +5,22 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets namespace Migrate_01.OldEvents
{ {
[EventType(nameof(AssetRenamed))] [EventType(nameof(AssetRenamed))]
public sealed class AssetRenamed : AssetEvent [Obsolete]
public sealed class AssetRenamed : AssetEvent, IMigrated<IEvent>
{ {
public string FileName { get; set; } public string FileName { get; set; }
public IEvent Migrate()
{
return new AssetAnnotated { FileName = FileName };
}
} }
} }

27
tools/Migrate_01/OldEvents/AssetTagged.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Migrate_01.OldEvents
{
[EventType(nameof(AssetTagged))]
[Obsolete]
public sealed class AssetTagged : AssetEvent, IMigrated<IEvent>
{
public HashSet<string> Tags { get; set; }
public IEvent Migrate()
{
return new AssetAnnotated { Tags = Tags };
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save