Browse Source

Tests for user management.

pull/271/head
Sebastian Stehle 9 years ago
parent
commit
cbb85c37e3
  1. 2
      src/Squidex/app-config/karma.coverage.conf.js
  2. 37
      src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts
  3. 4
      src/Squidex/app/features/administration/guards/unset-user.guard.ts
  4. 72
      src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts
  5. 4
      src/Squidex/app/features/administration/guards/user-must-exist.guard.ts
  6. 19
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  7. 4
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  8. 14
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  9. 4
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  10. 6
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  11. 23
      src/Squidex/app/features/administration/services/event-consumers.service.spec.ts
  12. 12
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  13. 40
      src/Squidex/app/features/administration/services/users.service.spec.ts
  14. 23
      src/Squidex/app/features/administration/services/users.service.ts
  15. 236
      src/Squidex/app/features/administration/state/users.state.spec.ts
  16. 36
      src/Squidex/app/features/administration/state/users.state.ts
  17. 6
      src/Squidex/app/framework/utils/immutable-array.spec.ts
  18. 4
      src/Squidex/app/framework/utils/immutable-array.ts
  19. 12
      src/Squidex/app/framework/utils/rxjs-extensions-impl.ts
  20. 19
      src/Squidex/app/framework/utils/rxjs-extensions.ts
  21. 2
      src/Squidex/app/shared/components/app-context.ts
  22. 107
      src/Squidex/app/shared/state/apps.state.spec.ts
  23. 69
      src/Squidex/app/shared/state/apps.state.ts
  24. 70
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  25. 32
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  26. 110
      src/Squidex/package-lock.json
  27. 5
      src/Squidex/package.json

2
src/Squidex/app-config/karma.coverage.conf.js

@ -70,7 +70,7 @@ module.exports = function (config) {
*
* available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
*/
browsers: ['PhantomJS']
browsers: ['ChromeHeadless']
};
config.set(_config);

37
src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts

@ -0,0 +1,37 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { UsersState } from './../state/users.state';
import { UnsetUserGuard } from './unset-user.guard';
describe('UnsetUserGuard', () => {
let usersState: IMock<UsersState>;
let userGuard: UnsetUserGuard;
beforeEach(() => {
usersState = Mock.ofType<UsersState>();
userGuard = new UnsetUserGuard(usersState.object);
});
it('should unset user', () => {
usersState.setup(x => x.selectUser(null))
.returns(() => Observable.of(null));
let result: boolean;
userGuard.canActivate().subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
usersState.verify(x => x.selectUser(null), Times.once());
});
});

4
src/Squidex/app/features/administration/guards/unset-user.guard.ts

@ -6,7 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs';
import { UsersState } from './../state/users.state';
@ -18,7 +18,7 @@ export class UnsetUserGuard implements CanActivate {
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
public canActivate(): Observable<boolean> {
return this.usersState.selectUser(null).map(u => u === null);
}
}

72
src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts

@ -0,0 +1,72 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { UserDto } from './../services/users.service';
import { UsersState } from './../state/users.state';
import { UserMustExistGuard } from './user-must-exist.guard';
describe('UserMustExistGuard', () => {
let usersState: IMock<UsersState>;
let router: IMock<Router>;
let userGuard: UserMustExistGuard;
beforeEach(() => {
router = Mock.ofType<Router>();
usersState = Mock.ofType<UsersState>();
userGuard = new UserMustExistGuard(usersState.object, router.object);
});
it('should load user and return true when found', () => {
usersState.setup(x => x.selectUser('123'))
.returns(() => Observable.of(<UserDto>{}));
let result: boolean;
const route: any = {
snapshot: {
params: {
userId: '123'
}
}
};
userGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
usersState.verify(x => x.selectUser('123'), Times.once());
});
it('should load user and return false when not found', () => {
usersState.setup(x => x.selectUser('123'))
.returns(() => Observable.of(null));
let result: boolean;
const route: any = {
snapshot: {
params: {
userId: '123'
}
}
};
userGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(['/404']), Times.once());
});
});

4
src/Squidex/app/features/administration/guards/user-must-exist.guard.ts

@ -6,7 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from '@app/framework';
@ -21,7 +21,7 @@ export class UserMustExistGuard implements CanActivate {
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
public canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const userId = allParams(route)['userId'];
const result =

19
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -69,7 +69,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
public start(consumer: EventConsumerDto) {
this.eventConsumersService.startEventConsumer(consumer.name)
.subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.start());
this.eventConsumers = this.eventConsumers.replaceBy('name', start(consumer));
}, error => {
this.dialogs.notifyError(error);
});
@ -78,7 +78,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
public stop(consumer: EventConsumerDto) {
this.eventConsumersService.stopEventConsumer(consumer.name)
.subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.stop());
this.eventConsumers = this.eventConsumers.replaceBy('name', stop(consumer));
}, error => {
this.dialogs.notifyError(error);
});
@ -87,7 +87,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
public reset(consumer: EventConsumerDto) {
this.eventConsumersService.resetEventConsumer(consumer.name)
.subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.reset());
this.eventConsumers = this.eventConsumers.replaceBy('name', reset(consumer));
}, error => {
this.dialogs.notifyError(error);
});
@ -99,3 +99,16 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
}
}
function start(es: EventConsumerDto) {
return new EventConsumerDto(es.name, false, false, es.error, es.position);
}
function stop(es: EventConsumerDto) {
return new EventConsumerDto(es.name, true, false, es.error, es.position);
}
function reset(es: EventConsumerDto) {
return new EventConsumerDto(es.name, es.isStopped, true, es.error, es.position);
}

