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