Browse Source

Continued

pull/356/head
Sebastian Stehle 7 years ago
parent
commit
d8628ccb5c
  1. 12
      src/Squidex/.vscode/settings.json
  2. 2
      src/Squidex/app-config/webpack.config.js
  3. 4
      src/Squidex/app-config/webpack.test.coverage.js
  4. 2
      src/Squidex/app/features/administration/pages/restore/restore-page.component.html
  5. 25
      src/Squidex/app/features/administration/pages/restore/restore-page.component.ts
  6. 11
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  7. 35
      src/Squidex/app/features/administration/services/users.service.ts
  8. 83
      src/Squidex/app/features/administration/state/event-consumers.state.ts
  9. 139
      src/Squidex/app/features/administration/state/users.state.ts
  10. 9
      src/Squidex/app/features/assets/pages/assets-filters-page.component.ts
  11. 15
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  12. 5
      src/Squidex/app/features/content/pages/content/content-history-page.component.ts
  13. 2
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  14. 3
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts
  15. 18
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  16. 3
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  17. 11
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  18. 13
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts
  19. 13
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  20. 3
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  21. 4
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  22. 3
      src/Squidex/app/features/settings/pages/languages/language.component.ts
  23. 5
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  24. 3
      src/Squidex/app/features/settings/pages/patterns/pattern.component.ts
  25. 5
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts
  26. 7
      src/Squidex/app/features/settings/pages/plans/plans-page.component.ts
  27. 3
      src/Squidex/app/features/settings/pages/roles/role.component.ts
  28. 5
      src/Squidex/app/features/settings/pages/roles/roles-page.component.ts
  29. 2
      src/Squidex/app/framework/angular/http/http-extensions.ts
  30. 59
      src/Squidex/app/framework/utils/rxjs-extensions.ts
  31. 11
      src/Squidex/app/framework/utils/version.spec.ts
  32. 12
      src/Squidex/app/framework/utils/version.ts
  33. 17
      src/Squidex/app/shared/components/asset-uploader.component.ts
  34. 15
      src/Squidex/app/shared/components/asset.component.ts
  35. 25
      src/Squidex/app/shared/components/assets-list.component.ts
  36. 9
      src/Squidex/app/shared/components/assets-selector.component.ts
  37. 7
      src/Squidex/app/shared/components/history.component.ts
  38. 3
      src/Squidex/app/shared/components/schema-category.component.ts
  39. 8
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  40. 31
      src/Squidex/app/shared/services/app-languages.service.ts
  41. 30
      src/Squidex/app/shared/services/apps.service.ts
  42. 22
      src/Squidex/app/shared/services/assets.service.ts
  43. 27
      src/Squidex/app/shared/services/backups.service.ts
  44. 8
      src/Squidex/app/shared/services/clients.service.spec.ts
  45. 31
      src/Squidex/app/shared/services/clients.service.ts
  46. 24
      src/Squidex/app/shared/services/comments.service.ts
  47. 50
      src/Squidex/app/shared/services/contents.service.ts
  48. 15
      src/Squidex/app/shared/services/contributors.service.spec.ts
  49. 38
      src/Squidex/app/shared/services/contributors.service.ts
  50. 11
      src/Squidex/app/shared/services/history.service.ts
  51. 21
      src/Squidex/app/shared/services/languages.service.ts
  52. 20
      src/Squidex/app/shared/services/news.service.ts
  53. 10
      src/Squidex/app/shared/services/patterns.service.spec.ts
  54. 34
      src/Squidex/app/shared/services/patterns.service.ts
  55. 21
      src/Squidex/app/shared/services/plans.service.spec.ts
  56. 51
      src/Squidex/app/shared/services/plans.service.ts
  57. 7
      src/Squidex/app/shared/services/roles.service.spec.ts
  58. 29
      src/Squidex/app/shared/services/roles.service.ts
  59. 42
      src/Squidex/app/shared/services/rules.service.ts
  60. 36
      src/Squidex/app/shared/services/schemas.service.ts
  61. 4
      src/Squidex/app/shared/services/translations.service.ts
  62. 39
      src/Squidex/app/shared/services/usages.service.ts
  63. 21
      src/Squidex/app/shared/services/users.service.ts
  64. 20
      src/Squidex/app/shared/state/apps.state.spec.ts
  65. 109
      src/Squidex/app/shared/state/apps.state.ts
  66. 4
      src/Squidex/app/shared/state/assets.state.spec.ts
  67. 6
      src/Squidex/app/shared/state/assets.state.ts
  68. 65
      src/Squidex/app/shared/state/backups.state.ts
  69. 15
      src/Squidex/app/shared/state/clients.state.spec.ts
  70. 97
      src/Squidex/app/shared/state/clients.state.ts
  71. 124
      src/Squidex/app/shared/state/comments.state.ts
  72. 19
      src/Squidex/app/shared/state/contents.state.ts
  73. 15
      src/Squidex/app/shared/state/contributors.state.spec.ts
  74. 79
      src/Squidex/app/shared/state/contributors.state.ts
  75. 11
      src/Squidex/app/shared/state/languages.state.spec.ts
  76. 63
      src/Squidex/app/shared/state/languages.state.ts
  77. 15
      src/Squidex/app/shared/state/patterns.state.spec.ts
  78. 105
      src/Squidex/app/shared/state/patterns.state.ts
  79. 147
      src/Squidex/app/shared/state/plans.state.spec.ts
  80. 18
      src/Squidex/app/shared/state/plans.state.ts
  81. 15
      src/Squidex/app/shared/state/roles.state.spec.ts
  82. 40
      src/Squidex/app/shared/state/roles.state.ts
  83. 8
      src/Squidex/app/shared/state/rule-events.state.ts
  84. 12
      src/Squidex/app/shared/state/rules.state.spec.ts
  85. 143
      src/Squidex/app/shared/state/rules.state.ts
  86. 630
      src/Squidex/app/shared/state/schemas.state.spec.ts
  87. 179
      src/Squidex/app/shared/state/schemas.state.ts
  88. 10700
      src/Squidex/package-lock.json
  89. 93
      src/Squidex/package.json
  90. 8
      src/Squidex/tsconfig.json

12
src/Squidex/.vscode/settings.json

@ -7,7 +7,6 @@
// Configure glob patterns for excluding files and folders. // Configure glob patterns for excluding files and folders.
"files.exclude": { "files.exclude": {
"_test-output": true,
"**/node_modules": true, "**/node_modules": true,
"**/Assets": true, "**/Assets": true,
"**/artifacts": true, "**/artifacts": true,
@ -23,10 +22,15 @@
"**/*.user": true, "**/*.user": true,
"**/*.xproj": true, "**/*.xproj": true,
"**/*.gitattributes": true, "**/*.gitattributes": true,
"appsetttings.Development.json", "appsetttings.Development.json": true,
"appsetttings.Production.json", "appsetttings.Production.json": true,
".awcache": true, ".awcache": true,
".vs:": true, ".vs:": true,
".vscode:": true ".vscode:": true
} },
"coverage-gutters.coverageFileNames": [
"_test-output/coverage/lcov.info"
],
"coverage-gutters.showLineCoverage": true
} }

2
src/Squidex/app-config/webpack.config.js

@ -57,7 +57,7 @@ module.exports = {
}, { }, {
test: /\.ts$/, test: /\.ts$/,
use: [{ use: [{
loader: 'awesome-typescript-loader', options: { useCache: true, useBabel: true } loader: 'awesome-typescript-loader'
}, { }, {
loader: 'angular-router-loader' loader: 'angular-router-loader'
}, { }, {

4
src/Squidex/app-config/webpack.test.coverage.js

@ -16,7 +16,7 @@ module.exports = webpackMerge(testConfig, {
rules: [{ rules: [{
test: /\.ts$/, test: /\.ts$/,
use: [{ use: [{
loader: 'awesome-typescript-loader' loader: 'ts-loader'
}], }],
include: [/\.(e2e|spec)\.ts$/], include: [/\.(e2e|spec)\.ts$/],
@ -25,7 +25,7 @@ module.exports = webpackMerge(testConfig, {
use: [{ use: [{
loader: 'istanbul-instrumenter-loader' loader: 'istanbul-instrumenter-loader'
}, { }, {
loader: 'awesome-typescript-loader' loader: 'ts-loader'
}, { }, {
loader: 'angular-router-loader' loader: 'angular-router-loader'
}, { }, {

2
src/Squidex/app/features/administration/pages/restore/restore-page.component.html

@ -6,7 +6,7 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="card section" *ngIf="restoreJob; let job"> <div class="card section" *ngIf="restoreJob | async; let job">
<div class="card-header"> <div class="card-header">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-auto pr-2"> <div class="col-auto pr-2">

25
src/Squidex/app/features/administration/pages/restore/restore-page.component.ts

@ -5,18 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import { import {
AuthService, AuthService,
BackupsService, BackupsService,
DialogService, DialogService,
ResourceOwner, RestoreForm,
RestoreDto, switchSafe
RestoreForm
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -24,27 +22,18 @@ import {
styleUrls: ['./restore-page.component.scss'], styleUrls: ['./restore-page.component.scss'],
templateUrl: './restore-page.component.html' templateUrl: './restore-page.component.html'
}) })
export class RestorePageComponent extends ResourceOwner implements OnInit { export class RestorePageComponent {
public restoreJob: RestoreDto | null;
public restoreForm = new RestoreForm(this.formBuilder); public restoreForm = new RestoreForm(this.formBuilder);
public restoreJob =
timer(0, 2000).pipe(switchSafe(() => this.backupsService.getRestore()));
constructor( constructor(
public readonly authState: AuthService, public readonly authState: AuthService,
private readonly backupsService: BackupsService, private readonly backupsService: BackupsService,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
super();
}
public ngOnInit() {
this.own(
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext())))
.subscribe(job => {
if (job) {
this.restoreJob = job;
}
}));
} }
public restore() { public restore() {

11
src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -40,15 +40,16 @@ export class EventConsumersService {
const url = this.apiUrl.buildUrl('/api/event-consumers'); const url = this.apiUrl.buildUrl('/api/event-consumers');
return this.http.get<any[]>(url).pipe( return this.http.get<any[]>(url).pipe(
map(response => { map(body => {
return response.map(item => { const eventConsumers = body.map(item =>
return new EventConsumerDto( new EventConsumerDto(
item.name, item.name,
item.isStopped, item.isStopped,
item.isResetting, item.isResetting,
item.error, item.error,
item.position); item.position));
});
return eventConsumers;
}), }),
pretifyError('Failed to load event consumers. Please reload.')); pretifyError('Failed to load event consumers. Please reload.'));
} }

35
src/Squidex/app/features/administration/services/users.service.ts

@ -61,17 +61,16 @@ export class UsersService {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`); const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return this.http.get<{ total: number, items: any[] }>(url).pipe( return this.http.get<{ total: number, items: any[] }>(url).pipe(
map(response => { map(body => {
const users = response.items.map(item => { const users = body.items.map(item =>
return new UserDto( new UserDto(
item.id, item.id,
item.email, item.email,
item.displayName, item.displayName,
item.permissions, item.permissions,
item.isLocked); item.isLocked));
});
return new UsersDto(response.total, users); return new UsersDto(body.total, users);
}), }),
pretifyError('Failed to load users. Please reload.')); pretifyError('Failed to load users. Please reload.'));
} }
@ -80,13 +79,15 @@ export class UsersService {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`); const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
return new UserDto( const user = new UserDto(
response.id, body.id,
response.email, body.email,
response.displayName, body.displayName,
response.permissions, body.permissions,
response.isLocked); body.isLocked);
return user;
}), }),
pretifyError('Failed to load user. Please reload.')); pretifyError('Failed to load user. Please reload.'));
} }
@ -95,13 +96,15 @@ export class UsersService {
const url = this.apiUrl.buildUrl('api/user-management'); const url = this.apiUrl.buildUrl('api/user-management');
return this.http.post<any>(url, dto).pipe( return this.http.post<any>(url, dto).pipe(
map(response => { map(body => {
return new UserDto( const user = new UserDto(
response.id, body.id,
dto.email, dto.email,
dto.displayName, dto.displayName,
dto.permissions, dto.permissions,
false); false);
return user;
}), }),
pretifyError('Failed to create user. Please reload.')); pretifyError('Failed to create user. Please reload.'));
} }

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

@ -7,11 +7,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
shareSubscribed,
State State
} from '@app/shared'; } from '@app/shared';
@ -49,66 +50,46 @@ export class EventConsumersState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
const http$ = return this.eventConsumersService.getEventConsumers().pipe(
this.eventConsumersService.getEventConsumers().pipe( tap(payload => {
share()); if (isReload && !silent) {
this.dialogs.notifyInfo('Event Consumers reloaded.');
}
http$.subscribe(response => { const eventConsumers = ImmutableArray.of(payload);
if (isReload && !silent) {
this.dialogs.notifyInfo('Event Consumers reloaded.');
}
const eventConsumers = ImmutableArray.of(response); this.next(s => {
return { ...s, eventConsumers, isLoaded: true };
this.next(s => { });
return { ...s, eventConsumers, isLoaded: true }; }),
}); shareSubscribed(this.dialogs, { silent }));
}, error => {
if (!silent) {
this.dialogs.notifyError(error);
}
});
return http$;
} }
public start(eventConsumer: EventConsumerDto): Observable<any> { public start(eventConsumer: EventConsumerDto): Observable<any> {
const http$ = return this.eventConsumersService.putStart(eventConsumer.name).pipe(
this.eventConsumersService.putStart(eventConsumer.name).pipe( map(() => setStopped(eventConsumer, false)),
map(() => setStopped(eventConsumer, false), share())); tap(updated => {
this.replaceEventConsumer(updated);
this.updateState(http$); }),
shareSubscribed(this.dialogs));
return http$;
} }
public stop(eventConsumer: EventConsumerDto): Observable<EventConsumerDto> { public stop(eventConsumer: EventConsumerDto): Observable<EventConsumerDto> {
const http$ = return this.eventConsumersService.putStop(eventConsumer.name).pipe(
this.eventConsumersService.putStop(eventConsumer.name).pipe( map(() => setStopped(eventConsumer, true)),
map(() => setStopped(eventConsumer, true), share())); tap(updated => {
this.replaceEventConsumer(updated);
this.updateState(http$); }),
shareSubscribed(this.dialogs));
return http$;
}
public reset(eventConsumer: EventConsumerDto): Observable<any> {
const http$ =
this.eventConsumersService.putReset(eventConsumer.name).pipe(
map(() => reset(eventConsumer), share()));
this.updateState(http$);
return http$;
} }
private updateState(http$: Observable<EventConsumerDto>) { public reset(eventConsumer: EventConsumerDto): Observable<EventConsumerDto> {
http$.subscribe(updated => { return this.eventConsumersService.putReset(eventConsumer.name).pipe(
this.replaceEventConsumer(updated); map(() => reset(eventConsumer)),
}, error => { tap(updated => {
this.dialogs.notifyError(error); this.replaceEventConsumer(updated);
}); }),
shareSubscribed(this.dialogs));
} }
private replaceEventConsumer(eventConsumer: EventConsumerDto) { private replaceEventConsumer(eventConsumer: EventConsumerDto) {

139
src/Squidex/app/features/administration/state/users.state.ts

@ -7,7 +7,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, share } from 'rxjs/operators'; import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
import '@app/framework/utils/rxjs-extensions'; import '@app/framework/utils/rxjs-extensions';
@ -16,6 +16,7 @@ import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
Pager, Pager,
shareSubscribed,
State State
} from '@app/shared'; } from '@app/shared';
@ -81,15 +82,11 @@ export class UsersState extends State<Snapshot> {
} }
public select(id: string | null): Observable<SnapshotUser | null> { public select(id: string | null): Observable<SnapshotUser | null> {
const http$ = return this.loadUser(id).pipe(
this.loadUser(id).pipe( tap(selectedUser => {
share()); this.next(s => ({ ...s, selectedUser }));
}),
http$.subscribe(selectedUser => { shareSubscribed(this.dialogs, { silent: true }));
this.next(s => ({ ...s, selectedUser }));
});
return http$;
} }
private loadUser(id: string | null) { private loadUser(id: string | null) {
@ -115,83 +112,69 @@ export class UsersState extends State<Snapshot> {
} }
private loadInternal(isReload = false): Observable<any> { private loadInternal(isReload = false): Observable<any> {
const http$ = return this.usersService.getUsers(
this.usersService.getUsers(
this.snapshot.usersPager.pageSize, this.snapshot.usersPager.pageSize,
this.snapshot.usersPager.skip, this.snapshot.usersPager.skip,
this.snapshot.usersQuery).pipe( this.snapshot.usersQuery).pipe(
share()); tap(({ total, items }) => {
if (isReload) {
http$.subscribe(response => { this.dialogs.notifyInfo('Users reloaded.');
if (isReload) {
this.dialogs.notifyInfo('Users reloaded.');
}
this.next(s => {
const usersPager = s.usersPager.setCount(response.total);
const users = ImmutableArray.of(response.items.map(x => this.createUser(x)));
let selectedUser = s.selectedUser;
if (selectedUser) {
selectedUser = users.find(x => x.user.id === selectedUser!.user.id) || selectedUser;
} }
return { ...s, users, usersPager, selectedUser, isLoaded: true }; this.next(s => {
}); const usersPager = s.usersPager.setCount(total);
const users = ImmutableArray.of(items.map(x => this.createUser(x)));
}, error => { let selectedUser = s.selectedUser;
this.dialogs.notifyError(error);
}); if (selectedUser) {
selectedUser = users.find(x => x.user.id === selectedUser!.user.id) || selectedUser;
}
return http$; return { ...s, users, usersPager, selectedUser, isLoaded: true };
});
}),
shareSubscribed(this.dialogs));
} }
public create(request: CreateUserDto): Observable<UserDto> { public create(request: CreateUserDto): Observable<UserDto> {
const http$ = return this.usersService.postUser(request).pipe(
this.usersService.postUser(request).pipe( tap(payload => {
share()); this.next(s => {
const users = s.users.pushFront(this.createUser(payload));
http$.subscribe(dto => { const usersPager = s.usersPager.incrementCount();
this.next(s => {
const users = s.users.pushFront(this.createUser(dto));
const usersPager = s.usersPager.incrementCount();
return { ...s, users, usersPager };
});
});
return http$; return { ...s, users, usersPager };
});
}),
shareSubscribed(this.dialogs, { silent: true }));
} }
public update(user: UserDto, request: UpdateUserDto): Observable<UserDto> { public update(user: UserDto, request: UpdateUserDto): Observable<UserDto> {
const http$ = return this.usersService.putUser(user.id, request).pipe(
this.usersService.putUser(user.id, request).pipe( map(() => update(user, request)),
map(() => update(user, request)), share()); tap(updated => {
this.replaceUser(updated);
this.updateState(http$, false); }),
shareSubscribed(this.dialogs));
return http$;
} }
public lock(user: UserDto): Observable<UserDto> { public lock(user: UserDto): Observable<UserDto> {
const http$ = return this.usersService.lockUser(user.id).pipe(
this.usersService.lockUser(user.id).pipe( map(() => setLocked(user, true)),
map(() => setLocked(user, true)), share()); tap(updated => {
this.replaceUser(updated);
this.updateState(http$, true); }),
shareSubscribed(this.dialogs));
return http$;
} }
public unlock(user: UserDto): Observable<UserDto> { public unlock(user: UserDto): Observable<UserDto> {
const http$ = return this.usersService.unlockUser(user.id).pipe(
this.usersService.unlockUser(user.id).pipe( map(() => setLocked(user, false)),
map(() => setLocked(user, false)), share()); tap(updated => {
this.replaceUser(updated);
this.updateState(http$, true); }),
shareSubscribed(this.dialogs));
return http$;
} }
public search(query: string): Observable<UsersResult> { public search(query: string): Observable<UsersResult> {
@ -214,7 +197,7 @@ export class UsersState extends State<Snapshot> {
private replaceUser(user: UserDto) { private replaceUser(user: UserDto) {
return this.next(s => { return this.next(s => {
const users = s.users.map(u => u.user.id === user.id ? this.createUser(user, u) : u); const users = s.users.map(u => u.user.id === user.id ? this.createUser(user) : u);
const selectedUser = const selectedUser =
s.selectedUser && s.selectedUser &&
@ -230,24 +213,8 @@ export class UsersState extends State<Snapshot> {
return this.authState.user!.id; return this.authState.user!.id;
} }
private updateState(stream: Observable<UserDto>, notify: boolean) { private createUser(user: UserDto): SnapshotUser {
stream.subscribe(dto => { return { user, isCurrentUser: user.id === this.userId };
this.replaceUser(dto);
}, error => {
if (notify) {
this.dialogs.notifyError(error);
}
});
}
private createUser(user: UserDto, current?: SnapshotUser): SnapshotUser {
if (!user) {
return null!;
} else if (current && current.user === user) {
return current;
} else {
return { user, isCurrentUser: user.id === this.userId };
}
} }
} }

9
src/Squidex/app/features/assets/pages/assets-filters-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AssetsState, AssetsState,
@ -29,19 +28,19 @@ export class AssetsFiltersPageComponent {
} }
public search(query: string) { public search(query: string) {
this.assetsState.search(query).pipe(onErrorResumeNext()).subscribe(); this.assetsState.search(query);
} }
public selectTags(tags: string[]) { public selectTags(tags: string[]) {
this.assetsState.selectTags(tags).pipe(onErrorResumeNext()).subscribe(); this.assetsState.selectTags(tags);
} }
public toggleTag(tag: string) { public toggleTag(tag: string) {
this.assetsState.toggleTag(tag).pipe(onErrorResumeNext()).subscribe(); this.assetsState.toggleTag(tag);
} }
public resetTags() { public resetTags() {
this.assetsState.resetTags().pipe(onErrorResumeNext()).subscribe(); this.assetsState.resetTags();
} }
public isSelectedQuery(query: string) { public isSelectedQuery(query: string) {

15
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -7,7 +7,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -45,7 +44,7 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.assetsState.load().pipe(onErrorResumeNext()).subscribe(); this.assetsState.load();
this.own( this.own(
this.assetsState.assetsQuery this.assetsState.assetsQuery
@ -55,27 +54,27 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
} }
public reload() { public reload() {
this.assetsState.load(true).pipe(onErrorResumeNext()).subscribe(); this.assetsState.load(true);
} }
public search() { public search() {
this.assetsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe(); this.assetsState.search(this.filter.apiFilter);
} }
public selectTags(tags: string[]) { public selectTags(tags: string[]) {
this.assetsState.selectTags(tags).pipe(onErrorResumeNext()).subscribe(); this.assetsState.selectTags(tags);
} }
public toggleTag(tag: string) { public toggleTag(tag: string) {
this.assetsState.toggleTag(tag).pipe(onErrorResumeNext()).subscribe(); this.assetsState.toggleTag(tag);
} }
public goNext() { public goNext() {
this.assetsState.goNext().pipe(onErrorResumeNext()).subscribe(); this.assetsState.goNext();
} }
public goPrev() { public goPrev() {
this.assetsState.goPrev().pipe(onErrorResumeNext()).subscribe(); this.assetsState.goPrev();
} }
public changeView(isListView: boolean) { public changeView(isListView: boolean) {

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

@ -8,7 +8,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { merge, Observable, timer } from 'rxjs'; import { merge, Observable, timer } from 'rxjs';
import { delay, onErrorResumeNext, switchMap } from 'rxjs/operators'; import { delay } from 'rxjs/operators';
import { import {
allParams, allParams,
@ -17,6 +17,7 @@ import {
HistoryEventDto, HistoryEventDto,
HistoryService, HistoryService,
MessageBus, MessageBus,
switchSafe,
Version Version
} from '@app/shared'; } from '@app/shared';
@ -51,7 +52,7 @@ export class ContentHistoryPageComponent {
timer(0, 10000), timer(0, 10000),
this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)) this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000))
).pipe( ).pipe(
switchMap(() => this.historyService.getHistory(this.appsState.appName, this.channel).pipe(onErrorResumeNext()))); switchSafe(() => this.historyService.getHistory(this.appsState.appName, this.channel)));
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,

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

@ -171,7 +171,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
} }
public discardChanges() { public discardChanges() {
this.contentsState.discardChanges(this.content).pipe(onErrorResumeNext()).subscribe(); this.contentsState.discardChanges(this.content);
} }
public publish() { public publish() {

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

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
ContentsState, ContentsState,
@ -43,7 +42,7 @@ export class ContentsFiltersPageComponent extends ResourceOwner implements OnIni
} }
public search(query: string) { public search(query: string) {
this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe(); this.contentsState.search(query);
} }
public isSelectedQuery(query: string) { public isSelectedQuery(query: string) {

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

@ -77,7 +77,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.schema = schema!; this.schema = schema!;
this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`); this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`);
this.contentsState.init().pipe(onErrorResumeNext()).subscribe(); this.contentsState.init();
})); }));
this.own( this.own(
@ -103,15 +103,15 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
} }
public reload() { public reload() {
this.contentsState.load(true).pipe(onErrorResumeNext()).subscribe(); this.contentsState.load(true);
} }
public deleteSelected() { public deleteSelected() {
this.contentsState.deleteMany(this.selectItems()).pipe(onErrorResumeNext()).subscribe(); this.contentsState.deleteMany(this.selectItems());
} }
public delete(content: ContentDto) { public delete(content: ContentDto) {
this.contentsState.deleteMany([content]).pipe(onErrorResumeNext()).subscribe(); this.contentsState.deleteMany([content]);
} }
public publish(content: ContentDto) { public publish(content: ContentDto) {
@ -147,7 +147,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
} }
public clone(content: ContentDto) { public clone(content: ContentDto) {
this.contentsState.create(content.dataDraft, false).pipe(onErrorResumeNext()).subscribe(); this.contentsState.create(content.dataDraft, false);
} }
private changeContentItems(contents: ContentDto[], action: string) { private changeContentItems(contents: ContentDto[], action: string) {
@ -165,19 +165,19 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
} }
public goArchive(isArchive: boolean) { public goArchive(isArchive: boolean) {
this.contentsState.goArchive(isArchive).pipe(onErrorResumeNext()).subscribe(); this.contentsState.goArchive(isArchive);
} }
public goPrev() { public goPrev() {
this.contentsState.goPrev().pipe(onErrorResumeNext()).subscribe(); this.contentsState.goPrev();
} }
public goNext() { public goNext() {
this.contentsState.goNext().pipe(onErrorResumeNext()).subscribe(); this.contentsState.goNext();
} }
public search() { public search() {
this.contentsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe(); this.contentsState.search(this.filter.apiFilter);
} }
public selectLanguage(language: AppLanguageDto) { public selectLanguage(language: AppLanguageDto) {

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

@ -7,7 +7,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import { AppsState, SchemasState } from '@app/shared'; import { AppsState, SchemasState } from '@app/shared';
@ -26,7 +25,7 @@ export class SchemasPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); this.schemasState.load();
} }
public trackByCategory(index: number, category: string) { public trackByCategory(index: number, category: string) {

11
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
ContentDto, ContentDto,
@ -54,23 +53,23 @@ export class ContentsSelectorComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.contentsState.schema = this.schema; this.contentsState.schema = this.schema;
this.contentsState.load().pipe(onErrorResumeNext()).subscribe(); this.contentsState.load();
} }
public reload() { public reload() {
this.contentsState.load(true).pipe(onErrorResumeNext()).subscribe(); this.contentsState.load(true);
} }
public search() { public search() {
this.contentsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe(); this.contentsState.search(this.filter.apiFilter);
} }
public goNext() { public goNext() {
this.contentsState.goNext().pipe(onErrorResumeNext()).subscribe(); this.contentsState.goNext();
} }
public goPrev() { public goPrev() {
this.contentsState.goPrev().pipe(onErrorResumeNext()).subscribe(); this.contentsState.goPrev();
} }
public isItemSelected(content: ContentDto) { public isItemSelected(content: ContentDto) {

13
src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -29,27 +28,27 @@ export class RuleEventsPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.ruleEventsState.load().pipe(onErrorResumeNext()).subscribe(); this.ruleEventsState.load();
} }
public reload() { public reload() {
this.ruleEventsState.load(true).pipe(onErrorResumeNext()).subscribe(); this.ruleEventsState.load(true);
} }
public goNext() { public goNext() {
this.ruleEventsState.goNext().pipe(onErrorResumeNext()).subscribe(); this.ruleEventsState.goNext();
} }
public goPrev() { public goPrev() {
this.ruleEventsState.goPrev().pipe(onErrorResumeNext()).subscribe(); this.ruleEventsState.goPrev();
} }
public enqueue(event: RuleEventDto) { public enqueue(event: RuleEventDto) {
this.ruleEventsState.enqueue(event).pipe(onErrorResumeNext()).subscribe(); this.ruleEventsState.enqueue(event);
} }
public cancel(event: RuleEventDto) { public cancel(event: RuleEventDto) {
this.ruleEventsState.cancel(event).pipe(onErrorResumeNext()).subscribe(); this.ruleEventsState.cancel(event);
} }
public selectEvent(id: string) { public selectEvent(id: string) {

13
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -7,7 +7,6 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
createProperties, createProperties,
@ -82,23 +81,23 @@ export class FieldComponent implements OnChanges {
} }
public deleteField() { public deleteField() {
this.schemasState.deleteField(this.schema, this.field).pipe(onErrorResumeNext()).subscribe(); this.schemasState.deleteField(this.schema, this.field);
} }
public enableField() { public enableField() {
this.schemasState.enableField(this.schema, this.field).pipe(onErrorResumeNext()).subscribe(); this.schemasState.enableField(this.schema, this.field);
} }
public disableField() { public disableField() {
this.schemasState.disableField(this.schema, this.field).pipe(onErrorResumeNext()).subscribe(); this.schemasState.disableField(this.schema, this.field);
} }
public showField() { public showField() {
this.schemasState.showField(this.schema, this.field).pipe(onErrorResumeNext()).subscribe(); this.schemasState.showField(this.schema, this.field);
} }
public hideField() { public hideField() {
this.schemasState.hideField(this.schema, this.field).pipe(onErrorResumeNext()).subscribe(); this.schemasState.hideField(this.schema, this.field);
} }
public sortFields(fields: NestedFieldDto[]) { public sortFields(fields: NestedFieldDto[]) {
@ -106,7 +105,7 @@ export class FieldComponent implements OnChanges {
} }
public lockField() { public lockField() {
this.schemasState.lockField(this.schema, this.field).pipe(onErrorResumeNext()).subscribe(); this.schemasState.lockField(this.schema, this.field);
} }
public trackByField(index: number, field: NestedFieldDto) { public trackByField(index: number, field: NestedFieldDto) {

3
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -9,7 +9,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -66,7 +65,7 @@ export class SchemaPageComponent extends ResourceOwner implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe(); this.patternsState.load();
this.own( this.own(
this.schemasState.selectedSchema this.schemasState.selectedSchema

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

@ -8,7 +8,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { map, onErrorResumeNext } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -63,7 +63,7 @@ export class SchemasPageComponent extends ResourceOwner implements OnInit {
} }
})); }));
this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); this.schemasState.load();
} }
public removeCategory(name: string) { public removeCategory(name: string) {

3
src/Squidex/app/features/settings/pages/languages/language.component.ts

@ -7,7 +7,6 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AppLanguageDto, AppLanguageDto,
@ -56,7 +55,7 @@ export class LanguageComponent implements OnChanges {
} }
public remove() { public remove() {
this.languagesState.remove(this.language).pipe(onErrorResumeNext()).subscribe(); this.languagesState.remove(this.language);
} }
public save() { public save() {

5
src/Squidex/app/features/settings/pages/languages/languages-page.component.ts

@ -7,7 +7,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AddLanguageForm, AddLanguageForm,
@ -42,11 +41,11 @@ export class LanguagesPageComponent extends ResourceOwner implements OnInit {
} }
})); }));
this.languagesState.load().pipe(onErrorResumeNext()).subscribe(); this.languagesState.load();
} }
public reload() { public reload() {
this.languagesState.load(true).pipe(onErrorResumeNext()).subscribe(); this.languagesState.load(true);
} }
public addLanguage() { public addLanguage() {

3
src/Squidex/app/features/settings/pages/patterns/pattern.component.ts

@ -7,7 +7,6 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
EditPatternForm, EditPatternForm,
@ -41,7 +40,7 @@ export class PatternComponent implements OnInit {
} }
public delete() { public delete() {
this.patternsState.delete(this.pattern).pipe(onErrorResumeNext()).subscribe(); this.patternsState.delete(this.pattern);
} }
public save() { public save() {

5
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts

@ -6,7 +6,6 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -27,11 +26,11 @@ export class PatternsPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe(); this.patternsState.load();
} }
public reload() { public reload() {
this.patternsState.load(true).pipe(onErrorResumeNext()).subscribe(); this.patternsState.load(true);
} }
public trackByPattern(index: number, pattern: PatternDto) { public trackByPattern(index: number, pattern: PatternDto) {

7
src/Squidex/app/features/settings/pages/plans/plans-page.component.ts

@ -7,7 +7,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
ApiUrlConfig, ApiUrlConfig,
@ -39,15 +38,15 @@ export class PlansPageComponent implements OnInit {
this.overridePlanId = params['planId']; this.overridePlanId = params['planId'];
}).unsubscribe(); }).unsubscribe();
this.plansState.load(false, this.overridePlanId).pipe(onErrorResumeNext()).subscribe(); this.plansState.load(false, this.overridePlanId);
} }
public reload() { public reload() {
this.plansState.load(true, this.overridePlanId).pipe(onErrorResumeNext()).subscribe(); this.plansState.load(true, this.overridePlanId);
} }
public change(planId: string) { public change(planId: string) {
this.plansState.change(planId).pipe(onErrorResumeNext()).subscribe(); this.plansState.change(planId);
} }
public trackByPlan(index: number, planInfo: { plan: PlanDto }) { public trackByPlan(index: number, planInfo: { plan: PlanDto }) {

3
src/Squidex/app/features/settings/pages/roles/role.component.ts

@ -7,7 +7,6 @@
import { Component, Input, OnChanges, ViewChild } from '@angular/core'; import { Component, Input, OnChanges, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AddPermissionForm, AddPermissionForm,
@ -76,7 +75,7 @@ export class RoleComponent implements OnChanges {
} }
public remove() { public remove() {
this.rolesState.delete(this.role).pipe(onErrorResumeNext()).subscribe(); this.rolesState.delete(this.role);
} }
public addPermission() { public addPermission() {

5
src/Squidex/app/features/settings/pages/roles/roles-page.component.ts

@ -8,7 +8,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AddRoleForm, AddRoleForm,
@ -50,11 +49,11 @@ export class RolesPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.rolesState.load().pipe(onErrorResumeNext()).subscribe(); this.rolesState.load();
} }
public reload() { public reload() {
this.rolesState.load(true).pipe(onErrorResumeNext()).subscribe(); this.rolesState.load(true);
} }
public cancelAddRole() { public cancelAddRole() {

2
src/Squidex/app/framework/angular/http/http-extensions.ts

@ -56,7 +56,7 @@ export module HTTP {
return httpRequest.pipe(map((response: HttpResponse<T>) => { return httpRequest.pipe(map((response: HttpResponse<T>) => {
const etag = response.headers.get('etag') || ''; const etag = response.headers.get('etag') || '';
return new Versioned(new Version(etag), response); return { version: new Version(etag), payload: response };
})); }));
} }
} }

59
src/Squidex/app/framework/utils/rxjs-extensions.ts

@ -5,16 +5,61 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
// tslint:disable: only-arrow-functions
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError, map, onErrorResumeNext, shareReplay, switchMap } from 'rxjs/operators';
import { DialogService } from './../services/dialog.service'; import { DialogService } from './../services/dialog.service';
/* tslint:disable:no-shadowed-variable */ import {
Version,
versioned,
Versioned
} from './version';
export function mapVersioned<T = any, R = any>(project: (value: T, version: Version) => R) {
return function mapOperation(source: Observable<Versioned<T>>) {
return source.pipe(map<Versioned<T>, Versioned<R>>(({ version, payload }) => {
return versioned(version, project(payload, version));
}));
};
}
export function notify<T>(dialogs: DialogService) {
return function mapOperation(source: Observable<T>) {
return source.pipe(catchError(error => {
dialogs.notifyError(error);
return throwError(error);
}));
};
}
type Options<T, R = T> = { silent?: boolean, project?: ((value: T) => R) };
export function shareSubscribed<T, R = T>(dialogs: DialogService, options?: Options<T, R>) {
return function mapOperation(source: Observable<T>) {
const shared = source.pipe(shareReplay());
shared.subscribe(undefined, error => {
if (dialogs && (!options || !options.silent)) {
dialogs.notifyError(error);
}
});
if (options && !!options.project) {
const project = options.project;
export const notify = (dialogs: DialogService) => <T>(source: Observable<T>) => return shared.pipe(map(x => project(x)));
source.pipe(catchError(error => { } else {
dialogs.notifyError(error); return <any>shared;
}
};
}
return throwError(error); export function switchSafe<T, R>(project: (source: T) => Observable<R>) {
})); return function mapOperation(source: Observable<T>) {
return source.pipe(switchMap(project), onErrorResumeNext<R, R>());
};
}

