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