Browse Source

Moire small refactorings.

pull/95/head
Sebastian Stehle 9 years ago
parent
commit
40acddfa74
  1. 9
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs
  2. 31
      src/Squidex.Infrastructure/FileExtensions.cs
  3. 4
      src/Squidex/Controllers/Api/Assets/AssetsController.cs
  4. 6
      src/Squidex/Controllers/Api/Assets/Models/AssetCreatedDto.cs
  5. 6
      src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs
  6. 1
      src/Squidex/Controllers/Api/Assets/Models/AssetReplacedDto.cs
  7. 12
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  8. 2
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  9. 16
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  10. 18
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  11. 2
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  12. 8
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  13. 35
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  14. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  15. 8
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  16. 22
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  17. 8
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  18. 2
      src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
  19. 10
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  20. 4
      src/Squidex/app/features/settings/pages/languages/language.component.ts
  21. 2
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  22. 4
      src/Squidex/app/features/settings/pages/plans/plans-page.component.html
  23. 23
      src/Squidex/app/features/settings/pages/plans/plans-page.component.ts
  24. 30
      src/Squidex/app/framework/angular/numbers.pipes.spec.ts
  25. 50
      src/Squidex/app/framework/angular/numbers.pipes.ts
  26. 1
      src/Squidex/app/framework/declarations.ts
  27. 6
      src/Squidex/app/framework/module.ts
  28. 4
      src/Squidex/app/framework/services/message-bus.spec.ts
  29. 2
      src/Squidex/app/framework/services/message-bus.ts
  30. 8
      src/Squidex/app/shared/components/app-form.component.ts
  31. 20
      src/Squidex/app/shared/components/asset.component.html
  32. 37
      src/Squidex/app/shared/components/asset.component.ts
  33. 62
      src/Squidex/app/shared/components/pipes.ts
  34. 1
      src/Squidex/app/shared/declarations-base.ts
  35. 11
      src/Squidex/app/shared/module.ts
  36. 37
      src/Squidex/app/shared/services/assets.service.spec.ts
  37. 6
      src/Squidex/app/shared/services/assets.service.ts
  38. 75
      src/Squidex/app/shared/utils/file-helper.spec.ts
  39. 112
      src/Squidex/app/shared/utils/file-helper.ts
  40. 38
      tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs

9
src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs

@ -10,6 +10,7 @@ using System;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Read.Assets; using Squidex.Domain.Apps.Read.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
{ {
@ -99,6 +100,14 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
Description = "The file name." Description = "The file name."
}); });
AddField(new FieldType
{
Name = "fileType",
Resolver = Resolver(x => x.FileName.FileType()),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Description = "The file type."
});
AddField(new FieldType AddField(new FieldType
{ {
Name = "fileSize", Name = "fileSize",

31
src/Squidex.Infrastructure/FileExtensions.cs

@ -0,0 +1,31 @@
// ==========================================================================
// FileExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.IO;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure
{
public static class FileExtensions
{
public static string FileType(this string fileName)
{
try
{
var fileInfo = new FileInfo(fileName);
return fileInfo.Extension.Substring(1).ToLowerInvariant();
}
catch
{
return "blob";
}
}
}
}

4
src/Squidex/Controllers/Api/Assets/AssetsController.cs

@ -108,7 +108,7 @@ namespace Squidex.Controllers.Api.Assets
var response = new AssetsDto var response = new AssetsDto
{ {
Total = taskForCount.Result, Total = taskForCount.Result,
Items = taskForItems.Result.Select(x => SimpleMapper.Map(x, new AssetDto())).ToArray() Items = taskForItems.Result.Select(x => SimpleMapper.Map(x, new AssetDto { FileType = x.FileName.FileType() })).ToArray()
}; };
return Ok(response); return Ok(response);
@ -137,7 +137,7 @@ namespace Squidex.Controllers.Api.Assets
return NotFound(); return NotFound();
} }
var response = SimpleMapper.Map(entity, new AssetDto()); var response = SimpleMapper.Map(entity, new AssetDto { FileType = entity.FileName.FileType() });
Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); Response.Headers["ETag"] = new StringValues(entity.Version.ToString());

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

@ -20,6 +20,12 @@ namespace Squidex.Controllers.Api.Assets.Models
/// </summary> /// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// The file type.
/// </summary>
[Required]
public string FileType { get; set; }
/// <summary> /// <summary>
/// The file name. /// The file name.
/// </summary> /// </summary>

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

@ -31,6 +31,12 @@ namespace Squidex.Controllers.Api.Assets.Models
/// </summary> /// </summary>
[Required] [Required]
public string MimeType { get; set; } public string MimeType { get; set; }
/// <summary>
/// The file type.
/// </summary>
[Required]
public string FileType { get; set; }
/// <summary> /// <summary>
/// The size of the file in bytes. /// The size of the file in bytes.

1
src/Squidex/Controllers/Api/Assets/Models/AssetReplacedDto.cs

@ -8,6 +8,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Write.Assets.Commands; using Squidex.Domain.Apps.Write.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Controllers.Api.Assets.Models namespace Squidex.Controllers.Api.Assets.Models

12
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -79,7 +79,7 @@ export class UserPageComponent extends ComponentBase implements OnInit {
created.pictureUrl!, created.pictureUrl!,
false); false);
this.sendUserCreated(this.user); this.emitUserCreated(this.user);
this.notifyInfo('User created successfully.'); this.notifyInfo('User created successfully.');
this.back(); this.back();
}, error => { }, error => {
@ -93,7 +93,7 @@ export class UserPageComponent extends ComponentBase implements OnInit {
requestDto.email, requestDto.email,
requestDto.displayMessage); requestDto.displayMessage);
this.sendUserUpdated(this.user); this.emitUserUpdated(this.user);
this.notifyInfo('User saved successfully.'); this.notifyInfo('User saved successfully.');
this.resetUserForm(); this.resetUserForm();
}, error => { }, error => {
@ -107,12 +107,12 @@ export class UserPageComponent extends ComponentBase implements OnInit {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true }); this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
} }
private sendUserCreated(user: UserDto) { private emitUserCreated(user: UserDto) {
this.messageBus.publish(new UserCreated(user)); this.messageBus.emit(new UserCreated(user));
} }
private sendUserUpdated(user: UserDto) { private emitUserUpdated(user: UserDto) {
this.messageBus.publish(new UserUpdated(user)); this.messageBus.emit(new UserUpdated(user));
} }
private setupAndPopulateForm() { private setupAndPopulateForm() {

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

@ -102,7 +102,7 @@ export class AssetsPageComponent extends AppComponentBase implements OnDestroy,
} }
public onAssetUpdated(asset: AssetDto) { public onAssetUpdated(asset: AssetDto) {
this.messageBus.publish(new AssetUpdated(asset, this)); this.messageBus.emit(new AssetUpdated(asset, this));
} }
public onAssetFailed(file: File) { public onAssetFailed(file: File) {

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

@ -146,7 +146,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
.subscribe(dto => { .subscribe(dto => {
this.content = dto; this.content = dto;
this.sendContentCreated(this.content); this.emitContentCreated(this.content);
this.notifyInfo('Content created successfully.'); this.notifyInfo('Content created successfully.');
this.back(); this.back();
}, error => { }, error => {
@ -159,7 +159,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
.subscribe(() => { .subscribe(() => {
this.content = this.content.update(requestDto, this.authService.user.token); this.content = this.content.update(requestDto, this.authService.user.token);
this.sendContentUpdated(this.content); this.emitContentUpdated(this.content);
this.notifyInfo('Content saved successfully.'); this.notifyInfo('Content saved successfully.');
this.enableContentForm(); this.enableContentForm();
}, error => { }, error => {
@ -172,16 +172,16 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
} }
} }
private sendContentCreated(content: ContentDto) { private back() {
this.messageBus.publish(new ContentCreated(content)); this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
} }
private sendContentUpdated(content: ContentDto) { private emitContentCreated(content: ContentDto) {
this.messageBus.publish(new ContentUpdated(content)); this.messageBus.emit(new ContentCreated(content));
} }
private back() { private emitContentUpdated(content: ContentDto) {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true }); this.messageBus.emit(new ContentUpdated(content));
} }
private disableContentForm() { private disableContentForm() {

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

@ -105,6 +105,10 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.isReadOnly = routeData['isReadOnly']; this.isReadOnly = routeData['isReadOnly'];
} }
public dropData(content: ContentDto) {
return { content, schemaId: this.schema.id };
}
public search() { public search() {
this.contentsQuery = this.contentsFilter.value; this.contentsQuery = this.contentsFilter.value;
this.contentsPager = new Pager(0); this.contentsPager = new Pager(0);
@ -139,7 +143,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.contentItems = this.contentItems.removeAll(x => x.id === content.id); this.contentItems = this.contentItems.removeAll(x => x.id === content.id);
this.contentsPager = this.contentsPager.decrementCount(); this.contentsPager = this.contentsPager.decrementCount();
this.sendContentDeleted(content); this.emitContentDeleted(content);
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -164,10 +168,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.languageSelected = language; this.languageSelected = language;
} }
public dropData(content: ContentDto) {
return { content, schemaId: this.schema.id };
}
public goNext() { public goNext() {
this.contentsPager = this.contentsPager.goNext(); this.contentsPager = this.contentsPager.goNext();
@ -180,6 +180,10 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.load(); this.load();
} }
private emitContentDeleted(content: ContentDto) {
this.messageBus.emit(new ContentDeleted(content));
}
private resetContents() { private resetContents() {
this.contentItems = ImmutableArray.empty<ContentDto>(); this.contentItems = ImmutableArray.empty<ContentDto>();
this.contentsQuery = ''; this.contentsQuery = '';
@ -202,9 +206,5 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.columnWidth = 100; this.columnWidth = 100;
} }
} }
private sendContentDeleted(content: ContentDto) {
this.messageBus.publish(new ContentDeleted(content));
}
} }

