diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index b11acbc6b5..8c1d45134f 100644 --- a/docs/en/UI/Angular/Component-Replacement.md +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -543,6 +543,10 @@ The final UI looks like below: ![New nav-items](./images/replaced-nav-items-component.png) +## See Also + +- [How to Replace PermissionManagementComponent](./Permission-Management-Component-Replacement.md) + ## What's Next? - [Custom Setting Page](./Custom-Setting-Page.md) diff --git a/docs/en/UI/Angular/Permission-Management-Component-Replacement.md b/docs/en/UI/Angular/Permission-Management-Component-Replacement.md new file mode 100644 index 0000000000..75a3833578 --- /dev/null +++ b/docs/en/UI/Angular/Permission-Management-Component-Replacement.md @@ -0,0 +1,500 @@ +# How to Replace PermissionManagementComponent + +![Permission management modal](./images/permission-management-modal.png) + +Run the following command in `angular` folder to create a new component called `PermissionManagementComponent`. + +```bash +yarn ng generate component permission-management --entryComponent --inlineStyle + +# You don't need the --entryComponent option in Angular 9 +``` + +Open the generated `permission-management.component.ts` in `src/app/permission-management` folder and replace the content with the following: + +```js +import { + Component, + EventEmitter, + Input, + Output, + Renderer2, + TrackByFunction, + Inject, + Optional, +} from '@angular/core'; +import { ReplaceableComponents } from '@abp/ng.core'; +import { Select, Store } from '@ngxs/store'; +import { Observable } from 'rxjs'; +import { finalize, map, pluck, take, tap } from 'rxjs/operators'; +import { + GetPermissions, + UpdatePermissions, + PermissionManagement, + PermissionManagementState, +} from '@abp/ng.permission-management'; + +type PermissionWithMargin = PermissionManagement.Permission & { + margin: number; +}; + +@Component({ + selector: 'app-permission-management', + templateUrl: './permission-management.component.html', + styles: [ + ` + .overflow-scroll { + max-height: 70vh; + overflow-y: scroll; + } + `, + ], +}) +export class PermissionManagementComponent + implements + PermissionManagement.PermissionManagementComponentInputs, + PermissionManagement.PermissionManagementComponentOutputs { + protected _providerName: string; + @Input() + get providerName(): string { + if (this.replaceableData) return this.replaceableData.inputs.providerName; + + return this._providerName; + } + + set providerName(value: string) { + this._providerName = value; + } + + protected _providerKey: string; + @Input() + get providerKey(): string { + if (this.replaceableData) return this.replaceableData.inputs.providerKey; + + return this._providerKey; + } + + set providerKey(value: string) { + this._providerKey = value; + } + + protected _hideBadges = false; + @Input() + get hideBadges(): boolean { + if (this.replaceableData) return this.replaceableData.inputs.hideBadges; + + return this._hideBadges; + } + + set hideBadges(value: boolean) { + this._hideBadges = value; + } + + protected _visible = false; + @Input() + get visible(): boolean { + return this._visible; + } + + set visible(value: boolean) { + if (value === this._visible) return; + + if (value) { + this.openModal().subscribe(() => { + this._visible = true; + this.visibleChange.emit(true); + if (this.replaceableData) this.replaceableData.outputs.visibleChange(true); + }); + } else { + this.selectedGroup = null; + this._visible = false; + this.visibleChange.emit(false); + if (this.replaceableData) this.replaceableData.outputs.visibleChange(false); + } + } + + @Output() readonly visibleChange = new EventEmitter(); + + @Select(PermissionManagementState.getPermissionGroups) + groups$: Observable; + + @Select(PermissionManagementState.getEntityDisplayName) + entityName$: Observable; + + selectedGroup: PermissionManagement.Group; + + permissions: PermissionManagement.Permission[] = []; + + selectThisTab = false; + + selectAllTab = false; + + modalBusy = false; + + trackByFn: TrackByFunction = (_, item) => item.name; + + get selectedGroupPermissions$(): Observable { + return this.groups$.pipe( + map((groups) => + this.selectedGroup + ? groups.find((group) => group.name === this.selectedGroup.name).permissions + : [] + ), + map((permissions) => + permissions.map( + (permission) => + (({ + ...permission, + margin: findMargin(permissions, permission), + isGranted: this.permissions.find((per) => per.name === permission.name).isGranted, + } as any) as PermissionWithMargin) + ) + ) + ); + } + + get isVisible(): boolean { + if (!this.replaceableData) return this.visible; + + return this.replaceableData.inputs.visible; + } + + constructor( + @Optional() + @Inject('REPLACEABLE_DATA') + public replaceableData: ReplaceableComponents.ReplaceableTemplateData< + PermissionManagement.PermissionManagementComponentInputs, + PermissionManagement.PermissionManagementComponentOutputs + >, + private store: Store + ) {} + + getChecked(name: string) { + return (this.permissions.find((per) => per.name === name) || { isGranted: false }).isGranted; + } + + isGrantedByOtherProviderName(grantedProviders: PermissionManagement.GrantedProvider[]): boolean { + if (grantedProviders.length) { + return grantedProviders.findIndex((p) => p.providerName !== this.providerName) > -1; + } + return false; + } + + onClickCheckbox(clickedPermission: PermissionManagement.Permission, value) { + if ( + clickedPermission.isGranted && + this.isGrantedByOtherProviderName(clickedPermission.grantedProviders) + ) + return; + + setTimeout(() => { + this.permissions = this.permissions.map((per) => { + if (clickedPermission.name === per.name) { + return { ...per, isGranted: !per.isGranted }; + } else if (clickedPermission.name === per.parentName && clickedPermission.isGranted) { + return { ...per, isGranted: false }; + } else if (clickedPermission.parentName === per.name && !clickedPermission.isGranted) { + return { ...per, isGranted: true }; + } + + return per; + }); + + this.setTabCheckboxState(); + this.setGrantCheckboxState(); + }, 0); + } + + setTabCheckboxState() { + this.selectedGroupPermissions$.pipe(take(1)).subscribe((permissions) => { + const selectedPermissions = permissions.filter((per) => per.isGranted); + const element = document.querySelector('#select-all-in-this-tabs') as any; + + if (selectedPermissions.length === permissions.length) { + element.indeterminate = false; + this.selectThisTab = true; + } else if (selectedPermissions.length === 0) { + element.indeterminate = false; + this.selectThisTab = false; + } else { + element.indeterminate = true; + } + }); + } + + setGrantCheckboxState() { + const selectedAllPermissions = this.permissions.filter((per) => per.isGranted); + const checkboxElement = document.querySelector('#select-all-in-all-tabs') as any; + + if (selectedAllPermissions.length === this.permissions.length) { + checkboxElement.indeterminate = false; + this.selectAllTab = true; + } else if (selectedAllPermissions.length === 0) { + checkboxElement.indeterminate = false; + this.selectAllTab = false; + } else { + checkboxElement.indeterminate = true; + } + } + + onClickSelectThisTab() { + this.selectedGroupPermissions$.pipe(take(1)).subscribe((permissions) => { + permissions.forEach((permission) => { + if (permission.isGranted && this.isGrantedByOtherProviderName(permission.grantedProviders)) + return; + + const index = this.permissions.findIndex((per) => per.name === permission.name); + + this.permissions = [ + ...this.permissions.slice(0, index), + { ...this.permissions[index], isGranted: !this.selectThisTab }, + ...this.permissions.slice(index + 1), + ]; + }); + }); + + this.setGrantCheckboxState(); + } + + onClickSelectAll() { + this.permissions = this.permissions.map((permission) => ({ + ...permission, + isGranted: + this.isGrantedByOtherProviderName(permission.grantedProviders) || !this.selectAllTab, + })); + + this.selectThisTab = !this.selectAllTab; + } + + onChangeGroup(group: PermissionManagement.Group) { + this.selectedGroup = group; + this.setTabCheckboxState(); + } + + submit() { + this.modalBusy = true; + const unchangedPermissions = getPermissions( + this.store.selectSnapshot(PermissionManagementState.getPermissionGroups) + ); + + const changedPermissions: PermissionManagement.MinimumPermission[] = this.permissions + .filter((per) => + unchangedPermissions.find((unchanged) => unchanged.name === per.name).isGranted === + per.isGranted + ? false + : true + ) + .map(({ name, isGranted }) => ({ name, isGranted })); + + if (changedPermissions.length) { + this.store + .dispatch( + new UpdatePermissions({ + providerKey: this.providerKey, + providerName: this.providerName, + permissions: changedPermissions, + }) + ) + .pipe(finalize(() => (this.modalBusy = false))) + .subscribe(() => { + this.visible = false; + }); + } else { + this.modalBusy = false; + this.visible = false; + } + } + + openModal() { + if (!this.providerKey || !this.providerName) { + throw new Error('Provider Key and Provider Name are required.'); + } + + return this.store + .dispatch( + new GetPermissions({ + providerKey: this.providerKey, + providerName: this.providerName, + }) + ) + .pipe( + pluck('PermissionManagementState', 'permissionRes'), + tap((permissionRes: PermissionManagement.Response) => { + this.selectedGroup = permissionRes.groups[0]; + this.permissions = getPermissions(permissionRes.groups); + }) + ); + } + + initModal() { + this.setTabCheckboxState(); + this.setGrantCheckboxState(); + } + + onVisibleChange(visible: boolean) { + this.visible = visible; + + if (this.replaceableData) { + this.replaceableData.inputs.visible = visible; + this.replaceableData.outputs.visibleChange(visible); + } + } +} + +function findMargin( + permissions: PermissionManagement.Permission[], + permission: PermissionManagement.Permission +) { + const parentPermission = permissions.find((per) => per.name === permission.parentName); + + if (parentPermission && parentPermission.parentName) { + let margin = 20; + return (margin += findMargin(permissions, parentPermission)); + } + + return parentPermission ? 20 : 0; +} + +function getPermissions(groups: PermissionManagement.Group[]): PermissionManagement.Permission[] { + return groups.reduce((acc, val) => [...acc, ...val.permissions], []); +} +``` + +Open the generated `permission-management.component.html` in `src/app/permission-management` folder and replace the content with the below: + +```html + + + +

+ {%{{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}}%} - {%{{{ data.entityName }}}%} +

+
+ +
+ + +
+ +
+
+ +
+

{%{{{ selectedGroup?.displayName }}}%}

+
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + + {%{{{ + 'AbpIdentity::Save' | abpLocalization + }}}%} + +
+
+``` + +Open `app.component.ts` in `src/app` folder and modify it as shown below: + +```js +import { AddReplaceableComponent } from '@abp/ng.core'; +import { ePermissionManagementComponents } from '@abp/ng.permission-management'; +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { PermissionManagementComponent } from './permission-management/permission-management.component'; + +//... +export class AppComponent implements OnInit { + constructor(private store: Store) {} // injected store + + ngOnInit() { + // added dispatching the AddReplaceableComponent action + this.store.dispatch( + new AddReplaceableComponent({ + component: PermissionManagementComponent, + key: ePermissionManagementComponents.PermissionManagement, + }) + ); + } +} +``` + +## See Also + +- [Component Replacement](./Component-Replacement.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/images/permission-management-modal.png b/docs/en/UI/Angular/images/permission-management-modal.png new file mode 100644 index 0000000000..05cee8a2e1 Binary files /dev/null and b/docs/en/UI/Angular/images/permission-management-modal.png differ