Browse Source

New API structure

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
247bfe3f36
  1. 44
      src/Squidex.Infrastructure/Assets/AssetFile.cs
  2. 20
      src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
  3. 5
      src/Squidex.Infrastructure/Assets/IAssetStore.cs
  4. 5
      src/Squidex.Infrastructure/Assets/ImageInfo.cs
  5. 5
      src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs
  6. 2
      src/Squidex.Write/Apps/AppDomainObject.cs
  7. 9
      src/Squidex.Write/Apps/Commands/CreateApp.cs
  8. 33
      src/Squidex.Write/Assets/AssetCommandHandler.cs
  9. 29
      src/Squidex.Write/Assets/AssetDomainObject.cs
  10. 14
      src/Squidex.Write/Assets/Commands/CreateAsset.cs
  11. 12
      src/Squidex.Write/Assets/Commands/UpdateAsset.cs
  12. 27
      src/Squidex/Controllers/Api/Assets/AssetContentController.cs
  13. 131
      src/Squidex/Controllers/Api/Assets/AssetController.cs
  14. 12
      src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs
  15. 21
      src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs
  16. 1
      src/Squidex/Squidex.csproj
  17. 14
      src/Squidex/app/features/assets/pages/asset.component.html
  18. 41
      src/Squidex/app/features/assets/pages/asset.component.scss
  19. 51
      src/Squidex/app/features/assets/pages/asset.component.ts
  20. 2
      src/Squidex/app/shared/services/assets.service.ts
  21. BIN
      src/Squidex/wwwroot/images/asset_doc.png
  22. BIN
      src/Squidex/wwwroot/images/asset_docx.png
  23. BIN
      src/Squidex/wwwroot/images/asset_generic.png
  24. BIN
      src/Squidex/wwwroot/images/asset_pdf.png
  25. BIN
      src/Squidex/wwwroot/images/asset_ppt.png
  26. BIN
      src/Squidex/wwwroot/images/asset_pptx.png
  27. BIN
      src/Squidex/wwwroot/images/asset_video.png
  28. BIN
      src/Squidex/wwwroot/images/asset_xls.png
  29. BIN
      src/Squidex/wwwroot/images/asset_xlsx.png
  30. 6
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  31. 2
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs
  32. 36
      tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs
  33. 42
      tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs
  34. 2
      tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs
  35. 2
      tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

44
src/Squidex.Infrastructure/Assets/AssetFile.cs

@ -0,0 +1,44 @@
// ==========================================================================
// IAssetFile.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.IO;
namespace Squidex.Infrastructure.Assets
{
public sealed class AssetFile
{
private readonly Func<Stream> openAction;
public string FileName { get; }
public string MimeType { get; }
public long FileSize { get; }
public AssetFile(string fileName, string mimeType, long fileSize, Func<Stream> openAction)
{
Guard.NotNullOrEmpty(fileName, nameof(fileName));
Guard.NotNullOrEmpty(mimeType, nameof(mimeType));
Guard.NotNull(openAction, nameof(openAction));
Guard.GreaterThan(fileSize, 0, nameof(fileSize));
FileName = fileName;
FileSize = fileSize;
MimeType = mimeType;
this.openAction = openAction;
}
public Stream OpenRead()
{
return openAction();
}
}
}