2
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -129,7 +129,7 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
} }
public onAssetUpdated(asset: AssetDto) { public onAssetUpdated(asset: AssetDto) {
this.messageBus.publish(new AssetUpdated(asset, this)); this.messageBus.emit(new AssetUpdated(asset, this));
} }
public onAssetFailed(file: File) { public onAssetFailed(file: File) {

8
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -84,8 +84,8 @@
<div class="card-block"> <div class="card-block">
<div class="aggregation" *ngIf="callsCurrent"> <div class="aggregation" *ngIf="callsCurrent">
<div class="aggregation-label">API calls this month</div> <div class="aggregation-label">API calls this month</div>
<div class="aggregation-value">{{callsCurrent}}</div> <div class="aggregation-value">{{callsCurrent | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsMax">Monthly limit: {{callsMax}}</div> <div class="aggregation-label" *ngIf="callsMax > 0">Monthly limit: {{callsMax | sqxKNumber}}</div>
</div> </div>
</div> </div>
</div> </div>
@ -100,8 +100,8 @@
<div class="card-block"> <div class="card-block">
<div class="aggregation" *ngIf="assetsCurrent"> <div class="aggregation" *ngIf="assetsCurrent">
<div class="aggregation-label">Asset size today</div> <div class="aggregation-label">Asset size today</div>
<div class="aggregation-value">{{assetsCurrent}}</div> <div class="aggregation-value">{{assetsCurrent | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="callsMax">Total limit: {{assetsMax}}</div> <div class="aggregation-label" *ngIf="assetsMax > 0">Total limit: {{assetsMax | sqxFileSize}}</div>
</div> </div>
</div> </div>
</div> </div>

35
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -13,7 +13,6 @@ import {
AuthService, AuthService,
DateTime, DateTime,
fadeAnimation, fadeAnimation,
FileHelper,
NotificationService, NotificationService,
UsagesService UsagesService
} from 'shared'; } from 'shared';
@ -55,11 +54,11 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit {
maintainAspectRatio: false maintainAspectRatio: false
}; };
public assetsCurrent: string | null = null; public assetsCurrent = 0;
public assetsMax: string | null = null; public assetsMax = 0;
public callsCurrent: string | null = null; public callsCurrent = 0;
public callsMax: string | null = null; public callsMax = 0;
constructor(apps: AppsStoreService, notifications: NotificationService, constructor(apps: AppsStoreService, notifications: NotificationService,
private readonly authService: AuthService, private readonly authService: AuthService,
@ -72,15 +71,15 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit {
this.appName() this.appName()
.switchMap(app => this.usagesService.getTodayStorage(app)) .switchMap(app => this.usagesService.getTodayStorage(app))
.subscribe(dto => { .subscribe(dto => {
this.assetsCurrent = FileHelper.fileSize(dto.size); this.assetsCurrent = dto.size;
this.assetsMax = FileHelper.fileSize(dto.maxAllowed); this.assetsMax = dto.maxAllowed;
}); });
this.appName() this.appName()
.switchMap(app => this.usagesService.getMonthCalls(app)) .switchMap(app => this.usagesService.getMonthCalls(app))
.subscribe(dto => { .subscribe(dto => {
this.callsCurrent = formatCalls(dto.count); this.callsCurrent = dto.count;
this.callsMax = formatCalls(dto.maxAllowed); this.callsMax = dto.maxAllowed;
}); });
this.appName() this.appName()
@ -155,24 +154,6 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit {
} }
} }
function formatCalls(count: number): string | null {
if (count > 1000) {
count = count / 1000;
if (count < 10) {
count = Math.round(count * 10) / 10;
} else {
count = Math.round(count);
}
return count + 'k';
} else if (count < 0) {
return null;
} else {
return count.toString();
}
}
function createLabels(dtos: { date: DateTime }[]): string[] { function createLabels(dtos: { date: DateTime }[]): string[] {
return dtos.map(d => d.date.toStringFormat('M-DD')); return dtos.map(d => d.date.toStringFormat('M-DD'));
} }

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

@ -109,11 +109,11 @@ export class FieldComponent implements OnInit {
this.field.partitioning, this.field.partitioning,
properties); properties);
this.sendSaving(field); this.emitSaving(field);
} }
} }
private sendSaving(field: FieldDto) { private emitSaving(field: FieldDto) {
this.saving.emit(field); this.saving.emit(field);
} }

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

