Browse Source

Some more progress.

pull/356/head
Sebastian Stehle 8 years ago
parent
commit
e6925253ed
  1. 21
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 35
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  3. 19
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  4. 3
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  5. 4
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  6. 14
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  7. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  8. 2
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  9. 8
      src/Squidex/app/framework/utils/rxjs-extensions.ts
  10. 15
      src/Squidex/app/shared/components/asset-uploader.component.html
  11. 32
      src/Squidex/app/shared/components/asset-uploader.component.scss
  12. 4
      src/Squidex/app/shared/components/asset-uploader.component.ts
  13. 2
      src/Squidex/app/shared/components/asset.component.html
  14. 5
      src/Squidex/app/shared/components/asset.component.ts
  15. 17
      src/Squidex/app/shared/components/assets-list.component.html
  16. 211
      src/Squidex/app/shared/state/asset-uploader.state.spec.ts
  17. 97
      src/Squidex/app/shared/state/asset-uploader.state.ts
  18. 2
      src/Squidex/app/shared/state/roles.state.ts
  19. 14
      src/Squidex/app/shared/state/schemas.state.ts
  20. 1027
      src/Squidex/package-lock.json
  21. 28
      src/Squidex/package.json
  22. 9
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  23. 8
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs
  24. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  25. 1
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

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

@ -53,6 +53,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
new CreateIndexModel<MongoAssetEntity>(
Index
.Ascending(x => x.AppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Slug))
},
ct);
@ -114,32 +115,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetEntity =
await Collection.Find(x => x.IndexedAppId == appId && x.Slug == slug)
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.Slug == slug)
.FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true)
{
return null;
}
return assetEntity;
}
}
public async Task<IAssetEntity> FindAssetByHashAsync(Guid appId, string hash)
public async Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetEntity =
var assetEntities =
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash)
.FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true)
{
return null;
}
.ToListAsync();
return assetEntity;
return assetEntities.OfType<IAssetEntity>().ToList();
}
}

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

@ -11,7 +11,6 @@ using System.Security.Cryptography;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
@ -22,13 +21,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain>
{
private readonly IAssetStore assetStore;
private readonly AssetQueryService assetQueryService;
private readonly IAssetQueryService assetQueryService;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
AssetQueryService assetQueryService,
IAssetQueryService assetQueryService,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
@ -63,21 +62,27 @@ namespace Squidex.Domain.Apps.Entities.Assets
try
{
var existing = await assetQueryService.FindAssetByHashAsync(createAsset.AppId.Id, createAsset.FileHash);
var existings = await assetQueryService.QueryByHashAsync(createAsset.AppId.Id, createAsset.FileHash);
AssetCreatedResult result;
AssetCreatedResult result = null;
if (IsDuplicate(createAsset, existing))
foreach (var existing in existings)
{
result = new AssetCreatedResult(
existing.Id,
existing.Tags,
existing.Version,
existing.FileVersion,
existing.FileHash,
true);
if (IsDuplicate(createAsset, existing))
{
result = new AssetCreatedResult(
existing.Id,
existing.Tags,
existing.Version,
existing.FileVersion,
existing.FileHash,
true);
}
break;
}
else
if (result == null)
{
foreach (var tagGenerator in tagGenerators)
{
@ -147,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
await assetStore.UploadAsync(context.ContextId.ToString(), hashStream);
hash = hashStream.GetHashStringAndReset();
hash = $"{hashStream.GetHashStringAndReset()}{file.FileName}{file.FileSize}".Sha256Base64();
}
return hash;

19
src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs

@ -38,14 +38,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
this.tagService = tagService;
}
public virtual Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id)
public Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id)
{
Guard.NotNull(context, nameof(context));
return FindAssetAsync(context.App.Id, id);
}
public virtual async Task<IAssetEntity> FindAssetAsync(Guid appId, Guid id)
public async Task<IAssetEntity> FindAssetAsync(Guid appId, Guid id)
{
var asset = await assetRepository.FindAssetAsync(id);
@ -57,19 +57,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
return asset;
}
public virtual async Task<IAssetEntity> FindAssetByHashAsync(Guid appId, string hash)
public async Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash)
{
var asset = await assetRepository.FindAssetByHashAsync(appId, hash);
Guard.NotNull(hash, nameof(hash));
if (asset != null)
{
await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1));
}
var assets = await assetRepository.QueryByHashAsync(appId, hash);
return asset;
await DenormalizeTagsAsync(appId, assets);
return assets;
}
public virtual async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query)
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));

