diff --git a/npm/ng-packs/packages/components/lookup/ng-package.json b/npm/ng-packs/packages/components/lookup/ng-package.json new file mode 100644 index 0000000000..665ad2add2 --- /dev/null +++ b/npm/ng-packs/packages/components/lookup/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html new file mode 100644 index 0000000000..6dd240c996 --- /dev/null +++ b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html @@ -0,0 +1,46 @@ +
+ @if (label()) { + + } + +
+ + @if (displayValue() && !disabled()) { + + } +
+ + @if (showDropdown() && !disabled()) { +
+ @if (isLoading()) { +
+ + {{ 'AbpUi::Loading' | abpLocalization }} +
+ } @else if (searchResults().length > 0) { + @for (item of searchResults(); track item.key) { + + } + } @else if (displayValue()) { + @if (noResultsTemplate()) { + + } @else { +
+ {{ 'AbpUi::NoRecordsFound' | abpLocalization }} +
+ } + } +
+ } +
\ No newline at end of file diff --git a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts new file mode 100644 index 0000000000..17f30f83a8 --- /dev/null +++ b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts @@ -0,0 +1,144 @@ +import { + Component, + input, + output, + model, + signal, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + TemplateRef, + contentChild, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { LocalizationPipe } from '@abp/ng.core'; +import { Subject, Observable, debounceTime, distinctUntilChanged, takeUntil, of } from 'rxjs'; + +export interface LookupItem { + key: string; + displayName: string; + [key: string]: unknown; +} + +export type LookupSearchFn = (filter: string) => Observable; + +@Component({ + selector: 'abp-lookup-search', + templateUrl: './lookup-search.component.html', + imports: [CommonModule, FormsModule, LocalizationPipe], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LookupSearchComponent implements OnInit, OnDestroy { + // Inputs + readonly label = input(); + readonly placeholder = input(''); + readonly debounceTime = input(300); + readonly minSearchLength = input(0); + readonly displayKey = input('displayName' as keyof T); + readonly valueKey = input('key' as keyof T); + readonly disabled = input(false); + + // Search function - should be provided by parent + readonly searchFn = input>(() => of([])); + + // Two-way binding for selected value + readonly selectedValue = model(''); + readonly displayValue = model(''); + + // Outputs + readonly itemSelected = output(); + readonly searchChanged = output(); + + // Custom templates + readonly itemTemplate = contentChild>('itemTemplate'); + readonly noResultsTemplate = contentChild>('noResultsTemplate'); + + // Internal state + readonly searchResults = signal([]); + readonly showDropdown = signal(false); + readonly isLoading = signal(false); + + private readonly searchSubject = new Subject(); + private readonly destroy$ = new Subject(); + + ngOnInit() { + this.searchSubject.pipe( + debounceTime(this.debounceTime()), + distinctUntilChanged(), + takeUntil(this.destroy$) + ).subscribe(filter => { + this.performSearch(filter); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + 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( + takeUntil(this.destroy$) + ).subscribe({ + next: results => { + this.searchResults.set(results); + this.isLoading.set(false); + }, + error: () => { + this.searchResults.set([]); + this.isLoading.set(false); + } + }); + } + + getDisplayValue(item: T): string { + return String(item[this.displayKey()] ?? item[this.valueKey()] ?? ''); + } +} diff --git a/npm/ng-packs/packages/components/lookup/src/public-api.ts b/npm/ng-packs/packages/components/lookup/src/public-api.ts new file mode 100644 index 0000000000..b1232ef08f --- /dev/null +++ b/npm/ng-packs/packages/components/lookup/src/public-api.ts @@ -0,0 +1 @@ +export * from './lib/lookup-search.component'; diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html index 55e7000f27..41740a1f64 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html @@ -1,15 +1,4 @@ -
- - - @if (state.searchResults().length > 0 && state.showDropdown()) { -
- @for (result of state.searchResults(); track result.providerKey) { - - } -
- } -
\ No newline at end of file + \ No newline at end of file diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts index 57bbf8600a..d8f626db54 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts @@ -1,14 +1,19 @@ -import { Component, input, inject, output, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { Component, input, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; import { LocalizationPipe } from '@abp/ng.core'; import { PermissionsService, SearchProviderKeyInfo } from '@abp/ng.permission-management/proxy'; -import { Subject, debounceTime, distinctUntilChanged, takeUntil } from 'rxjs'; +import { LookupSearchComponent, LookupItem } from '@abp/ng.components/lookup'; +import { Observable, map, Subject, takeUntil } 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: [FormsModule, LocalizationPipe], + imports: [LocalizationPipe, LookupSearchComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProviderKeySearchComponent implements OnInit, OnDestroy { @@ -17,19 +22,12 @@ export class ProviderKeySearchComponent implements OnInit, OnDestroy { readonly resourceName = input.required(); - readonly keySelected = output(); - - private readonly searchSubject = new Subject(); private readonly destroy$ = new Subject(); + searchFn: (filter: string) => Observable = () => new Observable(); + ngOnInit() { - this.searchSubject.pipe( - debounceTime(300), - distinctUntilChanged(), - takeUntil(this.destroy$) - ).subscribe(filter => { - this.loadProviderKeys(filter); - }); + this.searchFn = (filter: string) => this.loadProviderKeys(filter); } ngOnDestroy() { @@ -37,40 +35,33 @@ export class ProviderKeySearchComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - onSearchInput(filter: string) { - this.state.searchFilter.set(filter); - this.state.showDropdown.set(true); - this.searchSubject.next(filter); - } - - onSearchFocus() { - this.state.showDropdown.set(true); - this.loadProviderKeys(this.state.searchFilter() || ''); + onItemSelected(item: ProviderKeyLookupItem) { + // State is already updated via displayValue and selectedValue bindings + // This handler can be used for additional side effects if needed } - onSearchBlur(event: FocusEvent) { - const relatedTarget = event.relatedTarget as HTMLElement; - if (!relatedTarget?.closest('.list-group')) { - this.state.showDropdown.set(false); - } - } - - selectProviderKey(key: SearchProviderKeyInfo) { - this.state.selectProviderKey(key); - this.keySelected.emit(key); - } - - private loadProviderKeys(filter: string) { + private loadProviderKeys(filter: string): Observable { const providerName = this.state.selectedProviderName(); - if (!providerName) return; + if (!providerName) { + return new Observable(subscriber => { + subscriber.next([]); + subscriber.complete(); + }); + } - this.service.searchResourceProviderKey( + return this.service.searchResourceProviderKey( this.resourceName(), providerName, filter, 1 - ).subscribe(res => { - this.state.searchResults.set(res.keys || []); - }); + ).pipe( + map(res => (res.keys || []).map(k => ({ + key: k.providerKey || '', + displayName: k.providerDisplayName || k.providerKey || '', + providerKey: k.providerKey || '', + providerDisplayName: k.providerDisplayName || undefined, + }))), + takeUntil(this.destroy$) + ); } } diff --git a/npm/ng-packs/tsconfig.base.json b/npm/ng-packs/tsconfig.base.json index f863f18520..07ab8e8ef4 100644 --- a/npm/ng-packs/tsconfig.base.json +++ b/npm/ng-packs/tsconfig.base.json @@ -10,45 +10,114 @@ "importHelpers": true, "target": "es2020", "module": "esnext", - "lib": ["es2020", "dom"], + "lib": [ + "es2020", + "dom" + ], "esModuleInterop": true, "baseUrl": "./", "allowSyntheticDefaultImports": true, "paths": { - "@abp/ng.account": ["packages/account/src/public-api.ts"], - "@abp/ng.account.core": ["packages/account-core/src/public-api.ts"], - "@abp/ng.account.core/proxy": ["packages/account-core/proxy/src/public-api.ts"], - "@abp/ng.account/config": ["packages/account/config/src/public-api.ts"], - "@abp/ng.components": ["packages/components/src/public-api.ts"], - "@abp/ng.components/chart.js": ["packages/components/chart.js/src/public-api.ts"], - "@abp/ng.components/extensible": ["packages/components/extensible/src/public-api.ts"], - "@abp/ng.components/page": ["packages/components/page/src/public-api.ts"], - "@abp/ng.components/tree": ["packages/components/tree/src/public-api.ts"], - "@abp/ng.core": ["packages/core/src/public-api.ts"], - "@abp/ng.core/locale": ["packages/core/locale/src/public-api.ts"], - "@abp/ng.core/testing": ["packages/core/testing/src/public-api.ts"], - "@abp/ng.feature-management": ["packages/feature-management/src/public-api.ts"], - "@abp/ng.feature-management/proxy": ["packages/feature-management/proxy/src/public-api.ts"], - "@abp/ng.identity": ["packages/identity/src/public-api.ts"], - "@abp/ng.identity/config": ["packages/identity/config/src/public-api.ts"], - "@abp/ng.identity/proxy": ["packages/identity/proxy/src/public-api.ts"], - "@abp/ng.oauth": ["packages/oauth/src/public-api.ts"], - "@abp/ng.permission-management": ["packages/permission-management/src/public-api.ts"], + "@abp/ng.account": [ + "packages/account/src/public-api.ts" + ], + "@abp/ng.account.core": [ + "packages/account-core/src/public-api.ts" + ], + "@abp/ng.account.core/proxy": [ + "packages/account-core/proxy/src/public-api.ts" + ], + "@abp/ng.account/config": [ + "packages/account/config/src/public-api.ts" + ], + "@abp/ng.components": [ + "packages/components/src/public-api.ts" + ], + "@abp/ng.components/chart.js": [ + "packages/components/chart.js/src/public-api.ts" + ], + "@abp/ng.components/extensible": [ + "packages/components/extensible/src/public-api.ts" + ], + "@abp/ng.components/lookup": [ + "packages/components/lookup/src/public-api.ts" + ], + "@abp/ng.components/page": [ + "packages/components/page/src/public-api.ts" + ], + "@abp/ng.components/tree": [ + "packages/components/tree/src/public-api.ts" + ], + "@abp/ng.core": [ + "packages/core/src/public-api.ts" + ], + "@abp/ng.core/locale": [ + "packages/core/locale/src/public-api.ts" + ], + "@abp/ng.core/testing": [ + "packages/core/testing/src/public-api.ts" + ], + "@abp/ng.feature-management": [ + "packages/feature-management/src/public-api.ts" + ], + "@abp/ng.feature-management/proxy": [ + "packages/feature-management/proxy/src/public-api.ts" + ], + "@abp/ng.identity": [ + "packages/identity/src/public-api.ts" + ], + "@abp/ng.identity/config": [ + "packages/identity/config/src/public-api.ts" + ], + "@abp/ng.identity/proxy": [ + "packages/identity/proxy/src/public-api.ts" + ], + "@abp/ng.oauth": [ + "packages/oauth/src/public-api.ts" + ], + "@abp/ng.permission-management": [ + "packages/permission-management/src/public-api.ts" + ], "@abp/ng.permission-management/proxy": [ "packages/permission-management/proxy/src/public-api.ts" ], - "@abp/ng.setting-management": ["packages/setting-management/src/public-api.ts"], - "@abp/ng.setting-management/config": ["packages/setting-management/config/src/public-api.ts"], - "@abp/ng.setting-management/proxy": ["packages/setting-management/proxy/src/public-api.ts"], - "@abp/ng.tenant-management": ["packages/tenant-management/src/public-api.ts"], - "@abp/ng.tenant-management/config": ["packages/tenant-management/config/src/public-api.ts"], - "@abp/ng.tenant-management/proxy": ["packages/tenant-management/proxy/src/public-api.ts"], - "@abp/ng.theme.basic": ["packages/theme-basic/src/public-api.ts"], - "@abp/ng.theme.basic/testing": ["packages/theme-basic/testing/src/public-api.ts"], - "@abp/ng.theme.shared": ["packages/theme-shared/src/public-api.ts"], - "@abp/ng.theme.shared/testing": ["packages/theme-shared/testing/src/public-api.ts"], - "@abp/nx.generators": ["packages/generators/src/index.ts"] + "@abp/ng.setting-management": [ + "packages/setting-management/src/public-api.ts" + ], + "@abp/ng.setting-management/config": [ + "packages/setting-management/config/src/public-api.ts" + ], + "@abp/ng.setting-management/proxy": [ + "packages/setting-management/proxy/src/public-api.ts" + ], + "@abp/ng.tenant-management": [ + "packages/tenant-management/src/public-api.ts" + ], + "@abp/ng.tenant-management/config": [ + "packages/tenant-management/config/src/public-api.ts" + ], + "@abp/ng.tenant-management/proxy": [ + "packages/tenant-management/proxy/src/public-api.ts" + ], + "@abp/ng.theme.basic": [ + "packages/theme-basic/src/public-api.ts" + ], + "@abp/ng.theme.basic/testing": [ + "packages/theme-basic/testing/src/public-api.ts" + ], + "@abp/ng.theme.shared": [ + "packages/theme-shared/src/public-api.ts" + ], + "@abp/ng.theme.shared/testing": [ + "packages/theme-shared/testing/src/public-api.ts" + ], + "@abp/nx.generators": [ + "packages/generators/src/index.ts" + ] } }, - "exclude": ["node_modules", "tmp"] -} + "exclude": [ + "node_modules", + "tmp" + ] +} \ No newline at end of file