Browse Source

More progress

pull/356/head
Sebastian Stehle 7 years ago
parent
commit
b9104c0910
  1. 104
      src/Squidex/app/features/administration/state/event-consumers.state.spec.ts
  2. 33
      src/Squidex/app/features/administration/state/event-consumers.state.ts
  3. 254
      src/Squidex/app/features/administration/state/users.state.spec.ts
  4. 46
      src/Squidex/app/features/administration/state/users.state.ts
  5. 13
      src/Squidex/app/features/rules/pages/rules/rules-page.component.ts
  6. 8
      src/Squidex/app/features/settings/pages/backups/backups-page.component.ts
  7. 5
      src/Squidex/app/framework/utils/immutable-array.ts
  8. 6
      src/Squidex/app/shared/components/comments.component.ts
  9. 19
      src/Squidex/app/shared/state/apps.state.spec.ts
  10. 107
      src/Squidex/app/shared/state/apps.state.ts
  11. 100
      src/Squidex/app/shared/state/backups.state.spec.ts
  12. 81
      src/Squidex/app/shared/state/backups.state.ts
  13. 94
      src/Squidex/app/shared/state/clients.state.spec.ts
  14. 35
      src/Squidex/app/shared/state/clients.state.ts
  15. 108
      src/Squidex/app/shared/state/comments.state.spec.ts
  16. 29
      src/Squidex/app/shared/state/comments.state.ts
  17. 134
      src/Squidex/app/shared/state/contributors.state.spec.ts
  18. 28
      src/Squidex/app/shared/state/contributors.state.ts
  19. 203
      src/Squidex/app/shared/state/languages.state.spec.ts
  20. 53
      src/Squidex/app/shared/state/languages.state.ts
  21. 98
      src/Squidex/app/shared/state/patterns.state.spec.ts
  22. 107
      src/Squidex/app/shared/state/patterns.state.ts
  23. 92
      src/Squidex/app/shared/state/roles.state.spec.ts
  24. 6
      src/Squidex/app/shared/state/roles.state.ts
  25. 154
      src/Squidex/app/shared/state/rules.state.spec.ts
  26. 143
      src/Squidex/app/shared/state/rules.state.ts

104
src/Squidex/app/features/administration/state/event-consumers.state.spec.ts

@ -28,90 +28,88 @@ describe('EventConsumersState', () => {
dialogs = Mock.ofType<DialogService>();
eventConsumersService = Mock.ofType<EventConsumersService>();
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();
});
});
});

33
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<Snapshot> {
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<Snapshot> {
}
});
return stream;
return http$;
}
public start(eventConsumer: EventConsumerDto): Observable<any> {
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<EventConsumerDto> {
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<any> {
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<EventConsumerDto>) {
stream.subscribe(updated => {
private updateState(http$: Observable<EventConsumerDto>) {
http$.subscribe(updated => {
this.replaceEventConsumer(updated);
}, error => {
this.dialogs.notifyError(error);

254
src/Squidex/app/features/administration/state/users.state.spec.ts

@ -40,181 +40,199 @@ describe('UsersState', () => {
dialogs = Mock.ofType<DialogService>();
usersService = Mock.ofType<UsersService>();
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);
});
});
});

46
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<Snapshot> {
}
public select(id: string | null): Observable<SnapshotUser | null> {
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<Snapshot> {
}
private loadInternal(isReload = false): Observable<any> {
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<Snapshot> {
this.dialogs.notifyError(error);
});
return stream;
return http$;
}
public create(request: CreateUserDto): Observable<UserDto> {
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<Snapshot> {
});
});
return stream;
return http$;
}
public update(user: UserDto, request: UpdateUserDto): Observable<UserDto> {
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<UserDto> {
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<UserDto> {
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<UsersResult> {

13
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);
}
}

8
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) {

5
src/Squidex/app/framework/utils/immutable-array.ts

@ -17,11 +17,6 @@ function freeze<T>(items: T[]): T[] {
return items;
}
export function array<V>(items?: V[]): ImmutableArray<V> {
return ImmutableArray.of(items);
}
export class ImmutableArray<T> implements Iterable<T> {
private static readonly EMPTY = new ImmutableArray<any>([]);
private readonly items: T[];

6
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();
}

19
src/Squidex/app/shared/state/apps.state.spec.ts

@ -37,13 +37,16 @@ describe('AppsState', () => {
appsService = Mock.ofType<AppsService>();
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();

107
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<AppDto>;
apps: AppsList;
// The selected app.
selectedApp: AppDto | null;
}
type AppsList = ImmutableArray<AppDto>;
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<Snapshot> {
}
public select(name: string | null): Observable<AppDto | null> {
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<any> {
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<AppDto> {
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<any> {
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<any> {
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$;
}
}

100
src/Squidex/app/shared/state/backups.state.spec.ts

@ -38,70 +38,88 @@ describe('BackupsState', () => {
dialogs = Mock.ofType<DialogService>();
backupsService = Mock.ofType<BackupsService>();
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());
});
});
});