20
src/Squidex.Infrastructure/Assets/FolderAssetStore.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
@ -49,9 +50,9 @@ namespace Squidex.Infrastructure.Assets
}
}
public Task<Stream> GetAssetAsync(string name)
public Task<Stream> GetAssetAsync(Guid id, long version, string suffix = null)
{
var file = GetFile(name);
var file = GetFile(id, version, suffix);
Stream stream = null;
@ -70,9 +71,9 @@ namespace Squidex.Infrastructure.Assets
return Task.FromResult(stream);
}
public async Task UploadAssetAsync(string name, Stream stream)
public async Task UploadAssetAsync(Guid id, long version, Stream stream, string suffix = null)
{
var file = GetFile(name);
var file = GetFile(id, version, suffix);
using (var fileStream = file.OpenWrite())
{
@ -80,13 +81,16 @@ namespace Squidex.Infrastructure.Assets
}
}
private FileInfo GetFile(string name)
private FileInfo GetFile(Guid id, long version, string suffix)
{
Guard.ValidFileName(name, nameof(name));
var path = Path.Combine(directory.FullName, $"{id}_{version}");
var file = new FileInfo(Path.Combine(directory.FullName, name));
if (!string.IsNullOrWhiteSpace(suffix))
{
path += "_" + suffix;
}
return file;
return new FileInfo(path);
}
}
}

5
src/Squidex.Infrastructure/Assets/IAssetStore.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
@ -13,8 +14,8 @@ namespace Squidex.Infrastructure.Assets
{
public interface IAssetStore
{
Task<Stream> GetAssetAsync(string name);
Task<Stream> GetAssetAsync(Guid id, long version, string suffix = null);
Task UploadAssetAsync(string name, Stream stream);
Task UploadAssetAsync(Guid id, long version, Stream stream, string suffix = null);
}
}

5
src/Squidex.Infrastructure/Assets/ImageInfo.cs

@ -3,7 +3,7 @@
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// All rights reserved.d
// ==========================================================================
namespace Squidex.Infrastructure.Assets
@ -16,6 +16,9 @@ namespace Squidex.Infrastructure.Assets
public ImageInfo(int pixelWidth, int pixelHeight)
{
Guard.GreaterThan(pixelWidth, 0, nameof(pixelWidth));
Guard.GreaterThan(pixelHeight, 0, nameof(pixelHeight));
PixelWidth = pixelWidth;
PixelHeight = pixelHeight;
}

5
src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs

@ -31,6 +31,11 @@ namespace Squidex.Read.MongoDb.Assets
return "Projections_Assets";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetEntity> collection)
{
return Collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.LastModified).Ascending(x => x.AppId).Ascending(x => x.FileName).Ascending(x => x.MimeType));
}
public async Task<IReadOnlyList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, string query = null, int take = 10, int skip = 0)
{
var filter = CreateFilter(appId, mimeTypes, query);

2
src/Squidex.Write/Apps/AppDomainObject.cs

@ -101,7 +101,7 @@ namespace Squidex.Write.Apps
ThrowIfCreated();
var appId = new NamedId<Guid>(command.AggregateId, command.Name);
var appId = new NamedId<Guid>(command.AppId, command.Name);
RaiseEvent(SimpleMapper.Map(command, new AppCreated { AppId = appId }));

9
src/Squidex.Write/Apps/Commands/CreateApp.cs

@ -17,11 +17,16 @@ namespace Squidex.Write.Apps.Commands
{
public string Name { get; set; }
public Guid AggregateId { get; set; }
public Guid AppId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return AppId; }
}
public CreateApp()
{
AggregateId = Guid.NewGuid();
AppId = Guid.NewGuid();
}
public void Validate(IList<ValidationError> errors)

33
src/Squidex.Write/Assets/AssetCommandHandler.cs

