Browse Source

Track by

pull/356/head
Sebastian 7 years ago
parent
commit
e575999914
  1. 2
      src/Squidex/app/features/apps/pages/apps-page.component.html
  2. 5
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  3. 2
      src/Squidex/app/features/apps/pages/news-dialog.component.html
  4. 4
      src/Squidex/app/features/apps/pages/news-dialog.component.ts
  5. 2
      src/Squidex/app/features/content/pages/content/content-page.component.html
  6. 5
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  7. 2
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html
  8. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  9. 4
      src/Squidex/app/features/content/shared/content-item.component.ts
  10. 2
      src/Squidex/app/features/content/shared/references-editor.component.html
  11. 4
      src/Squidex/app/features/content/shared/references-editor.component.ts
  12. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  13. 4
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  14. 2
      src/Squidex/app/features/settings/pages/clients/client.component.html
  15. 4
      src/Squidex/app/features/settings/pages/clients/client.component.ts
  16. 30
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  17. 4
      src/Squidex/app/shared/components/language-selector.component.html
  18. 5
      src/Squidex/app/shared/components/language-selector.component.ts
  19. 2
      src/Squidex/app/shared/components/pipes.ts
  20. 55
      src/Squidex/app/shared/services/assets.service.spec.ts
  21. 41
      src/Squidex/app/shared/services/assets.service.ts
  22. 39
      src/Squidex/app/shared/state/asset-uploader.state.spec.ts
  23. 48
      src/Squidex/app/shared/state/asset-uploader.state.ts
  24. 2
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  25. 5
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  26. 2
      src/Squidex/tslint.json

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

@ -14,7 +14,7 @@
<h3 class="empty-headline">You are not collaborating to any app yet</h3> <h3 class="empty-headline">You are not collaborating to any app yet</h3>
</div> </div>
<div class="card card-href card-app float-left" *ngFor="let app of apps" [routerLink]="['/app', app.name]"> <div class="card card-href card-app float-left" *ngFor="let app of apps; trackBy: trackByApp" [routerLink]="['/app', app.name]">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">{{app.name}}</h4> <h4 class="card-title">{{app.name}}</h4>

5
src/Squidex/app/features/apps/pages/apps-page.component.ts

@ -9,6 +9,7 @@ import { Component, OnInit } from '@angular/core';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { import {
AppDto,
AppsState, AppsState,
AuthService, AuthService,
DialogModel, DialogModel,
@ -77,4 +78,8 @@ export class AppsPageComponent implements OnInit {
public stop(event: Event) { public stop(event: Event) {
event.stopPropagation(); event.stopPropagation();
} }
public trackByApp(index: number, app: AppDto) {
return app.id;
}
} }

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

@ -7,7 +7,7 @@
<div class="help"> <div class="help">
<h1>What's new?</h1> <h1>What's new?</h1>
<div *ngFor="let feature of features"> <div *ngFor="let feature of features; trackBy: trackByFeature">
<h4>{{feature.name}}</h4> <h4>{{feature.name}}</h4>
<div [innerHTML]="feature.text | sqxHelpMarkdown"></div> <div [innerHTML]="feature.text | sqxHelpMarkdown"></div>

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

@ -24,4 +24,8 @@ export class NewsDialogComponent {
public emitClose() { public emitClose() {
this.close.emit(); this.close.emit();
} }
public trackByFeature(index: number, feature: FeatureDto) {
return feature;
}
} }

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

@ -111,7 +111,7 @@
Viewing <strong>version {{version.value}}</strong>. Viewing <strong>version {{version.value}}</strong>.
</div> </div>
<div *ngFor="let field of schema.fields"> <div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)">
<sqx-content-field <sqx-content-field
[form]="contentForm" [form]="contentForm"
[field]="field" [field]="field"

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

@ -21,6 +21,7 @@ import {
DialogService, DialogService,
EditContentForm, EditContentForm,
fadeAnimation, fadeAnimation,
FieldDto,
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
MessageBus, MessageBus,
@ -241,4 +242,8 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
public showLatest() { public showLatest() {
this.loadVersion(null, false); this.loadVersion(null, false);
} }
public trackByField(index: number, field: FieldDto) {
return field.fieldId + this.schema.id;
}
} }

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