4
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -5,7 +5,7 @@
<sqx-panel desiredWidth="26rem" isBlank="true">
<ng-container title>
<ng-container *ngIf="selectedUser | async; else noUser">
<ng-container *ngIf="usersState.selectedUser | async; else noUser">
Edit User
</ng-container>
@ -42,7 +42,7 @@
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" />
</div>
<div class="form-group form-group-password" [class.hidden]="isCurrentUser | async">
<div class="form-group form-group-password" [class.hidden]="usersState.isCurrentUser | async">
<div class="form-group">
<label for="password">Password</label>

14
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -24,18 +24,10 @@ export class UserPageComponent implements OnDestroy, OnInit {
public userForm: UserForm;
public selectedUser =
this.usersState.changes.map(x => x.selectedUser)
.distinctUntilChanged();
public isCurrentUser =
this.usersState.changes.map(x => x.isCurrentUser)
.distinctUntilChanged();
constructor(formBuilder: FormBuilder,
public readonly usersState: UsersState,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly usersState: UsersState
private readonly router: Router
) {
this.userForm = new UserForm(formBuilder);
}
@ -46,7 +38,7 @@ export class UserPageComponent implements OnDestroy, OnInit {
public ngOnInit() {
this.selectedUserSubscription =
this.usersState.changes.map(x => x.selectedUser)
this.usersState.selectedUser
.subscribe(user => {
this.user = user;
this.userForm.load(user);

4
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -49,7 +49,7 @@
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed">
<tbody>
<ng-template ngFor let-user [ngForOf]="users | async">
<ng-template ngFor let-user [ngForOf]="usersState.users | async">
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" />
@ -81,7 +81,7 @@
</div>
</div>
<sqx-pager [pager]="usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
<sqx-pager [pager]="usersState.usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
</ng-container>
</sqx-panel>

6
src/Squidex/app/features/administration/pages/users/users-page.component.ts

@ -22,12 +22,6 @@ import { UsersState } from './../../state/users.state';
export class UsersPageComponent implements OnInit {
public usersFilter = new FormControl();
public users =
this.usersState.changes.map(x => x.users);
public usersPager =
this.usersState.changes.map(x => x.usersPager);
constructor(
public readonly authState: AuthService,
public readonly usersState: UsersState

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

@ -12,29 +12,6 @@ import { ApiUrlConfig } from '@app/framework';
import { EventConsumerDto, EventConsumersService } from './event-consumers.service';
describe('EventConsumerDto', () => {
it('should update isStopped property when starting', () => {
const consumer_1 = new EventConsumerDto('consumer', true, false, 'error', 'position');
const consumer_2 = consumer_1.start();
expect(consumer_2.isStopped).toBeFalsy();
});
it('should update isStopped property when starting', () => {
const consumer_1 = new EventConsumerDto('consumer', false, false, 'error', 'position');
const consumer_2 = consumer_1.stop();
expect(consumer_2.isStopped).toBeTruthy();
});
it('should update isResetting property when resetting', () => {
const consumer_1 = new EventConsumerDto('consumer', false, false, 'error', 'position');
const consumer_2 = consumer_1.reset();
expect(consumer_2.isResetting).toBeTruthy();
});
});
describe('EventConsumersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({

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

@ -22,18 +22,6 @@ export class EventConsumerDto {
public readonly position: string
) {
}
public start(): EventConsumerDto {
return new EventConsumerDto(this.name, false, false, this.error, this.position);
}
public stop(): EventConsumerDto {
return new EventConsumerDto(this.name, true, false, this.error, this.position);
}
public reset(): EventConsumerDto {
return new EventConsumerDto(this.name, this.isStopped, true, this.error, this.position);
}
}
@Injectable()

40
src/Squidex/app/features/administration/services/users.service.spec.ts

@ -18,30 +18,6 @@ import {
UsersDto
} from './users.service';
describe('UserDto', () => {
it('should update email and display name property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true);
const user_2 = user_1.update('qaisar@squidex.io', 'Qaisar');
expect(user_2.email).toEqual('qaisar@squidex.io');
expect(user_2.displayName).toEqual('Qaisar');
});
it('should update isLocked property when locking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', false);
const user_2 = user_1.lock();
expect(user_2.isLocked).toBeTruthy();
});
it('should update isLocked property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true);
const user_2 = user_1.unlock();
expect(user_2.isLocked).toBeFalsy();
});
});
describe('UsersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
@ -80,14 +56,12 @@ describe('UsersService', () => {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
}
]
@ -95,8 +69,8 @@ describe('UsersService', () => {
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
]));
}));
@ -121,14 +95,12 @@ describe('UsersService', () => {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
}
]
@ -136,8 +108,8 @@ describe('UsersService', () => {
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
]));
}));
@ -163,7 +135,7 @@ describe('UsersService', () => {
isLocked: true
});
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true));
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', true));
}));
it('should make post request to create user',
@ -184,7 +156,7 @@ describe('UsersService', () => {
req.flush({ id: '123', pictureUrl: 'path/to/image1' });
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, 'path/to/image1', false));
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, false));
}));
it('should make put request to update user',

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

@ -26,22 +26,9 @@ export class UserDto {
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly pictureUrl: string | null,
public readonly isLocked: boolean
) {
}
public update(email: string, displayName: string): UserDto {
return new UserDto(this.id, email, displayName, this.pictureUrl, this.isLocked);
}
public lock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, true);
}
public unlock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, false);
}
}
export class CreateUserDto {
@ -57,7 +44,7 @@ export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
public readonly password?: string
) {
}
}
@ -84,7 +71,6 @@ export class UsersService {
item.id,
item.email,
item.displayName,
item.pictureUrl,
item.isLocked);
});
@ -104,7 +90,6 @@ export class UsersService {
body.id,
body.email,
body.displayName,
body.pictureUrl,
body.isLocked);
})
.pretifyError('Failed to load user. Please reload.');
@ -117,7 +102,11 @@ export class UsersService {
.map(response => {
const body = response.payload.body;
return new UserDto(body.id, dto.email, dto.displayName, body.pictureUrl, false);
return new UserDto(
body.id,
dto.email,
dto.displayName,
false);
})
.pretifyError('Failed to create user. Please reload.');
}

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

