Browse Source

Some progress

pull/271/head
Sebastian Stehle 9 years ago
parent
commit
4facb28893
  1. 10
      src/Squidex/app/features/administration/declarations.ts
  2. 87
      src/Squidex/app/features/administration/guards/resolve-user.guard.spec.ts
  3. 24
      src/Squidex/app/features/administration/guards/unset-user.guard.ts
  4. 48
      src/Squidex/app/features/administration/guards/user-must-exist.guard.ts
  5. 24
      src/Squidex/app/features/administration/module.ts
  6. 2
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  7. 72
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  8. 30
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  9. 81
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  10. 130
      src/Squidex/app/features/administration/services/event-consumers.service.spec.ts
  11. 88
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  12. 230
      src/Squidex/app/features/administration/services/users.service.spec.ts
  13. 145
      src/Squidex/app/features/administration/services/users.service.ts
  14. 138
      src/Squidex/app/features/administration/state/users.state.ts
  15. 10
      src/Squidex/app/framework/utils/rxjs-extensions-impl.ts
  16. 20
      src/Squidex/app/framework/utils/rxjs-extensions.ts

10
src/Squidex/app/features/administration/declarations.ts

@ -5,8 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export * from './administration-area.component';
export * from './guards/user-must-exist.guard';
export * from './guards/unset-user.guard';
export * from './pages/event-consumers/event-consumers-page.component';
export * from './pages/users/user-page.component';
export * from './pages/users/users-page.component';
export * from './administration-area.component';
export * from './services/event-consumers.service';
export * from './services/users.service';
export * from './state/users.state';

87
src/Squidex/app/features/administration/guards/resolve-user.guard.spec.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/*
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { UserManagementService } from 'shared';
import { ResolveUserGuard } from './resolve-user.guard';
import { RouterMockup } from './router-mockup';
describe('ResolveUserGuard', () => {
const route = {
params: {},
parent: {
params: {
userId: 'my-user'
}
}
};
let usersService: IMock<UserManagementService>;
beforeEach(() => {
usersService = Mock.ofType(UserManagementService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolveUserGuard(usersService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain user id.');
});
it('should navigate to 404 page if user is not found', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should navigate to 404 page if user loading fails', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should return user if loading succeeded', (done) => {
const user: any = {};
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(user));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBe(user);
done();
});
});
});
*/

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

@ -0,0 +1,24 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { UsersState } from './../state/users.state';
@Injectable()
export class UnsetUserGuard implements CanActivate {
constructor(
private readonly usersState: UsersState
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.usersState.selectUser(null).map(r => !r);
}
}

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

@ -0,0 +1,48 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
import { UsersState } from './../state/users.state';
@Injectable()
export class UserMustExistGuard implements CanActivate {
constructor(
private readonly usersState: UsersState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = allParams(route);
const userId = params['userId'];
if (!userId) {
throw 'Route must contain user id.';
}
const result =
this.usersState.selectUser(userId).map(u => !!u)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.catch(error => {
this.router.navigate(['/404']);
return Observable.of(false);
});
return result;
}
}

24
src/Squidex/app/features/administration/module.ts