@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks;
@ -18,32 +19,52 @@ namespace Squidex.Write.Assets
public class AssetCommandHandler : ICommandHandler
{
private readonly IAggregateHandler handler;
private readonly IAssetStore assetStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public AssetCommandHandler(IAggregateHandler handler)
public AssetCommandHandler(
IAggregateHandler handler,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
this.handler = handler;
this.assetStore = assetStore;
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
protected async Task On(CreateAsset command, CommandContext context)
{
await handler.CreateAsync<AssetDomainObject>(context, c =>
await handler.CreateAsync<AssetDomainObject>(context, async c =>
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
c.Create(command);
await assetStore.UploadAssetAsync(c.Id, c.Version, command.File.OpenRead());
context.Succeed(EntityCreatedResult.Create(c.Id, c.Version));
});
}
protected async Task On(RenameAsset command, CommandContext context)
protected async Task On(UpdateAsset command, CommandContext context)
{
await handler.UpdateAsync<AssetDomainObject>(context, c => c.Rename(command));
await handler.UpdateAsync<AssetDomainObject>(context, async c =>
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
c.Update(command);
await assetStore.UploadAssetAsync(c.Id, c.Version, command.File.OpenRead());
});
}
protected async Task On(UpdateAsset command, CommandContext context)
protected Task On(RenameAsset command, CommandContext context)
{
await handler.UpdateAsync<AssetDomainObject>(context, c => c.Update(command));
return handler.UpdateAsync<AssetDomainObject>(context, c => c.Rename(command));
}
protected Task On(DeleteAsset command, CommandContext context)

29
src/Squidex.Write/Assets/AssetDomainObject.cs

@ -60,29 +60,48 @@ namespace Squidex.Write.Assets
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AssetCreated()));
var @event = SimpleMapper.Map(command, new AssetCreated
{
FileName = command.File.FileName,
FileSize = command.File.FileSize,
MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null
});
RaiseEvent(@event);
return this;
}
public AssetDomainObject Delete(DeleteAsset command)
public AssetDomainObject Update(UpdateAsset command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted()));
var @event = SimpleMapper.Map(command, new AssetUpdated
{
FileSize = command.File.FileSize,
MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null
});
RaiseEvent(@event);
return this;
}
public AssetDomainObject Update(UpdateAsset command)
public AssetDomainObject Delete(DeleteAsset command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetUpdated()));
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted()));
return this;
}

14
src/Squidex.Write/Assets/Commands/CreateAsset.cs

@ -6,20 +6,14 @@
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Write.Assets.Commands
{
public sealed class CreateAsset : AssetAggregateCommand
{
public string FileName { get; set; }
public string MimeType { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }
public int? PixelWidth { get; set; }
public AssetFile File { get; set; }
public int? PixelHeight { get; set; }
public ImageInfo ImageInfo { get; set; }
}
}

12
src/Squidex.Write/Assets/Commands/UpdateAsset.cs

@ -6,18 +6,14 @@
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Write.Assets.Commands
{
public class UpdateAsset : AssetAggregateCommand
{
public string MimeType { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }
public int? PixelWidth { get; set; }
public AssetFile File { get; set; }
public int? PixelHeight { get; set; }
public ImageInfo ImageInfo { get; set; }
}
}

27
src/Squidex/Controllers/Api/Assets/AssetContentController.cs

