Browse Source

Performance optimizations

pull/1/head
Sebastian 9 years ago
parent
commit
6e43786192
  1. 4
      src/Squidex.Core/Contents/ContentData.cs
  2. 6
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  3. 11
      src/Squidex/app/features/content/module.ts
  4. 59
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  5. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  6. 8
      src/Squidex/app/features/content/pages/contents/contents-page.component.scss
  7. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  8. 2
      src/Squidex/app/features/content/pages/messages.ts
  9. 1
      src/Squidex/app/shared/declarations.ts
  10. 2
      src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts
  11. 2
      src/Squidex/app/shared/guards/resolve-app-languages.guard.ts
  12. 93
      src/Squidex/app/shared/guards/resolve-content.guard.spec.ts
  13. 64
      src/Squidex/app/shared/guards/resolve-content.guard.ts
  14. 22
      src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts
  15. 18
      src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts
  16. 2
      src/Squidex/app/shared/module.ts
  17. 4
      src/Squidex/app/shared/services/contents.service.spec.ts
  18. 4
      src/Squidex/app/shared/services/contents.service.ts

4
src/Squidex.Core/Contents/ContentData.cs

@ -48,7 +48,7 @@ namespace Squidex.Core.Contents
return new ContentData(request.ToImmutableDictionary(x => x.Key, x => new ContentFieldData(x.Value.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)), StringComparer.OrdinalIgnoreCase));
}
public Dictionary<string, Dictionary<string, JToken>> ToApiResponse(Schema schema, IReadOnlyCollection<Language> languages, Language masterLanguage)
public Dictionary<string, Dictionary<string, JToken>> ToApiResponse(Schema schema, IReadOnlyCollection<Language> languages, Language masterLanguage, bool excludeHidden = true)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(languages, nameof(languages));
@ -62,7 +62,7 @@ namespace Squidex.Core.Contents
{
Field field;
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out field) || field.IsHidden)
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out field) || (!excludeHidden && field.IsHidden))
{
continue;
}

6
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -42,7 +42,7 @@ namespace Squidex.Controllers.ContentApi
[HttpGet]
[Route("content/{app}/{name}")]
public async Task<IActionResult> GetContents(string name, [FromQuery] string query = null, [FromQuery] int? take = null, [FromQuery] int? skip = null, [FromQuery] bool nonPublished = false)
public async Task<IActionResult> GetContents(string name, [FromQuery] string query = null, [FromQuery] int? take = null, [FromQuery] int? skip = null, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false)
{
var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name);
@ -77,7 +77,7 @@ namespace Squidex.Controllers.ContentApi
[HttpGet]
[Route("content/{app}/{name}/{id}")]
public async Task<IActionResult> GetContent(string name, Guid id)
public async Task<IActionResult> GetContent(string name, Guid id, bool hidden = false)
{
var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name);
@ -97,7 +97,7 @@ namespace Squidex.Controllers.ContentApi
if (content.Data != null)
{
model.Data = content.Data.ToApiResponse(schemaEntity.Schema, App.Languages, App.MasterLanguage);
model.Data = content.Data.ToApiResponse(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden);
}
return Ok(model);

11
src/Squidex/app/features/content/module.ts

