From b9104c091023bc3c702ec8becb94a62b51584acd Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 27 Apr 2019 20:18:45 +0200 Subject: [PATCH] More progress --- .../state/event-consumers.state.spec.ts | 104 ++++--- .../state/event-consumers.state.ts | 33 +-- .../administration/state/users.state.spec.ts | 254 ++++++++++-------- .../administration/state/users.state.ts | 46 ++-- .../rules/pages/rules/rules-page.component.ts | 13 +- .../pages/backups/backups-page.component.ts | 8 +- .../app/framework/utils/immutable-array.ts | 5 - .../shared/components/comments.component.ts | 6 +- .../app/shared/state/apps.state.spec.ts | 19 +- src/Squidex/app/shared/state/apps.state.ts | 107 +++++--- .../app/shared/state/backups.state.spec.ts | 100 ++++--- src/Squidex/app/shared/state/backups.state.ts | 81 +++--- .../app/shared/state/clients.state.spec.ts | 94 ++++--- src/Squidex/app/shared/state/clients.state.ts | 35 +-- .../app/shared/state/comments.state.spec.ts | 108 ++++---- .../app/shared/state/comments.state.ts | 29 +- .../shared/state/contributors.state.spec.ts | 134 ++++----- .../app/shared/state/contributors.state.ts | 28 +- .../app/shared/state/languages.state.spec.ts | 203 +++++++------- .../app/shared/state/languages.state.ts | 53 ++-- .../app/shared/state/patterns.state.spec.ts | 98 ++++--- .../app/shared/state/patterns.state.ts | 107 +++++--- .../app/shared/state/roles.state.spec.ts | 92 ++++--- src/Squidex/app/shared/state/roles.state.ts | 6 +- .../app/shared/state/rules.state.spec.ts | 154 ++++++----- src/Squidex/app/shared/state/rules.state.ts | 143 ++++++---- 26 files changed, 1156 insertions(+), 904 deletions(-) diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts index a5b6120b4..9020ad510 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts +++ b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts @@ -28,90 +28,88 @@ describe('EventConsumersState', () => { dialogs = Mock.ofType(); eventConsumersService = Mock.ofType(); - - eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of(oldConsumers)).verifiable(Times.atLeastOnce()); - eventConsumersState = new EventConsumersState(dialogs.object, eventConsumersService.object); - eventConsumersState.load().subscribe(); }); afterEach(() => { eventConsumersService.verifyAll(); }); - it('should load event consumers', () => { - expect(eventConsumersState.snapshot.eventConsumers.values).toEqual(oldConsumers); - expect(eventConsumersState.snapshot.isLoaded).toBeTruthy(); + describe('Loading', () => { + it('should load event consumers', () => { + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => of(oldConsumers)).verifiable(); - expect().nothing(); + eventConsumersState.load().subscribe(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); - }); + expect(eventConsumersState.snapshot.eventConsumers.values).toEqual(oldConsumers); + expect(eventConsumersState.snapshot.isLoaded).toBeTruthy(); - it('should show notification on load when reload is true', () => { - eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of(oldConsumers)); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - eventConsumersState.load(true).subscribe(); + it('should show notification on load when reload is true', () => { + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => of(oldConsumers)).verifiable(); - expect().nothing(); - - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); - }); + eventConsumersState.load(true).subscribe(); - it('should show notification on load error when silent is false', () => { - eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => throwError({})); + expect().nothing(); - eventConsumersState.load(true, false).pipe(onErrorResumeNext()).subscribe(); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); - expect().nothing(); + it('should show notification on load error when silent is false', () => { + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => throwError({})).verifiable(); - dialogs.verify(x => x.notifyError(It.isAny()), Times.once()); - }); + eventConsumersState.load(true, false).pipe(onErrorResumeNext()).subscribe(); - it('should not show notification on load error when silent is true', () => { - eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => throwError({})); + expect().nothing(); - eventConsumersState.load(true, true).pipe(onErrorResumeNext()).subscribe(); + dialogs.verify(x => x.notifyError(It.isAny()), Times.once()); + }); + }); - expect().nothing(); + describe('Updates', () => { + beforeEach(() => { + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => of(oldConsumers)).verifiable(); - dialogs.verify(x => x.notifyError(It.isAny()), Times.never()); - }); + eventConsumersState.load().subscribe(); + }); - it('should unmark as stopped when started', () => { - eventConsumersService.setup(x => x.putStart(oldConsumers[1].name)) - .returns(() => of({})).verifiable(); + it('should unmark as stopped when started', () => { + eventConsumersService.setup(x => x.putStart(oldConsumers[1].name)) + .returns(() => of({})).verifiable(); - eventConsumersState.start(oldConsumers[1]).subscribe(); + eventConsumersState.start(oldConsumers[1]).subscribe(); - const es_1 = eventConsumersState.snapshot.eventConsumers.at(1); + const es_1 = eventConsumersState.snapshot.eventConsumers.at(1); - expect(es_1.isStopped).toBeFalsy(); - }); + expect(es_1.isStopped).toBeFalsy(); + }); - it('should mark as stopped when stopped', () => { - eventConsumersService.setup(x => x.putStop(oldConsumers[0].name)) - .returns(() => of({})).verifiable(); + it('should mark as stopped when stopped', () => { + eventConsumersService.setup(x => x.putStop(oldConsumers[0].name)) + .returns(() => of({})).verifiable(); - eventConsumersState.stop(oldConsumers[0]).subscribe(); + eventConsumersState.stop(oldConsumers[0]).subscribe(); - const es_1 = eventConsumersState.snapshot.eventConsumers.at(0); + const es_1 = eventConsumersState.snapshot.eventConsumers.at(0); - expect(es_1.isStopped).toBeTruthy(); - }); + expect(es_1.isStopped).toBeTruthy(); + }); - it('should mark as resetting when reset', () => { - eventConsumersService.setup(x => x.putReset(oldConsumers[0].name)) - .returns(() => of({})).verifiable(); + it('should mark as resetting when reset', () => { + eventConsumersService.setup(x => x.putReset(oldConsumers[0].name)) + .returns(() => of({})).verifiable(); - eventConsumersState.reset(oldConsumers[0]).subscribe(); + eventConsumersState.reset(oldConsumers[0]).subscribe(); - const es_1 = eventConsumersState.snapshot.eventConsumers.at(0); + const es_1 = eventConsumersState.snapshot.eventConsumers.at(0); - expect(es_1.isResetting).toBeTruthy(); + expect(es_1.isResetting).toBeTruthy(); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.ts b/src/Squidex/app/features/administration/state/event-consumers.state.ts index 5cd3e9d1d..8ca47e0e1 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.ts +++ b/src/Squidex/app/features/administration/state/event-consumers.state.ts @@ -10,7 +10,6 @@ import { Observable } from 'rxjs'; import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { - array, DialogService, ImmutableArray, State @@ -50,15 +49,17 @@ export class EventConsumersState extends State { this.resetState(); } - const stream = + const http$ = this.eventConsumersService.getEventConsumers().pipe( - map(dtos => array(dtos)), share()); + share()); - stream.subscribe(eventConsumers => { + http$.subscribe(response => { if (isReload && !silent) { this.dialogs.notifyInfo('Event Consumers reloaded.'); } + const eventConsumers = ImmutableArray.of(response); + this.next(s => { return { ...s, eventConsumers, isLoaded: true }; }); @@ -69,41 +70,41 @@ export class EventConsumersState extends State { } }); - return stream; + return http$; } public start(eventConsumer: EventConsumerDto): Observable { - const stream = + const http$ = this.eventConsumersService.putStart(eventConsumer.name).pipe( map(() => setStopped(eventConsumer, false), share())); - this.updateState(stream); + this.updateState(http$); - return stream; + return http$; } public stop(eventConsumer: EventConsumerDto): Observable { - const stream = + const http$ = this.eventConsumersService.putStop(eventConsumer.name).pipe( map(() => setStopped(eventConsumer, true), share())); - this.updateState(stream); + this.updateState(http$); - return stream; + return http$; } public reset(eventConsumer: EventConsumerDto): Observable { - const stream = + const http$ = this.eventConsumersService.putReset(eventConsumer.name).pipe( map(() => reset(eventConsumer), share())); - this.updateState(stream); + this.updateState(http$); - return stream; + return http$; } - private updateState(stream: Observable) { - stream.subscribe(updated => { + private updateState(http$: Observable) { + http$.subscribe(updated => { this.replaceEventConsumer(updated); }, error => { this.dialogs.notifyError(error); diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/src/Squidex/app/features/administration/state/users.state.spec.ts index 3d799ff27..235108d73 100644 --- a/src/Squidex/app/features/administration/state/users.state.spec.ts +++ b/src/Squidex/app/features/administration/state/users.state.spec.ts @@ -40,181 +40,199 @@ describe('UsersState', () => { dialogs = Mock.ofType(); usersService = Mock.ofType(); - - usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of(new UsersDto(200, oldUsers))).verifiable(Times.atLeastOnce()); - usersState = new UsersState(authService.object, dialogs.object, usersService.object); - usersState.load().subscribe(); }); afterEach(() => { usersService.verifyAll(); }); - it('should load users', () => { - expect(usersState.snapshot.users.values).toEqual([ - { isCurrentUser: false, user: oldUsers[0] }, - { isCurrentUser: true, user: oldUsers[1] } - ]); - expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200); - expect(usersState.snapshot.isLoaded).toBeTruthy(); + describe('Loading', () => { + it('should load users', () => { + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => of(new UsersDto(200, oldUsers))).verifiable(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); - }); + usersState.load().subscribe(); + + expect(usersState.snapshot.users.values).toEqual([ + { isCurrentUser: false, user: oldUsers[0] }, + { isCurrentUser: true, user: oldUsers[1] } + ]); + expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200); + expect(usersState.snapshot.isLoaded).toBeTruthy(); - it('should show notification on load when reload is true', () => { - usersState.load(true).subscribe(); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - expect().nothing(); + it('should show notification on load when reload is true', () => { + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => of(new UsersDto(200, oldUsers))).verifiable(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); - }); + usersState.load(true).subscribe(); - it('should replace selected user when reloading', () => { - usersState.select('id1').subscribe(); + expect().nothing(); - const newUsers = [ - new UserDto('id1', 'mail1@mail.de_new', 'name1_new', ['Permission1_New'], false), - new UserDto('id2', 'mail2@mail.de_new', 'name2_new', ['Permission2_New'], true) - ]; + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); - usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of(new UsersDto(200, newUsers))); + it('should replace selected user when reloading', () => { + const newUsers = [ + new UserDto('id1', 'mail1@mail.de_new', 'name1_new', ['Permission1_New'], false), + new UserDto('id2', 'mail2@mail.de_new', 'name2_new', ['Permission2_New'], true) + ]; - usersState.load().subscribe(); + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => of(new UsersDto(200, oldUsers))).verifiable(Times.exactly(2)); - expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUsers[0] }); - }); + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => of(new UsersDto(200, newUsers))); - it('should return user on select and not load when already loaded', () => { - let selectedUser: SnapshotUser; + usersState.load().subscribe(); + usersState.select('id1').subscribe(); + usersState.load().subscribe(); - usersState.select('id1').subscribe(x => { - selectedUser = x!; + expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUsers[0] }); }); - expect(selectedUser!.user).toEqual(oldUsers[0]); - expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: oldUsers[0] }); - }); + it('should load next page and prev page when paging', () => { + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => of(new UsersDto(200, oldUsers))).verifiable(Times.exactly(2)); - it('should return user on select and load when not loaded', () => { - usersService.setup(x => x.getUser('id3')) - .returns(() => of(newUser)); + usersService.setup(x => x.getUsers(10, 10, undefined)) + .returns(() => of(new UsersDto(200, []))).verifiable(); - let selectedUser: SnapshotUser; + usersState.load().subscribe(); + usersState.goNext().subscribe(); + usersState.goPrev().subscribe(); - usersState.select('id3').subscribe(x => { - selectedUser = x!; + expect().nothing(); }); - expect(selectedUser!.user).toEqual(newUser); - expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUser }); + it('should load with query when searching', () => { + usersService.setup(x => x.getUsers(10, 0, 'my-query')) + .returns(() => of(new UsersDto(0, []))).verifiable(); + + usersState.search('my-query').subscribe(); + + expect(usersState.snapshot.usersQuery).toEqual('my-query'); + }); }); - it('should return null on select when unselecting user', () => { - let selectedUser: SnapshotUser; + describe('Updates', () => { + beforeEach(() => { + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => of(new UsersDto(200, oldUsers))).verifiable(); - usersState.select(null).subscribe(x => { - selectedUser = x!; + usersState.load().subscribe(); }); - expect(selectedUser!).toBeNull(); - expect(usersState.snapshot.selectedUser).toBeNull(); - }); + it('should return user on select and not load when already loaded', () => { + let selectedUser: SnapshotUser; - it('should return null on select when user is not found', () => { - usersService.setup(x => x.getUser('unknown')) - .returns(() => throwError({})).verifiable(); + usersState.select('id1').subscribe(x => { + selectedUser = x!; + }); - let selectedUser: SnapshotUser; + expect(selectedUser!.user).toEqual(oldUsers[0]); + expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: oldUsers[0] }); + }); - usersState.select('unknown').subscribe(x => { - selectedUser = x!; - }).unsubscribe(); + it('should return user on select and load when not loaded', () => { + usersService.setup(x => x.getUser('id3')) + .returns(() => of(newUser)); - expect(selectedUser!).toBeNull(); - expect(usersState.snapshot.selectedUser).toBeNull(); - }); + let selectedUser: SnapshotUser; - it('should mark as locked when locked', () => { - usersService.setup(x => x.lockUser('id1')) - .returns(() => of({})).verifiable(); + usersState.select('id3').subscribe(x => { + selectedUser = x!; + }); - usersState.select('id1').subscribe(); - usersState.lock(oldUsers[0]).subscribe(); + expect(selectedUser!.user).toEqual(newUser); + expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUser }); + }); - const user_1 = usersState.snapshot.users.at(0); + it('should return null on select when unselecting user', () => { + let selectedUser: SnapshotUser; - expect(user_1.user.isLocked).toBeTruthy(); - expect(user_1).toBe(usersState.snapshot.selectedUser!); - }); + usersState.select(null).subscribe(x => { + selectedUser = x!; + }); - it('should unmark as locked when unlocked', () => { - usersService.setup(x => x.unlockUser('id2')) - .returns(() => of({})).verifiable(); + expect(selectedUser!).toBeNull(); + expect(usersState.snapshot.selectedUser).toBeNull(); + }); - usersState.select('id2').subscribe(); - usersState.unlock(oldUsers[1]).subscribe(); + it('should return null on select when user is not found', () => { + usersService.setup(x => x.getUser('unknown')) + .returns(() => throwError({})).verifiable(); - const user_1 = usersState.snapshot.users.at(1); + let selectedUser: SnapshotUser; - expect(user_1.user.isLocked).toBeFalsy(); - expect(user_1).toBe(usersState.snapshot.selectedUser!); - }); + usersState.select('unknown').subscribe(x => { + selectedUser = x!; + }).unsubscribe(); - it('should update user properties when updated', () => { - const request = { email: 'new@mail.com', displayName: 'New', permissions: ['Permission1'] }; + expect(selectedUser!).toBeNull(); + expect(usersState.snapshot.selectedUser).toBeNull(); + }); - usersService.setup(x => x.putUser('id1', request)) - .returns(() => of({})).verifiable(); + it('should mark as locked when locked', () => { + usersService.setup(x => x.lockUser('id1')) + .returns(() => of({})).verifiable(); - usersState.select('id1').subscribe(); - usersState.update(oldUsers[0], request).subscribe(); + usersState.select('id1').subscribe(); + usersState.lock(oldUsers[0]).subscribe(); - const user_1 = usersState.snapshot.users.at(0); + const user_1 = usersState.snapshot.users.at(0); - expect(user_1.user.email).toEqual(request.email); - expect(user_1.user.displayName).toEqual(request.displayName); - expect(user_1.user.permissions).toEqual(request.permissions); - expect(user_1).toBe(usersState.snapshot.selectedUser!); - }); + expect(user_1.user.isLocked).toBeTruthy(); + expect(user_1).toBe(usersState.snapshot.selectedUser!); + }); - it('should add user to snapshot when created', () => { - const request = { ...newUser, password: 'password' }; + it('should unmark as locked when unlocked', () => { + usersService.setup(x => x.unlockUser('id2')) + .returns(() => of({})).verifiable(); - usersService.setup(x => x.postUser(request)) - .returns(() => of(newUser)).verifiable(); + usersState.select('id2').subscribe(); + usersState.unlock(oldUsers[1]).subscribe(); - usersState.create(request).subscribe(); + const user_1 = usersState.snapshot.users.at(1); - expect(usersState.snapshot.users.values).toEqual([ - { isCurrentUser: false, user: newUser }, - { isCurrentUser: false, user: oldUsers[0] }, - { isCurrentUser: true, user: oldUsers[1] } - ]); - expect(usersState.snapshot.usersPager.numberOfItems).toBe(201); - }); + expect(user_1.user.isLocked).toBeFalsy(); + expect(user_1).toBe(usersState.snapshot.selectedUser!); + }); - it('should load next page and prev page when paging', () => { - usersService.setup(x => x.getUsers(10, 10, undefined)) - .returns(() => of(new UsersDto(200, []))).verifiable(); + it('should update user properties when updated', () => { + const request = { email: 'new@mail.com', displayName: 'New', permissions: ['Permission1'] }; - usersState.goNext().subscribe(); - usersState.goPrev().subscribe(); + usersService.setup(x => x.putUser('id1', request)) + .returns(() => of({})).verifiable(); - expect().nothing(); + usersState.select('id1').subscribe(); + usersState.update(oldUsers[0], request).subscribe(); - usersService.verify(x => x.getUsers(10, 10, undefined), Times.once()); - usersService.verify(x => x.getUsers(10, 0, undefined), Times.exactly(2)); - }); + const user_1 = usersState.snapshot.users.at(0); - it('should load with query when searching', () => { - usersService.setup(x => x.getUsers(10, 0, 'my-query')) - .returns(() => of(new UsersDto(0, []))).verifiable(); + expect(user_1.user.email).toEqual(request.email); + expect(user_1.user.displayName).toEqual(request.displayName); + expect(user_1.user.permissions).toEqual(request.permissions); + expect(user_1).toBe(usersState.snapshot.selectedUser!); + }); - usersState.search('my-query').subscribe(); + it('should add user to snapshot when created', () => { + const request = { ...newUser, password: 'password' }; - expect(usersState.snapshot.usersQuery).toEqual('my-query'); + usersService.setup(x => x.postUser(request)) + .returns(() => of(newUser)).verifiable(); + + usersState.create(request).subscribe(); + + expect(usersState.snapshot.users.values).toEqual([ + { isCurrentUser: false, user: newUser }, + { isCurrentUser: false, user: oldUsers[0] }, + { isCurrentUser: true, user: oldUsers[1] } + ]); + expect(usersState.snapshot.usersPager.numberOfItems).toBe(201); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index 2e0e7599b..51278708e 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/src/Squidex/app/features/administration/state/users.state.ts @@ -12,7 +12,6 @@ import { catchError, distinctUntilChanged, map, share } from 'rxjs/operators'; import '@app/framework/utils/rxjs-extensions'; import { - array, AuthService, DialogService, ImmutableArray, @@ -82,13 +81,15 @@ export class UsersState extends State { } public select(id: string | null): Observable { - const stream = this.loadUser(id).pipe(share()); + const http$ = + this.loadUser(id).pipe( + share()); - stream.subscribe(selectedUser => { + http$.subscribe(selectedUser => { this.next(s => ({ ...s, selectedUser })); }); - return stream; + return http$; } private loadUser(id: string | null) { @@ -114,20 +115,21 @@ export class UsersState extends State { } private loadInternal(isReload = false): Observable { - const stream = + const http$ = this.usersService.getUsers( this.snapshot.usersPager.pageSize, this.snapshot.usersPager.skip, this.snapshot.usersQuery).pipe( - map(({ total, items }) => ({ total, users: array(items.map(x => this.createUser(x))) })), share()); + share()); - stream.subscribe(({ total, users }) => { + http$.subscribe(response => { if (isReload) { this.dialogs.notifyInfo('Users reloaded.'); } this.next(s => { - const usersPager = s.usersPager.setCount(total); + const usersPager = s.usersPager.setCount(response.total); + const users = ImmutableArray.of(response.items.map(x => this.createUser(x))); let selectedUser = s.selectedUser; @@ -142,13 +144,15 @@ export class UsersState extends State { this.dialogs.notifyError(error); }); - return stream; + return http$; } public create(request: CreateUserDto): Observable { - const stream = this.usersService.postUser(request).pipe(share()); + const http$ = + this.usersService.postUser(request).pipe( + share()); - stream.subscribe(dto => { + http$.subscribe(dto => { this.next(s => { const users = s.users.pushFront(this.createUser(dto)); const usersPager = s.usersPager.incrementCount(); @@ -157,37 +161,37 @@ export class UsersState extends State { }); }); - return stream; + return http$; } public update(user: UserDto, request: UpdateUserDto): Observable { - const stream = + const http$ = this.usersService.putUser(user.id, request).pipe( map(() => update(user, request)), share()); - this.updateState(stream, false); + this.updateState(http$, false); - return stream; + return http$; } public lock(user: UserDto): Observable { - const stream = + const http$ = this.usersService.lockUser(user.id).pipe( map(() => setLocked(user, true)), share()); - this.updateState(stream, true); + this.updateState(http$, true); - return stream; + return http$; } public unlock(user: UserDto): Observable { - const stream = + const http$ = this.usersService.unlockUser(user.id).pipe( map(() => setLocked(user, false)), share()); - this.updateState(stream, true); + this.updateState(http$, true); - return stream; + return http$; } public search(query: string): Observable { diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts index 3c21249db..ff75e7716 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts @@ -6,7 +6,6 @@ */ import { Component, OnInit } from '@angular/core'; -import { onErrorResumeNext } from 'rxjs/operators'; import { ALL_TRIGGERS, @@ -42,29 +41,29 @@ export class RulesPageComponent implements OnInit { } public ngOnInit() { - this.rulesState.load().pipe(onErrorResumeNext()).subscribe(); + this.rulesState.load(); this.rulesService.getActions() .subscribe(actions => { this.ruleActions = actions; }); - this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); + this.schemasState.load(); } public reload() { - this.rulesState.load(true).pipe(onErrorResumeNext()).subscribe(); + this.rulesState.load(true); } public delete(rule: RuleDto) { - this.rulesState.delete(rule).pipe(onErrorResumeNext()).subscribe(); + this.rulesState.delete(rule); } public toggle(rule: RuleDto) { if (rule.isEnabled) { - this.rulesState.disable(rule).pipe(onErrorResumeNext()).subscribe(); + this.rulesState.disable(rule); } else { - this.rulesState.enable(rule).pipe(onErrorResumeNext()).subscribe(); + this.rulesState.enable(rule); } } diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts index 281c4f9e6..9a0f1b700 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts @@ -30,7 +30,7 @@ export class BackupsPageComponent extends ResourceOwner implements OnInit { } public ngOnInit() { - this.backupsState.load().pipe(onErrorResumeNext()).subscribe(); + this.backupsState.load(); this.own( timer(3000, 3000).pipe(switchMap(() => this.backupsState.load(true, true).pipe(onErrorResumeNext()))) @@ -38,15 +38,15 @@ export class BackupsPageComponent extends ResourceOwner implements OnInit { } public reload() { - this.backupsState.load(true, false).pipe(onErrorResumeNext()).subscribe(); + this.backupsState.load(true, false); } public start() { - this.backupsState.start().pipe(onErrorResumeNext()).subscribe(); + this.backupsState.start(); } public delete(backup: BackupDto) { - this.backupsState.delete(backup).pipe(onErrorResumeNext()).subscribe(); + this.backupsState.delete(backup); } public trackByBackup(index: number, item: BackupDto) { diff --git a/src/Squidex/app/framework/utils/immutable-array.ts b/src/Squidex/app/framework/utils/immutable-array.ts index ecb1a9d3f..0779e947d 100644 --- a/src/Squidex/app/framework/utils/immutable-array.ts +++ b/src/Squidex/app/framework/utils/immutable-array.ts @@ -17,11 +17,6 @@ function freeze(items: T[]): T[] { return items; } - -export function array(items?: V[]): ImmutableArray { - return ImmutableArray.of(items); -} - export class ImmutableArray implements Iterable { private static readonly EMPTY = new ImmutableArray([]); private readonly items: T[]; diff --git a/src/Squidex/app/shared/components/comments.component.ts b/src/Squidex/app/shared/components/comments.component.ts index 5fe06b302..7f02e8476 100644 --- a/src/Squidex/app/shared/components/comments.component.ts +++ b/src/Squidex/app/shared/components/comments.component.ts @@ -56,18 +56,18 @@ export class CommentsComponent extends ResourceOwner implements OnInit { } public delete(comment: CommentDto) { - this.state.delete(comment.id).pipe(onErrorResumeNext()).subscribe(); + this.state.delete(comment); } public update(comment: CommentDto, text: string) { - this.state.update(comment.id, text).pipe(onErrorResumeNext()).subscribe(); + this.state.update(comment, text); } public comment() { const value = this.commentForm.submit(); if (value) { - this.state.create(value.text).pipe(onErrorResumeNext()).subscribe(); + this.state.create(value.text); this.commentForm.submitCompleted(); } diff --git a/src/Squidex/app/shared/state/apps.state.spec.ts b/src/Squidex/app/shared/state/apps.state.spec.ts index 83eecd9ea..42b547fbd 100644 --- a/src/Squidex/app/shared/state/apps.state.spec.ts +++ b/src/Squidex/app/shared/state/apps.state.spec.ts @@ -37,13 +37,16 @@ describe('AppsState', () => { appsService = Mock.ofType(); appsService.setup(x => x.getApps()) - .returns(() => of(oldApps)) - .verifiable(Times.once()); + .returns(() => of(oldApps)).verifiable(); appsState = new AppsState(appsService.object, dialogs.object); appsState.load().subscribe(); }); + afterEach(() => { + appsService.verifyAll(); + }); + it('should load apps', () => { expect(appsState.snapshot.apps.values).toEqual(oldApps); }); @@ -53,7 +56,7 @@ describe('AppsState', () => { appsState.select(oldApps[0].name).subscribe(x => { selectedApp = x!; - }).unsubscribe(); + }); expect(selectedApp!).toBe(oldApps[0]); expect(appsState.snapshot.selectedApp).toBe(oldApps[0]); @@ -64,7 +67,7 @@ describe('AppsState', () => { appsState.select(null).subscribe(x => { selectedApp = x!; - }).unsubscribe(); + }); expect(selectedApp!).toBeNull(); expect(appsState.snapshot.selectedApp).toBeNull(); @@ -75,7 +78,7 @@ describe('AppsState', () => { appsState.select('unknown').subscribe(x => { selectedApp = x!; - }).unsubscribe(); + }); expect(selectedApp!).toBeNull(); expect(appsState.snapshot.selectedApp).toBeNull(); @@ -85,7 +88,7 @@ describe('AppsState', () => { const request = { ...newApp }; appsService.setup(x => x.postApp(request)) - .returns(() => of(newApp)); + .returns(() => of(newApp)).verifiable(); appsState.create(request).subscribe(); @@ -96,10 +99,10 @@ describe('AppsState', () => { const request = { ...newApp }; appsService.setup(x => x.postApp(request)) - .returns(() => of(newApp)); + .returns(() => of(newApp)).verifiable(); appsService.setup(x => x.deleteApp(newApp.name)) - .returns(() => of({})); + .returns(() => of({})).verifiable(); appsState.create(request).subscribe(); diff --git a/src/Squidex/app/shared/state/apps.state.ts b/src/Squidex/app/shared/state/apps.state.ts index 388825ab6..4da6f9dcd 100644 --- a/src/Squidex/app/shared/state/apps.state.ts +++ b/src/Squidex/app/shared/state/apps.state.ts @@ -7,12 +7,11 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { DialogService, ImmutableArray, - notify, State } from '@app/framework'; @@ -24,12 +23,14 @@ import { interface Snapshot { // All apps, loaded once. - apps: ImmutableArray; + apps: AppsList; // The selected app. selectedApp: AppDto | null; } +type AppsList = ImmutableArray; + function sameApp(lhs: AppDto, rhs?: AppDto): boolean { return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id); } @@ -56,50 +57,78 @@ export class AppsState extends State { } public select(name: string | null): Observable { - const observable = - !name ? - of(null) : - of(this.snapshot.apps.find(x => x.name === name) || null); - - return observable.pipe( - tap(selectedApp => { - this.next(s => ({ ...s, selectedApp })); - })); + const http$ = + this.loadApp(name) + .pipe(share()); + + http$.subscribe(selectedApp => { + this.next(s => ({ ...s, selectedApp })); + }); + + return http$; + } + + private loadApp(name: string | null) { + return of(name ? this.snapshot.apps.find(x => x.name === name) || null : null); } public load(): Observable { - return this.appsService.getApps().pipe( - tap((dto: AppDto[]) => { - this.next(s => { - const apps = ImmutableArray.of(dto); - - return { ...s, apps }; - }); - })); + const http$ = + this.appsService.getApps().pipe( + share()); + + http$.subscribe(response => { + this.next(s => { + const apps = ImmutableArray.of(response).sortByStringAsc(x => x.name); + + return { ...s, apps }; + }); + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } public create(request: CreateAppDto): Observable { - return this.appsService.postApp(request).pipe( - tap(dto => { - this.next(s => { - const apps = s.apps.push(dto).sortByStringAsc(x => x.name); - - return { ...s, apps }; - }); - })); - } + const http$ = + this.appsService.postApp(request).pipe( + share()); - public delete(name: string): Observable { - return this.appsService.deleteApp(name).pipe( - tap(() => { - this.next(s => { - const apps = s.apps.filter(x => x.name !== name); + http$.subscribe(app => { + this.next(s => { + const apps = s.apps.push(app).sortByStringAsc(x => x.name); - const selectedApp = s.selectedApp && s.selectedApp.name === name ? null : s.selectedApp; + return { ...s, apps }; + }); + }, error => { + this.dialogs.notifyError(error); + }); - return { ...s, apps, selectedApp }; - }); - }), - notify(this.dialogs)); + return http$; + } + + public delete(name: string): Observable { + const http$ = + this.appsService.deleteApp(name).pipe( + share()); + + http$.subscribe(() => { + this.next(s => { + const apps = s.apps.filter(x => x.name !== name); + + const selectedApp = + s.selectedApp && + s.selectedApp.name === name ? + null : + s.selectedApp; + + return { ...s, apps, selectedApp }; + }); + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } } \ No newline at end of file diff --git a/src/Squidex/app/shared/state/backups.state.spec.ts b/src/Squidex/app/shared/state/backups.state.spec.ts index 46d1119b1..e5f597be0 100644 --- a/src/Squidex/app/shared/state/backups.state.spec.ts +++ b/src/Squidex/app/shared/state/backups.state.spec.ts @@ -38,70 +38,88 @@ describe('BackupsState', () => { dialogs = Mock.ofType(); backupsService = Mock.ofType(); - - backupsService.setup(x => x.getBackups(app)) - .returns(() => of(oldBackups)); - backupsState = new BackupsState(appsState.object, backupsService.object, dialogs.object); - backupsState.load().subscribe(); }); - it('should load backups', () => { - expect(backupsState.snapshot.backups.values).toEqual(oldBackups); - expect(backupsState.snapshot.isLoaded).toBeTruthy(); - - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + afterEach(() => { + backupsService.verifyAll(); }); - it('should show notification on load when reload is true', () => { - backupsState.load(true, false).subscribe(); + describe('Loading', () => { + it('should load backups', () => { + backupsService.setup(x => x.getBackups(app)) + .returns(() => of(oldBackups)).verifiable(); - expect().nothing(); + backupsState.load().subscribe(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); - }); + expect(backupsState.snapshot.backups.values).toEqual(oldBackups); + expect(backupsState.snapshot.isLoaded).toBeTruthy(); - it('should show notification on load error when silent is false', () => { - backupsService.setup(x => x.getBackups(app)) - .returns(() => throwError({})); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - backupsState.load(true, false).pipe(onErrorResumeNext()).subscribe(); + it('should show notification on load when reload is true', () => { + backupsService.setup(x => x.getBackups(app)) + .returns(() => of(oldBackups)).verifiable(); - expect().nothing(); + backupsState.load(true, false).subscribe(); - dialogs.verify(x => x.notifyError(It.isAny()), Times.once()); - }); + expect().nothing(); - it('should not show notification on load error when silent is true', () => { - backupsService.setup(x => x.getBackups(app)) - .returns(() => throwError({})); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); - backupsState.load(true, true).pipe(onErrorResumeNext()).subscribe(); + it('should show notification on load error when silent is false', () => { + backupsService.setup(x => x.getBackups(app)) + .returns(() => throwError({})); - expect().nothing(); + backupsState.load(true, false).pipe(onErrorResumeNext()).subscribe(); - dialogs.verify(x => x.notifyError(It.isAny()), Times.never()); - }); + expect().nothing(); + + dialogs.verify(x => x.notifyError(It.isAny()), Times.once()); + }); - it('should not add backup to snapshot', () => { - backupsService.setup(x => x.postBackup(app)) - .returns(() => of({})); + it('should not show notification on load error when silent is true', () => { + backupsService.setup(x => x.getBackups(app)) + .returns(() => throwError({})); - backupsState.start().subscribe(); + backupsState.load(true, true).pipe(onErrorResumeNext()).subscribe(); - expect(backupsState.snapshot.backups.length).toBe(2); + expect().nothing(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + dialogs.verify(x => x.notifyError(It.isAny()), Times.never()); + }); }); - it('should not remove backup from snapshot', () => { - backupsService.setup(x => x.deleteBackup(app, oldBackups[0].id)) - .returns(() => of({})); + describe('Updates', () => { + beforeEach(() => { + backupsService.setup(x => x.getBackups(app)) + .returns(() => of(oldBackups)).verifiable(); + + backupsState.load().subscribe(); + }); + + it('should not add backup to snapshot', () => { + backupsService.setup(x => x.postBackup(app)) + .returns(() => of({})).verifiable(); + + backupsState.start().subscribe(); + + expect(backupsState.snapshot.backups.length).toBe(2); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); + + it('should not remove backup from snapshot', () => { + backupsService.setup(x => x.deleteBackup(app, oldBackups[0].id)) + .returns(() => of({})).verifiable(); - backupsState.delete(oldBackups[0]).subscribe(); + backupsState.delete(oldBackups[0]).subscribe(); - expect(backupsState.snapshot.backups.length).toBe(2); + expect(backupsState.snapshot.backups.length).toBe(2); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/backups.state.ts b/src/Squidex/app/shared/state/backups.state.ts index 02f885354..dedb7054b 100644 --- a/src/Squidex/app/shared/state/backups.state.ts +++ b/src/Squidex/app/shared/state/backups.state.ts @@ -6,13 +6,12 @@ */ import { Injectable } from '@angular/core'; -import { Observable, throwError } from 'rxjs'; -import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { DialogService, ImmutableArray, - notify, State } from '@app/framework'; @@ -22,12 +21,14 @@ import { BackupDto, BackupsService } from './../services/backups.service'; interface Snapshot { // The current backups. - backups: ImmutableArray; + backups: BackupsList; // Indicates if the backups are loaded. isLoaded?: boolean; } +type BackupsList = ImmutableArray; + @Injectable() export class BackupsState extends State { public backups = @@ -55,41 +56,55 @@ export class BackupsState extends State { this.resetState(); } - return this.backupsService.getBackups(this.appName).pipe( - tap(dtos => { - if (isReload && !silent) { - this.dialogs.notifyInfo('Backups reloaded.'); - } - - this.next(s => { - const backups = ImmutableArray.of(dtos); - - return { ...s, backups, isLoaded: true }; - }); - }), - catchError(error => { - if (!silent) { - this.dialogs.notifyError(error); - } - - return throwError(error); - })); + const http$ = + this.backupsService.getBackups(this.appName).pipe( + share()); + + http$.subscribe(dtos => { + if (isReload && !silent) { + this.dialogs.notifyInfo('Backups reloaded.'); + } + + this.next(s => { + const backups = ImmutableArray.of(dtos); + + return { ...s, backups, isLoaded: true }; + }); + }, error => { + if (!silent) { + this.dialogs.notifyError(error); + } + }); + + return http$; } public start(): Observable { - return this.backupsService.postBackup(this.appsState.appName).pipe( - tap(() => { - this.dialogs.notifyInfo('Backup started, it can take several minutes to complete.'); - }), - notify(this.dialogs)); + const http$ = + this.backupsService.postBackup(this.appsState.appName).pipe( + share()); + + http$.subscribe(() => { + this.dialogs.notifyInfo('Backup started, it can take several minutes to complete.'); + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } public delete(backup: BackupDto): Observable { - return this.backupsService.deleteBackup(this.appsState.appName, backup.id).pipe( - tap(() => { - this.dialogs.notifyInfo('Backup is about to be deleted.'); - }), - notify(this.dialogs)); + const http$ = + this.backupsService.deleteBackup(this.appsState.appName, backup.id).pipe( + share()); + + http$.subscribe(() => { + this.dialogs.notifyInfo('Backup is about to be deleted.'); + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } private get appName() { diff --git a/src/Squidex/app/shared/state/clients.state.spec.ts b/src/Squidex/app/shared/state/clients.state.spec.ts index ad086db0d..b16b78325 100644 --- a/src/Squidex/app/shared/state/clients.state.spec.ts +++ b/src/Squidex/app/shared/state/clients.state.spec.ts @@ -40,70 +40,84 @@ describe('ClientsState', () => { dialogs = Mock.ofType(); clientsService = Mock.ofType(); - - clientsService.setup(x => x.getClients(app)) - .returns(() => of(new ClientsDto(oldClients, version))).verifiable(Times.atLeastOnce()); - clientsState = new ClientsState(clientsService.object, appsState.object, dialogs.object); - clientsState.load().subscribe(); }); afterEach(() => { clientsService.verifyAll(); }); - it('should load clients', () => { - expect(clientsState.snapshot.clients.values).toEqual(oldClients); - expect(clientsState.snapshot.version).toEqual(version); - expect(clientsState.isLoaded).toBeTruthy(); + describe('Loading', () => { + it('should load clients', () => { + clientsService.setup(x => x.getClients(app)) + .returns(() => of(new ClientsDto(oldClients, version))).verifiable(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); - }); + clientsState.load().subscribe(); + + expect(clientsState.snapshot.clients.values).toEqual(oldClients); + expect(clientsState.snapshot.version).toEqual(version); + expect(clientsState.isLoaded).toBeTruthy(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - it('should show notification on load when reload is true', () => { - clientsState.load(true).subscribe(); + it('should show notification on load when reload is true', () => { + clientsService.setup(x => x.getClients(app)) + .returns(() => of(new ClientsDto(oldClients, version))).verifiable(); - expect().nothing(); + clientsState.load(true).subscribe(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + expect().nothing(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); }); - it('should add client to snapshot when created', () => { - const newClient = new ClientDto('id3', 'name3', 'secret3'); + describe('Updates', () => { + beforeEach(() => { + clientsService.setup(x => x.getClients(app)) + .returns(() => of(new ClientsDto(oldClients, version))).verifiable(); - const request = { id: 'id3' }; + clientsState.load().subscribe(); + }); - clientsService.setup(x => x.postClient(app, request, version)) - .returns(() => of(new Versioned(newVersion, newClient))).verifiable(); + it('should add client to snapshot when created', () => { + const newClient = new ClientDto('id3', 'name3', 'secret3'); - clientsState.attach(request).subscribe(); + const request = { id: 'id3' }; - expect(clientsState.snapshot.clients.values).toEqual([...oldClients, newClient]); - expect(clientsState.snapshot.version).toEqual(newVersion); - }); + clientsService.setup(x => x.postClient(app, request, version)) + .returns(() => of(new Versioned(newVersion, newClient))).verifiable(); - it('should update properties when updated', () => { - const request = { name: 'NewName', role: 'NewRole' }; + clientsState.attach(request).subscribe(); - clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version)) - .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + expect(clientsState.snapshot.clients.values).toEqual([...oldClients, newClient]); + expect(clientsState.snapshot.version).toEqual(newVersion); + }); - clientsState.update(oldClients[0], request).subscribe(); + it('should update properties when updated', () => { + const request = { name: 'NewName', role: 'NewRole' }; - const client_1 = clientsState.snapshot.clients.at(0); + clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); - expect(client_1.name).toBe('NewName'); - expect(client_1.role).toBe('NewRole'); - expect(clientsState.snapshot.version).toEqual(newVersion); - }); + clientsState.update(oldClients[0], request).subscribe(); + + const client_1 = clientsState.snapshot.clients.at(0); + + expect(client_1.name).toBe('NewName'); + expect(client_1.role).toBe('NewRole'); + expect(clientsState.snapshot.version).toEqual(newVersion); + }); - it('should remove client from snapshot when revoked', () => { - clientsService.setup(x => x.deleteClient(app, oldClients[0].id, version)) - .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + it('should remove client from snapshot when revoked', () => { + clientsService.setup(x => x.deleteClient(app, oldClients[0].id, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); - clientsState.revoke(oldClients[0]).subscribe(); + clientsState.revoke(oldClients[0]).subscribe(); - expect(clientsState.snapshot.clients.values).toEqual([oldClients[1]]); - expect(clientsState.snapshot.version).toEqual(newVersion); + expect(clientsState.snapshot.clients.values).toEqual([oldClients[1]]); + expect(clientsState.snapshot.version).toEqual(newVersion); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/clients.state.ts b/src/Squidex/app/shared/state/clients.state.ts index 2f43ec210..c8f185f80 100644 --- a/src/Squidex/app/shared/state/clients.state.ts +++ b/src/Squidex/app/shared/state/clients.state.ts @@ -12,7 +12,6 @@ import { Observable } from 'rxjs'; import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { - array, DialogService, ImmutableArray, State, @@ -64,45 +63,47 @@ export class ClientsState extends State { this.resetState(); } - const stream = + const http$ = this.clientsService.getClients(this.appName).pipe( - map(({ version, clients }) => ({ version, clients: array(clients) })), share()); + share()); - stream.subscribe(({ version, clients }) => { + http$.subscribe(response => { if (isReload) { this.dialogs.notifyInfo('Clients reloaded.'); } + const clients = ImmutableArray.of(response.clients); + this.next(s => { - return { ...s, clients, isLoaded: true, version }; + return { ...s, clients, isLoaded: true, version: response.version }; }); }); - return stream; + return http$; } public attach(request: CreateClientDto): Observable { - const stream = + const http$ = this.clientsService.postClient(this.appName, request, this.version).pipe( share()); - stream.subscribe(dto => { + http$.subscribe(({ version, payload }) => { this.next(s => { - const clients = s.clients.push(dto.payload); + const clients = s.clients.push(payload); - return { ...s, clients, version: dto.version }; + return { ...s, clients, version: version }; }); }); - return stream.pipe(map(x => x.payload)); + return http$.pipe(map(x => x.payload)); } public revoke(client: ClientDto): Observable { - const stream = + const http$ = this.clientsService.deleteClient(this.appName, client.id, this.version).pipe( share()); - stream.subscribe(({ version }) => { + http$.subscribe(({ version }) => { this.next(s => { const clients = s.clients.filter(c => c.id !== client.id); @@ -110,15 +111,15 @@ export class ClientsState extends State { }); }); - return stream; + return http$; } public update(client: ClientDto, request: UpdateClientDto): Observable { - const stream = + const http$ = this.clientsService.putClient(this.appName, client.id, request, this.version).pipe( map(({ version }) => ({ version, client: update(client, request) })), share()); - stream.subscribe(({ version, client }) => { + http$.subscribe(({ version, client }) => { this.next(s => { const clients = s.clients.replaceBy('id', client); @@ -126,7 +127,7 @@ export class ClientsState extends State { }); }); - return stream.pipe(map(x => x.client)); + return http$.pipe(map(x => x.client)); } private get appName() { diff --git a/src/Squidex/app/shared/state/comments.state.spec.ts b/src/Squidex/app/shared/state/comments.state.spec.ts index d917b340c..a26d15d56 100644 --- a/src/Squidex/app/shared/state/comments.state.spec.ts +++ b/src/Squidex/app/shared/state/comments.state.spec.ts @@ -6,7 +6,7 @@ */ import { of } from 'rxjs'; -import { IMock, It, Mock, Times } from 'typemoq'; +import { IMock, Mock } from 'typemoq'; import { CommentDto, @@ -43,78 +43,86 @@ describe('CommentsState', () => { dialogs = Mock.ofType(); commentsService = Mock.ofType(); - - commentsService.setup(x => x.getComments(app, commentsId, new Version('-1'))) - .returns(() => of(oldComments)).verifiable(Times.atLeastOnce()); - commentsState = new CommentsState(appsState.object, commentsId, commentsService.object, dialogs.object); - commentsState.load().subscribe(); }); beforeEach(() => { commentsService.verifyAll(); }); - it('should load and merge comments', () => { - const newComments = new CommentsDto([ - new CommentDto('3', now, 'text3', creator) - ], [ - new CommentDto('2', now, 'text2_2', creator) - ], ['1'], new Version('2')); + describe('Loading', () => { + it('should load and merge comments', () => { + const newComments = new CommentsDto([ + new CommentDto('3', now, 'text3', creator) + ], [ + new CommentDto('2', now, 'text2_2', creator) + ], ['1'], new Version('2')); - commentsService.setup(x => x.getComments(app, commentsId, new Version('1'))) - .returns(() => of(newComments)).verifiable(); + commentsService.setup(x => x.getComments(app, commentsId, new Version('-1'))) + .returns(() => of(oldComments)).verifiable(); - commentsState.load().subscribe(); + commentsService.setup(x => x.getComments(app, commentsId, new Version('1'))) + .returns(() => of(newComments)).verifiable(); - expect(commentsState.snapshot.isLoaded).toBeTruthy(); - expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ - new CommentDto('2', now, 'text2_2', creator), - new CommentDto('3', now, 'text3', creator) - ])); + commentsState.load().subscribe(); + commentsState.load().subscribe(); - commentsService.verify(x => x.getComments(app, commentsId, It.isAny()), Times.exactly(2)); + expect(commentsState.snapshot.isLoaded).toBeTruthy(); + expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ + new CommentDto('2', now, 'text2_2', creator), + new CommentDto('3', now, 'text3', creator) + ])); + }); }); - it('should add comment to snapshot when created', () => { - const newComment = new CommentDto('3', now, 'text3', creator); + describe('Updates', () => { + beforeEach(() => { + commentsService.setup(x => x.getComments(app, commentsId, new Version('-1'))) + .returns(() => of(oldComments)).verifiable(); - const request = { text: 'text3' }; + commentsState.load().subscribe(); + }); - commentsService.setup(x => x.postComment(app, commentsId, request)) - .returns(() => of(newComment)).verifiable(); + it('should add comment to snapshot when created', () => { + const newComment = new CommentDto('3', now, 'text3', creator); - commentsState.create('text3').subscribe(); + const request = { text: 'text3' }; - expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ - new CommentDto('1', now, 'text1', creator), - new CommentDto('2', now, 'text2', creator), - new CommentDto('3', now, 'text3', creator) - ])); - }); + commentsService.setup(x => x.postComment(app, commentsId, request)) + .returns(() => of(newComment)).verifiable(); - it('should update properties when updated', () => { - const request = { text: 'text2_2' }; + commentsState.create('text3').subscribe(); - commentsService.setup(x => x.putComment(app, commentsId, '2', request)) - .returns(() => of({})).verifiable(); + expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ + new CommentDto('1', now, 'text1', creator), + new CommentDto('2', now, 'text2', creator), + new CommentDto('3', now, 'text3', creator) + ])); + }); - commentsState.update(oldComments.createdComments[1], 'text2_2', now).subscribe(); + it('should update properties when updated', () => { + const request = { text: 'text2_2' }; - expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ - new CommentDto('1', now, 'text1', creator), - new CommentDto('2', now, 'text2_2', creator) - ])); - }); + commentsService.setup(x => x.putComment(app, commentsId, '2', request)) + .returns(() => of({})).verifiable(); + + commentsState.update(oldComments.createdComments[1], 'text2_2', now).subscribe(); + + expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ + new CommentDto('1', now, 'text1', creator), + new CommentDto('2', now, 'text2_2', creator) + ])); + }); - it('should remove comment from snapshot when deleted', () => { - commentsService.setup(x => x.deleteComment(app, commentsId, '2')) - .returns(() => of({})).verifiable(); + it('should remove comment from snapshot when deleted', () => { + commentsService.setup(x => x.deleteComment(app, commentsId, '2')) + .returns(() => of({})).verifiable(); - commentsState.delete(oldComments.createdComments[1]).subscribe(); + commentsState.delete(oldComments.createdComments[1]).subscribe(); - expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ - new CommentDto('1', now, 'text1', creator) - ])); + expect(commentsState.snapshot.comments).toEqual(ImmutableArray.of([ + new CommentDto('1', now, 'text1', creator) + ])); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/comments.state.ts b/src/Squidex/app/shared/state/comments.state.ts index 6dd85841c..d0701942c 100644 --- a/src/Squidex/app/shared/state/comments.state.ts +++ b/src/Squidex/app/shared/state/comments.state.ts @@ -12,12 +12,11 @@ import { DateTime, DialogService, ImmutableArray, - notify, State, Version } from '@app/framework'; -import { CommentDto, CommentsDto, CommentsService } from './../services/comments.service'; +import { CommentDto, CommentsService } from './../services/comments.service'; import { AppsState } from './apps.state'; interface Snapshot { @@ -51,12 +50,12 @@ export class CommentsState extends State { super({ comments: ImmutableArray.empty(), version: new Version('-1') }); } - public load(): Observable { - const stream = + public load(): Observable { + const http$ = this.commentsService.getComments(this.appName, this.commentsId, this.version).pipe( share()); - stream.subscribe(response => { + http$.subscribe(response => { this.next(s => { let comments = s.comments; @@ -80,15 +79,15 @@ export class CommentsState extends State { this.dialogs.notifyError(error); }); - return stream; + return http$; } public create(text: string): Observable { - const stream = + const http$ = this.commentsService.postComment(this.appName, this.commentsId, { text }).pipe( share()); - stream.subscribe(comment => { + http$.subscribe(comment => { this.next(s => { const comments = s.comments.push(comment); @@ -98,15 +97,15 @@ export class CommentsState extends State { this.dialogs.notifyError(error); }); - return stream; + return http$; } public update(comment: CommentDto, text: string, now?: DateTime): Observable { - const stream = + const http$ = this.commentsService.putComment(this.appName, this.commentsId, comment.id, { text }).pipe( map(() => update(comment, text, now || DateTime.now())), share()); - stream.subscribe(updated => { + http$.subscribe(updated => { this.next(s => { const comments = s.comments.replaceBy('id', updated); @@ -116,15 +115,15 @@ export class CommentsState extends State { this.dialogs.notifyError(error); }); - return stream; + return http$; } public delete(comment: CommentDto): Observable { - const stream = + const http$ = this.commentsService.deleteComment(this.appName, this.commentsId, comment.id).pipe( share()); - stream.subscribe(() => { + http$.subscribe(() => { this.next(s => { const comments = s.comments.removeBy('id', comment); @@ -134,7 +133,7 @@ export class CommentsState extends State { this.dialogs.notifyError(error); }); - return stream; + return http$; } private get version() { diff --git a/src/Squidex/app/shared/state/contributors.state.spec.ts b/src/Squidex/app/shared/state/contributors.state.spec.ts index ab605fc01..0633cbc0c 100644 --- a/src/Squidex/app/shared/state/contributors.state.spec.ts +++ b/src/Squidex/app/shared/state/contributors.state.spec.ts @@ -42,89 +42,103 @@ describe('ContributorsState', () => { dialogs = Mock.ofType(); contributorsService = Mock.ofType(); - - contributorsService.setup(x => x.getContributors(app)) - .returns(() => of(new ContributorsDto(oldContributors, 3, version))).verifiable(Times.atLeastOnce()); - contributorsState = new ContributorsState(contributorsService.object, appsState.object, authService.object, dialogs.object); - contributorsState.load().subscribe(); }); afterEach(() => { contributorsService.verifyAll(); }); - it('should load contributors', () => { - expect(contributorsState.snapshot.contributors.values).toEqual([ - { isCurrentUser: false, contributor: oldContributors[0] }, - { isCurrentUser: true, contributor: oldContributors[1] } - ]); - expect(contributorsState.snapshot.isMaxReached).toBeFalsy(); - expect(contributorsState.snapshot.isLoaded).toBeTruthy(); - expect(contributorsState.snapshot.maxContributors).toBe(3); - expect(contributorsState.snapshot.version).toEqual(version); - - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); - }); + describe('Loading', () => { + it('should load contributors', () => { + contributorsService.setup(x => x.getContributors(app)) + .returns(() => of(new ContributorsDto(oldContributors, 3, version))).verifiable(); + + contributorsState.load().subscribe(); + + expect(contributorsState.snapshot.contributors.values).toEqual([ + { isCurrentUser: false, contributor: oldContributors[0] }, + { isCurrentUser: true, contributor: oldContributors[1] } + ]); + expect(contributorsState.snapshot.isMaxReached).toBeFalsy(); + expect(contributorsState.snapshot.isLoaded).toBeTruthy(); + expect(contributorsState.snapshot.maxContributors).toBe(3); + expect(contributorsState.snapshot.version).toEqual(version); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - it('should show notification on load when reload is true', () => { - contributorsState.load(true).subscribe(); + it('should show notification on load when reload is true', () => { + contributorsService.setup(x => x.getContributors(app)) + .returns(() => of(new ContributorsDto(oldContributors, 3, version))).verifiable(); - expect().nothing(); + contributorsState.load(true).subscribe(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + expect().nothing(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); }); - it('should add contributor to snapshot when assigned', () => { - const newContributor = new ContributorDto('id3', 'Developer'); + describe('Updates', () => { + beforeEach(() => { + contributorsService.setup(x => x.getContributors(app)) + .returns(() => of(new ContributorsDto(oldContributors, 3, version))).verifiable(); - const request = { contributorId: 'mail2stehle@gmail.com', role: newContributor.role }; - const response = { contributorId: newContributor.contributorId, isCreated: true }; + contributorsState.load().subscribe(); + }); - contributorsService.setup(x => x.postContributor(app, request, version)) - .returns(() => of(new Versioned(newVersion, response))).verifiable(); + it('should add contributor to snapshot when assigned', () => { + const newContributor = new ContributorDto('id3', 'Developer'); - contributorsState.assign(request).subscribe(); + const request = { contributorId: 'mail2stehle@gmail.com', role: newContributor.role }; + const response = { contributorId: newContributor.contributorId, isCreated: true }; - expect(contributorsState.snapshot.contributors.values).toEqual([ - { isCurrentUser: false, contributor: oldContributors[0] }, - { isCurrentUser: true, contributor: oldContributors[1] }, - { isCurrentUser: false, contributor: newContributor } - ]); - expect(contributorsState.snapshot.isMaxReached).toBeTruthy(); - expect(contributorsState.snapshot.maxContributors).toBe(3); - expect(contributorsState.snapshot.version).toEqual(newVersion); - }); + contributorsService.setup(x => x.postContributor(app, request, version)) + .returns(() => of(new Versioned(newVersion, response))).verifiable(); - it('should update contributor in snapshot when assigned and already added', () => { - const newContributor = new ContributorDto(userId, 'Owner'); + contributorsState.assign(request).subscribe(); - const request = { ...newContributor }; - const response = { contributorId: newContributor.contributorId, isCreated: true }; + expect(contributorsState.snapshot.contributors.values).toEqual([ + { isCurrentUser: false, contributor: oldContributors[0] }, + { isCurrentUser: true, contributor: oldContributors[1] }, + { isCurrentUser: false, contributor: newContributor } + ]); + expect(contributorsState.snapshot.isMaxReached).toBeTruthy(); + expect(contributorsState.snapshot.maxContributors).toBe(3); + expect(contributorsState.snapshot.version).toEqual(newVersion); + }); - contributorsService.setup(x => x.postContributor(app, request, version)) - .returns(() => of(new Versioned(newVersion, response))).verifiable(); + it('should update contributor in snapshot when assigned and already added', () => { + const newContributor = new ContributorDto(userId, 'Owner'); - contributorsState.assign(request).subscribe(); + const request = { ...newContributor }; + const response = { contributorId: newContributor.contributorId, isCreated: true }; - expect(contributorsState.snapshot.contributors.values).toEqual([ - { isCurrentUser: false, contributor: oldContributors[0] }, - { isCurrentUser: true, contributor: newContributor } - ]); - expect(contributorsState.snapshot.isMaxReached).toBeFalsy(); - expect(contributorsState.snapshot.maxContributors).toBe(3); - expect(contributorsState.snapshot.version).toEqual(newVersion); - }); + contributorsService.setup(x => x.postContributor(app, request, version)) + .returns(() => of(new Versioned(newVersion, response))).verifiable(); + + contributorsState.assign(request).subscribe(); + + expect(contributorsState.snapshot.contributors.values).toEqual([ + { isCurrentUser: false, contributor: oldContributors[0] }, + { isCurrentUser: true, contributor: newContributor } + ]); + expect(contributorsState.snapshot.isMaxReached).toBeFalsy(); + expect(contributorsState.snapshot.maxContributors).toBe(3); + expect(contributorsState.snapshot.version).toEqual(newVersion); + }); - it('should remove contributor from snapshot when revoked', () => { - contributorsService.setup(x => x.deleteContributor(app, oldContributors[0].contributorId, version)) - .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + it('should remove contributor from snapshot when revoked', () => { + contributorsService.setup(x => x.deleteContributor(app, oldContributors[0].contributorId, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); - contributorsState.revoke(oldContributors[0]).subscribe(); + contributorsState.revoke(oldContributors[0]).subscribe(); - expect(contributorsState.snapshot.contributors.values).toEqual([ - { isCurrentUser: true, contributor: oldContributors[1] } - ]); - expect(contributorsState.snapshot.version).toEqual(newVersion); + expect(contributorsState.snapshot.contributors.values).toEqual([ + { isCurrentUser: true, contributor: oldContributors[1] } + ]); + expect(contributorsState.snapshot.version).toEqual(newVersion); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/contributors.state.ts b/src/Squidex/app/shared/state/contributors.state.ts index a8b4a7873..3571bf1ed 100644 --- a/src/Squidex/app/shared/state/contributors.state.ts +++ b/src/Squidex/app/shared/state/contributors.state.ts @@ -10,7 +10,6 @@ import { Observable, throwError } from 'rxjs'; import { catchError, distinctUntilChanged, map, share } from 'rxjs/operators'; import { - array, DialogService, ErrorDto, ImmutableArray, @@ -87,28 +86,31 @@ export class ContributorsState extends State { this.resetState(); } - const stream = + const http$ = this.contributorsService.getContributors(this.appName).pipe( - map(({ contributors, ...other }) => ({ ...other, contributors: array(contributors.map(x => this.createContributor(x))) })), share()); + share()); - stream.subscribe(({ version, contributors, maxContributors }) => { + http$.subscribe(response => { if (isReload) { this.dialogs.notifyInfo('Contributors reloaded.'); } - this.replaceContributors(contributors, version, maxContributors); + const contributors = ImmutableArray.of(response.contributors.map(x => this.createContributor(x))); + + this.replaceContributors(contributors, response.version, response.maxContributors); }, error => { this.dialogs.notifyError(error); }); - return stream; + return http$; } public revoke(contributor: ContributorDto): Observable { - const stream = - this.contributorsService.deleteContributor(this.appName, contributor.contributorId, this.version).pipe(share()); + const http$ = + this.contributorsService.deleteContributor(this.appName, contributor.contributorId, this.version).pipe( + share()); - stream.subscribe(({ version }) => { + http$.subscribe(({ version }) => { const contributors = this.snapshot.contributors.filter(x => x.contributor.contributorId !== contributor.contributorId); this.replaceContributors(contributors, version); @@ -116,11 +118,11 @@ export class ContributorsState extends State { this.dialogs.notifyError(error); }); - return stream; + return http$; } public assign(request: AssignContributorDto): Observable { - const stream = + const http$ = this.contributorsService.postContributor(this.appName, request, this.version).pipe( catchError(error => { if (Types.is(error, ErrorDto) && error.statusCode === 404) { @@ -131,7 +133,7 @@ export class ContributorsState extends State { }), share()); - stream.subscribe(({ payload, version }) => { + http$.subscribe(({ payload, version }) => { const contributors = this.updateContributors(payload.contributorId, request.role); this.replaceContributors(contributors, version); @@ -139,7 +141,7 @@ export class ContributorsState extends State { this.dialogs.notifyError(error); }); - return stream.pipe(map(x => x.payload.isCreated)); + return http$.pipe(map(x => x.payload.isCreated)); } private updateContributors(id: string, role: string) { diff --git a/src/Squidex/app/shared/state/languages.state.spec.ts b/src/Squidex/app/shared/state/languages.state.spec.ts index 74b810c8c..4fbf63d6a 100644 --- a/src/Squidex/app/shared/state/languages.state.spec.ts +++ b/src/Squidex/app/shared/state/languages.state.spec.ts @@ -41,9 +41,9 @@ describe('LanguagesState', () => { ]; let dialogs: IMock; - let allLanguagesService: IMock; let languagesService: IMock; let languagesState: LanguagesState; + let allLanguagesService: IMock; beforeEach(() => { dialogs = Mock.ofType(); @@ -51,111 +51,126 @@ describe('LanguagesState', () => { allLanguagesService = Mock.ofType(); allLanguagesService.setup(x => x.getLanguages()) - .returns(() => of([languageDE, languageEN, languageIT, languageES])); + .returns(() => of([languageDE, languageEN, languageIT, languageES])).verifiable(); languagesService = Mock.ofType(); languagesService.setup(x => x.getLanguages(app)) - .returns(() => of(new AppLanguagesDto(oldLanguages, version))); + .returns(() => of(new AppLanguagesDto(oldLanguages, version))).verifiable(); languagesState = new LanguagesState(languagesService.object, appsState.object, dialogs.object, allLanguagesService.object); - languagesState.load().subscribe(); - }); - - it('should load languages', () => { - expect(languagesState.snapshot.languages.values).toEqual([ - { - language: oldLanguages[0], - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1]]) - }, { - language: oldLanguages[1], - fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), - fallbackLanguagesNew: ImmutableArray.empty() - } - ]); - expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageIT, languageES]); - expect(languagesState.snapshot.isLoaded).toBeTruthy(); - expect(languagesState.snapshot.version).toEqual(version); - - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); }); - it('should show notification on load when reload is true', () => { - languagesState.load(true).subscribe(); - - expect().nothing(); - - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); - }); + afterEach(() => { + languagesService.verifyAll(); - it('should add language to snapshot when assigned', () => { - const newLanguage = new AppLanguageDto(languageIT.iso2Code, languageIT.englishName, false, false, []); - - languagesService.setup(x => x.postLanguage(app, It.isAny(), version)) - .returns(() => of(new Versioned(newVersion, newLanguage))); - - languagesState.add(languageIT).subscribe(); - - expect(languagesState.snapshot.languages.values).toEqual([ - { - language: oldLanguages[0], - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1], newLanguage]) - }, { - language: oldLanguages[1], - fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), - fallbackLanguagesNew: ImmutableArray.of([newLanguage]) - }, { - language: newLanguage, - fallbackLanguages: ImmutableArray.of(), - fallbackLanguagesNew: ImmutableArray.of([oldLanguages[0], oldLanguages[1]]) - } - ]); - expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageES]); - expect(languagesState.snapshot.version).toEqual(newVersion); + allLanguagesService.verifyAll(); }); - it('should update language in snapshot when updated', () => { - const request = { isMaster: true, isOptional: false, fallback: [] }; - - languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version)) - .returns(() => of(new Versioned(newVersion, {}))); - - languagesState.update(oldLanguages[1], request).subscribe(); - - const newLanguage1 = AppLanguageDto.fromLanguage(languageDE, true); - const newLanguage2 = AppLanguageDto.fromLanguage(languageEN); - - expect(languagesState.snapshot.languages.values).toEqual([ - { - language: newLanguage1, - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.of([newLanguage2]) - }, { - language: newLanguage2, - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.of([newLanguage1]) - } - ]); - expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageIT, languageES]); - expect(languagesState.snapshot.version).toEqual(newVersion); + describe('Loading', () => { + it('should load languages', () => { + languagesState.load().subscribe(); + + expect(languagesState.snapshot.languages.values).toEqual([ + { + language: oldLanguages[0], + fallbackLanguages: ImmutableArray.empty(), + fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1]]) + }, { + language: oldLanguages[1], + fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), + fallbackLanguagesNew: ImmutableArray.empty() + } + ]); + expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageIT, languageES]); + expect(languagesState.snapshot.isLoaded).toBeTruthy(); + expect(languagesState.snapshot.version).toEqual(version); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); + + it('should show notification on load when reload is true', () => { + languagesState.load(true).subscribe(); + + expect().nothing(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); }); - it('should remove language from snapshot when deleted', () => { - languagesService.setup(x => x.deleteLanguage(app, oldLanguages[1].iso2Code, version)) - .returns(() => of(new Versioned(newVersion, {}))); - - languagesState.remove(oldLanguages[1]).subscribe(); - - expect(languagesState.snapshot.languages.values).toEqual([ - { - language: oldLanguages[0], - fallbackLanguages: ImmutableArray.empty(), - fallbackLanguagesNew: ImmutableArray.empty() - } - ]); - expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]); - expect(languagesState.snapshot.version).toEqual(newVersion); + describe('Updates', () => { + beforeEach(() => { + languagesState.load().subscribe(); + }); + + it('should add language to snapshot when assigned', () => { + const newLanguage = new AppLanguageDto(languageIT.iso2Code, languageIT.englishName, false, false, []); + + languagesService.setup(x => x.postLanguage(app, It.isAny(), version)) + .returns(() => of(new Versioned(newVersion, newLanguage))).verifiable(); + + languagesState.add(languageIT).subscribe(); + + expect(languagesState.snapshot.languages.values).toEqual([ + { + language: oldLanguages[0], + fallbackLanguages: ImmutableArray.empty(), + fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1], newLanguage]) + }, { + language: oldLanguages[1], + fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), + fallbackLanguagesNew: ImmutableArray.of([newLanguage]) + }, { + language: newLanguage, + fallbackLanguages: ImmutableArray.of(), + fallbackLanguagesNew: ImmutableArray.of([oldLanguages[0], oldLanguages[1]]) + } + ]); + expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageES]); + expect(languagesState.snapshot.version).toEqual(newVersion); + }); + + it('should update language in snapshot when updated', () => { + const request = { isMaster: true, isOptional: false, fallback: [] }; + + languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + + languagesState.update(oldLanguages[1], request).subscribe(); + + const newLanguage1 = AppLanguageDto.fromLanguage(languageDE, true); + const newLanguage2 = AppLanguageDto.fromLanguage(languageEN); + + expect(languagesState.snapshot.languages.values).toEqual([ + { + language: newLanguage1, + fallbackLanguages: ImmutableArray.empty(), + fallbackLanguagesNew: ImmutableArray.of([newLanguage2]) + }, { + language: newLanguage2, + fallbackLanguages: ImmutableArray.empty(), + fallbackLanguagesNew: ImmutableArray.of([newLanguage1]) + } + ]); + expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageIT, languageES]); + expect(languagesState.snapshot.version).toEqual(newVersion); + }); + + it('should remove language from snapshot when deleted', () => { + languagesService.setup(x => x.deleteLanguage(app, oldLanguages[1].iso2Code, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + + languagesState.remove(oldLanguages[1]).subscribe(); + + expect(languagesState.snapshot.languages.values).toEqual([ + { + language: oldLanguages[0], + fallbackLanguages: ImmutableArray.empty(), + fallbackLanguagesNew: ImmutableArray.empty() + } + ]); + expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]); + expect(languagesState.snapshot.version).toEqual(newVersion); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/languages.state.ts b/src/Squidex/app/shared/state/languages.state.ts index 74210b669..e414c5e0c 100644 --- a/src/Squidex/app/shared/state/languages.state.ts +++ b/src/Squidex/app/shared/state/languages.state.ts @@ -7,7 +7,7 @@ import { Injectable } from '@angular/core'; import { forkJoin, Observable } from 'rxjs'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, share, tap } from 'rxjs/operators'; import { DialogService, @@ -31,24 +31,24 @@ interface SnapshotLanguage { language: AppLanguageDto; // All configured fallback languages. - fallbackLanguages: ImmutableArray; + fallbackLanguages: LanguageList; // The fallback languages that have not been added yet. - fallbackLanguagesNew: ImmutableArray; + fallbackLanguagesNew: LanguageList; } interface Snapshot { // the configured languages as plan format. - plainLanguages: ImmutableArray; + plainLanguages: AppLanguagesList; // All supported languages. - allLanguages: ImmutableArray; + allLanguages: LanguageList; // The languages that have not been added yet. - allLanguagesNew: ImmutableArray; + allLanguagesNew: LanguageList; // The configured languages with extra information. - languages: ImmutableArray; + languages: LanguageResultList; // The app version. version: Version; @@ -57,6 +57,10 @@ interface Snapshot { isLoaded?: boolean; } +type AppLanguagesList = ImmutableArray; +type LanguageList = ImmutableArray; +type LanguageResultList = ImmutableArray; + @Injectable() export class LanguagesState extends State { public languages = @@ -91,22 +95,29 @@ export class LanguagesState extends State { this.resetState(); } - return forkJoin( + const http$ = + forkJoin( this.languagesService.getLanguages(), this.appLanguagesService.getLanguages(this.appName)).pipe( - map(args => { - return { allLanguages: args[0], languages: args[1] }; - }), - tap(dtos => { - if (isReload) { - this.dialogs.notifyInfo('Languages reloaded.'); - } + map(args => { + return { allLanguages: args[0], languages: args[1] }; + }), + share()); - const sorted = ImmutableArray.of(dtos.allLanguages).sortByStringAsc(x => x.englishName); + http$.subscribe(response => { + if (isReload) { + this.dialogs.notifyInfo('Languages reloaded.'); + } - this.replaceLanguages(ImmutableArray.of(dtos.languages.languages), dtos.languages.version, sorted); - }), - notify(this.dialogs)); + const sorted = ImmutableArray.of(response.allLanguages).sortByStringAsc(x => x.englishName); + + this.replaceLanguages(ImmutableArray.of(response.languages.languages), response.languages.version, sorted); + + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } public add(language: LanguageDto): Observable { @@ -147,7 +158,7 @@ export class LanguagesState extends State { notify(this.dialogs)); } - private replaceLanguages(languages: ImmutableArray, version: Version, allLanguages?: ImmutableArray) { + private replaceLanguages(languages: AppLanguagesList, version: Version, allLanguages?: LanguageList) { this.next(s => { allLanguages = allLanguages || s.allLanguages; @@ -177,7 +188,7 @@ export class LanguagesState extends State { return this.snapshot.version; } - private createLanguage(language: AppLanguageDto, languages: ImmutableArray): SnapshotLanguage { + private createLanguage(language: AppLanguageDto, languages: AppLanguagesList): SnapshotLanguage { return { language, fallbackLanguages: diff --git a/src/Squidex/app/shared/state/patterns.state.spec.ts b/src/Squidex/app/shared/state/patterns.state.spec.ts index 3a1a3c56b..d92a2db72 100644 --- a/src/Squidex/app/shared/state/patterns.state.spec.ts +++ b/src/Squidex/app/shared/state/patterns.state.spec.ts @@ -40,66 +40,84 @@ describe('PatternsState', () => { dialogs = Mock.ofType(); patternsService = Mock.ofType(); - - patternsService.setup(x => x.getPatterns(app)) - .returns(() => of(new PatternsDto(oldPatterns, version))); - patternsState = new PatternsState(patternsService.object, appsState.object, dialogs.object); - patternsState.load().subscribe(); }); - it('should load patterns', () => { - expect(patternsState.snapshot.patterns.values).toEqual(oldPatterns); - expect(patternsState.snapshot.version).toEqual(version); - - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + afterEach(() => { + patternsService.verifyAll(); }); - it('should show notification on load when reload is true', () => { - patternsState.load(true).subscribe(); + describe('Loading', () => { + it('should load patterns', () => { + patternsService.setup(x => x.getPatterns(app)) + .returns(() => of(new PatternsDto(oldPatterns, version))).verifiable(); - expect().nothing(); + patternsState.load().subscribe(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); - }); + expect(patternsState.snapshot.patterns.values).toEqual(oldPatterns); + expect(patternsState.snapshot.version).toEqual(version); - it('should add pattern to snapshot when created', () => { - const newPattern = new PatternDto('id3', 'name3', 'pattern3', ''); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - const request = { ...newPattern }; + it('should show notification on load when reload is true', () => { + patternsService.setup(x => x.getPatterns(app)) + .returns(() => of(new PatternsDto(oldPatterns, version))).verifiable(); - patternsService.setup(x => x.postPattern(app, request, version)) - .returns(() => of(new Versioned(newVersion, newPattern))); + patternsState.load(true).subscribe(); - patternsState.create(request).subscribe(); + expect().nothing(); - expect(patternsState.snapshot.patterns.values).toEqual([...oldPatterns, newPattern]); - expect(patternsState.snapshot.version).toEqual(newVersion); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); }); - it('should update properties when updated', () => { - const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' }; + describe('Updates', () => { + beforeEach(() => { + patternsService.setup(x => x.getPatterns(app)) + .returns(() => of(new PatternsDto(oldPatterns, version))).verifiable(); - patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version)) - .returns(() => of(new Versioned(newVersion, {}))); + patternsState.load().subscribe(); + }); - patternsState.update(oldPatterns[1], request).subscribe(); + it('should add pattern to snapshot when created', () => { + const newPattern = new PatternDto('id3', 'name3', 'pattern3', ''); - const pattern_1 = patternsState.snapshot.patterns.at(1); + const request = { ...newPattern }; - expect(pattern_1.name).toBe(request.name); - expect(pattern_1.pattern).toBe(request.pattern); - expect(pattern_1.message).toBe(request.message); - expect(patternsState.snapshot.version).toEqual(newVersion); - }); + patternsService.setup(x => x.postPattern(app, request, version)) + .returns(() => of(new Versioned(newVersion, newPattern))).verifiable(); + + patternsState.create(request).subscribe(); + + expect(patternsState.snapshot.patterns.values).toEqual([...oldPatterns, newPattern]); + expect(patternsState.snapshot.version).toEqual(newVersion); + }); + + it('should update properties when updated', () => { + const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' }; + + patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + + patternsState.update(oldPatterns[1], request).subscribe(); + + const pattern_1 = patternsState.snapshot.patterns.at(1); + + expect(pattern_1.name).toBe(request.name); + expect(pattern_1.pattern).toBe(request.pattern); + expect(pattern_1.message).toBe(request.message); + expect(patternsState.snapshot.version).toEqual(newVersion); + }); - it('should remove pattern from snapshot when deleted', () => { - patternsService.setup(x => x.deletePattern(app, oldPatterns[0].id, version)) - .returns(() => of(new Versioned(newVersion, {}))); + it('should remove pattern from snapshot when deleted', () => { + patternsService.setup(x => x.deletePattern(app, oldPatterns[0].id, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); - patternsState.delete(oldPatterns[0]).subscribe(); + patternsState.delete(oldPatterns[0]).subscribe(); - expect(patternsState.snapshot.patterns.values).toEqual([oldPatterns[1]]); - expect(patternsState.snapshot.version).toEqual(newVersion); + expect(patternsState.snapshot.patterns.values).toEqual([oldPatterns[1]]); + expect(patternsState.snapshot.version).toEqual(newVersion); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/patterns.state.ts b/src/Squidex/app/shared/state/patterns.state.ts index 4ae94204a..7206ec626 100644 --- a/src/Squidex/app/shared/state/patterns.state.ts +++ b/src/Squidex/app/shared/state/patterns.state.ts @@ -7,12 +7,11 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { DialogService, ImmutableArray, - notify, State, Version } from '@app/framework'; @@ -27,7 +26,7 @@ import { interface Snapshot { // The current patterns. - patterns: ImmutableArray; + patterns: PatternsList; // The app version. version: Version; @@ -36,6 +35,8 @@ interface Snapshot { isLoaded?: boolean; } +type PatternsList = ImmutableArray; + @Injectable() export class PatternsState extends State { public patterns = @@ -59,55 +60,79 @@ export class PatternsState extends State { this.resetState(); } - return this.patternsService.getPatterns(this.appName).pipe( - tap(dtos => { - if (isReload) { - this.dialogs.notifyInfo('Patterns reloaded.'); - } + const update$ = + this.patternsService.getPatterns(this.appName).pipe( + share()); + + update$.subscribe(({ version, patterns: items }) => { + if (isReload) { + this.dialogs.notifyInfo('Patterns reloaded.'); + } + + this.next(s => { + const patterns = ImmutableArray.of(items).sortByStringAsc(x => x.name); - this.next(s => { - const patterns = ImmutableArray.of(dtos.patterns).sortByStringAsc(x => x.name); + return { ...s, patterns, isLoaded: true, version: version }; + }); + }, error => { + this.dialogs.notifyError(error); + }); - return { ...s, patterns, isLoaded: true, version: dtos.version }; - }); - }), - notify(this.dialogs)); + return update$; } - public create(request: EditPatternDto): Observable { - return this.patternsService.postPattern(this.appName, request, this.version).pipe( - tap(dto => { - this.next(s => { - const patterns = s.patterns.push(dto.payload).sortByStringAsc(x => x.name); + public create(request: EditPatternDto): Observable { + const update$ = + this.patternsService.postPattern(this.appName, request, this.version).pipe( + share()); + + update$.subscribe(({ version, payload: pattern }) => { + this.next(s => { + const patterns = s.patterns.push(pattern).sortByStringAsc(x => x.name); + + return { ...s, patterns, version: version }; + }); + }, error => { + this.dialogs.notifyError(error); + }); - return { ...s, patterns, version: dto.version }; - }); - }), - notify(this.dialogs)); + return update$.pipe(map(x => x.payload)); } - public update(pattern: PatternDto, request: EditPatternDto): Observable { - return this.patternsService.putPattern(this.appName, pattern.id, request, this.version).pipe( - tap(dto => { - this.next(s => { - const patterns = s.patterns.replaceBy('id', update(pattern, request)).sortByStringAsc(x => x.name); + public update(pattern: PatternDto, request: EditPatternDto): Observable { + const update$ = + this.patternsService.putPattern(this.appName, pattern.id, request, this.version).pipe( + map(({ version }) => ({ version, payload: update(pattern, request) })), share()); - return { ...s, patterns, version: dto.version }; - }); - }), - notify(this.dialogs)); + update$.subscribe(({ version, payload }) => { + this.next(s => { + const patterns = s.patterns.replaceBy('id', payload).sortByStringAsc(x => x.name); + + return { ...s, patterns, version: version }; + }); + }, error => { + this.dialogs.notifyError(error); + }); + + return update$.pipe(map(x => x.payload)); } public delete(pattern: PatternDto): Observable { - return this.patternsService.deletePattern(this.appName, pattern.id, this.version).pipe( - tap(dto => { - this.next(s => { - const patterns = s.patterns.filter(c => c.id !== pattern.id); - - return { ...s, patterns, version: dto.version }; - }); - }), - notify(this.dialogs)); + const update$ = + this.patternsService.deletePattern(this.appName, pattern.id, this.version).pipe( + share()); + + update$.subscribe(({ version }) => { + this.next(s => { + const patterns = s.patterns.filter(c => c.id !== pattern.id); + + return { ...s, patterns, version: version }; + }); + }, error => { + this.dialogs.notifyError(error); + }); + + return update$; } private get appName() { diff --git a/src/Squidex/app/shared/state/roles.state.spec.ts b/src/Squidex/app/shared/state/roles.state.spec.ts index 297eedb5e..f1715c2b7 100644 --- a/src/Squidex/app/shared/state/roles.state.spec.ts +++ b/src/Squidex/app/shared/state/roles.state.spec.ts @@ -40,65 +40,79 @@ describe('RolesState', () => { dialogs = Mock.ofType(); rolesService = Mock.ofType(); - - rolesService.setup(x => x.getRoles(app)) - .returns(() => of(new RolesDto(oldRoles, version))); - rolesState = new RolesState(rolesService.object, appsState.object, dialogs.object); - rolesState.load().subscribe(); }); - it('should load roles', () => { - expect(rolesState.snapshot.roles.values).toEqual(oldRoles); - expect(rolesState.snapshot.isLoaded).toBeTruthy(); - expect(rolesState.snapshot.version).toEqual(version); + describe('Loading', () => { + it('should load roles', () => { + rolesService.setup(x => x.getRoles(app)) + .returns(() => of(new RolesDto(oldRoles, version))).verifiable(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); - }); + rolesState.load().subscribe(); + + expect(rolesState.snapshot.roles.values).toEqual(oldRoles); + expect(rolesState.snapshot.isLoaded).toBeTruthy(); + expect(rolesState.snapshot.version).toEqual(version); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - it('should show notification on load when reload is true', () => { - rolesState.load(true).subscribe(); + it('should show notification on load when reload is true', () => { + rolesService.setup(x => x.getRoles(app)) + .returns(() => of(new RolesDto(oldRoles, version))).verifiable(); - expect().nothing(); + rolesState.load(true).subscribe(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + expect().nothing(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); }); - it('should add role to snapshot when added', () => { - const newRole = new RoleDto('Role3', 0, 0, ['P3']); + describe('Updates', () => { + beforeEach(() => { + rolesService.setup(x => x.getRoles(app)) + .returns(() => of(new RolesDto(oldRoles, version))); - const request = { name: newRole.name }; + rolesState.load().subscribe(); + }); - rolesService.setup(x => x.postRole(app, request, version)) - .returns(() => of(new Versioned(newVersion, newRole))); + it('should add role to snapshot when added', () => { + const newRole = new RoleDto('Role3', 0, 0, ['P3']); - rolesState.add(request).subscribe(); + const request = { name: newRole.name }; - expect(rolesState.snapshot.roles.values).toEqual([oldRoles[0], oldRoles[1], newRole]); - expect(rolesState.snapshot.version).toEqual(newVersion); - }); + rolesService.setup(x => x.postRole(app, request, version)) + .returns(() => of(new Versioned(newVersion, newRole))); - it('should update permissions when updated', () => { - const request = { permissions: ['P4', 'P5'] }; + rolesState.add(request).subscribe(); - rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version)) - .returns(() => of(new Versioned(newVersion, {}))); + expect(rolesState.snapshot.roles.values).toEqual([oldRoles[0], oldRoles[1], newRole]); + expect(rolesState.snapshot.version).toEqual(newVersion); + }); - rolesState.update(oldRoles[1], request).subscribe(); + it('should update permissions when updated', () => { + const request = { permissions: ['P4', 'P5'] }; - const role_1 = rolesState.snapshot.roles.at(1); + rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version)) + .returns(() => of(new Versioned(newVersion, {}))); - expect(role_1.permissions).toEqual(request.permissions); - expect(rolesState.snapshot.version).toEqual(newVersion); - }); + rolesState.update(oldRoles[1], request).subscribe(); + + const role_1 = rolesState.snapshot.roles.at(1); + + expect(role_1.permissions).toEqual(request.permissions); + expect(rolesState.snapshot.version).toEqual(newVersion); + }); - it('should remove role from snapshot when deleted', () => { - rolesService.setup(x => x.deleteRole(app, oldRoles[0].name, version)) - .returns(() => of(new Versioned(newVersion, {}))); + it('should remove role from snapshot when deleted', () => { + rolesService.setup(x => x.deleteRole(app, oldRoles[0].name, version)) + .returns(() => of(new Versioned(newVersion, {}))); - rolesState.delete(oldRoles[0]).subscribe(); + rolesState.delete(oldRoles[0]).subscribe(); - expect(rolesState.snapshot.roles.values).toEqual([oldRoles[1]]); - expect(rolesState.snapshot.version).toEqual(newVersion); + expect(rolesState.snapshot.roles.values).toEqual([oldRoles[1]]); + expect(rolesState.snapshot.version).toEqual(newVersion); + }); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/roles.state.ts b/src/Squidex/app/shared/state/roles.state.ts index 276a5b505..cd3f36e19 100644 --- a/src/Squidex/app/shared/state/roles.state.ts +++ b/src/Squidex/app/shared/state/roles.state.ts @@ -28,7 +28,7 @@ import { interface Snapshot { // The current roles. - roles: ImmutableArray; + roles: RolesList; // The app version. version: Version; @@ -37,6 +37,8 @@ interface Snapshot { isLoaded?: boolean; } +type RolesList = ImmutableArray; + @Injectable() export class RolesState extends State { public roles = @@ -91,7 +93,7 @@ export class RolesState extends State { return this.rolesService.deleteRole(this.appName, role.name, this.version).pipe( tap(dto => { this.next(s => { - const roles = s.roles.filter(c => c.name !== role.name); + const roles = s.roles.removeBy('name', role); return { ...s, roles, version: dto.version }; }); diff --git a/src/Squidex/app/shared/state/rules.state.spec.ts b/src/Squidex/app/shared/state/rules.state.spec.ts index 6292a1610..025e5c4c1 100644 --- a/src/Squidex/app/shared/state/rules.state.spec.ts +++ b/src/Squidex/app/shared/state/rules.state.spec.ts @@ -14,7 +14,6 @@ import { DialogService, RuleDto, RulesService, - UpdateRuleDto, Versioned } from './../'; @@ -46,106 +45,125 @@ describe('RulesState', () => { dialogs = Mock.ofType(); rulesService = Mock.ofType(); - - rulesService.setup(x => x.getRules(app)) - .returns(() => of(oldRules)); - rulesState = new RulesState(appsState.object, authService.object, dialogs.object, rulesService.object); - rulesState.load().subscribe(); }); - it('should load rules', () => { - expect(rulesState.snapshot.rules.values).toEqual(oldRules); - expect(rulesState.snapshot.isLoaded).toBeTruthy(); - - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + afterEach(() => { + rulesService.verifyAll(); }); - it('should show notification on load when reload is true', () => { - rulesState.load(true).subscribe(); + describe('Loading', () => { + it('should load rules', () => { + rulesService.setup(x => x.getRules(app)) + .returns(() => of(oldRules)).verifiable(); - expect().nothing(); + rulesState.load().subscribe(); - dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); - }); + expect(rulesState.snapshot.rules.values).toEqual(oldRules); + expect(rulesState.snapshot.isLoaded).toBeTruthy(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); + }); - it('should add rule to snapshot when created', () => { - const newRule = new RuleDto('id3', creator, creator, creation, creation, version, false, {}, 'trigger3', {}, 'action3'); + it('should show notification on load when reload is true', () => { + rulesService.setup(x => x.getRules(app)) + .returns(() => of(oldRules)).verifiable(); - const request = { action: {}, trigger: {} }; + rulesState.load(true).subscribe(); - rulesService.setup(x => x.postRule(app, request, modifier, creation)) - .returns(() => of(newRule)); + expect().nothing(); - rulesState.create(request, creation).subscribe(); + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); - expect(rulesState.snapshot.rules.values).toEqual([...oldRules, newRule]); }); - it('should update action and update and user info when updated action', () => { - const newAction = {}; + describe('Updates', () => { + beforeEach(() => { + rulesService.setup(x => x.getRules(app)) + .returns(() => of(oldRules)).verifiable(); - rulesService.setup(x => x.putRule(app, oldRules[0].id, It.is(() => true), version)) - .returns(() => of(new Versioned(newVersion, {}))); + rulesState.load().subscribe(); + }); - rulesState.updateAction(oldRules[0], newAction, modified).subscribe(); + it('should add rule to snapshot when created', () => { + const newRule = new RuleDto('id3', creator, creator, creation, creation, version, false, {}, 'trigger3', {}, 'action3'); - const rule_1 = rulesState.snapshot.rules.at(0); + const request = { action: {}, trigger: {} }; - expect(rule_1.action).toBe(newAction); - expectToBeModified(rule_1); - }); + rulesService.setup(x => x.postRule(app, request, modifier, creation)) + .returns(() => of(newRule)); - it('should update trigger and update and user info when updated trigger', () => { - const newTrigger = {}; + rulesState.create(request, creation).subscribe(); - rulesService.setup(x => x.putRule(app, oldRules[0].id, It.is(() => true), version)) - .returns(() => of(new Versioned(newVersion, {}))); + expect(rulesState.snapshot.rules.values).toEqual([...oldRules, newRule]); + }); - rulesState.updateTrigger(oldRules[0], newTrigger, modified).subscribe(); + it('should update action and update and user info when updated action', () => { + const newAction = {}; - const rule_1 = rulesState.snapshot.rules.at(0); + rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); - expect(rule_1.trigger).toBe(newTrigger); - expectToBeModified(rule_1); - }); + rulesState.updateAction(oldRules[0], newAction, modified).subscribe(); - it('should mark as enabled and update and user info when enabled', () => { - rulesService.setup(x => x.enableRule(app, oldRules[0].id, version)) - .returns(() => of(new Versioned(newVersion, {}))); + const rule_1 = rulesState.snapshot.rules.at(0); - rulesState.enable(oldRules[0], modified).subscribe(); + expect(rule_1.action).toBe(newAction); + expectToBeModified(rule_1); + }); - const rule_1 = rulesState.snapshot.rules.at(0); + it('should update trigger and update and user info when updated trigger', () => { + const newTrigger = {}; - expect(rule_1.isEnabled).toBeTruthy(); - expectToBeModified(rule_1); - }); + rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); - it('should mark as disabled and update and user info when disabled', () => { - rulesService.setup(x => x.disableRule(app, oldRules[1].id, version)) - .returns(() => of(new Versioned(newVersion, {}))); + rulesState.updateTrigger(oldRules[0], newTrigger, modified).subscribe(); - rulesState.disable(oldRules[1], modified).subscribe(); + const rule_1 = rulesState.snapshot.rules.at(0); - const rule_1 = rulesState.snapshot.rules.at(1); + expect(rule_1.trigger).toBe(newTrigger); + expectToBeModified(rule_1); + }); - expect(rule_1.isEnabled).toBeFalsy(); - expectToBeModified(rule_1); - }); + it('should mark as enabled and update and user info when enabled', () => { + rulesService.setup(x => x.enableRule(app, oldRules[0].id, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); - it('should remove rule from snapshot when deleted', () => { - rulesService.setup(x => x.deleteRule(app, oldRules[0].id, version)) - .returns(() => of(new Versioned(newVersion, {}))); + rulesState.enable(oldRules[0], modified).subscribe(); - rulesState.delete(oldRules[0]).subscribe(); + const rule_1 = rulesState.snapshot.rules.at(0); - expect(rulesState.snapshot.rules.values).toEqual([oldRules[1]]); - }); + expect(rule_1.isEnabled).toBeTruthy(); + expectToBeModified(rule_1); + }); + + it('should mark as disabled and update and user info when disabled', () => { + rulesService.setup(x => x.disableRule(app, oldRules[1].id, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + + rulesState.disable(oldRules[1], modified).subscribe(); + + const rule_1 = rulesState.snapshot.rules.at(1); + + expect(rule_1.isEnabled).toBeFalsy(); + expectToBeModified(rule_1); + }); - function expectToBeModified(rule_1: RuleDto) { - expect(rule_1.lastModified).toEqual(modified); - expect(rule_1.lastModifiedBy).toEqual(modifier); - expect(rule_1.version).toEqual(newVersion); - } + it('should remove rule from snapshot when deleted', () => { + rulesService.setup(x => x.deleteRule(app, oldRules[0].id, version)) + .returns(() => of(new Versioned(newVersion, {}))).verifiable(); + + rulesState.delete(oldRules[0]).subscribe(); + + expect(rulesState.snapshot.rules.values).toEqual([oldRules[1]]); + }); + + function expectToBeModified(rule_1: RuleDto) { + expect(rule_1.lastModified).toEqual(modified); + expect(rule_1.lastModifiedBy).toEqual(modifier); + expect(rule_1.version).toEqual(newVersion); + } + }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/rules.state.ts b/src/Squidex/app/shared/state/rules.state.ts index 6cc19956a..5288f3c44 100644 --- a/src/Squidex/app/shared/state/rules.state.ts +++ b/src/Squidex/app/shared/state/rules.state.ts @@ -7,13 +7,12 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { DateTime, DialogService, ImmutableArray, - notify, State, Version } from '@app/framework'; @@ -29,12 +28,14 @@ import { interface Snapshot { // The current rules. - rules: ImmutableArray; + rules: RulesList; // Indicates if the rules are loaded. isLoaded?: boolean; } +type RulesList = ImmutableArray; + @Injectable() export class RulesState extends State { public rules = @@ -59,82 +60,112 @@ export class RulesState extends State { this.resetState(); } - return this.rulesService.getRules(this.appName).pipe( - tap(dtos => { - if (isReload) { - this.dialogs.notifyInfo('Rules reloaded.'); - } + const http$ = + this.rulesService.getRules(this.appName).pipe( + share()); + + http$.subscribe(response => { + if (isReload) { + this.dialogs.notifyInfo('Rules reloaded.'); + } - this.next(s => { - const rules = ImmutableArray.of(dtos); + this.next(s => { + const rules = ImmutableArray.of(response); - return { ...s, rules, isLoaded: true }; - }); - }), - notify(this.dialogs)); + return { ...s, rules, isLoaded: true }; + }); + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } - public create(request: CreateRuleDto, now?: DateTime): Observable { - return this.rulesService.postRule(this.appName, request, this.user, now || DateTime.now()).pipe( - tap(dto => { - this.next(s => { - const rules = s.rules.push(dto); + public create(request: CreateRuleDto, now?: DateTime): Observable { + const http$ = + this.rulesService.postRule(this.appName, request, this.user, now || DateTime.now()).pipe( + share()); + + http$.subscribe(rule => { + this.next(s => { + const rules = s.rules.push(rule); - return { ...s, rules }; - }); - }), - notify(this.dialogs)); + return { ...s, rules }; + }); + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } public delete(rule: RuleDto): Observable { - return this.rulesService.deleteRule(this.appName, rule.id, rule.version).pipe( - tap(() => { - this.next(s => { - const rules = s.rules.removeAll(x => x.id === rule.id); - - return { ...s, rules }; - }); - }), - notify(this.dialogs)); + const http$ = + this.rulesService.deleteRule(this.appName, rule.id, rule.version).pipe( + share()); + + http$.subscribe(() => { + this.next(s => { + const rules = s.rules.removeAll(x => x.id === rule.id); + + return { ...s, rules }; + }); + }, error => { + this.dialogs.notifyError(error); + }); + + return http$; } public updateAction(rule: RuleDto, action: any, now?: DateTime): Observable { - return this.rulesService.putRule(this.appName, rule.id, { action }, rule.version).pipe( - tap(dto => { - this.replaceRule(updateAction(rule, action, this.user, dto.version, now)); - }), - notify(this.dialogs)); + const http$ = + this.rulesService.putRule(this.appName, rule.id, { action }, rule.version).pipe( + map(({ version }) => updateAction(rule, action, this.user, version, now)), share()); + + this.replaceRule(http$); + + return http$; } public updateTrigger(rule: RuleDto, trigger: any, now?: DateTime): Observable { - return this.rulesService.putRule(this.appName, rule.id, { trigger }, rule.version).pipe( - tap(dto => { - this.replaceRule(updateTrigger(rule, trigger, this.user, dto.version, now)); - }), - notify(this.dialogs)); + const http$ = + this.rulesService.putRule(this.appName, rule.id, { trigger }, rule.version).pipe( + map(({ version }) => updateTrigger(rule, trigger, this.user, version, now)), share()); + + this.replaceRule(http$); + + return http$; } public enable(rule: RuleDto, now?: DateTime): Observable { - return this.rulesService.enableRule(this.appName, rule.id, rule.version).pipe( - tap(dto => { - this.replaceRule(setEnabled(rule, true, this.user, dto.version, now)); - }), - notify(this.dialogs)); + const http$ = + this.rulesService.enableRule(this.appName, rule.id, rule.version).pipe( + map(({ version }) => setEnabled(rule, true, this.user, version, now)), share()); + + this.replaceRule(http$); + + return http$; } public disable(rule: RuleDto, now?: DateTime): Observable { - return this.rulesService.disableRule(this.appName, rule.id, rule.version).pipe( - tap(dto => { - this.replaceRule(setEnabled(rule, false, this.user, dto.version, now)); - }), - notify(this.dialogs)); + const http$ = + this.rulesService.disableRule(this.appName, rule.id, rule.version).pipe( + map(({ version }) => setEnabled(rule, false, this.user, version, now)), share()); + + this.replaceRule(http$); + + return http$; } - private replaceRule(rule: RuleDto) { - this.next(s => { - const rules = s.rules.replaceBy('id', rule); + private replaceRule(http$: Observable) { + http$.subscribe(rule => { + this.next(s => { + const rules = s.rules.replaceBy('id', rule); - return { ...s, rules }; + return { ...s, rules }; + }); + }, error => { + this.dialogs.notifyError(error); }); }