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>
</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">
<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 {
AppDto,
AppsState,
AuthService,
DialogModel,
@ -77,4 +78,8 @@ export class AppsPageComponent implements OnInit {
public stop(event: Event) {
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">
<h1>What's new?</h1>
<div *ngFor="let feature of features">
<div *ngFor="let feature of features; trackBy: trackByFeature">
<h4>{{feature.name}}</h4>
<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() {
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>.
</div>
<div *ngFor="let field of schema.fields">
<div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)">
<sqx-content-field
[form]="contentForm"
[field]="field"

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

@ -21,6 +21,7 @@ import {
DialogService,
EditContentForm,
fadeAnimation,
FieldDto,
ImmutableArray,
LanguagesState,
MessageBus,
@ -241,4 +242,8 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
public showLatest() {
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">
<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)">
{{query.name}}

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

@ -10,7 +10,7 @@
</ng-template>
</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">
<sqx-content-item-editor [form]="patchForm.form" [field]="field"></sqx-content-item-editor>
</ng-container>

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

@ -187,5 +187,9 @@ export class ContentItemComponent implements OnChanges {
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"
[sqxSortModel]="snapshot.contentItems.mutableValues"
(sqxSort)="sort($event)">
<tbody *ngFor="let content of snapshot.contentItems">
<tbody *ngFor="let content of snapshot.contentItems; trackBy: trackByContent">
<tr [sqxContent]="content"
[language]="language"
[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.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()">
<div class="form-group mr-1">
<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>
</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.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>
<div class="col cell-input">
<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>
</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);
});
}
public trackByRole(index: number, role: RoleDto) {
return role.name;
}
}
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.
*/
// tslint:disable:template-use-track-by-function
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
@ -362,32 +364,36 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
public onCopy(event: ClipboardEvent) {
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();
}
}
public onPaste(event: ClipboardEvent) {
const value = event.clipboardData.getData('text/plain');
if (event.clipboardData) {
const value = event.clipboardData.getData('text/plain');
if (value) {
this.resetForm();
if (value) {
this.resetForm();
const values = [...this.snapshot.items];
const values = [...this.snapshot.items];
for (let part of value.split(',')) {
const converted = this.converter.convert(part);
for (let part of value.split(',')) {
const converted = this.converter.convert(part);
if (converted) {
values.push(converted);
if (converted) {
values.push(converted);
}
}
this.updateItems(values);
}
this.updateItems(values);
event.preventDefault();
}
event.preventDefault();
}
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">
<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>
</button>
</div>
@ -9,7 +9,7 @@
{{selectedLanguage.iso2Code}}
</button>
<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}})
</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 { fadeAnimation, ModalModel } from '@app/shared/internal';
import { LanguageDto } from '../services/languages.service';
export interface Language { iso2Code: string; englishName: string; isMasterLanguage: true; }
@ -65,4 +66,8 @@ export class LanguageSelectorComponent implements OnChanges, OnInit {
this.selectedLanguage = 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.
*/
// tslint:disable:no-pipe-impure
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { Observable, of, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';

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

@ -20,6 +20,7 @@ import {
Version,
Versioned
} from '@app/shared/internal';
import { AssetUploadedDto } from './assets.service';
describe('AssetDto', () => {
const creation = DateTime.today();
@ -71,8 +72,6 @@ describe('AssetDto', () => {
});
describe('AssetsService', () => {
const now = DateTime.now();
const user = 'me';
const version = new Version('1');
beforeEach(() => {
@ -315,10 +314,10 @@ describe('AssetsService', () => {
it('should make post request to create asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let asset: AssetDto;
let asset: Versioned<AssetUploadedDto>;
assetsService.uploadFile('my-app', null!, user, now).subscribe(result => {
asset = <AssetDto>result;
assetsService.uploadFile('my-app', null!).subscribe(result => {
asset = <Versioned<AssetUploadedDto>>result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets');
@ -342,40 +341,38 @@ describe('AssetsService', () => {
tags: ['tag1', 'tag2']
}, {
headers: {
etag: '2'
etag: '1'
}
});
expect(asset!).toEqual(
new AssetDto(
'id1',
user,
user,
now,
now,
'My Asset1.png',
'My Hash1',
'png',
1024, 2,
'image/png',
true,
true,
1024,
2048,
'my-asset1.png',
['tag1', 'tag2'],
'http://service/p/api/assets/id1',
new Version('2')));
expect(asset!).toEqual({
payload: {
id: 'id1',
fileName: 'My Asset1.png',
fileHash: 'My Hash1',
fileType: 'png',
fileSize: 1024,
fileVersion: 2,
mimeType: 'image/png',
isDuplicate: true,
isImage: true,
pixelWidth: 1024,
pixelHeight: 2048,
slug: 'my-asset1.png',
tags: ['tag1', 'tag2']
},
version
});
}));
it('should return proper error when upload failed with 413',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let asset: AssetDto;
let asset: Versioned<AssetUploadedDto>;
let error: ErrorDto;
assetsService.uploadFile('my-app', null!, user, now).subscribe(result => {
asset = <AssetDto>result;
assetsService.uploadFile('my-app', null!).subscribe(result => {
asset = <Versioned<AssetUploadedDto>>result;
}, 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 isDuplicate: boolean,
public readonly isImage: boolean,
public readonly pixelWidth: number | null,
public readonly pixelHeight: number | null,
public readonly pixelWidth: number | null | undefined,
public readonly pixelHeight: number | null | undefined,
public readonly slug: string,
public readonly tags: string[],
public readonly url: string,
@ -91,6 +91,15 @@ export interface AssetReplacedDto {
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()
export class AssetsService {
constructor(
@ -176,7 +185,7 @@ export class AssetsService {
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 req = new HttpRequest('POST', url, getFormData(file), { reportProgress: true });
@ -192,32 +201,8 @@ export class AssetsService {
return percentDone;
} else if (Types.is(event, HttpResponse)) {
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 {
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 {
ApiUrlConfig,
AssetDto,
AssetReplacedDto,
AssetsService,
@ -28,10 +29,9 @@ describe('AssetsState', () => {
app,
appsState,
authService,
creator,
creation,
modified,
modifier
modifier,
version
} = TestValues;
let assetsService: IMock<AssetsService>;
@ -39,10 +39,10 @@ describe('AssetsState', () => {
let assetUploader: AssetUploaderState;
const asset = new AssetDto('id1',
creator,
creator,
creation,
creation,
modifier,
modifier,
modified,
modified,
'my-asset',
'my-hash',
'png',
@ -55,14 +55,16 @@ describe('AssetsState', () => {
600,
'my-slug',
[],
'http://url',
new Version('1'));
'http://url/api/assets/id1',
version);
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
const apiUrl = new ApiUrlConfig('http://url');
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(() => {
@ -72,7 +74,7 @@ describe('AssetsState', () => {
it('should create initial state when uploading 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();
assetUploader.uploadFile(file, undefined, modified).subscribe();
@ -86,7 +88,7 @@ describe('AssetsState', () => {
it('should update progress when uploading file makes progress', () => {
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();
assetUploader.uploadFile(file, undefined, modified).subscribe();
@ -100,7 +102,7 @@ describe('AssetsState', () => {
it('should update status when uploading file failed', () => {
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();
assetUploader.uploadFile(file, undefined, modified).pipe(onErrorResumeNext()).subscribe();
@ -114,8 +116,8 @@ describe('AssetsState', () => {
it('should update status when uploading file completes', (cb) => {
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.uploadFile(app, file, modifier, modified))
.returns(() => of(10, 20, asset)).verifiable();
assetsService.setup(x => x.uploadFile(app, file))
.returns(() => of(10, 20, versioned(version, { ...asset }))).verifiable();
let uploadedAsset: AssetDto;
@ -131,7 +133,7 @@ describe('AssetsState', () => {
expect(upload.status).toBe('Completed');
expect(upload.progress).toBe(100);
expect(uploadedAsset!).toBe(asset);
expect(uploadedAsset!).toEqual(asset);
});
it('should create initial state when uploading asset', () => {
@ -189,10 +191,11 @@ describe('AssetsState', () => {
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))
.returns(() => of(10, 20, versioned(new Version('2'), update))).verifiable();
.returns(() => of(10, 20, versioned(newVersion, update))).verifiable();
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 {
ApiUrlConfig,
DateTime,
DialogService,
ImmutableArray,
MathHelper,
State,
Types
Types,
Versioned
} 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 { AppsState } from './apps.state';
import { AssetsState } from './assets.state';
@ -58,6 +60,7 @@ export class AssetUploaderState extends State<Snapshot> {
constructor(
private readonly appsState: AppsState,
private readonly apiUrl: ApiUrlConfig,
private readonly assetsService: AssetsService,
private readonly authService: AuthService,
private readonly dialogs: DialogService
@ -76,9 +79,11 @@ export class AssetUploaderState extends State<Snapshot> {
}
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) {
this.dialogs.notifyError('Asset has already been uploaded.');
} else if (target) {
@ -92,7 +97,11 @@ export class AssetUploaderState extends State<Snapshot> {
public uploadAsset(asset: AssetDto, file: File, now?: DateTime): Observable<UploadResult> {
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)) {
@ -165,4 +174,33 @@ export class AssetUploaderState extends State<Snapshot> {
private get user() {
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="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>
</ng-container>

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

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

2
src/Squidex/tslint.json

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

Loading…
Cancel
Save