11
src/Squidex/app/framework/utils/version.spec.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Version, Versioned } from './version'; import { Version } from './version';
describe('Version', () => { describe('Version', () => {
it('should initialize with init value', () => { it('should initialize with init value', () => {
@ -20,13 +20,4 @@ describe('Version', () => {
expect(new Version('W/2').eq(new Version('2'))).toBeTruthy(); expect(new Version('W/2').eq(new Version('2'))).toBeTruthy();
expect(new Version('W/2').eq(new Version('W/2'))).toBeTruthy(); expect(new Version('W/2').eq(new Version('W/2'))).toBeTruthy();
}); });
});
describe('Versioned', () => {
it('should initialize with version and payload', () => {
const versioned = new Versioned(new Version('1.0'), 123);
expect(versioned.version.value).toBe('1.0');
expect(versioned.payload).toBe(123);
});
}); });

12
src/Squidex/app/framework/utils/version.ts

@ -24,10 +24,8 @@ export class Version {
} }
} }
export class Versioned<T> { export function versioned<T = any>(version: Version, payload: T = undefined!): Versioned<T> {
constructor( return { version, payload };
public readonly version: Version, }
public readonly payload: T
) { export type Versioned<T> = { readonly version: Version, readonly payload: T };
}
}

17
src/Squidex/app/shared/components/asset-uploader.component.ts

@ -10,8 +10,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { import {
AssetsState, AssetsState,
DialogModel, DialogModel,
fadeAnimation, fadeAnimation
UploadingAsset
} from '@app/shared/internal'; } from '@app/shared/internal';
@Component({ @Component({
@ -31,19 +30,7 @@ export class AssetUploaderComponent {
) { ) {
} }
public addFiles(files: File[]) { public addFiles() {
for (let file of files) {
this.assets.upload(file).subscribe();
}
this.modalMenu.show(); this.modalMenu.show();
} }
public stopUpload(upload: UploadingAsset) {
this.assets.remove(upload);
}
public trackByUpload(index: number, upload: UploadingAsset) {
return upload.id;
}
} }

15
src/Squidex/app/shared/components/asset.component.ts

@ -17,8 +17,7 @@ import {
DialogService, DialogService,
fadeAnimation, fadeAnimation,
StatefulComponent, StatefulComponent,
Types, Types
Versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
interface State { interface State {
@ -101,10 +100,10 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now()) this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now())
.subscribe(dto => { .subscribe(dto => {
if (Types.is(dto, AssetDto)) { if (Types.isNumber(dto)) {
this.emitLoad(dto);
} else {
this.setProgress(dto); this.setProgress(dto);
} else {
this.emitLoad(dto);
} }
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
@ -120,10 +119,10 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
this.assetsService.replaceFile(this.appsState.appName, this.asset.id, files[0], this.asset.version) this.assetsService.replaceFile(this.appsState.appName, this.asset.id, files[0], this.asset.version)
.subscribe(dto => { .subscribe(dto => {
if (Types.is(dto, Versioned)) { if (Types.isNumber(dto)) {
this.updateAsset(this.asset.update(dto.payload, this.authState.user!.token, dto.version), true);
} else {
this.setProgress(dto); this.setProgress(dto);
} else {
this.updateAsset(this.asset.update(dto.payload, this.authState.user!.token, dto.version), true);
} }
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);

25
src/Squidex/app/shared/components/assets-list.component.ts

@ -11,7 +11,7 @@ import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AssetDto, AssetDto,
AssetsState, AssetsState,
AssetWithUpload ImmutableArray
} from '@app/shared/internal'; } from '@app/shared/internal';
@Component({ @Component({
@ -21,6 +21,8 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetsListComponent { export class AssetsListComponent {
public newFiles = ImmutableArray.empty<File>();
@Input() @Input()
public state: AssetsState; public state: AssetsState;
@ -36,6 +38,12 @@ export class AssetsListComponent {
@Output() @Output()
public select = new EventEmitter<AssetDto>(); public select = new EventEmitter<AssetDto>();
public add(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file);
this.state.add(asset);
}
public search() { public search() {
this.state.load().pipe(onErrorResumeNext()).subscribe(); this.state.load().pipe(onErrorResumeNext()).subscribe();
} }
@ -56,10 +64,6 @@ export class AssetsListComponent {
this.state.update(asset); this.state.update(asset);
} }
public updateFile(asset: AssetDto, file: File) {
this.state.replaceFile(asset, file).pipe(onErrorResumeNext()).subscribe();
}
public emitSelect(asset: AssetDto) { public emitSelect(asset: AssetDto) {
this.select.emit(asset); this.select.emit(asset);
} }
@ -68,9 +72,13 @@ export class AssetsListComponent {
return this.selectedIds && this.selectedIds[asset.id]; return this.selectedIds && this.selectedIds[asset.id];
} }
public remove(file: File) {
this.newFiles = this.newFiles.remove(file);
}
public addFiles(files: File[]) { public addFiles(files: File[]) {
for (let file of files) { for (let file of files) {
this.state.upload(file); this.newFiles = this.newFiles.pushFront(file);
} }
return true; return true;
@ -79,9 +87,4 @@ export class AssetsListComponent {
public trackByAsset(index: number, asset: AssetDto) { public trackByAsset(index: number, asset: AssetDto) {
return asset.id; return asset.id;
} }
public trackByUpload(index: number, upload: AssetWithUpload) {
return upload.asset.id;
}
} }

9
src/Squidex/app/shared/components/assets-selector.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AssetDto, AssetDto,
@ -51,15 +50,15 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
} }
public ngOnInit() { public ngOnInit() {
this.assetsState.load().pipe(onErrorResumeNext()).subscribe(); this.assetsState.load();
} }
public reload() { public reload() {
this.assetsState.load(true).pipe(onErrorResumeNext()).subscribe(); this.assetsState.load(true);
} }
public search() { public search() {
this.assetsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe(); this.assetsState.search(this.filter.apiFilter);
} }
public emitComplete() { public emitComplete() {
@ -71,7 +70,7 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
} }
public selectTags(tags: string[]) { public selectTags(tags: string[]) {
this.assetsState.selectTags(tags).pipe(onErrorResumeNext()).subscribe(); this.assetsState.selectTags(tags);
} }
public selectAsset(asset: AssetDto) { public selectAsset(asset: AssetDto) {

7
src/Squidex/app/shared/components/history.component.ts

@ -8,7 +8,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { merge, Observable, timer } from 'rxjs'; import { merge, Observable, timer } from 'rxjs';
import { delay, onErrorResumeNext, switchMap } from 'rxjs/operators'; import { delay } from 'rxjs/operators';
import { import {
allParams, allParams,
@ -16,7 +16,8 @@ import {
HistoryChannelUpdated, HistoryChannelUpdated,
HistoryEventDto, HistoryEventDto,
HistoryService, HistoryService,
MessageBus MessageBus,
switchSafe
} from '@app/shared/internal'; } from '@app/shared/internal';
@Component({ @Component({
@ -33,7 +34,7 @@ export class HistoryComponent {
timer(0, 10000), timer(0, 10000),
this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)) this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000))
).pipe( ).pipe(
switchMap(() => this.historyService.getHistory(this.appsState.appName, this.channel).pipe(onErrorResumeNext()))); switchSafe(() => this.historyService.getHistory(this.appsState.appName, this.channel)));
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,

3
src/Squidex/app/shared/components/schema-category.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
fadeAnimation, fadeAnimation,
@ -127,7 +126,7 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
} }
public changeCategory(schema: SchemaDto) { public changeCategory(schema: SchemaDto) {
this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe(); this.schemasState.changeCategory(schema, this.name);
} }
public emitRemove() { public emitRemove() {

8
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -71,11 +71,13 @@ describe('AppLanguagesService', () => {
} }
}); });
expect(languages!).toEqual( expect(languages!).toEqual({
new AppLanguagesDto([ payload: [
new AppLanguageDto('en', 'English', true, true, ['de', 'en']), new AppLanguageDto('en', 'English', true, true, ['de', 'en']),
new AppLanguageDto('it', 'Italian', false, false, []) new AppLanguageDto('it', 'Italian', false, false, [])
], new Version('2'))); ],
version: new Version('2')
});
})); }));
it('should make post request to add language', it('should make post request to add language',

31
src/Squidex/app/shared/services/app-languages.service.ts

@ -8,12 +8,13 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
Version, Version,
@ -22,14 +23,7 @@ import {
import { LanguageDto } from './languages.service'; import { LanguageDto } from './languages.service';
export class AppLanguagesDto extends Model<AppLanguagesDto> { export type AppLanguagesDto = Versioned<AppLanguageDto[]>;
constructor(
public readonly languages: AppLanguageDto[],
public readonly version: Version
) {
super();
}
}
export class AppLanguageDto extends Model<AppLanguageDto> { export class AppLanguageDto extends Model<AppLanguageDto> {
constructor( constructor(
@ -75,21 +69,18 @@ export class AppLanguagesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const items: any[] = body; const items: any[] = body;
const languages = items.map(item => { const languages = items.map(item =>
return new AppLanguageDto( new AppLanguageDto(
item.iso2Code, item.iso2Code,
item.englishName, item.englishName,
item.isMaster, item.isMaster,
item.isOptional, item.isOptional,
item.fallback || []); item.fallback || []));
});
return new AppLanguagesDto(languages, response.version); return languages;
}), }),
pretifyError('Failed to load languages. Please reload.')); pretifyError('Failed to load languages. Please reload.'));
} }
@ -98,9 +89,7 @@ export class AppLanguagesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return HTTP.postVersioned<any>(this.http, url, dto, version).pipe( return HTTP.postVersioned<any>(this.http, url, dto, version).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const language = new AppLanguageDto( const language = new AppLanguageDto(
body.iso2Code, body.iso2Code,
body.englishName, body.englishName,
@ -108,7 +97,7 @@ export class AppLanguagesService {
body.isOptional, body.isOptional,
body.fallback || []); body.fallback || []);
return new Versioned(response.version, language); return language;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Language', 'Added', appName); this.analytics.trackEvent('Language', 'Added', appName);

30
src/Squidex/app/shared/services/apps.service.ts

@ -14,7 +14,6 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
HTTP,
Model, Model,
Permission, Permission,
pretifyError pretifyError
@ -51,13 +50,9 @@ export class AppsService {
public getApps(): Observable<AppDto[]> { public getApps(): Observable<AppDto[]> {
const url = this.apiUrl.buildUrl('/api/apps'); const url = this.apiUrl.buildUrl('/api/apps');
return HTTP.getVersioned<any>(this.http, url).pipe( return this.http.get<any[]>(url).pipe(
map(response => { map(body => {
const body = response.payload.body; const apps = body.map(item => {
const items: any[] = body;
return items.map(item => {
const permissions = (<string[]>item.permissions).map(x => new Permission(x)); const permissions = (<string[]>item.permissions).map(x => new Permission(x));
return new AppDto( return new AppDto(
@ -69,6 +64,8 @@ export class AppsService {
item.planName, item.planName,
item.planUpgrade); item.planUpgrade);
}); });
return apps;
}), }),
pretifyError('Failed to load apps. Please reload.')); pretifyError('Failed to load apps. Please reload.'));
} }
@ -76,15 +73,22 @@ export class AppsService {
public postApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> { public postApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
const url = this.apiUrl.buildUrl('api/apps'); const url = this.apiUrl.buildUrl('api/apps');
return HTTP.postVersioned<any>(this.http, url, dto).pipe( return this.http.post<any>(url, dto).pipe(
map(response => { map(body => {
const body = response.payload.body;
now = now || DateTime.now(); now = now || DateTime.now();
const permissions = (<string[]>body.permissions).map(x => new Permission(x)); const permissions = (<string[]>body.permissions).map(x => new Permission(x));
return new AppDto(body.id, dto.name, permissions, now, now, body.planName, body.planUpgrade); const app = new AppDto(
body.id,
dto.name,
permissions,
now,
now,
body.planName,
body.planUpgrade);
return app;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('App', 'Created', dto.name); this.analytics.trackEvent('App', 'Created', dto.name);

22
src/Squidex/app/shared/services/assets.service.ts

@ -21,7 +21,8 @@ import {
ResultSet, ResultSet,
Types, Types,
Version, Version,
Versioned Versioned,
versioned
} from '@app/framework'; } from '@app/framework';
export class AssetsDto extends ResultSet<AssetDto> { } export class AssetsDto extends ResultSet<AssetDto> { }
@ -102,8 +103,7 @@ export class AssetsService {
public getTags(appName: string): Observable<{ [name: string]: number }> { public getTags(appName: string): Observable<{ [name: string]: number }> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/tags`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/tags`);
return HTTP.getVersioned(this.http, url).pipe( return this.http.get<{ [name: string]: number }>(url);
map(response => <any>response.payload.body));
} }
public getAssets(appName: string, take: number, skip: number, query?: string, tags?: string[], ids?: string[]): Observable<AssetsDto> { public getAssets(appName: string, take: number, skip: number, query?: string, tags?: string[], ids?: string[]): Observable<AssetsDto> {
@ -141,12 +141,12 @@ export class AssetsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ payload }) => {
const body = response.payload.body; const body = payload.body;
const items: any[] = body.items; const items: any[] = body.items;
return new AssetsDto(body.total, items.map(item => { const assets = new AssetsDto(body.total, items.map(item => {
const assetUrl = this.apiUrl.buildUrl(`api/assets/${item.id}`); const assetUrl = this.apiUrl.buildUrl(`api/assets/${item.id}`);
return new AssetDto( return new AssetDto(
@ -170,6 +170,8 @@ export class AssetsService {
assetUrl, assetUrl,
new Version(item.version.toString())); new Version(item.version.toString()));
})); }));
return assets;
}), }),
pretifyError('Failed to load assets. Please reload.')); pretifyError('Failed to load assets. Please reload.'));
} }
@ -239,8 +241,8 @@ export class AssetsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ version, payload }) => {
const body = response.payload.body; const body = payload.body;
const assetUrl = this.apiUrl.buildUrl(`api/assets/${body.id}`); const assetUrl = this.apiUrl.buildUrl(`api/assets/${body.id}`);
@ -263,7 +265,7 @@ export class AssetsService {
body.slug, body.slug,
body.tags || [], body.tags || [],
assetUrl, assetUrl,
response.version); version);
}), }),
pretifyError('Failed to load assets. Please reload.')); pretifyError('Failed to load assets. Please reload.'));
} }
@ -285,7 +287,7 @@ export class AssetsService {
} else if (Types.is(event, HttpResponse)) { } else if (Types.is(event, HttpResponse)) {
const response: any = event.body; const response: any = event.body;
return new Versioned(new Version(event.headers.get('etag')!), response); return versioned(new Version(event.headers.get('etag')!), response);
} else { } else {
throw 'Invalid'; throw 'Invalid';
} }