@ -11,6 +11,7 @@ import { RouterModule, Routes } from '@angular/router';
import {
HistoryComponent,
ResolveAppLanguagesGuard,
ResolveContentGuard,
ResolvePublishedSchemaGuard,
SqxFrameworkModule,
SqxSharedModule
@ -40,13 +41,7 @@ const routes: Routes = [
children: [
{
path: 'new',
component: ContentPageComponent,
resolve: {
schema: ResolvePublishedSchemaGuard, appLanguages: ResolveAppLanguagesGuard
},
data: {
disableHistory: true
}
component: ContentPageComponent
}, {
path: 'history',
component: HistoryComponent,
@ -57,7 +52,7 @@ const routes: Routes = [
path: ':contentId',
component: ContentPageComponent,
resolve: {
schema: ResolvePublishedSchemaGuard, appLanguages: ResolveAppLanguagesGuard
content: ResolveContentGuard
},
children: [
{

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

@ -7,14 +7,15 @@
import { Component } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { ContentAdded } from './../messages';
import { ContentChanged } from './../messages';
import {
AppComponentBase,
AppLanguageDto,
AppsStoreService,
ContentDto,
ContentsService,
MessageBus,
NotificationService,
@ -35,32 +36,34 @@ export class ContentPageComponent extends AppComponentBase {
public contentFormSubmitted = false;
public contentForm: FormGroup;
public content: ContentDto = null;
public languages: AppLanguageDto[] = [];
public isNewMode = false;
public get isNewMode() {
return this.content !== null;
}
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly messageBus: MessageBus
) {
super(apps, notifications, users);
}
public ngOnInit() {
this.route.params.map(p => p['contentId']).subscribe(contentId => {
this.isNewMode = !contentId;
});
this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => {
this.route.parent.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => {
this.languages = languages;
});
this.route.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => {
this.schema = schema;
this.route.parent.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => {
this.setupForm(schema, this.route.snapshot.data['content']);
});
this.setupForm(schema);
this.route.data.map(p => p['content']).subscribe((content: ContentDto) => {
this.populateForm(content);
});
}
@ -73,10 +76,17 @@ export class ContentPageComponent extends AppComponentBase {
const data = this.contentForm.value;
this.appName()
.switchMap(app => this.contentsService.postContent(app, this.schema.name, data))
.switchMap(app => {
if (this.isNewMode) {
return this.contentsService.postContent(app, this.schema.name, data);
} else {
return this.contentsService.putContent(app, this.schema.name, data, this.content.id);
}
})
.subscribe(() => {
this.reset();
this.messageBus.publish(new ContentAdded());
this.router.navigate(['../'], { relativeTo: this.route });
this.messageBus.publish(new ContentChanged());
}, error => {
this.notifyError(error);
this.enable();
@ -107,7 +117,9 @@ export class ContentPageComponent extends AppComponentBase {
}
}
private setupForm(schema: SchemaDetailsDto) {
private setupForm(schema: SchemaDetailsDto, content?: ContentDto) {
this.schema = schema;
const controls: { [key: string]: AbstractControl } = {};
for (const field of schema.fields) {
@ -146,5 +158,22 @@ export class ContentPageComponent extends AppComponentBase {
this.contentForm = new FormGroup(controls);
}
private populateForm(content: ContentDto) {
this.content = content;
for (const field of this.schema.fields) {
const fieldValue = content.data[field.name] || {};
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.properties.isLocalizable) {
for (let language of this.languages) {
fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]);
}
} else {
fieldForm.controls['iv'].setValue(fieldValue['iv']);
}
}
}
}

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

@ -52,7 +52,7 @@
<tbody>
<template ngFor let-content [ngForOf]="contents.items">
<tr>
<tr [routerLink]="[content.id]" class="content">
<td *ngFor="let field of contentFields">
<span class="field">
{{getFieldContent(content, field)}}

8
src/Squidex/app/features/content/pages/contents/contents-page.component.scss

@ -6,12 +6,16 @@
max-width: 64rem;
}
.languages-buttons {
margin-right: 1rem;
}
.field {
@include truncate;
}
.languages-buttons {
margin-right: 1rem;
.content {
cursor: pointer;
}
.user-picture {

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

@ -9,7 +9,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { ContentAdded } from './../messages';
import { ContentChanged } from './../messages';
import {
AppComponentBase,
@ -62,7 +62,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnInit {
public ngOnInit() {
this.messageSubscription =
this.messageBus.of(ContentAdded).delay(2000).subscribe(message => {
this.messageBus.of(ContentChanged).delay(2000).subscribe(message => {
this.load();
});

2
src/Squidex/app/features/content/pages/messages.ts

@ -5,4 +5,4 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export class ContentAdded { }
export class ContentChanged { }

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

@ -13,6 +13,7 @@ export * from './guards/app-must-exist.guard';
export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard';
export * from './guards/resolve-app-languages.guard';
export * from './guards/resolve-content.guard';
export * from './guards/resolve-published-schema.guard';
export * from './guards/resolve-schema.guard';

2
src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts

@ -29,7 +29,7 @@ describe('ResolveAppLanguagesGuard', () => {
it('should throw if route does not contain parameter', () => {
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app.');
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app name.');
});
it('should navigate to 404 page if languages are not found', (done) => {

2
src/Squidex/app/shared/guards/resolve-app-languages.guard.ts

@ -22,7 +22,7 @@ export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> {
const appName = this.findParameter(route, 'appName');
if (!appName) {
throw 'Route must contain app and schema name.';
throw 'Route must contain app name.';
}
const result =

93
src/Squidex/app/shared/guards/resolve-content.guard.spec.ts

@ -0,0 +1,93 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { ContentsService } from 'shared';
import { ResolveContentGuard } from './resolve-content.guard';
import { RouterMockup } from './router-mockup';
describe('ResolveContentGuard', () => {
const route = {
params: {
appName: 'my-app'
},
parent: {
params: {
schemaName: 'my-schema'
},
parent: {
params: {
contentId: '123'
}
}
}
};
let appsStore: IMock<ContentsService>;
beforeEach(() => {
appsStore = Mock.ofType(ContentsService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app and schema name and id.');
});
it('should navigate to 404 page if schema is not found', (done) => {
appsStore.setup(x => x.getContent('my-app', 'my-schema', '123'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should navigate to 404 page if schema loading fails', (done) => {
appsStore.setup(x => x.getContent('my-app', 'my-schema', '123'))
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should return schema if loading succeeded', (done) => {
const schema = {};
appsStore.setup(x => x.getContent('my-app', 'my-schema', '123'))
.returns(() => Observable.of(schema));
const router = new RouterMockup();
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBe(schema);
done();
});
});
});

64
src/Squidex/app/shared/guards/resolve-content.guard.ts

@ -0,0 +1,64 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { ContentDto, ContentsService } from './../services/contents.service';
@Injectable()
export class ResolveContentGuard implements Resolve<ContentDto> {
constructor(
private readonly contentsService: ContentsService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<ContentDto> {
const appName = this.findParameter(route, 'appName');
const schemaName = this.findParameter(route, 'schemaName');
const contentId = this.findParameter(route, 'contentId');
if (!appName || !schemaName || !contentId) {
throw 'Route must contain app and schema name and id.';
}
const result =
this.contentsService.getContent(appName, schemaName, contentId).toPromise()
.then(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
this.router.navigate(['/404']);
return null;
});
return result;
}
private findParameter(route: ActivatedRouteSnapshot, name: string): string | null {
let result: string | null = null;
while (route) {
result = route.params[name];
if (result) {
break;
}
route = route.parent;
}
return result;
}
}

22
src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts

@ -25,24 +25,24 @@ describe('ResolvePublishedSchemaGuard', () => {
}
};
let appsStore: IMock<SchemasService>;
let schemasService: IMock<SchemasService>;
beforeEach(() => {
appsStore = Mock.ofType(SchemasService);
schemasService = Mock.ofType(SchemasService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolvePublishedSchemaGuard(appsStore.object, <any>new RouterMockup());
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app and schema name.');
});
it('should navigate to 404 page if schema is not found', (done) => {
appsStore.setup(x => x.getSchema('my-app', 'my-schema'))
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(appsStore.object, <any>router);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
@ -54,11 +54,11 @@ describe('ResolvePublishedSchemaGuard', () => {
});
it('should navigate to 404 page if schema loading fails', (done) => {
appsStore.setup(x => x.getSchema('my-app', 'my-schema'))
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.throw(null));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(appsStore.object, <any>router);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
@ -72,11 +72,11 @@ describe('ResolvePublishedSchemaGuard', () => {
it('should navigate to 404 page if schema not published', (done) => {
const schema = { isPublished: false };
appsStore.setup(x => x.getSchema('my-app', 'my-schema'))
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.of(schema));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(appsStore.object, <any>router);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
@ -90,11 +90,11 @@ describe('ResolvePublishedSchemaGuard', () => {
it('should return schema if loading succeeded', (done) => {
const schema = { isPublished: true };
appsStore.setup(x => x.getSchema('my-app', 'my-schema'))
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.of(schema));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(appsStore.object, <any>router);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {

18
src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts

@ -25,24 +25,24 @@ describe('ResolveSchemaGuard', () => {
}
};
let appsStore: IMock<SchemasService>;
let schemasService: IMock<SchemasService>;
beforeEach(() => {
appsStore = Mock.ofType(SchemasService);
schemasService = Mock.ofType(SchemasService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolveSchemaGuard(appsStore.object, <any>new RouterMockup());
const guard = new ResolveSchemaGuard(schemasService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app and schema name.');
});
it('should navigate to 404 page if schema is not found', (done) => {
appsStore.setup(x => x.getSchema('my-app', 'my-schema'))
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveSchemaGuard(appsStore.object, <any>router);
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
@ -54,11 +54,11 @@ describe('ResolveSchemaGuard', () => {
});
it('should navigate to 404 page if schema loading fails', (done) => {
appsStore.setup(x => x.getSchema('my-app', 'my-schema'))
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveSchemaGuard(appsStore.object, <any>router);
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
@ -72,11 +72,11 @@ describe('ResolveSchemaGuard', () => {
it('should return schema if loading succeeded', (done) => {
const schema = {};
appsStore.setup(x => x.getSchema('my-app', 'my-schema'))
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.of(schema));
const router = new RouterMockup();
const guard = new ResolveSchemaGuard(appsStore.object, <any>router);
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {

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

@ -26,6 +26,7 @@ import {
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
ResolveAppLanguagesGuard,
ResolveContentGuard,
ResolvePublishedSchemaGuard,
ResolveSchemaGuard,
SchemasService,
@ -66,6 +67,7 @@ export class SqxSharedModule {
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
ResolveAppLanguagesGuard,
ResolveContentGuard,
ResolvePublishedSchemaGuard,
ResolveSchemaGuard,
SchemasService,

4
src/Squidex/app/shared/services/contents.service.spec.ts

@ -29,7 +29,7 @@ describe('ContentsService', () => {
});
it('should make get request to get contents', () => {
authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema?take=17&skip=13&query=my-query&nonPublished=true'))
authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema?take=17&skip=13&query=my-query&nonPublished=true&hidden=true'))
.returns(() => Observable.of(
new Response(
new ResponseOptions({
@ -74,7 +74,7 @@ describe('ContentsService', () => {
});
it('should make get request to get content', () => {
authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1'))
authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1?hidden=true'))
.returns(() => Observable.of(
new Response(
new ResponseOptions({

4
src/Squidex/app/shared/services/contents.service.ts

@ -48,7 +48,7 @@ export class ContentsService {
}
public getContents(appName: string, schemaName: string, take: number, skip: number, query: string): Observable<ContentsDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?take=${take}&skip=${skip}&query=${query}&nonPublished=true`);
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?take=${take}&skip=${skip}&query=${query}&nonPublished=true&hidden=true`);
return this.authService.authGet(url)
.map(response => response.json())
@ -70,7 +70,7 @@ export class ContentsService {
}
public getContent(appName: string, schemaName: string, id: string): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}?hidden=true`);
return this.authService.authGet(url)
.map(response => response.json())

Loading…
Cancel
Save