@ -0,0 +1,236 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService, DialogService } from '@app/shared';
import { UsersState } from './users.state';
import {
CreateUserDto,
UserDto,
UpdateUserDto,
UsersDto,
UsersService
} from './../services/users.service';
describe('UsersState', () => {
const oldUsers = [
new UserDto('id1', 'mail1@mail.de', 'name1', false),
new UserDto('id2', 'mail2@mail.de', 'name2', true)
];
const newUser = new UserDto('id3', 'mail3@mail.de', 'name3', false);
let authService: IMock<AuthService>;
let dialogService: IMock<DialogService>;
let usersService: IMock<UsersService>;
let usersState: UsersState;
beforeEach(() => {
authService = Mock.ofType<AuthService>();
authService.setup(x => x.user)
.returns(() => <any>{ id: 'id2' });
dialogService = Mock.ofType<DialogService>();
usersService = Mock.ofType<UsersService>();
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => Observable.of(new UsersDto(200, oldUsers)));
usersState = new UsersState(authService.object, dialogService.object, usersService.object);
usersState.loadUsers().subscribe();
});
it('should load apps', () => {
expect(usersState.snapshot.users.values).toEqual(oldUsers);
expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200);
usersService.verifyAll();
});
it('should raise notification on load when notify is true', () => {
usersState.loadUsers(true).subscribe();
dialogService.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should not load user when already loaded', () => {
let selectedUser: UserDto;
usersState.selectUser('id1').subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!).toEqual(oldUsers[0]);
expect(usersState.snapshot.selectedUser).toBe(oldUsers[0]);
usersService.verify(x => x.getUser(It.isAnyString()), Times.never());
});
it('should mark as current user when selected user equals to profile', () => {
usersState.selectUser('id2').subscribe();
expect(usersState.snapshot.isCurrentUser).toBeTruthy();
});
it('should load user when not loaded', () => {
usersService.setup(x => x.getUser('id3'))
.returns(() => Observable.of(newUser));
let selectedUser: UserDto;
usersState.selectUser('id3').subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!).toEqual(newUser);
expect(usersState.snapshot.selectedUser).toBe(newUser);
});
it('should return null when unselecting user', () => {
let selectedUser: UserDto;
usersState.selectUser(null).subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!).toBeNull();
expect(usersState.snapshot.selectedUser).toBeNull();
usersService.verify(x => x.getUser(It.isAnyString()), Times.never());
});
it('should return null when user to select is not found', () => {
usersService.setup(x => x.getUser('unknown'))
.returns(() => Observable.throw({}));
let selectedUser: UserDto;
usersState.selectUser('unknown').subscribe(x => {
selectedUser = x!;
}).unsubscribe();
expect(selectedUser!).toBeNull();
expect(usersState.snapshot.selectedUser).toBeNull();
});
it('should mark user as locked', () => {
usersService.setup(x => x.lockUser('id1'))
.returns(() => Observable.of({}));
usersState.selectUser('id1').subscribe();
usersState.lockUser(oldUsers[0]).subscribe();
expect(usersState.snapshot.users.at(0).isLocked).toBeTruthy();
expect(usersState.snapshot.selectedUser!.isLocked).toBeTruthy();
});
it('should raise notification when locking failed', () => {
usersService.setup(x => x.lockUser('id1'))
.returns(() => Observable.throw({}));
usersState.lockUser(oldUsers[0]).onErrorResumeNext().subscribe();
dialogService.verify(x => x.notifyError(It.isAny()), Times.once());
});
it('should unmark user as locked', () => {
usersService.setup(x => x.unlockUser('id2'))
.returns(() => Observable.of({}));
usersState.selectUser('id2').subscribe();
usersState.unlockUser(oldUsers[1]).subscribe();
expect(usersState.snapshot.users.at(1).isLocked).toBeFalsy();
expect(usersState.snapshot.selectedUser!.isLocked).toBeFalsy();
});
it('should raise notification when unlocking failed', () => {
usersService.setup(x => x.unlockUser('id2'))
.returns(() => Observable.throw({}));
usersState.unlockUser(oldUsers[1]).onErrorResumeNext().subscribe();
dialogService.verify(x => x.notifyError(It.isAny()), Times.once());
});
it('should update user on update', () => {
const request = new UpdateUserDto('new@mail.com', 'New');
usersService.setup(x => x.putUser('id1', request))
.returns(() => Observable.of({}));
usersState.selectUser('id1').subscribe();
usersState.updateUser(oldUsers[0], request).subscribe();
expect(usersState.snapshot.users.at(0).email).toEqual('new@mail.com');
expect(usersState.snapshot.users.at(0).displayName).toEqual('New');
expect(usersState.snapshot.selectedUser!.email).toEqual('new@mail.com');
expect(usersState.snapshot.selectedUser!.displayName).toEqual('New');
});
it('should not raise notification when updating failed', () => {
const request = new UpdateUserDto('new@mail.com', 'New');
usersService.setup(x => x.putUser('id1', request))
.returns(() => Observable.throw({}));
usersState.updateUser(oldUsers[0], request).onErrorResumeNext().subscribe();
dialogService.verify(x => x.notifyError(It.isAny()), Times.never());
});
it('should add user to state when created', () => {
const request = new CreateUserDto(newUser.email, newUser.displayName, 'password');
usersService.setup(x => x.postUser(request))
.returns(() => Observable.of(newUser));
usersState.createUser(request).subscribe();
expect(usersState.snapshot.users.at(0)).toBe(newUser);
expect(usersState.snapshot.usersPager.numberOfItems).toBe(201);
});
it('should not raise notification when creating failed', () => {
const request = new CreateUserDto(newUser.email, newUser.displayName, 'password');
usersService.setup(x => x.postUser(request))
.returns(() => Observable.throw({}));
usersState.createUser(request).onErrorResumeNext().subscribe();
dialogService.verify(x => x.notifyError(It.isAny()), Times.never());
});
it('should load next page and prev page when paging', () => {
usersService.setup(x => x.getUsers(10, 10, undefined))
.returns(() => Observable.of(new UsersDto(200, [])));
usersState.goNext().subscribe();
usersState.goPrev().subscribe();
usersService.verify(x => x.getUsers(10, 10, undefined), Times.once());
usersService.verify(x => x.getUsers(10, 0, undefined), Times.exactly(2));
});
it('should load with query when searching', () => {
usersService.setup(x => x.getUsers(10, 0, 'my-query'))
.returns(() => Observable.of(new UsersDto(0, [])));
usersState.search('my-query').subscribe();
expect(usersState.snapshot.usersQuery).toEqual('my-query');
usersService.verify(x => x.getUsers(10, 0, 'my-query'), Times.once());
});
});

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