@ -20,12 +20,9 @@ using Squidex.Read.Assets.Repositories;
namespace Squidex.Controllers.Api.Assets
{
/// <summary>
/// Uploads and retrieves assets.
/// </summary>
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Assets")]
[SwaggerIgnore]
public class AssetContentController : ControllerBase
{
private readonly IAssetStore assetStorage;
@ -44,18 +41,6 @@ namespace Squidex.Controllers.Api.Assets
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
/// <summary>
/// Gets the content of the asset.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="mode">The resize mode.</param>
/// <param name="width">The target width of the image.</param>
/// <param name="height">The target width of the image.</param>
/// <returns>
/// 200 => Asset content returned.
/// 404 => App or Asset not found.
/// </returns>
[HttpGet]
[Route("assets/{id}/")]
public async Task<IActionResult> GetAssetContent(string app, Guid id, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null)
@ -71,13 +56,13 @@ namespace Squidex.Controllers.Api.Assets
if (asset.IsImage && (width.HasValue || height.HasValue))
{
var name = $"{asset.Id}_{asset.Version}_{width}_{height}_{mode}";
var suffix = $"{width}_{height}_{mode}";
content = await assetStorage.GetAssetAsync(name);
content = await assetStorage.GetAssetAsync(asset.Id, asset.Version, suffix);
if (content == null)
{
var fullSizeContent = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}");
var fullSizeContent = await assetStorage.GetAssetAsync(asset.Id, asset.Version);
if (fullSizeContent == null)
{
@ -86,14 +71,14 @@ namespace Squidex.Controllers.Api.Assets
content = await assetThumbnailGenerator.CreateThumbnailAsync(fullSizeContent, width, height, mode);
await assetStorage.UploadAssetAsync(name, content);
await assetStorage.UploadAssetAsync(asset.Id, asset.Version, content, suffix);
content.Position = 0;
}
}
else
{
content = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}");
content = await assetStorage.GetAssetAsync(asset.Id, asset.Version);
}
if (content == null)

131
src/Squidex/Controllers/Api/Assets/AssetController.cs

@ -8,7 +8,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
@ -26,8 +25,6 @@ using Squidex.Pipeline;
using Squidex.Read.Assets.Repositories;
using Squidex.Write.Assets.Commands;
#pragma warning disable 1573
namespace Squidex.Controllers.Api.Assets
{
/// <summary>
@ -39,28 +36,26 @@ namespace Squidex.Controllers.Api.Assets
[SwaggerTag("Assets")]
public class AssetController : ControllerBase
{
private readonly IAssetStore assetStorage;
private readonly IAssetRepository assetRepository;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly AssetConfig assetsConfig;
public AssetController(
ICommandBus commandBus,
IAssetStore assetStorage,
IAssetRepository assetRepository,
IAssetThumbnailGenerator assetThumbnailGenerator,
IOptions<AssetConfig> assetsConfig)
: base(commandBus)
{
this.assetStorage = assetStorage;
this.assetsConfig = assetsConfig.Value;
this.assetRepository = assetRepository;
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
/// <summary>
/// Get assets.
/// </summary>
/// <param name="skip">The number of assets to skip.</param>
/// <param name="take">The number of assets to take.</param>
/// <param name="query">The query to limit the files by name.</param>
/// <param name="mimeTypes">Comma separated list of mime types to get.</param>
/// <returns>
/// 200 => assets returned.
/// </returns>
@ -96,7 +91,8 @@ namespace Squidex.Controllers.Api.Assets
/// <summary>
/// Creates and uploads a new asset.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="app">The app where the asset is a part of.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// 201 => Asset created.
/// 404 => App not found.
@ -106,53 +102,104 @@ namespace Squidex.Controllers.Api.Assets
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PostAsset(string app, List<IFormFile> files)
public async Task<IActionResult> PostAsset(string app, List<IFormFile> file)
{
if (files.Count != 1)
{
var error = new ValidationError($"Can only upload one file, found ${files.Count}.");
var assetFile = GetAssetFile(file);
throw new ValidationException("Cannot create asset.", error);
}
var command = new CreateAsset { File = assetFile };
var context = await CommandBus.PublishAsync(command);
var file = files[0];
var result = context.Result<EntityCreatedResult<Guid>>();
var response = AssetDto.Create(command, result);
if (file.Length > assetsConfig.MaxSize)
{
var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}.");
return StatusCode(201, response);
}
throw new ValidationException("Cannot create asset.", error);
}
/// <summary>
/// Replaces the content of the asset with a newer version.
/// </summary>
/// <param name="app">The app where the asset is a part of.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// 201 => Asset updated.
/// 404 => App or Asset not found.
/// 400 => Asset exceeds the maximum size.
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/{id}/content")]
[ProducesResponseType(typeof(AssetDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PutAssetContent(string app, Guid id, List<IFormFile> file)
{
var assetFile = GetAssetFile(file);
await CommandBus.PublishAsync(new UpdateAsset { File = assetFile });
var fileContent = new MemoryStream();
return NoContent();
}
await file.OpenReadStream().CopyToAsync(fileContent);
/// <summary>
/// Updates the asset.
/// </summary>
/// <param name="app">The app where the asset is a part of.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param>
/// <returns>
/// 201 => Asset updated.
/// 404 => App or Asset not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/assets/{id}/content")]
[ProducesResponseType(typeof(AssetDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request)
{
var command = SimpleMapper.Map(request, new RenameAsset());
await CommandBus.PublishAsync(command);
fileContent.Position = 0;
return NoContent();
}
/// <summary>
/// Delete an asset.
/// </summary>
/// <param name="app">The app where the schema is a part of.</param>
/// <param name="id">The id of the asset to delete.</param>
/// <returns>
/// 204 => Asset has been deleted.
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/")]
public async Task<IActionResult> DeleteSchema(string app, Guid id)
{
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id });
var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(fileContent);
return NoContent();
}
var command = new CreateAsset
private AssetFile GetAssetFile(IReadOnlyList<IFormFile> file)
{
if (file.Count != 1)
{
AssetId = Guid.NewGuid(),
FileSize = file.Length,
FileName = file.FileName,
MimeType = file.ContentType,
IsImage = imageInfo != null,
PixelWidth = imageInfo?.PixelWidth,
PixelHeight = imageInfo?.PixelHeight
};
fileContent.Position = 0;
var error = new ValidationError($"Can only upload one file, found ${file.Count}.");
await assetStorage.UploadAssetAsync($"{command.AssetId}_0", fileContent);
throw new ValidationException("Cannot create asset.", error);
}
var context = await CommandBus.PublishAsync(command);
var formFile = file[0];
var result = context.Result<EntityCreatedResult<Guid>>();
var response = AssetDto.Create(command, result);
if (formFile.Length > assetsConfig.MaxSize)
{
var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}.");
return StatusCode(201, response);
throw new ValidationException("Cannot create asset.", error);
}
var assetFile = new AssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream);
return assetFile;
}
}
}

12
src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs

@ -93,12 +93,12 @@ namespace Squidex.Controllers.Api.Assets.Models
CreatedBy = command.Actor,
LastModified = now,
LastModifiedBy = command.Actor,
FileName = command.FileName,
FileSize = command.FileSize,
MimeType = command.MimeType,
IsImage = command.IsImage,
PixelWidth = command.PixelWidth,
PixelHeight = command.PixelHeight
FileName = command.File.FileName,
FileSize = command.File.FileSize,
MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight
};
return response;