27
src/Squidex/app/shared/services/backups.service.ts

@ -62,16 +62,17 @@ export class BackupsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.get<any[]>(url).pipe( return this.http.get<any[]>(url).pipe(
map(response => { map(body => {
return response.map(item => { const backups = body.map(item =>
return new BackupDto( new BackupDto(
item.id, item.id,
DateTime.parseISO_UTC(item.started), DateTime.parseISO_UTC(item.started),
item.stopped ? DateTime.parseISO_UTC(item.stopped) : null, item.stopped ? DateTime.parseISO_UTC(item.stopped) : null,
item.handledEvents, item.handledEvents,
item.handledAssets, item.handledAssets,
item.status); item.status));
});
return backups;
}), }),
pretifyError('Failed to load backups.')); pretifyError('Failed to load backups.'));
} }
@ -80,13 +81,15 @@ export class BackupsService {
const url = this.apiUrl.buildUrl(`api/apps/restore`); const url = this.apiUrl.buildUrl(`api/apps/restore`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
return new RestoreDto( const restore = new RestoreDto(
response.url, body.url,
DateTime.parseISO_UTC(response.started), DateTime.parseISO_UTC(body.started),
response.stopped ? DateTime.parseISO_UTC(response.stopped) : null, body.stopped ? DateTime.parseISO_UTC(body.stopped) : null,
response.status, body.status,
response.log); body.log);
return restore;
}), }),
catchError(error => { catchError(error => {
if (Types.is(error, HttpErrorResponse) && error.status === 404) { if (Types.is(error, HttpErrorResponse) && error.status === 404) {

8
src/Squidex/app/shared/services/clients.service.spec.ts

@ -71,11 +71,13 @@ describe('ClientsService', () => {
} }
}); });
expect(clients!).toEqual( expect(clients!).toEqual({
new ClientsDto([ payload: [
new ClientDto('client1', 'Client 1', 'secret1', 'Editor'), new ClientDto('client1', 'Client 1', 'secret1', 'Editor'),
new ClientDto('client2', 'Client 2', 'secret2', 'Developer') new ClientDto('client2', 'Client 2', 'secret2', 'Developer')
], new Version('2'))); ],
version: new Version('2')
});
})); }));
it('should make post request to create client', it('should make post request to create client',

31
src/Squidex/app/shared/services/clients.service.ts

@ -14,20 +14,14 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
Version, Version,
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export class ClientsDto extends Model<ClientsDto> { export type ClientsDto = Versioned<ClientDto[]>;
constructor(
public readonly clients: ClientDto[],
public readonly version: Version
) {
super();
}
}
export class ClientDto extends Model<ClientDto> { export class ClientDto extends Model<ClientDto> {
constructor( constructor(
@ -70,20 +64,17 @@ export class ClientsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const items: any[] = body; const items: any[] = body;
const clients = items.map(item => { const clients = items.map(item =>
return new ClientDto( new ClientDto(
item.id, item.id,
item.name || body.id, item.name || item.id,
item.secret, item.secret,
item.role); item.role));
});
return new ClientsDto(clients, response.version); return clients;
}), }),
pretifyError('Failed to load clients. Please reload.')); pretifyError('Failed to load clients. Please reload.'));
} }
@ -92,16 +83,14 @@ export class ClientsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return HTTP.postVersioned<any>(this.http, url, dto, version).pipe( return HTTP.postVersioned<any>(this.http, url, dto, version).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const client = new ClientDto( const client = new ClientDto(
body.id, body.id,
body.name || body.id, body.name || body.id,
body.secret, body.secret,
body.role); body.role);
return new Versioned(response.version, client); return client;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Client', 'Created', appName); this.analytics.trackEvent('Client', 'Created', appName);

24
src/Squidex/app/shared/services/comments.service.ts

@ -56,25 +56,27 @@ export class CommentsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}?version=${version.value}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}?version=${version.value}`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
return new CommentsDto( const comments = new CommentsDto(
response.createdComments.map((item: any) => { body.createdComments.map((item: any) => {
return new CommentDto( return new CommentDto(
item.id, item.id,
DateTime.parseISO_UTC(item.time), DateTime.parseISO_UTC(item.time),
item.text, item.text,
item.user); item.user);
}), }),
response.updatedComments.map((item: any) => { body.updatedComments.map((item: any) => {
return new CommentDto( return new CommentDto(
item.id, item.id,
DateTime.parseISO_UTC(item.time), DateTime.parseISO_UTC(item.time),
item.text, item.text,
item.user); item.user);
}), }),
response.deletedComments, body.deletedComments,
new Version(response.version) new Version(body.version)
); );
return comments;
}), }),
pretifyError('Failed to load comments.')); pretifyError('Failed to load comments.'));
} }
@ -82,15 +84,15 @@ export class CommentsService {
public postComment(appName: string, commentsId: string, dto: UpsertCommentDto): Observable<CommentDto> { public postComment(appName: string, commentsId: string, dto: UpsertCommentDto): Observable<CommentDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}`);
return this.http.post(url, dto).pipe( return this.http.post<any>(url, dto).pipe(
map(response => { map(body => {
const body: any = response; const comment = new CommentDto(
return new CommentDto(
body.id, body.id,
DateTime.parseISO_UTC(body.time), DateTime.parseISO_UTC(body.time),
body.text, body.text,
body.user); body.user);
return comment;
}), }),
pretifyError('Failed to create comment.')); pretifyError('Failed to create comment.'));
} }

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

@ -15,6 +15,7 @@ import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
ResultSet, ResultSet,
@ -99,13 +100,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ payload }) => {
const body = response.payload.body; const body = payload.body;
const items: any[] = body.items; const items: any[] = body.items;
return new ContentsDto(body.total, items.map(item => { const contents = new ContentsDto(body.total, items.map(item =>
return new ContentDto( new ContentDto(
item.id, item.id,
item.status, item.status,
DateTime.parseISO_UTC(item.created), item.createdBy, DateTime.parseISO_UTC(item.created), item.createdBy,
@ -119,8 +120,9 @@ export class ContentsService {
item.isPending === true, item.isPending === true,
item.data, item.data,
item.dataDraft, item.dataDraft,
new Version(item.version.toString())); new Version(item.version.toString()))));
}));
return contents;
}), }),
pretifyError('Failed to load contents. Please reload.')); pretifyError('Failed to load contents. Please reload.'));
} }
@ -129,10 +131,10 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ version, payload }) => {
const body = response.payload.body; const body = payload.body;
return new ContentDto( const content = new ContentDto(
body.id, body.id,
body.status, body.status,
DateTime.parseISO_UTC(body.created), body.createdBy, DateTime.parseISO_UTC(body.created), body.createdBy,
@ -146,7 +148,9 @@ export class ContentsService {
body.isPending === true, body.isPending === true,
body.data, body.data,
body.dataDraft, body.dataDraft,
response.version); version);
return content;
}), }),
pretifyError('Failed to load content. Please reload.')); pretifyError('Failed to load content. Please reload.'));
} }
@ -155,8 +159,8 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { mapVersioned(({ body }) => {
return new Versioned(response.version, response.payload.body); return body;
}), }),
pretifyError('Failed to load data. Please reload.')); pretifyError('Failed to load data. Please reload.'));
} }
@ -165,10 +169,10 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?publish=${publish}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?publish=${publish}`);
return HTTP.postVersioned<any>(this.http, url, dto).pipe( return HTTP.postVersioned<any>(this.http, url, dto).pipe(
map(response => { map(({ version, payload }) => {
const body = response.payload.body; const body = payload.body;
return new ContentDto( const content = new ContentDto(
body.id, body.id,
body.status, body.status,
DateTime.parseISO_UTC(body.created), body.createdBy, DateTime.parseISO_UTC(body.created), body.createdBy,
@ -177,7 +181,9 @@ export class ContentsService {
body.isPending, body.isPending,
null, null,
body.data, body.data,
response.version); version);
return content;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Content', 'Created', appName); this.analytics.trackEvent('Content', 'Created', appName);
@ -189,10 +195,8 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}?asDraft=${asDraft}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}?asDraft=${asDraft}`);
return HTTP.putVersioned(this.http, url, dto, version).pipe( return HTTP.putVersioned(this.http, url, dto, version).pipe(
map(response => { mapVersioned(payload => {
const body = response.payload.body; return payload.body;
return new Versioned(response.version, body);
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName); this.analytics.trackEvent('Content', 'Updated', appName);
@ -204,10 +208,8 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.patchVersioned(this.http, url, dto, version).pipe( return HTTP.patchVersioned(this.http, url, dto, version).pipe(
map(response => { mapVersioned(payload => {
const body = response.payload.body; return payload.body;
return new Versioned(response.version, body);
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Content', 'Updated', appName); this.analytics.trackEvent('Content', 'Updated', appName);

15
src/Squidex/app/shared/services/contributors.service.spec.ts

@ -70,11 +70,16 @@ describe('ContributorsService', () => {
} }
}); });
expect(contributors!).toEqual( expect(contributors!).toEqual({
new ContributorsDto([ payload: {
new ContributorDto('123', 'Owner'), contributors: [
new ContributorDto('456', 'Owner') new ContributorDto('123', 'Owner'),
], 100, new Version('2'))); new ContributorDto('456', 'Owner')
],
maxContributors: 100
},
version: new Version('2')
});
})); }));
it('should make post request to assign contributor', it('should make post request to assign contributor',

38
src/Squidex/app/shared/services/contributors.service.ts

@ -8,27 +8,23 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
Version, Version,
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export class ContributorsDto extends Model<ContributorsDto> { export type ContributorsDto = Versioned<{
constructor( readonly contributors: ContributorDto[],
public readonly contributors: ContributorDto[], readonly maxContributors: number
public readonly maxContributors: number, }>;
public readonly version: Version
) {
super();
}
}
export class ContributorDto extends Model<AssignContributorDto> { export class ContributorDto extends Model<AssignContributorDto> {
constructor( constructor(
@ -63,18 +59,18 @@ export class ContributorsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { mapVersioned(payload => {
const body = response.payload.body; const body = payload.body;
const items: any[] = body.contributors; const items: any[] = body.contributors;
return new ContributorsDto( const contributors =
items.map(item => { items.map(item =>
return new ContributorDto( new ContributorDto(
item.contributorId, item.contributorId,
item.role); item.role));
}),
body.maxContributors, response.version); return { contributors, maxContributors: body.maxContributors };
}), }),
pretifyError('Failed to load contributors. Please reload.')); pretifyError('Failed to load contributors. Please reload.'));
} }
@ -83,10 +79,8 @@ export class ContributorsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.postVersioned(this.http, url, dto, version).pipe( return HTTP.postVersioned(this.http, url, dto, version).pipe(
map(response => { mapVersioned(payload => {
const body: any = response.payload.body; return <ContributorAssignedDto>payload.body;
return new Versioned(response.version, body);
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Contributor', 'Configured', appName); this.analytics.trackEvent('Contributor', 'Configured', appName);

11
src/Squidex/app/shared/services/history.service.ts

@ -82,15 +82,16 @@ export class HistoryService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/history?channel=${channel}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/history?channel=${channel}`);
return this.http.get<any[]>(url).pipe( return this.http.get<any[]>(url).pipe(
map(response => { map(body => {
return response.map(item => { const history = body.map(item =>
return new HistoryEventDto( new HistoryEventDto(
item.eventId, item.eventId,
item.actor, item.actor,
item.message, item.message,
item.version, item.version,
DateTime.parseISO_UTC(item.created)); DateTime.parseISO_UTC(item.created)));
});
return history;
}), }),
pretifyError('Failed to load history. Please reload.')); pretifyError('Failed to load history. Please reload.'));
} }

21
src/Squidex/app/shared/services/languages.service.ts

@ -10,11 +10,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { import { ApiUrlConfig, pretifyError } from '@app/framework';
ApiUrlConfig,
HTTP,
pretifyError
} from '@app/framework';
export class LanguageDto { export class LanguageDto {
constructor( constructor(
@ -35,15 +31,14 @@ export class LanguagesService {
public getLanguages(): Observable<LanguageDto[]> { public getLanguages(): Observable<LanguageDto[]> {
const url = this.apiUrl.buildUrl('api/languages'); const url = this.apiUrl.buildUrl('api/languages');
return HTTP.getVersioned(this.http, url).pipe( return this.http.get<any[]>(url).pipe(
map(response => { map(body => {
const items: any[] = <any>response.payload.body; const languages = body.map(item =>
new LanguageDto(
return items.map(item => {
return new LanguageDto(
item.iso2Code, item.iso2Code,
item.englishName); item.englishName));
});
return languages;
}), }),
pretifyError('Failed to load languages. Please reload.')); pretifyError('Failed to load languages. Please reload.'));
} }

20
src/Squidex/app/shared/services/news.service.ts

@ -40,18 +40,18 @@ export class NewsService {
const url = this.apiUrl.buildUrl(`api/news/features?version=${version}`); const url = this.apiUrl.buildUrl(`api/news/features?version=${version}`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
const items: any[] = response.features; const items: any[] = body.features;
return new FeaturesDto( const features = new FeaturesDto(
items.map(item => { items.map(item =>
return new FeatureDto( new FeatureDto(
item.name, item.name,
item.text item.text)
); ),
}), body.version);
response.version
); return features;
}), }),
pretifyError('Failed to load features. Please reload.')); pretifyError('Failed to load features. Please reload.'));
} }

10
src/Squidex/app/shared/services/patterns.service.spec.ts

@ -69,11 +69,13 @@ describe('PatternsService', () => {
} }
}); });
expect(patterns!).toEqual( expect(patterns!).toEqual({
new PatternsDto([ payload: [
new PatternDto('1', 'Number', '[0-9]', 'Message1'), new PatternDto('1', 'Number', '[0-9]', 'Message1'),
new PatternDto('2', 'Numbers', '[0-9]*', 'Message2') new PatternDto('2', 'Numbers', '[0-9]*', 'Message2')
], new Version('2'))); ],
version: new Version('2')
});
})); }));
it('should make post request to add pattern', it('should make post request to add pattern',
@ -93,9 +95,9 @@ describe('PatternsService', () => {
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({ req.flush({
name: 'Number',
patternId: '1', patternId: '1',
pattern: '[0-9]', pattern: '[0-9]',
name: 'Number',
message: 'Message1' message: 'Message1'
}); });

34
src/Squidex/app/shared/services/patterns.service.ts

@ -8,26 +8,20 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
Version, Version,
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export class PatternsDto extends Model<PatternsDto> { export type PatternsDto = Versioned<PatternDto[]>;
constructor(
public readonly patterns: PatternDto[],
public readonly version: Version
) {
super();
}
}
export class PatternDto extends Model<PatternDto> { export class PatternDto extends Model<PatternDto> {
constructor( constructor(
@ -59,20 +53,18 @@ export class PatternsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const items: any[] = body; const items: any[] = body;
return new PatternsDto( const patterns =
items.map(item => { items.map(item =>
return new PatternDto( new PatternDto(
item.patternId, item.patternId,
item.name, item.name,
item.pattern, item.pattern,
item.message); item.message));
}),
response.version); return patterns;
}), }),
pretifyError('Failed to add pattern. Please reload.')); pretifyError('Failed to add pattern. Please reload.'));
} }
@ -81,16 +73,14 @@ export class PatternsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`);
return HTTP.postVersioned<any>(this.http, url, dto, version).pipe( return HTTP.postVersioned<any>(this.http, url, dto, version).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const pattern = new PatternDto( const pattern = new PatternDto(
body.patternId, body.patternId,
body.name, body.name,
body.pattern, body.pattern,
body.message); body.message);
return new Versioned(response.version, pattern); return pattern;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Patterns', 'Created', appName); this.analytics.trackEvent('Patterns', 'Created', appName);

21
src/Squidex/app/shared/services/plans.service.spec.ts

@ -54,7 +54,6 @@ describe('PlansService', () => {
req.flush({ req.flush({
currentPlanId: '123', currentPlanId: '123',
hasPortal: true,
planOwner: '456', planOwner: '456',
plans: [ plans: [
{ {
@ -77,24 +76,26 @@ describe('PlansService', () => {
maxAssetSize: 5500, maxAssetSize: 5500,
maxContributors: 6500 maxContributors: 6500
} }
] ],
hasPortal: true
}, { }, {
headers: { headers: {
etag: '2' etag: '2'
} }
}); });
expect(plans!).toEqual( expect(plans!).toEqual({
new PlansDto( payload: {
'123', currentPlanId: '123',
'456', planOwner: '456',
true, plans: [
[
new PlanDto('free', 'Free', '14 €', 'free_yearly', '12 €', 1000, 1500, 2500), new PlanDto('free', 'Free', '14 €', 'free_yearly', '12 €', 1000, 1500, 2500),
new PlanDto('prof', 'Prof', '18 €', 'prof_yearly', '16 €', 4000, 5500, 6500) new PlanDto('prof', 'Prof', '18 €', 'prof_yearly', '16 €', 4000, 5500, 6500)
], ],
new Version('2') hasPortal: true
)); },
version: new Version('2')
});
})); }));
it('should make put request to change plan', it('should make put request to change plan',

51
src/Squidex/app/shared/services/plans.service.ts

@ -8,29 +8,25 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
Version, Version,
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export class PlansDto extends Model<PlansDto> { export type PlansDto = Versioned<{
constructor( readonly currentPlanId: string,
public readonly currentPlanId: string, readonly planOwner: string,
public readonly planOwner: string, readonly hasPortal: boolean,
public readonly hasPortal: boolean, readonly plans: PlanDto[]
public readonly plans: PlanDto[], }>;
public readonly version: Version
) {
super();
}
}
export class PlanDto extends Model<PlanDto> { export class PlanDto extends Model<PlanDto> {
constructor( constructor(
@ -68,17 +64,16 @@ export class PlansService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/plans`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/plans`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const items: any[] = body.plans; const items: any[] = body.plans;
return new PlansDto( const { hasPortal, currentPlanId, planOwner } = body;
body.currentPlanId,
body.planOwner, const plans = {
body.hasPortal, currentPlanId,
items.map(item => { planOwner,
return new PlanDto( plans: items.map(item =>
new PlanDto(
item.id, item.id,
item.name, item.name,
item.costs, item.costs,
@ -86,9 +81,11 @@ export class PlansService {
item.yearlyCosts, item.yearlyCosts,
item.maxApiCalls, item.maxApiCalls,
item.maxAssetSize, item.maxAssetSize,
item.maxContributors); item.maxContributors)),
}), hasPortal
response.version); };
return plans;
}), }),
pretifyError('Failed to load plans. Please reload.')); pretifyError('Failed to load plans. Please reload.'));
} }
@ -97,10 +94,8 @@ export class PlansService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`);
return HTTP.putVersioned<any>(this.http, url, dto, version).pipe( return HTTP.putVersioned<any>(this.http, url, dto, version).pipe(
map(response => { mapVersioned(payload => {
const body = response.payload.body; return <PlanChangedDto>payload.body;
return new Versioned(response.version, body);
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Plan', 'Changed', appName); this.analytics.trackEvent('Plan', 'Changed', appName);

7
src/Squidex/app/shared/services/roles.service.spec.ts

@ -88,12 +88,13 @@ describe('RolesService', () => {
} }
}); });
expect(roles!).toEqual( expect(roles!).toEqual({
new RolesDto([ payload: [
new RoleDto('Role1', 3, 5, ['P1']), new RoleDto('Role1', 3, 5, ['P1']),
new RoleDto('Role2', 7, 9, ['P2']) new RoleDto('Role2', 7, 9, ['P2'])
], ],
new Version('2'))); version: new Version('2')
});
})); }));
it('should make post request to add role', it('should make post request to add role',

29
src/Squidex/app/shared/services/roles.service.ts

@ -8,26 +8,20 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
Version, Version,
Versioned Versioned
} from '@app/framework'; } from '@app/framework';
export class RolesDto extends Model<RolesDto> { export type RolesDto = Versioned<RoleDto[]>;
constructor(
public readonly roles: RoleDto[],
public readonly version: Version
) {
super();
}
}
export class RoleDto extends Model<RoleDto> { export class RoleDto extends Model<RoleDto> {
constructor( constructor(
@ -61,20 +55,17 @@ export class RolesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
const items: any[] = body.roles; const items: any[] = body.roles;
const roles = items.map(item => { const roles = items.map(item =>
return new RoleDto( new RoleDto(
item.name, item.name,
item.numClients, item.numClients,
item.numContributors, item.numContributors,
item.permissions); item.permissions));
});
return new RolesDto(roles, response.version); return roles;
}), }),
pretifyError('Failed to load roles. Please reload.')); pretifyError('Failed to load roles. Please reload.'));
} }
@ -83,10 +74,10 @@ export class RolesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`);
return HTTP.postVersioned<any>(this.http, url, dto, version).pipe( return HTTP.postVersioned<any>(this.http, url, dto, version).pipe(
map(response => { mapVersioned(() => {
const role = new RoleDto(dto.name, 0, 0, []); const role = new RoleDto(dto.name, 0, 0, []);
return new Versioned(response.version, role); return role;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Role', 'Created', appName); this.analytics.trackEvent('Role', 'Created', appName);

42
src/Squidex/app/shared/services/rules.service.ts

@ -132,10 +132,10 @@ export class RulesService {
const url = this.apiUrl.buildUrl('api/rules/actions'); const url = this.apiUrl.buildUrl('api/rules/actions');
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ payload }) => {
const items: { [name: string]: any } = response.payload.body; const items: { [name: string]: any } = payload.body;
const result: { [name: string]: RuleElementDto } = {}; const actions: { [name: string]: RuleElementDto } = {};
for (let key of Object.keys(items).sort()) { for (let key of Object.keys(items).sort()) {
const value = items[key]; const value = items[key];
@ -150,7 +150,7 @@ export class RulesService {
property.isRequired property.isRequired
)); ));
result[key] = new RuleElementDto( actions[key] = new RuleElementDto(
value.display, value.display,
value.description, value.description,
value.iconColor, value.iconColor,
@ -159,7 +159,7 @@ export class RulesService {
properties); properties);
} }
return result; return actions;
}), }),
pretifyError('Failed to load Rules. Please reload.')); pretifyError('Failed to load Rules. Please reload.'));
} }
@ -168,11 +168,11 @@ export class RulesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ payload }) => {
const items: any[] = response.payload.body; const items: any[] = payload.body;
return items.map(item => { const rules = items.map(item =>
return new RuleDto( new RuleDto(
item.id, item.id,
item.createdBy, item.createdBy,
item.lastModifiedBy, item.lastModifiedBy,
@ -183,8 +183,9 @@ export class RulesService {
item.trigger, item.trigger,
item.trigger.triggerType, item.trigger.triggerType,
item.action, item.action,
item.action.actionType); item.action.actionType));
});
return rules;
}), }),
pretifyError('Failed to load Rules. Please reload.')); pretifyError('Failed to load Rules. Please reload.'));
} }
@ -193,8 +194,8 @@ export class RulesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.postVersioned<any>(this.http, url, dto).pipe( return HTTP.postVersioned<any>(this.http, url, dto).pipe(
map(response => { map(({ version, payload }) => {
const body = response.payload.body; const body = payload.body;
return new RuleDto( return new RuleDto(
body.id, body.id,
@ -202,7 +203,7 @@ export class RulesService {
user, user,
now, now,
now, now,
response.version, version,
true, true,
dto.trigger, dto.trigger,
dto.trigger.triggerType, dto.trigger.triggerType,
@ -259,13 +260,13 @@ export class RulesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ payload }) => {
const body = response.payload.body; const body = payload.body;
const items: any[] = body.items; const items: any[] = body.items;
return new RuleEventsDto(body.total, items.map(item => { const ruleEvents = new RuleEventsDto(body.total, items.map(item =>
return new RuleEventDto( new RuleEventDto(
item.id, item.id,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null,
@ -274,8 +275,9 @@ export class RulesService {
item.lastDump, item.lastDump,
item.result, item.result,
item.jobResult, item.jobResult,
item.numCalls); item.numCalls)));
}));
return ruleEvents;
}), }),
pretifyError('Failed to load events. Please reload.')); pretifyError('Failed to load events. Please reload.'));
} }

36
src/Squidex/app/shared/services/schemas.service.ts

@ -15,6 +15,7 @@ import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
HTTP, HTTP,
mapVersioned,
Model, Model,
pretifyError, pretifyError,
StringHelper, StringHelper,
@ -215,12 +216,12 @@ export class SchemasService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ payload }) => {
const body = response.payload.body; const body = payload.body;
const items: any[] = body; const items: any[] = body;
return items.map(item => { const schemas = items.map(item => {
const properties = new SchemaPropertiesDto(item.properties.label, item.properties.hints); const properties = new SchemaPropertiesDto(item.properties.label, item.properties.hints);
return new SchemaDto( return new SchemaDto(
@ -233,6 +234,8 @@ export class SchemasService {
DateTime.parseISO_UTC(item.lastModified), item.lastModifiedBy, DateTime.parseISO_UTC(item.lastModified), item.lastModifiedBy,
new Version(item.version.toString())); new Version(item.version.toString()));
}); });
return schemas;
}), }),
pretifyError('Failed to load schemas. Please reload.')); pretifyError('Failed to load schemas. Please reload.'));
} }
@ -241,8 +244,8 @@ export class SchemasService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`);
return HTTP.getVersioned<any>(this.http, url).pipe( return HTTP.getVersioned<any>(this.http, url).pipe(
map(response => { map(({ version, payload }) => {
const body = response.payload.body; const body = payload.body;
const fields = body.fields.map((item: any) => { const fields = body.fields.map((item: any) => {
const propertiesDto = const propertiesDto =
@ -292,7 +295,7 @@ export class SchemasService {
body.isPublished, body.isPublished,
DateTime.parseISO_UTC(body.created), body.createdBy, DateTime.parseISO_UTC(body.created), body.createdBy,
DateTime.parseISO_UTC(body.lastModified), body.lastModifiedBy, DateTime.parseISO_UTC(body.lastModified), body.lastModifiedBy,
response.version, version,
fields, fields,
body.scripts || {}, body.scripts || {},
body.previewUrls || {}); body.previewUrls || {});
@ -304,22 +307,23 @@ export class SchemasService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return HTTP.postVersioned<any>(this.http, url, dto).pipe( return HTTP.postVersioned<any>(this.http, url, dto).pipe(
map(response => { map(({ version, payload }) => {
const body = response.payload.body; const body = payload.body;
now = now || DateTime.now(); now = now || DateTime.now();
return new SchemaDetailsDto( const schema = new SchemaDetailsDto(
body.id, body.id,
dto.name, dto.name, '',
'',
dto.properties || new SchemaPropertiesDto(), dto.properties || new SchemaPropertiesDto(),
dto.isSingleton === true, dto.isSingleton === true,
false, false,
now, user, now, user,
now, user, now, user,
response.version, version,
dto.fields || []); dto.fields || []);
return schema;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Schema', 'Created', appName); this.analytics.trackEvent('Schema', 'Created', appName);
@ -401,17 +405,15 @@ export class SchemasService {
const url = this.buildUrl(appName, schemaName, parentId, ''); const url = this.buildUrl(appName, schemaName, parentId, '');
return HTTP.postVersioned<any>(this.http, url, dto, version).pipe( return HTTP.postVersioned<any>(this.http, url, dto, version).pipe(
map(response => { mapVersioned(({ body }) => {
const body = response.payload.body;
if (parentId) { if (parentId) {
const field = new NestedFieldDto(body.id, dto.name, dto.properties, parentId); const field = new NestedFieldDto(body.id, dto.name, dto.properties, parentId);
return new Versioned(response.version, field); return field;
} else { } else {
const field = new RootFieldDto(body.id, dto.name, dto.properties, dto.partitioning); const field = new RootFieldDto(body.id, dto.name, dto.properties, dto.partitioning);
return new Versioned(response.version, field); return field;
} }
}), }),
tap(() => { tap(() => {

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

@ -38,8 +38,8 @@ export class TranslationsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/translations`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/translations`);
return this.http.post<any>(url, request).pipe( return this.http.post<any>(url, request).pipe(
map(response => { map(body => {
return new TranslationDto(response.result, response.text); return new TranslationDto(body.result, body.text);
}), }),
pretifyError('Failed to translate text. Please reload.')); pretifyError('Failed to translate text. Please reload.'));
} }

39
src/Squidex/app/shared/services/usages.service.ts

@ -62,8 +62,8 @@ export class UsagesService {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/log`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/log`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
return response.downloadUrl; return body.downloadUrl;
}), }),
pretifyError('Failed to load monthly api calls. Please reload.')); pretifyError('Failed to load monthly api calls. Please reload.'));
} }
@ -72,8 +72,8 @@ export class UsagesService {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/month`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/month`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
return new CurrentCallsDto(response.count, response.maxAllowed); return new CurrentCallsDto(body.count, body.maxAllowed);
}), }),
pretifyError('Failed to load monthly api calls. Please reload.')); pretifyError('Failed to load monthly api calls. Please reload.'));
} }
@ -82,8 +82,8 @@ export class UsagesService {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
return new CurrentStorageDto(response.size, response.maxAllowed); return new CurrentStorageDto(body.size, body.maxAllowed);
}), }),
pretifyError('Failed to load todays storage size. Please reload.')); pretifyError('Failed to load todays storage size. Please reload.'));
} }
@ -92,18 +92,18 @@ export class UsagesService {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
const result: { [category: string]: CallsUsageDto[] } = {}; const usages: { [category: string]: CallsUsageDto[] } = {};
for (let category of Object.keys(response)) { for (let category of Object.keys(body)) {
result[category] = response[category].map((item: any) => { usages[category] = body[category].map((item: any) =>
return new CallsUsageDto( new CallsUsageDto(
DateTime.parseISO_UTC(item.date), DateTime.parseISO_UTC(item.date),
item.count, item.count,
item.averageMs); item.averageMs));
});
} }
return result;
return usages;
}), }),
pretifyError('Failed to load calls usage. Please reload.')); pretifyError('Failed to load calls usage. Please reload.'));
} }
@ -112,13 +112,14 @@ export class UsagesService {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`);
return this.http.get<any[]>(url).pipe( return this.http.get<any[]>(url).pipe(
map(response => { map(body => {
return response.map(item => { const usages = body.map(item =>
return new StorageUsageDto( new StorageUsageDto(
DateTime.parseISO_UTC(item.date), DateTime.parseISO_UTC(item.date),
item.count, item.count,
item.size); item.size));
});
return usages;
}), }),
pretifyError('Failed to load storage usage. Please reload.')); pretifyError('Failed to load storage usage. Please reload.'));
} }