@ -14,7 +14,7 @@
<div class="sidebar-section"> <div class="sidebar-section">
<h3>Saved queries</h3> <h3>Saved queries</h3>
<a class="sidebar-item" *ngFor="let query of schemaQueries.queries | async; trackByQuery" (click)="search(query.filter)" <a class="sidebar-item" *ngFor="let query of schemaQueries.queries | async; trackBy: trackByQuery" (click)="search(query.filter)"
[class.active]="isSelectedQuery(query.filter)"> [class.active]="isSelectedQuery(query.filter)">
{{query.name}} {{query.name}}

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

@ -10,7 +10,7 @@
</ng-template> </ng-template>
</td> </td>
<td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event, field)"> <td class="cell-auto" *ngFor="let field of schema.listFields; let i = index; trackBy: trackByField.bind(this)" (click)="shouldStop($event, field)">
<ng-container *ngIf="field.isInlineEditable && !isReadOnly; else displayTemplate"> <ng-container *ngIf="field.isInlineEditable && !isReadOnly; else displayTemplate">
<sqx-content-item-editor [form]="patchForm.form" [field]="field"></sqx-content-item-editor> <sqx-content-item-editor [form]="patchForm.form" [field]="field"></sqx-content-item-editor>
</ng-container> </ng-container>

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

@ -187,5 +187,9 @@ export class ContentItemComponent implements OnChanges {
return undefined; return undefined;
} }
public trackByField(index: number, field: FieldDto) {
return field.fieldId + this.schema.id;
}
} }

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

@ -9,7 +9,7 @@
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.schema && snapshot.contentItems && snapshot.contentItems.length > 0" <table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.schema && snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems.mutableValues" [sqxSortModel]="snapshot.contentItems.mutableValues"
(sqxSort)="sort($event)"> (sqxSort)="sort($event)">
<tbody *ngFor="let content of snapshot.contentItems"> <tbody *ngFor="let content of snapshot.contentItems; trackBy: trackByContent">
<tr [sqxContent]="content" <tr [sqxContent]="content"
[language]="language" [language]="language"
[isReadOnly]="true" [isReadOnly]="true"

4
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -144,4 +144,8 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, s
this.callTouched(); this.callTouched();
this.callChange(ids); this.callChange(ids);
} }
public trackByContent(index: number, content: ContentDto) {
return content.id;
}
} }

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -38,7 +38,7 @@
<form class="form-inline" (ngSubmit)="addSchema()"> <form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1"> <div class="form-group mr-1">
<select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema"> <select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd" [ngValue]="schema">{{schema.displayName}}</option> <option *ngFor="let schema of schemasToAdd; trackBy: trackBySchema" [ngValue]="schema">{{schema.displayName}}</option>
</select> </select>
</div> </div>

4
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -103,4 +103,8 @@ export class ContentChangedTriggerComponent implements OnInit {
this.schemasToAdd = this.schemas.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortByStringAsc(x => x.name); this.schemasToAdd = this.schemas.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortByStringAsc(x => x.name);
this.schemaToAdd = this.schemasToAdd.at(0); this.schemaToAdd = this.schemasToAdd.at(0);
} }
public trackBySchema(index: number, schema: SchemaDto) {
return schema.id;
}
} }

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

@ -71,7 +71,7 @@
</label> </label>
<div class="col cell-input"> <div class="col cell-input">
<select class="form-control" [ngModel]="client.role" (ngModelChange)="update($event)"> <select class="form-control" [ngModel]="client.role" (ngModelChange)="update($event)">
<option *ngFor="let role of clientRoles" [ngValue]="role.name">{{role.name}}</option> <option *ngFor="let role of clientRoles; trackBy: trackByRole" [ngValue]="role.name">{{role.name}}</option>
</select> </select>
</div> </div>
<div class="col-auto cell-actions"></div> <div class="col-auto cell-actions"></div>

4
src/Squidex/app/features/settings/pages/clients/client.component.ts