21
src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs

@ -0,0 +1,21 @@
// ==========================================================================
// AssetUpdateDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Assets.Models
{
public class AssetUpdateDto
{
/// <summary>
/// The new name of the asset.
/// </summary>
[Required]
public string Name { get; set; }
}
}

1
src/Squidex/Squidex.csproj

@ -64,6 +64,7 @@
<PackageReference Include="ReportGenerator" Version="2.5.6" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.ValueTuple" Version="4.3.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.1' ">

14
src/Squidex/app/features/assets/pages/asset.component.html

@ -1,12 +1,15 @@
<div class="asset">
<div class="card">
<div class="card" (sqxFileDrop)="updateFile($event)">
<div class="card-block">
<div *ngIf="asset">
<div *ngIf="asset" [@fade]>
<div class="file-type">{{fileType}}</div>
<div *ngIf="asset.isImage" [@fade]>
<div *ngIf="asset.isImage">
<img [attr.src]="previewUrl | async" sqxHideInvalidImage>
</div>
<div *ngIf="!asset.isImage" class="file-icon-container">
<img [attr.src]="fileIcon" sqxHideInvalidImage>
</div>
</div>
</div>
<div class="card-footer">
@ -17,5 +20,10 @@
{{fileInfo}}
</div>
</div>
<div class="drop-overlay">
<div class="drop-overlay-background"></div>
<span class="drop-overlay-text">Drop to update</span>
</div>
</div>
</div>

41
src/Squidex/app/features/assets/pages/asset.component.scss