@ -65,7 +65,7 @@ export class SchemaEditFormComponent extends ComponentBase implements OnInit {
} }
public cancel() { public cancel() {
this.sendCancelled(); this.emitCancelled();
this.resetEditForm(); this.resetEditForm();
} }
@ -79,7 +79,7 @@ export class SchemaEditFormComponent extends ComponentBase implements OnInit {
this.schemas.putSchema(this.appName, this.name, requestDto, this.version) this.schemas.putSchema(this.appName, this.name, requestDto, this.version)
.subscribe(dto => { .subscribe(dto => {
this.sendSaved(requestDto); this.emitSaved(requestDto);
this.resetEditForm(); this.resetEditForm();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
@ -88,11 +88,11 @@ export class SchemaEditFormComponent extends ComponentBase implements OnInit {
} }
} }
private sendCancelled() { private emitCancelled() {
this.cancelled.emit(); this.cancelled.emit();
} }
private sendSaved(requestDto: any) { private emitSaved(requestDto: any) {
this.saved.emit(new SchemaPropertiesDto(requestDto.label, requestDto.hints)); this.saved.emit(new SchemaPropertiesDto(requestDto.label, requestDto.hints));
} }

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

@ -201,7 +201,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.deleteSchema(app, this.schema.name, this.schema.version)).retry(2) .switchMap(app => this.schemasService.deleteSchema(app, this.schema.name, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(() => {
this.sendSchemaDeleted(this.schema); this.emitSchemaDeleted(this.schema);
this.hideDeleteDialog(); this.hideDeleteDialog();
this.back(); this.back();
}, error => { }, error => {
@ -252,7 +252,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
private updateSchema(schema: SchemaDetailsDto) { private updateSchema(schema: SchemaDetailsDto) {
this.schema = schema; this.schema = schema;
this.sendSchemaUpdated(schema); this.emitSchemaUpdated(schema);
this.notify(); this.notify();
this.export(); this.export();
} }
@ -288,24 +288,24 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.schemaExport = result; this.schemaExport = result;
} }
private hideDeleteDialog() {
this.confirmDeleteDialog.hide();
}
private back() { private back() {
this.router.navigate(['../'], { relativeTo: this.route }); this.router.navigate(['../'], { relativeTo: this.route });
} }
private sendSchemaDeleted(schema: SchemaDto) { private emitSchemaDeleted(schema: SchemaDto) {
this.messageBus.publish(new SchemaDeleted(schema)); this.messageBus.emit(new SchemaDeleted(schema));
} }
private sendSchemaUpdated(schema: SchemaDto) { private emitSchemaUpdated(schema: SchemaDto) {
this.messageBus.publish(new SchemaUpdated(schema)); this.messageBus.emit(new SchemaUpdated(schema));
}
private hideDeleteDialog() {
this.confirmDeleteDialog.hide();
} }
private notify() { private notify() {
this.messageBus.publish(new HistoryChannelUpdated()); this.messageBus.emit(new HistoryChannelUpdated());
} }
} }

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

@ -72,7 +72,7 @@ export class SchemaFormComponent {
} }
public cancel() { public cancel() {
this.sendCancelled(); this.emitCancelled();
this.resetCreateForm(); this.resetCreateForm();
} }
@ -91,7 +91,7 @@ export class SchemaFormComponent {
this.schemas.postSchema(this.appName, requestDto, me, undefined, schemaVersion) this.schemas.postSchema(this.appName, requestDto, me, undefined, schemaVersion)
.subscribe(dto => { .subscribe(dto => {
this.sendCreated(dto); this.emitCreated(dto);
this.resetCreateForm(); this.resetCreateForm();
}, error => { }, error => {
this.enableCreateForm(error.displayMessage); this.enableCreateForm(error.displayMessage);
@ -99,11 +99,11 @@ export class SchemaFormComponent {
} }
} }
private sendCancelled() { private emitCancelled() {
this.cancelled.emit(); this.cancelled.emit();
} }
private sendCreated(schema: SchemaDto) { private emitCreated(schema: SchemaDto) {
this.created.emit(schema); this.created.emit(schema);
} }

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

@ -136,6 +136,6 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
private updateClients(clients: ImmutableArray<AppClientDto>) { private updateClients(clients: ImmutableArray<AppClientDto>) {
this.appClients = clients; this.appClients = clients;
this.messageBus.publish(new HistoryChannelUpdated()); this.messageBus.emit(new HistoryChannelUpdated());
} }
} }