@ -9,16 +9,19 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
ResolveUserGuard,
SqxFrameworkModule,
SqxSharedModule
SqxFrameworkModule
} from 'shared';
import {
AdministrationAreaComponent,
EventConsumersPageComponent,
EventConsumersService,
UnsetUserGuard,
UserPageComponent,
UsersPageComponent
UserMustExistGuard,
UsersPageComponent,
UsersService,
UsersState
} from './declarations';
const routes: Routes = [
@ -39,14 +42,13 @@ const routes: Routes = [
children: [
{
path: 'new',
component: UserPageComponent
component: UserPageComponent,
canActivate: [UnsetUserGuard]
},
{
path: ':userId',
component: UserPageComponent,
resolve: {
user: ResolveUserGuard
}
canActivate: [UserMustExistGuard]
}
]
}
@ -59,7 +61,6 @@ const routes: Routes = [
@NgModule({
imports: [
SqxFrameworkModule,
SqxSharedModule,
RouterModule.forChild(routes)
],
declarations: [
@ -67,6 +68,11 @@ const routes: Routes = [
EventConsumersPageComponent,
UserPageComponent,
UsersPageComponent
],
providers: [
EventConsumersService,
UsersService,
UsersState
]
})
export class SqxFeatureAdministrationModule { }

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

@ -50,7 +50,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]="user.id === ctx.userId">
<div class="form-group form-group-password" [class.hidden]="isCurrentUser">
<div class="form-group">
<label for="password">Password</label>

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

@ -5,18 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import {
AppContext,
UserDto,
UserManagementService,
ValidatorsEx
} from 'shared';
import { UserCreated, UserUpdated } from './../messages';
import { UsersState } from './../../state/users.state';
@Component({
selector: 'sqx-user-page',
@ -26,8 +25,8 @@ import { UserCreated, UserUpdated } from './../messages';
AppContext
]
})
export class UserPageComponent implements OnInit {
public user: UserDto;
export class UserPageComponent implements OnDestroy, OnInit {
private selectedUserSubscription: Subscription;
public userFormSubmitted = false;
public userForm: FormGroup;
@ -38,18 +37,17 @@ export class UserPageComponent implements OnInit {
constructor(public readonly ctx: AppContext,
private readonly formBuilder: FormBuilder,
private readonly router: Router,
private readonly userManagementService: UserManagementService
private readonly state: UsersState
) {
}
public ngOnInit() {
this.ctx.route.data.map(d => d.user)
.subscribe((user: UserDto) => {
this.user = user;
public ngOnDestroy() {
this.selectedUserSubscription.unsubscribe();
}
this.setupAndPopulateForm();
});
public ngOnInit() {
this.selectedUserSubscription =
this.state.selectedUser.subscribe(user => this.setupAndPopulateForm(user!));
}
public save() {
@ -61,34 +59,19 @@ export class UserPageComponent implements OnInit {
const requestDto = this.userForm.value;
if (this.isNewMode) {
this.userManagementService.postUser(requestDto)
.subscribe(created => {
this.user =
new UserDto(
created.id,
requestDto.email,
requestDto.displayName,
created.pictureUrl!,
false);
this.state.createUser(requestDto)
.subscribe(() => {
this.ctx.notifyInfo('User created successfully.');
this.emitUserCreated(this.user);
this.back();
this.resetUserForm();
}, error => {
this.resetUserForm(error.displayMessage);
});
} else {
this.userManagementService.putUser(this.user.id, requestDto)
this.state.updateUser(requestDto)
.subscribe(() => {
this.user =
this.user.update(
requestDto.email,
requestDto.displayMessage);
this.ctx.notifyInfo('User saved successfully.');
this.emitUserUpdated(this.user);
this.resetUserForm();
}, error => {
this.resetUserForm(error.displayMessage);
@ -97,31 +80,21 @@ export class UserPageComponent implements OnInit {
}
}
private back() {
this.router.navigate(['../'], { relativeTo: this.ctx.route, replaceUrl: true });
}
private emitUserCreated(user: UserDto) {
this.ctx.bus.emit(new UserCreated(user));
}
private emitUserUpdated(user: UserDto) {
this.ctx.bus.emit(new UserUpdated(user));
}
private setupAndPopulateForm(user: UserDto | null) {
const userData: any = user || {};
private setupAndPopulateForm() {
const input = this.user || {};
this.isNewMode = !user;
this.isCurrentUser = user !== null && user.id === this.ctx.userId;
this.isNewMode = !this.user;
this.userForm =
this.formBuilder.group({
email: [input['email'],
email: [userData.email,
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]],
displayName: [input['displayName'],
displayName: [userData.displayName,
[
Validators.required,
Validators.maxLength(100)
@ -136,7 +109,6 @@ export class UserPageComponent implements OnInit {
]]
});
this.isCurrentUser = this.user && this.user.id === this.ctx.userId;
this.resetUserForm();
}

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

@ -10,13 +10,13 @@
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form>
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + M)">
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
<i class="icon-plus"></i> New
</button>
</div>
@ -56,7 +56,7 @@
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed">
<tbody>
<ng-template ngFor let-user [ngForOf]="usersItems">
<ng-template ngFor let-user [ngForOf]="state.userItems | async" [ngForTrackBy]="state.trackBy">
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" />
@ -88,18 +88,20 @@
</div>
</div>
<div class="grid-footer clearfix" *ngIf="usersPager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{usersPager.itemFirst}}-{{usersPager.itemLast}} of {{usersPager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
<ng-container *ngIf="state.usersPager | async as pager">
<div class="grid-footer clearfix" *ngIf="pager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="state.goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="state.goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
</div>
</ng-container>
</div>
</div>
</sqx-panel>

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

@ -5,19 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AppContext,
ImmutableArray,
Pager,
UserDto,
UserManagementService
} from 'shared';
import { AppContext, UserDto } from 'shared';
import { UserCreated, UserUpdated } from './../messages';
import { UsersState } from './../../state/users.state';
@Component({
selector: 'sqx-users-page',
@ -27,91 +20,37 @@ import { UserCreated, UserUpdated } from './../messages';
AppContext
]
})
export class UsersPageComponent implements OnDestroy, OnInit {
private userCreatedSubscription: Subscription;
private userUpdatedSubscription: Subscription;
public usersItems = ImmutableArray.empty<UserDto>();
public usersPager = new Pager(0);
export class UsersPageComponent implements OnInit {
public usersFilter = new FormControl();
public usersQuery = '';
constructor(public readonly ctx: AppContext,
private readonly userManagementService: UserManagementService
public readonly state: UsersState
) {
}
public ngOnDestroy() {
this.userCreatedSubscription.unsubscribe();
this.userUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.userCreatedSubscription =
this.ctx.bus.of(UserCreated)
.subscribe(message => {
this.usersItems = this.usersItems.pushFront(message.user);
this.usersPager = this.usersPager.incrementCount();
});
this.userUpdatedSubscription =
this.ctx.bus.of(UserUpdated)
.subscribe(message => {
this.usersItems = this.usersItems.replaceBy('id', message.user);
});
this.load();
}
public search() {
this.usersPager = new Pager(0);
this.usersQuery = this.usersFilter.value;
this.load();
this.state.search(this.usersFilter.value);
}
public load(showInfo = false) {
this.userManagementService.getUsers(this.usersPager.pageSize, this.usersPager.skip, this.usersQuery)
.subscribe(dtos => {
this.usersItems = ImmutableArray.of(dtos.items);
this.usersPager = this.usersPager.setCount(dtos.total);
this.state.loadUsers()
.subscribe(() => {
if (showInfo) {
this.ctx.notifyInfo('Users reloaded.');
}
}, error => {
this.ctx.notifyError(error);
});
}
public lock(user: UserDto) {
this.userManagementService.lockUser(user.id)
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.lock());
}, error => {
this.ctx.notifyError(error);
});
this.state.lockUser(user.id).subscribe();
}
public unlock(user: UserDto) {
this.userManagementService.unlockUser(user.id)
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.unlock());
}, error => {
this.ctx.notifyError(error);
});
}
public goNext() {
this.usersPager = this.usersPager.goNext();
this.load();
}
public goPrev() {
this.usersPager = this.usersPager.goPrev();
this.load();
this.state.unlockUser(user.id).subscribe();
}
}

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

@ -0,0 +1,130 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig } from '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({
imports: [
HttpClientTestingModule
],
providers: [
EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get event consumers',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
let eventConsumers: EventConsumerDto[] | null = null;
eventConsumersService.getEventConsumers().subscribe(result => {
eventConsumers = result;
});
const req = httpMock.expectOne('http://service/p/api/event-consumers');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([
{
name: 'event-consumer1',
position: '13',
isStopped: true,
isResetting: true,
error: 'an error 1'
},
{
name: 'event-consumer2',
position: '29',
isStopped: true,
isResetting: true,
error: 'an error 2'
}
]);
expect(eventConsumers).toEqual([
new EventConsumerDto('event-consumer1', true, true, 'an error 1', '13'),
new EventConsumerDto('event-consumer2', true, true, 'an error 2', '29')
]);
}));
it('should make put request to start event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
eventConsumersService.startEventConsumer('event-consumer1').subscribe();
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/start');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to stop event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
eventConsumersService.stopEventConsumer('event-consumer1').subscribe();
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/stop');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to reset event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
eventConsumersService.resetEventConsumer('event-consumer1').subscribe();
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/reset');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
});

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

@ -0,0 +1,88 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import { ApiUrlConfig, HTTP } from 'framework';
export class EventConsumerDto {
constructor(
public readonly name: string,
public readonly isStopped: boolean,
public readonly isResetting: boolean,
public readonly error: string,
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()
export class EventConsumersService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getEventConsumers(): Observable<EventConsumerDto[]> {
const url = this.apiUrl.buildUrl('/api/event-consumers');
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
const items: any[] = body;
return items.map(item => {
return new EventConsumerDto(
item.name,
item.isStopped,
item.isResetting,
item.error,
item.position);
});
})
.pretifyError('Failed to load event consumers. Please reload.');
}
public startEventConsumer(name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/start`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to start event consumer. Please reload.');
}
public stopEventConsumer(name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/stop`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to stop event consumer. Please reload.');
}
public resetEventConsumer(name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/reset`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to reset event consumer. Please reload.');
}
}

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

@ -0,0 +1,230 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig } from 'framework';
import {
CreateUserDto,
UpdateUserDto,
UserDto,
UsersService,
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({
imports: [
HttpClientTestingModule
],
providers: [
UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30).subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{
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
}
]
});
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)
]));
}));
it('should make get request with query to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{
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
}
]
});
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)
]));
}));
it('should make get request to get single user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto | null = null;
userManagementService.getUser('123').subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
});
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true));
}));
it('should make post request to create user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new CreateUserDto('mail@squidex.io', 'Squidex User', 'password');
let user: UserDto | null = null;
userManagementService.postUser(dto).subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '123', pictureUrl: 'path/to/image1' });
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, 'path/to/image1', false));
}));
it('should make put request to update user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new UpdateUserDto('mail@squidex.io', 'Squidex User', 'password');
userManagementService.putUser('123', dto).subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to lock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.lockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/lock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to unlock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.unlockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
});

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

@ -0,0 +1,145 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import { ApiUrlConfig, HTTP } from 'framework';
export class UsersDto {
constructor(
public readonly total: number,
public readonly items: UserDto[]
) {
}
}
export class UserDto {
constructor(
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 {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
@Injectable()
export class UsersService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
const items: any[] = body.items;
const users = items.map(item => {
return new UserDto(
item.id,
item.email,
item.displayName,
item.pictureUrl,
item.isLocked);
});
return new UsersDto(body.total, users);
})
.pretifyError('Failed to load users. Please reload.');
}
public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
return new UserDto(
body.id,
body.email,
body.displayName,
body.pictureUrl,
body.isLocked);
})
.pretifyError('Failed to load user. Please reload.');
}
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl('api/user-management');
return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => {
const body = response.payload.body;
return new UserDto(body.id, dto.email, dto.displayName, body.pictureUrl, false);
})
.pretifyError('Failed to create user. Please reload.');
}
public putUser(id: string, dto: UpdateUserDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.putVersioned(this.http, url, dto)
.pretifyError('Failed to update user. Please reload.');
}
public lockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
public unlockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/unlock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
}

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

@ -0,0 +1,138 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import 'framework/utils/rxjs-extensions';
import {
DialogService,
ErrorDto,
ImmutableArray,
Notification,
Pager
} from 'shared';
import {
CreateUserDto,
UserDto,
UsersService
} from './../services/users.service';
@Injectable()
export class UsersState {
public usersItems = new BehaviorSubject<ImmutableArray<UserDto>>(ImmutableArray.empty());
public usersPager = new BehaviorSubject(new Pager(0));
public usersQuery = new BehaviorSubject('');
public selectedUser = new BehaviorSubject<UserDto | null>(null);
constructor(
private readonly usersService: UsersService,
private readonly dialogs: DialogService
) {
}
public selectUser(id: string | null): Observable<boolean> {
const observable =
!id ?
Observable.of(null) :
Observable.of(this.usersItems.value.find(x => x.id === id))
.switchMap(user => {
if (!user) {
return this.usersService.getUser(id).catch(() => Observable.of(null));
} else {
return Observable.of(user);
}
});
return observable
.do(user => {
this.selectedUser.next(user);
})
.map(u => u !== null);
}
public loadUsers(): Observable<any> {
return this.usersService.getUsers(this.usersPager.value.pageSize, this.usersPager.value.skip, this.usersQuery.value)
.catch(error => this.notifyError(error))
.do(dtos => {
this.usersItems.nextBy(v => ImmutableArray.of(dtos.items));
this.usersPager.nextBy(v => v.setCount(dtos.total));
});
}
public updateUser(request: CreateUserDto): Observable<any> {
const id = this.selectedUser.value!.id;
return this.usersService.putUser(id, request)
.do(() => {
this.dialogs.notify(Notification.info('User saved successsfull'));
this.usersItems.nextBy(v => v.replaceAll(x => x.id === id, u => u.update(request.email, request.displayName)));
});
}
public createUser(request: CreateUserDto): Observable<UserDto> {
return this.usersService.postUser(request)
.catch(error => this.notifyError(error))
.do(user => {
this.usersItems.nextBy(v => v.pushFront(user));
this.usersPager.nextBy(v => v.incrementCount());
});
}
public lockUser(id: string): Observable<any> {
return this.usersService.lockUser(id)
.catch(error => this.notifyError(error))
.do(() => {
this.usersItems.nextBy(v => v.replaceAll(x => x.id === id, u => u.lock()));
});
}
public unlockUser(id: string): Observable<any> {
return this.usersService.lockUser(id)
.catch(error => this.notifyError(error))
.do(() => {
this.usersItems.nextBy(v => v.replaceAll(x => x.id === id, u => u.unlock()));
});
}
public search(filter: string) {
this.usersPager.nextBy(v => new Pager(0));
this.usersQuery.nextBy(v => filter);
this.loadUsers();
}
public goNext() {
this.usersPager.nextBy(v => v.goNext());
this.loadUsers();
}
public goPrev() {
this.usersPager.nextBy(v => v.goPrev());
this.loadUsers();
}
public trackBy(index: number, user: UserDto): any {
return user.id;
}
private notifyError(error: string | ErrorDto) {
if (error instanceof ErrorDto) {
this.dialogs.notify(Notification.error(error.displayMessage));
} else {
this.dialogs.notify(Notification.error(error));
}
return Observable.throw(error);
}
}

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

@ -0,0 +1,10 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export function nextBy<T>(updater: (value: T) => T): void {
return this.next(updater(this.value));
}

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

@ -0,0 +1,20 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { nextBy } from './rxjs-extensions-impl';
/* tslint:disable:no-shadowed-variable */
declare module 'rxjs/BehaviorSubject' {
interface BehaviorSubject<T> {
nextBy(updater: (value: T) => T): void;
}
}
BehaviorSubject.prototype['nextBy'] = nextBy;
Loading…
Cancel
Save