Browse Source

Fix contributors page.

pull/282/head
Sebastian Stehle 8 years ago
parent
commit
ea789b8aff
  1. 7
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  2. 18
      src/Squidex/app/features/administration/state/users.state.spec.ts
  3. 2
      src/Squidex/app/features/administration/state/users.state.ts
  4. 20
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  5. 93
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  6. 5
      src/Squidex/app/features/settings/pages/more/more-page.component.ts
  7. 1
      src/Squidex/app/shared/internal.ts
  8. 2
      src/Squidex/app/shared/module.ts
  9. 9
      src/Squidex/app/shared/state/apps.state.spec.ts
  10. 7
      src/Squidex/app/shared/state/apps.state.ts
  11. 5
      src/Squidex/app/shared/state/clients.state.spec.ts
  12. 116
      src/Squidex/app/shared/state/contributors.state.spec.ts
  13. 137
      src/Squidex/app/shared/state/contributors.state.ts
  14. 8
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

7
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
@ -44,7 +45,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
command.ContributorId = user.Id; command.ContributorId = user.Id;
if (contributors.TryGetValue(command.ContributorId, out var existing)) if (string.Equals(command.ContributorId, command.Actor.Identifier, StringComparison.OrdinalIgnoreCase))
{
error(new ValidationError("You cannot change your own permission."));
}
else if (contributors.TryGetValue(command.ContributorId, out var existing))
{ {
if (existing == command.Permission) if (existing == command.Permission)
{ {

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

@ -51,7 +51,7 @@ describe('UsersState', () => {
}); });
it('should load users', () => { it('should load users', () => {
expect(usersState.snapshot.users.values).toEqual([{ isCurrentUser: false, user: oldUsers[0] }, { isCurrentUser: true, user: oldUsers[1] }]); expect(usersState.snapshot.users.values).toEqual(oldUsers.map(x => u(x)));
expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200); expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200);
usersService.verifyAll(); usersService.verifyAll();
@ -76,7 +76,7 @@ describe('UsersState', () => {
usersState.load().subscribe(); usersState.load().subscribe();
expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUsers[0] }); expect(usersState.snapshot.selectedUser).toEqual(u(newUsers[0]));
}); });
it('should mark as current user when selected user equals to profile', () => { it('should mark as current user when selected user equals to profile', () => {
@ -87,7 +87,7 @@ describe('UsersState', () => {
}); });
expect(selectedUser!).toEqual(oldUsers[1]); expect(selectedUser!).toEqual(oldUsers[1]);
expect(usersState.snapshot.selectedUser!).toEqual({ isCurrentUser: true, user: oldUsers[1] }); expect(usersState.snapshot.selectedUser).toEqual(u(oldUsers[1]));
}); });
it('should not load user when already loaded', () => { it('should not load user when already loaded', () => {
@ -98,7 +98,7 @@ describe('UsersState', () => {
}); });
expect(selectedUser!).toEqual(oldUsers[0]); expect(selectedUser!).toEqual(oldUsers[0]);
expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: oldUsers[0] }); expect(usersState.snapshot.selectedUser).toEqual(u(oldUsers[0]));
usersService.verify(x => x.getUser(It.isAnyString()), Times.never()); usersService.verify(x => x.getUser(It.isAnyString()), Times.never());
}); });
@ -114,7 +114,7 @@ describe('UsersState', () => {
}); });
expect(selectedUser!).toEqual(newUser); expect(selectedUser!).toEqual(newUser);
expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUser }); expect(usersState.snapshot.selectedUser).toEqual(u(newUser));
usersService.verify(x => x.getUser('id3'), Times.once()); usersService.verify(x => x.getUser('id3'), Times.once());
}); });
@ -166,7 +166,7 @@ describe('UsersState', () => {
usersState.select('id2').subscribe(); usersState.select('id2').subscribe();
usersState.unlock(oldUsers[1]).subscribe(); usersState.unlock(oldUsers[1]).subscribe();
const user_1 = usersState.snapshot.users.at(0); const user_1 = usersState.snapshot.users.at(1);
expect(user_1.user.isLocked).toBeFalsy(); expect(user_1.user.isLocked).toBeFalsy();
expect(user_1).toBe(usersState.snapshot.selectedUser); expect(user_1).toBe(usersState.snapshot.selectedUser);
@ -196,7 +196,7 @@ describe('UsersState', () => {
usersState.create(request).subscribe(); usersState.create(request).subscribe();
expect(usersState.snapshot.users.at(0).user).toBe(newUser); expect(usersState.snapshot.users.values).toEqual([u(newUser), ...oldUsers.map(x => u(x))]);
expect(usersState.snapshot.usersPager.numberOfItems).toBe(201); expect(usersState.snapshot.usersPager.numberOfItems).toBe(201);
}); });
@ -221,4 +221,8 @@ describe('UsersState', () => {
usersService.verify(x => x.getUsers(10, 0, 'my-query'), Times.once()); usersService.verify(x => x.getUsers(10, 0, 'my-query'), Times.once());
}); });
function u(user: UserDto) {
return { user, isCurrentUser: user.id === 'id2' };
}
}); });

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