10
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -137,13 +137,17 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
.switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version)) .switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version))
.subscribe(() => { .subscribe(() => {
this.updateContributors(this.appContributors.push(requestDto)); this.updateContributors(this.appContributors.push(requestDto));
this.resetContributorForm();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}, () => { this.resetContributorForm();
this.addContributorForm.reset();
}); });
} }
private resetContributorForm() {
this.addContributorForm.reset();
}
private updateContributorsFromDto(dto: AppContributorsDto) { private updateContributorsFromDto(dto: AppContributorsDto) {
this.updateContributors(ImmutableArray.of(dto.contributors)); this.updateContributors(ImmutableArray.of(dto.contributors));
@ -153,6 +157,6 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
private updateContributors(contributors: ImmutableArray<AppContributorDto>) { private updateContributors(contributors: ImmutableArray<AppContributorDto>) {
this.appContributors = contributors; this.appContributors = contributors;
this.messageBus.publish(new HistoryChannelUpdated()); this.messageBus.emit(new HistoryChannelUpdated());
} }
} }

4
src/Squidex/app/features/settings/pages/languages/language.component.ts

@ -119,11 +119,11 @@ export class LanguageComponent implements OnInit, OnChanges, OnDestroy {
this.editForm.controls['isOptional'].value, this.editForm.controls['isOptional'].value,
this.fallbackLanguages.map(l => l.iso2Code)); this.fallbackLanguages.map(l => l.iso2Code));
this.sendSaving(newLanguage); this.emitSaving(newLanguage);
} }
} }
private sendSaving(language: AppLanguageDto) { private emitSaving(language: AppLanguageDto) {
this.saving.emit(language); this.saving.emit(language);
} }

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

@ -135,7 +135,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
this.updateNewLanguages(); this.updateNewLanguages();
this.messageBus.publish(new HistoryChannelUpdated()); this.messageBus.emit(new HistoryChannelUpdated());
} }
private updateNewLanguages() { private updateNewLanguages() {

4
src/Squidex/app/features/settings/pages/plans/plans-page.component.html

@ -45,10 +45,10 @@
</div> </div>
<div class="card-block"> <div class="card-block">
<div class="plan-fact"> <div class="plan-fact">
{{formatCalls(plan.maxApiCalls)}} API Calls {{plan.maxApiCalls | sqxKNumber}} API Calls
</div> </div>
<div class="plan-fact"> <div class="plan-fact">
{{formatSize(plan.maxAssetSize)}} Storage {{plan.maxAssetSize | sqxFileSize}} Storage
</div> </div>
<div class="plan-fact"> <div class="plan-fact">
{{plan.maxContributors}} Contributors {{plan.maxContributors}} Contributors

23
src/Squidex/app/features/settings/pages/plans/plans-page.component.ts

@ -14,7 +14,6 @@ import {
AppsStoreService, AppsStoreService,
AuthService, AuthService,
ChangePlanDto, ChangePlanDto,
FileHelper,
NotificationService, NotificationService,
PlansService, PlansService,
Version Version
@ -80,27 +79,5 @@ export class PlansPageComponent extends AppComponentBase implements OnInit {
this.isDisabled = false; this.isDisabled = false;
}); });
} }
public formatSize(count: number): string {
return FileHelper.fileSize(count);
}
public formatCalls(count: number): string | null {
if (count > 1000) {
count = count / 1000;
if (count < 10) {
count = Math.round(count * 10) / 10;
} else {
count = Math.round(count);
}
return count + 'k';
} else if (count < 0) {
return null;
} else {
return count.toString();
}
}
} }

30
src/Squidex/app/framework/angular/numbers.pipes.spec.ts

@ -0,0 +1,30 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { FileSizePipe, KNumberPipe } from './..';
describe('FileSizePipe', () => {
it('should calculate correct human file size', () => {
const pipe = new FileSizePipe();
expect(pipe.transform(50)).toBe('50 B');
expect(pipe.transform(1024)).toBe('1.0 kB');
expect(pipe.transform(1260000)).toBe('1.2 MB');
});
});
describe('KNumberPipe', () => {
it('should calculate correct human string', () => {
const pipe = new KNumberPipe();
expect(pipe.transform(0)).toBe('0');
expect(pipe.transform(-1)).toBe('');
expect(pipe.transform(50)).toBe('50');
expect(pipe.transform(1024)).toBe('1k');
expect(pipe.transform(1260000)).toBe('1000k');
});
});

50
src/Squidex/app/framework/angular/numbers.pipes.ts

@ -0,0 +1,50 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sqxKNumber',
pure: true
})
export class KNumberPipe implements PipeTransform {
public transform(value: number) {
if (value > 1000) {
value = value / 1000;
if (value < 10) {
value = Math.round(value * 10) / 10;
} else {
value = Math.round(value);
}
return value + 'k';
} else if (value < 0) {
return '';
} else {
return value.toString();
}
}
}
@Pipe({
name: 'sqxFileSize',
pure: true
})
export class FileSizePipe implements PipeTransform {
public transform(value: number) {
let u = 0, s = 1024;
while (value >= s || -value >= s) {
value /= s;
u++;
}
return (u ? value.toFixed(1) + ' ' : value) + ' kMGTPEZY'[u] + 'B';
}
}

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

@ -26,6 +26,7 @@ export * from './angular/modal-target.directive';
export * from './angular/modal-view.directive'; export * from './angular/modal-view.directive';
export * from './angular/money.pipe'; export * from './angular/money.pipe';
export * from './angular/name.pipe'; export * from './angular/name.pipe';
export * from './angular/numbers.pipes';
export * from './angular/panel.component'; export * from './angular/panel.component';
export * from './angular/panel-container.directive'; export * from './angular/panel-container.directive';
export * from './angular/parent-link.directive'; export * from './angular/parent-link.directive';

6
src/Squidex/app/framework/module.ts

