Browse Source

1) Progress bar.

2) Replace content.
pull/65/head
Sebastian Stehle 9 years ago
parent
commit
b36c8e23e5
  1. 2
      src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs
  2. 6
      src/Squidex.Write/Assets/Commands/CreateAsset.cs
  3. 29
      src/Squidex/Controllers/Api/Assets/AssetController.cs
  4. 80
      src/Squidex/Controllers/Api/Assets/Models/AssetUpdatedDto.cs
  5. 35
      src/Squidex/app/features/assets/pages/asset.component.html
  6. 7
      src/Squidex/app/features/assets/pages/asset.component.scss
  7. 112
      src/Squidex/app/features/assets/pages/asset.component.ts
  8. 2
      src/Squidex/app/features/assets/pages/assets-page.component.html
  9. 4
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  10. 77
      src/Squidex/app/framework/angular/progress-bar.component.ts
  11. 1
      src/Squidex/app/framework/declarations.ts
  12. 3
      src/Squidex/app/framework/module.ts
  13. 3
      src/Squidex/app/shared/module.ts
  14. 57
      src/Squidex/app/shared/services/assets.service.ts
  15. 2
      src/Squidex/app/shared/services/schemas.service.spec.ts
  16. 2
      src/Squidex/package.json

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

@ -33,7 +33,7 @@ namespace Squidex.Read.MongoDb.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));
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)

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

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.Assets;
namespace Squidex.Write.Assets.Commands
@ -15,5 +16,10 @@ namespace Squidex.Write.Assets.Commands
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public CreateAsset()
{
AssetId = Guid.NewGuid();
}
}
}

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