21
src/Squidex/app/shared/services/users.service.ts

@ -32,12 +32,13 @@ export class UsersService {
const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`); const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`);
return this.http.get<any[]>(url).pipe( return this.http.get<any[]>(url).pipe(
map(response => { map(body => {
return response.map(item => { const users = body.map(item =>
return new UserDto( new UserDto(
item.id, item.id,
item.displayName); item.displayName));
});
return users;
}), }),
pretifyError('Failed to load users. Please reload.')); pretifyError('Failed to load users. Please reload.'));
} }
@ -46,10 +47,12 @@ export class UsersService {
const url = this.apiUrl.buildUrl(`api/users/${id}`); const url = this.apiUrl.buildUrl(`api/users/${id}`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(response => { map(body => {
return new UserDto( const user = new UserDto(
response.id, body.id,
response.displayName); body.displayName);
return user;
}), }),
pretifyError('Failed to load user. Please reload.')); pretifyError('Failed to load user. Please reload.'));
} }

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

@ -6,7 +6,7 @@
*/ */
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, Mock } from 'typemoq';
import { import {
AppDto, AppDto,
@ -95,7 +95,7 @@ describe('AppsState', () => {
expect(appsState.snapshot.apps.values).toEqual([newApp, ...oldApps]); expect(appsState.snapshot.apps.values).toEqual([newApp, ...oldApps]);
}); });
it('should remove app from snashot when archived', () => { it('should remove app from snapshot when archived', () => {
const request = { ...newApp }; const request = { ...newApp };
appsService.setup(x => x.postApp(request)) appsService.setup(x => x.postApp(request))
@ -115,4 +115,20 @@ describe('AppsState', () => {
expect(appsAfterCreate).toEqual([newApp, ...oldApps]); expect(appsAfterCreate).toEqual([newApp, ...oldApps]);
expect(appsAfterDelete).toEqual(oldApps); expect(appsAfterDelete).toEqual(oldApps);
}); });
it('should selected app from snapshot when archived', () => {
const request = { ...newApp };
appsService.setup(x => x.postApp(request))
.returns(() => of(newApp)).verifiable();
appsService.setup(x => x.deleteApp(newApp.name))
.returns(() => of({})).verifiable();
appsState.create(request).subscribe();
appsState.select(newApp.name).subscribe();
appsState.delete(newApp.name).subscribe();
expect(appsState.snapshot.selectedApp).toBeNull();
});
}); });

109
src/Squidex/app/shared/state/apps.state.ts

