From 8dc89b5dc930ef5e902477eaaab26a9d60eebd7c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 5 Nov 2017 19:59:36 +0100 Subject: [PATCH] app selection fixed. --- src/Squidex/app/app.routes.ts | 10 +- .../apps/pages/apps-page.component.ts | 6 +- src/Squidex/app/features/content/module.ts | 2 +- .../pages/content/content-page.component.ts | 9 +- .../pages/contents/contents-page.component.ts | 17 +-- .../app/shared/components/app-context.ts | 19 +++- .../shared/components/app-form.component.ts | 6 +- src/Squidex/app/shared/declarations-base.ts | 4 +- .../guards/app-must-exist.guard.spec.ts | 73 ++++++++++++ .../app/shared/guards/app-must-exist.guard.ts | 40 +++++++ .../app/shared/guards/resolve-app.guard.ts | 45 -------- .../app/shared/guards/unset-app.guard.ts | 25 ++++ src/Squidex/app/shared/module.ts | 8 +- .../services/apps-store.service.spec.ts | 107 ++++++++++++++++++ .../app/shared/services/apps-store.service.ts | 70 ++++++++++++ .../app/shared/services/apps.service.spec.ts | 21 +++- .../app/shared/services/apps.service.ts | 66 ++++------- .../pages/internal/apps-menu.component.html | 11 ++ .../pages/internal/apps-menu.component.scss | 21 +++- .../pages/internal/apps-menu.component.ts | 8 +- src/Squidex/app/theme/_mixins.scss | 7 ++ 21 files changed, 442 insertions(+), 133 deletions(-) create mode 100644 src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts create mode 100644 src/Squidex/app/shared/guards/app-must-exist.guard.ts delete mode 100644 src/Squidex/app/shared/guards/resolve-app.guard.ts create mode 100644 src/Squidex/app/shared/guards/unset-app.guard.ts create mode 100644 src/Squidex/app/shared/services/apps-store.service.spec.ts create mode 100644 src/Squidex/app/shared/services/apps-store.service.ts diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 72ca0bca7..66c223c1c 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -18,9 +18,10 @@ import { } from './shell'; import { + AppMustExistGuard, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, - ResolveAppGuard + UnsetAppGuard } from './shared'; export const routes: Routes = [ @@ -36,7 +37,8 @@ export const routes: Routes = [ children: [ { path: '', - loadChildren: './features/apps/module#SqxFeatureAppsModule' + loadChildren: './features/apps/module#SqxFeatureAppsModule', + canActivate: [UnsetAppGuard] }, { path: 'administration', @@ -45,9 +47,7 @@ export const routes: Routes = [ { path: ':appName', component: AppAreaComponent, - resolve: { - app: ResolveAppGuard - }, + canActivate: [AppMustExistGuard], children: [ { path: '', diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/src/Squidex/app/features/apps/pages/apps-page.component.ts index 4cb17902c..5b33ebf9b 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.ts +++ b/src/Squidex/app/features/apps/pages/apps-page.component.ts @@ -10,7 +10,7 @@ import { Subscription } from 'rxjs'; import { AppDto, - AppsService, + AppsStoreService, fadeAnimation, ModalView, OnboardingService @@ -33,7 +33,7 @@ export class AppsPageComponent implements OnDestroy, OnInit { public onboardingModal = new ModalView(); constructor( - private readonly appsService: AppsService, + private readonly appsStore: AppsStoreService, private readonly onboardingService: OnboardingService ) { } @@ -44,7 +44,7 @@ export class AppsPageComponent implements OnDestroy, OnInit { public ngOnInit() { this.appsSubscription = - this.appsService.getApps() + this.appsStore.apps .subscribe(apps => { if (apps.length === 0 && this.onboardingService.shouldShow('dialog')) { this.onboardingService.disable('dialog'); diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index 7b91bb002..a621e5a0c 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -61,7 +61,7 @@ const routes: Routes = [ isReadOnly: true }, resolve: { - schemaOverride: ResolvePublishedSchemaGuard + schema: ResolvePublishedSchemaGuard } } ] diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 61717ed0a..bd7f81dc7 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -68,9 +68,6 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, } public ngOnInit() { - const routeData = allData(this.ctx.route); - - this.languages = routeData['appLanguages']; this.contentVersionSelectedSubscription = this.ctx.bus.of(ContentVersionSelected) @@ -102,7 +99,11 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, } }); - this.setupContentForm(routeData['schema']); + const routeData = allData(this.ctx.route); + + this.languages = routeData.appLanguages; + + this.setupContentForm(routeData.schema); this.ctx.route.data.map(d => d.content) .subscribe((content: ContentDto) => { diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 8c3c30f26..dc74f9a34 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -72,10 +72,6 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public ngOnInit() { - const routeData = allData(this.ctx.route); - - this.languages = routeData['appLanguages']; - this.contentCreatedSubscription = this.ctx.bus.of(ContentCreated) .subscribe(message => { @@ -89,20 +85,27 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.contentItems = this.contentItems.replaceBy('id', message.content, (o, n) => o.update(n.data, n.lastModifiedBy, n.version, n.lastModified)); }); + const routeData = allData(this.ctx.route); + + this.languages = routeData.appLanguages; + + this.ctx.route.data.map(p => p.isReadOnly) + .subscribe(isReadOnly => { + this.isReadOnly = isReadOnly; + }); + this.ctx.route.params.map(p => p.language) .subscribe(language => { this.languageSelected = this.languages.find(l => l.iso2Code === language) || this.languages.find(l => l.isMaster) || this.languages[0]; }); - this.ctx.route.data.map(d => d.schemaOverride || d.schema) + this.ctx.route.data.map(d => d.schema) .subscribe(schema => { this.schema = schema; this.resetContents(); this.load(); }); - - this.isReadOnly = routeData['isReadOnly']; } public dropData(content: ContentDto) { diff --git a/src/Squidex/app/shared/components/app-context.ts b/src/Squidex/app/shared/components/app-context.ts index 65471fa6f..ac77c6021 100644 --- a/src/Squidex/app/shared/components/app-context.ts +++ b/src/Squidex/app/shared/components/app-context.ts @@ -5,14 +5,15 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Injectable, OnDestroy } from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; import { ActivatedRoute } from '@angular/router'; import { MessageBus } from 'framework'; import { AppDto, + AppsStoreService, AuthService, DialogService, ErrorDto, @@ -21,7 +22,8 @@ import { } from './../declarations-base'; @Injectable() -export class AppContext { +export class AppContext implements OnDestroy { + private readonly appSubscription: Subscription; private appField: AppDto; public get app(): AppDto { @@ -29,7 +31,7 @@ export class AppContext { } public get appName(): string { - return this.appField.name; + return this.appField ? this.appField.name : ''; } public get userToken(): string { @@ -47,15 +49,20 @@ export class AppContext { constructor( public readonly dialogs: DialogService, public readonly authService: AuthService, + public readonly appsStore: AppsStoreService, public readonly route: ActivatedRoute, public readonly bus: MessageBus ) { - Observable.merge(...this.route.pathFromRoot.map(r => r.data)).map(d => d.app).filter(a => !!a) - .subscribe((app: AppDto) => { + this.appSubscription = + this.appsStore.selectedApp.subscribe(app => { this.appField = app; }); } + public ngOnDestroy() { + this.appSubscription.unsubscribe(); + } + public confirmUnsavedChanges(): Observable { return this.dialogs.confirmUnsavedChanges(); } diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/src/Squidex/app/shared/components/app-form.component.ts index bcd9f63c6..d75af7e42 100644 --- a/src/Squidex/app/shared/components/app-form.component.ts +++ b/src/Squidex/app/shared/components/app-form.component.ts @@ -12,7 +12,7 @@ import { ApiUrlConfig, ValidatorsEx } from 'framework'; import { AppDto, - AppsService, + AppsStoreService, CreateAppDto } from './../declarations-base'; @@ -48,7 +48,7 @@ export class AppFormComponent { constructor( public readonly apiUrl: ApiUrlConfig, - private readonly appsService: AppsService, + private readonly appsStore: AppsStoreService, private readonly formBuilder: FormBuilder ) { } @@ -66,7 +66,7 @@ export class AppFormComponent { const request = new CreateAppDto(this.createForm.controls['name'].value); - this.appsService.postApp(request) + this.appsStore.createApp(request) .subscribe(dto => { this.resetCreateForm(); this.emitCreated(dto); diff --git a/src/Squidex/app/shared/declarations-base.ts b/src/Squidex/app/shared/declarations-base.ts index dd1657237..fecc4272e 100644 --- a/src/Squidex/app/shared/declarations-base.ts +++ b/src/Squidex/app/shared/declarations-base.ts @@ -5,20 +5,22 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ +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-app.guard'; export * from './guards/resolve-content.guard'; export * from './guards/resolve-published-schema.guard'; export * from './guards/resolve-schema.guard'; export * from './guards/resolve-user.guard'; +export * from './guards/unset-app.guard'; export * from './interceptors/auth.interceptor'; export * from './services/app-contributors.service'; export * from './services/app-clients.service'; export * from './services/app-languages.service'; +export * from './services/apps-store.service'; export * from './services/apps.service'; export * from './services/assets.service'; export * from './services/auth.service'; diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts b/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts new file mode 100644 index 000000000..bc86f1e14 --- /dev/null +++ b/src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts @@ -0,0 +1,73 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { IMock, Mock } from 'typemoq'; +import { Observable } from 'rxjs'; + +import { AppsStoreService } from 'shared'; + +import { AppMustExistGuard } from './app-must-exist.guard'; +import { RouterMockup } from './router-mockup'; + +describe('AppMustExistGuard', () => { + let appsStore: IMock; + + beforeEach(() => { + appsStore = Mock.ofType(AppsStoreService); + }); + + it('should navigate to 404 page if app is not found', (done) => { + appsStore.setup(x => x.selectApp('my-app')) + .returns(() => Observable.of(false)); + const router = new RouterMockup(); + const route = { params: { appName: 'my-app' } }; + + const guard = new AppMustExistGuard(appsStore.object, router); + + guard.canActivate(route, {}) + .subscribe(result => { + expect(result).toBeFalsy(); + expect(router.lastNavigation).toEqual(['/404']); + + done(); + }); + }); + + it('should navigate to 404 page if app loading fails', (done) => { + appsStore.setup(x => x.selectApp('my-app')) + .returns(() => Observable.throw('error')); + const router = new RouterMockup(); + const route = { params: { appName: 'my-app' } }; + + const guard = new AppMustExistGuard(appsStore.object, router); + + guard.canActivate(route, {}) + .subscribe(result => { + expect(result).toBeFalsy(); + expect(router.lastNavigation).toEqual(['/404']); + + done(); + }); + }); + + it('should return true if app is found', (done) => { + appsStore.setup(x => x.selectApp('my-app')) + .returns(() => Observable.of(true)); + const router = new RouterMockup(); + const route = { params: { appName: 'my-app' } }; + + const guard = new AppMustExistGuard(appsStore.object, router); + + guard.canActivate(route, {}) + .subscribe(result => { + expect(result).toBeTruthy(); + expect(router.lastNavigation).toBeUndefined(); + + done(); + }); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/app-must-exist.guard.ts b/src/Squidex/app/shared/guards/app-must-exist.guard.ts new file mode 100644 index 000000000..945caf277 --- /dev/null +++ b/src/Squidex/app/shared/guards/app-must-exist.guard.ts @@ -0,0 +1,40 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { AppsStoreService } from './../services/apps-store.service'; + +@Injectable() +export class AppMustExistGuard implements CanActivate { + constructor( + private readonly appsStore: AppsStoreService, + private readonly router: Router + ) { + } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const appName = route.params['appName']; + + const result = + this.appsStore.selectApp(appName) + .do(dto => { + if (!dto) { + this.router.navigate(['/404']); + } + }) + .catch(error => { + this.router.navigate(['/404']); + + return Observable.of(false); + }); + + return result; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/resolve-app.guard.ts b/src/Squidex/app/shared/guards/resolve-app.guard.ts deleted file mode 100644 index e64c91b7a..000000000 --- a/src/Squidex/app/shared/guards/resolve-app.guard.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 { Observable } from 'rxjs'; - -import { allParams } from 'framework'; - -import { AppDto, AppsService } from './../services/apps.service'; - -@Injectable() -export class ResolveAppGuard implements Resolve { - constructor( - private readonly appsService: AppsService, - private readonly router: Router - ) { - } - - public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const params = allParams(route); - - const appName = params['appName']; - - if (!appName) { - throw 'Route must contain app name.'; - } - - return this.appsService.getApps().first().map(x => x.find(a => a.name === appName)) - .do(dto => { - if (!dto) { - this.router.navigate(['/404']); - } - }) - .catch(error => { - this.router.navigate(['/404']); - - return Observable.of(null); - }); - } -} \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/unset-app.guard.ts b/src/Squidex/app/shared/guards/unset-app.guard.ts new file mode 100644 index 000000000..0f7cf6084 --- /dev/null +++ b/src/Squidex/app/shared/guards/unset-app.guard.ts @@ -0,0 +1,25 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; + +import { AppsStoreService } from './../services/apps-store.service'; + +@Injectable() +export class UnsetAppGuard implements CanActivate { + constructor( + private readonly appsStore: AppsStoreService + ) { + } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + this.appsStore.selectApp(null); + + return true; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 0a9776fc3..eeec43d6b 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -16,6 +16,8 @@ import { AppClientsService, AppContributorsService, AppLanguagesService, + AppMustExistGuard, + AppsStoreService, AppsService, AssetComponent, AssetPreviewUrlPipe, @@ -36,7 +38,6 @@ import { MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, PlansService, - ResolveAppGuard, ResolveAppLanguagesGuard, ResolveContentGuard, ResolvePublishedSchemaGuard, @@ -45,6 +46,7 @@ import { ResolveUserGuard, RulesService, UIService, + UnsetAppGuard, UsagesService, UserDtoPicture, UserEmailPipe, @@ -109,7 +111,9 @@ export class SqxSharedModule { AppClientsService, AppContributorsService, AppLanguagesService, + AppMustExistGuard, AppsService, + AppsStoreService, AssetsService, AuthService, ContentsService, @@ -121,7 +125,6 @@ export class SqxSharedModule { MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, PlansService, - ResolveAppGuard, ResolveAppLanguagesGuard, ResolveContentGuard, ResolvePublishedSchemaGuard, @@ -130,6 +133,7 @@ export class SqxSharedModule { RulesService, SchemasService, UIService, + UnsetAppGuard, UsagesService, UserManagementService, UsersProviderService, diff --git a/src/Squidex/app/shared/services/apps-store.service.spec.ts b/src/Squidex/app/shared/services/apps-store.service.spec.ts new file mode 100644 index 000000000..96f86dded --- /dev/null +++ b/src/Squidex/app/shared/services/apps-store.service.spec.ts @@ -0,0 +1,107 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Observable } from 'rxjs'; +import { It, IMock, Mock, Times } from 'typemoq'; + +import { + AppDto, + AppsService, + AppsStoreService, + CreateAppDto, + DateTime +} from './../'; + +describe('AppsStoreService', () => { + const now = DateTime.now(); + + const oldApps = [ + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan'), + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan') + ]; + const newApp = new AppDto('id', 'new-name', 'Owner', now, now, 'Free', 'Plan'); + + let appsService: IMock; + + beforeEach(() => { + appsService = Mock.ofType(AppsService); + }); + + it('should load automatically', () => { + appsService.setup(x => x.getApps()) + .returns(() => Observable.of(oldApps)) + .verifiable(Times.once()); + + const store = new AppsStoreService(appsService.object); + + let result1: AppDto[] | null = null; + let result2: AppDto[] | null = null; + + store.apps.subscribe(x => { + result1 = x; + }).unsubscribe(); + + store.apps.subscribe(x => { + result2 = x; + }).unsubscribe(); + + expect(result1).toEqual(oldApps); + expect(result2).toEqual(oldApps); + + appsService.verifyAll(); + }); + + it('should add app to cache when created', () => { + appsService.setup(x => x.getApps()) + .returns(() => Observable.of(oldApps)) + .verifiable(Times.once()); + + appsService.setup(x => x.postApp(It.isAny())) + .returns(() => Observable.of(newApp)) + .verifiable(Times.once()); + + const store = new AppsStoreService(appsService.object); + + let result1: AppDto[] | null = null; + let result2: AppDto[] | null = null; + + store.apps.subscribe(x => { + result1 = x; + }).unsubscribe(); + + store.createApp(new CreateAppDto('new-name'), now).subscribe(); + + store.apps.subscribe(x => { + result2 = x; + }).unsubscribe(); + + expect(result1).toEqual(oldApps); + expect(JSON.stringify(result2)).toEqual(JSON.stringify(oldApps.concat([newApp]))); + + appsService.verifyAll(); + }); + + it('should select app', (done) => { + appsService.setup(x => x.getApps()) + .returns(() => Observable.of(oldApps)) + .verifiable(Times.once()); + + const store = new AppsStoreService(appsService.object); + + store.selectApp('old-name').subscribe(isSelected => { + expect(isSelected).toBeTruthy(); + + appsService.verifyAll(); + + done(); + }, err => { + expect(err).toBeNull(); + + done(); + }); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps-store.service.ts b/src/Squidex/app/shared/services/apps-store.service.ts new file mode 100644 index 000000000..f9590f77c --- /dev/null +++ b/src/Squidex/app/shared/services/apps-store.service.ts @@ -0,0 +1,70 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable, Observer, ReplaySubject } from 'rxjs'; + +import { DateTime } from 'framework'; + +import { + AppDto, + AppsService, + CreateAppDto +} from './apps.service'; + +@Injectable() +export class AppsStoreService { + private readonly apps$ = new ReplaySubject(1); + private readonly app$ = new BehaviorSubject(null); + + public get apps(): Observable { + return this.apps$; + } + + public get selectedApp(): Observable { + return this.app$; + } + + constructor( + private readonly appsService: AppsService + ) { + if (!appsService) { + return; + } + + this.appsService.getApps() + .subscribe(apps => { + this.apps$.next(apps); + }, error => { + this.apps$.next([]); + }); + } + + public selectApp(name: string | null): Observable { + return Observable.create((observer: Observer) => { + this.apps$.subscribe(apps => { + const app = apps.find(x => x.name === name) || null; + + this.app$.next(app); + + observer.next(app !== null); + observer.complete(); + }, error => { + observer.error(error); + }); + }); + } + + public createApp(dto: CreateAppDto, now?: DateTime): Observable { + return this.appsService.postApp(dto) + .do(app => { + this.apps$.first().subscribe(apps => { + this.apps$.next(apps.concat([app])); + }); + }); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/src/Squidex/app/shared/services/apps.service.spec.ts index 4f29d6075..d255b7f23 100644 --- a/src/Squidex/app/shared/services/apps.service.spec.ts +++ b/src/Squidex/app/shared/services/apps.service.spec.ts @@ -57,20 +57,24 @@ describe('AppsService', () => { name: 'name1', permission: 'Owner', created: '2016-01-01', - lastModified: '2016-02-02' + lastModified: '2016-02-02', + planName: 'Free', + planUpgrade: 'Basic' }, { id: '456', name: 'name2', permission: 'Owner', created: '2017-01-01', - lastModified: '2017-02-02' + lastModified: '2017-02-02', + planName: 'Basic', + planUpgrade: 'Enterprise' } ]); expect(apps).toEqual([ - new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02')), - new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02')) + new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'), + new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise') ]); })); @@ -90,8 +94,13 @@ describe('AppsService', () => { expect(req.request.method).toEqual('POST'); expect(req.request.headers.get('If-Match')).toBeNull(); - req.flush({ id: '123' }); + req.flush({ + id: '123', + permission: 'Reader', + planName: 'Basic', + planUpgrade: 'Enterprise' + }); - expect(app).toEqual(new AppDto('123', dto.name, 'Owner', now, now)); + expect(app).toEqual(new AppDto('123', dto.name, 'Reader', now, now, 'Basic', 'Enterprise')); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index 0e2634049..d1f99b348 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -7,7 +7,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, ReplaySubject } from 'rxjs'; +import { Observable } from 'rxjs'; import 'framework/angular/http-extensions'; @@ -24,7 +24,9 @@ export class AppDto { public readonly name: string, public readonly permission: string, public readonly created: DateTime, - public readonly lastModified: DateTime + public readonly lastModified: DateTime, + public readonly planName: string, + public readonly planUpgrade: string ) { } } @@ -38,8 +40,6 @@ export class CreateAppDto { @Injectable() export class AppsService { - private apps$: ReplaySubject | null = null; - constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, @@ -48,38 +48,26 @@ export class AppsService { } public getApps(): Observable { - if (this.apps$ === null) { - const url = this.apiUrl.buildUrl('/api/apps'); - - const loadedApps = - HTTP.getVersioned(this.http, url) - .map(response => { - const body = response.payload.body; - - const items: any[] = body; - - return items.map(item => { - return new AppDto( - item.id, - item.name, - item.permission, - DateTime.parseISO(item.created), - DateTime.parseISO(item.lastModified)); - }); - }) - .pretifyError('Failed to load apps. Please reload.'); - - this.apps$ = new ReplaySubject(1); + const url = this.apiUrl.buildUrl('/api/apps'); - loadedApps - .subscribe(apps => { - this.apps$.next(apps); - }, error => { - this.apps$.error(loadedApps); - }); - } + return HTTP.getVersioned(this.http, url) + .map(response => { + const body = response.payload.body; - return this.apps$; + const items: any[] = body; + + return items.map(item => { + return new AppDto( + item.id, + item.name, + item.permission, + DateTime.parseISO(item.created), + DateTime.parseISO(item.lastModified), + item.planName, + item.planUpgrade); + }); + }) + .pretifyError('Failed to load apps. Please reload.'); } public postApp(dto: CreateAppDto, now?: DateTime): Observable { @@ -91,16 +79,10 @@ export class AppsService { now = now || DateTime.now(); - return new AppDto(body.id, dto.name, 'Owner', now, now); + return new AppDto(body.id, dto.name, body.permission, now, now, body.planName, body.planUpgrade); }) - .do(app => { + .do(() => { this.analytics.trackEvent('App', 'Created', dto.name); - - if (this.apps$) { - this.apps$.first().subscribe(apps => { - this.apps$.next(apps.concat([app])); - }); - } }) .pretifyError('Failed to create app. Please reload.'); } diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.html b/src/Squidex/app/shell/pages/internal/apps-menu.component.html index 17b6fba5d..27315a568 100644 --- a/src/Squidex/app/shell/pages/internal/apps-menu.component.html +++ b/src/Squidex/app/shell/pages/internal/apps-menu.component.html @@ -21,6 +21,17 @@ + +