@ -10,11 +10,42 @@ $color-type-foreground: #fff;
padding-bottom: 1rem;
}
.drop-overlay {
& {
@include transition(opacity.4s ease);
@include absolute(0, 0, 0, 0);
@include opacity(0);
pointer-events: none;
}
&-text {
@include absolute(0, 0, 0, 0);
text-align: center;
line-height: 14rem;
font-size: 1.2rem;
font-weight: lighter;
color: $color-type-foreground;
}
&-background {
@include absolute(0, 0, 0, 0);
@include opacity(.7);
background: $color-type-background;
}
}
.card {
& {
position: relative;
height: $card-size;
}
&.drag {
.drop-overlay {
@include opacity(1);
}
}
&-block {
padding: .8rem .8rem 0;
position: relative;
@ -33,6 +64,13 @@ $color-type-foreground: #fff;
font-size: .8rem;
}
&-icon {
&-container {
background: $color-border;
height: 155px;
}
}
&-name {
@include truncate;
font-size: 1rem;
@ -47,7 +85,8 @@ $color-type-foreground: #fff;
background: $color-type-background;
border: 0;
padding: .1rem .3rem;
font-size: .8rem;
text-transform: uppercase;
font-size: .7rem;
font-weight: normal;
color: $color-type-foreground;
}

51
src/Squidex/app/features/assets/pages/asset.component.ts

@ -48,6 +48,16 @@ export class AssetComponent extends AppComponentBase implements OnInit {
return result;
}
public get fileIcon(): string {
let result = '';
if (this.asset != null) {
result = fileIcon(this.fileType);
}
return result;
}
public get fileName(): string {
let result = '';
@ -108,4 +118,45 @@ function fileSize(b: number) {
}
return (u ? b.toFixed(1) + ' ' : b) + ' kMGTPEZY'[u] + 'B';
}
const mimeMapping = {
'pdf': 'pdf',
'vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'vnd.openxmlformats-officedocument.wordprocessingml.template': 'docx',
'vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
'vnd.openxmlformats-officedocument.spreadsheetml.template': 'xlsx',
'vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
'vnd.openxmlformats-officedocument.presentationml.template': 'pptx',
'vnd.openxmlformats-officedocument.presentationml.slideshow': 'pptx',
'msword': 'doc',
'vnd.ms-word': 'doc',
'vnd.ms-word.document.macroEnabled.12': 'docx',
'vnd.ms-word.template.macroEnabled.12': 'docx',
'vnd.ms-excel': 'xls',
'vnd.ms-excel.sheet.macroEnabled.12': 'xlsx',
'vnd.ms-excel.template.macroEnabled.12': 'xlsx',
'vnd.ms-excel.addin.macroEnabled.12': 'xlsx',
'vnd.ms-excel.sheet.binary.macroEnabled.12': 'xlsx',
'vnd.ms-powerpoint': 'ppt',
'vnd.ms-powerpoint.addin.macroEnabled.12': 'pptx',
'vnd.ms-powerpoint.presentation.macroEnabled.12': 'pptx',
'vnd.ms-powerpoint.template.macroEnabled.12': 'pptx',
'vnd.ms-powerpoint.slideshow.macroEnabled.12': 'pptx'
};
function fileIcon(mimeType: string) {
const mimeParts = mimeType.split('/');
const mimePrefix = mimeParts[0].toLowerCase();
const mimeSuffix = mimeParts[1].toLowerCase();
let mimeIcon = 'generic';
if (mimePrefix === 'video') {
mimeIcon = 'video';
} else {
mimeIcon = mimeMapping[mimeSuffix] || 'generic';;
}
return `/images/asset_${mimeIcon}.png`;
}

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

@ -116,7 +116,7 @@ export class AssetsService {
'Authorization': `${this.authService.user.user.token_type} ${this.authService.user.user.access_token}`
});
content.append('files', file);
content.append('file', file);
this.http
.post(url, content, { headers })