@ -71,15 +71,33 @@ export class UserForm extends Form<FormGroup> {
}
interface Snapshot {
isCurrentUser?: boolean;
users: ImmutableArray<UserDto>;
usersPager: Pager;
usersQuery?: string;
selectedUser?: UserDto;
isCurrentUser?: boolean;
}
@Injectable()
export class UsersState extends State<Snapshot> {
public users =
this.changes.map(x => x.users)
.distinctUntilChanged();
public usersPager =
this.changes.map(x => x.usersPager)
.distinctUntilChanged();
public selectedUser =
this.changes.map(x => x.selectedUser)
.distinctUntilChanged();
public isCurrentUser =
this.changes.map(x => x.isCurrentUser)
.distinctUntilChanged();
constructor(
private readonly authState: AuthService,
private readonly dialogs: DialogService,
@ -93,7 +111,7 @@ export class UsersState extends State<Snapshot> {
.do(selectedUser => {
const isCurrentUser = id === this.authState.user!.id;
this.next(s => ({...s, selectedUser, isCurrentUser }));
this.next(s => ({ ...s, selectedUser, isCurrentUser }));
});
}
@ -125,7 +143,7 @@ export class UsersState extends State<Snapshot> {
return { ...s, users, usersPager, usersLoading: false };
});
})
.catch(error => this.dialogs.notifyError(error));
.notify(this.dialogs);
}
public createUser(request: CreateUserDto): Observable<UserDto> {
@ -140,7 +158,7 @@ export class UsersState extends State<Snapshot> {
});
}
public updateUser(user: UserDto, request: CreateUserDto): Observable<any> {
public updateUser(user: UserDto, request: UpdateUserDto): Observable<any> {
return this.usersService.putUser(user.id, request)
.do(() => {
this.dialogs.notifyInfo('User saved successsfull');
@ -154,7 +172,7 @@ export class UsersState extends State<Snapshot> {
.do(() => {
this.replaceUser(setLocked(user, true));
})
.catch(error => this.dialogs.notifyError(error));
.notify(this.dialogs);
}
public unlockUser(user: UserDto): Observable<any> {
@ -162,11 +180,11 @@ export class UsersState extends State<Snapshot> {
.do(() => {
this.replaceUser(setLocked(user, false));
})
.catch(error => this.dialogs.notifyError(error));
.notify(this.dialogs);
}
public search(query: string): Observable<any> {
this.next({ usersPager: new Pager(0), usersQuery: query });
this.next(s => ({ ...s, usersPager: new Pager(0), usersQuery: query }));
return this.loadUsers();
}
@ -195,7 +213,7 @@ export class UsersState extends State<Snapshot> {
const update = (user: UserDto, request: UpdateUserDto) =>
new UserDto(user.id, request.email, request.displayName, user.pictureUrl, user.isLocked);
new UserDto(user.id, request.email, request.displayName, user.isLocked);
const setLocked = (user: UserDto, locked: boolean) =>
new UserDto(user.id, user.email, user.displayName, user.pictureUrl, locked);
new UserDto(user.id, user.email, user.displayName, locked);

6
src/Squidex/app/framework/utils/immutable-array.spec.ts

@ -195,6 +195,12 @@ describe('ImmutableArray', () => {
expect(array_1.mutableValues).toBe(array_1.mutableValues);
});
it('should provider value at index', () => {
const array_1 = ImmutableArray.of([3, 1, 4, 2]);
expect(array_1.at(2)).toBe(4);
});
it('should iterate over array items', () => {
const array_1 = ImmutableArray.of([3, 1, 4, 2]);

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

@ -53,6 +53,10 @@ export class ImmutableArray<T> implements Iterable<T> {
}
}
public at(index: number) {
return this.values[index];
}
public map<R>(projection: (item: T) => R): ImmutableArray<R> {
return new ImmutableArray<R>(freeze(this.items.map(v => projection(v!))));
}

12
src/Squidex/app/framework/utils/rxjs-extensions-impl.ts

@ -5,6 +5,18 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { DialogService } from './../services/dialog.service';
export function nextBy<T>(updater: (value: T) => T): void {
return this.next(updater(this.value));
}
export function notify(dialogs: DialogService) {
return this.catch((error: any) => {
dialogs.notifyError(error);
return Observable.throw(error);
});
}

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

@ -6,15 +6,26 @@
*/
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { nextBy } from './rxjs-extensions-impl';
import { DialogService } from './../services/dialog.service';
import { nextBy, notify } from './rxjs-extensions-impl';
/* tslint:disable:no-shadowed-variable */
declare module 'rxjs/BehaviorSubject' {
interface BehaviorSubject<T> {
nextBy(updater: (value: T) => T): void;
interface Observable<T> {
notify(dialogs: DialogService): Observable<T>;
}
}
BehaviorSubject.prototype['nextBy'] = nextBy;
declare module 'rxjs/Observable' {
interface Observable<T> {
notify(dialogs: DialogService): Observable<T>;
}
}
BehaviorSubject.prototype['nextBy'] = nextBy;
Observable.prototype['notify'] = notify;

2
src/Squidex/app/shared/components/app-context.ts

@ -23,7 +23,7 @@ import {
@Injectable()
export class AppContext {
public get app(): AppDto {
return this.appsState.selectedApp.value!;
return this.appsState.snapshot.selectedApp!;
}
public get appChanges(): Observable<AppDto | null> {

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

@ -13,8 +13,7 @@ import {
AppsService,
AppsState,
CreateAppDto,
DateTime,
ImmutableArray
DateTime
} from './../';
describe('AppsState', () => {
@ -27,6 +26,7 @@ describe('AppsState', () => {
const newApp = new AppDto('id3', 'new-name', 'Owner', now, now, 'Free', 'Plan');
let appsService: IMock<AppsService>;
let appsState: AppsState;
beforeEach(() => {
appsService = Mock.ofType(AppsService);
@ -34,57 +34,32 @@ describe('AppsState', () => {
appsService.setup(x => x.getApps())
.returns(() => Observable.of(oldApps))
.verifiable(Times.once());
});
it('should load automatically', () => {
const store = new AppsState(appsService.object);
let result1: ImmutableArray<AppDto>;
let result2: ImmutableArray<AppDto>;
store.apps.subscribe(x => {
result1 = x;
}).unsubscribe();
store.apps.subscribe(x => {
result2 = x;
}).unsubscribe();
appsState = new AppsState(appsService.object);
appsState.loadApps().subscribe();
});
expect(result1!.values).toEqual(oldApps);
expect(result2!.values).toEqual(oldApps);
it('should load apps', () => {
expect(appsState.snapshot.apps.values).toEqual(oldApps);
appsService.verifyAll();
});
it('should add app to cache when created', () => {
it('should add app to state when created', () => {
const request = new CreateAppDto(newApp.name);
appsService.setup(x => x.postApp(request))
.returns(() => Observable.of(newApp))
.verifiable(Times.once());
const store = new AppsState(appsService.object);
let result1: ImmutableArray<AppDto>;
let result2: ImmutableArray<AppDto>;
store.apps.subscribe(x => {
result1 = x;
}).unsubscribe();
store.createApp(request, now).subscribe();
store.apps.subscribe(x => {
result2 = x;
}).unsubscribe();
appsState.createApp(request, now).subscribe();
expect(result1!.values).toEqual(oldApps);
expect(result2!.values).toEqual(oldApps.concat([newApp]));
expect(appsState.snapshot.apps.values).toEqual([newApp, ...oldApps]);
appsService.verifyAll();
});
it('should remove app from cache when archived', () => {
it('should remove app from state when archived', () => {
const request = new CreateAppDto(newApp.name);
appsService.setup(x => x.postApp(request))
@ -95,48 +70,50 @@ describe('AppsState', () => {
.returns(() => Observable.of({}))
.verifiable(Times.once());
const store = new AppsState(appsService.object);
appsState.createApp(request, now).subscribe();
let result1: ImmutableArray<AppDto>;
let result2: ImmutableArray<AppDto>;
let result3: ImmutableArray<AppDto>;
const appsAfterCreate = appsState.snapshot.apps.values;
store.apps.subscribe(x => {
result1 = x;
}).unsubscribe();
appsState.deleteApp(newApp.name).subscribe();
store.createApp(request, now).subscribe();
const appsAfterDelete = appsState.snapshot.apps.values;
store.apps.subscribe(x => {
result2 = x;
}).unsubscribe();
expect(appsAfterCreate).toEqual([newApp, ...oldApps]);
expect(appsAfterDelete).toEqual(oldApps);
store.deleteApp(newApp.name).subscribe();
appsService.verifyAll();
});
store.apps.subscribe(x => {
result3 = x;
}).unsubscribe();
it('should select app', () => {
let selectedApp: AppDto;
expect(result1!.values).toEqual(oldApps);
expect(result2!.values).toEqual(oldApps.concat([newApp]));
expect(result3!.values).toEqual(oldApps);
appsState.selectApp(oldApps[0].name).subscribe(x => {
selectedApp = x!;
}).unsubscribe();
appsService.verifyAll();
expect(selectedApp!).toBe(oldApps[0]);
expect(appsState.snapshot.selectedApp).toBe(oldApps[0]);
});
it('should select app', (done) => {
const store = new AppsState(appsService.object);
it('should return null when unselecting app', () => {
let selectedApp: AppDto;
appsState.selectApp(null).subscribe(x => {
selectedApp = x!;
}).unsubscribe();
store.selectApp(oldApps[0].name).subscribe(isSelected => {
expect(isSelected).toBeTruthy();
expect(selectedApp!).toBeNull();
expect(appsState.snapshot.selectedApp).toBeNull();
});
appsService.verifyAll();
it('should return null when app to select is not found', () => {
let selectedApp: AppDto;
done();
}, err => {
expect(err).toBeNull();
appsState.selectApp('unknown').subscribe(x => {
selectedApp = x!;
}).unsubscribe();
done();
});
expect(selectedApp!).toBeNull();
expect(appsState.snapshot.selectedApp).toBeNull();
});
});

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

@ -9,9 +9,13 @@
import '@app/framework/utils/rxjs-extensions';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { DateTime, ImmutableArray } from '@app/framework';
import {
DateTime,
ImmutableArray,
State
} from '@app/framework';
import {
AppDto,
@ -19,51 +23,70 @@ import {
CreateAppDto
} from './../services/apps.service';
@Injectable()
export class AppsState {
public apps = new BehaviorSubject<ImmutableArray<AppDto>>(ImmutableArray.empty());
interface Snapshot {
apps: ImmutableArray<AppDto>;
public selectedApp = new BehaviorSubject<AppDto | null>(null);
selectedApp: AppDto | null;
}
@Injectable()
export class AppsState extends State<Snapshot> {
public get appName() {
return this.selectedApp.value!.name;
return this.snapshot.selectedApp!.name;
}
public selectedApp =
this.changes.map(s => s.selectedApp);
public apps =
this.changes.map(s => s.apps);
constructor(
private readonly appsService: AppsService
) {
}
public loadApps(): Observable<any> {
return this.appsService.getApps()
.do(dtos => {
this.apps.nextBy(v => ImmutableArray.of(dtos));
});
super({ apps: ImmutableArray.empty(), selectedApp: null });
}
public selectApp(name: string | null): Observable<AppDto | null> {
const observable =
!name ?
Observable.of(null) :
Observable.of(this.apps.value.find(x => x.name === name) || null);
Observable.of(null) :
Observable.of(this.snapshot.apps.find(x => x.name === name) || null);
return observable
.do(app => {
this.selectedApp.next(app);
.do(selectedApp => {
this.next(s => {
return { ...s, selectedApp };
});
});
}
public createApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
return this.appsService.postApp(dto)
.do(app => {
this.apps.nextBy(v => v.push(app));
public loadApps(): Observable<any> {
return this.appsService.getApps()
.do(dtos => {
this.next(s => {
return { ...s, apps: ImmutableArray.of(dtos) };
});
});
}
public createApp(request: CreateAppDto, now?: DateTime): Observable<AppDto> {
return this.appsService.postApp(request)
.do(dto => {
this.next(s => {
return { ...s, apps: s.apps.push(dto).sortByStringAsc(x => x.name) };
});
});
}
public deleteApp(name: string): Observable<any> {
return this.appsService.deleteApp(name)
.do(app => {
this.apps.nextBy(v => v.filter(a => a.name !== name));
this.next(s => {
const selectedApp = s.selectedApp && s.selectedApp.name === name ? null : s.selectedApp;
return { apps: s.apps.filter(x => x.name !== name), selectedApp };
});
});
}
}

70
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -1,37 +1,51 @@
<ul class="nav navbar-nav" *ngIf="apps">
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" id="app-name" (click)="appsMenu.toggle()">{{selectedApp ? selectedApp.name : 'Apps Overview'}}</span>
<div class="dropdown-menu" *sqxModalView="appsMenu" @fade>
<a class="dropdown-item all-apps" routerLink="/app">
<span class="all-apps-text">All Apps</span>
<span class="all-apps-pill badge badge-pill badge-primary">{{apps.length}}</span>
</a>
<div class="dropdown-divider"></div>
<div *ngIf="apps.length > 0">
<a class="dropdown-item" *ngFor="let app of apps" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a>
<span class="nav-link dropdown-toggle" id="app-name" (click)="appsMenu.toggle()">
<ng-container *ngIf="appsState | selectedApp | async; let apps; else noApp">
{{selectedApp.name}}
</ng-container>
<ng-template #noApp>
Apps Overview
</ng-template>
</span>
<ng-container *ngIf="appsState.apps | async; let apps">
<div class="dropdown-menu" *sqxModalView="appsMenu" @fade>
<a class="dropdown-item all-apps" routerLink="/app">
<span class="all-apps-text">All Apps</span>
<span class="all-apps-pill badge badge-pill badge-primary">{{apps.length}}</span>
</a>
<div class="dropdown-divider"></div>
<div *ngIf="apps.length > 0">
<a class="dropdown-item" *ngFor="let app of apps" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a>
<div class="dropdown-divider"></div>
</div>
<div class="drodown-button">
<button class="btn btn-block btn-success" (click)="createApp()">
<i class="icon-plus"></i> Create New App
</button>
</div>
</div>
<div class="drodown-button">
<button class="btn btn-block btn-success" (click)="createApp()"><i class="icon-plus"></i> Create New App</button>
</div>
</div>
</ng-container>
</li>
<li class="nav-item" *ngIf="selectedApp && selectedApp.planUpgrade && selectedApp.planName && selectedApp.permission === 'Owner'">
<div class="btn-group app-upgrade">
<button type="button" class="btn btn-primary btn-plan">
You are on the <strong>{{selectedApp.planName}}</strong> plan.
</button>
<button type="button" class="btn btn-warning" [routerLink]="['/app', selectedApp.name, 'settings', 'plans']">
Upgrade!
</button>
</div>
</li>
<ng-container *ngIf="appsState.selectedApp | async; let selectedApp">
<li class="nav-item" *ngIf="selectedApp.planUpgrade && selectedApp.planName && selectedApp.permission === 'Owner'">
<div class="btn-group app-upgrade">
<button type="button" class="btn btn-primary btn-plan">
You are on the <strong>{{selectedApp.planName}}</strong> plan.
</button>
<button type="button" class="btn btn-warning" [routerLink]="['/app', selectedApp.name, 'settings', 'plans']">
Upgrade!
</button>
</div>
</li>
</ng-container>
</ul>
<div class="modal" *sqxModalView="addAppDialog;onRoot:true" @fade>

32
src/Squidex/app/shell/pages/internal/apps-menu.component.ts

@ -5,13 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { Component } from '@angular/core';
import {
AppDto,
AppsState,
ImmutableArray,
fadeAnimation,
ModalView
} from '@app/shared';
@ -24,39 +21,16 @@ import {
fadeAnimation
]
})
export class AppsMenuComponent implements OnDestroy, OnInit {
private appsSubscription: Subscription;
private appSubscription: Subscription;
export class AppsMenuComponent {
public addAppDialog = new ModalView();
public appsMenu = new ModalView(false, true);
public apps = ImmutableArray.of<AppDto>([]);
public selectedApp: AppDto | null;
constructor(
private readonly appsStore: AppsState
public readonly appsState: AppsState
) {
}
public ngOnDestroy() {
this.appsSubscription.unsubscribe();
this.appSubscription.unsubscribe();
}
public ngOnInit() {
this.appsSubscription =
this.appsStore.apps.subscribe(apps => {
this.apps = apps;
});
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
this.selectedApp = app;
});
}
public createApp() {
this.appsMenu.hide();
this.addAppDialog.show();

110
src/Squidex/package-lock.json

@ -608,8 +608,7 @@
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=",
"dev": true
"integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg="
},
"asynckit": {
"version": "0.4.0",
@ -2135,8 +2134,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cpx": {
"version": "1.5.0",
@ -2451,7 +2449,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
"dev": true,
"requires": {
"ms": "2.0.0"
}
@ -2995,8 +2992,15 @@
"es6-promise": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.2.tgz",
"integrity": "sha1-9yLXdpr4i9M7wT7GYF4fkpZrgtk=",
"dev": true
"integrity": "sha1-9yLXdpr4i9M7wT7GYF4fkpZrgtk="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "4.2.2"
}
},
"es6-set": {
"version": "0.1.5",
@ -3528,7 +3532,6 @@
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz",
"integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=",
"dev": true,
"requires": {
"concat-stream": "1.6.0",
"debug": "2.6.9",
@ -3540,7 +3543,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
"integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
"dev": true,
"requires": {
"inherits": "2.0.3",
"readable-stream": "2.3.3",
@ -3550,14 +3552,12 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mkdirp": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
"integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
@ -3628,7 +3628,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
"dev": true,
"requires": {
"pend": "1.2.0"
}
@ -6208,8 +6207,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isbinaryfile": {
"version": "3.0.2",
@ -7442,8 +7440,7 @@
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=",
"dev": true
"integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE="
},
"mime-db": {
"version": "1.30.0",
@ -7584,8 +7581,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multicast-dns": {
"version": "6.2.2",
@ -8719,8 +8715,7 @@
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
"dev": true
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"performance-now": {
"version": "2.1.0",
@ -9453,8 +9448,7 @@
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
"dev": true
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"progress": {
"version": "1.1.8",
@ -9515,6 +9509,11 @@
"socks-proxy-agent": "2.1.1"
}
},
"proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
"integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4="
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -9546,6 +9545,55 @@
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"puppeteer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.2.0.tgz",
"integrity": "sha512-4sY/6mB7+kNPGAzPGKq65tH0VG3ohUEkXHuOReB9K/tw3m1TqifYmxnMR/uDeci/UPwyk5K1gWYh8rw0U0Zscw==",
"requires": {
"debug": "2.6.9",
"extract-zip": "1.6.6",
"https-proxy-agent": "2.2.1",
"mime": "1.6.0",
"progress": "2.0.0",
"proxy-from-env": "1.0.0",
"rimraf": "2.6.2",
"ws": "3.3.3"
},
"dependencies": {
"agent-base": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz",
"integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==",
"requires": {
"es6-promisify": "5.0.0"
}
},
"https-proxy-agent": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
"requires": {
"agent-base": "4.2.0",
"debug": "3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"progress": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
"integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8="
}
}
},
"q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@ -9732,7 +9780,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
"integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
@ -10154,8 +10201,7 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=",
"dev": true
"integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM="
},
"sass-graph": {
"version": "2.2.4",
@ -11163,7 +11209,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
@ -11846,8 +11891,7 @@
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typemoq": {
"version": "2.1.0",
@ -11936,8 +11980,7 @@
"ultron": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=",
"dev": true
"integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw="
},
"umd": {
"version": "3.0.1",
@ -12236,8 +12279,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utila": {
"version": "0.4.0",
@ -13107,7 +13149,6 @@
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=",
"dev": true,
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
@ -13206,7 +13247,6 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
"integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
"dev": true,
"requires": {
"fd-slicer": "1.0.1"
}

5
src/Squidex/package.json

@ -38,8 +38,8 @@
"progressbar.js": "1.0.1",
"react": "16.2.0",
"react-dom": "16.2.0",
"slugify": "1.2.9",
"rxjs": "5.5.6",
"slugify": "1.2.9",
"zone.js": "0.8.20"
},
"devDependencies": {
@ -74,13 +74,12 @@
"karma-jasmine": "1.1.1",
"karma-jasmine-html-reporter": "0.2.2",
"karma-mocha-reporter": "2.2.5",
"karma-phantomjs-launcher": "1.0.4",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "2.0.9",
"node-sass": "4.7.2",
"noop-loader": "1.0.0",
"null-loader": "0.1.1",
"phantomjs-prebuilt": "2.1.16",
"puppeteer": "^1.2.0",
"raw-loader": "0.5.1",
"rimraf": "2.6.2",
"sass-lint": "1.12.1",

Loading…
Cancel
Save