@ -7,11 +7,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
shareSubscribed,
State State
} from '@app/framework'; } from '@app/framework';
@ -23,14 +24,12 @@ import {
interface Snapshot { interface Snapshot {
// All apps, loaded once. // All apps, loaded once.
apps: AppsList; apps: ImmutableArray<AppDto>;
// The selected app. // The selected app.
selectedApp: AppDto | null; selectedApp: AppDto | null;
} }
type AppsList = ImmutableArray<AppDto>;
function sameApp(lhs: AppDto, rhs?: AppDto): boolean { function sameApp(lhs: AppDto, rhs?: AppDto): boolean {
return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id); return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id);
} }
@ -57,78 +56,52 @@ export class AppsState extends State<Snapshot> {
} }
public select(name: string | null): Observable<AppDto | null> { public select(name: string | null): Observable<AppDto | null> {
const http$ = const observable =
this.loadApp(name) !name ?
.pipe(share()); of(null) :
of(this.snapshot.apps.find(x => x.name === name) || null);
http$.subscribe(selectedApp => {
this.next(s => ({ ...s, selectedApp })); return observable.pipe(
}); tap(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> { public load(): Observable<any> {
const http$ = return this.appsService.getApps().pipe(
this.appsService.getApps().pipe( tap((dto: AppDto[]) => {
share()); this.next(s => {
const apps = ImmutableArray.of(dto);
http$.subscribe(response => {
this.next(s => { return { ...s, apps };
const apps = ImmutableArray.of(response).sortByStringAsc(x => x.name); });
}),
return { ...s, apps }; shareSubscribed(this.dialogs));
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
public create(request: CreateAppDto): Observable<AppDto> { public create(request: CreateAppDto): Observable<AppDto> {
const http$ = return this.appsService.postApp(request).pipe(
this.appsService.postApp(request).pipe( tap(dto => {
share()); this.next(s => {
const apps = s.apps.push(dto).sortByStringAsc(x => x.name);
http$.subscribe(app => {
this.next(s => { return { ...s, apps };
const apps = s.apps.push(app).sortByStringAsc(x => x.name); });
}),
return { ...s, apps }; shareSubscribed(this.dialogs, { silent: true }));
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
public delete(name: string): Observable<any> { public delete(name: string): Observable<any> {
const http$ = return this.appsService.deleteApp(name).pipe(
this.appsService.deleteApp(name).pipe( tap(() => {
share()); this.next(s => {
const apps = s.apps.filter(x => x.name !== name);
http$.subscribe(() => {
this.next(s => { const selectedApp = s.selectedApp && s.selectedApp.name === name ? null : s.selectedApp;
const apps = s.apps.filter(x => x.name !== name);
return { ...s, apps, selectedApp };
const selectedApp = });
s.selectedApp && }),
s.selectedApp.name === name ? shareSubscribed(this.dialogs));
null :
s.selectedApp;
return { ...s, apps, selectedApp };
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
} }

4
src/Squidex/app/shared/state/assets.state.spec.ts

@ -14,7 +14,7 @@ import {
AssetsService, AssetsService,
AssetsState, AssetsState,
DialogService, DialogService,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -98,7 +98,7 @@ describe('AssetsState', () => {
it('should remove asset from snapshot when deleted', () => { it('should remove asset from snapshot when deleted', () => {
assetsService.setup(x => x.deleteAsset(app, oldAssets[0].id, version)) assetsService.setup(x => x.deleteAsset(app, oldAssets[0].id, version))
.returns(() => of(new Versioned<any>(newVersion, {}))); .returns(() => of(versioned(newVersion)));
assetsState.delete(oldAssets[0]).subscribe(); assetsState.delete(oldAssets[0]).subscribe();

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

@ -12,8 +12,8 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
notify,
Pager, Pager,
shareSubscribed,
State State
} from '@app/framework'; } from '@app/framework';
@ -107,7 +107,7 @@ export class AssetsState extends State<Snapshot> {
return { ...s, assets, assetsPager, isLoaded: true, tags: dtos[1] }; return { ...s, assets, assetsPager, isLoaded: true, tags: dtos[1] };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public add(asset: AssetDto) { public add(asset: AssetDto) {
@ -138,7 +138,7 @@ export class AssetsState extends State<Snapshot> {
return { ...s, assets, assetsPager, tags, tagsSelected }; return { ...s, assets, assetsPager, tags, tagsSelected };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public update(asset: AssetDto) { public update(asset: AssetDto) {

65
src/Squidex/app/shared/state/backups.state.ts

@ -7,11 +7,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
shareSubscribed,
State State
} from '@app/framework'; } from '@app/framework';
@ -56,55 +57,35 @@ export class BackupsState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
const http$ = return this.backupsService.getBackups(this.appName).pipe(
this.backupsService.getBackups(this.appName).pipe( tap(payload => {
share()); if (isReload && !silent) {
this.dialogs.notifyInfo('Backups reloaded.');
}
http$.subscribe(dtos => { this.next(s => {
if (isReload && !silent) { const backups = ImmutableArray.of(payload);
this.dialogs.notifyInfo('Backups reloaded.');
}
this.next(s => { return { ...s, backups, isLoaded: true };
const backups = ImmutableArray.of(dtos); });
}),
return { ...s, backups, isLoaded: true }; shareSubscribed(this.dialogs, { silent }));
});
}, error => {
if (!silent) {
this.dialogs.notifyError(error);
}
});
return http$;
} }
public start(): Observable<any> { public start(): Observable<any> {
const http$ = return this.backupsService.postBackup(this.appsState.appName).pipe(
this.backupsService.postBackup(this.appsState.appName).pipe( tap(() => {
share()); this.dialogs.notifyInfo('Backup started, it can take several minutes to complete.');
}),
http$.subscribe(() => { shareSubscribed(this.dialogs));
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> { public delete(backup: BackupDto): Observable<any> {
const http$ = return this.backupsService.deleteBackup(this.appsState.appName, backup.id).pipe(
this.backupsService.deleteBackup(this.appsState.appName, backup.id).pipe( tap(() => {
share()); this.dialogs.notifyInfo('Backup is about to be deleted.');
}),
http$.subscribe(() => { shareSubscribed(this.dialogs));
this.dialogs.notifyInfo('Backup is about to be deleted.');
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
private get appName() { private get appName() {

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

@ -10,11 +10,10 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
ClientDto, ClientDto,
ClientsDto,
ClientsService, ClientsService,
ClientsState, ClientsState,
DialogService, DialogService,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -50,7 +49,7 @@ describe('ClientsState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load clients', () => { it('should load clients', () => {
clientsService.setup(x => x.getClients(app)) clientsService.setup(x => x.getClients(app))
.returns(() => of(new ClientsDto(oldClients, version))).verifiable(); .returns(() => of(versioned(version, oldClients))).verifiable();
clientsState.load().subscribe(); clientsState.load().subscribe();
@ -63,7 +62,7 @@ describe('ClientsState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
clientsService.setup(x => x.getClients(app)) clientsService.setup(x => x.getClients(app))
.returns(() => of(new ClientsDto(oldClients, version))).verifiable(); .returns(() => of(versioned(version, oldClients))).verifiable();
clientsState.load(true).subscribe(); clientsState.load(true).subscribe();
@ -76,7 +75,7 @@ describe('ClientsState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
clientsService.setup(x => x.getClients(app)) clientsService.setup(x => x.getClients(app))
.returns(() => of(new ClientsDto(oldClients, version))).verifiable(); .returns(() => of(versioned(version, oldClients))).verifiable();
clientsState.load().subscribe(); clientsState.load().subscribe();
}); });
@ -87,7 +86,7 @@ describe('ClientsState', () => {
const request = { id: 'id3' }; const request = { id: 'id3' };
clientsService.setup(x => x.postClient(app, request, version)) clientsService.setup(x => x.postClient(app, request, version))
.returns(() => of(new Versioned(newVersion, newClient))).verifiable(); .returns(() => of(versioned(newVersion, newClient))).verifiable();
clientsState.attach(request).subscribe(); clientsState.attach(request).subscribe();
@ -99,7 +98,7 @@ describe('ClientsState', () => {
const request = { name: 'NewName', role: 'NewRole' }; const request = { name: 'NewName', role: 'NewRole' };
clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version)) clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
clientsState.update(oldClients[0], request).subscribe(); clientsState.update(oldClients[0], request).subscribe();
@ -112,7 +111,7 @@ describe('ClientsState', () => {
it('should remove client from snapshot when revoked', () => { it('should remove client from snapshot when revoked', () => {
clientsService.setup(x => x.deleteClient(app, oldClients[0].id, version)) clientsService.setup(x => x.deleteClient(app, oldClients[0].id, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
clientsState.revoke(oldClients[0]).subscribe(); clientsState.revoke(oldClients[0]).subscribe();

97
src/Squidex/app/shared/state/clients.state.ts

@ -9,11 +9,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
mapVersioned,
shareSubscribed,
State, State,
Version Version
} from '@app/framework'; } from '@app/framework';
@ -63,71 +65,56 @@ export class ClientsState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
const http$ = return this.clientsService.getClients(this.appName).pipe(
this.clientsService.getClients(this.appName).pipe( tap(({ version, payload: newClients }) => {
share()); if (isReload) {
this.dialogs.notifyInfo('Clients reloaded.');
}
http$.subscribe(response => { const clients = ImmutableArray.of(newClients);
if (isReload) {
this.dialogs.notifyInfo('Clients reloaded.');
}
const clients = ImmutableArray.of(response.clients); this.next(s => {
return { ...s, clients, isLoaded: true, version };
this.next(s => { });
return { ...s, clients, isLoaded: true, version: response.version }; }),
}); shareSubscribed(this.dialogs));
});
return http$;
} }
public attach(request: CreateClientDto): Observable<ClientDto> { public attach(request: CreateClientDto): Observable<ClientDto> {
const http$ = return this.clientsService.postClient(this.appName, request, this.version).pipe(
this.clientsService.postClient(this.appName, request, this.version).pipe( tap(({ version, payload }) => {
share()); this.next(s => {
const clients = s.clients.push(payload);
http$.subscribe(({ version, payload }) => {
this.next(s => { return { ...s, clients, version: version };
const clients = s.clients.push(payload); });
}),
return { ...s, clients, version: version }; shareSubscribed(this.dialogs, { project: x => x.payload }));
});
});
return http$.pipe(map(x => x.payload));
} }
public revoke(client: ClientDto): Observable<any> { public revoke(client: ClientDto): Observable<any> {
const http$ = return this.clientsService.deleteClient(this.appName, client.id, this.version).pipe(
this.clientsService.deleteClient(this.appName, client.id, this.version).pipe( tap(({ version }) => {
share()); this.next(s => {
const clients = s.clients.filter(c => c.id !== client.id);
http$.subscribe(({ version }) => {
this.next(s => { return { ...s, clients, version };
const clients = s.clients.filter(c => c.id !== client.id); });
}),
return { ...s, clients, version }; shareSubscribed(this.dialogs));
});
});
return http$;
} }
public update(client: ClientDto, request: UpdateClientDto): Observable<ClientDto> { public update(client: ClientDto, request: UpdateClientDto): Observable<ClientDto> {
const http$ = return this.clientsService.putClient(this.appName, client.id, request, this.version).pipe(
this.clientsService.putClient(this.appName, client.id, request, this.version).pipe( mapVersioned(() => update(client, request)),
map(({ version }) => ({ version, client: update(client, request) })), share()); tap(({ version, payload }) => {
this.next(s => {
http$.subscribe(({ version, client }) => { const clients = s.clients.replaceBy('id', payload);
this.next(s => {
const clients = s.clients.replaceBy('id', client); return { ...s, clients, version };
});
return { ...s, clients, version }; }),
}); shareSubscribed(this.dialogs, { project: x => x.payload }));
});
return http$.pipe(map(x => x.client));
} }
private get appName() { private get appName() {

124
src/Squidex/app/shared/state/comments.state.ts

@ -6,12 +6,13 @@
*/ */
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DateTime, DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
shareSubscribed,
State, State,
Version Version
} from '@app/framework'; } from '@app/framework';
@ -51,89 +52,66 @@ export class CommentsState extends State<Snapshot> {
} }
public load(): Observable<any> { public load(): Observable<any> {
const http$ = return this.commentsService.getComments(this.appName, this.commentsId, this.version).pipe(
this.commentsService.getComments(this.appName, this.commentsId, this.version).pipe( tap(payload => {
share()); this.next(s => {
let comments = s.comments;
http$.subscribe(response => {
this.next(s => { for (let created of payload.createdComments) {
let comments = s.comments; if (!comments.find(x => x.id === created.id)) {
comments = comments.push(created);
for (let created of response.createdComments) { }
if (!comments.find(x => x.id === created.id)) {
comments = comments.push(created);
} }
}
for (let updated of response.updatedComments) {
comments = comments.replaceBy('id', updated);
}
for (let deleted of response.deletedComments) { for (let updated of payload.updatedComments) {
comments = comments.filter(x => x.id !== deleted); comments = comments.replaceBy('id', updated);
} }
return { ...s, comments, isLoaded: true, version: response.version }; for (let deleted of payload.deletedComments) {
}); comments = comments.filter(x => x.id !== deleted);
}, error => { }
this.dialogs.notifyError(error);
});
return http$; return { ...s, comments, isLoaded: true, version: payload.version };
});
}),
shareSubscribed(this.dialogs));
} }
public create(text: string): Observable<CommentDto> { public create(text: string): Observable<CommentDto> {
const http$ = return this.commentsService.postComment(this.appName, this.commentsId, { text }).pipe(
this.commentsService.postComment(this.appName, this.commentsId, { text }).pipe( tap(comment => {
share()); this.next(s => {
const comments = s.comments.push(comment);
http$.subscribe(comment => {
this.next(s => { return { ...s, comments };
const comments = s.comments.push(comment); });
}),
return { ...s, comments }; shareSubscribed(this.dialogs));
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
}
public update(comment: CommentDto, text: string, now?: DateTime): Observable<CommentDto> {
const http$ =
this.commentsService.putComment(this.appName, this.commentsId, comment.id, { text }).pipe(
map(() => update(comment, text, now || DateTime.now())), share());
http$.subscribe(updated => {
this.next(s => {
const comments = s.comments.replaceBy('id', updated);
return { ...s, comments };
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
public delete(comment: CommentDto): Observable<any> { public delete(comment: CommentDto): Observable<any> {
const http$ = return this.commentsService.deleteComment(this.appName, this.commentsId, comment.id).pipe(
this.commentsService.deleteComment(this.appName, this.commentsId, comment.id).pipe( tap(() => {
share()); this.next(s => {
const comments = s.comments.removeBy('id', comment);
http$.subscribe(() => {
this.next(s => { return { ...s, comments };
const comments = s.comments.removeBy('id', comment); });
}),
return { ...s, comments }; shareSubscribed(this.dialogs));
}); }
}, error => {
this.dialogs.notifyError(error);
});
return http$; public update(comment: CommentDto, text: string, now?: DateTime): Observable<CommentDto> {
return this.commentsService.putComment(this.appName, this.commentsId, comment.id, { text }).pipe(
map(() => update(comment, text, now || DateTime.now())),
tap(updated => {
this.next(s => {
const comments = s.comments.replaceBy('id', updated);
return { ...s, comments };
});
}),
shareSubscribed(this.dialogs));
} }
private get version() { private get version() {

19
src/Squidex/app/shared/state/contents.state.ts

@ -16,6 +16,7 @@ import {
ImmutableArray, ImmutableArray,
notify, notify,
Pager, Pager,
shareSubscribed,
State, State,
Version, Version,
Versioned Versioned
@ -142,10 +143,10 @@ export abstract class ContentsStateBase extends State<Snapshot> {
return { ...s, contents, contentsPager, selectedContent, isLoaded: true }; return { ...s, contents, contentsPager, selectedContent, isLoaded: true };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public create(request: any, publish: boolean) { public create(request: any, publish: boolean): Observable<ContentDto> {
return this.contentsService.postContent(this.appName, this.schemaName, request, publish).pipe( return this.contentsService.postContent(this.appName, this.schemaName, request, publish).pipe(
tap(dto => { tap(dto => {
this.dialogs.notifyInfo('Contents created successfully.'); this.dialogs.notifyInfo('Contents created successfully.');
@ -157,7 +158,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
return { ...s, contents, contentsPager }; return { ...s, contents, contentsPager };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public changeManyStatus(contents: ContentDto[], action: string, dueTime: string | null): Observable<any> { public changeManyStatus(contents: ContentDto[], action: string, dueTime: string | null): Observable<any> {
@ -205,7 +206,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.replaceContent(confirmChanges(content, this.user, dto.version, now)); this.replaceContent(confirmChanges(content, this.user, dto.version, now));
} }
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public changeStatus(content: ContentDto, action: string, status: string, dueTime: string | null, now?: DateTime): Observable<any> { public changeStatus(content: ContentDto, action: string, status: string, dueTime: string | null, now?: DateTime): Observable<any> {
@ -219,7 +220,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.replaceContent(changeStatus(content, status, this.user, dto.version, now)); this.replaceContent(changeStatus(content, status, this.user, dto.version, now));
} }
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public update(content: ContentDto, request: any, now?: DateTime): Observable<any> { public update(content: ContentDto, request: any, now?: DateTime): Observable<any> {
@ -231,7 +232,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.replaceContent(updateData(content, dto.payload, this.user, dto.version, now)); this.replaceContent(updateData(content, dto.payload, this.user, dto.version, now));
} }
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public proposeUpdate(content: ContentDto, request: any, now?: DateTime): Observable<any> { public proposeUpdate(content: ContentDto, request: any, now?: DateTime): Observable<any> {
@ -243,7 +244,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.replaceContent(updateDataDraft(content, dto.payload, this.user, dto.version, now)); this.replaceContent(updateDataDraft(content, dto.payload, this.user, dto.version, now));
} }
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public discardChanges(content: ContentDto, now?: DateTime): Observable<any> { public discardChanges(content: ContentDto, now?: DateTime): Observable<any> {
@ -255,7 +256,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.replaceContent(discardChanges(content, this.user, dto.version, now)); this.replaceContent(discardChanges(content, this.user, dto.version, now));
} }
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public patch(content: ContentDto, request: any, now?: DateTime): Observable<any> { public patch(content: ContentDto, request: any, now?: DateTime): Observable<any> {
@ -267,7 +268,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.replaceContent(updateData(content, dto.payload, this.user, dto.version, now)); this.replaceContent(updateData(content, dto.payload, this.user, dto.version, now));
} }
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
private replaceContent(content: ContentDto) { private replaceContent(content: ContentDto) {

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

@ -10,11 +10,10 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
ContributorDto, ContributorDto,
ContributorsDto,
ContributorsService, ContributorsService,
ContributorsState, ContributorsState,
DialogService, DialogService,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -52,7 +51,7 @@ describe('ContributorsState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load contributors', () => { it('should load contributors', () => {
contributorsService.setup(x => x.getContributors(app)) contributorsService.setup(x => x.getContributors(app))
.returns(() => of(new ContributorsDto(oldContributors, 3, version))).verifiable(); .returns(() => of(versioned(version, { contributors: oldContributors, maxContributors: 3 }))).verifiable();
contributorsState.load().subscribe(); contributorsState.load().subscribe();
@ -70,7 +69,7 @@ describe('ContributorsState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
contributorsService.setup(x => x.getContributors(app)) contributorsService.setup(x => x.getContributors(app))
.returns(() => of(new ContributorsDto(oldContributors, 3, version))).verifiable(); .returns(() => of(versioned(version, { contributors: oldContributors, maxContributors: 3 }))).verifiable();
contributorsState.load(true).subscribe(); contributorsState.load(true).subscribe();
@ -83,7 +82,7 @@ describe('ContributorsState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
contributorsService.setup(x => x.getContributors(app)) contributorsService.setup(x => x.getContributors(app))
.returns(() => of(new ContributorsDto(oldContributors, 3, version))).verifiable(); .returns(() => of(versioned(version, { contributors: oldContributors, maxContributors: 3 }))).verifiable();
contributorsState.load().subscribe(); contributorsState.load().subscribe();
}); });
@ -95,7 +94,7 @@ describe('ContributorsState', () => {
const response = { contributorId: newContributor.contributorId, isCreated: true }; const response = { contributorId: newContributor.contributorId, isCreated: true };
contributorsService.setup(x => x.postContributor(app, request, version)) contributorsService.setup(x => x.postContributor(app, request, version))
.returns(() => of(new Versioned(newVersion, response))).verifiable(); .returns(() => of(versioned(newVersion, response))).verifiable();
contributorsState.assign(request).subscribe(); contributorsState.assign(request).subscribe();
@ -116,7 +115,7 @@ describe('ContributorsState', () => {
const response = { contributorId: newContributor.contributorId, isCreated: true }; const response = { contributorId: newContributor.contributorId, isCreated: true };
contributorsService.setup(x => x.postContributor(app, request, version)) contributorsService.setup(x => x.postContributor(app, request, version))
.returns(() => of(new Versioned(newVersion, response))).verifiable(); .returns(() => of(versioned(newVersion, response))).verifiable();
contributorsState.assign(request).subscribe(); contributorsState.assign(request).subscribe();
@ -131,7 +130,7 @@ describe('ContributorsState', () => {
it('should remove contributor from snapshot when revoked', () => { it('should remove contributor from snapshot when revoked', () => {
contributorsService.setup(x => x.deleteContributor(app, oldContributors[0].contributorId, version)) contributorsService.setup(x => x.deleteContributor(app, oldContributors[0].contributorId, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
contributorsState.revoke(oldContributors[0]).subscribe(); contributorsState.revoke(oldContributors[0]).subscribe();

79
src/Squidex/app/shared/state/contributors.state.ts

@ -7,12 +7,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError, distinctUntilChanged, map, share } from 'rxjs/operators'; import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ErrorDto, ErrorDto,
ImmutableArray, ImmutableArray,
shareSubscribed,
State, State,
Types, Types,
Version Version
@ -86,62 +87,44 @@ export class ContributorsState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
const http$ = return this.contributorsService.getContributors(this.appName).pipe(
this.contributorsService.getContributors(this.appName).pipe( tap(({ version, payload }) => {
share()); if (isReload) {
this.dialogs.notifyInfo('Contributors reloaded.');
}
http$.subscribe(response => { const contributors = ImmutableArray.of(payload.contributors.map(x => this.createContributor(x)));
if (isReload) {
this.dialogs.notifyInfo('Contributors reloaded.');
}
const contributors = ImmutableArray.of(response.contributors.map(x => this.createContributor(x))); this.replaceContributors(contributors, version, payload.maxContributors);
}),
this.replaceContributors(contributors, response.version, response.maxContributors); shareSubscribed(this.dialogs));
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
public revoke(contributor: ContributorDto): Observable<any> { public revoke(contributor: ContributorDto): Observable<any> {
const http$ = return this.contributorsService.deleteContributor(this.appName, contributor.contributorId, this.version).pipe(
this.contributorsService.deleteContributor(this.appName, contributor.contributorId, this.version).pipe( tap(({ version }) => {
share()); const contributors = this.snapshot.contributors.filter(x => x.contributor.contributorId !== contributor.contributorId);
http$.subscribe(({ version }) => {
const contributors = this.snapshot.contributors.filter(x => x.contributor.contributorId !== contributor.contributorId);
this.replaceContributors(contributors, version); this.replaceContributors(contributors, version);
}, error => { }),
this.dialogs.notifyError(error); shareSubscribed(this.dialogs));
});
return http$;
} }
public assign(request: AssignContributorDto): Observable<boolean | undefined> { public assign(request: AssignContributorDto): Observable<boolean | undefined> {
const http$ = return this.contributorsService.postContributor(this.appName, request, this.version).pipe(
this.contributorsService.postContributor(this.appName, request, this.version).pipe( catchError(error => {
catchError(error => { if (Types.is(error, ErrorDto) && error.statusCode === 404) {
if (Types.is(error, ErrorDto) && error.statusCode === 404) { return throwError(new ErrorDto(404, 'The user does not exist.'));
return throwError(new ErrorDto(404, 'The user does not exist.')); } else {
} else { return throwError(error);
return throwError(error); }
} }),
}), tap(({ version, payload }) => {
share()); const contributors = this.updateContributors(payload.contributorId, request.role);
http$.subscribe(({ payload, version }) => { this.replaceContributors(contributors, version);
const contributors = this.updateContributors(payload.contributorId, request.role); }),
shareSubscribed(this.dialogs, { project: x => x.payload.isCreated }));
this.replaceContributors(contributors, version);
}, error => {
this.dialogs.notifyError(error);
});
return http$.pipe(map(x => x.payload.isCreated));
} }
private updateContributors(id: string, role: string) { private updateContributors(id: string, role: string) {

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

@ -10,14 +10,13 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
AppLanguageDto, AppLanguageDto,
AppLanguagesDto,
AppLanguagesService, AppLanguagesService,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
LanguageDto, LanguageDto,
LanguagesService, LanguagesService,
LanguagesState, LanguagesState,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -56,7 +55,7 @@ describe('LanguagesState', () => {
languagesService = Mock.ofType<AppLanguagesService>(); languagesService = Mock.ofType<AppLanguagesService>();
languagesService.setup(x => x.getLanguages(app)) languagesService.setup(x => x.getLanguages(app))
.returns(() => of(new AppLanguagesDto(oldLanguages, version))).verifiable(); .returns(() => of({ payload: oldLanguages, version })).verifiable();
languagesState = new LanguagesState(languagesService.object, appsState.object, dialogs.object, allLanguagesService.object); languagesState = new LanguagesState(languagesService.object, appsState.object, dialogs.object, allLanguagesService.object);
}); });
@ -107,7 +106,7 @@ describe('LanguagesState', () => {
const newLanguage = new AppLanguageDto(languageIT.iso2Code, languageIT.englishName, false, false, []); const newLanguage = new AppLanguageDto(languageIT.iso2Code, languageIT.englishName, false, false, []);
languagesService.setup(x => x.postLanguage(app, It.isAny(), version)) languagesService.setup(x => x.postLanguage(app, It.isAny(), version))
.returns(() => of(new Versioned(newVersion, newLanguage))).verifiable(); .returns(() => of(versioned(newVersion, newLanguage))).verifiable();
languagesState.add(languageIT).subscribe(); languagesState.add(languageIT).subscribe();
@ -134,7 +133,7 @@ describe('LanguagesState', () => {
const request = { isMaster: true, isOptional: false, fallback: [] }; const request = { isMaster: true, isOptional: false, fallback: [] };
languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version)) languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
languagesState.update(oldLanguages[1], request).subscribe(); languagesState.update(oldLanguages[1], request).subscribe();
@ -158,7 +157,7 @@ describe('LanguagesState', () => {
it('should remove language from snapshot when deleted', () => { it('should remove language from snapshot when deleted', () => {
languagesService.setup(x => x.deleteLanguage(app, oldLanguages[1].iso2Code, version)) languagesService.setup(x => x.deleteLanguage(app, oldLanguages[1].iso2Code, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
languagesState.remove(oldLanguages[1]).subscribe(); languagesState.remove(oldLanguages[1]).subscribe();

63
src/Squidex/app/shared/state/languages.state.ts

@ -7,12 +7,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { forkJoin, Observable } from 'rxjs'; import { forkJoin, Observable } from 'rxjs';
import { distinctUntilChanged, map, share, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
notify, mapVersioned,
shareSubscribed,
State, State,
Version Version
} from '@app/framework'; } from '@app/framework';
@ -95,57 +96,51 @@ export class LanguagesState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
const http$ = return forkJoin(
forkJoin(
this.languagesService.getLanguages(), this.languagesService.getLanguages(),
this.appLanguagesService.getLanguages(this.appName)).pipe( this.appLanguagesService.getLanguages(this.appName)).pipe(
map(args => { map(args => {
return { allLanguages: args[0], languages: args[1] }; return { allLanguages: args[0], languages: args[1] };
}), }),
share()); tap(({ allLanguages, languages }) => {
if (isReload) {
http$.subscribe(response => { this.dialogs.notifyInfo('Languages reloaded.');
if (isReload) { }
this.dialogs.notifyInfo('Languages reloaded.');
}
const sorted = ImmutableArray.of(response.allLanguages).sortByStringAsc(x => x.englishName);
this.replaceLanguages(ImmutableArray.of(response.languages.languages), response.languages.version, sorted);
}, error => { const sorted = ImmutableArray.of(allLanguages).sortByStringAsc(x => x.englishName);
this.dialogs.notifyError(error);
});
return http$; this.replaceLanguages(ImmutableArray.of(languages.payload), languages.version, sorted);
}),
shareSubscribed(this.dialogs));
} }
public add(language: LanguageDto): Observable<any> { public add(language: LanguageDto): Observable<AppLanguageDto> {
return this.appLanguagesService.postLanguage(this.appName, { language: language.iso2Code }, this.version).pipe( return this.appLanguagesService.postLanguage(this.appName, { language: language.iso2Code }, this.version).pipe(
tap(dto => { tap(({ version, payload }) => {
const languages = this.snapshot.plainLanguages.push(dto.payload).sortByStringAsc(x => x.englishName); const languages = this.snapshot.plainLanguages.push(payload).sortByStringAsc(x => x.englishName);
this.replaceLanguages(languages, dto.version); this.replaceLanguages(languages, version);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public remove(language: AppLanguageDto): Observable<any> { public remove(language: AppLanguageDto): Observable<any> {
return this.appLanguagesService.deleteLanguage(this.appName, language.iso2Code, this.version).pipe( return this.appLanguagesService.deleteLanguage(this.appName, language.iso2Code, this.version).pipe(
tap(dto => { tap(({ version }) => {
const languages = this.snapshot.plainLanguages.filter(x => x.iso2Code !== language.iso2Code); const languages = this.snapshot.plainLanguages.filter(x => x.iso2Code !== language.iso2Code);
this.replaceLanguages(languages, dto.version); this.replaceLanguages(languages, version);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public update(language: AppLanguageDto, request: UpdateAppLanguageDto): Observable<any> { public update(language: AppLanguageDto, request: UpdateAppLanguageDto): Observable<AppLanguageDto> {
return this.appLanguagesService.putLanguage(this.appName, language.iso2Code, request, this.version).pipe( return this.appLanguagesService.putLanguage(this.appName, language.iso2Code, request, this.version).pipe(
tap(dto => { mapVersioned(() => update(language, request)),
tap(({ version, payload }) => {
const languages = this.snapshot.plainLanguages.map(x => { const languages = this.snapshot.plainLanguages.map(x => {
if (x.iso2Code === language.iso2Code) { if (x.iso2Code === language.iso2Code) {
return update(x, request); return payload;
} else if (x.isMaster && request.isMaster) { } else if (x.isMaster && request.isMaster) {
return update(x, { isMaster: false }); return update(x, { isMaster: false });
} else { } else {
@ -153,9 +148,9 @@ export class LanguagesState extends State<Snapshot> {
} }
}); });
this.replaceLanguages(languages, dto.version); this.replaceLanguages(languages, version);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
private replaceLanguages(languages: AppLanguagesList, version: Version, allLanguages?: LanguageList) { private replaceLanguages(languages: AppLanguagesList, version: Version, allLanguages?: LanguageList) {

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

@ -11,10 +11,9 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DialogService, DialogService,
PatternDto, PatternDto,
PatternsDto,
PatternsService, PatternsService,
PatternsState, PatternsState,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -50,7 +49,7 @@ describe('PatternsState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load patterns', () => { it('should load patterns', () => {
patternsService.setup(x => x.getPatterns(app)) patternsService.setup(x => x.getPatterns(app))
.returns(() => of(new PatternsDto(oldPatterns, version))).verifiable(); .returns(() => of({ payload: oldPatterns, version })).verifiable();
patternsState.load().subscribe(); patternsState.load().subscribe();
@ -62,7 +61,7 @@ describe('PatternsState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
patternsService.setup(x => x.getPatterns(app)) patternsService.setup(x => x.getPatterns(app))
.returns(() => of(new PatternsDto(oldPatterns, version))).verifiable(); .returns(() => of({ payload: oldPatterns, version })).verifiable();
patternsState.load(true).subscribe(); patternsState.load(true).subscribe();
@ -75,7 +74,7 @@ describe('PatternsState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
patternsService.setup(x => x.getPatterns(app)) patternsService.setup(x => x.getPatterns(app))
.returns(() => of(new PatternsDto(oldPatterns, version))).verifiable(); .returns(() => of({ payload: oldPatterns, version })).verifiable();
patternsState.load().subscribe(); patternsState.load().subscribe();
}); });
@ -86,7 +85,7 @@ describe('PatternsState', () => {
const request = { ...newPattern }; const request = { ...newPattern };
patternsService.setup(x => x.postPattern(app, request, version)) patternsService.setup(x => x.postPattern(app, request, version))
.returns(() => of(new Versioned(newVersion, newPattern))).verifiable(); .returns(() => of(versioned(newVersion, newPattern))).verifiable();
patternsState.create(request).subscribe(); patternsState.create(request).subscribe();
@ -98,7 +97,7 @@ describe('PatternsState', () => {
const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' }; const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' };
patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version)) patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
patternsState.update(oldPatterns[1], request).subscribe(); patternsState.update(oldPatterns[1], request).subscribe();
@ -112,7 +111,7 @@ describe('PatternsState', () => {
it('should remove pattern from snapshot when deleted', () => { it('should remove pattern from snapshot when deleted', () => {
patternsService.setup(x => x.deletePattern(app, oldPatterns[0].id, version)) patternsService.setup(x => x.deletePattern(app, oldPatterns[0].id, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
patternsState.delete(oldPatterns[0]).subscribe(); patternsState.delete(oldPatterns[0]).subscribe();

105
src/Squidex/app/shared/state/patterns.state.ts

@ -7,11 +7,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
mapVersioned,
shareSubscribed,
State, State,
Version Version
} from '@app/framework'; } from '@app/framework';
@ -60,79 +62,56 @@ export class PatternsState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
const update$ = return this.patternsService.getPatterns(this.appName).pipe(
this.patternsService.getPatterns(this.appName).pipe( tap(({ version, payload }) => {
share()); if (isReload) {
this.dialogs.notifyInfo('Patterns reloaded.');
}
update$.subscribe(({ version, patterns: items }) => { this.next(s => {
if (isReload) { const patterns = ImmutableArray.of(payload).sortByStringAsc(x => x.name);
this.dialogs.notifyInfo('Patterns reloaded.');
}
this.next(s => { return { ...s, patterns, isLoaded: true, version: version };
const patterns = ImmutableArray.of(items).sortByStringAsc(x => x.name); });
}),
return { ...s, patterns, isLoaded: true, version: version }; shareSubscribed(this.dialogs, { project: x => x.payload }));
});
}, error => {
this.dialogs.notifyError(error);
});
return update$;
} }
public create(request: EditPatternDto): Observable<PatternDto> { public create(request: EditPatternDto): Observable<PatternDto> {
const update$ = return this.patternsService.postPattern(this.appName, request, this.version).pipe(
this.patternsService.postPattern(this.appName, request, this.version).pipe( tap(({ version, payload }) => {
share()); this.next(s => {
const patterns = s.patterns.push(payload).sortByStringAsc(x => x.name);
update$.subscribe(({ version, payload: pattern }) => {
this.next(s => { return { ...s, patterns, version: version };
const patterns = s.patterns.push(pattern).sortByStringAsc(x => x.name); });
}),
return { ...s, patterns, version: version }; shareSubscribed(this.dialogs, { project: x => x.payload }));
});
}, error => {
this.dialogs.notifyError(error);
});
return update$.pipe(map(x => x.payload));
} }
public update(pattern: PatternDto, request: EditPatternDto): Observable<PatternDto> { public update(pattern: PatternDto, request: EditPatternDto): Observable<PatternDto> {
const update$ = return this.patternsService.putPattern(this.appName, pattern.id, request, this.version).pipe(
this.patternsService.putPattern(this.appName, pattern.id, request, this.version).pipe( mapVersioned(() => update(pattern, request)),
map(({ version }) => ({ version, payload: update(pattern, request) })), share()); tap(({ version, payload }) => {
this.next(s => {
update$.subscribe(({ version, payload }) => { const patterns = s.patterns.replaceBy('id', payload).sortByStringAsc(x => x.name);
this.next(s => {
const patterns = s.patterns.replaceBy('id', payload).sortByStringAsc(x => x.name); return { ...s, patterns, version: version };
});
return { ...s, patterns, version: version }; }),
}); shareSubscribed(this.dialogs, { project: x => x.payload }));
}, error => {
this.dialogs.notifyError(error);
});
return update$.pipe(map(x => x.payload));
} }
public delete(pattern: PatternDto): Observable<any> { public delete(pattern: PatternDto): Observable<any> {
const update$ = return this.patternsService.deletePattern(this.appName, pattern.id, this.version).pipe(
this.patternsService.deletePattern(this.appName, pattern.id, this.version).pipe( tap(({ version }) => {
share()); this.next(s => {
const patterns = s.patterns.filter(c => c.id !== pattern.id);
update$.subscribe(({ version }) => {
this.next(s => { return { ...s, patterns, version: version };
const patterns = s.patterns.filter(c => c.id !== pattern.id); });
}),
return { ...s, patterns, version: version }; shareSubscribed(this.dialogs));
});
}, error => {
this.dialogs.notifyError(error);
});
return update$;
} }
private get appName() { private get appName() {

147
src/Squidex/app/shared/state/plans.state.spec.ts

@ -12,10 +12,9 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DialogService, DialogService,
PlanDto, PlanDto,
PlansDto,
PlansService, PlansService,
PlansState, PlansState,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -25,16 +24,20 @@ describe('PlansState', () => {
app, app,
appsState, appsState,
authService, authService,
creator,
newVersion, newVersion,
version version
} = TestValues; } = TestValues;
const oldPlans = const oldPlans = {
new PlansDto('id1', 'id2', true, [ currentPlanId: 'id1',
planOwner: creator,
plans: [
new PlanDto('id1', 'name1', '100€', 'id1_yearly', '200€', 1, 1, 1), new PlanDto('id1', 'name1', '100€', 'id1_yearly', '200€', 1, 1, 1),
new PlanDto('id2', 'name2', '400€', 'id2_yearly', '800€', 2, 2, 2) new PlanDto('id2', 'name2', '400€', 'id2_yearly', '800€', 2, 2, 2)
], ],
version); hasPortal: true
};
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let plansService: IMock<PlansService>; let plansService: IMock<PlansService>;
@ -44,84 +47,102 @@ describe('PlansState', () => {
dialogs = Mock.ofType<DialogService>(); dialogs = Mock.ofType<DialogService>();
plansService = Mock.ofType<PlansService>(); plansService = Mock.ofType<PlansService>();
plansService.setup(x => x.getPlans(app))
.returns(() => of(oldPlans));
plansState = new PlansState(appsState.object, authService.object, dialogs.object, plansService.object); plansState = new PlansState(appsState.object, authService.object, dialogs.object, plansService.object);
}); });
it('should load plans', () => { afterEach(() => {
plansState.load().pipe(onErrorResumeNext()).subscribe(); plansService.verifyAll();
});
describe('Loading', () => {
it('should load plans', () => {
plansService.setup(x => x.getPlans(app))
.returns(() => of(versioned(version, oldPlans))).verifiable();
expect(plansState.snapshot.plans.values).toEqual([ plansState.load().subscribe();
{ isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] },
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[1] }
]);
expect(plansState.snapshot.isOwner).toBeFalsy();
expect(plansState.snapshot.isLoaded).toBeTruthy();
expect(plansState.snapshot.hasPortal).toBeTruthy();
expect(plansState.snapshot.version).toEqual(version);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); expect(plansState.snapshot.plans.values).toEqual([
}); { isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] },
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[1] }
]);
expect(plansState.snapshot.isOwner).toBeFalsy();
expect(plansState.snapshot.isLoaded).toBeTruthy();
expect(plansState.snapshot.hasPortal).toBeTruthy();
expect(plansState.snapshot.version).toEqual(version);
it('should load plans with overriden id', () => { dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
plansState.load(false, 'id2_yearly').pipe(onErrorResumeNext()).subscribe(); });
expect(plansState.snapshot.plans.values).toEqual([ it('should load plans with overriden id', () => {
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, plansService.setup(x => x.getPlans(app))
{ isSelected: false, isYearlySelected: true, plan: oldPlans.plans[1] } .returns(() => of(versioned(version, oldPlans))).verifiable();
]);
expect(plansState.snapshot.isOwner).toBeFalsy();
expect(plansState.snapshot.isLoaded).toBeTruthy();
expect(plansState.snapshot.hasPortal).toBeTruthy();
expect(plansState.snapshot.version).toEqual(version);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); plansState.load(false, 'id2_yearly').subscribe();
});
it('should show notification on load when reload is true', () => { expect(plansState.snapshot.plans.values).toEqual([
plansState.load(true).subscribe(); { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] },
{ isSelected: false, isYearlySelected: true, plan: oldPlans.plans[1] }
]);
expect(plansState.snapshot.isOwner).toBeFalsy();
expect(plansState.snapshot.isLoaded).toBeTruthy();
expect(plansState.snapshot.hasPortal).toBeTruthy();
expect(plansState.snapshot.version).toEqual(version);
expect().nothing(); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
});
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); it('should show notification on load when reload is true', () => {
plansService.setup(x => x.getPlans(app))
.returns(() => of(versioned(version, oldPlans))).verifiable();
plansState.load(true).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
}); });
it('should redirect when returning url', () => { describe('Updates', () => {
plansState.window = <any>{ location: {} }; beforeEach(() => {
plansState.window = <any>{ location: {} };
const result = { redirectUri: 'http://url' }; plansService.setup(x => x.getPlans(app))
.returns(() => of(versioned(version, oldPlans))).verifiable();
plansService.setup(x => x.putPlan(app, It.isAny(), version)) plansState.load().subscribe();
.returns(() => of(new Versioned(newVersion, result))); });
plansState.load().subscribe(); it('should redirect when returning url', () => {
plansState.change('free').pipe(onErrorResumeNext()).subscribe(); plansState.window = <any>{ location: {} };
expect(plansState.snapshot.plans.values).toEqual([ const result = { redirectUri: 'http://url' };
{ isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] },
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[1] } plansService.setup(x => x.putPlan(app, It.isAny(), version))
]); .returns(() => of(versioned(newVersion, result)));
expect(plansState.window.location.href).toBe(result.redirectUri);
expect(plansState.snapshot.version).toEqual(version); plansState.change('free').pipe(onErrorResumeNext()).subscribe();
});
it('should update plans when no returning url', () => { expect(plansState.snapshot.plans.values).toEqual([
plansState.window = <any>{ location: {} }; { isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] },
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[1] }
]);
expect(plansState.window.location.href).toBe(result.redirectUri);
expect(plansState.snapshot.version).toEqual(version);
});
plansService.setup(x => x.putPlan(app, It.isAny(), version)) it('should update plans when no returning url', () => {
.returns(() => of(new Versioned(newVersion, { redirectUri: '' }))); plansService.setup(x => x.putPlan(app, It.isAny(), version))
.returns(() => of(versioned(newVersion, { redirectUri: '' })));
plansState.load().subscribe(); plansState.change('id2_yearly').pipe(onErrorResumeNext()).subscribe();
plansState.change('id2_yearly').pipe(onErrorResumeNext()).subscribe();
expect(plansState.snapshot.plans.values).toEqual([ expect(plansState.snapshot.plans.values).toEqual([
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] }, { isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] },
{ isSelected: false, isYearlySelected: true, plan: oldPlans.plans[1] } { isSelected: false, isYearlySelected: true, plan: oldPlans.plans[1] }
]); ]);
expect(plansState.snapshot.isOwner).toBeTruthy(); expect(plansState.snapshot.isOwner).toBeTruthy();
expect(plansState.snapshot.version).toEqual(newVersion); expect(plansState.snapshot.version).toEqual(newVersion);
});
}); });
}); });

18
src/Squidex/app/shared/state/plans.state.ts

@ -12,7 +12,7 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
notify, shareSubscribed,
State, State,
Version Version
} from '@app/framework'; } from '@app/framework';
@ -88,26 +88,26 @@ export class PlansState extends State<Snapshot> {
} }
return this.plansService.getPlans(this.appName).pipe( return this.plansService.getPlans(this.appName).pipe(
tap(dto => { tap(({ version, payload }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Plans reloaded.'); this.dialogs.notifyInfo('Plans reloaded.');
} }
this.next(s => { this.next(s => {
const planId = overridePlanId || dto.currentPlanId; const planId = overridePlanId || payload.currentPlanId;
const plans = ImmutableArray.of(dto.plans.map(x => this.createPlan(x, planId))); const plans = ImmutableArray.of(payload.plans.map(x => this.createPlan(x, planId)));
return { return {
...s, ...s,
plans: plans, plans: plans,
isOwner: !dto.planOwner || dto.planOwner === this.userId, isOwner: !payload.planOwner || payload.planOwner === this.userId,
isLoaded: true, isLoaded: true,
version: dto.version, version,
hasPortal: dto.hasPortal hasPortal: payload.hasPortal
}; };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public change(planId: string): Observable<any> { public change(planId: string): Observable<any> {
@ -123,7 +123,7 @@ export class PlansState extends State<Snapshot> {
}); });
} }
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
private createPlan(plan: PlanDto, id: string) { private createPlan(plan: PlanDto, id: string) {

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

@ -11,10 +11,9 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DialogService, DialogService,
RoleDto, RoleDto,
RolesDto,
RolesService, RolesService,
RolesState, RolesState,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -46,7 +45,7 @@ describe('RolesState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load roles', () => { it('should load roles', () => {
rolesService.setup(x => x.getRoles(app)) rolesService.setup(x => x.getRoles(app))
.returns(() => of(new RolesDto(oldRoles, version))).verifiable(); .returns(() => of({ payload: oldRoles, version })).verifiable();
rolesState.load().subscribe(); rolesState.load().subscribe();
@ -59,7 +58,7 @@ describe('RolesState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
rolesService.setup(x => x.getRoles(app)) rolesService.setup(x => x.getRoles(app))
.returns(() => of(new RolesDto(oldRoles, version))).verifiable(); .returns(() => of(versioned(version, oldRoles))).verifiable();
rolesState.load(true).subscribe(); rolesState.load(true).subscribe();
@ -72,7 +71,7 @@ describe('RolesState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
rolesService.setup(x => x.getRoles(app)) rolesService.setup(x => x.getRoles(app))
.returns(() => of(new RolesDto(oldRoles, version))); .returns(() => of(versioned(version, oldRoles))).verifiable();
rolesState.load().subscribe(); rolesState.load().subscribe();
}); });
@ -83,7 +82,7 @@ describe('RolesState', () => {
const request = { name: newRole.name }; const request = { name: newRole.name };
rolesService.setup(x => x.postRole(app, request, version)) rolesService.setup(x => x.postRole(app, request, version))
.returns(() => of(new Versioned(newVersion, newRole))); .returns(() => of(versioned(newVersion, newRole)));
rolesState.add(request).subscribe(); rolesState.add(request).subscribe();
@ -95,7 +94,7 @@ describe('RolesState', () => {
const request = { permissions: ['P4', 'P5'] }; const request = { permissions: ['P4', 'P5'] };
rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version)) rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version))
.returns(() => of(new Versioned(newVersion, {}))); .returns(() => of(versioned(newVersion)));
rolesState.update(oldRoles[1], request).subscribe(); rolesState.update(oldRoles[1], request).subscribe();
@ -107,7 +106,7 @@ describe('RolesState', () => {
it('should remove role from snapshot when deleted', () => { it('should remove role from snapshot when deleted', () => {
rolesService.setup(x => x.deleteRole(app, oldRoles[0].name, version)) rolesService.setup(x => x.deleteRole(app, oldRoles[0].name, version))
.returns(() => of(new Versioned(newVersion, {}))); .returns(() => of(versioned(newVersion)));
rolesState.delete(oldRoles[0]).subscribe(); rolesState.delete(oldRoles[0]).subscribe();

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

@ -12,7 +12,8 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
notify, mapVersioned,
shareSubscribed,
State, State,
Version Version
} from '@app/framework'; } from '@app/framework';
@ -62,55 +63,56 @@ export class RolesState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
return this.rolesService.getRoles(this.appName).pipe( return this.rolesService.getRoles(this.appName).pipe(
tap(dtos => { tap(({ payload, version }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Roles reloaded.'); this.dialogs.notifyInfo('Roles reloaded.');
} }
this.next(s => { this.next(s => {
const roles = ImmutableArray.of(dtos.roles).sortByStringAsc(x => x.name); const roles = ImmutableArray.of(payload).sortByStringAsc(x => x.name);
return { ...s, roles, isLoaded: true, version: dtos.version }; return { ...s, roles, isLoaded: true, version };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public add(request: CreateRoleDto): Observable<any> { public add(request: CreateRoleDto): Observable<RoleDto> {
return this.rolesService.postRole(this.appName, request, this.version).pipe( return this.rolesService.postRole(this.appName, request, this.version).pipe(
tap(dto => { tap(({ version, payload }) => {
this.next(s => { this.next(s => {
const roles = s.roles.push(dto.payload).sortByStringAsc(x => x.name); const roles = s.roles.push(payload).sortByStringAsc(x => x.name);
return { ...s, roles, version: dto.version }; return { ...s, roles, version };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public delete(role: RoleDto): Observable<any> { public delete(role: RoleDto): Observable<any> {
return this.rolesService.deleteRole(this.appName, role.name, this.version).pipe( return this.rolesService.deleteRole(this.appName, role.name, this.version).pipe(
tap(dto => { tap(({ version }) => {
this.next(s => { this.next(s => {
const roles = s.roles.removeBy('name', role); const roles = s.roles.removeBy('name', role);
return { ...s, roles, version: dto.version }; return { ...s, roles, version };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public update(role: RoleDto, request: UpdateRoleDto): Observable<any> { public update(role: RoleDto, request: UpdateRoleDto): Observable<RoleDto> {
return this.rolesService.putRole(this.appName, role.name, request, this.version).pipe( return this.rolesService.putRole(this.appName, role.name, request, this.version).pipe(
tap(dto => { mapVersioned(() => update(role, request)),
tap(({ version, payload }) => {
this.next(s => { this.next(s => {
const roles = s.roles.replaceBy('name', update(role, request)); const roles = s.roles.replaceBy('name', payload);
return { ...s, roles, version: dto.version }; return { ...s, roles, version };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
private get appName() { private get appName() {

8
src/Squidex/app/shared/state/rule-events.state.ts

@ -12,8 +12,8 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
notify,
Pager, Pager,
shareSubscribed,
State State
} from '@app/framework'; } from '@app/framework';
@ -78,7 +78,7 @@ export class RuleEventsState extends State<Snapshot> {
return { ...s, ruleEvents, ruleEventsPager, isLoaded: true }; return { ...s, ruleEvents, ruleEventsPager, isLoaded: true };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public enqueue(event: RuleEventDto): Observable<any> { public enqueue(event: RuleEventDto): Observable<any> {
@ -86,7 +86,7 @@ export class RuleEventsState extends State<Snapshot> {
tap(() => { tap(() => {
this.dialogs.notifyInfo('Events enqueued. Will be resend in a few seconds.'); this.dialogs.notifyInfo('Events enqueued. Will be resend in a few seconds.');
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public cancel(event: RuleEventDto): Observable<any> { public cancel(event: RuleEventDto): Observable<any> {
@ -98,7 +98,7 @@ export class RuleEventsState extends State<Snapshot> {
return { ...s, ruleEvents, isLoaded: true }; return { ...s, ruleEvents, isLoaded: true };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public goNext(): Observable<any> { public goNext(): Observable<any> {

12
src/Squidex/app/shared/state/rules.state.spec.ts

@ -14,7 +14,7 @@ import {
DialogService, DialogService,
RuleDto, RuleDto,
RulesService, RulesService,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -103,7 +103,7 @@ describe('RulesState', () => {
const newAction = {}; const newAction = {};
rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version)) rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
rulesState.updateAction(oldRules[0], newAction, modified).subscribe(); rulesState.updateAction(oldRules[0], newAction, modified).subscribe();
@ -117,7 +117,7 @@ describe('RulesState', () => {
const newTrigger = {}; const newTrigger = {};
rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version)) rulesService.setup(x => x.putRule(app, oldRules[0].id, It.isAny(), version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
rulesState.updateTrigger(oldRules[0], newTrigger, modified).subscribe(); rulesState.updateTrigger(oldRules[0], newTrigger, modified).subscribe();
@ -129,7 +129,7 @@ describe('RulesState', () => {
it('should mark as enabled and update and user info when enabled', () => { it('should mark as enabled and update and user info when enabled', () => {
rulesService.setup(x => x.enableRule(app, oldRules[0].id, version)) rulesService.setup(x => x.enableRule(app, oldRules[0].id, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
rulesState.enable(oldRules[0], modified).subscribe(); rulesState.enable(oldRules[0], modified).subscribe();
@ -141,7 +141,7 @@ describe('RulesState', () => {
it('should mark as disabled and update and user info when disabled', () => { it('should mark as disabled and update and user info when disabled', () => {
rulesService.setup(x => x.disableRule(app, oldRules[1].id, version)) rulesService.setup(x => x.disableRule(app, oldRules[1].id, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
rulesState.disable(oldRules[1], modified).subscribe(); rulesState.disable(oldRules[1], modified).subscribe();
@ -153,7 +153,7 @@ describe('RulesState', () => {
it('should remove rule from snapshot when deleted', () => { it('should remove rule from snapshot when deleted', () => {
rulesService.setup(x => x.deleteRule(app, oldRules[0].id, version)) rulesService.setup(x => x.deleteRule(app, oldRules[0].id, version))
.returns(() => of(new Versioned(newVersion, {}))).verifiable(); .returns(() => of(versioned(newVersion))).verifiable();
rulesState.delete(oldRules[0]).subscribe(); rulesState.delete(oldRules[0]).subscribe();

143
src/Squidex/app/shared/state/rules.state.ts

@ -7,12 +7,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, share } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DateTime, DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
shareSubscribed,
State, State,
Version Version
} from '@app/framework'; } from '@app/framework';
@ -60,112 +61,86 @@ export class RulesState extends State<Snapshot> {
this.resetState(); this.resetState();
} }
const http$ = return this.rulesService.getRules(this.appName).pipe(
this.rulesService.getRules(this.appName).pipe( tap(payload => {
share()); if (isReload) {
this.dialogs.notifyInfo('Rules reloaded.');
}
http$.subscribe(response => { this.next(s => {
if (isReload) { const rules = ImmutableArray.of(payload);
this.dialogs.notifyInfo('Rules reloaded.');
}
this.next(s => { return { ...s, rules, isLoaded: true };
const rules = ImmutableArray.of(response); });
}),
return { ...s, rules, isLoaded: true }; shareSubscribed(this.dialogs));
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
public create(request: CreateRuleDto, now?: DateTime): Observable<RuleDto> { public create(request: CreateRuleDto, now?: DateTime): Observable<RuleDto> {
const http$ = return this.rulesService.postRule(this.appName, request, this.user, now || DateTime.now()).pipe(
this.rulesService.postRule(this.appName, request, this.user, now || DateTime.now()).pipe( tap(rule => {
share()); this.next(s => {
const rules = s.rules.push(rule);
http$.subscribe(rule => {
this.next(s => { return { ...s, rules };
const rules = s.rules.push(rule); });
}),
return { ...s, rules }; shareSubscribed(this.dialogs));
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
public delete(rule: RuleDto): Observable<any> { public delete(rule: RuleDto): Observable<any> {
const http$ = return this.rulesService.deleteRule(this.appName, rule.id, rule.version).pipe(
this.rulesService.deleteRule(this.appName, rule.id, rule.version).pipe( tap(() => {
share()); this.next(s => {
const rules = s.rules.removeAll(x => x.id === rule.id);
http$.subscribe(() => {
this.next(s => { return { ...s, rules };
const rules = s.rules.removeAll(x => x.id === rule.id); });
}),
return { ...s, rules }; shareSubscribed(this.dialogs));
});
}, error => {
this.dialogs.notifyError(error);
});
return http$;
} }
public updateAction(rule: RuleDto, action: any, now?: DateTime): Observable<any> { public updateAction(rule: RuleDto, action: any, now?: DateTime): Observable<any> {
const http$ = return this.rulesService.putRule(this.appName, rule.id, { action }, rule.version).pipe(
this.rulesService.putRule(this.appName, rule.id, { action }, rule.version).pipe( map(({ version }) => updateAction(rule, action, this.user, version, now)),
map(({ version }) => updateAction(rule, action, this.user, version, now)), share()); tap(updated => {
this.replaceRule(updated);
this.replaceRule(http$); }),
shareSubscribed(this.dialogs));
return http$;
} }
public updateTrigger(rule: RuleDto, trigger: any, now?: DateTime): Observable<any> { public updateTrigger(rule: RuleDto, trigger: any, now?: DateTime): Observable<any> {
const http$ = return this.rulesService.putRule(this.appName, rule.id, { trigger }, rule.version).pipe(
this.rulesService.putRule(this.appName, rule.id, { trigger }, rule.version).pipe( map(({ version }) => updateTrigger(rule, trigger, this.user, version, now)),
map(({ version }) => updateTrigger(rule, trigger, this.user, version, now)), share()); tap(updated => {
this.replaceRule(updated);
this.replaceRule(http$); }),
shareSubscribed(this.dialogs));
return http$;
} }
public enable(rule: RuleDto, now?: DateTime): Observable<any> { public enable(rule: RuleDto, now?: DateTime): Observable<any> {
const http$ = return this.rulesService.enableRule(this.appName, rule.id, rule.version).pipe(
this.rulesService.enableRule(this.appName, rule.id, rule.version).pipe( map(({ version }) => setEnabled(rule, true, this.user, version, now)),
map(({ version }) => setEnabled(rule, true, this.user, version, now)), share()); tap(updated => {
this.replaceRule(updated);
this.replaceRule(http$); }),
shareSubscribed(this.dialogs));
return http$;
} }
public disable(rule: RuleDto, now?: DateTime): Observable<any> { public disable(rule: RuleDto, now?: DateTime): Observable<any> {
const http$ = return this.rulesService.disableRule(this.appName, rule.id, rule.version).pipe(
this.rulesService.disableRule(this.appName, rule.id, rule.version).pipe( map(({ version }) => setEnabled(rule, false, this.user, version, now)),
map(({ version }) => setEnabled(rule, false, this.user, version, now)), share()); tap(updated => {
this.replaceRule(updated);
this.replaceRule(http$); }),
shareSubscribed(this.dialogs));
return http$;
} }
private replaceRule(http$: Observable<RuleDto>) { private replaceRule(rule: RuleDto) {
http$.subscribe(rule => { this.next(s => {
this.next(s => { const rules = s.rules.replaceBy('id', rule);
const rules = s.rules.replaceBy('id', rule);
return { ...s, rules }; return { ...s, rules };
});
}, error => {
this.dialogs.notifyError(error);
}); });
} }

630
src/Squidex/app/shared/state/schemas.state.spec.ts

@ -21,7 +21,7 @@ import {
SchemaDto, SchemaDto,
SchemasService, SchemasService,
UpdateSchemaCategoryDto, UpdateSchemaCategoryDto,
Versioned versioned
} from './../'; } from './../';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -65,167 +65,152 @@ describe('SchemasState', () => {
dialogs = Mock.ofType<DialogService>(); dialogs = Mock.ofType<DialogService>();
schemasService = Mock.ofType<SchemasService>(); schemasService = Mock.ofType<SchemasService>();
schemasService.setup(x => x.getSchemas(app))
.returns(() => of(oldSchemas));
schemasService.setup(x => x.getSchema(app, schema.name))
.returns(() => of(schema));
schemasService.setup(x => x.getSchema(app, schema.name))
.returns(() => of(schema));
schemasState = new SchemasState(appsState.object, authService.object, dialogs.object, schemasService.object); schemasState = new SchemasState(appsState.object, authService.object, dialogs.object, schemasService.object);
schemasState.load().subscribe();
}); });
it('should load schemas', () => { afterEach(() => {
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas);
expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false });
schemasService.verifyAll(); schemasService.verifyAll();
}); });
it('should not remove custom category when loading schemas', () => { describe('Loading', () => {
schemasState.addCategory('category3'); it('should load schemas', () => {
schemasState.load(true).subscribe(); schemasService.setup(x => x.getSchemas(app))
.returns(() => of(oldSchemas)).verifiable();
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas); schemasState.load().subscribe();
expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true });
schemasService.verifyAll(); expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas);
}); expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false });
it('should show notification on load when reload is true', () => { schemasService.verifyAll();
schemasState.load(true).subscribe(); });
expect().nothing(); it('should not remove custom category when loading schemas', () => {
schemasService.setup(x => x.getSchemas(app))
.returns(() => of(oldSchemas)).verifiable();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); schemasState.addCategory('category3');
}); schemasState.load(true).subscribe();
it('should add category', () => { expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas);
schemasState.addCategory('category3'); expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true });
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true }); schemasService.verifyAll();
}); });
it('should remove category', () => { it('should show notification on load when reload is true', () => {
schemasState.removeCategory('category1'); schemasService.setup(x => x.getSchemas(app))
.returns(() => of(oldSchemas)).verifiable();
expect(schemasState.snapshot.categories).toEqual({ 'category2': false }); schemasState.load(true).subscribe();
});
it('should return schema on select and reload when already loaded', () => { expect().nothing();
schemasState.select('name2').subscribe();
schemasState.select('name2').subscribe();
schemasService.verify(x => x.getSchema(app, 'name2'), Times.exactly(2)); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
expect().nothing();
}); });
it('should return schema on select and load when not loaded', () => { describe('Updates', () => {
let selectedSchema: SchemaDetailsDto; beforeEach(() => {
schemasService.setup(x => x.getSchemas(app))
.returns(() => of(oldSchemas)).verifiable();
schemasState.select('name2').subscribe(x => { schemasState.load().subscribe();
selectedSchema = x!;
}); });
expect(selectedSchema!).toBe(schema); it('should add category', () => {
expect(schemasState.snapshot.selectedSchema).toBe(schema); schemasState.addCategory('category3');
expect(schemasState.snapshot.selectedSchema).toBe(<SchemaDetailsDto>schemasState.snapshot.schemas.at(1));
});
it('should return null on select when loading failed', () => { expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true });
schemasService.setup(x => x.getSchema(app, 'failed')) });
.returns(() => throwError({}));
let selectedSchema: SchemaDetailsDto; it('should remove category', () => {
schemasState.removeCategory('category1');
schemasState.select('failed').subscribe(x => { expect(schemasState.snapshot.categories).toEqual({ 'category2': false });
selectedSchema = x!;
}); });
expect(selectedSchema!).toBeNull(); it('should return schema on select and reload when already loaded', () => {
expect(schemasState.snapshot.selectedSchema).toBeNull(); schemasService.setup(x => x.getSchema(app, schema.name))
}); .returns(() => of(schema)).verifiable(Times.exactly(2));
it('should return null on select when unselecting schema', () => { schemasState.select('name2').subscribe();
let selectedSchema: SchemaDetailsDto; schemasState.select('name2').subscribe();
schemasState.select(null).subscribe(x => { expect().nothing();
selectedSchema = x!;
}); });
expect(selectedSchema!).toBeNull(); it('should return schema on select and load when not loaded', () => {
expect(schemasState.snapshot.selectedSchema).toBeNull(); schemasService.setup(x => x.getSchema(app, schema.name))
.returns(() => of(schema)).verifiable();
schemasService.verify(x => x.getSchema(app, It.isAnyString()), Times.never()); let selectedSchema: SchemaDetailsDto;
});
it('should mark published and update user info when published', () => { schemasState.select('name2').subscribe(x => {
schemasService.setup(x => x.publishSchema(app, oldSchemas[0].name, version)) selectedSchema = x!;
.returns(() => of(new Versioned(newVersion, {}))); });
schemasState.publish(oldSchemas[0], modified).subscribe(); expect(selectedSchema!).toBe(schema);
expect(schemasState.snapshot.selectedSchema).toBe(schema);
expect(schemasState.snapshot.selectedSchema).toBe(<SchemaDetailsDto>schemasState.snapshot.schemas.at(1));
});
const schema_1 = schemasState.snapshot.schemas.at(0); it('should return null on select when loading failed', () => {
schemasService.setup(x => x.getSchema(app, 'failed'))
.returns(() => throwError({})).verifiable();
expect(schema_1.isPublished).toBeTruthy(); let selectedSchema: SchemaDetailsDto;
expectToBeModified(schema_1);
});
it('should unmark published and update user info when unpublished', () => { schemasState.select('failed').subscribe(x => {
schemasService.setup(x => x.unpublishSchema(app, oldSchemas[1].name, version)) selectedSchema = x!;
.returns(() => of(new Versioned(newVersion, {}))); });
schemasState.unpublish(oldSchemas[1], modified).subscribe(); expect(selectedSchema!).toBeNull();
expect(schemasState.snapshot.selectedSchema).toBeNull();
const schema_1 = schemasState.snapshot.schemas.at(1); });
expect(schema_1.isPublished).toBeFalsy(); it('should return null on select when unselecting schema', () => {
expectToBeModified(schema_1); let selectedSchema: SchemaDetailsDto;
});
it('should change category and update user info when category changed', () => { schemasState.select(null).subscribe(x => {
const category = 'my-new-category'; selectedSchema = x!;
});
schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.name === category), version)) expect(selectedSchema!).toBeNull();
.returns(() => of(new Versioned(newVersion, {}))); expect(schemasState.snapshot.selectedSchema).toBeNull();
});
schemasState.changeCategory(oldSchemas[0], category, modified).subscribe(); it('should mark published and update user info when published', () => {
schemasService.setup(x => x.publishSchema(app, oldSchemas[0].name, version))
.returns(() => of(versioned(newVersion))).verifiable();
const schema_1 = schemasState.snapshot.schemas.at(0); schemasState.publish(oldSchemas[0], modified).subscribe();
expect(schema_1.category).toEqual(category); const schema_1 = schemasState.snapshot.schemas.at(0);
expectToBeModified(schema_1);
});
describe('with selection', () => { expect(schema_1.isPublished).toBeTruthy();
beforeEach(() => { expectToBeModified(schema_1);
schemasState.select(schema.name).subscribe();
}); });
it('should nmark published and update user info when published selected schema', () => { it('should unmark published and update user info when unpublished', () => {
schemasService.setup(x => x.publishSchema(app, schema.name, version)) schemasService.setup(x => x.unpublishSchema(app, oldSchemas[1].name, version))
.returns(() => of(new Versioned(newVersion, {}))); .returns(() => of(versioned(newVersion))).verifiable();
schemasState.publish(schema, modified).subscribe(); schemasState.unpublish(oldSchemas[1], modified).subscribe();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); const schema_1 = schemasState.snapshot.schemas.at(1);
expect(schema_1.isPublished).toBeTruthy(); expect(schema_1.isPublished).toBeFalsy();
expectToBeModified(schema_1); expectToBeModified(schema_1);
}); });
it('should change category and update user info when category of selected schema changed', () => { it('should change category and update user info when category changed', () => {
const category = 'my-new-category'; const category = 'my-new-category';
schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.name === category), version)) schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.name === category), version))
.returns(() => of(new Versioned(newVersion, {}))); .returns(() => of(versioned(newVersion))).verifiable();
schemasState.changeCategory(oldSchemas[0], category, modified).subscribe(); schemasState.changeCategory(oldSchemas[0], category, modified).subscribe();
@ -235,305 +220,340 @@ describe('SchemasState', () => {
expectToBeModified(schema_1); expectToBeModified(schema_1);
}); });
it('should update properties and update user info when updated', () => { describe('with selection', () => {
const request = { label: 'name2_label', hints: 'name2_hints' }; beforeEach(() => {
schemasService.setup(x => x.getSchema(app, schema.name))
.returns(() => of(schema)).verifiable();
schemasService.setup(x => x.putSchema(app, schema.name, It.isAny(), version)) schemasState.select(schema.name).subscribe();
.returns(() => of(new Versioned(newVersion, {}))); });
schemasState.update(schema, request, modified).subscribe(); it('should nmark published and update user info when published selected schema', () => {
schemasService.setup(x => x.publishSchema(app, schema.name, version))
.returns(() => of(versioned(newVersion))).verifiable();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); schemasState.publish(schema, modified).subscribe();
expect(schema_1.properties.label).toEqual(request.label); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expect(schema_1.properties.hints).toEqual(request.hints);
expectToBeModified(schema_1);
});
it('should update script properties and update user info when scripts configured', () => { expect(schema_1.isPublished).toBeTruthy();
const request = { query: '<query-script>' }; expectToBeModified(schema_1);
});
schemasService.setup(x => x.putScripts(app, schema.name, It.isAny(), version)) it('should change category and update user info when category of selected schema changed', () => {
.returns(() => of(new Versioned(newVersion, {}))); const category = 'my-new-category';
schemasState.configureScripts(schema, request, modified).subscribe(); schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.name === category), version))
.returns(() => of(versioned(newVersion))).verifiable();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); schemasState.changeCategory(oldSchemas[0], category, modified).subscribe();
expect(schema_1.scripts['query']).toEqual('<query-script>'); const schema_1 = schemasState.snapshot.schemas.at(0);
expectToBeModified(schema_1);
});
it('should update script properties and update user info when preview urls configured', () => { expect(schema_1.category).toEqual(category);
const request = { web: 'url' }; expectToBeModified(schema_1);
});
schemasService.setup(x => x.putPreviewUrls(app, schema.name, It.isAny(), version)) it('should update properties and update user info when updated', () => {
.returns(() => of(new Versioned(newVersion, {}))); const request = { label: 'name2_label', hints: 'name2_hints' };
schemasState.configurePreviewUrls(schema, request, modified).subscribe(); schemasService.setup(x => x.putSchema(app, schema.name, It.isAny(), version))
.returns(() => of(versioned(newVersion))).verifiable();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); schemasState.update(schema, request, modified).subscribe();
expect(schema_1.previewUrls).toEqual(request); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expectToBeModified(schema_1);
});
it('should add schema to snapshot when created', () => { expect(schema_1.properties.label).toEqual(request.label);
const request = new CreateSchemaDto('newName'); expect(schema_1.properties.hints).toEqual(request.hints);
expectToBeModified(schema_1);
});
const result = new SchemaDetailsDto('id4', 'newName', '', {}, false, false, modified, modifier, modified, modifier, version); it('should update script properties and update user info when scripts configured', () => {
const request = { query: '<query-script>' };
schemasService.setup(x => x.postSchema(app, request, modifier, modified)) schemasService.setup(x => x.putScripts(app, schema.name, It.isAny(), version))
.returns(() => of(result)); .returns(() => of(versioned(newVersion))).verifiable();
schemasState.create(request, modified).subscribe(); schemasState.configureScripts(schema, request, modified).subscribe();
expect(schemasState.snapshot.schemas.values.length).toBe(3); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expect(schemasState.snapshot.schemas.at(2)).toBe(result);
});
it('should remove schema from snapshot when deleted', () => { expect(schema_1.scripts['query']).toEqual('<query-script>');
schemasService.setup(x => x.deleteSchema(app, schema.name, version)) expectToBeModified(schema_1);
.returns(() => of(new Versioned(newVersion, {}))); });
schemasState.delete(schema).subscribe(); it('should update script properties and update user info when preview urls configured', () => {
const request = { web: 'url' };
expect(schemasState.snapshot.schemas.values.length).toBe(1); schemasService.setup(x => x.putPreviewUrls(app, schema.name, It.isAny(), version))
expect(schemasState.snapshot.selectedSchema).toBeNull(); .returns(() => of(versioned(newVersion))).verifiable();
});
it('should add field and update user info when field added', () => { schemasState.configurePreviewUrls(schema, request, modified).subscribe();
const request = new AddFieldDto(field1.name, field1.partitioning, field1.properties);
const newField = new RootFieldDto(3, '3', createProperties('String'), 'invariant'); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
schemasService.setup(x => x.postField(app, schema.name, It.isAny(), undefined, version)) expect(schema_1.previewUrls).toEqual(request);
.returns(() => of(new Versioned(newVersion, newField))); expectToBeModified(schema_1);
});
schemasState.addField(schema, request, undefined, modified).subscribe(); it('should add schema to snapshot when created', () => {
const request = new CreateSchemaDto('newName');
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); const result = new SchemaDetailsDto('id4', 'newName', '', {}, false, false, modified, modifier, modified, modifier, version);
expect(schema_1.fields).toEqual([field1, field2, newField]); schemasService.setup(x => x.postSchema(app, request, modifier, modified))
expectToBeModified(schema_1); .returns(() => of(result)).verifiable();
});
it('should add field and update user info when nested field added', () => { schemasState.create(request, modified).subscribe();
const request = new AddFieldDto(field1.name, field1.partitioning, field1.properties);
const newField = new NestedFieldDto(3, '3', createProperties('String'), 2); expect(schemasState.snapshot.schemas.values.length).toBe(3);
expect(schemasState.snapshot.schemas.at(2)).toBe(result);
});
schemasService.setup(x => x.postField(app, schema.name, It.isAny(), 2, version)) it('should remove schema from snapshot when deleted', () => {
.returns(() => of(new Versioned(newVersion, newField))); schemasService.setup(x => x.deleteSchema(app, schema.name, version))
.returns(() => of(versioned(newVersion))).verifiable();
schemasState.addField(schema, request, field2, modified).subscribe(); schemasState.delete(schema).subscribe();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schemasState.snapshot.schemas.values.length).toBe(1);
expect(schemasState.snapshot.selectedSchema).toBeNull();
});
expect(schema_1.fields[1].nested).toEqual([nested1, nested2, newField]); it('should add field and update user info when field added', () => {
expectToBeModified(schema_1); const request = new AddFieldDto(field1.name, field1.partitioning, field1.properties);
});
it('should remove field and update user info when field removed', () => { const newField = new RootFieldDto(3, '3', createProperties('String'), 'invariant');
schemasService.setup(x => x.deleteField(app, schema.name, field1.fieldId, undefined, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.deleteField(schema, field1, modified).subscribe(); schemasService.setup(x => x.postField(app, schema.name, It.isAny(), undefined, version))
.returns(() => of(versioned(newVersion, newField))).verifiable();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); schemasState.addField(schema, request, undefined, modified).subscribe();
expect(schema_1.fields).toEqual([field2]); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expectToBeModified(schema_1);
});
it('should remove field and update user info when nested field removed', () => { expect(schema_1.fields).toEqual([field1, field2, newField]);
schemasService.setup(x => x.deleteField(app, schema.name, nested1.fieldId, 2, version)) expectToBeModified(schema_1);
.returns(() => of(new Versioned(newVersion, {}))); });
schemasState.deleteField(schema, nested1, modified).subscribe(); it('should add field and update user info when nested field added', () => {
const request = new AddFieldDto(field1.name, field1.partitioning, field1.properties);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); const newField = new NestedFieldDto(3, '3', createProperties('String'), 2);
expect(schema_1.fields[1].nested).toEqual([nested2]); schemasService.setup(x => x.postField(app, schema.name, It.isAny(), 2, version))
expectToBeModified(schema_1); .returns(() => of(versioned(newVersion, newField))).verifiable();
});
it('should sort fields and update user info when fields sorted', () => { schemasState.addField(schema, request, field2, modified).subscribe();
schemasService.setup(x => x.putFieldOrdering(app, schema.name, [field2.fieldId, field1.fieldId], undefined, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.sortFields(schema, [field2, field1], undefined, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[1].nested).toEqual([nested1, nested2, newField]);
expectToBeModified(schema_1);
});
expect(schema_1.fields).toEqual([field2, field1]); it('should remove field and update user info when field removed', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.deleteField(app, schema.name, field1.fieldId, undefined, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should sort fields and update user info when nested fields sorted', () => { schemasState.deleteField(schema, field1, modified).subscribe();
schemasService.setup(x => x.putFieldOrdering(app, schema.name, [nested2.fieldId, nested1.fieldId], 2, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.sortFields(schema, [nested2, nested1], field2, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields).toEqual([field2]);
expectToBeModified(schema_1);
});
expect(schema_1.fields[1].nested).toEqual([nested2, nested1]); it('should remove field and update user info when nested field removed', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.deleteField(app, schema.name, nested1.fieldId, 2, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should update field properties and update user info when field updated', () => { schemasState.deleteField(schema, nested1, modified).subscribe();
const request = { properties: createProperties('String') };
schemasService.setup(x => x.putField(app, schema.name, field1.fieldId, request, undefined, version)) const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
.returns(() => of(new Versioned(newVersion, {})));
schemasState.updateField(schema, field1, request, modified).subscribe(); expect(schema_1.fields[1].nested).toEqual([nested2]);
expectToBeModified(schema_1);
});
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); it('should sort fields and update user info when fields sorted', () => {
schemasService.setup(x => x.putFieldOrdering(app, schema.name, [field2.fieldId, field1.fieldId], undefined, version))
.returns(() => of(versioned(newVersion))).verifiable();
expect(schema_1.fields[0].properties).toBe(request.properties); schemasState.sortFields(schema, [field2, field1], undefined, modified).subscribe();
expectToBeModified(schema_1);
});
it('should update field properties and update user info when nested field updated', () => { const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const request = { properties: createProperties('String') };
schemasService.setup(x => x.putField(app, schema.name, nested1.fieldId, request, 2, version)) expect(schema_1.fields).toEqual([field2, field1]);
.returns(() => of(new Versioned(newVersion, {}))); expectToBeModified(schema_1);
});
schemasState.updateField(schema, nested1, request, modified).subscribe(); it('should sort fields and update user info when nested fields sorted', () => {
schemasService.setup(x => x.putFieldOrdering(app, schema.name, [nested2.fieldId, nested1.fieldId], 2, version))
.returns(() => of(versioned(newVersion))).verifiable();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); schemasState.sortFields(schema, [nested2, nested1], field2, modified).subscribe();
expect(schema_1.fields[1].nested[0].properties).toBe(request.properties); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expectToBeModified(schema_1);
});
it('should mark field hidden and update user info when field hidden', () => { expect(schema_1.fields[1].nested).toEqual([nested2, nested1]);
schemasService.setup(x => x.hideField(app, schema.name, field1.fieldId, undefined, version)) expectToBeModified(schema_1);
.returns(() => of(new Versioned(newVersion, {}))); });
schemasState.hideField(schema, field1, modified).subscribe(); it('should update field properties and update user info when field updated', () => {
const request = { properties: createProperties('String') };
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); schemasService.setup(x => x.putField(app, schema.name, field1.fieldId, request, undefined, version))
.returns(() => of(versioned(newVersion))).verifiable();
expect(schema_1.fields[0].isHidden).toBeTruthy(); schemasState.updateField(schema, field1, request, modified).subscribe();
expectToBeModified(schema_1);
});
it('should mark field hidden and update user info when nested field hidden', () => { const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
schemasService.setup(x => x.hideField(app, schema.name, nested1.fieldId, 2, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.hideField(schema, nested1, modified).subscribe(); expect(schema_1.fields[0].properties).toBe(request.properties);
expectToBeModified(schema_1);
});
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); it('should update field properties and update user info when nested field updated', () => {
const request = { properties: createProperties('String') };
expect(schema_1.fields[1].nested[0].isHidden).toBeTruthy(); schemasService.setup(x => x.putField(app, schema.name, nested1.fieldId, request, 2, version))
expectToBeModified(schema_1); .returns(() => of(versioned(newVersion))).verifiable();
});
it('should mark field disabled and update user info when field disabled', () => { schemasState.updateField(schema, nested1, request, modified).subscribe();
schemasService.setup(x => x.disableField(app, schema.name, field1.fieldId, undefined, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.disableField(schema, field1, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[1].nested[0].properties).toBe(request.properties);
expectToBeModified(schema_1);
});
expect(schema_1.fields[0].isDisabled).toBeTruthy(); it('should mark field hidden and update user info when field hidden', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.hideField(app, schema.name, field1.fieldId, undefined, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should mark field disabled and update user info when nested disabled', () => { schemasState.hideField(schema, field1, modified).subscribe();
schemasService.setup(x => x.disableField(app, schema.name, nested1.fieldId, 2, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.disableField(schema, nested1, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[0].isHidden).toBeTruthy();
expectToBeModified(schema_1);
});
expect(schema_1.fields[1].nested[0].isDisabled).toBeTruthy(); it('should mark field hidden and update user info when nested field hidden', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.hideField(app, schema.name, nested1.fieldId, 2, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should mark field locked and update user info when field locked', () => { schemasState.hideField(schema, nested1, modified).subscribe();
schemasService.setup(x => x.lockField(app, schema.name, field1.fieldId, undefined, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.lockField(schema, field1, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[1].nested[0].isHidden).toBeTruthy();
expectToBeModified(schema_1);
});
expect(schema_1.fields[0].isLocked).toBeTruthy(); it('should mark field disabled and update user info when field disabled', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.disableField(app, schema.name, field1.fieldId, undefined, version))
}); .returns(() => of(versioned(newVersion)));
it('should mark field locked and update user info when nested field locked', () => { schemasState.disableField(schema, field1, modified).subscribe();
schemasService.setup(x => x.lockField(app, schema.name, nested1.fieldId, 2, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.lockField(schema, nested1, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[0].isDisabled).toBeTruthy();
expectToBeModified(schema_1);
});
expect(schema_1.fields[1].nested[0].isLocked).toBeTruthy(); it('should mark field disabled and update user info when nested disabled', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.disableField(app, schema.name, nested1.fieldId, 2, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should unmark field hidden and update user info when field shown', () => { schemasState.disableField(schema, nested1, modified).subscribe();
schemasService.setup(x => x.showField(app, schema.name, field2.fieldId, undefined, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.showField(schema, field2, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[1].nested[0].isDisabled).toBeTruthy();
expectToBeModified(schema_1);
});
expect(schema_1.fields[1].isHidden).toBeFalsy(); it('should mark field locked and update user info when field locked', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.lockField(app, schema.name, field1.fieldId, undefined, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should unmark field hidden and update user info when nested field shown', () => { schemasState.lockField(schema, field1, modified).subscribe();
schemasService.setup(x => x.showField(app, schema.name, nested2.fieldId, 2, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.showField(schema, nested2, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[0].isLocked).toBeTruthy();
expectToBeModified(schema_1);
});
expect(schema_1.fields[1].nested[1].isHidden).toBeFalsy(); it('should mark field locked and update user info when nested field locked', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.lockField(app, schema.name, nested1.fieldId, 2, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should unmark field disabled and update user info when field enabled', () => { schemasState.lockField(schema, nested1, modified).subscribe();
schemasService.setup(x => x.enableField(app, schema.name, field2.fieldId, undefined, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.enableField(schema, field2, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[1].nested[0].isLocked).toBeTruthy();
expectToBeModified(schema_1);
});
expect(schema_1.fields[1].isDisabled).toBeFalsy(); it('should unmark field hidden and update user info when field shown', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.showField(app, schema.name, field2.fieldId, undefined, version))
}); .returns(() => of(versioned(newVersion))).verifiable();
it('should unmark field disabled and update user info when nested field enabled', () => { schemasState.showField(schema, field2, modified).subscribe();
schemasService.setup(x => x.enableField(app, schema.name, nested2.fieldId, 2, version))
.returns(() => of(new Versioned(newVersion, {})));
schemasState.enableField(schema, nested2, modified).subscribe(); const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1); expect(schema_1.fields[1].isHidden).toBeFalsy();
expectToBeModified(schema_1);
});
expect(schema_1.fields[1].nested[1].isDisabled).toBeFalsy(); it('should unmark field hidden and update user info when nested field shown', () => {
expectToBeModified(schema_1); schemasService.setup(x => x.showField(app, schema.name, nested2.fieldId, 2, version))
.returns(() => of(versioned(newVersion))).verifiable();
schemasState.showField(schema, nested2, modified).subscribe();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expect(schema_1.fields[1].nested[1].isHidden).toBeFalsy();
expectToBeModified(schema_1);
});
it('should unmark field disabled and update user info when field enabled', () => {
schemasService.setup(x => x.enableField(app, schema.name, field2.fieldId, undefined, version))
.returns(() => of(versioned(newVersion))).verifiable();
schemasState.enableField(schema, field2, modified).subscribe();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expect(schema_1.fields[1].isDisabled).toBeFalsy();
expectToBeModified(schema_1);
});
it('should unmark field disabled and update user info when nested field enabled', () => {
schemasService.setup(x => x.enableField(app, schema.name, nested2.fieldId, 2, version))
.returns(() => of(versioned(newVersion))).verifiable();
schemasState.enableField(schema, nested2, modified).subscribe();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expect(schema_1.fields[1].nested[1].isDisabled).toBeFalsy();
expectToBeModified(schema_1);
});
}); });
});
function expectToBeModified(schema_1: SchemaDto) { function expectToBeModified(schema_1: SchemaDto) {
expect(schema_1.lastModified).toEqual(modified); expect(schema_1.lastModified).toEqual(modified);
expect(schema_1.lastModifiedBy).toEqual(modifier); expect(schema_1.lastModifiedBy).toEqual(modifier);
expect(schema_1.version).toEqual(newVersion); expect(schema_1.version).toEqual(newVersion);
} }
});
}); });

179
src/Squidex/app/shared/state/schemas.state.ts

@ -13,7 +13,8 @@ import {
DateTime, DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
notify, mapVersioned,
shareSubscribed,
State, State,
Types, Types,
Version Version
@ -45,7 +46,7 @@ interface Snapshot {
categories: { [name: string]: boolean }; categories: { [name: string]: boolean };
// The current schemas. // The current schemas.
schemas: ImmutableArray<SchemaDto>; schemas: SchemasList;
// Indicates if the schemas are loaded. // Indicates if the schemas are loaded.
isLoaded?: boolean; isLoaded?: boolean;
@ -54,6 +55,8 @@ interface Snapshot {
selectedSchema?: SchemaDetailsDto | null; selectedSchema?: SchemaDetailsDto | null;
} }
export type SchemasList = ImmutableArray<SchemaDto>;
function sameSchema(lhs: SchemaDetailsDto | null, rhs?: SchemaDetailsDto | null): boolean { function sameSchema(lhs: SchemaDetailsDto | null, rhs?: SchemaDetailsDto | null): boolean {
return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id && lhs.version === rhs.version); return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id && lhs.version === rhs.version);
} }
@ -116,44 +119,45 @@ export class SchemasState extends State<Snapshot> {
} }
return this.schemasService.getSchemas(this.appName).pipe( return this.schemasService.getSchemas(this.appName).pipe(
tap(dtos => { tap(payload => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Schemas reloaded.'); this.dialogs.notifyInfo('Schemas reloaded.');
} }
return this.next(s => { return this.next(s => {
const schemas = ImmutableArray.of(dtos).sortByStringAsc(x => x.displayName); const schemas = ImmutableArray.of(payload).sortByStringAsc(x => x.displayName);
const categories = buildCategories(s.categories, schemas); const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, isLoaded: true, categories }; return { ...s, schemas, isLoaded: true, categories };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public create(request: CreateSchemaDto, now?: DateTime) { public create(request: CreateSchemaDto, now?: DateTime): Observable<SchemaDetailsDto> {
return this.schemasService.postSchema(this.appName, request, this.user, now || DateTime.now()).pipe( return this.schemasService.postSchema(this.appName, request, this.user, now || DateTime.now()).pipe(
tap(dto => { tap(dto => {
return this.next(s => { this.next(s => {
const schemas = s.schemas.push(dto).sortByStringAsc(x => x.displayName); const schemas = s.schemas.push(dto).sortByStringAsc(x => x.displayName);
return { ...s, schemas }; return { ...s, schemas };
}); });
})); }),
shareSubscribed(this.dialogs, { silent: true }));
} }
public delete(schema: SchemaDto): Observable<any> { public delete(schema: SchemaDto): Observable<any> {
return this.schemasService.deleteSchema(this.appName, schema.name, schema.version).pipe( return this.schemasService.deleteSchema(this.appName, schema.name, schema.version).pipe(
tap(() => { tap(() => {
return this.next(s => { this.next(s => {
const schemas = s.schemas.filter(x => x.id !== schema.id); const schemas = s.schemas.filter(x => x.id !== schema.id);
const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? null : s.selectedSchema; const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? null : s.selectedSchema;
return { ...s, schemas, selectedSchema }; return { ...s, schemas, selectedSchema };
}); });
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public addCategory(name: string) { public addCategory(name: string) {
@ -172,138 +176,165 @@ export class SchemasState extends State<Snapshot> {
}); });
} }
public publish(schema: SchemaDto, now?: DateTime): Observable<any> { public publish(schema: SchemaDto, now?: DateTime): Observable<SchemaDto> {
return this.schemasService.publishSchema(this.appName, schema.name, schema.version).pipe( return this.schemasService.publishSchema(this.appName, schema.name, schema.version).pipe(
tap(dto => { map(({ version }) => setPublished(schema, true, this.user, version, now)),
this.replaceSchema(setPublished(schema, true, this.user, dto.version, now)); tap(newSchema => {
this.replaceSchema(newSchema);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public unpublish(schema: SchemaDto, now?: DateTime): Observable<any> { public unpublish(schema: SchemaDto, now?: DateTime): Observable<SchemaDto> {
return this.schemasService.unpublishSchema(this.appName, schema.name, schema.version).pipe( return this.schemasService.unpublishSchema(this.appName, schema.name, schema.version).pipe(
tap(dto => { map(({ version }) => setPublished(schema, false, this.user, version, now)),
this.replaceSchema(setPublished(schema, false, this.user, dto.version, now)); tap(newSchema => {
this.replaceSchema(newSchema);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public changeCategory(schema: SchemaDto, name: string, now?: DateTime): Observable<any> { public changeCategory(schema: SchemaDto, name: string, now?: DateTime): Observable<SchemaDto> {
return this.schemasService.putCategory(this.appName, schema.name, { name }, schema.version).pipe( return this.schemasService.putCategory(this.appName, schema.name, { name }, schema.version).pipe(
tap(dto => { map(({ version }) => changeCategory(schema, name, this.user, version, now)),
this.replaceSchema(changeCategory(schema, name, this.user, dto.version, now)); tap(newSchema => {
this.replaceSchema(newSchema);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public configurePreviewUrls(schema: SchemaDetailsDto, request: {}, now?: DateTime): Observable<any> { public configurePreviewUrls(schema: SchemaDetailsDto, request: {}, now?: DateTime): Observable<SchemaDetailsDto> {
return this.schemasService.putPreviewUrls(this.appName, schema.name, request, schema.version).pipe( return this.schemasService.putPreviewUrls(this.appName, schema.name, request, schema.version).pipe(
tap(dto => { map(({ version }) => configurePreviewUrls(schema, request, this.user, version, now)),
this.replaceSchema(configurePreviewUrls(schema, request, this.user, dto.version, now)); tap(newSchema => {
this.replaceSchema(newSchema);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public configureScripts(schema: SchemaDetailsDto, request: {}, now?: DateTime): Observable<any> { public configureScripts(schema: SchemaDetailsDto, request: {}, now?: DateTime): Observable<SchemaDetailsDto> {
return this.schemasService.putScripts(this.appName, schema.name, request, schema.version).pipe( return this.schemasService.putScripts(this.appName, schema.name, request, schema.version).pipe(
tap(dto => { map(({ version }) => configureScripts(schema, request, this.user, version, now)),
this.replaceSchema(configureScripts(schema, request, this.user, dto.version, now)); tap(newSchema => {
this.replaceSchema(newSchema);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public update(schema: SchemaDetailsDto, request: UpdateSchemaDto, now?: DateTime): Observable<any> { public update(schema: SchemaDetailsDto, request: UpdateSchemaDto, now?: DateTime): Observable<SchemaDetailsDto> {
return this.schemasService.putSchema(this.appName, schema.name, request, schema.version).pipe( return this.schemasService.putSchema(this.appName, schema.name, request, schema.version).pipe(
tap(dto => { map(({ version }) => updateProperties(schema, request, this.user, version, now)),
this.replaceSchema(updateProperties(schema, request, this.user, dto.version, now)); tap(newSchema => {
this.replaceSchema(newSchema);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public addField(schema: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto, now?: DateTime): Observable<FieldDto> { public addField(schema: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto, now?: DateTime): Observable<FieldDto> {
return this.schemasService.postField(this.appName, schema.name, request, pid(parent), schema.version).pipe( return this.schemasService.postField(this.appName, schema.name, request, pid(parent), schema.version).pipe(
tap(dto => { map(({ version, payload }) => {
if (Types.is(dto.payload, NestedFieldDto)) { let newSchema: SchemaDto;
this.replaceSchema(updateField(schema, addNested(parent!, dto.payload), this.user, dto.version, now));
if (Types.is(payload, NestedFieldDto)) {
newSchema = updateField(schema, addNested(parent!, payload), this.user, version, now);
} else { } else {
this.replaceSchema(addField(schema, dto.payload, this.user, dto.version, now)); newSchema = addField(schema, payload, this.user, version, now);
} }
return { newSchema, field: payload };
}),
tap(({ newSchema }) => {
this.replaceSchema(newSchema);
}), }),
map(d => d.payload)); shareSubscribed(this.dialogs, { silent: true, project: x => x.field }));
} }
public sortFields(schema: SchemaDetailsDto, fields: any[], parent?: RootFieldDto, now?: DateTime): Observable<any> { public sortFields(schema: SchemaDetailsDto, fields: any[], parent?: RootFieldDto, now?: DateTime): Observable<SchemaDetailsDto> {
return this.schemasService.putFieldOrdering(this.appName, schema.name, fields.map(t => t.fieldId), pid(parent), schema.version).pipe( return this.schemasService.putFieldOrdering(this.appName, schema.name, fields.map(t => t.fieldId), pid(parent), schema.version).pipe(
tap(dto => { map(({ version }) => {
let newSchema: SchemaDto;
if (!parent) { if (!parent) {
this.replaceSchema(replaceFields(schema, fields, this.user, dto.version, now)); newSchema = replaceFields(schema, fields, this.user, version, now);
} else { } else {
this.replaceSchema(updateField(schema, replaceNested(parent, fields), this.user, dto.version, now)); newSchema = updateField(schema, replaceNested(parent, fields), this.user, version, now);
} }
return newSchema;
}),
tap(newSchema => {
this.replaceSchema(newSchema);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
public lockField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> { public lockField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.lockField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe( return this.schemasService.lockField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
tap(dto => { mapVersioned(() => setLocked(field, true)),
this.replaceField(schema, setLocked(field, true), dto.version, now); tap(({ payload, version }) => {
this.replaceField(schema, payload, version, now);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public enableField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> { public enableField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.enableField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe( return this.schemasService.enableField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
tap(dto => { mapVersioned(() => setDisabled(field, false)),
this.replaceField(schema, setDisabled(field, false), dto.version, now); tap(({ payload, version }) => {
this.replaceField(schema, payload, version, now);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public disableField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> { public disableField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.disableField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe( return this.schemasService.disableField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
tap(dto => { mapVersioned(() => setDisabled(field, true)),
this.replaceField(schema, setDisabled(field, true), dto.version, now); tap(({ payload, version }) => {
this.replaceField(schema, payload, version, now);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public showField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> { public showField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.showField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe( return this.schemasService.showField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
tap(dto => { mapVersioned(() => setHidden(field, false)),
this.replaceField(schema, setHidden(field, false), dto.version, now); tap(({ payload, version }) => {
this.replaceField(schema, payload, version, now);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public hideField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> { public hideField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, now?: DateTime): Observable<T> {
return this.schemasService.hideField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe( return this.schemasService.hideField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
tap(dto => { mapVersioned(() => setHidden(field, true)),
this.replaceField(schema, setHidden(field, true), dto.version, now); tap(({ payload, version }) => {
this.replaceField(schema, payload, version, now);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public updateField(schema: SchemaDetailsDto, field: AnyFieldDto, request: UpdateFieldDto, now?: DateTime): Observable<any> { public updateField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, request: UpdateFieldDto, now?: DateTime): Observable<T> {
return this.schemasService.putField(this.appName, schema.name, field.fieldId, request, pidof(field), schema.version).pipe( return this.schemasService.putField(this.appName, schema.name, field.fieldId, request, pidof(field), schema.version).pipe(
tap(dto => { mapVersioned(() => update(field, request.properties)),
this.replaceField(schema, update(field, request.properties), dto.version, now); tap(({ payload, version }) => {
this.replaceField(schema, payload, version, now);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs, { project: x => x.payload }));
} }
public deleteField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> { public deleteField(schema: SchemaDetailsDto, field: AnyFieldDto, now?: DateTime): Observable<any> {
return this.schemasService.deleteField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe( return this.schemasService.deleteField(this.appName, schema.name, field.fieldId, pidof(field), schema.version).pipe(
tap(dto => { mapVersioned(() => field),
this.removeField(schema, field, dto.version, now); tap(({ payload, version }) => {
this.removeField(schema, payload, version, now);
}), }),
notify(this.dialogs)); shareSubscribed(this.dialogs));
} }
private replaceField(schema: SchemaDetailsDto, field: AnyFieldDto, version: Version, now?: DateTime) { private replaceField<T extends FieldDto>(schema: SchemaDetailsDto, field: T, version: Version, now?: DateTime) {
if (Types.is(field, RootFieldDto)) { if (Types.is(field, RootFieldDto)) {
this.replaceSchema(updateField(schema, field, this.user, version, now)); this.replaceSchema(updateField(schema, field, this.user, version, now));
} else { } else if (Types.is(field, NestedFieldDto)) {
const parent = schema.fields.find(x => x.fieldId === field.parentId); const parent = schema.fields.find(x => x.fieldId === field.parentId);
if (parent) { if (parent) {
@ -344,7 +375,7 @@ export class SchemasState extends State<Snapshot> {
} }
} }
function buildCategories(categories: { [name: string]: boolean }, schemas: ImmutableArray<SchemaDto>) { function buildCategories(categories: { [name: string]: boolean }, schemas: SchemasList) {
categories = { ...categories }; categories = { ...categories };
for (let category in categories) { for (let category in categories) {

10700
src/Squidex/package-lock.json

File diff suppressed because it is too large

93
src/Squidex/package.json

@ -15,91 +15,92 @@
"build:clean": "rimraf wwwroot/build" "build:clean": "rimraf wwwroot/build"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "7.2.3", "@angular/animations": "7.2.14",
"@angular/common": "7.2.3", "@angular/common": "7.2.14",
"@angular/core": "7.2.3", "@angular/core": "7.2.14",
"@angular/forms": "7.2.3", "@angular/forms": "7.2.14",
"@angular/http": "7.2.3", "@angular/http": "7.2.14",
"@angular/platform-browser": "7.2.3", "@angular/platform-browser": "7.2.14",
"@angular/platform-browser-dynamic": "7.2.3", "@angular/platform-browser-dynamic": "7.2.14",
"@angular/platform-server": "7.2.3", "@angular/platform-server": "7.2.14",
"@angular/router": "7.2.3", "@angular/router": "7.2.14",
"angular2-chartjs": "0.5.1", "angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"bootstrap": "4.2.1", "bootstrap": "4.3.1",
"core-js": "2.6.3", "core-js": "2.6.3",
"graphiql": "0.12.0", "graphiql": "0.13.0",
"graphql": "14.1.1", "graphql": "14.2.1",
"marked": "0.6.0", "marked": "0.6.2",
"moment": "2.24.0", "moment": "2.24.0",
"mousetrap": "1.6.2", "mousetrap": "1.6.3",
"ng2-dnd": "5.0.2", "ng2-dnd": "5.0.2",
"ngx-color-picker": "7.3.0", "ngx-color-picker": "7.5.0",
"oidc-client": "1.6.1", "oidc-client": "1.7.1",
"pikaday": "1.8.0", "pikaday": "1.8.0",
"progressbar.js": "1.0.1", "progressbar.js": "1.0.1",
"react": "16.7.0", "react": "16.8.6",
"react-dom": "16.7.0", "react-dom": "16.8.6",
"rxjs": "6.3.3", "rxjs": "6.5.1",
"slugify": "1.3.4", "slugify": "1.3.4",
"sortablejs": "1.8.1", "sortablejs": "1.9.0",
"tslib": "1.9.3", "tslib": "1.9.3",
"zone.js": "0.8.29" "zone.js": "0.9.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler": "7.2.3", "@angular/compiler": "7.2.14",
"@angular/compiler-cli": "7.2.3", "@angular/compiler-cli": "7.2.14",
"@ngtools/webpack": "7.3.0", "@ngtools/webpack": "7.3.8",
"@types/core-js": "2.5.0", "@types/core-js": "2.5.0",
"@types/jasmine": "3.3.8", "@types/jasmine": "3.3.12",
"@types/marked": "0.6.0", "@types/marked": "0.6.5",
"@types/mousetrap": "1.6", "@types/mousetrap": "1.6",
"@types/node": "10.12.21", "@types/node": "11.13.8",
"@types/react": "16.8.1", "@types/react": "16.8.14",
"@types/react-dom": "16.0.11", "@types/react-dom": "16.8.4",
"@types/sortablejs": "1.7.2", "@types/sortablejs": "1.7.2",
"angular-router-loader": "0.8.5", "angular-router-loader": "0.8.5",
"angular2-template-loader": "0.6.2", "angular2-template-loader": "0.6.2",
"awesome-typescript-loader": "5.2.1", "awesome-typescript-loader": "5.2.1",
"babel-core": "6.26.3", "babel-core": "6.26.3",
"codelyzer": "4.5.0", "codelyzer": "5.0.1",
"cpx": "1.5.0", "cpx": "1.5.0",
"css-loader": "2.1.0", "css-loader": "2.1.1",
"file-loader": "3.0.1", "file-loader": "3.0.1",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"ignore-loader": "0.1.2", "ignore-loader": "0.1.2",
"istanbul-instrumenter-loader": "3.0.1", "istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "3.3.0", "jasmine-core": "3.4.0",
"karma": "4.0.0", "karma": "4.1.0",
"karma-chrome-launcher": "2.2.0", "karma-chrome-launcher": "2.2.0",
"karma-cli": "2.0.0", "karma-cli": "2.0.0",
"karma-coverage-istanbul-reporter": "2.0.4", "karma-coverage-istanbul-reporter": "2.0.5",
"karma-htmlfile-reporter": "0.3.8", "karma-htmlfile-reporter": "0.3.8",
"karma-jasmine": "2.0.1", "karma-jasmine": "2.0.1",
"karma-jasmine-html-reporter": "1.4.0", "karma-jasmine-html-reporter": "1.4.2",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-sourcemap-loader": "0.3.7", "karma-sourcemap-loader": "0.3.7",
"karma-webpack": "3.0.5", "karma-webpack": "3.0.5",
"mini-css-extract-plugin": "0.5.0", "mini-css-extract-plugin": "0.6.0",
"node-sass": "4.11.0", "node-sass": "4.12.0",
"optimize-css-assets-webpack-plugin": "5.0.1", "optimize-css-assets-webpack-plugin": "5.0.1",
"raw-loader": "1.0.0", "raw-loader": "2.0.0",
"rimraf": "2.6.3", "rimraf": "2.6.3",
"rxjs-tslint": "0.1.6", "rxjs-tslint": "0.1.7",
"sass-lint": "1.12.1", "sass-lint": "1.13.1",
"sass-loader": "7.1.0", "sass-loader": "7.1.0",
"style-loader": "0.23.1", "style-loader": "0.23.1",
"ts-loader": "^5.4.4",
"tsconfig-paths-webpack-plugin": "3.2.0", "tsconfig-paths-webpack-plugin": "3.2.0",
"tslint": "5.12.1", "tslint": "5.16.0",
"tslint-webpack-plugin": "2.0.2", "tslint-webpack-plugin": "2.0.4",
"typemoq": "2.1.0", "typemoq": "2.1.0",
"typescript": "3.2.4", "typescript": "3.2.4",
"uglifyjs-webpack-plugin": "2.1.1", "uglifyjs-webpack-plugin": "2.1.2",
"underscore": "1.9.1", "underscore": "1.9.1",
"webpack": "4.29.0", "webpack": "4.30.0",
"webpack-cli": "3.2.1", "webpack-cli": "3.3.1",
"webpack-dev-server": "3.1.14", "webpack-dev-server": "3.3.1",
"webpack-merge": "4.2.1" "webpack-merge": "4.2.1"
} }
} }

8
src/Squidex/tsconfig.json

@ -21,8 +21,12 @@
] ]
} }
}, },
"awesomeTypescriptLoaderOptions": {
"useBabel": true,
"useCache": true,
"emitRequireType": false
},
"angularCompilerOptions": { "angularCompilerOptions": {
"fullTemplateTypeCheck": true, "fullTemplateTypeCheck": true
"strictInjectionParameters": true
} }
} }
Loading…
Cancel
Save