BIN
src/Squidex/wwwroot/images/asset_doc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

BIN
src/Squidex/wwwroot/images/asset_docx.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

BIN
src/Squidex/wwwroot/images/asset_generic.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

BIN
src/Squidex/wwwroot/images/asset_pdf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/Squidex/wwwroot/images/asset_ppt.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/Squidex/wwwroot/images/asset_pptx.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/Squidex/wwwroot/images/asset_video.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

BIN
src/Squidex/wwwroot/images/asset_xls.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
src/Squidex/wwwroot/images/asset_xlsx.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

6
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -39,7 +39,7 @@ namespace Squidex.Write.Apps
public AppCommandHandlerTests()
{
app = new AppDomainObject(AppId, 0);
app = new AppDomainObject(AppId, -1);
sut = new AppCommandHandler(Handler, appRepository.Object, userRepository.Object, keyGenerator.Object);
}
@ -47,7 +47,7 @@ namespace Squidex.Write.Apps
[Fact]
public async Task Create_should_throw_if_a_name_with_same_name_already_exists()
{
var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId });
var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId });
appRepository.Setup(x => x.FindAppAsync(AppName))
.Returns(Task.FromResult(new Mock<IAppEntity>().Object))
@ -64,7 +64,7 @@ namespace Squidex.Write.Apps
[Fact]
public async Task Create_should_create_app_if_name_is_free()
{
var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId });
var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId });
appRepository.Setup(x => x.FindAppAsync(AppName))
.Returns(Task.FromResult<IAppEntity>(null))

2
tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

@ -56,7 +56,7 @@ namespace Squidex.Write.Apps
[Fact]
public void Create_should_specify_name_and_owner()
{
sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AggregateId = AppId }));
sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId }));
Assert.Equal(AppName, sut.Name);

36
tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs

@ -7,11 +7,15 @@
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Write.Assets.Commands;
using Squidex.Write.TestHelpers;
using Xunit;
using Moq;
using Squidex.Infrastructure.Tasks;
// ReSharper disable ConvertToConstant.Local
@ -19,24 +23,31 @@ namespace Squidex.Write.Assets
{
public class AssetCommandHandlerTests : HandlerTestBase<AssetDomainObject>
{
private readonly Mock<IAssetThumbnailGenerator> assetThumbnailGenerator = new Mock<IAssetThumbnailGenerator>();
private readonly Mock<IAssetStore> assetStore = new Mock<IAssetStore>();
private readonly AssetCommandHandler sut;
private readonly AssetDomainObject asset;
private readonly Guid assetId = Guid.NewGuid();
private readonly string fileName = "my-image.png";
private readonly string mimeType = "image/png";
private readonly long fileSize = 1024;
private readonly Stream stream = new MemoryStream();
private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly AssetFile file;
public AssetCommandHandlerTests()
{
asset = new AssetDomainObject(assetId, 0);
file = new AssetFile("my-image.png", "image/png", 1024, () => stream);
sut = new AssetCommandHandler(Handler);
asset = new AssetDomainObject(assetId, -1);
sut = new AssetCommandHandler(Handler, assetStore.Object, assetThumbnailGenerator.Object);
}
[Fact]
public async Task Create_should_create_asset()
{
var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, FileName = fileName, FileSize = fileSize, MimeType = mimeType });
assetStore.Setup(x => x.UploadAssetAsync(assetId, 0, stream, null)).Returns(TaskHelper.Done).Verifiable();
assetThumbnailGenerator.Setup(x => x.GetImageInfoAsync(stream)).Returns(Task.FromResult(image)).Verifiable();
var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file });
await TestCreate(asset, async _ =>
{
@ -44,19 +55,28 @@ namespace Squidex.Write.Assets
});
Assert.Equal(assetId, context.Result<EntityCreatedResult<Guid>>().IdOrValue);
assetStore.VerifyAll();
assetThumbnailGenerator.VerifyAll();
}
[Fact]
public async Task Update_should_update_domain_object()
{
assetStore.Setup(x => x.UploadAssetAsync(assetId, 1, stream, null)).Returns(TaskHelper.Done).Verifiable();
assetThumbnailGenerator.Setup(x => x.GetImageInfoAsync(stream)).Returns(Task.FromResult(image)).Verifiable();
CreateAsset();
var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, FileSize = fileSize, MimeType = mimeType });
var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file });
await TestUpdate(asset, async _ =>
{
await sut.HandleAsync(context);
});
assetStore.VerifyAll();
assetThumbnailGenerator.VerifyAll();
}
[Fact]
@ -87,7 +107,7 @@ namespace Squidex.Write.Assets
private void CreateAsset()
{
asset.Create(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType });
asset.Create(new CreateAsset { File = file });
}
}
}