@ -24,12 +24,14 @@ import {
DropdownComponent, DropdownComponent,
DurationPipe, DurationPipe,
FileDropDirective, FileDropDirective,
FileSizePipe,
FocusOnInitDirective, FocusOnInitDirective,
FromNowPipe, FromNowPipe,
GeolocationEditorComponent, GeolocationEditorComponent,
ImageSourceDirective, ImageSourceDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
JsonEditorComponent, JsonEditorComponent,
KNumberPipe,
LocalStoreService, LocalStoreService,
LowerCaseInputDirective, LowerCaseInputDirective,
MarkdownEditorComponent, MarkdownEditorComponent,
@ -83,12 +85,14 @@ import {
DropdownComponent, DropdownComponent,
DurationPipe, DurationPipe,
FileDropDirective, FileDropDirective,
FileSizePipe,
FocusOnInitDirective, FocusOnInitDirective,
FromNowPipe, FromNowPipe,
GeolocationEditorComponent, GeolocationEditorComponent,
ImageSourceDirective, ImageSourceDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
JsonEditorComponent, JsonEditorComponent,
KNumberPipe,
LowerCaseInputDirective, LowerCaseInputDirective,
MarkdownEditorComponent, MarkdownEditorComponent,
ModalTargetDirective, ModalTargetDirective,
@ -126,12 +130,14 @@ import {
DropdownComponent, DropdownComponent,
DurationPipe, DurationPipe,
FileDropDirective, FileDropDirective,
FileSizePipe,
FocusOnInitDirective, FocusOnInitDirective,
FromNowPipe, FromNowPipe,
GeolocationEditorComponent, GeolocationEditorComponent,
ImageSourceDirective, ImageSourceDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
JsonEditorComponent, JsonEditorComponent,
KNumberPipe,
LowerCaseInputDirective, LowerCaseInputDirective,
MarkdownEditorComponent, MarkdownEditorComponent,
ModalTargetDirective, ModalTargetDirective,

4
src/Squidex/app/framework/services/message-bus.spec.ts

@ -34,8 +34,8 @@ describe('MessageBus', () => {
lastEvent = event; lastEvent = event;
}); });
messageBus.publish(event1); messageBus.emit(event1);
messageBus.publish(event2); messageBus.emit(event2);
expect(lastEvent).toBe(event1); expect(lastEvent).toBe(event1);
}); });

2
src/Squidex/app/framework/services/message-bus.ts