81
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<BackupDto>;
backups: BackupsList;
// Indicates if the backups are loaded.
isLoaded?: boolean;
}
type BackupsList = ImmutableArray<BackupDto>;
@Injectable()
export class BackupsState extends State<Snapshot> {
public backups =
@ -55,41 +56,55 @@ export class BackupsState extends State<Snapshot> {
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<any> {
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<any> {
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() {

94
src/Squidex/app/shared/state/clients.state.spec.ts

@ -40,70 +40,84 @@ describe('ClientsState', () => {
dialogs = Mock.ofType<DialogService>();
clientsService = Mock.ofType<ClientsService>();
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);
});
});
});

35
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<Snapshot> {
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<ClientDto> {
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<any> {
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<Snapshot> {
});
});
return stream;
return http$;
}
public update(client: ClientDto, request: UpdateClientDto): Observable<ClientDto> {
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<Snapshot> {
});
});
return stream.pipe(map(x => x.client));
return http$.pipe(map(x => x.client));
}
private get appName() {

108
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<DialogService>();
commentsService = Mock.ofType<CommentsService>();
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)
]));
});
});
});

29
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<Snapshot> {
super({ comments: ImmutableArray.empty(), version: new Version('-1') });
}
public load(): Observable<CommentsDto> {
const stream =
public load(): Observable<any> {
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<Snapshot> {
this.dialogs.notifyError(error);
});
return stream;
return http$;
}
public create(text: string): Observable<CommentDto> {
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<Snapshot> {
this.dialogs.notifyError(error);
});
return stream;
return http$;
}
public update(comment: CommentDto, text: string, now?: DateTime): Observable<CommentDto> {
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<Snapshot> {
this.dialogs.notifyError(error);
});
return stream;
return http$;
}
public delete(comment: CommentDto): Observable<any> {
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<Snapshot> {
this.dialogs.notifyError(error);
});
return stream;
return http$;
}
private get version() {

134
src/Squidex/app/shared/state/contributors.state.spec.ts

@ -42,89 +42,103 @@ describe('ContributorsState', () => {
dialogs = Mock.ofType<DialogService>();
contributorsService = Mock.ofType<ContributorsService>();
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);
});
});
});

28
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<Snapshot> {
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<any> {
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<Snapshot> {
this.dialogs.notifyError(error);
});
return stream;
return http$;
}
public assign(request: AssignContributorDto): Observable<boolean | undefined> {
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<Snapshot> {
}),
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<Snapshot> {
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) {

203
src/Squidex/app/shared/state/languages.state.spec.ts

@ -41,9 +41,9 @@ describe('LanguagesState', () => {
];
let dialogs: IMock<DialogService>;
let allLanguagesService: IMock<LanguagesService>;
let languagesService: IMock<AppLanguagesService>;
let languagesState: LanguagesState;
let allLanguagesService: IMock<LanguagesService>;
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
@ -51,111 +51,126 @@ describe('LanguagesState', () => {
allLanguagesService = Mock.ofType<LanguagesService>();
allLanguagesService.setup(x => x.getLanguages())
.returns(() => of([languageDE, languageEN, languageIT, languageES]));
.returns(() => of([languageDE, languageEN, languageIT, languageES])).verifiable();
languagesService = Mock.ofType<AppLanguagesService>();
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);
});
});
});

