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 './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 { |
export const enum ePermissionManagementComponents { |
||||
PermissionManagement = 'PermissionManagement.PermissionManagementComponent', |
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