@ -21,7 +21,7 @@ export const MessageBusFactory = () => {
export class MessageBus { export class MessageBus {
private message$ = new Subject<Message>(); private message$ = new Subject<Message>();
public publish<T>(message: T): void { public emit<T>(message: T): void {
const channel = (<any>message.constructor).name; const channel = (<any>message.constructor).name;
this.message$.next({ channel: channel, data: message }); this.message$.next({ channel: channel, data: message });

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

@ -54,7 +54,7 @@ export class AppFormComponent {
} }
public cancel() { public cancel() {
this.sendCancelled(); this.emitCancelled();
this.resetCreateForm(); this.resetCreateForm();
} }
@ -69,18 +69,18 @@ export class AppFormComponent {
this.appsStore.createApp(request) this.appsStore.createApp(request)
.subscribe(dto => { .subscribe(dto => {
this.resetCreateForm(); this.resetCreateForm();
this.sendCreated(dto); this.emitCreated(dto);
}, error => { }, error => {
this.enableCreateForm(error.displayMessage); this.enableCreateForm(error.displayMessage);
}); });
} }
} }
private sendCancelled() { private emitCancelled() {
this.cancelled.emit(); this.cancelled.emit();
} }
private sendCreated(app: AppDto) { private emitCreated(app: AppDto) {
this.created.emit(app); this.created.emit(app);
} }

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

@ -1,15 +1,15 @@
<div class="card" (sqxFileDrop)="updateFile($event)" dnd-draggable [dragEnabled]="!!asset" [dragData]="asset"> <div class="card" (sqxFileDrop)="updateFile($event)" dnd-draggable [dragEnabled]="!!asset" [dragData]="asset">
<div class="card-block"> <div class="card-block">
<div class="file-preview" *ngIf="asset && progress == 0" [@fade]> <div class="file-preview" *ngIf="asset && progress == 0" [@fade]>
<span class="file-type" *ngIf="fileType"> <span class="file-type" *ngIf="asset.fileType">
{{fileType}} {{asset.fileType}}
</span> </span>
<div *ngIf="asset.isImage" class="file-image"> <div *ngIf="asset.isImage" class="file-image">
<img [sqxImageSource]="previewUrl"> <img [sqxImageSource]="asset | sqxAssetPreviewUrl">
</div> </div>
<div *ngIf="!asset.isImage" class="file-icon-container"> <div *ngIf="!asset.isImage" class="file-icon-container">
<img class="file-icon" [attr.src]="fileIcon"> <img class="file-icon" [attr.src]="asset | sqxFileIcon">
</div> </div>
<div class="file-overlay"> <div class="file-overlay">
@ -18,7 +18,7 @@
<a class="file-edit" (click)="renameDialog.show()"> <a class="file-edit" (click)="renameDialog.show()">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</a> </a>
<a class="file-download" [attr.href]="fileUrl" target="_blank"> <a class="file-download" [attr.href]="asset | sqxAssetUrl" target="_blank">
<i class="icon-download"></i> <i class="icon-download"></i>
</a> </a>
@ -29,8 +29,8 @@
<i class="icon-close"></i> <i class="icon-close"></i>
</a> </a>
<span class="file-overlay-type" *ngIf="fileType"> <span class="file-overlay-type" *ngIf="asset.fileType">
{{fileType}} {{asset.fileType}}
</span> </span>
<span class="file-user"> <span class="file-user">
<i class="icon-user"></i> {{asset.lastModifiedBy | sqxUserNameRef}} <i class="icon-user"></i> {{asset.lastModifiedBy | sqxUserNameRef}}
@ -42,11 +42,11 @@
</div> </div>
</div> </div>
<div class="card-footer" dnd-draggable-handle *ngIf="asset && progress == 0"> <div class="card-footer" dnd-draggable-handle *ngIf="asset && progress == 0">
<div class="file-name" [attr.title]="fileName"> <div class="file-name" [attr.title]="asset.fileName">
{{fileName}} {{asset.fileName}}
</div> </div>
<div class="file-info"> <div class="file-info">
{{fileInfo}} <span *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </span> {{asset.fileSize | sqxFileSize}}
</div> </div>
</div> </div>

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

@ -11,14 +11,12 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { AppComponentBase } from './app.component-base'; import { AppComponentBase } from './app.component-base';
import { import {
ApiUrlConfig,
AppsStoreService, AppsStoreService,
AssetDto, AssetDto,
AssetReplacedDto, AssetReplacedDto,
AssetsService, AssetsService,
AuthService, AuthService,
fadeAnimation, fadeAnimation,
FileHelper,
ModalView, ModalView,
NotificationService, NotificationService,
UpdateAssetDto, UpdateAssetDto,
@ -34,7 +32,7 @@ import {
] ]
}) })
export class AssetComponent extends AppComponentBase implements OnInit { export class AssetComponent extends AppComponentBase implements OnInit {
private version: Version; private assetVersion: Version;
@Input() @Input()
public initFile: File; public initFile: File;
@ -71,18 +69,11 @@ export class AssetComponent extends AppComponentBase implements OnInit {
}); });
public progress = 0; public progress = 0;
public previewUrl: string;
public fileUrl: string;
public fileName: string;
public fileType: string;
public fileIcon: string;
public fileInfo: string;
constructor(apps: AppsStoreService, notifications: NotificationService, constructor(apps: AppsStoreService, notifications: NotificationService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly authService: AuthService, private readonly authService: AuthService
private readonly apiUrl: ApiUrlConfig
) { ) {
super(notifications, apps); super(notifications, apps);
} }
@ -95,7 +86,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
.switchMap(app => this.assetsService.uploadFile(app, initFile, this.authService.user.token)) .switchMap(app => this.assetsService.uploadFile(app, initFile, this.authService.user.token))
.subscribe(dto => { .subscribe(dto => {
if (dto instanceof AssetDto) { if (dto instanceof AssetDto) {
this.sendLoaded(dto); this.emitLoaded(dto);
} else { } else {
this.progress = dto; this.progress = dto;
} }
@ -110,7 +101,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
public updateFile(files: FileList) { public updateFile(files: FileList) {
if (files.length === 1) { if (files.length === 1) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.version)) .switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.assetVersion))
.subscribe(dto => { .subscribe(dto => {
if (dto instanceof AssetReplacedDto) { if (dto instanceof AssetReplacedDto) {
this.updateAsset(this.asset.update(dto, this.authService.user.token), true); this.updateAsset(this.asset.update(dto, this.authService.user.token), true);
@ -119,7 +110,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
} }
}, error => { }, error => {
this.setProgress(); this.setProgress();
this.sendFailed(error); this.emitFailed(error);
}); });
} }
} }
@ -133,7 +124,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
const requestDto = new UpdateAssetDto(this.renameForm.controls['name'].value); const requestDto = new UpdateAssetDto(this.renameForm.controls['name'].value);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.version)) .switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion))
.subscribe(() => { .subscribe(() => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.authService.user.token), true); this.updateAsset(this.asset.rename(requestDto.fileName, this.authService.user.token), true);
this.resetRenameForm(); this.resetRenameForm();
@ -152,15 +143,15 @@ export class AssetComponent extends AppComponentBase implements OnInit {
this.progress = progress; this.progress = progress;
} }
private sendFailed(error: any) { private emitFailed(error: any) {
this.failed.emit(error); this.failed.emit(error);
} }
private sendLoaded(asset: AssetDto) { private emitLoaded(asset: AssetDto) {
this.loaded.emit(asset); this.loaded.emit(asset);
} }
private sendUpdated(asset: AssetDto) { private emitUpdated(asset: AssetDto) {
this.updated.emit(asset); this.updated.emit(asset);
} }
@ -177,17 +168,11 @@ export class AssetComponent extends AppComponentBase implements OnInit {
private updateAsset(asset: AssetDto, emitEvent: boolean) { private updateAsset(asset: AssetDto, emitEvent: boolean) {
this.asset = asset; this.asset = asset;
this.fileUrl = FileHelper.assetUrl(this.apiUrl, asset); this.assetVersion = asset.version;
this.fileInfo = FileHelper.assetInfo(asset);
this.fileName = FileHelper.assetName(asset);
this.fileType = FileHelper.fileType(asset.mimeType, this.asset.fileName);
this.fileIcon = FileHelper.fileIcon(asset.mimeType);
this.progress = 0; this.progress = 0;
this.previewUrl = FileHelper.assetPreviewUrl(this.apiUrl, asset);
this.version = asset.version;
if (emitEvent) { if (emitEvent) {
this.sendUpdated(asset); this.emitUpdated(asset);
} }
this.resetRenameForm(); this.resetRenameForm();

62
src/Squidex/app/shared/components/pipes.ts

@ -8,7 +8,7 @@
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core'; import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { ApiUrlConfig } from 'framework'; import { ApiUrlConfig, MathHelper, Version } from 'framework';
import { UserDto, UsersProviderService } from './../declarations-base'; import { UserDto, UsersProviderService } from './../declarations-base';
@ -192,4 +192,64 @@ export class UserPictureRefPipe extends UserAsyncPipe implements PipeTransform {
} }
}); });
} }
}
@Pipe({
name: 'sqxAssetUrl',
pure: false
})
export class AssetUrlPipe implements PipeTransform {
constructor(
private readonly apiUrl: ApiUrlConfig
) {
}
public transform(asset: { id: any }): string {
return this.apiUrl.buildUrl(`api/assets/${asset.id}?q=${MathHelper.guid()}`);
}
}
@Pipe({
name: 'sqxAssetPreviewUrl',
pure: false
})
export class AssetPreviewUrlPipe implements PipeTransform {
constructor(
private readonly apiUrl: ApiUrlConfig
) {
}
public transform(asset: { id: any, version: Version }): string {
return this.apiUrl.buildUrl(`api/assets/${asset.id}?version=${asset.version.value}`)
}
}
@Pipe({
name: 'sqxFileIcon',
pure: false
})
export class FileIconPipe implements PipeTransform {
public transform(asset: { mimeType: string, fileType: string }): string {
const knownTypes = [
'doc',
'docx',
'pdf',
'ppt',
'pptx',
'video',
'xls',
'xlsx'
];
let mimeIcon = '';
let mimeParts = asset.mimeType.split('/');
if (mimeParts.length === 2 && mimeParts[0].toLowerCase() === 'video') {
mimeIcon = 'video';
} else {
mimeIcon = knownTypes.indexOf(asset.fileType) >= 0 ? asset.fileType : 'generic'
}
return `/images/asset_${mimeIcon}.png`;
}
} }

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

@ -36,7 +36,6 @@ export * from './services/users-provider.service';
export * from './services/users.service'; export * from './services/users.service';
export * from './services/webhooks.service'; export * from './services/webhooks.service';
export * from './utils/file-helper';
export * from './utils/messages'; export * from './utils/messages';
export * from 'framework'; export * from 'framework';

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