@ -206,7 +206,7 @@ export class UsersState extends State<Snapshot> {
private replaceUser(userDto: UserDto) { private replaceUser(userDto: UserDto) {
return this.next(s => { return this.next(s => {
const user = this.createUser(userDto); const user = this.createUser(userDto);
const users = s.users.replaceBy('id', user); const users = s.users.map(u => u.user.id === userDto.id ? user : u);
const selectedUser = s.selectedUser && s.selectedUser.user.id === userDto.id ? user : s.selectedUser; const selectedUser = s.selectedUser && s.selectedUser.user.id === userDto.id ? user : s.selectedUser;

20
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -6,27 +6,30 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<ng-container *ngIf="contributorsState.maxContributors | async; let maxContributors">
<div class="panel-alert panel-alert-success" *ngIf="maxContributors > 0"> <div class="panel-alert panel-alert-success" *ngIf="maxContributors > 0">
Your plan allows up to {{maxContributors}} contributors. Your plan allows up to {{maxContributors}} contributors.
</div> </div>
</ng-container>
<ng-container *ngIf="contributorsState.contributors | async; let contributors">
<table class="table table-items table-fixed"> <table class="table table-items table-fixed">
<tbody> <tbody>
<ng-template ngFor let-contributor [ngForOf]="appContributors?.contributors"> <ng-template ngFor let-contributorInfo [ngForOf]="contributors">
<tr> <tr>
<td class="cell-user"> <td class="cell-user">
<img class="user-picture" [attr.title]="contributor.contributorId | sqxUserName" [attr.src]="contributor.contributorId | sqxUserPicture" /> <img class="user-picture" [attr.title]="contributorInfo.contributor.contributorId | sqxUserName" [attr.src]="contributorInfo.contributor.contributorId | sqxUserPicture" />
</td> </td>
<td class="cell-auto"> <td class="cell-auto">
<span class="user-name table-cell">{{contributor.contributorId | sqxUserName}}</span> <span class="user-name table-cell">{{contributorInfo.contributor.contributorId | sqxUserName}}</span>
</td> </td>
<td class="cell-time"> <td class="cell-time">
<select class="form-control" [ngModel]="contributor.permission" (ngModelChange)="changePermission(contributor, $event)" [disabled]="authState.user?.id === contributor.contributorId"> <select class="form-control" [ngModel]="contributorInfo.contributor.permission" (ngModelChange)="changePermission(contributorInfo.contributor, $event)" [disabled]="contributorInfo.isCurrentUser">
<option *ngFor="let permission of usersPermissions" [ngValue]="permission">{{permission}}</option> <option *ngFor="let permission of usersPermissions" [ngValue]="permission">{{permission}}</option>
</select> </select>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<button *ngIf="authState.user?.id !== contributor.contributorId" type="button" class="btn btn-link btn-danger" (click)="removeContributor(contributor)"> <button *ngIf="!contributorInfo.isCurrentUser" type="button" class="btn btn-link btn-danger" (click)="removeContributor(contributorInfo.contributor)">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</td> </td>
@ -35,9 +38,10 @@
</ng-template> </ng-template>
</tbody> </tbody>
</table> </table>
</ng-container>
<div class="table-items-footer" *ngIf="appContributors"> <div class="table-items-footer" *ngIf="(contributorsState.isMaxReached | async) === false">
<form [formGroup]="addContributorForm" (ngSubmit)="assignContributor()"> <form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="displayName"> <sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="displayName">
@ -51,7 +55,7 @@
</sqx-autocomplete> </sqx-autocomplete>
</div> </div>
<div class="col col-auto pl-1"> <div class="col col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="!canAddContributor">Add Contributor</button> <button type="submit" class="btn btn-success" [disabled]="assignContributorForm.noUser | async">Add Contributor</button>
</div> </div>
</div> </div>
</form> </form>

93
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -6,17 +6,15 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
AppContributorDto, AppContributorDto,
AppContributorsDto,
AppContributorsService,
AppsState, AppsState,
AuthService, AssignContributorForm,
AutocompleteSource, AutocompleteSource,
DialogService, ContributorsState,
PublicUserDto, PublicUserDto,
UsersService UsersService
} from '@app/shared'; } from '@app/shared';
@ -30,11 +28,11 @@ export class UsersDataSource implements AutocompleteSource {
public find(query: string): Observable<any[]> { public find(query: string): Observable<any[]> {
return this.usersService.getUsers(query) return this.usersService.getUsers(query)
.map(users => { .withLatestFrom(this.component.contributorsState.contributors, (users, contributors) => {
const results: any[] = []; const results: any[] = [];
for (let user of users) { for (let user of users) {
if (!this.component.appContributors || !this.component.appContributors.contributors.find(t => t.contributorId === user.id)) { if (!contributors.find(t => t.contributor.contributorId === user.id)) {
results.push(user); results.push(user);
} }
} }
@ -49,31 +47,14 @@ export class UsersDataSource implements AutocompleteSource {
templateUrl: './contributors-page.component.html' templateUrl: './contributors-page.component.html'
}) })
export class ContributorsPageComponent implements OnInit { export class ContributorsPageComponent implements OnInit {
public appContributors: AppContributorsDto;
public maxContributors = -1;
public usersDataSource: UsersDataSource; public usersDataSource: UsersDataSource;
public usersPermissions = [ 'Owner', 'Developer', 'Editor' ]; public usersPermissions = [ 'Owner', 'Developer', 'Editor' ];
public get canAddContributor() { public assignContributorForm = new AssignContributorForm(this.formBuilder);
return this.addContributorForm.valid && (this.maxContributors <= -1 || this.appContributors.contributors.length < this.maxContributors);
}
public addContributorForm =
this.formBuilder.group({
user: [null,
[
Validators.required
]
]
});
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly authState: AuthService, public readonly contributorsState: ContributorsState,
private readonly appContributorsService: AppContributorsService,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
usersService: UsersService usersService: UsersService
) { ) {
@ -81,69 +62,35 @@ export class ContributorsPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.load(); this.contributorsState.load().onErrorResumeNext().subscribe();
}
public load() {
this.appContributorsService.getContributors(this.appsState.appName)
.subscribe(dto => {
this.updateContributorsFromDto(dto);
}, error => {
this.dialogs.notifyError(error);
});
} }
public removeContributor(contributor: AppContributorDto) { public removeContributor(contributor: AppContributorDto) {
this.appContributorsService.deleteContributor(this.appsState.appName, contributor.contributorId, this.appContributors.version) this.contributorsState.revoke(contributor).onErrorResumeNext().subscribe();
.subscribe(dto => {
this.updateContributors(this.appContributors.removeContributor(contributor, dto.version));
}, error => {
this.dialogs.notifyError(error);
});
} }
public changePermission(contributor: AppContributorDto, permission: string) { public changePermission(contributor: AppContributorDto, permission: string) {
const requestDto = contributor.changePermission(permission); this.contributorsState.assign(new AppContributorDto(contributor.contributorId, permission)).onErrorResumeNext().subscribe();
this.appContributorsService.postContributor(this.appsState.appName, requestDto, this.appContributors.version)
.subscribe(dto => {
this.updateContributors(this.appContributors.updateContributor(contributor, dto.version));
}, error => {
this.dialogs.notifyError(error);
});
} }
public assignContributor() { public assignContributor() {
let value: any = this.addContributorForm.controls['user'].value; const value = this.assignContributorForm.submit();
if (value) {
let user = value.user;
if (value instanceof PublicUserDto) { if (user instanceof PublicUserDto) {
value = value.id; user = user.id;
} }
const requestDto = new AppContributorDto(value, 'Editor'); const requestDto = new AppContributorDto(user, 'Editor');
this.appContributorsService.postContributor(this.appsState.appName, requestDto, this.appContributors.version) this.contributorsState.assign(requestDto)
.subscribe(dto => { .subscribe(dto => {
this.updateContributors(this.appContributors.addContributor(new AppContributorDto(dto.payload.contributorId, requestDto.permission), dto.version)); this.assignContributorForm.submitCompleted();
this.resetContributorForm();
}, error => { }, error => {
this.dialogs.notifyError(error); this.assignContributorForm.submitFailed(error);
this.resetContributorForm();
}); });
} }
private resetContributorForm() {
this.addContributorForm.reset();
}
private updateContributorsFromDto(appContributors: AppContributorsDto) {
this.updateContributors(appContributors);
this.maxContributors = appContributors.maxContributors;
}
private updateContributors(appContributors: AppContributorsDto) {
this.appContributors = appContributors;
} }
} }

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

@ -8,7 +8,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AppsState, DialogService } from '@app/shared'; import { AppsState } from '@app/shared';
@Component({ @Component({
selector: 'sqx-more-page', selector: 'sqx-more-page',
@ -18,7 +18,6 @@ import { AppsState, DialogService } from '@app/shared';
export class MorePageComponent { export class MorePageComponent {
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
private readonly dialogs: DialogService,
private readonly router: Router private readonly router: Router
) { ) {
} }
@ -27,8 +26,6 @@ export class MorePageComponent {
this.appsState.delete(this.appsState.appName) this.appsState.delete(this.appsState.appName)
.subscribe(() => { .subscribe(() => {
this.router.navigate(['/app']); this.router.navigate(['/app']);
}, error => {
this.dialogs.notifyError(error);
}); });
} }
} }

1
src/Squidex/app/shared/internal.ts

@ -41,6 +41,7 @@ export * from './services/users.service';
export * from './state/apps.state'; export * from './state/apps.state';
export * from './state/assets.state'; export * from './state/assets.state';
export * from './state/clients.state'; export * from './state/clients.state';
export * from './state/contributors.state';
export * from './state/patterns.state'; export * from './state/patterns.state';
export * from './state/schemas.state'; export * from './state/schemas.state';

2
src/Squidex/app/shared/module.ts

@ -34,6 +34,7 @@ import {
BackupsService, BackupsService,
ClientsState, ClientsState,
ContentsService, ContentsService,
ContributorsState,
FileIconPipe, FileIconPipe,
GeolocationEditorComponent, GeolocationEditorComponent,
GraphQlService, GraphQlService,
@ -141,6 +142,7 @@ export class SqxSharedModule {
BackupsService, BackupsService,
ClientsState, ClientsState,
ContentsService, ContentsService,
ContributorsState,
GraphQlService, GraphQlService,
HelpService, HelpService,
HistoryService, HistoryService,

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

@ -13,7 +13,8 @@ import {
AppsService, AppsService,
AppsState, AppsState,
CreateAppDto, CreateAppDto,
DateTime DateTime,
DialogService
} from './../'; } from './../';
describe('AppsState', () => { describe('AppsState', () => {
@ -23,19 +24,23 @@ describe('AppsState', () => {
new AppDto('id1', 'old-name1', 'Owner', now, now, 'Free', 'Plan'), new AppDto('id1', 'old-name1', 'Owner', now, now, 'Free', 'Plan'),
new AppDto('id2', 'old-name2', 'Owner', now, now, 'Free', 'Plan') new AppDto('id2', 'old-name2', 'Owner', now, now, 'Free', 'Plan')
]; ];
const newApp = new AppDto('id3', 'new-name', 'Owner', now, now, 'Free', 'Plan'); const newApp = new AppDto('id3', 'new-name', 'Owner', now, now, 'Free', 'Plan');
let dialogs: IMock<DialogService>;
let appsService: IMock<AppsService>; let appsService: IMock<AppsService>;
let appsState: AppsState; let appsState: AppsState;
beforeEach(() => { beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
appsService = Mock.ofType<AppsService>(); appsService = Mock.ofType<AppsService>();
appsService.setup(x => x.getApps()) appsService.setup(x => x.getApps())
.returns(() => Observable.of(oldApps)) .returns(() => Observable.of(oldApps))
.verifiable(Times.once()); .verifiable(Times.once());
appsState = new AppsState(appsService.object); appsState = new AppsState(appsService.object, dialogs.object);
appsState.load().subscribe(); appsState.load().subscribe();
}); });

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

@ -14,6 +14,7 @@ import '@app/framework/utils/rxjs-extensions';
import { import {
DateTime, DateTime,
DialogService,
Form, Form,
ImmutableArray, ImmutableArray,
State, State,
@ -67,7 +68,8 @@ export class AppsState extends State<Snapshot> {
.distinctUntilChanged(); .distinctUntilChanged();
constructor( constructor(
private readonly appsService: AppsService private readonly appsService: AppsService,
private readonly dialogs: DialogService
) { ) {
super({ apps: ImmutableArray.empty(), selectedApp: null }); super({ apps: ImmutableArray.empty(), selectedApp: null });
} }
@ -116,6 +118,7 @@ export class AppsState extends State<Snapshot> {
return { ...s, apps, selectedApp }; return { ...s, apps, selectedApp };
}); });
}); })
.notify(this.dialogs);
} }
} }

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

@ -14,11 +14,11 @@ import {
AppClientsDto, AppClientsDto,
AppClientsService, AppClientsService,
ClientsState, ClientsState,
CreateAppClientDto,
DialogService, DialogService,
UpdateAppClientDto, UpdateAppClientDto,
Version, Version,
Versioned, Versioned
CreateAppClientDto
} from '@app/shared'; } from '@app/shared';
describe('ClientsState', () => { describe('ClientsState', () => {
@ -55,6 +55,7 @@ describe('ClientsState', () => {
it('should load clients', () => { it('should load clients', () => {
expect(clientsState.snapshot.clients.values).toEqual(oldClients); expect(clientsState.snapshot.clients.values).toEqual(oldClients);
expect(clientsState.snapshot.isLoaded).toBeTruthy();
expect(clientsState.snapshot.version).toEqual(version); expect(clientsState.snapshot.version).toEqual(version);
}); });

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

@ -0,0 +1,116 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { IMock, Mock } from 'typemoq';
import {
AppsState,
AppContributorDto,
AppContributorsDto,
AppContributorsService,
AuthService,
ContributorsState,
DialogService,
Version,
Versioned
} from '@app/shared';
describe('ContributorsState', () => {
const app = 'my-app';
const version = new Version('1');
const newVersion = new Version('2');
const oldContributors = [
new AppContributorDto('id1', 'Developer'),
new AppContributorDto('id2', 'Developer')
];
let dialogs: IMock<DialogService>;
let appsState: IMock<AppsState>;
let authService: IMock<AuthService>;
let contributorsService: IMock<AppContributorsService>;
let contributorsState: ContributorsState;
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
authService = Mock.ofType<AuthService>();
authService.setup(x => x.user)
.returns(() => <any>{ id: 'id2' });
appsState = Mock.ofType<AppsState>();
appsState.setup(x => x.appName)
.returns(() => app);
contributorsService = Mock.ofType<AppContributorsService>();
contributorsService.setup(x => x.getContributors(app))
.returns(() => Observable.of(new AppContributorsDto(oldContributors, 3, version)));
contributorsState = new ContributorsState(contributorsService.object, appsState.object, authService.object, dialogs.object);
contributorsState.load().subscribe();
});
it('should load contributors', () => {
expect(contributorsState.snapshot.contributors.values).toEqual(oldContributors.map(x => c(x)));
expect(contributorsState.snapshot.isLoaded).toBeTruthy();
expect(contributorsState.snapshot.isMaxReached).toBeFalsy();
expect(contributorsState.snapshot.maxContributors).toBe(3);
expect(contributorsState.snapshot.version).toEqual(version);
});
it('should add contributor to snapshot', () => {
const newContributor = new AppContributorDto('id3', 'Developer');
const request = new AppContributorDto('mail2stehle@gmail.com', 'Developer');
contributorsService.setup(x => x.postContributor(app, request, version))
.returns(() => Observable.of(new Versioned<AppContributorDto>(newVersion, newContributor)));
contributorsState.assign(request).subscribe();
expect(contributorsState.snapshot.contributors.values).toEqual([...oldContributors.map(x => c(x)), c(newContributor)]);
expect(contributorsState.snapshot.isLoaded).toBeTruthy();
expect(contributorsState.snapshot.isMaxReached).toBeTruthy();
expect(contributorsState.snapshot.maxContributors).toBe(3);
expect(contributorsState.snapshot.version).toEqual(newVersion);
});
it('should update contributor in snapshot', () => {
const newContributor = new AppContributorDto('id2', 'Owner');
const request = new AppContributorDto('mail2stehle@gmail.com', 'Owner');
contributorsService.setup(x => x.postContributor(app, request, version))
.returns(() => Observable.of(new Versioned<AppContributorDto>(newVersion, newContributor)));
contributorsState.assign(request).subscribe();
expect(contributorsState.snapshot.contributors.values).toEqual([c(oldContributors[0]), c(newContributor)]);
expect(contributorsState.snapshot.isLoaded).toBeTruthy();
expect(contributorsState.snapshot.isMaxReached).toBeFalsy();
expect(contributorsState.snapshot.maxContributors).toBe(3);
expect(contributorsState.snapshot.version).toEqual(newVersion);
});
it('should remove contributor from snapshot', () => {
contributorsService.setup(x => x.deleteContributor(app, oldContributors[0].contributorId, version))
.returns(() => Observable.of(new Versioned<any>(newVersion, {})));
contributorsState.revoke(oldContributors[0]).subscribe();
expect(contributorsState.snapshot.contributors.values).toEqual([c(oldContributors[1])]);
expect(contributorsState.snapshot.version).toEqual(newVersion);
});
function c(contributor: AppContributorDto) {
return { contributor, isCurrentUser: contributor.contributorId === 'id2' };
}
});

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

@ -0,0 +1,137 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import '@app/framework/utils/rxjs-extensions';
import {
DialogService,
ImmutableArray,
Form,
State,
Version
} from '@app/framework';
import { AuthService } from './../services/auth.service';
import { AppsState } from './apps.state';
import { AppContributorDto, AppContributorsService } from './../services/app-contributors.service';
export class AssignContributorForm extends Form<FormGroup> {
public hasNoUser =
this.form.controls['user'].valueChanges.startWith(null).map(x => !x);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
user: [null,
[
Validators.required
]
]
}));
}
}
interface SnapshotContributor {
contributor: AppContributorDto;
isCurrentUser: boolean;
}
interface Snapshot {
contributors: ImmutableArray<SnapshotContributor>;
isLoaded: boolean;
isMaxReached: boolean;
maxContributors: number;
version: Version;
}
@Injectable()
export class ContributorsState extends State<Snapshot> {
public contributors =
this.changes.map(x => x.contributors);
public isLoaded =
this.changes.map(x => x.isLoaded);
public isMaxReached =
this.changes.map(x => x.isMaxReached);
public maxContributors =
this.changes.map(x => x.maxContributors);
constructor(
private readonly appContributorsService: AppContributorsService,
private readonly appsState: AppsState,
private readonly authState: AuthService,
private readonly dialogs: DialogService
) {
super({ contributors: ImmutableArray.empty(), version: new Version(''), isLoaded: false, isMaxReached: true, maxContributors: -1 });
}
public load(): Observable<any> {
return this.appContributorsService.getContributors(this.appName)
.do(dtos => {
const contributors = ImmutableArray.of(dtos.contributors.map(x => this.createContributor(x)));
this.replaceContributors(contributors, dtos.version, dtos.maxContributors);
})
.notify(this.dialogs);
}
public revoke(contributor: AppContributorDto): Observable<any> {
return this.appContributorsService.deleteContributor(this.appName, contributor.contributorId, this.snapshot.version)
.do(dto => {
const contributors = this.snapshot.contributors.filter(x => x.contributor.contributorId !== contributor.contributorId);
this.replaceContributors(contributors, dto.version);
})
.notify(this.dialogs);
}
public assign(request: AppContributorDto): Observable<any> {
return this.appContributorsService.postContributor(this.appName, request, this.snapshot.version)
.do(dto => {
const contributor = this.createContributor(new AppContributorDto(dto.payload.contributorId, request.permission));
let contributors = this.snapshot.contributors;
if (contributors.find(x => x.contributor.contributorId === dto.payload.contributorId)) {
contributors = contributors.map(c => c.contributor.contributorId === dto.payload.contributorId ? contributor : c);
} else {
contributors = contributors.push(contributor);
}
this.replaceContributors(contributors, dto.version);
})
.notify(this.dialogs);
}
private replaceContributors(contributors: ImmutableArray<SnapshotContributor>, version: Version, maxContributors?: number) {
this.next(s => {
maxContributors = maxContributors || s.maxContributors;
const isLoaded = true;
const isMaxReached = maxContributors > 0 && maxContributors <= contributors.length;
return { ...s, contributors, maxContributors, isLoaded, isMaxReached, version };
});
}
private get appName() {
return this.appsState.appName;
}
private createContributor(contributor: AppContributorDto): SnapshotContributor {
return { contributor, isCurrentUser: contributor.contributorId === this.authState.user!.id };
}
}

8
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -82,6 +82,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
} }
[Fact]
public Task CanAssign_should_throw_exception_if_user_is_actor()
{
var command = new AssignContributor { ContributorId = "3", Permission = (AppContributorPermission)10, Actor = new RefToken("user", "3") };
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
}
[Fact] [Fact]
public Task CanAssign_should_throw_exception_if_contributor_max_reached() public Task CanAssign_should_throw_exception_if_contributor_max_reached()
{ {

Loading…
Cancel
Save