@ -111,6 +111,10 @@ export class ClientComponent implements OnChanges {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
}); });
} }
public trackByRole(index: number, role: RoleDto) {
return role.name;
}
} }
function connectHttpText(apiUrl: ApiUrlConfig, app: string, client: { id: string, secret: string }) { function connectHttpText(apiUrl: ApiUrlConfig, app: string, client: { id: string, secret: string }) {

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

@ -5,6 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:template-use-track-by-function
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
@ -362,32 +364,36 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
public onCopy(event: ClipboardEvent) { public onCopy(event: ClipboardEvent) {
if (!this.hasSelection()) { if (!this.hasSelection()) {
event.clipboardData.setData('text/plain', this.snapshot.items.filter(x => !!x).join(',')); if (event.clipboardData) {
event.clipboardData.setData('text/plain', this.snapshot.items.filter(x => !!x).join(','));
}
event.preventDefault(); event.preventDefault();
} }
} }
public onPaste(event: ClipboardEvent) { public onPaste(event: ClipboardEvent) {
const value = event.clipboardData.getData('text/plain'); if (event.clipboardData) {
const value = event.clipboardData.getData('text/plain');
if (value) { if (value) {
this.resetForm(); this.resetForm();
const values = [...this.snapshot.items]; const values = [...this.snapshot.items];
for (let part of value.split(',')) { for (let part of value.split(',')) {
const converted = this.converter.convert(part); const converted = this.converter.convert(part);
if (converted) { if (converted) {
values.push(converted); values.push(converted);
}
} }
this.updateItems(values);
} }
this.updateItems(values); event.preventDefault();
} }
event.preventDefault();
} }
private hasSelection() { private hasSelection() {

4
src/Squidex/app/shared/components/language-selector.component.html

@ -1,5 +1,5 @@
<div class="btn-group btn-group-{{size}}" *ngIf="isSmallMode"> <div class="btn-group btn-group-{{size}}" *ngIf="isSmallMode">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" title="{{language.englishName}}" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)" tabindex="-1"> <button type="button" class="btn btn-secondary" *ngFor="let language of languages; trackBy: trackByLanguage" title="{{language.englishName}}" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)" tabindex="-1">
<span class="iso-code">{{language.iso2Code}}</span> <span class="iso-code">{{language.iso2Code}}</span>
</button> </button>
</div> </div>
@ -9,7 +9,7 @@
{{selectedLanguage.iso2Code}} {{selectedLanguage.iso2Code}}
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="button" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="button" @fade>
<div class="dropdown-item" *ngFor="let language of languages" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)"> <div class="dropdown-item" *ngFor="let language of languages; trackBy: trackByLanguage" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
<strong class="iso-code iso-code-dropdown">{{language.iso2Code}}</strong> ({{language.englishName}}) <strong class="iso-code iso-code-dropdown">{{language.iso2Code}}</strong> ({{language.englishName}})
</div> </div>
</div> </div>

5
src/Squidex/app/shared/components/language-selector.component.ts

@ -8,6 +8,7 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { fadeAnimation, ModalModel } from '@app/shared/internal'; import { fadeAnimation, ModalModel } from '@app/shared/internal';
import { LanguageDto } from '../services/languages.service';
export interface Language { iso2Code: string; englishName: string; isMasterLanguage: true; } export interface Language { iso2Code: string; englishName: string; isMasterLanguage: true; }
@ -65,4 +66,8 @@ export class LanguageSelectorComponent implements OnChanges, OnInit {
this.selectedLanguage = language; this.selectedLanguage = language;
this.selectedLanguageChange.emit(language); this.selectedLanguageChange.emit(language);
} }
public trackByLanguage(index: number, language: LanguageDto) {
return language.iso2Code;
}
} }

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

@ -5,6 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable:no-pipe-impure
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core'; import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';

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

