Browse Source

Improve provider key search UX and add delete confirmation

Enhances the provider key search input with focus/blur handling and a dropdown for results, improving user experience. Adds a confirmation dialog before deleting a permission, preventing accidental deletions. Refactors related logic for clarity and better state management.
pull/24459/head
Fahri Gedik 5 months ago
parent
commit
6a6ae98c4e
  1. 9
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html
  2. 86
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts

9
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html

@ -48,7 +48,7 @@
@if (resourcePermissions().length > 0) { @if (resourcePermissions().length > 0) {
<abp-extensible-table [data]="resourcePermissions()" [recordsTotal]="totalCount()" [list]="list" <abp-extensible-table [data]="resourcePermissions()" [recordsTotal]="totalCount()" [list]="list"
[actionsTemplate]="actionsTemplate"></abp-extensible-table> [actionsTemplate]="actionsTemplate" actionsText="AbpUi::Actions"></abp-extensible-table>
} @else { } @else {
<div class="alert alert-info"> <div class="alert alert-info">
{{ 'AbpPermissionManagement::NoPermissionsAssigned' | abpLocalization }} {{ 'AbpPermissionManagement::NoPermissionsAssigned' | abpLocalization }}
@ -77,13 +77,14 @@
<label class="form-label">{{ 'AbpPermissionManagement::SearchProviderKey' | abpLocalization }}</label> <label class="form-label">{{ 'AbpPermissionManagement::SearchProviderKey' | abpLocalization }}</label>
<input type="text" class="form-control" <input type="text" class="form-control"
[placeholder]="'AbpPermissionManagement::SearchProviderKey' | abpLocalization" [placeholder]="'AbpPermissionManagement::SearchProviderKey' | abpLocalization"
[ngModel]="searchFilter()" (ngModelChange)="onSearchInput($event)" /> [ngModel]="searchFilter()" (ngModelChange)="onSearchInput($event)" (focus)="onSearchFocus()"
@if (searchResults().length > 0) { (blur)="onSearchBlur()" />
@if (searchResults().length > 0 && showDropdown()) {
<div class="list-group position-absolute w-100 shadow" <div class="list-group position-absolute w-100 shadow"
style="z-index: 1000; max-height: 200px; overflow-y: auto;"> style="z-index: 1000; max-height: 200px; overflow-y: auto;">
@for (result of searchResults(); track result.providerKey) { @for (result of searchResults(); track result.providerKey) {
<button type="button" class="list-group-item list-group-item-action" <button type="button" class="list-group-item list-group-item-action"
(click)="selectProviderKey(result)"> (mousedown)="selectProviderKey(result)">
{{ result.providerDisplayName || result.providerKey }} {{ result.providerDisplayName || result.providerKey }}
</button> </button>
} }

86
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts

@ -1,6 +1,8 @@
import { ListService, LocalizationPipe } from '@abp/ng.core'; import { ListService, LocalizationPipe } from '@abp/ng.core';
import { import {
ButtonComponent, ButtonComponent,
Confirmation,
ConfirmationService,
ModalCloseDirective, ModalCloseDirective,
ModalComponent, ModalComponent,
ToasterService, ToasterService,
@ -57,6 +59,7 @@ type ViewMode = 'list' | 'add' | 'edit';
export class ResourcePermissionManagementComponent implements OnInit { export class ResourcePermissionManagementComponent implements OnInit {
protected readonly service = inject(PermissionsService); protected readonly service = inject(PermissionsService);
protected readonly toasterService = inject(ToasterService); protected readonly toasterService = inject(ToasterService);
protected readonly confirmationService = inject(ConfirmationService);
readonly list = inject(ListService); readonly list = inject(ListService);
@Input() resourceName!: string; @Input() resourceName!: string;
@ -84,13 +87,11 @@ export class ResourcePermissionManagementComponent implements OnInit {
@Output() readonly visibleChange = new EventEmitter<boolean>(); @Output() readonly visibleChange = new EventEmitter<boolean>();
// State signals
viewMode = signal<ViewMode>('list'); viewMode = signal<ViewMode>('list');
modalBusy = signal(false); modalBusy = signal(false);
hasResourcePermission = signal(false); hasResourcePermission = signal(false);
hasProviderKeyLookupService = signal(false); hasProviderKeyLookupService = signal(false);
// Data
allResourcePermissions = signal<ResourcePermissionGrantInfoDto[]>([]); // All data for client-side pagination allResourcePermissions = signal<ResourcePermissionGrantInfoDto[]>([]); // All data for client-side pagination
resourcePermissions = signal<ResourcePermissionGrantInfoDto[]>([]); // Paginated data for table resourcePermissions = signal<ResourcePermissionGrantInfoDto[]>([]); // Paginated data for table
totalCount = signal(0); totalCount = signal(0);
@ -99,21 +100,19 @@ export class ResourcePermissionManagementComponent implements OnInit {
searchResults = signal<SearchProviderKeyInfo[]>([]); searchResults = signal<SearchProviderKeyInfo[]>([]);
permissionsWithProvider = signal<ResourcePermissionWithProdiverGrantInfoDto[]>([]); permissionsWithProvider = signal<ResourcePermissionWithProdiverGrantInfoDto[]>([]);
// Form state for add/edit
selectedProviderName = signal<string>(''); selectedProviderName = signal<string>('');
selectedProviderKey = signal<string>(''); selectedProviderKey = signal<string>('');
searchFilter = signal(''); searchFilter = signal('');
selectedPermissions = signal<string[]>([]); selectedPermissions = signal<string[]>([]);
// Edit mode state
editProviderName = signal<string>(''); editProviderName = signal<string>('');
editProviderKey = signal<string>(''); editProviderKey = signal<string>('');
// Search subject for debounce showDropdown = signal(false);
private searchSubject = new Subject<string>(); private searchSubject = new Subject<string>();
constructor() { constructor() {
// Configure extensions for entity props
configureResourcePermissionExtensions(); configureResourcePermissionExtensions();
this.searchSubject.pipe( this.searchSubject.pipe(
@ -125,16 +124,13 @@ export class ResourcePermissionManagementComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
// Configure list service for pagination
this.list.maxResultCount = 10; this.list.maxResultCount = 10;
// Hook to query for client-side pagination
this.list.hookToQuery(query => { this.list.hookToQuery(query => {
const allData = this.allResourcePermissions(); const allData = this.allResourcePermissions();
const skipCount = query.skipCount || 0; const skipCount = query.skipCount || 0;
const maxResultCount = query.maxResultCount || 10; const maxResultCount = query.maxResultCount || 10;
// Client-side pagination
const paginatedData = allData.slice(skipCount, skipCount + maxResultCount); const paginatedData = allData.slice(skipCount, skipCount + maxResultCount);
return of({ return of({
@ -150,12 +146,10 @@ export class ResourcePermissionManagementComponent implements OnInit {
openModal() { openModal() {
this.modalBusy.set(true); this.modalBusy.set(true);
// Load resource permissions and providers
this.service.getResource(this.resourceName, this.resourceKey).pipe( this.service.getResource(this.resourceName, this.resourceKey).pipe(
switchMap(permRes => { switchMap(permRes => {
this.allResourcePermissions.set(permRes.permissions || []); this.allResourcePermissions.set(permRes.permissions || []);
this.totalCount.set(permRes.permissions?.length || 0); this.totalCount.set(permRes.permissions?.length || 0);
// Trigger list refresh
this.list.get(); this.list.get();
return this.service.getResourceProviderKeyLookupServices(this.resourceName); return this.service.getResourceProviderKeyLookupServices(this.resourceName);
}), }),
@ -191,7 +185,6 @@ export class ResourcePermissionManagementComponent implements OnInit {
this.searchResults.set([]); this.searchResults.set([]);
} }
// View mode navigation
goToAddMode() { goToAddMode() {
this.viewMode.set('add'); this.viewMode.set('add');
this.selectedPermissions.set([]); this.selectedPermissions.set([]);
@ -227,7 +220,6 @@ export class ResourcePermissionManagementComponent implements OnInit {
this.selectedPermissions.set([]); this.selectedPermissions.set([]);
} }
// Provider selection
onProviderChange(providerName: string) { onProviderChange(providerName: string) {
this.selectedProviderName.set(providerName); this.selectedProviderName.set(providerName);
this.selectedProviderKey.set(''); this.selectedProviderKey.set('');
@ -235,14 +227,25 @@ export class ResourcePermissionManagementComponent implements OnInit {
this.searchFilter.set(''); this.searchFilter.set('');
} }
// Search
onSearchInput(filter: string) { onSearchInput(filter: string) {
this.searchFilter.set(filter); this.searchFilter.set(filter);
this.showDropdown.set(true);
this.searchSubject.next(filter); this.searchSubject.next(filter);
} }
private performSearch(filter: string) { onSearchFocus() {
if (!filter || !this.selectedProviderName()) return; this.showDropdown.set(true);
this.loadProviderKeys(this.searchFilter() || '');
}
onSearchBlur() {
setTimeout(() => {
this.showDropdown.set(false);
}, 200);
}
private loadProviderKeys(filter: string) {
if (!this.selectedProviderName()) return;
this.service.searchResourceProviderKey( this.service.searchResourceProviderKey(
this.resourceName, this.resourceName,
@ -254,13 +257,17 @@ export class ResourcePermissionManagementComponent implements OnInit {
}); });
} }
private performSearch(filter: string) {
this.loadProviderKeys(filter);
}
selectProviderKey(key: SearchProviderKeyInfo) { selectProviderKey(key: SearchProviderKeyInfo) {
this.selectedProviderKey.set(key.providerKey || ''); this.selectedProviderKey.set(key.providerKey || '');
this.searchFilter.set(key.providerDisplayName || key.providerKey || ''); this.searchFilter.set(key.providerDisplayName || key.providerKey || '');
this.searchResults.set([]); this.searchResults.set([]);
this.showDropdown.set(false);
} }
// Permission toggle
togglePermission(permissionName: string) { togglePermission(permissionName: string) {
const current = this.selectedPermissions(); const current = this.selectedPermissions();
if (current.includes(permissionName)) { if (current.includes(permissionName)) {
@ -298,7 +305,6 @@ export class ResourcePermissionManagementComponent implements OnInit {
definitions.every(p => this.selectedPermissions().includes(p.name || '')); definitions.every(p => this.selectedPermissions().includes(p.name || ''));
}); });
// Save operations
saveAddPermission() { saveAddPermission() {
if (!this.selectedProviderKey() || this.selectedPermissions().length === 0) { if (!this.selectedProviderKey() || this.selectedPermissions().length === 0) {
this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions'); this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions');
@ -351,21 +357,33 @@ export class ResourcePermissionManagementComponent implements OnInit {
} }
deletePermission(grant: ResourcePermissionGrantInfoDto) { deletePermission(grant: ResourcePermissionGrantInfoDto) {
this.modalBusy.set(true); this.confirmationService
this.service.deleteResource( .warn(
this.resourceName, 'AbpPermissionManagement::PermissionDeletionConfirmationMessage',
this.resourceKey, 'AbpPermissionManagement::AreYouSure',
grant.providerName || '', {
grant.providerKey || '' messageLocalizationParams: [grant.providerKey || ''],
).pipe( }
switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)), )
finalize(() => this.modalBusy.set(false)) .subscribe((status: Confirmation.Status) => {
).subscribe({ if (status === Confirmation.Status.confirm) {
next: res => { this.modalBusy.set(true);
this.allResourcePermissions.set(res.permissions || []); this.service.deleteResource(
this.list.get(); this.resourceName,
this.toasterService.success('AbpUi::SuccessfullyDeleted'); this.resourceKey,
} grant.providerName || '',
}); grant.providerKey || ''
).pipe(
switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)),
finalize(() => this.modalBusy.set(false))
).subscribe({
next: res => {
this.allResourcePermissions.set(res.permissions || []);
this.list.get();
this.toasterService.success('AbpUi::SuccessfullyDeleted');
}
});
}
});
} }
} }

Loading…
Cancel
Save