53
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<LanguageDto>;
fallbackLanguages: LanguageList;
// The fallback languages that have not been added yet.
fallbackLanguagesNew: ImmutableArray<LanguageDto>;
fallbackLanguagesNew: LanguageList;
}
interface Snapshot {
// the configured languages as plan format.
plainLanguages: ImmutableArray<AppLanguageDto>;
plainLanguages: AppLanguagesList;
// All supported languages.
allLanguages: ImmutableArray<LanguageDto>;
allLanguages: LanguageList;
// The languages that have not been added yet.
allLanguagesNew: ImmutableArray<LanguageDto>;
allLanguagesNew: LanguageList;
// The configured languages with extra information.
languages: ImmutableArray<SnapshotLanguage>;
languages: LanguageResultList;
// The app version.
version: Version;
@ -57,6 +57,10 @@ interface Snapshot {
isLoaded?: boolean;
}
type AppLanguagesList = ImmutableArray<AppLanguageDto>;
type LanguageList = ImmutableArray<LanguageDto>;
type LanguageResultList = ImmutableArray<SnapshotLanguage>;
@Injectable()
export class LanguagesState extends State<Snapshot> {
public languages =
@ -91,22 +95,29 @@ export class LanguagesState extends State<Snapshot> {
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<any> {
@ -147,7 +158,7 @@ export class LanguagesState extends State<Snapshot> {
notify(this.dialogs));
}
private replaceLanguages(languages: ImmutableArray<AppLanguageDto>, version: Version, allLanguages?: ImmutableArray<LanguageDto>) {
private replaceLanguages(languages: AppLanguagesList, version: Version, allLanguages?: LanguageList) {
this.next(s => {
allLanguages = allLanguages || s.allLanguages;
@ -177,7 +188,7 @@ export class LanguagesState extends State<Snapshot> {
return this.snapshot.version;
}
private createLanguage(language: AppLanguageDto, languages: ImmutableArray<AppLanguageDto>): SnapshotLanguage {
private createLanguage(language: AppLanguageDto, languages: AppLanguagesList): SnapshotLanguage {
return {
language,
fallbackLanguages:

98
src/Squidex/app/shared/state/patterns.state.spec.ts

@ -40,66 +40,84 @@ describe('PatternsState', () => {
dialogs = Mock.ofType<DialogService>();
patternsService = Mock.ofType<PatternsService>();
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);
});
});
});

107
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<PatternDto>;
patterns: PatternsList;
// The app version.
version: Version;
@ -36,6 +35,8 @@ interface Snapshot {
isLoaded?: boolean;
}
type PatternsList = ImmutableArray<PatternDto>;
@Injectable()
export class PatternsState extends State<Snapshot> {
public patterns =
@ -59,55 +60,79 @@ export class PatternsState extends State<Snapshot> {
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<any> {
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<PatternDto> {
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<any> {
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<PatternDto> {
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<any> {
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() {

92
src/Squidex/app/shared/state/roles.state.spec.ts

@ -40,65 +40,79 @@ describe('RolesState', () => {
dialogs = Mock.ofType<DialogService>();
rolesService = Mock.ofType<RolesService>();
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);
});
});
});

6
src/Squidex/app/shared/state/roles.state.ts

@ -28,7 +28,7 @@ import {
interface Snapshot {
// The current roles.
roles: ImmutableArray<RoleDto>;
roles: RolesList;
// The app version.
version: Version;
@ -37,6 +37,8 @@ interface Snapshot {
isLoaded?: boolean;
}
type RolesList = ImmutableArray<RoleDto>;
@Injectable()
export class RolesState extends State<Snapshot> {
public roles =
@ -91,7 +93,7 @@ export class RolesState extends State<Snapshot> {
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 };
});

154
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<DialogService>();
rulesService = Mock.ofType<RulesService>();
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<UpdateRuleDto>(() => 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<UpdateRuleDto>(() => 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);
}
});
});

143
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<RuleDto>;
rules: RulesList;
// Indicates if the rules are loaded.
isLoaded?: boolean;
}
type RulesList = ImmutableArray<RuleDto>;
@Injectable()
export class RulesState extends State<Snapshot> {
public rules =
@ -59,82 +60,112 @@ export class RulesState extends State<Snapshot> {
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<any> {
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<RuleDto> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<RuleDto>) {
http$.subscribe(rule => {
this.next(s => {
const rules = s.rules.replaceBy('id', rule);
return { ...s, rules };
return { ...s, rules };
});
}, error => {
this.dialogs.notifyError(error);
});
}

Loading…
Cancel
Save