mirror of https://github.com/Squidex/squidex.git
35 changed files with 1052 additions and 45 deletions
@ -0,0 +1,54 @@ |
|||
<div class="table-items-row table-items-row-expandable"> |
|||
<div class="table-items-row-summary"> |
|||
<div class="row"> |
|||
<div class="col" [class.built]="isDefaultRole"> |
|||
{{role.name}} |
|||
</div> |
|||
<div class="col col-options"> |
|||
<div class="float-right"> |
|||
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="isEditing" (click)="toggleEditing()"> |
|||
<i class="icon-settings"></i> |
|||
</button> |
|||
|
|||
<button type="button" class="btn btn-link btn-danger" [class.invisible]="isDefaultRole" |
|||
(sqxConfirmClick)="remove()" |
|||
confirmTitle="Delete role" |
|||
confirmText="Do you really want to delete the language?"> |
|||
<i class="icon-bin2"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="table-items-row-details" *ngIf="isEditing"> |
|||
<form [formGroup]="editForm.form" (ngSubmit)="save()"> |
|||
<div class="table-items-row-details-tabs clearfix"> |
|||
<div class="float-right"> |
|||
<button type="reset" class="btn btn-link" (click)="toggleEditing()">Cancel</button> |
|||
<button type="submit" class="btn btn-primary" *ngIf="!isDefaultRole">Save</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="table-items-row-details-tab"> |
|||
<div class="form-group row no-gutters" *ngFor="let control of editForm.form.controls; let i = index"> |
|||
<div class="col"> |
|||
<sqx-control-errors [for]="control" [fieldName]="'Permission'" [submitted]="editForm.submitted | async"></sqx-control-errors> |
|||
|
|||
<sqx-autocomplete [formControl]="control" [source]="allPermissions"></sqx-autocomplete> |
|||
</div> |
|||
<div class="col col-auto" *ngIf="!isDefaultRole"> |
|||
<button type="button" class="btn btn-link btn-danger" (click)="removePermission(i)"> |
|||
<i class="icon-bin2"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="form-group" *ngIf="!isDefaultRole"> |
|||
<button type="button" class="btn btn-success btn-sm" (click)="addPermission()"> |
|||
Add Permission |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,22 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.form-group { |
|||
& { |
|||
margin-bottom: .375rem; |
|||
} |
|||
|
|||
&:last-child { |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
.built { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.table-items-row-details { |
|||
&::before { |
|||
right: 4.4rem; |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, Input, OnChanges } from '@angular/core'; |
|||
import { onErrorResumeNext } from 'rxjs/operators'; |
|||
|
|||
import { |
|||
AppRoleDto, |
|||
AutocompleteSource, |
|||
EditPermissionsForm, |
|||
fadeAnimation, |
|||
RolesState, |
|||
UpdateAppRoleDto |
|||
} from '@app/shared'; |
|||
|
|||
const DEFAULT_ROLES = [ |
|||
'Owner', |
|||
'Developer', |
|||
'Editor', |
|||
'Reader' |
|||
]; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-role', |
|||
styleUrls: ['./role.component.scss'], |
|||
templateUrl: './role.component.html', |
|||
animations: [ |
|||
fadeAnimation |
|||
] |
|||
}) |
|||
export class RoleComponent implements OnChanges { |
|||
@Input() |
|||
public role: AppRoleDto; |
|||
|
|||
@Input() |
|||
public allPermissions: AutocompleteSource; |
|||
|
|||
public isEditing = false; |
|||
public isDefaultRole = false; |
|||
|
|||
public editForm = new EditPermissionsForm(); |
|||
|
|||
constructor( |
|||
private readonly rolesState: RolesState |
|||
) { |
|||
} |
|||
|
|||
public ngOnChanges() { |
|||
this.isDefaultRole = DEFAULT_ROLES.indexOf(this.role.name) >= 0; |
|||
|
|||
this.editForm.load(this.role.permissions); |
|||
|
|||
if (this.isDefaultRole) { |
|||
this.editForm.form.disable(); |
|||
} |
|||
} |
|||
|
|||
public toggleEditing() { |
|||
this.isEditing = !this.isEditing; |
|||
} |
|||
|
|||
public addPermission() { |
|||
this.editForm.add(); |
|||
} |
|||
|
|||
public removePermission(index: number) { |
|||
this.editForm.remove(index); |
|||
} |
|||
|
|||
public remove() { |
|||
this.rolesState.delete(this.role).pipe(onErrorResumeNext()).subscribe(); |
|||
} |
|||
|
|||
public save() { |
|||
const value = this.editForm.submit(); |
|||
|
|||
if (value) { |
|||
const request = new UpdateAppRoleDto(value); |
|||
|
|||
this.rolesState.update(this.role, request) |
|||
.subscribe(() => { |
|||
this.editForm.submitCompleted(); |
|||
|
|||
this.toggleEditing(); |
|||
}, error => { |
|||
this.editForm.submitFailed(error); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,50 @@ |
|||
<sqx-title message="{app} | Roles | Settings" parameter1="app" [value1]="appsState.appName"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="50rem" [showSidebar]="true"> |
|||
<ng-container title> |
|||
Roles |
|||
</ng-container> |
|||
|
|||
<ng-container menu> |
|||
<button class="btn btn-link btn-secondary" (click)="reload()" title="Refresh roles (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<ng-container *ngIf="rolesState.isLoaded | async"> |
|||
<sqx-role *ngFor="let role of rolesState.roles | async; trackBy: trackByRole" [role]="role" [allPermissions]="allPermissions"></sqx-role> |
|||
|
|||
<div class="table-items-footer"> |
|||
<form [formGroup]="addRoleForm.form" (ngSubmit)="addRole()"> |
|||
<div class="row no-gutters"> |
|||
<div class="col"> |
|||
<sqx-control-errors for="name" [submitted]="addRoleForm.submitted | async"></sqx-control-errors> |
|||
|
|||
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter role name" autocomplete="off" /> |
|||
</div> |
|||
<div class="col col-auto pl-1"> |
|||
<button type="submit" class="btn btn-success" [disabled]="addRoleForm.hasNoName | async">Add role</button> |
|||
</div> |
|||
<div class="col col-auto pl-1"> |
|||
<button type="reset" class="btn btn-secondary" (click)="cancelAddRole()">Cancel</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</ng-container> |
|||
</ng-container> |
|||
|
|||
<ng-container sidebar> |
|||
<a class="panel-link" routerLink="history" routerLinkActive="active"> |
|||
<i class="icon-time"></i> |
|||
</a> |
|||
<a class="panel-link" routerLink="help" routerLinkActive="active"> |
|||
<i class="icon-help"></i> |
|||
</a> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -0,0 +1,2 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
@ -0,0 +1,81 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { FormBuilder } from '@angular/forms'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { onErrorResumeNext } from 'rxjs/operators'; |
|||
|
|||
import { |
|||
AddRoleForm, |
|||
AppRoleDto, |
|||
AppRolesService, |
|||
AppsState, |
|||
AutocompleteSource, |
|||
RolesState |
|||
} from '@app/shared'; |
|||
|
|||
class PermissionsAutocomplete implements AutocompleteSource { |
|||
private permissions: string[] = []; |
|||
|
|||
constructor(appsState: AppsState, rolesService: AppRolesService) { |
|||
rolesService.getPermissions(appsState.appName).subscribe(x => this.permissions = x); |
|||
} |
|||
|
|||
public find(query: string): Observable<any[]> { |
|||
return of(this.permissions.filter(y => y.indexOf(query) === 0)); |
|||
} |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'sqx-roles-page', |
|||
styleUrls: ['./roles-page.component.scss'], |
|||
templateUrl: './roles-page.component.html' |
|||
}) |
|||
export class RolesPageComponent implements OnInit { |
|||
public addRoleForm = new AddRoleForm(this.formBuilder); |
|||
|
|||
public allPermissions: AutocompleteSource = new PermissionsAutocomplete(this.appsState, this.rolesService); |
|||
|
|||
constructor( |
|||
public readonly appsState: AppsState, |
|||
public readonly rolesService: AppRolesService, |
|||
public readonly rolesState: RolesState, |
|||
private readonly formBuilder: FormBuilder |
|||
) { |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.rolesState.load().pipe(onErrorResumeNext()).subscribe(); |
|||
} |
|||
|
|||
public reload() { |
|||
this.rolesState.load(true).pipe(onErrorResumeNext()).subscribe(); |
|||
} |
|||
|
|||
public cancelAddRole() { |
|||
this.addRoleForm.submitCompleted(); |
|||
} |
|||
|
|||
public addRole() { |
|||
const value = this.addRoleForm.submit(); |
|||
|
|||
if (value) { |
|||
this.rolesState.add(value) |
|||
.subscribe(() => { |
|||
this.addRoleForm.submitCompleted(); |
|||
}, error => { |
|||
this.addRoleForm.submitFailed(error); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public trackByRole(index: number, role: AppRoleDto) { |
|||
return role.name; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,145 @@ |
|||
/* |
|||
* 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 { |
|||
AnalyticsService, |
|||
ApiUrlConfig, |
|||
AppRoleDto, |
|||
AppRolesDto, |
|||
AppRolesService, |
|||
UpdateAppRoleDto, |
|||
Version |
|||
} from './../'; |
|||
import { CreateAppRoleDto } from './app-roles.service'; |
|||
|
|||
describe('AppRolesService', () => { |
|||
const version = new Version('1'); |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [ |
|||
HttpClientTestingModule |
|||
], |
|||
providers: [ |
|||
AppRolesService, |
|||
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, |
|||
{ provide: AnalyticsService, useValue: new AnalyticsService() } |
|||
] |
|||
}); |
|||
}); |
|||
|
|||
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
|||
httpMock.verify(); |
|||
})); |
|||
|
|||
it('should make get request to get all permissions', |
|||
inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => { |
|||
|
|||
let permissions: string[]; |
|||
|
|||
roleService.getPermissions('my-app').subscribe(result => { |
|||
permissions = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles/permissions'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush(['P1', 'P2']); |
|||
|
|||
expect(permissions!).toEqual(['P1', 'P2']); |
|||
})); |
|||
|
|||
it('should make get request to get roles', |
|||
inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => { |
|||
|
|||
let roles: AppRolesDto; |
|||
|
|||
roleService.getRoles('my-app').subscribe(result => { |
|||
roles = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({ |
|||
roles: [{ |
|||
name: 'Role1', |
|||
permissions: ['P1'] |
|||
}, { |
|||
name: 'Role2', |
|||
permissions: ['P2'] |
|||
}] |
|||
}, { |
|||
headers: { |
|||
etag: '2' |
|||
} |
|||
}); |
|||
|
|||
expect(roles!).toEqual( |
|||
new AppRolesDto([ |
|||
new AppRoleDto('Role1', ['P1']), |
|||
new AppRoleDto('Role2', ['P2']) |
|||
], |
|||
new Version('2'))); |
|||
})); |
|||
|
|||
it('should make post request to add role', |
|||
inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new CreateAppRoleDto('Role3'); |
|||
|
|||
let role: AppRoleDto; |
|||
|
|||
roleService.postRole('my-app', dto, version).subscribe(result => { |
|||
role = result.payload; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles'); |
|||
|
|||
expect(req.request.method).toEqual('POST'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
|
|||
req.flush({}); |
|||
|
|||
expect(role!).toEqual(new AppRoleDto('Role3', [])); |
|||
})); |
|||
|
|||
it('should make put request to update role', |
|||
inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new UpdateAppRoleDto(['P4', 'P5']); |
|||
|
|||
roleService.putRole('my-app', 'role1', dto, version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles/role1'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
|
|||
it('should make delete request to remove role', |
|||
inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => { |
|||
|
|||
roleService.deleteRole('my-app', 'role1', version).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles/role1'); |
|||
|
|||
expect(req.request.method).toEqual('DELETE'); |
|||
expect(req.request.headers.get('If-Match')).toEqual(version.value); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
}); |
|||
@ -0,0 +1,129 @@ |
|||
/* |
|||
* 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 { map, tap } from 'rxjs/operators'; |
|||
|
|||
import { |
|||
AnalyticsService, |
|||
ApiUrlConfig, |
|||
HTTP, |
|||
Model, |
|||
pretifyError, |
|||
Version, |
|||
Versioned |
|||
} from '@app/framework'; |
|||
|
|||
export class AppRolesDto extends Model { |
|||
constructor( |
|||
public readonly roles: AppRoleDto[], |
|||
public readonly version: Version |
|||
) { |
|||
super(); |
|||
} |
|||
} |
|||
|
|||
export class AppRoleDto extends Model { |
|||
constructor( |
|||
public readonly name: string, |
|||
public readonly permissions: string[] |
|||
) { |
|||
super(); |
|||
} |
|||
|
|||
public with(value: Partial<AppRoleDto>): AppRoleDto { |
|||
return this.clone(value); |
|||
} |
|||
} |
|||
|
|||
export class CreateAppRoleDto { |
|||
constructor( |
|||
public readonly name: string |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class UpdateAppRoleDto { |
|||
constructor( |
|||
public readonly permissions: string[] |
|||
) { |
|||
} |
|||
} |
|||
|
|||
@Injectable() |
|||
export class AppRolesService { |
|||
constructor( |
|||
private readonly http: HttpClient, |
|||
private readonly apiUrl: ApiUrlConfig, |
|||
private readonly analytics: AnalyticsService |
|||
) { |
|||
} |
|||
|
|||
public getRoles(appName: string): Observable<AppRolesDto> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`); |
|||
|
|||
return HTTP.getVersioned<any>(this.http, url).pipe( |
|||
map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
const items: any[] = body.roles; |
|||
|
|||
const roles = items.map(item => { |
|||
return new AppRoleDto( |
|||
item.name, |
|||
item.permissions); |
|||
}); |
|||
|
|||
return new AppRolesDto(roles, response.version); |
|||
}), |
|||
pretifyError('Failed to load roles. Please reload.')); |
|||
} |
|||
|
|||
public postRole(appName: string, dto: CreateAppRoleDto, version: Version): Observable<Versioned<AppRoleDto>> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`); |
|||
|
|||
return HTTP.postVersioned<any>(this.http, url, dto, version).pipe( |
|||
map(response => { |
|||
const role = new AppRoleDto(dto.name, []); |
|||
|
|||
return new Versioned(response.version, role); |
|||
}), |
|||
tap(() => { |
|||
this.analytics.trackEvent('Role', 'Created', appName); |
|||
}), |
|||
pretifyError('Failed to add role. Please reload.')); |
|||
} |
|||
|
|||
public putRole(appName: string, name: string, dto: UpdateAppRoleDto, version: Version): Observable<Versioned<any>> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles/${name}`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, dto, version).pipe( |
|||
tap(() => { |
|||
this.analytics.trackEvent('Role', 'Updated', appName); |
|||
}), |
|||
pretifyError('Failed to revoke role. Please reload.')); |
|||
} |
|||
|
|||
public deleteRole(appName: string, name: string, version: Version): Observable<Versioned<any>> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles/${name}`); |
|||
|
|||
return HTTP.deleteVersioned(this.http, url, version).pipe( |
|||
tap(() => { |
|||
this.analytics.trackEvent('Role', 'Deleted', appName); |
|||
}), |
|||
pretifyError('Failed to revoke role. Please reload.')); |
|||
} |
|||
|
|||
public getPermissions(appName: string): Observable<string[]> { |
|||
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles/permissions`); |
|||
|
|||
return this.http.get<string[]>(url).pipe( |
|||
pretifyError('Failed to load permissions. Please reload.')); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; |
|||
import { map, startWith } from 'rxjs/operators'; |
|||
|
|||
import { Form } from '@app/framework'; |
|||
|
|||
export class EditPermissionsForm extends Form<FormArray> { |
|||
constructor() { |
|||
super(new FormArray([])); |
|||
} |
|||
|
|||
public add() { |
|||
this.form.push(new FormControl(undefined, Validators.required)); |
|||
} |
|||
|
|||
public remove(index: number) { |
|||
this.form.removeAt(index); |
|||
} |
|||
|
|||
public load(permissions: string[]) { |
|||
while (this.form.controls.length < permissions.length) { |
|||
this.add(); |
|||
} |
|||
|
|||
while (permissions.length > this.form.controls.length) { |
|||
this.form.removeAt(this.form.controls.length - 1); |
|||
} |
|||
|
|||
super.load(permissions); |
|||
} |
|||
} |
|||
|
|||
export class AddRoleForm extends Form<FormGroup> { |
|||
public hasNoName = |
|||
this.form.controls['name'].valueChanges.pipe(startWith(''), map(x => !x || x.length === 0)); |
|||
|
|||
constructor(formBuilder: FormBuilder) { |
|||
super(formBuilder.group({ |
|||
name: [null, |
|||
[ |
|||
Validators.required |
|||
] |
|||
] |
|||
})); |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { of } from 'rxjs'; |
|||
import { IMock, It, Mock, Times } from 'typemoq'; |
|||
|
|||
import { |
|||
AppRoleDto, |
|||
AppRolesDto, |
|||
AppRolesService, |
|||
AppsState, |
|||
DialogService, |
|||
RolesState, |
|||
Version, |
|||
Versioned |
|||
} from '@app/shared'; |
|||
import { CreateAppRoleDto, UpdateAppRoleDto } from '../services/app-roles.service'; |
|||
|
|||
describe('RolesState', () => { |
|||
const app = 'my-app'; |
|||
const version = new Version('1'); |
|||
const newVersion = new Version('2'); |
|||
|
|||
const oldRoles = [ |
|||
new AppRoleDto('Role1', ['P1']), |
|||
new AppRoleDto('Role2', ['P2']) |
|||
]; |
|||
|
|||
let dialogs: IMock<DialogService>; |
|||
let appsState: IMock<AppsState>; |
|||
let rolesService: IMock<AppRolesService>; |
|||
let rolesState: RolesState; |
|||
|
|||
beforeEach(() => { |
|||
dialogs = Mock.ofType<DialogService>(); |
|||
|
|||
appsState = Mock.ofType<AppsState>(); |
|||
|
|||
appsState.setup(x => x.appName) |
|||
.returns(() => app); |
|||
|
|||
rolesService = Mock.ofType<AppRolesService>(); |
|||
|
|||
rolesService.setup(x => x.getRoles(app)) |
|||
.returns(() => of(new AppRolesDto(oldRoles, version))); |
|||
|
|||
rolesState = new RolesState(rolesService.object, appsState.object, dialogs.object); |
|||
rolesState.load().subscribe(); |
|||
}); |
|||
|
|||
it('should load roles', () => { |
|||
expect(rolesState.snapshot.roles.values).toEqual(oldRoles); |
|||
expect(rolesState.snapshot.isLoaded).toBeTruthy(); |
|||
expect(rolesState.snapshot.version).toEqual(version); |
|||
|
|||
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); |
|||
}); |
|||
|
|||
it('should show notification on load when reload is true', () => { |
|||
rolesState.load(true).subscribe(); |
|||
|
|||
expect().nothing(); |
|||
|
|||
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); |
|||
}); |
|||
|
|||
it('should add role to snapshot when added', () => { |
|||
const newRole = new AppRoleDto('Role3', ['P3']); |
|||
|
|||
const request = new CreateAppRoleDto('Role3'); |
|||
|
|||
rolesService.setup(x => x.postRole(app, request, version)) |
|||
.returns(() => of(new Versioned<AppRoleDto>(newVersion, newRole))); |
|||
|
|||
rolesState.add(request).subscribe(); |
|||
|
|||
expect(rolesState.snapshot.roles.values).toEqual([oldRoles[0], oldRoles[1], newRole]); |
|||
expect(rolesState.snapshot.version).toEqual(newVersion); |
|||
}); |
|||
|
|||
it('should update permissions when updated', () => { |
|||
const request = new UpdateAppRoleDto(['P4', 'P5']); |
|||
|
|||
rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version)) |
|||
.returns(() => of(new Versioned<any>(newVersion, {}))); |
|||
|
|||
rolesState.update(oldRoles[1], request).subscribe(); |
|||
|
|||
const role_1 = rolesState.snapshot.roles.at(1); |
|||
|
|||
expect(role_1.permissions).toEqual(request.permissions); |
|||
expect(rolesState.snapshot.version).toEqual(newVersion); |
|||
}); |
|||
|
|||
it('should remove role from snapshot when deleted', () => { |
|||
rolesService.setup(x => x.deleteRole(app, oldRoles[0].name, version)) |
|||
.returns(() => of(new Versioned<any>(newVersion, {}))); |
|||
|
|||
rolesState.delete(oldRoles[0]).subscribe(); |
|||
|
|||
expect(rolesState.snapshot.roles.values).toEqual([oldRoles[1]]); |
|||
expect(rolesState.snapshot.version).toEqual(newVersion); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,121 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { Observable } from 'rxjs'; |
|||
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; |
|||
|
|||
import { |
|||
DialogService, |
|||
ImmutableArray, |
|||
notify, |
|||
State, |
|||
Version |
|||
} from '@app/framework'; |
|||
|
|||
import { AppsState } from './apps.state'; |
|||
|
|||
import { |
|||
AppRoleDto, |
|||
AppRolesService, |
|||
CreateAppRoleDto, |
|||
UpdateAppRoleDto |
|||
} from './../services/app-roles.service'; |
|||
|
|||
interface Snapshot { |
|||
roles: ImmutableArray<AppRoleDto>; |
|||
|
|||
version: Version; |
|||
|
|||
isLoaded?: boolean; |
|||
} |
|||
|
|||
@Injectable() |
|||
export class RolesState extends State<Snapshot> { |
|||
public roles = |
|||
this.changes.pipe(map(x => x.roles), |
|||
distinctUntilChanged()); |
|||
|
|||
public isLoaded = |
|||
this.changes.pipe(map(x => !!x.isLoaded), |
|||
distinctUntilChanged()); |
|||
|
|||
constructor( |
|||
private readonly appRolesService: AppRolesService, |
|||
private readonly appsState: AppsState, |
|||
private readonly dialogs: DialogService |
|||
) { |
|||
super({ roles: ImmutableArray.empty(), version: new Version('') }); |
|||
} |
|||
|
|||
public load(isReload = false): Observable<any> { |
|||
if (!isReload) { |
|||
this.resetState(); |
|||
} |
|||
|
|||
return this.appRolesService.getRoles(this.appName).pipe( |
|||
tap(dtos => { |
|||
if (isReload) { |
|||
this.dialogs.notifyInfo('Roles reloaded.'); |
|||
} |
|||
|
|||
this.next(s => { |
|||
const roles = ImmutableArray.of(dtos.roles).sortByStringAsc(x => x.name); |
|||
|
|||
return { ...s, roles, isLoaded: true, version: dtos.version }; |
|||
}); |
|||
}), |
|||
notify(this.dialogs)); |
|||
} |
|||
|
|||
public add(request: CreateAppRoleDto): Observable<any> { |
|||
return this.appRolesService.postRole(this.appName, request, this.version).pipe( |
|||
tap(dto => { |
|||
this.next(s => { |
|||
const roles = s.roles.push(dto.payload); |
|||
|
|||
return { ...s, roles, version: dto.version }; |
|||
}); |
|||
}), |
|||
notify(this.dialogs)); |
|||
} |
|||
|
|||
public delete(role: AppRoleDto): Observable<any> { |
|||
return this.appRolesService.deleteRole(this.appName, role.name, this.version).pipe( |
|||
tap(dto => { |
|||
this.next(s => { |
|||
const roles = s.roles.filter(c => c.name !== role.name); |
|||
|
|||
return { ...s, roles, version: dto.version }; |
|||
}); |
|||
}), |
|||
notify(this.dialogs)); |
|||
} |
|||
|
|||
public update(role: AppRoleDto, request: UpdateAppRoleDto): Observable<any> { |
|||
return this.appRolesService.putRole(this.appName, role.name, request, this.version).pipe( |
|||
tap(dto => { |
|||
this.next(s => { |
|||
const roles = s.roles.replaceBy('name', update(role, request)); |
|||
|
|||
return { ...s, roles, version: dto.version }; |
|||
}); |
|||
}), |
|||
notify(this.dialogs)); |
|||
} |
|||
|
|||
private get appName() { |
|||
return this.appsState.appName; |
|||
} |
|||
|
|||
private get version() { |
|||
return this.snapshot.version; |
|||
} |
|||
} |
|||
|
|||
const update = (role: AppRoleDto, request: UpdateAppRoleDto) => |
|||
role.with({ permissions: request.permissions }); |
|||
Loading…
Reference in new issue