@ -20,6 +20,7 @@ import {
Version, Version,
Versioned Versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
import { AssetUploadedDto } from './assets.service';
describe('AssetDto', () => { describe('AssetDto', () => {
const creation = DateTime.today(); const creation = DateTime.today();
@ -71,8 +72,6 @@ describe('AssetDto', () => {
}); });
describe('AssetsService', () => { describe('AssetsService', () => {
const now = DateTime.now();
const user = 'me';
const version = new Version('1'); const version = new Version('1');
beforeEach(() => { beforeEach(() => {
@ -315,10 +314,10 @@ describe('AssetsService', () => {
it('should make post request to create asset', it('should make post request to create asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let asset: AssetDto; let asset: Versioned<AssetUploadedDto>;
assetsService.uploadFile('my-app', null!, user, now).subscribe(result => { assetsService.uploadFile('my-app', null!).subscribe(result => {
asset = <AssetDto>result; asset = <Versioned<AssetUploadedDto>>result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets');
@ -342,40 +341,38 @@ describe('AssetsService', () => {
tags: ['tag1', 'tag2'] tags: ['tag1', 'tag2']
}, { }, {
headers: { headers: {
etag: '2' etag: '1'
} }
}); });
expect(asset!).toEqual( expect(asset!).toEqual({
new AssetDto( payload: {
'id1', id: 'id1',
user, fileName: 'My Asset1.png',
user, fileHash: 'My Hash1',
now, fileType: 'png',
now, fileSize: 1024,
'My Asset1.png', fileVersion: 2,
'My Hash1', mimeType: 'image/png',
'png', isDuplicate: true,
1024, 2, isImage: true,
'image/png', pixelWidth: 1024,
true, pixelHeight: 2048,
true, slug: 'my-asset1.png',
1024, tags: ['tag1', 'tag2']
2048, },
'my-asset1.png', version
['tag1', 'tag2'], });
'http://service/p/api/assets/id1',
new Version('2')));
})); }));
it('should return proper error when upload failed with 413', it('should return proper error when upload failed with 413',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let asset: AssetDto; let asset: Versioned<AssetUploadedDto>;
let error: ErrorDto; let error: ErrorDto;
assetsService.uploadFile('my-app', null!, user, now).subscribe(result => { assetsService.uploadFile('my-app', null!).subscribe(result => {
asset = <AssetDto>result; asset = <Versioned<AssetUploadedDto>>result;
}, e => { }, e => {
error = e; error = e;
}); });

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

@ -46,8 +46,8 @@ export class AssetDto extends Model<AssetDto> {
public readonly mimeType: string, public readonly mimeType: string,
public readonly isDuplicate: boolean, public readonly isDuplicate: boolean,
public readonly isImage: boolean, public readonly isImage: boolean,
public readonly pixelWidth: number | null, public readonly pixelWidth: number | null | undefined,
public readonly pixelHeight: number | null, public readonly pixelHeight: number | null | undefined,
public readonly slug: string, public readonly slug: string,
public readonly tags: string[], public readonly tags: string[],
public readonly url: string, public readonly url: string,
@ -91,6 +91,15 @@ export interface AssetReplacedDto {
readonly pixelHeight?: number | null; readonly pixelHeight?: number | null;
} }
export interface AssetUploadedDto extends AssetReplacedDto {
readonly id: string;
readonly fileName: string;
readonly fileType: string;
readonly slug: string;
readonly isDuplicate: boolean;
readonly tags?: string[];
}
@Injectable() @Injectable()
export class AssetsService { export class AssetsService {
constructor( constructor(
@ -176,7 +185,7 @@ export class AssetsService {
pretifyError('Failed to load assets. Please reload.')); pretifyError('Failed to load assets. Please reload.'));
} }
public uploadFile(appName: string, file: File, user: string, now: DateTime): Observable<number | AssetDto> { public uploadFile(appName: string, file: File): Observable<number | Versioned<AssetUploadedDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets`);
const req = new HttpRequest('POST', url, getFormData(file), { reportProgress: true }); const req = new HttpRequest('POST', url, getFormData(file), { reportProgress: true });
@ -192,32 +201,8 @@ export class AssetsService {
return percentDone; return percentDone;
} else if (Types.is(event, HttpResponse)) { } else if (Types.is(event, HttpResponse)) {
const response: any = event.body; const response: any = event.body;
const assetUrl = this.apiUrl.buildUrl(`api/assets/${response.id}`);
now = now || DateTime.now();
const dto = new AssetDto(
response.id,
user,
user,
now,
now,
response.fileName,
response.fileHash,
response.fileType,
response.fileSize,
response.fileVersion,
response.mimeType,
response.isDuplicate,
response.isImage,
response.pixelWidth,
response.pixelHeight,
response.slug,
response.tags || [],
assetUrl,
new Version(event.headers.get('etag')!));
return dto; return versioned(new Version(event.headers.get('etag')!), response);
} else { } else {
throw 'Invalid'; throw 'Invalid';
} }

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

@ -10,6 +10,7 @@ import { onErrorResumeNext } from 'rxjs/operators';
import { IMock, Mock } from 'typemoq'; import { IMock, Mock } from 'typemoq';
import { import {
ApiUrlConfig,
AssetDto, AssetDto,
AssetReplacedDto, AssetReplacedDto,
AssetsService, AssetsService,
@ -28,10 +29,9 @@ describe('AssetsState', () => {
app, app,
appsState, appsState,
authService, authService,
creator,
creation,
modified, modified,
modifier modifier,
version
} = TestValues; } = TestValues;
let assetsService: IMock<AssetsService>; let assetsService: IMock<AssetsService>;
@ -39,10 +39,10 @@ describe('AssetsState', () => {
let assetUploader: AssetUploaderState; let assetUploader: AssetUploaderState;
const asset = new AssetDto('id1', const asset = new AssetDto('id1',
creator, modifier,
creator, modifier,
creation, modified,
creation, modified,
'my-asset', 'my-asset',
'my-hash', 'my-hash',
'png', 'png',
@ -55,14 +55,16 @@ describe('AssetsState', () => {
600, 600,
'my-slug', 'my-slug',
[], [],
'http://url', 'http://url/api/assets/id1',
new Version('1')); version);
beforeEach(() => { beforeEach(() => {
dialogs = Mock.ofType<DialogService>(); dialogs = Mock.ofType<DialogService>();
const apiUrl = new ApiUrlConfig('http://url');
assetsService = Mock.ofType<AssetsService>(); assetsService = Mock.ofType<AssetsService>();
assetUploader = new AssetUploaderState(appsState.object, assetsService.object, authService.object, dialogs.object); assetUploader = new AssetUploaderState(appsState.object, apiUrl, assetsService.object, authService.object, dialogs.object);
}); });
afterEach(() => { afterEach(() => {
@ -72,7 +74,7 @@ describe('AssetsState', () => {
it('should create initial state when uploading file', () => { it('should create initial state when uploading file', () => {
const file: File = <any>{ name: 'my-file' }; const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, modifier, modified)) assetsService.setup(x => x.uploadFile(app, file))
.returns(() => never()).verifiable(); .returns(() => never()).verifiable();
assetUploader.uploadFile(file, undefined, modified).subscribe(); assetUploader.uploadFile(file, undefined, modified).subscribe();
@ -86,7 +88,7 @@ describe('AssetsState', () => {
it('should update progress when uploading file makes progress', () => { it('should update progress when uploading file makes progress', () => {
const file: File = <any>{ name: 'my-file' }; const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, modifier, modified)) assetsService.setup(x => x.uploadFile(app, file))
.returns(() => ofForever(10, 20)).verifiable(); .returns(() => ofForever(10, 20)).verifiable();
assetUploader.uploadFile(file, undefined, modified).subscribe(); assetUploader.uploadFile(file, undefined, modified).subscribe();
@ -100,7 +102,7 @@ describe('AssetsState', () => {
it('should update status when uploading file failed', () => { it('should update status when uploading file failed', () => {
const file: File = <any>{ name: 'my-file' }; const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, modifier, modified)) assetsService.setup(x => x.uploadFile(app, file))
.returns(() => throwError('Error')).verifiable(); .returns(() => throwError('Error')).verifiable();
assetUploader.uploadFile(file, undefined, modified).pipe(onErrorResumeNext()).subscribe(); assetUploader.uploadFile(file, undefined, modified).pipe(onErrorResumeNext()).subscribe();
@ -114,8 +116,8 @@ describe('AssetsState', () => {
it('should update status when uploading file completes', (cb) => { it('should update status when uploading file completes', (cb) => {
const file: File = <any>{ name: 'my-file' }; const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, modifier, modified)) assetsService.setup(x => x.uploadFile(app, file))
.returns(() => of(10, 20, asset)).verifiable(); .returns(() => of(10, 20, versioned(version, { ...asset }))).verifiable();
let uploadedAsset: AssetDto; let uploadedAsset: AssetDto;
@ -131,7 +133,7 @@ describe('AssetsState', () => {
expect(upload.status).toBe('Completed'); expect(upload.status).toBe('Completed');
expect(upload.progress).toBe(100); expect(upload.progress).toBe(100);
expect(uploadedAsset!).toBe(asset); expect(uploadedAsset!).toEqual(asset);
}); });
it('should create initial state when uploading asset', () => { it('should create initial state when uploading asset', () => {
@ -189,10 +191,11 @@ describe('AssetsState', () => {
fileVersion: 2 fileVersion: 2
}; };
const newAsset = asset.update(update, modifier, new Version('2'), modified); const newVersion = new Version('2');
const newAsset = asset.update(update, modifier, newVersion, modified);
assetsService.setup(x => x.replaceFile(app, asset.id, file, asset.version)) assetsService.setup(x => x.replaceFile(app, asset.id, file, asset.version))
.returns(() => of(10, 20, versioned(new Version('2'), update))).verifiable(); .returns(() => of(10, 20, versioned(newVersion, update))).verifiable();
let uploadedAsset: AssetDto; let uploadedAsset: AssetDto;

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

@ -10,15 +10,17 @@ import { Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, publishReplay, refCount, takeUntil } from 'rxjs/operators'; import { distinctUntilChanged, map, publishReplay, refCount, takeUntil } from 'rxjs/operators';
import { import {
ApiUrlConfig,
DateTime, DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
MathHelper, MathHelper,
State, State,
Types Types,
Versioned
} from '@app/framework'; } from '@app/framework';
import { AssetDto, AssetsService } from './../services/assets.service'; import { AssetDto, AssetsService, AssetUploadedDto } from './../services/assets.service';
import { AuthService } from './../services/auth.service'; import { AuthService } from './../services/auth.service';
import { AppsState } from './apps.state'; import { AppsState } from './apps.state';
import { AssetsState } from './assets.state'; import { AssetsState } from './assets.state';
@ -58,6 +60,7 @@ export class AssetUploaderState extends State<Snapshot> {
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly apiUrl: ApiUrlConfig,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly dialogs: DialogService private readonly dialogs: DialogService
@ -76,9 +79,11 @@ export class AssetUploaderState extends State<Snapshot> {
} }
public uploadFile(file: File, target?: AssetsState, now?: DateTime): Observable<UploadResult> { public uploadFile(file: File, target?: AssetsState, now?: DateTime): Observable<UploadResult> {
const stream = this.assetsService.uploadFile(this.appName, file, this.user, now || DateTime.now()); const stream = this.assetsService.uploadFile(this.appName, file);
return this.upload(stream, MathHelper.guid(), file, response => {
const asset = createAsset(response, this.apiUrl, this.user, now);
return this.upload(stream, MathHelper.guid(), file, asset => {
if (asset.isDuplicate) { if (asset.isDuplicate) {
this.dialogs.notifyError('Asset has already been uploaded.'); this.dialogs.notifyError('Asset has already been uploaded.');
} else if (target) { } else if (target) {
@ -92,7 +97,11 @@ export class AssetUploaderState extends State<Snapshot> {
public uploadAsset(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> { public uploadAsset(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> {
const stream = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version); const stream = this.assetsService.replaceFile(this.appName, asset.id, file, asset.version);
return this.upload(stream, asset.id, file, ({ version, payload }) => asset.update(payload, this.user, version, now)); return this.upload(stream, asset.id, file, ({ version, payload }) => {
const newAsset = asset.update(payload, this.user, version, now);
return newAsset;
});
} }
private upload<T>(source: Observable<number | T>, id: string, file: File, complete: ((completion: T) => AssetDto)) { private upload<T>(source: Observable<number | T>, id: string, file: File, complete: ((completion: T) => AssetDto)) {
@ -165,4 +174,33 @@ export class AssetUploaderState extends State<Snapshot> {
private get user() { private get user() {
return this.authService.user!.token; return this.authService.user!.token;
} }
}
function createAsset({ payload, version }: Versioned<AssetUploadedDto>, apiUrl: ApiUrlConfig, user: string, now?: DateTime) {
const assetUrl = apiUrl.buildUrl(`api/assets/${payload.id}`);
now = now || DateTime.now();
const asset = new AssetDto(
payload.id,
user,
user,
now,
now,
payload.fileName,
payload.fileHash,
payload.fileType,
payload.fileSize,
payload.fileVersion,
payload.mimeType,
payload.isDuplicate,
payload.isImage,
payload.pixelWidth,
payload.pixelHeight,
payload.slug,
payload.tags || [],
assetUrl,
version);
return asset;
} }

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

@ -21,7 +21,7 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<div class="apps-list"> <div class="apps-list">
<a class="dropdown-item" *ngFor="let app of apps" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a> <a class="dropdown-item" *ngFor="let app of apps; trackBy: trackByApp" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a>
</div> </div>
</ng-container> </ng-container>

5
src/Squidex/app/shell/pages/internal/apps-menu.component.ts

@ -8,6 +8,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { import {
AppDto,
AppsState, AppsState,
DialogModel, DialogModel,
fadeAnimation, fadeAnimation,
@ -39,4 +40,8 @@ export class AppsMenuComponent {
this.appsMenu.hide(); this.appsMenu.hide();
this.addAppDialog.show(); this.addAppDialog.show();
} }
public trackByApp(index: number, app: AppDto) {
return app.id;
}
} }

2
src/Squidex/tslint.json

@ -105,7 +105,7 @@
"always" "always"
], ],
"template-no-negated-async": true, "template-no-negated-async": true,
"template-use-track-by-function": true, "template-use-track-by-function": false,
"trailing-comma": [ "trailing-comma": [
true, true,
{ {

Loading…
Cancel
Save