@ -57,7 +57,8 @@ namespace Squidex.Controllers.Api.Assets
/// <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.
/// 200 => Assets returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/assets/")]
@ -123,7 +124,7 @@ namespace Squidex.Controllers.Api.Assets
/// <param name="file">The file to upload.</param>
/// <returns>
/// 201 => Asset updated.
/// 404 => App or Asset not found.
/// 404 => Asset or app not found.
/// 400 => Asset exceeds the maximum size.
/// </returns>
[HttpPut]
@ -133,10 +134,14 @@ namespace Squidex.Controllers.Api.Assets
public async Task<IActionResult> PutAssetContent(string app, Guid id, List<IFormFile> file)
{
var assetFile = GetAssetFile(file);
await CommandBus.PublishAsync(new UpdateAsset { File = assetFile });
return NoContent();
var command = new UpdateAsset { File = assetFile, AssetId = id };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntitySavedResult>();
var response = AssetUpdatedDto.Create(command, result);
return StatusCode(201, response);
}
/// <summary>
@ -146,14 +151,13 @@ namespace Squidex.Controllers.Api.Assets
/// <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.
/// 204 => Asset updated.
/// 404 => Asset or app not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/assets/{id}/content")]
[ProducesResponseType(typeof(AssetDto), 201)]
[HttpPut]
[Route("apps/{app}/assets/{id}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request)
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request)
{
var command = SimpleMapper.Map(request, new RenameAsset());
@ -169,6 +173,7 @@ namespace Squidex.Controllers.Api.Assets
/// <param name="id">The id of the asset to delete.</param>
/// <returns>
/// 204 => Asset has been deleted.
/// 404 => Asset or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/")]
@ -183,7 +188,7 @@ namespace Squidex.Controllers.Api.Assets
{
if (file.Count != 1)
{
var error = new ValidationError($"Can only upload one file, found ${file.Count}.");
var error = new ValidationError($"Can only upload one file, found {file.Count}.");
throw new ValidationException("Cannot create asset.", error);
}

80
src/Squidex/Controllers/Api/Assets/Models/AssetUpdatedDto.cs

@ -0,0 +1,80 @@
// ==========================================================================
// AssetUpdatedDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Write.Assets.Commands;
namespace Squidex.Controllers.Api.Assets.Models
{
public sealed class AssetUpdatedDto
{
/// <summary>
/// The mime type.
/// </summary>
[Required]
public string MimeType { get; set; }
/// <summary>
/// The size of the file in bytes.
/// </summary>
public long FileSize { get; set; }
/// <summary>
/// Determines of the created file is an image.
/// </summary>
public bool IsImage { get; set; }
/// <summary>
/// The width of the image in pixels if the asset is an image.
/// </summary>
public int? PixelWidth { get; set; }
/// <summary>
/// The height of the image in pixels if the asset is an image.
/// </summary>
public int? PixelHeight { get; set; }
/// <summary>
/// The user that has updated the asset.
/// </summary>
[Required]
public RefToken LastModifiedBy { get; set; }
/// <summary>
/// The date and time when the asset has been modified last.
/// </summary>
public Instant LastModified { get; set; }
/// <summary>
/// The version of the asset.
/// </summary>
public long Version { get; set; }
public static AssetUpdatedDto Create(UpdateAsset command, EntitySavedResult result)
{
var now = SystemClock.Instance.GetCurrentInstant();
var response = new AssetUpdatedDto
{
Version = result.Version,
LastModified = now,
LastModifiedBy = command.Actor,
FileSize = command.File.FileSize,
MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight
};
return response;
}
}
}

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

@ -1,24 +1,29 @@
<div class="asset">
<div class="card" (sqxFileDrop)="updateFile($event)">
<div class="card-block">
<div *ngIf="asset" [@fade]>
<div class="file-type">{{fileType}}</div>
<div *ngIf="asset.isImage">
<img [attr.src]="previewUrl | async" sqxHideInvalidImage>
<div *ngIf="asset && progress == 0" [@fade]>
<div class="card-block">
<div>
<div class="file-type">{{fileType}}</div>
<div *ngIf="asset.isImage">
<img [attr.src]="previewUrl" sqxHideInvalidImage>
</div>
<div *ngIf="!asset.isImage" class="file-icon-container">
<img class="file-icon" [attr.src]="fileIcon" sqxHideInvalidImage>
</div>
</div>
<div *ngIf="!asset.isImage" class="file-icon-container">
<img [attr.src]="fileIcon" sqxHideInvalidImage>
</div>
<div class="card-footer">
<div class="file-name">
{{fileName}}
</div>
<div class="file-info">
{{fileInfo}}
</div>
</div>
</div>
<div class="card-footer">
<div class="file-name">
{{fileName}}
</div>
<div class="file-info">
{{fileInfo}}
</div>
<div *ngIf="progress > 0">
<sqx-progress-bar class="upload-progress" style="width: 120px; height: 120px" mode="Circle" [value]="progress"></sqx-progress-bar>
</div>
<div class="drop-overlay">
<div class="drop-overlay-background"></div>

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

@ -59,6 +59,10 @@ $color-type-foreground: #fff;
}
}
.upload-progress {
margin: 60px 70px;
}
.file {
&-info {
font-size: .8rem;
@ -67,7 +71,10 @@ $color-type-foreground: #fff;
&-icon {
&-container {
background: $color-border;
border: 0;
height: 155px;
line-height: 155px;
text-align: center;
}
}

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

@ -5,16 +5,17 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, Input, NgZone, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
ApiUrlConfig,
AppComponentBase,
AppsStoreService,
AssetDto,
AssetUpdatedDto,
AssetsService,
fadeAnimation,
MathHelper,
NotificationService,
UsersProviderService
} from 'shared';
@ -28,44 +29,36 @@ import {
]
})
export class AssetComponent extends AppComponentBase implements OnInit {
private assetVersion = MathHelper.guid();
@Input()
public initFile: File;
@Input()
public asset: AssetDto;
public get previewUrl(): Observable<string> {
return this.appName().map(app => this.apiUrl.buildUrl(`api/assets/${this.asset.id}/?width=230&height=155&mode=Crop`));
}
@Output()
public deleting = new EventEmitter();
public get fileType(): string {
let result = '';
@Output()
public failed = new EventEmitter();
if (this.asset != null) {
result = this.asset.mimeType.split('/')[1];
}
public progress = 0;
return result;
public get previewUrl(): string {
return this.apiUrl.buildUrl(`api/assets/${this.asset.id}/?width=230&height=155&mode=Crop&q=${this.assetVersion}`);
}
public get fileIcon(): string {
let result = '';
if (this.asset != null) {
result = fileIcon(this.fileType);
}
return result;
public get fileType(): string {
return this.asset.mimeType.split('/')[1];
}
public get fileName(): string {
let result = '';
if (this.asset != null) {
result = this.asset.fileName;
}
return this.asset.fileName;
}
return result;
public get fileIcon(): string {
return fileIcon(this.asset.mimeType);
}
public get fileInfo(): string {
@ -84,8 +77,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly assetsService: AssetsService,
private readonly apiUrl: ApiUrlConfig,
private readonly zone: NgZone
private readonly apiUrl: ApiUrlConfig
) {
super(notifications, users, apps);
}
@ -95,14 +87,55 @@ export class AssetComponent extends AppComponentBase implements OnInit {
if (initFile) {
this.appName()
.switchMap(app => this.assetsService.uploadFile(app, initFile)).delay(2000)
.switchMap(app => this.assetsService.uploadFile(app, initFile))
.subscribe(result => {
this.zone.run(() => {
if (result instanceof AssetDto) {
if (result instanceof AssetDto) {
setTimeout(() => {
this.asset = result;
}
});
this.assetVersion = MathHelper.guid();
this.progress = 0;
}, 2000);
} else {
this.progress = result;
}
}, error => {
this.failed.emit();
this.notifyError(error);
});
}
}
public updateFile(files: FileList) {
if (files.length === 1) {
this.appName()
.switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0]))
.subscribe(result => {
if (result instanceof AssetUpdatedDto) {
setTimeout(() => {
const asset = new AssetDto(
this.asset.id,
this.asset.createdBy,
result.lastModifiedBy,
this.asset.created,
result.lastModified,
this.asset.fileName,
result.fileSize,
result.mimeType,
result.isImage,
result.pixelWidth,
result.pixelHeight,
result.version);
this.asset = asset;
this.assetVersion = MathHelper.guid();
this.progress = 0;
}, 2000);
} else {
this.progress = result;
}
}, error => {
this.progress = 0;
this.notifyError(error);
});
}
@ -147,15 +180,18 @@ const mimeMapping = {
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';;
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`;

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

@ -36,7 +36,7 @@
</div>
<div class="row">
<sqx-asset class="col-3" *ngFor="let file of newFiles" [initFile]="file"></sqx-asset>
<sqx-asset class="col-3" *ngFor="let file of newFiles" [initFile]="file" (failed)="removeFile(file)"></sqx-asset>
<sqx-asset class="col-3" *ngFor="let asset of assetsItems" [asset]="asset"></sqx-asset>
</div>

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

@ -78,6 +78,10 @@ export class AssetsPageComponent extends AppComponentBase implements OnInit {
this.load();
}
public removeFile(file: File) {
this.newFiles = this.newFiles.remove(file);
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
this.newFiles = this.newFiles.pushFront(files[i]);

77
src/Squidex/app/framework/angular/progress-bar.component.ts

@ -0,0 +1,77 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, ElementRef, Input, OnChanges, OnInit, Renderer } from '@angular/core';
const ProgressBar = require('progressbar.js');
@Component({
selector: 'sqx-progress-bar',
template: ''
})
export class ProgressBarComponent implements OnChanges, OnInit {
private progressBar: any;
@Input()
public mode = 'Line';
@Input()
public color = '#3d7dd5';
@Input()
public trailColor = '#f4f4f4';
@Input()
public trailWidth = 4;
@Input()
public strokeWidth = 4;
@Input()
public value = 0;
constructor(
private readonly elementRef: ElementRef,
private readonly renderer: Renderer
) {
}
public ngOnInit() {
const options = {
color: this.color,
trailColor: this.trailColor,
trailWidth: this.trailWidth,
strokeWidth: this.strokeWidth
};
this.renderer.setElementStyle(this.elementRef.nativeElement, 'display', 'block');
if (this.mode === 'Circle') {
this.progressBar = new ProgressBar.Circle(this.elementRef.nativeElement, options);
} else {
this.progressBar = new ProgressBar.Line(this.elementRef.nativeElement, options);
}
this.updateValue();
}
public ngOnChanges() {
if (this.progressBar) {
this.updateValue();
}
}
private updateValue() {
const value = this.value;
this.progressBar.animate(value / 100);
if (value > 0) {
this.progressBar.setText(Math.round(value) + '%');
}
}
}

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

@ -28,6 +28,7 @@ export * from './angular/money.pipe';
export * from './angular/name.pipe';
export * from './angular/panel.component';
export * from './angular/panel-container.directive';
export * from './angular/progress-bar.component';
export * from './angular/rich-editor.component';
export * from './angular/scroll-active.directive';
export * from './angular/shortcut.component';

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

@ -41,6 +41,7 @@ import {
PanelContainerDirective,
PanelComponent,
PanelService,
ProgressBarComponent,
ResourceLoaderService,
RichEditorComponent,
ScrollActiveDirective,
@ -90,6 +91,7 @@ import {
MonthPipe,
PanelContainerDirective,
PanelComponent,
ProgressBarComponent,
RichEditorComponent,
ScrollActiveDirective,
ShortcutComponent,
@ -127,6 +129,7 @@ import {
MonthPipe,
PanelContainerDirective,
PanelComponent,
ProgressBarComponent,
RichEditorComponent,
ScrollActiveDirective,
ShortcutComponent,

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

@ -7,6 +7,8 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { ProgressHttpModule } from 'angular-progress-http';
import { SqxFrameworkModule } from 'framework';
import {
@ -42,6 +44,7 @@ import {
@NgModule({
imports: [
ProgressHttpModule,
SqxFrameworkModule
],
declarations: [

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

@ -6,8 +6,9 @@
*/
import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import { Headers } from '@angular/http';
import { Observable } from 'rxjs';
import { ProgressHttp } from 'angular-progress-http';
import {
ApiUrlConfig,
@ -43,10 +44,24 @@ export class AssetDto {
}
}
export class AssetUpdatedDto {
constructor(
public readonly lastModifiedBy: string,
public readonly lastModified: DateTime,
public readonly fileSize: number,
public readonly mimeType: string,
public readonly isImage: boolean,
public readonly pixelWidth: number | null,
public readonly pixelHeight: number | null,
public readonly version: Version
) {
}
}
@Injectable()
export class AssetsService {
constructor(
private readonly http: Http,
private readonly http: ProgressHttp,
private readonly apiUrl: ApiUrlConfig,
private readonly authService: AuthService
) {
@ -118,7 +133,7 @@ export class AssetsService {
content.append('file', file);
this.http
this.http.withUploadProgressListener(progress => subscriber.next(progress.percentage))
.post(url, content, { headers })
.map(response => response.json())
.map(response => {
@ -146,4 +161,40 @@ export class AssetsService {
});
});
}
public replaceFile(appName: string, id: string, file: File): Observable<number | AssetUpdatedDto> {
return new Observable<number | AssetDto>(subscriber => {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}/content`);
const content = new FormData();
const headers = new Headers({
'Authorization': `${this.authService.user.user.token_type} ${this.authService.user.user.access_token}`
});
content.append('file', file);
this.http.withUploadProgressListener(progress => subscriber.next(progress.percentage))
.put(url, content, { headers })
.map(response => response.json())
.map(response => {
return new AssetUpdatedDto(
response.lastModifiedBy,
DateTime.parseISO_UTC(response.lastModified),
response.fileSize,
response.mimeType,
response.isImage,
response.pixelWidth,
response.pixelHeight,
new Version(response.version.toString()));
})
.catchError('Failed to replace asset. Please reload.')
.subscribe(value => {
subscriber.next(value);
}, err => {
subscriber.error(err);
}, () => {
subscriber.complete();
});
});
}
}

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

@ -274,7 +274,7 @@ describe('SchemasService', () => {
});
it('should make put request to update field ordering', () => {
const dto = [1, 2, 3]
const dto = [1, 2, 3];
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/ordering', It.isAny(), version))
.returns(() => Observable.of(

2
src/Squidex/package.json

@ -24,6 +24,7 @@
"@angular/platform-browser": "4.0.1",
"@angular/platform-browser-dynamic": "4.0.1",
"@angular/router": "4.0.1",
"angular-progress-http": "^0.4.1",
"babel-polyfill": "6.23.0",
"bootstrap": "4.0.0-alpha.6",
"core-js": "2.4.1",
@ -32,6 +33,7 @@
"ng2-dnd": "^4.0.2",
"oidc-client": "1.3.0",
"pikaday": "1.5.1",
"progressbar.js": "^1.0.1",
"redoc": "1.12.1",
"rxjs": "5.3.0",
"zone.js": "0.8.5"

Loading…
Cancel
Save