mirror of https://github.com/abpframework/abp.git
committed by
GitHub
58 changed files with 8890 additions and 2230 deletions
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<div class="abp-lookup-container position-relative"> |
|||
@if (label()) { |
|||
<label class="form-label">{{ label() | abpLocalization }}</label> |
|||
} |
|||
|
|||
<div class="input-group"> |
|||
<input type="text" class="form-control" [placeholder]="placeholder() | abpLocalization" [ngModel]="displayValue()" |
|||
(ngModelChange)="onSearchInput($event)" (focus)="onSearchFocus()" (blur)="onSearchBlur($event)" |
|||
[disabled]="disabled()" /> |
|||
@if (displayValue() && !disabled()) { |
|||
<button type="button" class="btn btn-outline-secondary" (mousedown)="clearSelection()" tabindex="-1"> |
|||
<i class="fa fa-times"></i> |
|||
</button> |
|||
} |
|||
</div> |
|||
|
|||
@if (showDropdown() && !disabled()) { |
|||
<div class="abp-lookup-dropdown list-group position-absolute w-100 shadow"> |
|||
@if (isLoading()) { |
|||
<div class="list-group-item text-center py-3"> |
|||
<i class="fa fa-spinner fa-spin me-2"></i> |
|||
{{ 'AbpUi::Loading' | abpLocalization }} |
|||
</div> |
|||
} @else if (searchResults().length > 0) { |
|||
@for (item of searchResults(); track item.key) { |
|||
<button type="button" class="list-group-item list-group-item-action" (mousedown)="selectItem(item)"> |
|||
@if (itemTemplate()) { |
|||
<ng-container *ngTemplateOutlet="itemTemplate()!; context: { $implicit: item }" /> |
|||
} @else { |
|||
{{ getDisplayValue(item) }} |
|||
} |
|||
</button> |
|||
} |
|||
} @else if (displayValue()) { |
|||
@if (noResultsTemplate()) { |
|||
<ng-container *ngTemplateOutlet="noResultsTemplate()!" /> |
|||
} @else { |
|||
<div class="list-group-item text-muted"> |
|||
{{ 'AbpUi::NoRecordsFound' | abpLocalization }} |
|||
</div> |
|||
} |
|||
} |
|||
</div> |
|||
} |
|||
</div> |
|||
@ -0,0 +1,5 @@ |
|||
.abp-lookup-dropdown { |
|||
z-index: 1050; |
|||
max-height: 200px; |
|||
overflow-y: auto; |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
import { |
|||
Component, |
|||
input, |
|||
output, |
|||
model, |
|||
signal, |
|||
OnInit, |
|||
ChangeDetectionStrategy, |
|||
TemplateRef, |
|||
contentChild, |
|||
DestroyRef, |
|||
inject, |
|||
} from '@angular/core'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { Subject, Observable, debounceTime, distinctUntilChanged, of, finalize } from 'rxjs'; |
|||
|
|||
export interface LookupItem { |
|||
key: string; |
|||
displayName: string; |
|||
[key: string]: unknown; |
|||
} |
|||
|
|||
export type LookupSearchFn<T = LookupItem> = (filter: string) => Observable<T[]>; |
|||
|
|||
@Component({ |
|||
selector: 'abp-lookup-search', |
|||
templateUrl: './lookup-search.component.html', |
|||
styleUrl: './lookup-search.component.scss', |
|||
imports: [CommonModule, FormsModule, LocalizationPipe], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class LookupSearchComponent<T extends LookupItem = LookupItem> implements OnInit { |
|||
private readonly destroyRef = inject(DestroyRef); |
|||
|
|||
readonly label = input<string>(); |
|||
readonly placeholder = input<string>(''); |
|||
readonly debounceTime = input<number>(300); |
|||
readonly minSearchLength = input<number>(0); |
|||
readonly displayKey = input<keyof T>('displayName' as keyof T); |
|||
readonly valueKey = input<keyof T>('key' as keyof T); |
|||
readonly disabled = input<boolean>(false); |
|||
|
|||
readonly searchFn = input<LookupSearchFn<T>>(() => of([])); |
|||
|
|||
readonly selectedValue = model<string>(''); |
|||
readonly displayValue = model<string>(''); |
|||
|
|||
readonly itemSelected = output<T>(); |
|||
readonly searchChanged = output<string>(); |
|||
|
|||
readonly itemTemplate = contentChild<TemplateRef<{ $implicit: T }>>('itemTemplate'); |
|||
readonly noResultsTemplate = contentChild<TemplateRef<void>>('noResultsTemplate'); |
|||
|
|||
readonly searchResults = signal<T[]>([]); |
|||
readonly showDropdown = signal(false); |
|||
readonly isLoading = signal(false); |
|||
|
|||
private readonly searchSubject = new Subject<string>(); |
|||
|
|||
ngOnInit() { |
|||
this.searchSubject.pipe( |
|||
debounceTime(this.debounceTime()), |
|||
distinctUntilChanged(), |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe(filter => { |
|||
this.performSearch(filter); |
|||
}); |
|||
} |
|||
|
|||
onSearchInput(filter: string) { |
|||
this.displayValue.set(filter); |
|||
this.showDropdown.set(true); |
|||
this.searchChanged.emit(filter); |
|||
|
|||
if (filter.length >= this.minSearchLength()) { |
|||
this.searchSubject.next(filter); |
|||
} else { |
|||
this.searchResults.set([]); |
|||
} |
|||
} |
|||
|
|||
onSearchFocus() { |
|||
this.showDropdown.set(true); |
|||
const currentFilter = this.displayValue() || ''; |
|||
if (currentFilter.length >= this.minSearchLength()) { |
|||
this.performSearch(currentFilter); |
|||
} |
|||
} |
|||
|
|||
onSearchBlur(event: FocusEvent) { |
|||
const relatedTarget = event.relatedTarget as HTMLElement; |
|||
if (!relatedTarget?.closest('.abp-lookup-dropdown')) { |
|||
this.showDropdown.set(false); |
|||
} |
|||
} |
|||
|
|||
selectItem(item: T) { |
|||
const displayKeyValue = String(item[this.displayKey()] ?? ''); |
|||
const valueKeyValue = String(item[this.valueKey()] ?? ''); |
|||
|
|||
this.displayValue.set(displayKeyValue); |
|||
this.selectedValue.set(valueKeyValue); |
|||
this.searchResults.set([]); |
|||
this.showDropdown.set(false); |
|||
this.itemSelected.emit(item); |
|||
} |
|||
|
|||
clearSelection() { |
|||
this.displayValue.set(''); |
|||
this.selectedValue.set(''); |
|||
this.searchResults.set([]); |
|||
} |
|||
|
|||
private performSearch(filter: string) { |
|||
this.isLoading.set(true); |
|||
|
|||
this.searchFn()(filter).pipe( |
|||
takeUntilDestroyed(this.destroyRef), |
|||
finalize(() => this.isLoading.set(false)) |
|||
).subscribe({ |
|||
next: results => { |
|||
this.searchResults.set(results); |
|||
}, |
|||
error: () => { |
|||
this.searchResults.set([]); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
getDisplayValue(item: T): string { |
|||
return String(item[this.displayKey()] ?? item[this.valueKey()] ?? ''); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './lib/lookup-search.component'; |
|||
File diff suppressed because it is too large
@ -1 +1,6 @@ |
|||
export * from './permission-management.component'; |
|||
export * from './resource-permission-management/resource-permission-management.component'; |
|||
export * from './resource-permission-management/provider-key-search/provider-key-search.component'; |
|||
export * from './resource-permission-management/permission-checkbox-list/permission-checkbox-list.component'; |
|||
export * from './resource-permission-management/resource-permission-list/resource-permission-list.component'; |
|||
export * from './resource-permission-management/resource-permission-form/resource-permission-form.component'; |
|||
|
|||
@ -0,0 +1,25 @@ |
|||
<div class="mb-3"> |
|||
@if (showTitle()) { |
|||
<h5>{{ title() | abpLocalization }}</h5> |
|||
} |
|||
<div class="form-check form-switch mb-2"> |
|||
<input class="form-check-input" type="checkbox" id="grantAll-{{ idPrefix() }}" |
|||
[checked]="state.allPermissionsSelected()" |
|||
(change)="state.toggleAllPermissions(!state.allPermissionsSelected())" /> |
|||
<label class="form-check-label" for="grantAll-{{ idPrefix() }}"> |
|||
{{ 'AbpPermissionManagement::GrantAllResourcePermissions' | abpLocalization }} |
|||
</label> |
|||
</div> |
|||
<div class="abp-permission-list-container border rounded p-3"> |
|||
@for (perm of permissions(); track perm.name) { |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="checkbox" id="perm-{{ idPrefix() }}-{{ perm.name }}" |
|||
[checked]="state.isPermissionSelected(perm.name || '')" |
|||
(change)="state.togglePermission(perm.name || '')" /> |
|||
<label class="form-check-label" for="perm-{{ idPrefix() }}-{{ perm.name }}"> |
|||
{{ perm.displayName }} |
|||
</label> |
|||
</div> |
|||
} |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,4 @@ |
|||
.abp-permission-list-container { |
|||
max-height: 300px; |
|||
overflow-y: auto; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
import { Component, input, inject, ChangeDetectionStrategy } from '@angular/core'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { ResourcePermissionStateService } from '../../../services/resource-permission-state.service'; |
|||
|
|||
interface PermissionItem { |
|||
name?: string | null; |
|||
displayName?: string | null; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-permission-checkbox-list', |
|||
templateUrl: './permission-checkbox-list.component.html', |
|||
styleUrl: './permission-checkbox-list.component.scss', |
|||
imports: [LocalizationPipe], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class PermissionCheckboxListComponent { |
|||
readonly state = inject(ResourcePermissionStateService); |
|||
|
|||
readonly permissions = input.required<PermissionItem[]>(); |
|||
readonly idPrefix = input<string>('default'); |
|||
readonly title = input<string>('AbpPermissionManagement::ResourcePermissionPermissions'); |
|||
readonly showTitle = input<boolean>(true); |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
<abp-lookup-search label="AbpPermissionManagement::SearchProviderKey" |
|||
placeholder="AbpPermissionManagement::SearchProviderKey" [searchFn]="searchFn" [displayValue]="state.searchFilter()" |
|||
(displayValueChange)="state.searchFilter.set($event)" [selectedValue]="state.selectedProviderKey()" |
|||
(selectedValueChange)="state.selectedProviderKey.set($event)" (itemSelected)="onItemSelected($event)" /> |
|||
@ -0,0 +1,61 @@ |
|||
import { Component, input, inject, OnInit, ChangeDetectionStrategy, DestroyRef } from '@angular/core'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { PermissionsService } from '@abp/ng.permission-management/proxy'; |
|||
import { LookupSearchComponent, LookupItem } from '@abp/ng.components/lookup'; |
|||
import { Observable, map } from 'rxjs'; |
|||
import { ResourcePermissionStateService } from '../../../services/resource-permission-state.service'; |
|||
|
|||
interface ProviderKeyLookupItem extends LookupItem { |
|||
providerKey: string; |
|||
providerDisplayName?: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-provider-key-search', |
|||
templateUrl: './provider-key-search.component.html', |
|||
imports: [LookupSearchComponent], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class ProviderKeySearchComponent implements OnInit { |
|||
readonly state = inject(ResourcePermissionStateService); |
|||
private readonly service = inject(PermissionsService); |
|||
private readonly destroyRef = inject(DestroyRef); |
|||
|
|||
readonly resourceName = input.required<string>(); |
|||
|
|||
searchFn: (filter: string) => Observable<ProviderKeyLookupItem[]> = () => new Observable(); |
|||
|
|||
ngOnInit() { |
|||
this.searchFn = (filter: string) => this.loadProviderKeys(filter); |
|||
} |
|||
|
|||
onItemSelected(item: ProviderKeyLookupItem) { |
|||
// State is already updated via displayValue and selectedValue bindings
|
|||
// This handler can be used for additional side effects if needed
|
|||
} |
|||
|
|||
private loadProviderKeys(filter: string): Observable<ProviderKeyLookupItem[]> { |
|||
const providerName = this.state.selectedProviderName(); |
|||
if (!providerName) { |
|||
return new Observable(subscriber => { |
|||
subscriber.next([]); |
|||
subscriber.complete(); |
|||
}); |
|||
} |
|||
|
|||
return this.service.searchResourceProviderKey( |
|||
this.resourceName(), |
|||
providerName, |
|||
filter, |
|||
1 |
|||
).pipe( |
|||
map(res => (res.keys || []).map(k => ({ |
|||
key: k.providerKey || '', |
|||
displayName: k.providerDisplayName || k.providerKey || '', |
|||
providerKey: k.providerKey || '', |
|||
providerDisplayName: k.providerDisplayName || undefined, |
|||
}))), |
|||
takeUntilDestroyed(this.destroyRef) |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
@if (mode() === eResourcePermissionViewModes.Add) { |
|||
<div class="mb-3"> |
|||
<label class="form-label fw-bold"> |
|||
{{ 'AbpPermissionManagement::SelectProvider' | abpLocalization }} |
|||
</label> |
|||
<div class="mb-2"> |
|||
@for (provider of state.providers(); track provider.name; let i = $index) { |
|||
<div class="form-check form-check-inline"> |
|||
<input class="form-check-input" type="radio" [id]="'provider-' + i" [value]="provider.name" |
|||
[checked]="state.selectedProviderName() === provider.name" |
|||
(change)="state.onProviderChange(provider.name || '')" /> |
|||
<label class="form-check-label" [for]="'provider-' + i"> |
|||
{{ provider.displayName }} |
|||
</label> |
|||
</div> |
|||
} |
|||
</div> |
|||
|
|||
<abp-provider-key-search [resourceName]="resourceName()" /> |
|||
</div> |
|||
|
|||
<abp-permission-checkbox-list [permissions]="state.permissionDefinitions()" idPrefix="add" /> |
|||
} @else { |
|||
<div class="mb-3" id="permissionList"> |
|||
<h4>{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}</h4> |
|||
<abp-permission-checkbox-list [permissions]="state.permissionsWithProvider()" idPrefix="edit" [showTitle]="false" /> |
|||
</div> |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
import { Component, input, inject, output, ChangeDetectionStrategy } from '@angular/core'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { ResourcePermissionStateService } from '../../../services/resource-permission-state.service'; |
|||
import { ProviderKeySearchComponent } from '../provider-key-search/provider-key-search.component'; |
|||
import { PermissionCheckboxListComponent } from '../permission-checkbox-list/permission-checkbox-list.component'; |
|||
|
|||
import { eResourcePermissionViewModes } from '../../../enums/view-modes'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-resource-permission-form', |
|||
templateUrl: './resource-permission-form.component.html', |
|||
imports: [ |
|||
FormsModule, |
|||
LocalizationPipe, |
|||
ProviderKeySearchComponent, |
|||
PermissionCheckboxListComponent, |
|||
], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class ResourcePermissionFormComponent { |
|||
readonly state = inject(ResourcePermissionStateService); |
|||
readonly eResourcePermissionViewModes = eResourcePermissionViewModes; |
|||
|
|||
readonly mode = input.required<eResourcePermissionViewModes>(); |
|||
readonly resourceName = input.required<string>(); |
|||
|
|||
readonly save = output<void>(); |
|||
readonly cancel = output<void>(); |
|||
} |
|||
|
|||
@ -0,0 +1,41 @@ |
|||
<div class="d-grid gap-2 mb-3 d-md-flex justify-content-md-end"> |
|||
<button class="btn btn-sm btn-primary" type="button" (click)="addClicked.emit()"> |
|||
<i class="fa fa-plus me-1"></i> |
|||
{{ 'AbpPermissionManagement::AddResourcePermission' | abpLocalization }} |
|||
</button> |
|||
</div> |
|||
|
|||
<ng-template #actionsTemplate let-row> |
|||
<div class="btn-group btn-group-sm" role="group"> |
|||
<button |
|||
class="btn btn-outline-primary" |
|||
type="button" |
|||
(click)="editClicked.emit(row)" |
|||
[title]="'AbpUi::Edit' | abpLocalization" |
|||
> |
|||
<i class="fa fa-edit"></i> |
|||
</button> |
|||
<button |
|||
class="btn btn-outline-danger" |
|||
type="button" |
|||
(click)="deleteClicked.emit(row)" |
|||
[title]="'AbpUi::Delete' | abpLocalization" |
|||
> |
|||
<i class="fa fa-trash"></i> |
|||
</button> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
@if (state.resourcePermissions().length > 0) { |
|||
<abp-extensible-table |
|||
[data]="state.resourcePermissions()" |
|||
[recordsTotal]="state.totalCount()" |
|||
[list]="list" |
|||
[actionsTemplate]="actionsTemplate" |
|||
actionsText="AbpUi::Actions" |
|||
/> |
|||
} @else { |
|||
<div class="alert alert-info"> |
|||
{{ 'AbpPermissionManagement::NoPermissionsAssigned' | abpLocalization }} |
|||
</div> |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
import { Component, inject, output, ChangeDetectionStrategy } from '@angular/core'; |
|||
import { ListService, LocalizationPipe } from '@abp/ng.core'; |
|||
import { ExtensibleTableComponent, EXTENSIONS_IDENTIFIER } from '@abp/ng.components/extensible'; |
|||
import { ResourcePermissionGrantInfoDto } from '@abp/ng.permission-management/proxy'; |
|||
import { ResourcePermissionStateService } from '../../../services/resource-permission-state.service'; |
|||
import { ePermissionManagementComponents } from '../../../enums/components'; |
|||
import { configureResourcePermissionExtensions } from '../../../services/extensions.service'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-resource-permission-list', |
|||
templateUrl: './resource-permission-list.component.html', |
|||
providers: [ |
|||
ListService, |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: ePermissionManagementComponents.ResourcePermissions, |
|||
}, |
|||
], |
|||
imports: [LocalizationPipe, ExtensibleTableComponent], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class ResourcePermissionListComponent { |
|||
readonly state = inject(ResourcePermissionStateService); |
|||
readonly list = inject(ListService); |
|||
|
|||
readonly addClicked = output<void>(); |
|||
readonly editClicked = output<ResourcePermissionGrantInfoDto>(); |
|||
readonly deleteClicked = output<ResourcePermissionGrantInfoDto>(); |
|||
|
|||
constructor() { |
|||
configureResourcePermissionExtensions(); |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
<abp-modal [(visible)]="visible" [busy]="state.modalBusy()" [options]="{ size: 'xl', scrollable: false }"> |
|||
<ng-template #abpHeader> |
|||
<h5 class="modal-title"> |
|||
@switch (state.viewMode()) { |
|||
@case (eResourcePermissionViewModes.Edit) { |
|||
{{ 'AbpPermissionManagement::UpdatePermission' | abpLocalization }} |
|||
} |
|||
@case (eResourcePermissionViewModes.Add) { |
|||
{{ 'AbpPermissionManagement::AddResourcePermission' | abpLocalization }} |
|||
} |
|||
@default { |
|||
{{ 'AbpPermissionManagement::ResourcePermissions' | abpLocalization }} |
|||
@if (resourceDisplayName()) { |
|||
- {{ resourceDisplayName() }} |
|||
} |
|||
} |
|||
} |
|||
</h5> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (!state.hasResourcePermission() || !state.hasProviderKeyLookupService()) { |
|||
<div class="alert alert-warning" role="alert"> |
|||
@if (!state.hasResourcePermission()) { |
|||
{{ 'AbpPermissionManagement::NoResourcePermissionFound' | abpLocalization }} |
|||
} @else { |
|||
{{ 'AbpPermissionManagement::NoResourceProviderKeyLookupServiceFound' | abpLocalization }} |
|||
} |
|||
</div> |
|||
} @else { |
|||
@switch (state.viewMode()) { |
|||
@case (eResourcePermissionViewModes.List) { |
|||
<abp-resource-permission-list (addClicked)="onAddClicked()" (editClicked)="onEditClicked($event)" |
|||
(deleteClicked)="onDeleteClicked($event)" /> |
|||
} |
|||
@case (eResourcePermissionViewModes.Add) { |
|||
<abp-resource-permission-form mode="add" [resourceName]="resourceName()" /> |
|||
} |
|||
@case (eResourcePermissionViewModes.Edit) { |
|||
<abp-resource-permission-form mode="edit" [resourceName]="resourceName()" /> |
|||
} |
|||
} |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
@if (state.isListMode()) { |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Close' | abpLocalization }} |
|||
</button> |
|||
} @else { |
|||
<button type="button" class="btn btn-outline-secondary" (click)="state.goToListMode()"> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" (click)="savePermission()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
} |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,217 @@ |
|||
import { ListService, LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ButtonComponent, |
|||
Confirmation, |
|||
ConfirmationService, |
|||
ModalCloseDirective, |
|||
ModalComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { |
|||
PermissionsService, |
|||
ResourcePermissionGrantInfoDto, |
|||
} from '@abp/ng.permission-management/proxy'; |
|||
import { |
|||
Component, |
|||
inject, |
|||
input, |
|||
model, |
|||
OnInit, |
|||
effect, |
|||
untracked, |
|||
signal, |
|||
} from '@angular/core'; |
|||
import { finalize, switchMap, of } from 'rxjs'; |
|||
import { ResourcePermissionStateService } from '../../services/resource-permission-state.service'; |
|||
import { ResourcePermissionListComponent } from './resource-permission-list/resource-permission-list.component'; |
|||
import { ResourcePermissionFormComponent } from './resource-permission-form/resource-permission-form.component'; |
|||
|
|||
import { eResourcePermissionViewModes } from '../../enums/view-modes'; |
|||
|
|||
const DEFAULT_MAX_RESULT_COUNT = 10; |
|||
|
|||
@Component({ |
|||
selector: 'abp-resource-permission-management', |
|||
templateUrl: './resource-permission-management.component.html', |
|||
exportAs: 'abpResourcePermissionManagement', |
|||
providers: [ResourcePermissionStateService, ListService], |
|||
imports: [ |
|||
ModalComponent, |
|||
LocalizationPipe, |
|||
ButtonComponent, |
|||
ModalCloseDirective, |
|||
ResourcePermissionListComponent, |
|||
ResourcePermissionFormComponent, |
|||
], |
|||
}) |
|||
export class ResourcePermissionManagementComponent implements OnInit { |
|||
readonly eResourcePermissionViewModes = eResourcePermissionViewModes; |
|||
|
|||
protected readonly service = inject(PermissionsService); |
|||
protected readonly toasterService = inject(ToasterService); |
|||
protected readonly confirmationService = inject(ConfirmationService); |
|||
protected readonly state = inject(ResourcePermissionStateService); |
|||
private readonly list = inject(ListService); |
|||
|
|||
readonly resourceName = input.required<string>(); |
|||
readonly resourceKey = input.required<string>(); |
|||
readonly resourceDisplayName = input<string>(); |
|||
|
|||
readonly visible = model<boolean>(false); |
|||
|
|||
private readonly previousVisible = signal(false); |
|||
|
|||
constructor() { |
|||
effect(() => { |
|||
const resourceName = this.resourceName(); |
|||
const resourceKey = this.resourceKey(); |
|||
const resourceDisplayName = this.resourceDisplayName(); |
|||
|
|||
untracked(() => { |
|||
this.state.resourceName.set(resourceName); |
|||
this.state.resourceKey.set(resourceKey); |
|||
this.state.resourceDisplayName.set(resourceDisplayName); |
|||
}); |
|||
}); |
|||
|
|||
effect(() => { |
|||
const isVisible = this.visible(); |
|||
const wasVisible = this.previousVisible(); |
|||
if (isVisible && !wasVisible) { |
|||
this.openModal(); |
|||
} else if (!isVisible && wasVisible) { |
|||
this.state.reset(); |
|||
} |
|||
untracked(() => this.previousVisible.set(isVisible)); |
|||
}); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.list.maxResultCount = DEFAULT_MAX_RESULT_COUNT; |
|||
|
|||
this.list.hookToQuery(query => { |
|||
const allData = this.state.allResourcePermissions(); |
|||
const skipCount = query.skipCount || 0; |
|||
const maxResultCount = query.maxResultCount || DEFAULT_MAX_RESULT_COUNT; |
|||
|
|||
const paginatedData = allData.slice(skipCount, skipCount + maxResultCount); |
|||
|
|||
return of({ |
|||
items: paginatedData, |
|||
totalCount: allData.length |
|||
}); |
|||
}).subscribe(result => { |
|||
this.state.resourcePermissions.set(result.items); |
|||
this.state.totalCount.set(result.totalCount); |
|||
}); |
|||
} |
|||
|
|||
openModal() { |
|||
this.state.modalBusy.set(true); |
|||
|
|||
this.service.getResource(this.resourceName(), this.resourceKey()).pipe( |
|||
switchMap(permRes => { |
|||
this.state.setResourceData(permRes.permissions || []); |
|||
this.list.get(); |
|||
return this.service.getResourceProviderKeyLookupServices(this.resourceName()); |
|||
}), |
|||
switchMap(providerRes => { |
|||
this.state.setProviders(providerRes.providers || []); |
|||
return this.service.getResourceDefinitions(this.resourceName()); |
|||
}), |
|||
finalize(() => this.state.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: defRes => { |
|||
this.state.setDefinitions(defRes.permissions || []); |
|||
}, |
|||
error: () => { |
|||
this.toasterService.error('AbpPermissionManagement::ErrorLoadingPermissions'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
onAddClicked() { |
|||
this.state.goToAddMode(); |
|||
} |
|||
|
|||
onEditClicked(grant: ResourcePermissionGrantInfoDto) { |
|||
this.state.prepareEditMode(grant); |
|||
this.state.modalBusy.set(true); |
|||
|
|||
this.service.getResourceByProvider( |
|||
this.resourceName(), |
|||
this.resourceKey(), |
|||
grant.providerName || '', |
|||
grant.providerKey || '' |
|||
).pipe( |
|||
finalize(() => this.state.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: res => { |
|||
this.state.setEditModePermissions(res.permissions || []); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
onDeleteClicked(grant: ResourcePermissionGrantInfoDto) { |
|||
this.confirmationService |
|||
.warn( |
|||
'AbpPermissionManagement::PermissionDeletionConfirmationMessage', |
|||
'AbpPermissionManagement::AreYouSure', |
|||
{ |
|||
messageLocalizationParams: [grant.providerKey || ''], |
|||
} |
|||
) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.state.modalBusy.set(true); |
|||
this.service.deleteResource( |
|||
this.resourceName(), |
|||
this.resourceKey(), |
|||
grant.providerName || '', |
|||
grant.providerKey || '' |
|||
).pipe( |
|||
switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())), |
|||
finalize(() => this.state.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: res => { |
|||
this.state.setResourceData(res.permissions || []); |
|||
this.list.get(); |
|||
this.toasterService.success('AbpUi::SuccessfullyDeleted'); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
savePermission() { |
|||
const isEdit = this.state.isEditMode(); |
|||
const providerName = isEdit ? this.state.editProviderName() : this.state.selectedProviderName(); |
|||
const providerKey = isEdit ? this.state.editProviderKey() : this.state.selectedProviderKey(); |
|||
|
|||
if (!isEdit && !this.state.canSave()) { |
|||
this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions'); |
|||
return; |
|||
} |
|||
|
|||
this.state.modalBusy.set(true); |
|||
this.service.updateResource( |
|||
this.resourceName(), |
|||
this.resourceKey(), |
|||
{ |
|||
providerName, |
|||
providerKey, |
|||
permissions: this.state.selectedPermissions() |
|||
} |
|||
).pipe( |
|||
switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())), |
|||
finalize(() => this.state.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: res => { |
|||
this.state.setResourceData(res.permissions || []); |
|||
this.list.get(); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.state.goToListMode(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
import { EntityProp, ePropType } from '@abp/ng.components/extensible'; |
|||
import { ResourcePermissionGrantInfoDto } from '@abp/ng.permission-management/proxy'; |
|||
import { of } from 'rxjs'; |
|||
|
|||
export const DEFAULT_RESOURCE_PERMISSION_ENTITY_PROPS = EntityProp.createMany<ResourcePermissionGrantInfoDto>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'providerWithKey', |
|||
displayName: 'AbpPermissionManagement::Provider', |
|||
sortable: false, |
|||
valueResolver: data => { |
|||
const providerName = data.record.providerName || ''; |
|||
const providerDisplayName = data.record.providerDisplayName || data.record.providerKey || ''; |
|||
// Get first letter of provider name for abbreviation
|
|||
const abbr = providerName.charAt(0).toUpperCase(); |
|||
return of( |
|||
`<span class="d-inline-block bg-light rounded-pill px-2 me-1 ms-1 mb-1" title="${data.record.providerNameDisplayName || providerName}">${abbr}</span>${providerDisplayName}` |
|||
); |
|||
}, |
|||
}, |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'permissions', |
|||
displayName: 'AbpPermissionManagement::Permissions', |
|||
sortable: false, |
|||
valueResolver: data => { |
|||
const permissions = data.record.permissions || []; |
|||
const pills = permissions |
|||
.map(p => `<span class="d-inline-block bg-light rounded-pill px-2 me-1 mb-1">${p.displayName}</span>`) |
|||
.join(''); |
|||
return of(pills); |
|||
}, |
|||
}, |
|||
]); |
|||
@ -0,0 +1 @@ |
|||
export * from './default-resource-permission-entity-props'; |
|||
@ -1,3 +1,4 @@ |
|||
export const enum ePermissionManagementComponents { |
|||
PermissionManagement = 'PermissionManagement.PermissionManagementComponent', |
|||
ResourcePermissions = 'PermissionManagement.ResourcePermissionsComponent', |
|||
} |
|||
|
|||
@ -0,0 +1,2 @@ |
|||
export * from './view-modes'; |
|||
export * from './components'; |
|||
@ -0,0 +1,5 @@ |
|||
export enum eResourcePermissionViewModes { |
|||
List = 'list', |
|||
Add = 'add', |
|||
Edit = 'edit', |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
import { |
|||
ExtensionsService, |
|||
mergeWithDefaultProps, |
|||
} from '@abp/ng.components/extensible'; |
|||
import { inject } from '@angular/core'; |
|||
import { |
|||
RESOURCE_PERMISSION_ENTITY_PROP_CONTRIBUTORS, |
|||
DEFAULT_RESOURCE_PERMISSION_ENTITY_PROPS_MAP, |
|||
} from '../tokens'; |
|||
|
|||
export function configureResourcePermissionExtensions() { |
|||
const extensions = inject(ExtensionsService); |
|||
|
|||
const config = { optional: true }; |
|||
|
|||
const propContributors = inject(RESOURCE_PERMISSION_ENTITY_PROP_CONTRIBUTORS, config) || {}; |
|||
|
|||
mergeWithDefaultProps( |
|||
extensions.entityProps, |
|||
DEFAULT_RESOURCE_PERMISSION_ENTITY_PROPS_MAP, |
|||
propContributors, |
|||
); |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export * from './extensions.service'; |
|||
export * from './resource-permission-state.service'; |
|||
@ -0,0 +1,164 @@ |
|||
import { Injectable, signal, computed } from '@angular/core'; |
|||
import { |
|||
ResourcePermissionGrantInfoDto, |
|||
ResourceProviderDto, |
|||
ResourcePermissionDefinitionDto, |
|||
SearchProviderKeyInfo, |
|||
ResourcePermissionWithProdiverGrantInfoDto, |
|||
} from '@abp/ng.permission-management/proxy'; |
|||
import { eResourcePermissionViewModes } from '../enums/view-modes'; |
|||
|
|||
@Injectable() |
|||
export class ResourcePermissionStateService { |
|||
// View state
|
|||
readonly viewMode = signal<eResourcePermissionViewModes>(eResourcePermissionViewModes.List); |
|||
readonly modalBusy = signal(false); |
|||
readonly hasResourcePermission = signal(false); |
|||
readonly hasProviderKeyLookupService = signal(false); |
|||
|
|||
// Resource data
|
|||
readonly resourceName = signal(''); |
|||
readonly resourceKey = signal(''); |
|||
readonly resourceDisplayName = signal<string | undefined>(undefined); |
|||
|
|||
// Permissions data
|
|||
readonly allResourcePermissions = signal<ResourcePermissionGrantInfoDto[]>([]); |
|||
readonly resourcePermissions = signal<ResourcePermissionGrantInfoDto[]>([]); |
|||
readonly totalCount = signal(0); |
|||
readonly permissionDefinitions = signal<ResourcePermissionDefinitionDto[]>([]); |
|||
readonly permissionsWithProvider = signal<ResourcePermissionWithProdiverGrantInfoDto[]>([]); |
|||
readonly selectedPermissions = signal<string[]>([]); |
|||
|
|||
// Provider data
|
|||
readonly providers = signal<ResourceProviderDto[]>([]); |
|||
readonly selectedProviderName = signal(''); |
|||
readonly selectedProviderKey = signal(''); |
|||
|
|||
// Edit mode specific
|
|||
readonly editProviderName = signal(''); |
|||
readonly editProviderKey = signal(''); |
|||
|
|||
// Search state
|
|||
readonly searchFilter = signal(''); |
|||
readonly searchResults = signal<SearchProviderKeyInfo[]>([]); |
|||
readonly showDropdown = signal(false); |
|||
|
|||
// Computed properties
|
|||
readonly isAddMode = computed(() => this.viewMode() === eResourcePermissionViewModes.Add); |
|||
readonly isEditMode = computed(() => this.viewMode() === eResourcePermissionViewModes.Edit); |
|||
readonly isListMode = computed(() => this.viewMode() === eResourcePermissionViewModes.List); |
|||
|
|||
readonly currentPermissionsList = computed(() => |
|||
this.isAddMode() ? this.permissionDefinitions() : this.permissionsWithProvider() |
|||
); |
|||
|
|||
readonly allPermissionsSelected = computed(() => { |
|||
const definitions = this.currentPermissionsList(); |
|||
return definitions.length > 0 && |
|||
definitions.every(p => this.selectedPermissions().includes(p.name || '')); |
|||
}); |
|||
|
|||
readonly canSave = computed(() => { |
|||
if (this.isAddMode()) { |
|||
return !!this.selectedProviderKey() && this.selectedPermissions().length > 0; |
|||
} |
|||
return this.selectedPermissions().length >= 0; |
|||
}); |
|||
|
|||
// State transition methods
|
|||
goToListMode() { |
|||
this.viewMode.set(eResourcePermissionViewModes.List); |
|||
this.selectedPermissions.set([]); |
|||
} |
|||
|
|||
goToAddMode() { |
|||
this.viewMode.set(eResourcePermissionViewModes.Add); |
|||
this.selectedPermissions.set([]); |
|||
this.selectedProviderKey.set(''); |
|||
this.searchResults.set([]); |
|||
this.searchFilter.set(''); |
|||
} |
|||
|
|||
prepareEditMode(grant: ResourcePermissionGrantInfoDto) { |
|||
this.editProviderName.set(grant.providerName || ''); |
|||
this.editProviderKey.set(grant.providerKey || ''); |
|||
} |
|||
|
|||
setEditModePermissions(permissions: ResourcePermissionWithProdiverGrantInfoDto[]) { |
|||
this.permissionsWithProvider.set(permissions); |
|||
this.selectedPermissions.set( |
|||
permissions.filter(p => p.isGranted).map(p => p.name || '') |
|||
); |
|||
this.viewMode.set(eResourcePermissionViewModes.Edit); |
|||
} |
|||
|
|||
// Permission selection methods
|
|||
togglePermission(permissionName: string) { |
|||
const current = this.selectedPermissions(); |
|||
if (current.includes(permissionName)) { |
|||
this.selectedPermissions.set(current.filter(p => p !== permissionName)); |
|||
} else { |
|||
this.selectedPermissions.set([...current, permissionName]); |
|||
} |
|||
} |
|||
|
|||
toggleAllPermissions(selectAll: boolean) { |
|||
const permissions = this.currentPermissionsList(); |
|||
this.selectedPermissions.set( |
|||
selectAll ? permissions.map(p => p.name || '') : [] |
|||
); |
|||
} |
|||
|
|||
isPermissionSelected(permissionName: string): boolean { |
|||
return this.selectedPermissions().includes(permissionName); |
|||
} |
|||
|
|||
// Provider search methods
|
|||
onProviderChange(providerName: string) { |
|||
this.selectedProviderName.set(providerName); |
|||
this.selectedProviderKey.set(''); |
|||
this.searchResults.set([]); |
|||
this.searchFilter.set(''); |
|||
} |
|||
|
|||
selectProviderKey(key: SearchProviderKeyInfo) { |
|||
this.selectedProviderKey.set(key.providerKey || ''); |
|||
this.searchFilter.set(key.providerDisplayName || key.providerKey || ''); |
|||
this.searchResults.set([]); |
|||
this.showDropdown.set(false); |
|||
} |
|||
|
|||
// Reset all state
|
|||
reset() { |
|||
this.viewMode.set(eResourcePermissionViewModes.List); |
|||
this.allResourcePermissions.set([]); |
|||
this.resourcePermissions.set([]); |
|||
this.totalCount.set(0); |
|||
this.selectedProviderName.set(''); |
|||
this.selectedProviderKey.set(''); |
|||
this.searchFilter.set(''); |
|||
this.selectedPermissions.set([]); |
|||
this.searchResults.set([]); |
|||
this.editProviderName.set(''); |
|||
this.editProviderKey.set(''); |
|||
} |
|||
|
|||
// Data loading helpers
|
|||
setResourceData(permissions: ResourcePermissionGrantInfoDto[]) { |
|||
this.allResourcePermissions.set(permissions); |
|||
this.totalCount.set(permissions.length); |
|||
} |
|||
|
|||
setProviders(providers: ResourceProviderDto[]) { |
|||
this.providers.set(providers); |
|||
this.hasProviderKeyLookupService.set(providers.length > 0); |
|||
if (providers.length) { |
|||
this.selectedProviderName.set(providers[0].name || ''); |
|||
} |
|||
} |
|||
|
|||
setDefinitions(permissions: ResourcePermissionDefinitionDto[]) { |
|||
this.permissionDefinitions.set(permissions); |
|||
this.hasResourcePermission.set(permissions.length > 0); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
import { ResourcePermissionGrantInfoDto } from '@abp/ng.permission-management/proxy'; |
|||
import { EntityPropContributorCallback } from '@abp/ng.components/extensible'; |
|||
import { InjectionToken } from '@angular/core'; |
|||
import { DEFAULT_RESOURCE_PERMISSION_ENTITY_PROPS } from '../defaults/default-resource-permission-entity-props'; |
|||
import { ePermissionManagementComponents } from '../enums/components'; |
|||
|
|||
export const DEFAULT_RESOURCE_PERMISSION_ENTITY_PROPS_MAP = { |
|||
[ePermissionManagementComponents.ResourcePermissions]: DEFAULT_RESOURCE_PERMISSION_ENTITY_PROPS, |
|||
}; |
|||
|
|||
export const RESOURCE_PERMISSION_ENTITY_PROP_CONTRIBUTORS = new InjectionToken<EntityPropContributors>( |
|||
'RESOURCE_PERMISSION_ENTITY_PROP_CONTRIBUTORS', |
|||
); |
|||
|
|||
type EntityPropContributors = Partial<{ |
|||
[ePermissionManagementComponents.ResourcePermissions]: EntityPropContributorCallback<ResourcePermissionGrantInfoDto>[]; |
|||
}>; |
|||
@ -0,0 +1 @@ |
|||
export * from './extensions.token'; |
|||
Loading…
Reference in new issue