@ -20,13 +20,16 @@ import {
AppsService, AppsService,
AppMustExistGuard, AppMustExistGuard,
AssetComponent, AssetComponent,
AssetPreviewUrlPipe,
AssetsService, AssetsService,
AssetUrlPipe,
AuthInterceptor, AuthInterceptor,
AuthService, AuthService,
ContentsService, ContentsService,
EventConsumersService, EventConsumersService,
HelpComponent, FileIconPipe,
GraphQlService, GraphQlService,
HelpComponent,
HelpService, HelpService,
HistoryComponent, HistoryComponent,
HistoryService, HistoryService,
@ -64,6 +67,9 @@ import {
declarations: [ declarations: [
AppFormComponent, AppFormComponent,
AssetComponent, AssetComponent,
AssetPreviewUrlPipe,
AssetUrlPipe,
FileIconPipe,
HelpComponent, HelpComponent,
HistoryComponent, HistoryComponent,
LanguageSelectorComponent, LanguageSelectorComponent,
@ -79,6 +85,9 @@ import {
exports: [ exports: [
AppFormComponent, AppFormComponent,
AssetComponent, AssetComponent,
AssetPreviewUrlPipe,
AssetUrlPipe,
FileIconPipe,
HelpComponent, HelpComponent,
HistoryComponent, HistoryComponent,
LanguageSelectorComponent, LanguageSelectorComponent,

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

@ -23,10 +23,10 @@ describe('AssetDto', () => {
it('should update name property and user info when renaming', () => { it('should update name property and user info when renaming', () => {
const now = DateTime.now(); const now = DateTime.now();
const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name', 1, 1, 'image/png', false, 1, 1, null); const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, null);
const asset_2 = asset_1.rename('new-name', 'me', now); const asset_2 = asset_1.rename('new-name', 'me', now);
expect(asset_2.fileName).toEqual('new-name'); expect(asset_2.fileName).toEqual('new-name.png');
expect(asset_2.lastModified).toEqual(now); expect(asset_2.lastModified).toEqual(now);
expect(asset_2.lastModifiedBy).toEqual('me'); expect(asset_2.lastModifiedBy).toEqual('me');
}); });
@ -36,7 +36,7 @@ describe('AssetDto', () => {
const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2, null); const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2, null);
const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name', 1, 1, 'image/png', false, 1, 1, null); const asset_1 = new AssetDto('1', 'other', 'other', DateTime.today(), DateTime.today(), 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, null);
const asset_2 = asset_1.update(update, 'me', now); const asset_2 = asset_1.update(update, 'me', now);
expect(asset_2.fileSize).toEqual(2); expect(asset_2.fileSize).toEqual(2);
@ -95,9 +95,10 @@ describe('AssetsService', () => {
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
fileName: 'my-asset1.png', fileName: 'my-asset1.png',
fileType: 'png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2000, fileVersion: 2000,
mimeType: 'text/plain', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
@ -110,9 +111,10 @@ describe('AssetsService', () => {
lastModified: '2017-10-12T10:10', lastModified: '2017-10-12T10:10',
lastModifiedBy: 'LastModifiedBy2', lastModifiedBy: 'LastModifiedBy2',
fileName: 'my-asset2.png', fileName: 'my-asset2.png',
fileType: 'png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2000, fileVersion: 2000,
mimeType: 'text/plain', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
@ -128,9 +130,10 @@ describe('AssetsService', () => {
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',
1024, 1024,
2000, 2000,
'text/plain', 'image/png',
true, true,
1024, 1024,
2048, 2048,
@ -139,9 +142,10 @@ describe('AssetsService', () => {
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',
1024, 1024,
2000, 2000,
'text/plain', 'image/png',
true, true,
1024, 1024,
2048, 2048,
@ -170,9 +174,10 @@ describe('AssetsService', () => {
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
fileName: 'my-asset1.png', fileName: 'my-asset1.png',
fileType: 'png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2000, fileVersion: 2000,
mimeType: 'text/plain', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
@ -185,9 +190,10 @@ describe('AssetsService', () => {
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',
1024, 1024,
2000, 2000,
'text/plain', 'image/png',
true, true,
1024, 1024,
2048, 2048,
@ -210,9 +216,9 @@ describe('AssetsService', () => {
it('should append mime types to find by types', it('should append mime types to find by types',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', 17, 13, undefined, ['text/plain', 'image/png']).subscribe(); assetsService.getAssets('my-app', 17, 13, undefined, ['image/png', 'image/png']).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?mimeTypes=text/plain,image/png&take=17&skip=13'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?mimeTypes=image/png,image/png&take=17&skip=13');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
@ -252,7 +258,7 @@ describe('AssetsService', () => {
fileName: 'my-asset1.png', fileName: 'my-asset1.png',
fileSize: 1024, fileSize: 1024,
fileVersion: 2, fileVersion: 2,
mimeType: 'text/plain', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
@ -267,8 +273,9 @@ describe('AssetsService', () => {
now, now,
now, now,
'my-asset1.png', 'my-asset1.png',
'png',
1024, 2, 1024, 2,
'text/plain', 'image/png',
true, true,
1024, 1024,
2048, 2048,
@ -292,7 +299,7 @@ describe('AssetsService', () => {
req.flush({ req.flush({
fileSize: 1024, fileSize: 1024,
fileVersion: 2, fileVersion: 2,
mimeType: 'text/plain', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048,
@ -302,7 +309,7 @@ describe('AssetsService', () => {
expect(asset).toEqual( expect(asset).toEqual(
new AssetReplacedDto( new AssetReplacedDto(
1024, 2, 1024, 2,
'text/plain', 'image/png',
true, true,
1024, 1024,
2048, 2048,

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

@ -32,6 +32,7 @@ export class AssetDto {
public readonly created: DateTime, public readonly created: DateTime,
public readonly lastModified: DateTime, public readonly lastModified: DateTime,
public readonly fileName: string, public readonly fileName: string,
public readonly fileType: string,
public readonly fileSize: number, public readonly fileSize: number,
public readonly fileVersion: number, public readonly fileVersion: number,
public readonly mimeType: string, public readonly mimeType: string,
@ -48,6 +49,7 @@ export class AssetDto {
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.fileName, this.fileName,
this.fileType,
update.fileSize, update.fileSize,
update.fileVersion, update.fileVersion,
update.mimeType, update.mimeType,
@ -63,6 +65,7 @@ export class AssetDto {
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
name, name,
this.fileType,
this.fileSize, this.fileSize,
this.fileVersion, this.fileVersion,
this.mimeType, this.mimeType,
@ -135,6 +138,7 @@ export class AssetsService {
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified), DateTime.parseISO_UTC(item.lastModified),
item.fileName, item.fileName,
item.fileType,
item.fileSize, item.fileSize,
item.fileVersion, item.fileVersion,
item.mimeType, item.mimeType,
@ -174,6 +178,7 @@ export class AssetsService {
now, now,
now, now,
response.fileName, response.fileName,
response.fileType,
response.fileSize, response.fileSize,
response.fileVersion, response.fileVersion,
response.mimeType, response.mimeType,
@ -204,6 +209,7 @@ export class AssetsService {
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.created),
DateTime.parseISO_UTC(response.lastModified), DateTime.parseISO_UTC(response.lastModified),
response.fileName, response.fileName,
response.fileType,
response.fileSize, response.fileSize,
response.fileVersion, response.fileVersion,
response.mimeType, response.mimeType,

75
src/Squidex/app/shared/utils/file-helper.spec.ts

@ -1,75 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { ApiUrlConfig, DateTime, Version } from 'framework';
import { FileHelper } from './file-helper';
import { AssetDto } from './../services/assets.service';
describe('FileHelper', () => {
const now = DateTime.now();
const user = 'user';
it('should calculate correct human file size', () => {
expect(FileHelper.fileSize(50)).toBe('50 B');
expect(FileHelper.fileSize(1024)).toBe('1.0 kB');
expect(FileHelper.fileSize(1260000)).toBe('1.2 MB');
});
it('should calculate icon', () => {
expect(FileHelper.fileIcon('video/mp4')).toBe('/images/asset_video.png');
expect(FileHelper.fileIcon('application/text')).toBe('/images/asset_generic.png');
expect(FileHelper.fileIcon('application/msword')).toBe('/images/asset_doc.png');
});
it('should calculate file type', () => {
expect(FileHelper.fileType('video/mp4', 'test.mp4')).toBe('mp4');
expect(FileHelper.fileType('video/mp4')).toBe('mp4');
expect(FileHelper.fileType('application/text', 'test.txt')).toBe('txt');
expect(FileHelper.fileType('application/text')).toBe('text');
expect(FileHelper.fileType('invalid')).toBeUndefined();
expect(FileHelper.fileType(null!)).toBeUndefined();
});
it('should calculate asset info for image asset', () => {
const asset = new AssetDto('1', user, user, now, now, 'File.png', 50, 1, 'image/png', true, 100, 20, new Version('123'));
expect(FileHelper.assetInfo(asset)).toBe('100x20px, 50 B');
});
it('should calculate asset info for text asset', () => {
const asset = new AssetDto('1', user, user, now, now, 'File.txt', 50, 1, 'text/plain', false, 0, 0, null!);
expect(FileHelper.assetInfo(asset)).toBe('50 B');
});
it('should return asset name', () => {
const asset = new AssetDto('1', user, user, now, now, 'File.txt', 50, 1, 'text/plain', false, 0, 0, null!);
expect(FileHelper.assetName(asset)).toBe('File.txt');
});
it('should return empty string for invalid asset', () => {
expect(FileHelper.assetInfo(undefined!)).toBe('');
expect(FileHelper.assetInfo(null!)).toBe('');
});
it('should return preview url', () => {
const apiUrl = new ApiUrlConfig('my/');
const asset = new AssetDto('1', user, user, now, now, 'File.txt', 50, 1, 'text/plain', false, 0, 0, new Version('123'));
expect(FileHelper.assetPreviewUrl(apiUrl, asset)).toBe('my/api/assets/1?version=123');
});
it('should return download url', () => {
const apiUrl = new ApiUrlConfig('my/');
const asset = new AssetDto('1', user, user, now, now, 'File.txt', 50, 1, 'text/plain', false, 0, 0, new Version('123'));
expect(FileHelper.assetUrl(apiUrl, asset).startsWith('my/api/assets/1?q=')).toBeTruthy();
});
});

112
src/Squidex/app/shared/utils/file-helper.ts

@ -1,112 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { ApiUrlConfig, MathHelper } from 'framework';
import { AssetDto } from './../services/assets.service';
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'
};
export module FileHelper {
export function assetUrl(apiUrl: ApiUrlConfig, asset: AssetDto): string {
return apiUrl.buildUrl(`api/assets/${asset.id}?q=${MathHelper.guid()}`);
}
export function assetName(asset: AssetDto): string {
return asset.fileName;
}
export function assetPreviewUrl(apiUrl: ApiUrlConfig, asset: AssetDto) {
return apiUrl.buildUrl(`api/assets/${asset.id}?version=${asset.version.value}`);
}
export function assetInfo(asset: AssetDto): string {
let result = '';
if (asset != null) {
if (asset.pixelWidth) {
result = `${asset.pixelWidth}x${asset.pixelHeight}px, `;
}
result += FileHelper.fileSize(asset.fileSize);
}
return result;
}
export function fileType(mimeType: string, fileName?: string) {
if (fileName) {
const parts = fileName.split('.');
if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase();
}
}
if (mimeType) {
const parts = mimeType.split('/');
if (parts.length === 2) {
const mimeSuffix = parts[1].toLowerCase();
return mimeMapping[mimeSuffix] || mimeSuffix;
}
}
return undefined;
}
export function fileIcon(mimeType: string) {
const mimeParts = mimeType.split('/');
let mimeIcon = 'generic';
if (mimeParts.length === 2) {
const mimePrefix = mimeParts[0].toLowerCase();
const mimeSuffix = mimeParts[1].toLowerCase();
if (mimePrefix === 'video') {
mimeIcon = 'video';
} else {
mimeIcon = mimeMapping[mimeSuffix] || 'generic';
}
}
return `/images/asset_${mimeIcon}.png`;
}
export function fileSize(bytes: number) {
let u = 0, s = 1024;
while (bytes >= s || -bytes >= s) {
bytes /= s;
u++;
}
return (u ? bytes.toFixed(1) + ' ' : bytes) + ' kMGTPEZY'[u] + 'B';
}
}

38
tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs

@ -0,0 +1,38 @@
// ==========================================================================
// FileExtensionsTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Xunit;
namespace Squidex.Infrastructure
{
public class FileExtensionsTests
{
[Theory]
[InlineData("test.mp4", "mp4")]
[InlineData("test.MP4", "mp4")]
[InlineData("test.txt", "txt")]
[InlineData("test.TXT", "txt")]
public void Should_calculate_file_type(string fileName, string expected)
{
var actual = fileName.FileType();
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Should_blob_for_invalid_file_types(string fileName)
{
var actual = fileName.FileType();
Assert.Equal("blob", actual);
}
}
}
Loading…
Cancel
Save