3
src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetQueryService
{
Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Q query);
Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id);

4
src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetRepository
{
Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
@ -23,8 +25,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
Task<IAssetEntity> FindAssetBySlugAsync(Guid appId, string slug);
Task<IAssetEntity> FindAssetByHashAsync(Guid appId, string hash);
Task RemoveAsync(Guid appId);
}
}

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

@ -101,10 +101,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = "slug",
Name = "fileHash",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Slug),
Description = "The file name as slug."
Resolver = Resolve(x => x.FileHash),
Description = "The hash of the file. Can be null for old files."
});
AddField(new FieldType
@ -131,6 +131,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "The version of the file."
});
AddField(new FieldType
{
Name = "slug",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Slug),
Description = "The file name as slug."
});
AddField(new FieldType
{
Name = "isImage",

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

@ -108,7 +108,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
}
else
{
entity = await assetRepository.FindAssetByHashAsync(App.Id, idOrSlug);
entity = await assetRepository.FindAssetBySlugAsync(App.Id, idOrSlug);
}
return DeliverAsset(entity, version, width, height, quality, mode);

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

@ -40,7 +40,7 @@
<div *ngSwitchCase="'Completed'" class="backup-status backup-status-success">
<i class="icon-checkmark"></i>
</div>
<div *NgSwitchDefault class="backup-status backup-status-pending spin">
<div *ngSwitchDefault class="backup-status backup-status-pending spin">
<i class="icon-hour-glass"></i>
</div>
</div>

8
src/Squidex/app/framework/utils/rxjs-extensions.ts

@ -52,4 +52,12 @@ export function switchSafe<T, R>(project: (source: T) => Observable<R>) {
return function mapOperation(source: Observable<T>) {
return source.pipe(switchMap(project), onErrorResumeNext<R, R>());
};
}
export function ofForever<T>(...values: T[]) {
return new Observable<T>(s => {
for (let value of values) {
s.next(value);
}
});
}

15
src/Squidex/app/shared/components/asset-uploader.component.html

@ -12,12 +12,23 @@
No upload in progress, drop files here.
</small>
<div class="upload row no-gutters" *ngFor="let upload of uploads">
<div class="upload row no-gutters" *ngFor="let upload of uploads; trackBy: trackByUpload">
<div class="col-auto" [ngSwitch]="upload.status">
<div *ngSwitchCase="'Failed'" class="upload-status upload-status-failed">
<i class="icon-exclamation"></i>
</div>
<div *ngSwitchCase="'Completed'" class="upload-status upload-status-success">
<i class="icon-checkmark"></i>
</div>
<div *ngSwitchDefault class="upload-status upload-status-running">
<i class="icon-hour-glass"></i>
</div>
</div>
<div class="col-6">
<div class="upload-name">{{upload.name}}</div>
</div>
<div class="col">
<sqx-progress-bar [value]="upload.progress" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false" [animated]="false"></sqx-progress-bar>
<sqx-progress-bar [value]="upload.progress" [trailWidth]="1.5" [strokeWidth]="1.5" [showText]="false" [animated]="false"></sqx-progress-bar>
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-secondary" (click)="stopUpload(upload)">

32
src/Squidex/app/shared/components/asset-uploader.component.scss

@ -37,10 +37,12 @@
.uploads {
& {
border: 2px solid transparent;
background: none;
min-height: 2rem;
}
&-empty {
line-height: 2rem;
line-height: 1.8rem;
}
}
@ -63,6 +65,34 @@
}
}
$circle-size: 1.6rem;
.upload-status {
& {
@include circle($circle-size);
display: inline-block;
line-height: $circle-size + .1rem;
text-align: center;
font-size: .4 * $circle-size;
font-weight: normal;
background: $color-border;
color: $color-dark-foreground;
cursor: none;
}
&-running {
color: inherit;
}
&-failed {
background: $color-theme-error;
}
&-success {
background: $color-theme-green;
}
}
.drag {
& > .uploads {
border-color: $color-theme-blue;

4
src/Squidex/app/shared/components/asset-uploader.component.ts

@ -44,4 +44,8 @@ export class AssetUploaderComponent {
public stopUpload(upload: Upload) {
this.assetUploader.stopUpload(upload);
}
public trackByUpload(index: number, upload: Upload) {
return upload.id;
}
}

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

@ -1,4 +1,4 @@
<ng-container *ngIf="!isListView; else listTemplate">
<ng-container *ngIf="!isListView; else listTemplate">
<div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="emitSelect()" (sqxFileDrop)="updateFile($event)" [noDrop]="true">
<div class="card-body">
<div class="file-preview" *ngIf="asset && snapshot.progress === 0" @fade>

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

@ -9,7 +9,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Ho
import {
AssetDto,
AuthService,
DialogModel,
DialogService,
fadeAnimation,
@ -113,7 +112,7 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
if (files.length === 1) {
this.setProgress(1);
this.assetUploader.uploadUpdate(this.asset, files[0])
this.assetUploader.uploadAsset(this.asset, files[0])
.subscribe(dto => {
if (Types.isNumber(dto)) {
this.setProgress(dto);
@ -123,6 +122,8 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
}, error => {
this.dialogs.notifyError(error);
this.setProgress(0);
}, () => {
this.setProgress(0);
});
}

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

@ -15,21 +15,20 @@
</div>
<div class="row assets" [class.unrow]="isListView" *ngIf="state.tagsNames | async; let tags" (paste)="addFiles($event)">
<ng-container *ngIf="state.uploads | async; let uploads">
<sqx-asset *ngFor="let upload of uploads; trackBy: trackByUpload" [upload]="upload"
[isListView]="isListView">
</sqx-asset>
</ng-container>
<sqx-asset *ngFor="let file of newFiles" [initFile]="file"
[isListView]="isListView"
(loadError)="remove(file)"
(load)="add(file, $event)">
</sqx-asset>
<ng-container *ngIf="state.assetsWithUploads | async; let assets">
<sqx-asset *ngFor="let a of assets; trackBy: trackByAsset" [asset]="a.asset" [upload]="a.upload"
<ng-container *ngIf="state.assets | async; let assets">
<sqx-asset *ngFor="let asset of assets; trackBy: trackByAsset" [asset]="asset"
[isListView]="isListView"
[isDisabled]="isDisabled"
[isSelectable]="selectedIds"
[isSelected]="isSelected(a.asset)"
[isSelected]="isSelected(asset)"
[allTags]="tags"
(update)="update($event)"
(uploadFile)="updateFile(a.asset, $event)"
(select)="emitSelect($event)"
(delete)="delete($event)">
</sqx-asset>

211
src/Squidex/app/shared/state/asset-uploader.state.spec.ts

@ -0,0 +1,211 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { never, of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { IMock, Mock } from 'typemoq';
import {
AssetUploaderState,
DialogService,
ofForever,
Types,
Version,
versioned
} from './../';
import { AssetDto, AssetReplacedDto, AssetsService } from '../services/assets.service';
import { TestValues } from './_test-helpers';
describe('AssetsState', () => {
const {
app,
appsState,
authService,
creator,
creation,
modified,
modifier,
now,
userId
} = TestValues;
let assetsService: IMock<AssetsService>;
let dialogs: IMock<DialogService>;
let assetUploader: AssetUploaderState;
const asset = new AssetDto('id1',
creator,
creator,
creation,
creation,
'my-asset',
'my-hash',
'png',
100,
1,
'image/png',
true,
true,
800,
600,
'my-slug',
[],
'http://url',
new Version('1'));
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
assetsService = Mock.ofType<AssetsService>();
assetUploader = new AssetUploaderState(appsState.object, assetsService.object, authService.object, dialogs.object);
});
afterEach(() => {
assetsService.verifyAll();
});
it('should create initial state when uploading file', () => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, userId, now))
.returns(() => never()).verifiable();
assetUploader.uploadFile(file, undefined, now).subscribe();
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Running');
expect(upload.progress).toBe(1);
});
it('should update progress when uploading file makes progress', () => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, userId, now))
.returns(() => ofForever(10, 20)).verifiable();
assetUploader.uploadFile(file, undefined, now).subscribe();
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Running');
expect(upload.progress).toBe(20);
});
it('should update status when uploading file failed', () => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, userId, now))
.returns(() => throwError('Error')).verifiable();
assetUploader.uploadFile(file, undefined, now).pipe(onErrorResumeNext()).subscribe();
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Failed');
expect(upload.progress).toBe(1);
});
it('should update status when uploading file completes', (cb) => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, userId, now))
.returns(() => of(10, 20, asset)).verifiable();
let uploadedAsset: AssetDto;
assetUploader.uploadFile(file, undefined, now).subscribe(dto => {
if (Types.is(dto, AssetDto)) {
uploadedAsset = dto;
}
cb();
});
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Completed');
expect(upload.progress).toBe(100);
expect(uploadedAsset!).toBe(asset);
});
it('should create initial state when uploading asset', () => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.replaceFile(app, asset.id, file, asset.version))
.returns(() => never()).verifiable();
assetUploader.uploadAsset(asset, file, now).subscribe();
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Running');
expect(upload.progress).toBe(1);
});
it('should update progress when uploading asset makes progress', () => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.replaceFile(app, asset.id, file, asset.version))
.returns(() => ofForever(10, 20)).verifiable();
assetUploader.uploadAsset(asset, file, now).subscribe();
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Running');
expect(upload.progress).toBe(20);
});
it('should update status when uploading asset failed', () => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.replaceFile(app, asset.id, file, asset.version))
.returns(() => throwError('Error')).verifiable();
assetUploader.uploadAsset(asset, file, now).pipe(onErrorResumeNext()).subscribe();
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Failed');
expect(upload.progress).toBe(1);
});
it('should update status when uploading asset completes', () => {
const file: File = <any>{ name: 'my-file' };
let update: AssetReplacedDto = {
isImage: true,
mimeType: 'image/jpeg',
pixelWidth: 800,
pixelHeight: 600,
fileHash: 'my-hash2',
fileSize: 200,
fileVersion: 2
};
const newAsset = asset.update(update, modifier, new Version('2'), modified);
assetsService.setup(x => x.replaceFile(app, asset.id, file, asset.version))
.returns(() => of(10, 20, versioned(new Version('2'), update))).verifiable();
let uploadedAsset: AssetDto;
assetUploader.uploadAsset(asset, file, modified).subscribe(dto => {
if (Types.is(dto, AssetDto)) {
uploadedAsset = dto;
}
});
const upload = assetUploader.snapshot.uploads.at(0);
expect(upload.status).toBe('Completed');
expect(upload.progress).toBe(100);
expect(uploadedAsset!).toEqual(newAsset);
});
});

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