42
tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs

@ -7,8 +7,10 @@
// ==========================================================================
using System;
using System.IO;
using Squidex.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS;
using Squidex.Write.Assets.Commands;
using Squidex.Write.TestHelpers;
@ -21,9 +23,8 @@ namespace Squidex.Write.Assets
public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject>
{
private readonly AssetDomainObject sut;
private readonly string fileName = "my-image.png";
private readonly string mimeType = "image/png";
private readonly long fileSize = 1024;
private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
public Guid AssetId { get; } = Guid.NewGuid();
@ -35,22 +36,30 @@ namespace Squidex.Write.Assets
[Fact]
public void Create_should_throw_if_created()
{
sut.Create(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType });
sut.Create(new CreateAsset { File = file });
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }));
sut.Create(CreateAssetCommand(new CreateAsset { File = file }));
});
}
[Fact]
public void Create_should_create_events()
{
sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }));
sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetCreated { FileName = fileName, FileSize = fileSize, MimeType = mimeType })
CreateAssetEvent(new AssetCreated
{
IsImage = true,
FileName = file.FileName,
FileSize = file.FileSize,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
);
}
@ -59,7 +68,7 @@ namespace Squidex.Write.Assets
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType }));
sut.Update(CreateAssetCommand(new UpdateAsset { File = file }));
});
}
@ -80,11 +89,18 @@ namespace Squidex.Write.Assets
{
CreateAsset();
sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType }));
sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetUpdated { FileSize = fileSize, MimeType = mimeType })
CreateAssetEvent(new AssetUpdated
{
IsImage = true,
FileSize = file.FileSize,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
);
}
@ -93,7 +109,7 @@ namespace Squidex.Write.Assets
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType }));
sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" }));
});
}
@ -127,7 +143,7 @@ namespace Squidex.Write.Assets
Assert.Throws<ValidationException>(() =>
{
sut.Rename(CreateAssetCommand(new RenameAsset { FileName = fileName }));
sut.Rename(CreateAssetCommand(new RenameAsset { FileName = file.FileName }));
});
}
@ -182,7 +198,7 @@ namespace Squidex.Write.Assets
private void CreateAsset()
{
sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }));
sut.Create(CreateAssetCommand(new CreateAsset { File = file }));
((IAggregate)sut).ClearUncommittedEvents();
}

2
tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

@ -43,7 +43,7 @@ namespace Squidex.Write.Contents
.AddOrUpdateField(new NumberField(1, "my-field",
new NumberFieldProperties { IsRequired = true }));
content = new ContentDomainObject(contentId, 0);
content = new ContentDomainObject(contentId, -1);
sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object);

2
tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Write.Schemas
public SchemaCommandHandlerTests()
{
schema = new SchemaDomainObject(SchemaId, 0, registry);
schema = new SchemaDomainObject(SchemaId, -1, registry);
sut = new SchemaCommandHandler(Handler, schemaProvider.Object);
}

Loading…
Cancel
Save