diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index cc8e5d6ec..2b138cddd 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -67,8 +67,8 @@ - - + (); + public created = new EventEmitter(); @Output() public cancelled = new EventEmitter(); @@ -63,7 +59,6 @@ export class SchemaFormComponent { constructor( public readonly apiUrl: ApiUrlConfig, - private readonly notifications: NotificationService, private readonly schemas: SchemasService, private readonly formBuilder: FormBuilder, private readonly authService: AuthService @@ -95,25 +90,11 @@ export class SchemaFormComponent { const me = this.authService.user!.token; this.schemas.postSchema(this.appName, requestDto, me, undefined, schemaVersion) - .switchMap(dto => { - return this.schemas.getSchema(this.appName, dto.id) - .retryWhen(errors => errors - .delay(500) - .take(10) - .concat(Observable.throw(dto))); - }) .subscribe(dto => { - this.emitCreated(dto, true); + this.emitCreated(dto); this.resetCreateForm(); }, error => { - if (error instanceof SchemaDetailsDto) { - this.notifications.notify(Notification.error('Schema has been created but is awaiting to be processed. Reload in a few seconds.')); - - this.emitCreated(error, false); - this.resetCreateForm(); - } else { - this.enableCreateForm(error.displayMessage); - } + this.enableCreateForm(error.displayMessage); }); } } @@ -122,8 +103,8 @@ export class SchemaFormComponent { this.cancelled.emit(); } - private emitCreated(schema: SchemaDto, isAvailable: boolean) { - this.created.emit({ schema, isAvailable }); + private emitCreated(schema: SchemaDetailsDto) { + this.created.emit(schema); } private enableCreateForm(message: string) { diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html index d1c6d1646..4c45aafdb 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html @@ -66,7 +66,7 @@ diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 75c79aaa1..a246013f1 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -18,6 +18,7 @@ import { MessageBus, ModalView, NotificationService, + RoutingCache, SchemaDto, SchemasService } from 'shared'; @@ -47,7 +48,8 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, private readonly schemasService: SchemasService, private readonly messageBus: MessageBus, private readonly router: Router, - private readonly route: ActivatedRoute + private readonly route: ActivatedRoute, + private readonly routingCache: RoutingCache ) { super(notifications, apps); } @@ -97,14 +99,13 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, }); } - public onSchemaCreated(dto: SchemaDto, isAvailable: boolean) { + public onSchemaCreated(dto: SchemaDto) { this.updateSchemas(this.schemas.push(dto), this.schemaQuery); this.addSchemaDialog.hide(); - if (isAvailable) { - this.router.navigate([ dto.name ], { relativeTo: this.route }); - } + this.routingCache.set(`schema.${dto.name}`, dto); + this.router.navigate([ dto.name ], { relativeTo: this.route }); } private updateSchemas(schemas: ImmutableArray, query?: string) { diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 2352082bb..8c98a1f7e 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -54,6 +54,7 @@ export * from './services/message-bus'; export * from './services/notification.service'; export * from './services/resource-loader.service'; export * from './services/root-view.service'; +export * from './services/routing-cache.service'; export * from './services/shortcut.service'; export * from './services/title.service'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index b255de2f1..d219c8157 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -50,6 +50,7 @@ import { RichEditorComponent, RootViewDirective, RootViewService, + RoutingCache, ScrollActiveDirective, ShortcutComponent, ShortcutService, @@ -182,6 +183,7 @@ export class SqxFrameworkModule { NotificationService, ResourceLoaderService, RootViewService, + RoutingCache, ShortcutService, TitleService ] diff --git a/src/Squidex/app/framework/services/routing-cache.service.ts b/src/Squidex/app/framework/services/routing-cache.service.ts new file mode 100644 index 000000000..ead7b1cfa --- /dev/null +++ b/src/Squidex/app/framework/services/routing-cache.service.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export class RoutingCache { + private readonly entries: { [key: string]: { value: any, inserted: number } } = {}; + + public getValue(key: string) { + let entry = this.entries[key]; + + if (entry && (new Date().getTime() - entry.inserted) < 100) { + return entry.value; + } else { + return undefined; + } + } + + public set(key: string, value: T) { + this.entries[key] = { value, inserted: new Date().getTime() }; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts index 63a8003fc..73fee408f 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts @@ -8,7 +8,7 @@ import { IMock, Mock } from 'typemoq'; import { Observable } from 'rxjs'; -import { ContentsService } from 'shared'; +import { ContentsService, RoutingCache } from 'shared'; import { ResolveContentGuard } from './resolve-content.guard'; import { RouterMockup } from './router-mockup'; @@ -31,35 +31,54 @@ describe('ResolveContentGuard', () => { }; let appsStore: IMock; + let routingCache: IMock; beforeEach(() => { appsStore = Mock.ofType(ContentsService); + + routingCache = Mock.ofType(RoutingCache); }); it('should throw if route does not contain app name', () => { - const guard = new ResolveContentGuard(appsStore.object, new RouterMockup()); + const guard = new ResolveContentGuard(appsStore.object, new RouterMockup(), routingCache.object); expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app name.'); }); it('should throw if route does not contain schema name', () => { - const guard = new ResolveContentGuard(appsStore.object, new RouterMockup()); + const guard = new ResolveContentGuard(appsStore.object, new RouterMockup(), routingCache.object); expect(() => guard.resolve({ params: { appName: 'my-app' } }, {})).toThrow('Route must contain schema name.'); }); it('should throw if route does not contain content id', () => { - const guard = new ResolveContentGuard(appsStore.object, new RouterMockup()); + const guard = new ResolveContentGuard(appsStore.object, new RouterMockup(), routingCache.object); expect(() => guard.resolve({ params: { appName: 'my-app', schemaName: 'my-schema' } }, {})).toThrow('Route must contain content id.'); }); + it('should provide content from cache if found', (done) => { + const content = { }; + + routingCache.setup(x => x.getValue('content.123')) + .returns(() => content); + + const guard = new ResolveContentGuard(appsStore.object, new RouterMockup(), routingCache.object); + + guard.resolve(route, {}) + .subscribe(result => { + expect(result).toBe(content); + + done(); + }); + }); + 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, router); + const guard = new ResolveContentGuard(appsStore.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { @@ -75,7 +94,7 @@ describe('ResolveContentGuard', () => { .returns(() => Observable.throw(null!)); const router = new RouterMockup(); - const guard = new ResolveContentGuard(appsStore.object, router); + const guard = new ResolveContentGuard(appsStore.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { @@ -93,7 +112,7 @@ describe('ResolveContentGuard', () => { .returns(() => Observable.of(content)); const router = new RouterMockup(); - const guard = new ResolveContentGuard(appsStore.object, router); + const guard = new ResolveContentGuard(appsStore.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.ts b/src/Squidex/app/shared/guards/resolve-content.guard.ts index d2e9efdfd..4b8b4b47f 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-content.guard.ts @@ -9,7 +9,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; -import { allParams } from 'framework'; +import { allParams, RoutingCache } from 'framework'; import { ContentDto, ContentsService } from './../services/contents.service'; @@ -17,7 +17,8 @@ import { ContentDto, ContentsService } from './../services/contents.service'; export class ResolveContentGuard implements Resolve { constructor( private readonly contentsService: ContentsService, - private readonly router: Router + private readonly router: Router, + private readonly routingCache: RoutingCache ) { } @@ -42,6 +43,12 @@ export class ResolveContentGuard implements Resolve { throw 'Route must contain content id.'; } + const content = this.routingCache.getValue(`content.${contentId}`); + + if (content) { + return Observable.of(content); + } + const result = this.contentsService.getContent(appName, schemaName, contentId) .do(dto => { diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts index 29d17ff91..5d1bc2978 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts @@ -8,10 +8,9 @@ import { IMock, Mock } from 'typemoq'; import { Observable } from 'rxjs'; -import { SchemasService } from 'shared'; - -import { ResolvePublishedSchemaGuard } from './resolve-published-schema.guard'; +import { RoutingCache, SchemasService } from 'shared'; import { RouterMockup } from './router-mockup'; +import { ResolvePublishedSchemaGuard } from './resolve-published-schema.guard'; describe('ResolvePublishedSchemaGuard', () => { const route = { @@ -26,29 +25,48 @@ describe('ResolvePublishedSchemaGuard', () => { }; let schemasService: IMock; + let routingCache: IMock; beforeEach(() => { schemasService = Mock.ofType(SchemasService); + + routingCache = Mock.ofType(RoutingCache); }); it('should throw if route does not contain app name', () => { - const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup()); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app name.'); }); it('should throw if route does not contain schema name', () => { - const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup()); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); expect(() => guard.resolve({ params: { appName: 'my-app' } }, {})).toThrow('Route must contain schema name.'); }); + it('should provide schema from cache if found', (done) => { + const schema = { isPublished: true }; + + routingCache.setup(x => x.getValue('schema.my-schema')) + .returns(() => schema); + + const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); + + guard.resolve(route, {}) + .subscribe(result => { + expect(result).toBe(schema); + + done(); + }); + }); + it('should navigate to 404 page if schema is not found', (done) => { schemasService.setup(x => x.getSchema('my-app', 'my-schema')) .returns(() => Observable.of(null!)); const router = new RouterMockup(); - const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { @@ -64,7 +82,7 @@ describe('ResolvePublishedSchemaGuard', () => { .returns(() => Observable.throw(null)); const router = new RouterMockup(); - const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { @@ -82,11 +100,11 @@ describe('ResolvePublishedSchemaGuard', () => { .returns(() => Observable.of(schema)); const router = new RouterMockup(); - const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { - expect(result).toBeFalsy(); + expect(result).toBe(schema); expect(router.lastNavigation).toEqual(['/404']); done(); @@ -100,7 +118,7 @@ describe('ResolvePublishedSchemaGuard', () => { .returns(() => Observable.of(schema)); const router = new RouterMockup(); - const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts index 8c061d63a..ad94f8ac8 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts @@ -9,7 +9,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; -import { allParams } from 'framework'; +import { allParams, RoutingCache } from 'framework'; import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'; @@ -17,7 +17,8 @@ import { SchemaDetailsDto, SchemasService } from './../services/schemas.service' export class ResolvePublishedSchemaGuard implements Resolve { constructor( private readonly schemasService: SchemasService, - private readonly router: Router + private readonly router: Router, + private readonly routingCache: RoutingCache ) { } @@ -36,10 +37,16 @@ export class ResolvePublishedSchemaGuard implements Resolve { throw 'Route must contain schema name.'; } + const schema = this.routingCache.getValue(`schema.${schemaName}`); + + if (schema && schema.isPublished) { + return Observable.of(schema); + } + const result = - this.schemasService.getSchema(appName, schemaName).map(dto => dto && dto.isPublished ? dto : null) + this.schemasService.getSchema(appName, schemaName) .do(dto => { - if (!dto) { + if (!dto || !dto.isPublished) { this.router.navigate(['/404']); } }) diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts index 2abb64e09..2628b7f58 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts @@ -8,10 +8,9 @@ import { IMock, Mock } from 'typemoq'; import { Observable } from 'rxjs'; -import { SchemasService } from 'shared'; - -import { ResolveSchemaGuard } from './resolve-schema.guard'; +import { RoutingCache, SchemasService } from 'shared'; import { RouterMockup } from './router-mockup'; +import { ResolveSchemaGuard } from './resolve-schema.guard'; describe('ResolveSchemaGuard', () => { const route = { @@ -26,29 +25,48 @@ describe('ResolveSchemaGuard', () => { }; let schemasService: IMock; + let routingCache: IMock; beforeEach(() => { schemasService = Mock.ofType(SchemasService); + + routingCache = Mock.ofType(RoutingCache); }); it('should throw if route does not contain app name', () => { - const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup()); + const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app name.'); }); it('should throw if route does not contain schema name', () => { - const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup()); + const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); expect(() => guard.resolve({ params: { appName: 'my-app' } }, {})).toThrow('Route must contain schema name.'); }); + it('should provide schema from cache if found', (done) => { + const schema = { isPublished: true }; + + routingCache.setup(x => x.getValue('schema.my-schema')) + .returns(() => schema); + + const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); + + guard.resolve(route, {}) + .subscribe(result => { + expect(result).toBe(schema); + + done(); + }); + }); + it('should navigate to 404 page if schema is not found', (done) => { schemasService.setup(x => x.getSchema('my-app', 'my-schema')) .returns(() => Observable.of(null!)); const router = new RouterMockup(); - const guard = new ResolveSchemaGuard(schemasService.object, router); + const guard = new ResolveSchemaGuard(schemasService.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { @@ -64,7 +82,7 @@ describe('ResolveSchemaGuard', () => { .returns(() => Observable.throw(null!)); const router = new RouterMockup(); - const guard = new ResolveSchemaGuard(schemasService.object, router); + const guard = new ResolveSchemaGuard(schemasService.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { @@ -82,7 +100,7 @@ describe('ResolveSchemaGuard', () => { .returns(() => Observable.of(schema)); const router = new RouterMockup(); - const guard = new ResolveSchemaGuard(schemasService.object, router); + const guard = new ResolveSchemaGuard(schemasService.object, router, routingCache.object); guard.resolve(route, {}) .subscribe(result => { diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.ts index 093bd45e2..95e494cc4 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.ts @@ -9,7 +9,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; -import { allParams } from 'framework'; +import { allParams, RoutingCache } from 'framework'; import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'; @@ -17,7 +17,8 @@ import { SchemaDetailsDto, SchemasService } from './../services/schemas.service' export class ResolveSchemaGuard implements Resolve { constructor( private readonly schemasService: SchemasService, - private readonly router: Router + private readonly router: Router, + private readonly routingCache: RoutingCache ) { } @@ -36,6 +37,12 @@ export class ResolveSchemaGuard implements Resolve { throw 'Route must contain schema name.'; } + const schema = this.routingCache.getValue(`schema.${schemaName}`); + + if (schema) { + return Observable.of(schema); + } + const result = this.schemasService.getSchema(appName, schemaName) .do(dto => {