@ -6,8 +6,8 @@
*/
import { Injectable } from '@angular/core';
import { Observable, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { Observable, ReplaySubject, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
import {
DateTime,
@ -31,7 +31,10 @@ export interface Upload {
name: string;
// The upload subscription.
subscription: Subscription;
subscription?: Subscription;
// The subject to notify subscribers.
subject: ReplaySubject<UploadResult>;
// The progress.
progress: number;
@ -64,7 +67,10 @@ export class AssetUploaderState extends State<Snapshot> {
}
public stopUpload(upload: Upload) {
upload.subscription.unsubscribe();
if (upload.subscription) {
upload.subscription.unsubscribe();
upload.subject.complete();
}
this.next(s => {
const uploads = s.uploads.removeBy('id', upload);
@ -74,76 +80,61 @@ export class AssetUploaderState extends State<Snapshot> {
}
public uploadFile(file: File, target?: AssetsState, now?: DateTime): Observable<UploadResult> {
const observable = this.assetsService.uploadFile(this.appName, file, this.user, now || DateTime.now());
let upload: Upload;
const subject = new Subject<UploadResult>();
const subscription = observable.subscribe(event => {
if (Types.isNumber(event)) {
this.update(upload, { progress: event });
} else {
if (event.isDuplicate) {
this.dialogs.notifyError('Asset has already been uploaded.');
}
const stream = this.assetsService.uploadFile(this.appName, file, this.user, now || DateTime.now());
if (target) {
target.add(event);
}
return this.upload(stream, MathHelper.guid(), file, (asset, subject) => {
if (asset.isDuplicate) {
this.dialogs.notifyError('Asset has already been uploaded.');
} else if (target) {
target.add(asset);
}
subject.next(event);
}, error => {
subject.error(error);
this.remove(upload, 'failed');
}, () => {
subject.complete();
this.remove(upload, 'completed');
subject.next(asset);
});
}
upload = { id: MathHelper.guid(), name: file.name, progress: 1, subscription, status: 'running' };
public uploadAsset(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> {
const stream = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version);
this.addUpload(upload);
return subject;
return this.upload(stream, asset.id, file, ({ version, payload }, subject) => {
subject.next(asset.update(payload, this.user, version, now));
});
}
public uploadUpdate(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> {
const observable = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version);
private upload<T, R>(stream: Observable<number | R>, id: string, file: File, complete: (completion: R, subject: Subject<UploadResult>) => void) {
const subject = new ReplaySubject<UploadResult>();
let upload = { id, name: file.name, progress: 1, status: 'Running', subject };
let upload: Upload;
this.addUpload(upload);
const subject = new Subject<UploadResult>();
const subscription = observable.subscribe(event => {
const subscription = stream.subscribe(event => {
if (Types.isNumber(event)) {
this.update(upload, { progress: event });
upload = this.update(upload, { progress: event });
subject.next(event);
} else {
subject.next(asset.update(event.payload, this.user, event.version, now));
complete(event, subject);
}
}, error => {
subject.error(error);
this.remove(upload, 'failed');
upload = this.remove(upload, { status: 'Failed' });
}, () => {
subject.complete();
if (!subject.isStopped) {
subject.complete();
}
this.remove(upload, 'completed');
upload = this.remove(upload, { status: 'Completed', progress: 100 });
});
upload = { id: asset.id, name: file.name, progress: 1, subscription, status: 'running' };
upload = this.update(upload, { subscription });
this.addUpload(upload);
return subject;
return subject.pipe(shareReplay());
}
private remove(upload: Upload, status: string) {
this.update(upload, { status });
private remove(upload: Upload, update: Partial<Upload>) {
upload = this.update(upload, update);
setTimeout(() => {
this.next(s => {
@ -153,14 +144,20 @@ export class AssetUploaderState extends State<Snapshot> {
});
}, 10000);
return upload;
}
private update(upload: Upload, update: Partial<Upload>) {
upload = { ...upload, ...update };
this.next(s => {
const uploads = s.uploads.replaceBy('id', { ...upload, ...update });
const uploads = s.uploads.replaceBy('id', upload);
return { ...s, uploads };
});
return upload;
}
private addUpload(upload: Upload) {

2
src/Squidex/app/shared/state/roles.state.ts

@ -64,7 +64,7 @@ export class RolesState extends State<Snapshot> {
}
return this.rolesService.getRoles(this.appName).pipe(
tap(({ payload, version }) => {
tap(({ version, payload }) => {
if (isReload) {
this.dialogs.notifyInfo('Roles reloaded.');
}

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

@ -271,7 +271,7 @@ export class SchemasState extends State<Snapshot> {
public lockField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.lockField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
mapVersioned(() => setLocked(field, true)),
tap(({ payload, version }) => {
tap(({ version, payload }) => {
this.replaceField(schema, payload, version, now);
}),
shareSubscribed(this.dialogs, { project: x => x.payload }));
@ -280,7 +280,7 @@ export class SchemasState extends State<Snapshot> {
public enableField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.enableField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
mapVersioned(() => setDisabled(field, false)),
tap(({ payload, version }) => {
tap(({ version, payload }) => {
this.replaceField(schema, payload, version, now);
}),
shareSubscribed(this.dialogs, { project: x => x.payload }));
@ -289,7 +289,7 @@ export class SchemasState extends State<Snapshot> {
public disableField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.disableField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
mapVersioned(() => setDisabled(field, true)),
tap(({ payload, version }) => {
tap(({ version, payload }) => {
this.replaceField(schema, payload, version, now);
}),
shareSubscribed(this.dialogs, { project: x => x.payload }));
@ -298,7 +298,7 @@ export class SchemasState extends State<Snapshot> {
public showField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.showField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
mapVersioned(() => setHidden(field, false)),
tap(({ payload, version }) => {
tap(({ version, payload }) => {
this.replaceField(schema, payload, version, now);
}),
shareSubscribed(this.dialogs, { project: x => x.payload }));
@ -307,7 +307,7 @@ export class SchemasState extends State<Snapshot> {
public hideField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.hideField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
mapVersioned(() => setHidden(field, true)),
tap(({ payload, version }) => {
tap(({ version, payload }) => {
this.replaceField(schema, payload, version, now);
}),
shareSubscribed(this.dialogs, { project: x => x.payload }));
@ -316,7 +316,7 @@ export class SchemasState extends State<Snapshot> {
public updateField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, request: UpdateFieldDto, now?: DateTime): Observable<T> {
return this.schemasService.putField(this.appName, schema.name, field.fieldId, request, pidof(field), schema.version).pipe(
mapVersioned(() => update(field, request.properties)),
tap(({ payload, version }) => {
tap(({ version, payload }) => {
this.replaceField(schema, payload, version, now);
}),
shareSubscribed(this.dialogs, { project: x => x.payload }));
@ -325,7 +325,7 @@ export class SchemasState extends State<Snapshot> {
public deleteField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> {
return this.schemasService.deleteField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
mapVersioned(() => field),
tap(({ payload, version }) => {
tap(({ version, payload }) => {
this.removeField(schema, payload, version, now);
}),
shareSubscribed(this.dialogs));

1027
src/Squidex/package-lock.json

File diff suppressed because it is too large

28
src/Squidex/package.json

@ -15,15 +15,15 @@
"build:clean": "rimraf wwwroot/build"
},
"dependencies": {
"@angular/animations": "7.2.14",
"@angular/common": "7.2.14",
"@angular/core": "7.2.14",
"@angular/forms": "7.2.14",
"@angular/http": "7.2.14",
"@angular/platform-browser": "7.2.14",
"@angular/platform-browser-dynamic": "7.2.14",
"@angular/platform-server": "7.2.14",
"@angular/router": "7.2.14",
"@angular/animations": "7.2.3",
"@angular/common": "7.2.3",
"@angular/core": "7.2.3",
"@angular/forms": "7.2.3",
"@angular/http": "7.2.3",
"@angular/platform-browser": "7.2.3",
"@angular/platform-browser-dynamic": "7.2.3",
"@angular/platform-server": "7.2.3",
"@angular/router": "7.2.3",
"angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0",
"bootstrap": "4.3.1",
@ -34,7 +34,7 @@
"moment": "2.24.0",
"mousetrap": "1.6.3",
"ng2-dnd": "5.0.2",
"ngx-color-picker": "7.5.0",
"ngx-color-picker": "7.3.0",
"oidc-client": "1.7.1",
"pikaday": "1.8.0",
"progressbar.js": "1.0.1",
@ -47,9 +47,9 @@
"zone.js": "0.9.0"
},
"devDependencies": {
"@angular/compiler": "7.2.14",
"@angular/compiler-cli": "7.2.14",
"@ngtools/webpack": "7.3.8",
"@angular/compiler": "7.2.3",
"@angular/compiler-cli": "7.2.3",
"@ngtools/webpack": "7.3.0",
"@types/core-js": "2.5.0",
"@types/jasmine": "3.3.12",
"@types/marked": "0.6.5",
@ -84,7 +84,7 @@
"mini-css-extract-plugin": "0.6.0",
"node-sass": "4.12.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"raw-loader": "2.0.0",
"raw-loader": "1.0.0",
"rimraf": "2.6.3",
"rxjs-tslint": "0.1.7",
"sass-lint": "1.13.1",

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

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetCommandMiddlewareTests : HandlerTestBase<AssetState>
{
private readonly AssetQueryService assetQueryService = A.Fake<AssetQueryService>();
private readonly IAssetQueryService assetQueryService = A.Fake<IAssetQueryService>();
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetStore assetStore = A.Fake<MemoryAssetStore>();
private readonly ITagService tagService = A.Fake<ITagService>();
@ -52,6 +52,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
asset = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>());
asset.ActivateAsync(Id).Wait();
A.CallTo(() => assetQueryService.QueryByHashAsync(AppId, A<string>.Ignored))
.Returns(new List<IAssetEntity>());
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>());
@ -202,8 +205,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => temp.FileName).Returns(fileName);
A.CallTo(() => temp.FileSize).Returns(fileSize);
A.CallTo(() => assetQueryService.FindAssetByHashAsync(A<Guid>.Ignored, A<string>.Ignored))
.Returns(existing);
A.CallTo(() => assetQueryService.QueryByHashAsync(A<Guid>.Ignored, A<string>.Ignored))
.Returns(new List<IAssetEntity> { existing });
}
private void SetupImageInfo()

8
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs

@ -70,12 +70,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var id = Guid.NewGuid();
A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash"))
.Returns(CreateAsset(id, "id1", "id2", "id3"));
A.CallTo(() => assetRepository.QueryByHashAsync(appId.Id, "hash"))
.Returns(new List<IAssetEntity> { CreateAsset(id, "id1", "id2", "id3") });
var result = await sut.FindAssetByHashAsync(appId.Id, "hash");
var result = await sut.QueryByHashAsync(appId.Id, "hash");
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags);
Assert.Equal(HashSet.Of("name1", "name2", "name3"), result[0].Tags);
}
[Fact]

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

@ -51,6 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl
mimeType
fileName
fileHash
fileSize
fileVersion
isImage
@ -86,6 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileHash = "ABC123",
fileSize = 1024,
fileVersion = 123,
isImage = true,
@ -119,6 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl
mimeType
fileName
fileHash
fileSize
fileVersion
isImage
@ -158,6 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileHash = "ABC123",
fileSize = 1024,
fileVersion = 123,
isImage = true,
@ -193,6 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl
mimeType
fileName
fileHash
fileSize
fileVersion
isImage
@ -224,6 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileHash = "ABC123",
fileSize = 1024,
fileVersion = 123,
isImage = true,

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

@ -172,6 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
FileName = "MyFile.png",
Slug = "myfile.png",
FileSize = 1024,
FileHash = "ABC123",
FileVersion = 123,
MimeType = "image/png",
IsImage = true,

Loading…
Cancel
Save