diff --git a/README.md b/README.md index 9aee6f25c..d138ebd42 100644 --- a/README.md +++ b/README.md @@ -100,5 +100,6 @@ This section provides clear and concise instructions for installing RocketAdmin ## Usage -1. After installation rocketadmin will create a user with email admin@email.local and autogenerated password. The message will be `Admin user created with email: "admin@email.local" and password: ""` -2. You can sign in using these credentials. We recommend to change email and password after first login +1. After installation, open RocketAdmin in your browser at `http://localhost:8080`. You will be redirected to the setup page. +2. On the setup page, create your admin account by entering your email and password. +3. After completing the setup, sign in with your new credentials. diff --git a/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.html b/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.html index 74f93bb83..0cd1c5af9 100644 --- a/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.html +++ b/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.html @@ -4,18 +4,22 @@
{{normalizedLabel}} - + @if (fetching()) { + + } + (ngModelChange)="onSearchInput()"> - - {{suggestion.displayString}} - + @for (suggestion of suggestions(); track suggestion.fieldValue) { + + {{suggestion.displayString}} + + } Improve search performance by configuring Foreign key search fields  here. diff --git a/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.spec.ts b/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.spec.ts index 25d86d9c7..3eb625c7a 100644 --- a/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.spec.ts +++ b/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.spec.ts @@ -152,7 +152,7 @@ describe('ForeignKeyFilterComponent', () => { expect(component).toBeTruthy(); }); - it('should fill initial dropdown values when identity_column is set', () => { + it('should fill initial dropdown values when identity_column is set', async () => { const usersTableNetworkWithIdentityColumn = { ...usersTableNetwork, identity_column: 'lastname' }; vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetworkWithIdentityColumn)); @@ -160,13 +160,14 @@ describe('ForeignKeyFilterComponent', () => { component.connectionID = '12345678'; component.value = '33'; // Must be truthy to trigger currentDisplayedString setting - fixture.detectChanges(); // This triggers ngOnInit + await component.ngOnInit(); + fixture.detectChanges(); expect(component.identityColumn).toEqual('lastname'); expect(component.currentDisplayedString).toEqual('Taylor (Alex | new-user-5@email.com)'); expect(component.currentFieldValue).toEqual(33); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'Taylor (Alex | new-user-5@email.com)', primaryKeys: { id: 33 }, @@ -185,20 +186,21 @@ describe('ForeignKeyFilterComponent', () => { ]); }); - it('should fill initial dropdown values when identity_column is not set', () => { + it('should fill initial dropdown values when identity_column is not set', async () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork)); component.connectionID = '12345678'; component.value = '33'; // Must be truthy to trigger currentDisplayedString setting - fixture.detectChanges(); // This triggers ngOnInit + await component.ngOnInit(); + fixture.detectChanges(); expect(component.identityColumn).toBeUndefined; expect(component.currentDisplayedString).toEqual('Alex | Taylor | new-user-5@email.com'); expect(component.currentFieldValue).toEqual(33); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'Alex | Taylor | new-user-5@email.com', primaryKeys: { id: 33 }, @@ -217,7 +219,7 @@ describe('ForeignKeyFilterComponent', () => { ]); }); - it('should fill initial dropdown values when autocomplete_columns is not set', () => { + it('should fill initial dropdown values when autocomplete_columns is not set', async () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork)); component.connectionID = '12345678'; @@ -231,13 +233,14 @@ describe('ForeignKeyFilterComponent', () => { }; component.value = '33'; // Must be truthy to trigger currentDisplayedString setting - fixture.detectChanges(); // This triggers ngOnInit + await component.ngOnInit(); + fixture.detectChanges(); expect(component.identityColumn).toBeUndefined; expect(component.currentDisplayedString).toEqual('33 | Alex | Taylor | new-user-5@email.com | 24'); expect(component.currentFieldValue).toEqual(33); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: '33 | Alex | Taylor | new-user-5@email.com | 24', primaryKeys: { id: 33 }, @@ -256,10 +259,10 @@ describe('ForeignKeyFilterComponent', () => { ]); }); - it('should set current value if necessary row is in suggestions list', () => { + it('should set current value if necessary row is in suggestions list', async () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork)); fixture.detectChanges(); - component.suggestions = [ + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', primaryKeys: { id: 33 }, @@ -275,17 +278,18 @@ describe('ForeignKeyFilterComponent', () => { primaryKeys: { id: 35 }, fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'Alex | Johnson | new-user-4@email.com'; - component.fetchSuggestions(); + await component.fetchSuggestions(); expect(component.currentFieldValue).toEqual(34); }); - it('should fetch suggestions list if user types search query and identity column is set', () => { + it('should fetch suggestions list if user types search query and identity column is set', async () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork)); fixture.detectChanges(); + const searchSuggestionsNetwork = { rows: [ { @@ -311,7 +315,7 @@ describe('ForeignKeyFilterComponent', () => { component.relations = fakeRelations; - component.suggestions = [ + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', fieldValue: 33, @@ -324,12 +328,12 @@ describe('ForeignKeyFilterComponent', () => { displayString: 'Alex | Smith | some-new@email.com', fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'John'; - component.fetchSuggestions(); + await component.fetchSuggestions(); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'Taylor (John | new-user-0@email.com)', primaryKeys: { id: 23 }, @@ -343,14 +347,14 @@ describe('ForeignKeyFilterComponent', () => { ]); }); - it('should fetch suggestions list if user types search query and show No matches message if the list is empty', () => { + it('should fetch suggestions list if user types search query and show No matches message if the list is empty', async () => { const searchSuggestionsNetwork = { rows: [], }; vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(searchSuggestionsNetwork)); - component.suggestions = [ + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', primaryKeys: { id: 33 }, @@ -366,19 +370,19 @@ describe('ForeignKeyFilterComponent', () => { primaryKeys: { id: 35 }, fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'skjfhskjdf'; - component.fetchSuggestions(); + await component.fetchSuggestions(); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'No field starts with "skjfhskjdf" in foreign entity.', }, ]); }); - it('should fetch suggestions list if user types search query and identity column is not set', () => { + it('should fetch suggestions list if user types search query and identity column is not set', async () => { const searchSuggestionsNetwork = { rows: [ { @@ -403,7 +407,7 @@ describe('ForeignKeyFilterComponent', () => { component.connectionID = '12345678'; component.relations = fakeRelations; - component.suggestions = [ + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', fieldValue: 33, @@ -416,10 +420,10 @@ describe('ForeignKeyFilterComponent', () => { displayString: 'Alex | Smith | some-new@email.com', fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'Alex'; - component.fetchSuggestions(); + await component.fetchSuggestions(); fixture.detectChanges(); @@ -433,7 +437,7 @@ describe('ForeignKeyFilterComponent', () => { referencedColumn: component.relations.referenced_column_name, }); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'John | Taylor | new-user-0@email.com', primaryKeys: { id: 23 }, diff --git a/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.ts b/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.ts index d0189c441..c014911ec 100644 --- a/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.ts +++ b/frontend/src/app/components/ui-components/filter-fields/foreign-key/foreign-key.component.ts @@ -1,176 +1,240 @@ -import { Component, Input } from '@angular/core'; -import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; - -import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field.component'; import { CommonModule } from '@angular/common'; -import { ConnectionsService } from 'src/app/services/connections.service'; +import { Component, Input, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSpinner } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { Subject } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; +import { ConnectionsService } from 'src/app/services/connections.service'; import { TablesService } from 'src/app/services/tables.service'; +import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field.component'; + +interface FetchTableResponse { + rows: Record[]; + primaryColumns: { column_name: string; data_type: string }[]; + identity_column?: string; +} interface Suggestion { - displayString: string; - primaryKeys?: any; - fieldValue?: any + displayString: string; + primaryKeys?: Record; + fieldValue?: unknown; } @Component({ - selector: 'app-filter-foreign-key', - templateUrl: './foreign-key.component.html', - styleUrls: ['./foreign-key.component.css'], - imports: [ - CommonModule, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatAutocompleteModule, - MatIconModule, - MatTooltipModule, - MatSpinner, - RouterModule - ] + selector: 'app-filter-foreign-key', + templateUrl: './foreign-key.component.html', + styleUrls: ['./foreign-key.component.css'], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatAutocompleteModule, + MatIconModule, + MatTooltipModule, + MatSpinner, + RouterModule, + ], }) export class ForeignKeyFilterComponent extends BaseFilterFieldComponent { - @Input() value; - - public connectionID: string; - public currentDisplayedString: string; - public currentFieldValue: any; - public currentFieldQueryParams: any; - public suggestions: Suggestion[]; - public fetching: boolean = true; - public identityColumn: string; - public primaeyKeys: {data_type: string, column_name: string}[]; - - autocmpleteUpdate =new Subject(); - - constructor( - private _tables: TablesService, - private _connections: ConnectionsService, - ) { - super(); - this.autocmpleteUpdate.pipe( - debounceTime(500), - distinctUntilChanged()) - .subscribe(_value => { - if (this.currentDisplayedString === '') this.onFieldChange.emit(null); - this.fetchSuggestions(); - }); - } - - ngOnInit(): void { - super.ngOnInit(); - this.connectionID = this._connections.currentConnectionID; - this.relations && this._tables.fetchTable({ - connectionID: this.connectionID, - tableName: this.relations.referenced_table_name, - requstedPage: 1, - chunkSize: 10, - foreignKeyRowName: this.relations.referenced_column_name, - foreignKeyRowValue: this.value - }).subscribe((res: any) => { - if (res.rows.length) { - this.identityColumn = res.identity_column; - // this.primaeyKeys = res.primaryColumns; - const modifiedRow = this.getModifiedRow(res.rows[0]); - if (this.value) { - this.currentDisplayedString = - this.identityColumn ? - `${res.rows[0][this.identityColumn]} (${Object.values(modifiedRow).filter(value => value).join(' | ')})` : - Object.values(modifiedRow).filter(value => value).join(' | '); - this.currentFieldValue = res.rows[0][this.relations.referenced_column_name]; - this.currentFieldQueryParams = Object.assign({}, ...res.primaryColumns.map((primaeyKey) => ({[primaeyKey.column_name]: res.rows[0][primaeyKey.column_name]}))); - this.onFieldChange.emit(this.currentFieldValue); - } - } - - this._tables.fetchTable({ - connectionID: this.connectionID, - tableName: this.relations.referenced_table_name, - requstedPage: 1, - chunkSize: 20, - foreignKeyRowName: 'autocomplete', - foreignKeyRowValue: '', - referencedColumn: this.relations.referenced_column_name - }).subscribe((res:any) => { - this.identityColumn = res.identity_column; - this.suggestions = res.rows.map(row => { - const modifiedRow = this.getModifiedRow(row); - return { - displayString: this.identityColumn ? `${row[this.identityColumn]} (${Object.values(modifiedRow).filter(value => value).join(' | ')})` : Object.values(modifiedRow).filter(value => value).join(' | '), - primaryKeys: Object.assign({}, ...res.primaryColumns.map((primaeyKey) => ({[primaeyKey.column_name]: row[primaeyKey.column_name]}))), - fieldValue: row[this.relations.referenced_column_name] - } - }); - this.fetching = false; - }); - }); - } - - fetchSuggestions() { - const currentRow = this.suggestions?.find(suggestion => suggestion.displayString === this.currentDisplayedString); - if (currentRow !== undefined) { - this.currentFieldValue = currentRow.fieldValue; - // this.currentFieldQueryParams = Object.assign({}, ...this.primaeyKeys.map((primaeyKey) => ({[primaeyKey.column_name]: currentRow[primaeyKey.column_name]}))); - this.onFieldChange.emit(this.currentFieldValue); - } else { - this.fetching = true; - this._tables.fetchTable({ - connectionID: this.connectionID, - tableName: this.relations.referenced_table_name, - requstedPage: 1, - chunkSize: 20, - foreignKeyRowName: 'autocomplete', - foreignKeyRowValue: this.currentDisplayedString, - referencedColumn: this.relations.referenced_column_name - }).subscribe((res: any) => { - this.identityColumn = res.identity_column; - if (res.rows.length === 0) { - this.suggestions = [{ - displayString: `No field starts with "${this.currentDisplayedString}" in foreign entity.` - }] - } else { - this.suggestions = res.rows.map(row => { - const modifiedRow = this.getModifiedRow(row); - return { - displayString: this.identityColumn ? `${row[this.identityColumn]} (${Object.values(modifiedRow).filter(value => value).join(' | ')})` : Object.values(modifiedRow).filter(value => value).join(' | '), - primaryKeys: Object.assign({}, ...res.primaryColumns.map((primaeyKey) => ({[primaeyKey.column_name]: row[primaeyKey.column_name]}))), - fieldValue: row[this.relations.referenced_column_name] - } - }); - } - this.fetching = false; - }); - } - } - - getModifiedRow(row) { - let modifiedRow; - if (this.relations.autocomplete_columns && this.relations.autocomplete_columns.length > 0) { - let autocompleteColumns = [...this.relations.autocomplete_columns] - if (this.identityColumn) autocompleteColumns.splice(this.relations.autocomplete_columns.indexOf(this.identityColumn), 1); - modifiedRow = autocompleteColumns - .reduce((rowObject, columnName)=> (rowObject[columnName]=row[columnName],rowObject),{}); - } else { - modifiedRow = Object.entries(row).reduce((rowObject, [columnName, value]) => { - if (value) { - rowObject[columnName] = value; - } - return rowObject; - }, {}) - } - return modifiedRow; - } - - updateRelatedLink(e: MatAutocompleteSelectedEvent) { - this.currentFieldQueryParams = this.suggestions.find(suggestion => suggestion.displayString === e.option.value).primaryKeys; - } + @Input() value; + + public connectionID: string; + public currentDisplayedString: string; + public currentFieldValue: unknown; + public currentFieldQueryParams: Record; + public suggestions = signal([]); + public fetching = signal(true); + public identityColumn: string; + public primaeyKeys: { data_type: string; column_name: string }[]; + + private _debounceTimer: ReturnType; + + constructor( + private _tables: TablesService, + private _connections: ConnectionsService, + ) { + super(); + } + + async ngOnInit(): Promise { + super.ngOnInit(); + this.connectionID = this._connections.currentConnectionID; + + if (this.relations) { + try { + const res = (await firstValueFrom( + this._tables.fetchTable({ + connectionID: this.connectionID, + tableName: this.relations.referenced_table_name, + requstedPage: 1, + chunkSize: 10, + foreignKeyRowName: this.relations.referenced_column_name, + foreignKeyRowValue: this.value, + }), + )) as FetchTableResponse; + + if (res.rows.length) { + this.identityColumn = res.identity_column; + const modifiedRow = this.getModifiedRow(res.rows[0]); + if (this.value) { + this.currentDisplayedString = this.identityColumn + ? `${res.rows[0][this.identityColumn]} (${Object.values(modifiedRow) + .filter((value) => value) + .join(' | ')})` + : Object.values(modifiedRow) + .filter((value) => value) + .join(' | '); + this.currentFieldValue = res.rows[0][this.relations.referenced_column_name]; + this.currentFieldQueryParams = Object.assign( + {}, + ...res.primaryColumns.map((primaeyKey) => ({ + [primaeyKey.column_name]: res.rows[0][primaeyKey.column_name], + })), + ); + this.onFieldChange.emit(this.currentFieldValue); + } + } + + const suggestionsRes = (await firstValueFrom( + this._tables.fetchTable({ + connectionID: this.connectionID, + tableName: this.relations.referenced_table_name, + requstedPage: 1, + chunkSize: 20, + foreignKeyRowName: 'autocomplete', + foreignKeyRowValue: '', + referencedColumn: this.relations.referenced_column_name, + }), + )) as FetchTableResponse; + + this.identityColumn = suggestionsRes.identity_column; + this.suggestions.set( + suggestionsRes.rows.map((row) => { + const modifiedRow = this.getModifiedRow(row); + return { + displayString: this.identityColumn + ? `${row[this.identityColumn]} (${Object.values(modifiedRow) + .filter((value) => value) + .join(' | ')})` + : Object.values(modifiedRow) + .filter((value) => value) + .join(' | '), + primaryKeys: Object.assign( + {}, + ...suggestionsRes.primaryColumns.map((primaeyKey) => ({ + [primaeyKey.column_name]: row[primaeyKey.column_name], + })), + ), + fieldValue: row[this.relations.referenced_column_name], + }; + }), + ); + this.fetching.set(false); + } catch (error) { + console.error('Error loading foreign key data:', error); + this.fetching.set(false); + } + } + } + + onSearchInput(): void { + clearTimeout(this._debounceTimer); + this._debounceTimer = setTimeout(() => { + if (this.currentDisplayedString === '') this.onFieldChange.emit(null); + this.fetchSuggestions(); + }, 500); + } + + async fetchSuggestions(): Promise { + const currentRow = this.suggestions()?.find( + (suggestion) => suggestion.displayString === this.currentDisplayedString, + ); + if (currentRow !== undefined) { + this.currentFieldValue = currentRow.fieldValue; + this.onFieldChange.emit(this.currentFieldValue); + } else { + this.fetching.set(true); + try { + const res = (await firstValueFrom( + this._tables.fetchTable({ + connectionID: this.connectionID, + tableName: this.relations.referenced_table_name, + requstedPage: 1, + chunkSize: 20, + foreignKeyRowName: 'autocomplete', + foreignKeyRowValue: this.currentDisplayedString, + referencedColumn: this.relations.referenced_column_name, + }), + )) as FetchTableResponse; + + this.identityColumn = res.identity_column; + if (res.rows.length === 0) { + this.suggestions.set([ + { + displayString: `No field starts with "${this.currentDisplayedString}" in foreign entity.`, + }, + ]); + } else { + this.suggestions.set( + res.rows.map((row) => { + const modifiedRow = this.getModifiedRow(row); + return { + displayString: this.identityColumn + ? `${row[this.identityColumn]} (${Object.values(modifiedRow) + .filter((value) => value) + .join(' | ')})` + : Object.values(modifiedRow) + .filter((value) => value) + .join(' | '), + primaryKeys: Object.assign( + {}, + ...res.primaryColumns.map((primaeyKey) => ({ + [primaeyKey.column_name]: row[primaeyKey.column_name], + })), + ), + fieldValue: row[this.relations.referenced_column_name], + }; + }), + ); + } + this.fetching.set(false); + } catch (error) { + console.error('Error fetching suggestions:', error); + this.fetching.set(false); + } + } + } + + getModifiedRow(row: Record): Record { + let modifiedRow: Record; + if (this.relations.autocomplete_columns && this.relations.autocomplete_columns.length > 0) { + let autocompleteColumns = [...this.relations.autocomplete_columns]; + if (this.identityColumn) + autocompleteColumns.splice(this.relations.autocomplete_columns.indexOf(this.identityColumn), 1); + modifiedRow = autocompleteColumns.reduce>( + (rowObject, columnName) => ((rowObject[columnName] = row[columnName]), rowObject), + {}, + ); + } else { + modifiedRow = Object.entries(row).reduce>((rowObject, [columnName, value]) => { + if (value) { + rowObject[columnName] = value; + } + return rowObject; + }, {}); + } + return modifiedRow; + } + + updateRelatedLink(e: MatAutocompleteSelectedEvent) { + this.currentFieldQueryParams = this.suggestions().find( + (suggestion) => suggestion.displayString === e.option.value, + ).primaryKeys; + } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html index abe05600e..229239a2e 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.html @@ -1,7 +1,9 @@ {{normalizedLabel}}
- {{selectedCountryFlag}} + @if (selectedCountryFlag() && showFlag()) { + {{selectedCountryFlag()}} + }
- - {{country.flag}} - {{country.label}} - ({{country.value}}) - + @for (country of filteredCountries(); track country.value) { + + @if (country.flag && showFlag()) { + {{country.flag}} + } + {{country.label}} + @if (country.value) { + ({{country.value}}) + } + + } -
\ No newline at end of file +
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts index 1900d19a2..6ba610a7b 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/country/country.component.ts @@ -1,116 +1,112 @@ -import { COUNTRIES, getCountryFlag } from '../../../../consts/countries'; -import { CUSTOM_ELEMENTS_SCHEMA, Component, Input } from '@angular/core'; -import { map, startWith } from 'rxjs/operators'; - -import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; import { CommonModule } from '@angular/common'; -import { FormControl } from '@angular/forms'; -import { FormsModule } from '@angular/forms'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, computed, Input } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { Observable } from 'rxjs'; -import { ReactiveFormsModule } from '@angular/forms'; +import { COUNTRIES, getCountryFlag } from '../../../../consts/countries'; +import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; + +interface CountryOption { + value: string | null; + label: string; + flag: string; +} @Component({ - selector: 'app-edit-country', - imports: [CommonModule, FormsModule, ReactiveFormsModule, MatFormFieldModule, MatAutocompleteModule, MatInputModule], - templateUrl: './country.component.html', - styleUrls: ['./country.component.css'], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + selector: 'app-edit-country', + imports: [CommonModule, FormsModule, ReactiveFormsModule, MatFormFieldModule, MatAutocompleteModule, MatInputModule], + templateUrl: './country.component.html', + styleUrls: ['./country.component.css'], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class CountryEditComponent extends BaseEditFieldComponent { - @Input() value: string; - - public countries: {value: string | null, label: string, flag: string}[] = []; - public countryControl = new FormControl<{value: string | null, label: string, flag: string} | string>(''); - public filteredCountries: Observable<{value: string | null, label: string, flag: string}[]>; - public showFlag: boolean = true; - public selectedCountryFlag: string = ''; - - originalOrder = () => { return 0; } - - getCountryFlag = getCountryFlag; - - ngOnInit(): void { - super.ngOnInit(); - this.parseWidgetParams(); - this.loadCountries(); - this.setupAutocomplete(); - this.setInitialValue(); - } - - private parseWidgetParams(): void { - if (this.widgetStructure?.widget_params) { - try { - const params = typeof this.widgetStructure.widget_params === 'string' - ? JSON.parse(this.widgetStructure.widget_params) - : this.widgetStructure.widget_params; - - if (params.show_flag !== undefined) { - this.showFlag = params.show_flag; - } - } catch (e) { - console.error('Error parsing country widget params:', e); - } - } - } - - private setupAutocomplete(): void { - this.filteredCountries = this.countryControl.valueChanges.pipe( - startWith(''), - map(value => { - // Update flag when value changes - if (typeof value === 'object' && value !== null) { - this.selectedCountryFlag = value.flag; - } else if (typeof value === 'string') { - // Clear flag if user is typing - this.selectedCountryFlag = ''; - } - return this._filter(typeof value === 'string' ? value : (value?.label || '')); - }) - ); - } - - private setInitialValue(): void { - if (this.value) { - const country = this.countries.find(c => c.value === this.value); - if (country) { - this.countryControl.setValue(country); - this.selectedCountryFlag = country.flag; - } - } - } - - private _filter(value: string): {value: string | null, label: string, flag: string}[] { - const filterValue = value.toLowerCase(); - return this.countries.filter(country => - country.label?.toLowerCase().includes(filterValue) || - (country.value?.toLowerCase().includes(filterValue)) - ); - } - - onCountrySelected(selectedCountry: {value: string | null, label: string, flag: string}): void { - this.value = selectedCountry.value; - this.selectedCountryFlag = selectedCountry.flag; - this.onFieldChange.emit(this.value); - } - - displayFn(country: any): string { - if (!country) return ''; - // Only return the country label, flag is shown separately - return typeof country === 'string' ? country : country.label; - } - - private loadCountries(): void { - this.countries = COUNTRIES.map(country => ({ - value: country.code, - label: country.name, - flag: getCountryFlag(country.code) - })).toSorted((a, b) => a.label.localeCompare(b.label)); - - if (this.widgetStructure?.widget_params?.allow_null || this.structure?.allow_null) { - this.countries = [{ value: null, label: '', flag: '' }, ...this.countries]; - } - } -} \ No newline at end of file + @Input() value: string; + + public countries: CountryOption[] = []; + public countryControl = new FormControl(''); + public selectedCountryFlag = computed(() => { + const value = this._controlValue(); + if (typeof value === 'object' && value !== null) { + return value.flag; + } + return ''; + }); + + public showFlag = computed(() => { + if (this.widgetStructure?.widget_params) { + try { + const params = + typeof this.widgetStructure.widget_params === 'string' + ? JSON.parse(this.widgetStructure.widget_params) + : this.widgetStructure.widget_params; + + if (params.show_flag !== undefined) { + return params.show_flag; + } + } catch (e) { + console.error('Error parsing country widget params:', e); + } + } + return true; + }); + + private _controlValue = toSignal(this.countryControl.valueChanges, { initialValue: '' as CountryOption | string }); + + public filteredCountries = computed(() => { + const value = this._controlValue(); + return this._filter(typeof value === 'string' ? value : value?.label || ''); + }); + + originalOrder = () => { + return 0; + }; + + getCountryFlag = getCountryFlag; + + ngOnInit(): void { + super.ngOnInit(); + this.loadCountries(); + this.setInitialValue(); + } + + onCountrySelected(selectedCountry: CountryOption): void { + this.value = selectedCountry.value; + this.onFieldChange.emit(this.value); + } + + displayFn(country: CountryOption | string): string { + if (!country) return ''; + return typeof country === 'string' ? country : country.label; + } + + private setInitialValue(): void { + if (this.value) { + const country = this.countries.find((c) => c.value === this.value); + if (country) { + this.countryControl.setValue(country); + } + } + } + + private _filter(value: string): CountryOption[] { + const filterValue = value.toLowerCase(); + return this.countries.filter( + (country) => + country.label?.toLowerCase().includes(filterValue) || country.value?.toLowerCase().includes(filterValue), + ); + } + + private loadCountries(): void { + this.countries = COUNTRIES.map((country) => ({ + value: country.code, + label: country.name, + flag: getCountryFlag(country.code), + })).toSorted((a, b) => a.label.localeCompare(b.label)); + + if (this.widgetStructure?.widget_params?.allow_null || this.structure?.allow_null) { + this.countries = [{ value: null, label: '', flag: '' }, ...this.countries]; + } + } +} diff --git a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html index 1c1b2a961..601574eee 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html @@ -3,14 +3,16 @@
{{normalizedLabel}} - + @if (fetching()) { + + } + (ngModelChange)="onSearchInput()"> - @for (suggestion of suggestions; track suggestion.fieldValue) { + @for (suggestion of suggestions(); track suggestion.fieldValue) { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts index ffa92a5ae..c8c106c56 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts @@ -145,7 +145,7 @@ describe('ForeignKeyEditComponent', () => { expect(component).toBeTruthy(); }); - it('should fill initial dropdown values when identity_column is set', () => { + it('should fill initial dropdown values when identity_column is set', async () => { const usersTableNetworkWithIdentityColumn = { ...usersTableNetwork, identity_column: 'lastname' }; vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetworkWithIdentityColumn)); @@ -153,14 +153,14 @@ describe('ForeignKeyEditComponent', () => { component.connectionID = '12345678'; component.value = ''; - component.ngOnInit(); + await component.ngOnInit(); fixture.detectChanges(); expect(component.identityColumn).toEqual('lastname'); expect(component.currentDisplayedString).toBeUndefined; expect(component.currentFieldValue).toBeUndefined; - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'Taylor (Alex | new-user-5@email.com)', primaryKeys: { id: 33 }, @@ -179,21 +179,21 @@ describe('ForeignKeyEditComponent', () => { ]); }); - it('should fill initial dropdown values when identity_column is not set', () => { + it('should fill initial dropdown values when identity_column is not set', async () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork)); component.connectionID = '12345678'; component.value = ''; - component.ngOnInit(); + await component.ngOnInit(); fixture.detectChanges(); expect(component.identityColumn).toBeUndefined; expect(component.currentDisplayedString).toBeUndefined; expect(component.currentFieldValue).toBeUndefined; - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'Alex | Taylor | new-user-5@email.com', primaryKeys: { id: 33 }, @@ -212,7 +212,7 @@ describe('ForeignKeyEditComponent', () => { ]); }); - it('should fill initial dropdown values when autocomplete_columns and field value is not set', () => { + it('should fill initial dropdown values when autocomplete_columns and field value is not set', async () => { vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(usersTableNetwork)); component.connectionID = '12345678'; @@ -226,14 +226,14 @@ describe('ForeignKeyEditComponent', () => { }; component.value = ''; - component.ngOnInit(); + await component.ngOnInit(); fixture.detectChanges(); expect(component.identityColumn).toBeUndefined; expect(component.currentDisplayedString).toBeUndefined; expect(component.currentFieldValue).toBeUndefined; - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: '33 | Alex | Taylor | new-user-5@email.com | 24', primaryKeys: { id: 33 }, @@ -252,8 +252,8 @@ describe('ForeignKeyEditComponent', () => { ]); }); - it('should set current value if necessary row is in suggestions list', () => { - component.suggestions = [ + it('should set current value if necessary row is in suggestions list', async () => { + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', primaryKeys: { id: 33 }, @@ -269,15 +269,15 @@ describe('ForeignKeyEditComponent', () => { primaryKeys: { id: 35 }, fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'Alex | Johnson | new-user-4@email.com'; - component.fetchSuggestions(); + await component.fetchSuggestions(); expect(component.currentFieldValue).toEqual(34); }); - it('should fetch suggestions list if user types search query and identity column is set', () => { + it('should fetch suggestions list if user types search query and identity column is set', async () => { const searchSuggestionsNetwork = { rows: [ { @@ -303,7 +303,7 @@ describe('ForeignKeyEditComponent', () => { component.relations = fakeRelations; - component.suggestions = [ + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', fieldValue: 33, @@ -316,12 +316,12 @@ describe('ForeignKeyEditComponent', () => { displayString: 'Alex | Smith | some-new@email.com', fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'John'; - component.fetchSuggestions(); + await component.fetchSuggestions(); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'Taylor (John | new-user-0@email.com)', primaryKeys: { id: 23 }, @@ -335,14 +335,14 @@ describe('ForeignKeyEditComponent', () => { ]); }); - it('should fetch suggestions list if user types search query and show No matches message if the list is empty', () => { + it('should fetch suggestions list if user types search query and show No matches message if the list is empty', async () => { const searchSuggestionsNetwork = { rows: [], }; vi.spyOn(tablesService, 'fetchTable').mockReturnValue(of(searchSuggestionsNetwork)); - component.suggestions = [ + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', primaryKeys: { id: 33 }, @@ -358,19 +358,19 @@ describe('ForeignKeyEditComponent', () => { primaryKeys: { id: 35 }, fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'skjfhskjdf'; - component.fetchSuggestions(); + await component.fetchSuggestions(); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'No field starts with "skjfhskjdf" in foreign entity.', }, ]); }); - it('should fetch suggestions list if user types search query and identity column is not set', () => { + it('should fetch suggestions list if user types search query and identity column is not set', async () => { const searchSuggestionsNetwork = { rows: [ { @@ -395,7 +395,7 @@ describe('ForeignKeyEditComponent', () => { component.connectionID = '12345678'; component.relations = fakeRelations; - component.suggestions = [ + component.suggestions.set([ { displayString: 'Alex | Taylor | new-user-5@email.com', fieldValue: 33, @@ -408,10 +408,10 @@ describe('ForeignKeyEditComponent', () => { displayString: 'Alex | Smith | some-new@email.com', fieldValue: 35, }, - ]; + ]); component.currentDisplayedString = 'Alex'; - component.fetchSuggestions(); + await component.fetchSuggestions(); fixture.detectChanges(); @@ -425,7 +425,7 @@ describe('ForeignKeyEditComponent', () => { referencedColumn: component.relations.referenced_column_name, }); - expect(component.suggestions).toEqual([ + expect(component.suggestions()).toEqual([ { displayString: 'John | Taylor | new-user-0@email.com', primaryKeys: { id: 23 }, diff --git a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts index f7661f925..9ac292260 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts @@ -1,186 +1,248 @@ -import { Component, Input } from '@angular/core'; -import { debounceTime, } from 'rxjs/operators'; - -import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; import { CommonModule } from '@angular/common'; -import { ConnectionsService } from 'src/app/services/connections.service'; +import { Component, Input, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSpinner } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { Subject } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { TableForeignKey } from 'src/app/models/table'; +import { ConnectionsService } from 'src/app/services/connections.service'; import { TablesService } from 'src/app/services/tables.service'; +import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; + +interface FetchTableResponse { + rows: Record[]; + primaryColumns: { column_name: string; data_type: string }[]; + identity_column?: string; +} interface Suggestion { - displayString: string; - primaryKeys?: any; - fieldValue?: any + displayString: string; + primaryKeys?: Record; + fieldValue?: unknown; } @Component({ - selector: 'app-edit-foreign-key', - templateUrl: './foreign-key.component.html', - styleUrls: ['./foreign-key.component.css'], - imports: [ - CommonModule, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatAutocompleteModule, - MatIconModule, - MatTooltipModule, - MatSpinner, - RouterModule - ] + selector: 'app-edit-foreign-key', + templateUrl: './foreign-key.component.html', + styleUrls: ['./foreign-key.component.css'], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatAutocompleteModule, + MatIconModule, + MatTooltipModule, + MatSpinner, + RouterModule, + ], }) export class ForeignKeyEditComponent extends BaseEditFieldComponent { - @Input() value; - - public connectionID: string; - public currentDisplayedString: string; - public currentFieldValue: any; - public currentFieldQueryParams: any; - public suggestions: Suggestion[]; - public fetching: boolean = true; - public identityColumn: string; - public primaeyKeys: {data_type: string, column_name: string}[]; - - public fkRelations: TableForeignKey = null; - autocmpleteUpdate = new Subject(); - - constructor( - private _tables: TablesService, - private _connections: ConnectionsService, - ) { - super(); - this.autocmpleteUpdate.pipe( - debounceTime(500)) - .subscribe(_value => { - if (this.currentDisplayedString === '') this.onFieldChange.emit(null); - this.fetchSuggestions(); - }); - } - - ngOnInit(): void { - super.ngOnInit(); - this.connectionID = this._connections.currentConnectionID; - - if (this.widgetStructure?.widget_params) { - this.fkRelations = this.widgetStructure.widget_params as TableForeignKey; - } else if (this.relations) { - this.fkRelations = this.relations; - } - - this.fkRelations && this._tables.fetchTable({ - connectionID: this.connectionID, - tableName: this.fkRelations.referenced_table_name, - requstedPage: 1, - chunkSize: 10, - foreignKeyRowName: this.fkRelations.referenced_column_name, - foreignKeyRowValue: this.value - }).subscribe((res: any) => { - if (res.rows.length) { - this.identityColumn = res.identity_column; - // this.primaeyKeys = res.primaryColumns; - const modifiedRow = this.getModifiedRow(res.rows[0]); - if (this.value) { - this.currentDisplayedString = - this.identityColumn ? - `${res.rows[0][this.identityColumn]} (${Object.values(modifiedRow).filter(value => value).join(' | ')})` : - Object.values(modifiedRow).filter(value => value).join(' | '); - this.currentFieldValue = res.rows[0][this.fkRelations.referenced_column_name]; - this.currentFieldQueryParams = Object.assign({}, ...res.primaryColumns.map((primaeyKey) => ({[primaeyKey.column_name]: res.rows[0][primaeyKey.column_name]}))); - this.onFieldChange.emit(this.currentFieldValue); - } - } - - this._tables.fetchTable({ - connectionID: this.connectionID, - tableName: this.fkRelations.referenced_table_name, - requstedPage: 1, - chunkSize: 20, - foreignKeyRowName: 'autocomplete', - foreignKeyRowValue: '', - referencedColumn: this.fkRelations.referenced_column_name - }).subscribe((res:any) => { - this.identityColumn = res.identity_column; - this.suggestions = res.rows.map(row => { - const modifiedRow = this.getModifiedRow(row); - return { - displayString: this.identityColumn ? `${row[this.identityColumn]} (${Object.values(modifiedRow).filter(value => value).join(' | ')})` : Object.values(modifiedRow).filter(value => value).join(' | '), - primaryKeys: Object.assign({}, ...res.primaryColumns.map((primaeyKey) => ({[primaeyKey.column_name]: row[primaeyKey.column_name]}))), - fieldValue: row[this.fkRelations.referenced_column_name] - } - }); - this.fetching = false; - }); - }); - } - - fetchSuggestions() { - const currentRow = this.suggestions?.find(suggestion => suggestion.displayString === this.currentDisplayedString); - if (currentRow !== undefined) { - this.currentFieldValue = currentRow.fieldValue; - // this.currentFieldQueryParams = Object.assign({}, ...this.primaeyKeys.map((primaeyKey) => ({[primaeyKey.column_name]: currentRow[primaeyKey.column_name]}))); - console.log(this.label + 'this.currentFieldValue'); - console.log(this.currentFieldValue); - this.onFieldChange.emit(this.currentFieldValue); - } else { - this.fetching = true; - this._tables.fetchTable({ - connectionID: this.connectionID, - tableName: this.fkRelations.referenced_table_name, - requstedPage: 1, - chunkSize: 20, - foreignKeyRowName: 'autocomplete', - foreignKeyRowValue: this.currentDisplayedString, - referencedColumn: this.fkRelations.referenced_column_name - }).subscribe((res: any) => { - this.identityColumn = res.identity_column; - if (res.rows.length === 0) { - this.suggestions = [{ - displayString: `No field starts with "${this.currentDisplayedString}" in foreign entity.` - }] - } else { - this.suggestions = res.rows.map(row => { - const modifiedRow = this.getModifiedRow(row); - return { - displayString: this.identityColumn ? `${row[this.identityColumn]} (${Object.values(modifiedRow).filter(value => value).join(' | ')})` : Object.values(modifiedRow).filter(value => value).join(' | '), - primaryKeys: Object.assign({}, ...res.primaryColumns.map((primaeyKey) => ({[primaeyKey.column_name]: row[primaeyKey.column_name]}))), - fieldValue: row[this.fkRelations.referenced_column_name] - } - }); - } - this.fetching = false; - }); - } - } - - getModifiedRow(row) { - let modifiedRow; - if (this.fkRelations.autocomplete_columns && this.fkRelations.autocomplete_columns.length > 0) { - let autocompleteColumns = [...this.fkRelations.autocomplete_columns] - if (this.identityColumn) autocompleteColumns.splice(this.fkRelations.autocomplete_columns.indexOf(this.identityColumn), 1); - modifiedRow = autocompleteColumns - .reduce((rowObject, columnName)=> (rowObject[columnName]=row[columnName],rowObject),{}); - } else { - modifiedRow = Object.entries(row).reduce((rowObject, [columnName, value]) => { - if (value) { - rowObject[columnName] = value; - } - return rowObject; - }, {}) - } - return modifiedRow; - } - - updateRelatedLink(e: MatAutocompleteSelectedEvent) { - this.currentFieldQueryParams = this.suggestions.find(suggestion => suggestion.displayString === e.option.value).primaryKeys; - } + @Input() value; + + public connectionID: string; + public currentDisplayedString: string; + public currentFieldValue: unknown; + public currentFieldQueryParams: Record; + public suggestions = signal([]); + public fetching = signal(true); + public identityColumn: string; + public primaeyKeys: { data_type: string; column_name: string }[]; + + public fkRelations: TableForeignKey = null; + private _debounceTimer: ReturnType; + + constructor( + private _tables: TablesService, + private _connections: ConnectionsService, + ) { + super(); + } + + async ngOnInit(): Promise { + super.ngOnInit(); + this.connectionID = this._connections.currentConnectionID; + + if (this.widgetStructure?.widget_params) { + this.fkRelations = this.widgetStructure.widget_params as TableForeignKey; + } else if (this.relations) { + this.fkRelations = this.relations; + } + + if (this.fkRelations) { + try { + const res = (await firstValueFrom( + this._tables.fetchTable({ + connectionID: this.connectionID, + tableName: this.fkRelations.referenced_table_name, + requstedPage: 1, + chunkSize: 10, + foreignKeyRowName: this.fkRelations.referenced_column_name, + foreignKeyRowValue: this.value, + }), + )) as FetchTableResponse; + + if (res.rows.length) { + this.identityColumn = res.identity_column; + const modifiedRow = this.getModifiedRow(res.rows[0]); + if (this.value) { + this.currentDisplayedString = this.identityColumn + ? `${res.rows[0][this.identityColumn]} (${Object.values(modifiedRow) + .filter((value) => value) + .join(' | ')})` + : Object.values(modifiedRow) + .filter((value) => value) + .join(' | '); + this.currentFieldValue = res.rows[0][this.fkRelations.referenced_column_name]; + this.currentFieldQueryParams = Object.assign( + {}, + ...res.primaryColumns.map((primaeyKey) => ({ + [primaeyKey.column_name]: res.rows[0][primaeyKey.column_name], + })), + ); + this.onFieldChange.emit(this.currentFieldValue); + } + } + + const suggestionsRes = (await firstValueFrom( + this._tables.fetchTable({ + connectionID: this.connectionID, + tableName: this.fkRelations.referenced_table_name, + requstedPage: 1, + chunkSize: 20, + foreignKeyRowName: 'autocomplete', + foreignKeyRowValue: '', + referencedColumn: this.fkRelations.referenced_column_name, + }), + )) as FetchTableResponse; + + this.identityColumn = suggestionsRes.identity_column; + this.suggestions.set( + suggestionsRes.rows.map((row) => { + const modifiedRow = this.getModifiedRow(row); + return { + displayString: this.identityColumn + ? `${row[this.identityColumn]} (${Object.values(modifiedRow) + .filter((value) => value) + .join(' | ')})` + : Object.values(modifiedRow) + .filter((value) => value) + .join(' | '), + primaryKeys: Object.assign( + {}, + ...suggestionsRes.primaryColumns.map((primaeyKey) => ({ + [primaeyKey.column_name]: row[primaeyKey.column_name], + })), + ), + fieldValue: row[this.fkRelations.referenced_column_name], + }; + }), + ); + this.fetching.set(false); + } catch (error) { + console.error('Error loading foreign key data:', error); + this.fetching.set(false); + } + } + } + + onSearchInput(): void { + clearTimeout(this._debounceTimer); + this._debounceTimer = setTimeout(() => { + if (this.currentDisplayedString === '') this.onFieldChange.emit(null); + this.fetchSuggestions(); + }, 500); + } + + async fetchSuggestions(): Promise { + const currentRow = this.suggestions()?.find( + (suggestion) => suggestion.displayString === this.currentDisplayedString, + ); + if (currentRow !== undefined) { + this.currentFieldValue = currentRow.fieldValue; + this.onFieldChange.emit(this.currentFieldValue); + } else { + this.fetching.set(true); + try { + const res = (await firstValueFrom( + this._tables.fetchTable({ + connectionID: this.connectionID, + tableName: this.fkRelations.referenced_table_name, + requstedPage: 1, + chunkSize: 20, + foreignKeyRowName: 'autocomplete', + foreignKeyRowValue: this.currentDisplayedString, + referencedColumn: this.fkRelations.referenced_column_name, + }), + )) as FetchTableResponse; + + this.identityColumn = res.identity_column; + if (res.rows.length === 0) { + this.suggestions.set([ + { + displayString: `No field starts with "${this.currentDisplayedString}" in foreign entity.`, + }, + ]); + } else { + this.suggestions.set( + res.rows.map((row) => { + const modifiedRow = this.getModifiedRow(row); + return { + displayString: this.identityColumn + ? `${row[this.identityColumn]} (${Object.values(modifiedRow) + .filter((value) => value) + .join(' | ')})` + : Object.values(modifiedRow) + .filter((value) => value) + .join(' | '), + primaryKeys: Object.assign( + {}, + ...res.primaryColumns.map((primaeyKey) => ({ + [primaeyKey.column_name]: row[primaeyKey.column_name], + })), + ), + fieldValue: row[this.fkRelations.referenced_column_name], + }; + }), + ); + } + this.fetching.set(false); + } catch (error) { + console.error('Error fetching suggestions:', error); + this.fetching.set(false); + } + } + } + + getModifiedRow(row: Record): Record { + let modifiedRow: Record; + if (this.fkRelations.autocomplete_columns && this.fkRelations.autocomplete_columns.length > 0) { + let autocompleteColumns = [...this.fkRelations.autocomplete_columns]; + if (this.identityColumn) + autocompleteColumns.splice(this.fkRelations.autocomplete_columns.indexOf(this.identityColumn), 1); + modifiedRow = autocompleteColumns.reduce>( + (rowObject, columnName) => ((rowObject[columnName] = row[columnName]), rowObject), + {}, + ); + } else { + modifiedRow = Object.entries(row).reduce>((rowObject, [columnName, value]) => { + if (value) { + rowObject[columnName] = value; + } + return rowObject; + }, {}); + } + return modifiedRow; + } + + updateRelatedLink(e: MatAutocompleteSelectedEvent) { + this.currentFieldQueryParams = this.suggestions().find( + (suggestion) => suggestion.displayString === e.option.value, + ).primaryKeys; + } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html index 7f25a437e..4986d60a2 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.html @@ -12,7 +12,7 @@ class="language-input">
- @for (language of filteredLanguages | async; track language.value) { + @for (language of filteredLanguages(); track language.value) { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts index 65ee3dc33..da1cd31ab 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/language/language.component.ts @@ -1,11 +1,10 @@ import { CommonModule } from '@angular/common'; -import { Component, CUSTOM_ELEMENTS_SCHEMA, computed, input, OnInit, output, signal } from '@angular/core'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, computed, input, OnInit, output } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; import { TableField, TableForeignKey, WidgetStructure } from 'src/app/models/table'; import { getLanguageFlag, LANGUAGES } from '../../../../consts/languages'; import { normalizeFieldName } from '../../../../lib/normalize'; @@ -35,7 +34,7 @@ export class LanguageEditComponent implements OnInit { readonly relations = input(); readonly value = input(); - readonly onFieldChange = output(); + readonly onFieldChange = output(); readonly normalizedLabel = computed(() => normalizeFieldName(this.label() || '')); @@ -52,8 +51,20 @@ export class LanguageEditComponent implements OnInit { public languages: LanguageOption[] = []; public languageControl = new FormControl(''); - public filteredLanguages: Observable; - public selectedLanguageFlag = signal(''); + public selectedLanguageFlag = computed(() => { + const value = this._controlValue(); + if (typeof value === 'object' && value !== null) { + return value.flag; + } + return ''; + }); + + private _controlValue = toSignal(this.languageControl.valueChanges, { initialValue: '' as LanguageOption | string }); + + public filteredLanguages = computed(() => { + const value = this._controlValue(); + return this._filter(typeof value === 'string' ? value : value?.label || ''); + }); originalOrder = () => { return 0; @@ -61,41 +72,24 @@ export class LanguageEditComponent implements OnInit { ngOnInit(): void { this.loadLanguages(); - this.setupAutocomplete(); this.setInitialValue(); } - displayFn(language: any): string { + displayFn(language: LanguageOption | string): string { if (!language) return ''; return typeof language === 'string' ? language : language.label; } onLanguageSelected(selectedLanguage: LanguageOption): void { - this.selectedLanguageFlag.set(selectedLanguage.flag); this.onFieldChange.emit(selectedLanguage.value); } - private setupAutocomplete(): void { - this.filteredLanguages = this.languageControl.valueChanges.pipe( - startWith(''), - map((value) => { - if (typeof value === 'object' && value !== null) { - this.selectedLanguageFlag.set(value.flag); - } else if (typeof value === 'string') { - this.selectedLanguageFlag.set(''); - } - return this._filter(typeof value === 'string' ? value : value?.label || ''); - }), - ); - } - private setInitialValue(): void { const val = this.value(); if (val) { const language = this.languages.find((l) => l.value && l.value.toLowerCase() === val.toLowerCase()); if (language) { this.languageControl.setValue(language); - this.selectedLanguageFlag.set(language.flag); } } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html index 0dcbdcb6d..1dbf20da7 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.html @@ -1,7 +1,7 @@
Country - - - - - - {{country.flag}} - {{country.dialCode}} - {{country.name}} - - + @for (country of filteredCountries(); track country.code) { + + + {{country.flag}} + {{country.dialCode}} + {{country.name}} + + + } {{normalizedLabel}} - - Example: {{getExamplePhoneNumber()}} - Enter international number (e.g., +1234567890) or select country first - - Invalid phone number format - + @if (selectedCountry && !displayPhoneNumber.startsWith('+')) { + Example: {{getExamplePhoneNumber()}} + } + @if (!selectedCountry || displayPhoneNumber.startsWith('+')) { + Enter international number (e.g., +1234567890) or select country first + } + @if (phoneValidation && !isValidPhoneNumber() && displayPhoneNumber) { + + Invalid phone number format + + } -
\ No newline at end of file +
diff --git a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts index 87751b419..d5254ea04 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts @@ -6,7 +6,8 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { PhoneEditComponent } from './phone.component'; +import { WidgetStructure } from 'src/app/models/table'; +import { CountryCode, PhoneEditComponent } from './phone.component'; describe('PhoneEditComponent', () => { let component: PhoneEditComponent; @@ -177,7 +178,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); expect(component.selectedCountry.code).toBe('GB'); - expect(component.countryControl.value?.code).toBe('GB'); + expect((component.countryControl.value as CountryCode)?.code).toBe('GB'); }); it('should detect US from +1 number', () => { @@ -187,7 +188,7 @@ describe('PhoneEditComponent', () => { component.onPhoneNumberChange(); expect(component.selectedCountry.code).toBe('US'); - expect(component.countryControl.value?.code).toBe('US'); + expect((component.countryControl.value as CountryCode)?.code).toBe('US'); }); }); @@ -269,7 +270,7 @@ describe('PhoneEditComponent', () => { }); it('should handle empty selected country', () => { - component.selectedCountry = null as any; + component.selectedCountry = null as unknown as CountryCode; component.displayPhoneNumber = '5551234567'; expect(() => component.onPhoneNumberChange()).not.toThrow(); @@ -296,7 +297,7 @@ describe('PhoneEditComponent', () => { enable_placeholder: false, phone_validation: false, }, - } as any; + } as Partial as WidgetStructure; component.configureFromWidgetParams(); @@ -306,7 +307,7 @@ describe('PhoneEditComponent', () => { }); it('should handle missing widget params', () => { - component.widgetStructure = {} as any; + component.widgetStructure = {} as Partial as WidgetStructure; expect(() => component.configureFromWidgetParams()).not.toThrow(); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts index e785d751e..15d764adb 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.ts @@ -1,709 +1,695 @@ -import { AsYouType, CountryCode as LibPhoneCountryCode, parsePhoneNumber } from 'libphonenumber-js'; -import { Component, Injectable, Input, OnInit } from '@angular/core'; -import { Observable, map, startWith } from 'rxjs'; - -import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; import { CommonModule } from '@angular/common'; -import { FormControl } from '@angular/forms'; -import { FormsModule } from '@angular/forms'; +import { Component, computed, Input, OnInit } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { ReactiveFormsModule } from '@angular/forms'; +import { AsYouType, CountryCode as LibPhoneCountryCode, parsePhoneNumber } from 'libphonenumber-js'; +import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; -interface CountryCode { - code: string; - name: string; - dialCode: string; - flag: string; +export interface CountryCode { + code: string; + name: string; + dialCode: string; + flag: string; } -@Injectable() - @Component({ - selector: 'app-edit-phone', - templateUrl: './phone.component.html', - styleUrls: ['./phone.component.css'], - imports: [CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatAutocompleteModule, FormsModule, ReactiveFormsModule] + selector: 'app-edit-phone', + templateUrl: './phone.component.html', + styleUrls: ['./phone.component.css'], + imports: [ + CommonModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatAutocompleteModule, + FormsModule, + ReactiveFormsModule, + ], }) export class PhoneEditComponent extends BaseEditFieldComponent implements OnInit { - @Input() value: string = ''; - - static type = 'phone'; - - preferredCountries: string[] = ['US', 'GB']; - enablePlaceholder: boolean = true; - phoneValidation: boolean = true; - - selectedCountry: CountryCode; - phoneNumber: string = ''; - displayPhoneNumber: string = ''; - formatter: AsYouType | null = null; - - countryControl = new FormControl(null); - filteredCountries$: Observable; - - countries: CountryCode[] = [ - { code: 'AF', name: 'Afghanistan', dialCode: '+93', flag: '๐Ÿ‡ฆ๐Ÿ‡ซ' }, - { code: 'AL', name: 'Albania', dialCode: '+355', flag: '๐Ÿ‡ฆ๐Ÿ‡ฑ' }, - { code: 'DZ', name: 'Algeria', dialCode: '+213', flag: '๐Ÿ‡ฉ๐Ÿ‡ฟ' }, - { code: 'AS', name: 'American Samoa', dialCode: '+1684', flag: '๐Ÿ‡ฆ๐Ÿ‡ธ' }, - { code: 'AD', name: 'Andorra', dialCode: '+376', flag: '๐Ÿ‡ฆ๐Ÿ‡ฉ' }, - { code: 'AO', name: 'Angola', dialCode: '+244', flag: '๐Ÿ‡ฆ๐Ÿ‡ด' }, - { code: 'AI', name: 'Anguilla', dialCode: '+1264', flag: '๐Ÿ‡ฆ๐Ÿ‡ฎ' }, - { code: 'AQ', name: 'Antarctica', dialCode: '+672', flag: '๐Ÿ‡ฆ๐Ÿ‡ถ' }, - { code: 'AG', name: 'Antigua and Barbuda', dialCode: '+1268', flag: '๐Ÿ‡ฆ๐Ÿ‡ฌ' }, - { code: 'AR', name: 'Argentina', dialCode: '+54', flag: '๐Ÿ‡ฆ๐Ÿ‡ท' }, - { code: 'AM', name: 'Armenia', dialCode: '+374', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ' }, - { code: 'AW', name: 'Aruba', dialCode: '+297', flag: '๐Ÿ‡ฆ๐Ÿ‡ผ' }, - { code: 'AU', name: 'Australia', dialCode: '+61', flag: '๐Ÿ‡ฆ๐Ÿ‡บ' }, - { code: 'AT', name: 'Austria', dialCode: '+43', flag: '๐Ÿ‡ฆ๐Ÿ‡น' }, - { code: 'AZ', name: 'Azerbaijan', dialCode: '+994', flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ' }, - { code: 'BS', name: 'Bahamas', dialCode: '+1242', flag: '๐Ÿ‡ง๐Ÿ‡ธ' }, - { code: 'BH', name: 'Bahrain', dialCode: '+973', flag: '๐Ÿ‡ง๐Ÿ‡ญ' }, - { code: 'BD', name: 'Bangladesh', dialCode: '+880', flag: '๐Ÿ‡ง๐Ÿ‡ฉ' }, - { code: 'BB', name: 'Barbados', dialCode: '+1246', flag: '๐Ÿ‡ง๐Ÿ‡ง' }, - { code: 'BY', name: 'Belarus', dialCode: '+375', flag: '๐Ÿ‡ง๐Ÿ‡พ' }, - { code: 'BE', name: 'Belgium', dialCode: '+32', flag: '๐Ÿ‡ง๐Ÿ‡ช' }, - { code: 'BZ', name: 'Belize', dialCode: '+501', flag: '๐Ÿ‡ง๐Ÿ‡ฟ' }, - { code: 'BJ', name: 'Benin', dialCode: '+229', flag: '๐Ÿ‡ง๐Ÿ‡ฏ' }, - { code: 'BM', name: 'Bermuda', dialCode: '+1441', flag: '๐Ÿ‡ง๐Ÿ‡ฒ' }, - { code: 'BT', name: 'Bhutan', dialCode: '+975', flag: '๐Ÿ‡ง๐Ÿ‡น' }, - { code: 'BO', name: 'Bolivia', dialCode: '+591', flag: '๐Ÿ‡ง๐Ÿ‡ด' }, - { code: 'BA', name: 'Bosnia and Herzegovina', dialCode: '+387', flag: '๐Ÿ‡ง๐Ÿ‡ฆ' }, - { code: 'BW', name: 'Botswana', dialCode: '+267', flag: '๐Ÿ‡ง๐Ÿ‡ผ' }, - { code: 'BR', name: 'Brazil', dialCode: '+55', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, - { code: 'IO', name: 'British Indian Ocean Territory', dialCode: '+246', flag: '๐Ÿ‡ฎ๐Ÿ‡ด' }, - { code: 'BN', name: 'Brunei', dialCode: '+673', flag: '๐Ÿ‡ง๐Ÿ‡ณ' }, - { code: 'BG', name: 'Bulgaria', dialCode: '+359', flag: '๐Ÿ‡ง๐Ÿ‡ฌ' }, - { code: 'BF', name: 'Burkina Faso', dialCode: '+226', flag: '๐Ÿ‡ง๐Ÿ‡ซ' }, - { code: 'BI', name: 'Burundi', dialCode: '+257', flag: '๐Ÿ‡ง๐Ÿ‡ฎ' }, - { code: 'KH', name: 'Cambodia', dialCode: '+855', flag: '๐Ÿ‡ฐ๐Ÿ‡ญ' }, - { code: 'CM', name: 'Cameroon', dialCode: '+237', flag: '๐Ÿ‡จ๐Ÿ‡ฒ' }, - { code: 'CA', name: 'Canada', dialCode: '+1', flag: '๐Ÿ‡จ๐Ÿ‡ฆ' }, - { code: 'CV', name: 'Cape Verde', dialCode: '+238', flag: '๐Ÿ‡จ๐Ÿ‡ป' }, - { code: 'KY', name: 'Cayman Islands', dialCode: '+1345', flag: '๐Ÿ‡ฐ๐Ÿ‡พ' }, - { code: 'CF', name: 'Central African Republic', dialCode: '+236', flag: '๐Ÿ‡จ๐Ÿ‡ซ' }, - { code: 'TD', name: 'Chad', dialCode: '+235', flag: '๐Ÿ‡น๐Ÿ‡ฉ' }, - { code: 'CL', name: 'Chile', dialCode: '+56', flag: '๐Ÿ‡จ๐Ÿ‡ฑ' }, - { code: 'CN', name: 'China', dialCode: '+86', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, - { code: 'CX', name: 'Christmas Island', dialCode: '+61', flag: '๐Ÿ‡จ๐Ÿ‡ฝ' }, - { code: 'CC', name: 'Cocos Islands', dialCode: '+61', flag: '๐Ÿ‡จ๐Ÿ‡จ' }, - { code: 'CO', name: 'Colombia', dialCode: '+57', flag: '๐Ÿ‡จ๐Ÿ‡ด' }, - { code: 'KM', name: 'Comoros', dialCode: '+269', flag: '๐Ÿ‡ฐ๐Ÿ‡ฒ' }, - { code: 'CG', name: 'Congo', dialCode: '+242', flag: '๐Ÿ‡จ๐Ÿ‡ฌ' }, - { code: 'CD', name: 'Congo (DRC)', dialCode: '+243', flag: '๐Ÿ‡จ๐Ÿ‡ฉ' }, - { code: 'CK', name: 'Cook Islands', dialCode: '+682', flag: '๐Ÿ‡จ๐Ÿ‡ฐ' }, - { code: 'CR', name: 'Costa Rica', dialCode: '+506', flag: '๐Ÿ‡จ๐Ÿ‡ท' }, - { code: 'CI', name: 'Cรดte d\'Ivoire', dialCode: '+225', flag: '๐Ÿ‡จ๐Ÿ‡ฎ' }, - { code: 'HR', name: 'Croatia', dialCode: '+385', flag: '๐Ÿ‡ญ๐Ÿ‡ท' }, - { code: 'CU', name: 'Cuba', dialCode: '+53', flag: '๐Ÿ‡จ๐Ÿ‡บ' }, - { code: 'CW', name: 'Curaรงao', dialCode: '+599', flag: '๐Ÿ‡จ๐Ÿ‡ผ' }, - { code: 'CY', name: 'Cyprus', dialCode: '+357', flag: '๐Ÿ‡จ๐Ÿ‡พ' }, - { code: 'CZ', name: 'Czech Republic', dialCode: '+420', flag: '๐Ÿ‡จ๐Ÿ‡ฟ' }, - { code: 'DK', name: 'Denmark', dialCode: '+45', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ' }, - { code: 'DJ', name: 'Djibouti', dialCode: '+253', flag: '๐Ÿ‡ฉ๐Ÿ‡ฏ' }, - { code: 'DM', name: 'Dominica', dialCode: '+1767', flag: '๐Ÿ‡ฉ๐Ÿ‡ฒ' }, - { code: 'DO', name: 'Dominican Republic', dialCode: '+1', flag: '๐Ÿ‡ฉ๐Ÿ‡ด' }, - { code: 'EC', name: 'Ecuador', dialCode: '+593', flag: '๐Ÿ‡ช๐Ÿ‡จ' }, - { code: 'EG', name: 'Egypt', dialCode: '+20', flag: '๐Ÿ‡ช๐Ÿ‡ฌ' }, - { code: 'SV', name: 'El Salvador', dialCode: '+503', flag: '๐Ÿ‡ธ๐Ÿ‡ป' }, - { code: 'GQ', name: 'Equatorial Guinea', dialCode: '+240', flag: '๐Ÿ‡ฌ๐Ÿ‡ถ' }, - { code: 'ER', name: 'Eritrea', dialCode: '+291', flag: '๐Ÿ‡ช๐Ÿ‡ท' }, - { code: 'EE', name: 'Estonia', dialCode: '+372', flag: '๐Ÿ‡ช๐Ÿ‡ช' }, - { code: 'ET', name: 'Ethiopia', dialCode: '+251', flag: '๐Ÿ‡ช๐Ÿ‡น' }, - { code: 'FK', name: 'Falkland Islands', dialCode: '+500', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ' }, - { code: 'FO', name: 'Faroe Islands', dialCode: '+298', flag: '๐Ÿ‡ซ๐Ÿ‡ด' }, - { code: 'FJ', name: 'Fiji', dialCode: '+679', flag: '๐Ÿ‡ซ๐Ÿ‡ฏ' }, - { code: 'FI', name: 'Finland', dialCode: '+358', flag: '๐Ÿ‡ซ๐Ÿ‡ฎ' }, - { code: 'FR', name: 'France', dialCode: '+33', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, - { code: 'GF', name: 'French Guiana', dialCode: '+594', flag: '๐Ÿ‡ฌ๐Ÿ‡ซ' }, - { code: 'PF', name: 'French Polynesia', dialCode: '+689', flag: '๐Ÿ‡ต๐Ÿ‡ซ' }, - { code: 'GA', name: 'Gabon', dialCode: '+241', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ' }, - { code: 'GM', name: 'Gambia', dialCode: '+220', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ' }, - { code: 'GE', name: 'Georgia', dialCode: '+995', flag: '๐Ÿ‡ฌ๐Ÿ‡ช' }, - { code: 'DE', name: 'Germany', dialCode: '+49', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, - { code: 'GH', name: 'Ghana', dialCode: '+233', flag: '๐Ÿ‡ฌ๐Ÿ‡ญ' }, - { code: 'GI', name: 'Gibraltar', dialCode: '+350', flag: '๐Ÿ‡ฌ๐Ÿ‡ฎ' }, - { code: 'GR', name: 'Greece', dialCode: '+30', flag: '๐Ÿ‡ฌ๐Ÿ‡ท' }, - { code: 'GL', name: 'Greenland', dialCode: '+299', flag: '๐Ÿ‡ฌ๐Ÿ‡ฑ' }, - { code: 'GD', name: 'Grenada', dialCode: '+1473', flag: '๐Ÿ‡ฌ๐Ÿ‡ฉ' }, - { code: 'GP', name: 'Guadeloupe', dialCode: '+590', flag: '๐Ÿ‡ฌ๐Ÿ‡ต' }, - { code: 'GU', name: 'Guam', dialCode: '+1671', flag: '๐Ÿ‡ฌ๐Ÿ‡บ' }, - { code: 'GT', name: 'Guatemala', dialCode: '+502', flag: '๐Ÿ‡ฌ๐Ÿ‡น' }, - { code: 'GG', name: 'Guernsey', dialCode: '+44', flag: '๐Ÿ‡ฌ๐Ÿ‡ฌ' }, - { code: 'GN', name: 'Guinea', dialCode: '+224', flag: '๐Ÿ‡ฌ๐Ÿ‡ณ' }, - { code: 'GW', name: 'Guinea-Bissau', dialCode: '+245', flag: '๐Ÿ‡ฌ๐Ÿ‡ผ' }, - { code: 'GY', name: 'Guyana', dialCode: '+592', flag: '๐Ÿ‡ฌ๐Ÿ‡พ' }, - { code: 'HT', name: 'Haiti', dialCode: '+509', flag: '๐Ÿ‡ญ๐Ÿ‡น' }, - { code: 'VA', name: 'Holy See', dialCode: '+379', flag: '๐Ÿ‡ป๐Ÿ‡ฆ' }, - { code: 'HN', name: 'Honduras', dialCode: '+504', flag: '๐Ÿ‡ญ๐Ÿ‡ณ' }, - { code: 'HK', name: 'Hong Kong', dialCode: '+852', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ' }, - { code: 'HU', name: 'Hungary', dialCode: '+36', flag: '๐Ÿ‡ญ๐Ÿ‡บ' }, - { code: 'IS', name: 'Iceland', dialCode: '+354', flag: '๐Ÿ‡ฎ๐Ÿ‡ธ' }, - { code: 'IN', name: 'India', dialCode: '+91', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, - { code: 'ID', name: 'Indonesia', dialCode: '+62', flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ' }, - { code: 'IR', name: 'Iran', dialCode: '+98', flag: '๐Ÿ‡ฎ๐Ÿ‡ท' }, - { code: 'IQ', name: 'Iraq', dialCode: '+964', flag: '๐Ÿ‡ฎ๐Ÿ‡ถ' }, - { code: 'IE', name: 'Ireland', dialCode: '+353', flag: '๐Ÿ‡ฎ๐Ÿ‡ช' }, - { code: 'IM', name: 'Isle of Man', dialCode: '+44', flag: '๐Ÿ‡ฎ๐Ÿ‡ฒ' }, - { code: 'IL', name: 'Israel', dialCode: '+972', flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ' }, - { code: 'IT', name: 'Italy', dialCode: '+39', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, - { code: 'JM', name: 'Jamaica', dialCode: '+1876', flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ' }, - { code: 'JP', name: 'Japan', dialCode: '+81', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, - { code: 'JE', name: 'Jersey', dialCode: '+44', flag: '๐Ÿ‡ฏ๐Ÿ‡ช' }, - { code: 'JO', name: 'Jordan', dialCode: '+962', flag: '๐Ÿ‡ฏ๐Ÿ‡ด' }, - { code: 'KZ', name: 'Kazakhstan', dialCode: '+7', flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ' }, - { code: 'KE', name: 'Kenya', dialCode: '+254', flag: '๐Ÿ‡ฐ๐Ÿ‡ช' }, - { code: 'KI', name: 'Kiribati', dialCode: '+686', flag: '๐Ÿ‡ฐ๐Ÿ‡ฎ' }, - { code: 'KP', name: 'North Korea', dialCode: '+850', flag: '๐Ÿ‡ฐ๐Ÿ‡ต' }, - { code: 'KR', name: 'South Korea', dialCode: '+82', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, - { code: 'KW', name: 'Kuwait', dialCode: '+965', flag: '๐Ÿ‡ฐ๐Ÿ‡ผ' }, - { code: 'KG', name: 'Kyrgyzstan', dialCode: '+996', flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ' }, - { code: 'LA', name: 'Laos', dialCode: '+856', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ' }, - { code: 'LV', name: 'Latvia', dialCode: '+371', flag: '๐Ÿ‡ฑ๐Ÿ‡ป' }, - { code: 'LB', name: 'Lebanon', dialCode: '+961', flag: '๐Ÿ‡ฑ๐Ÿ‡ง' }, - { code: 'LS', name: 'Lesotho', dialCode: '+266', flag: '๐Ÿ‡ฑ๐Ÿ‡ธ' }, - { code: 'LR', name: 'Liberia', dialCode: '+231', flag: '๐Ÿ‡ฑ๐Ÿ‡ท' }, - { code: 'LY', name: 'Libya', dialCode: '+218', flag: '๐Ÿ‡ฑ๐Ÿ‡พ' }, - { code: 'LI', name: 'Liechtenstein', dialCode: '+423', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ' }, - { code: 'LT', name: 'Lithuania', dialCode: '+370', flag: '๐Ÿ‡ฑ๐Ÿ‡น' }, - { code: 'LU', name: 'Luxembourg', dialCode: '+352', flag: '๐Ÿ‡ฑ๐Ÿ‡บ' }, - { code: 'MO', name: 'Macau', dialCode: '+853', flag: '๐Ÿ‡ฒ๐Ÿ‡ด' }, - { code: 'MK', name: 'North Macedonia', dialCode: '+389', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ' }, - { code: 'MG', name: 'Madagascar', dialCode: '+261', flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ' }, - { code: 'MW', name: 'Malawi', dialCode: '+265', flag: '๐Ÿ‡ฒ๐Ÿ‡ผ' }, - { code: 'MY', name: 'Malaysia', dialCode: '+60', flag: '๐Ÿ‡ฒ๐Ÿ‡พ' }, - { code: 'MV', name: 'Maldives', dialCode: '+960', flag: '๐Ÿ‡ฒ๐Ÿ‡ป' }, - { code: 'ML', name: 'Mali', dialCode: '+223', flag: '๐Ÿ‡ฒ๐Ÿ‡ฑ' }, - { code: 'MT', name: 'Malta', dialCode: '+356', flag: '๐Ÿ‡ฒ๐Ÿ‡น' }, - { code: 'MH', name: 'Marshall Islands', dialCode: '+692', flag: '๐Ÿ‡ฒ๐Ÿ‡ญ' }, - { code: 'MQ', name: 'Martinique', dialCode: '+596', flag: '๐Ÿ‡ฒ๐Ÿ‡ถ' }, - { code: 'MR', name: 'Mauritania', dialCode: '+222', flag: '๐Ÿ‡ฒ๐Ÿ‡ท' }, - { code: 'MU', name: 'Mauritius', dialCode: '+230', flag: '๐Ÿ‡ฒ๐Ÿ‡บ' }, - { code: 'YT', name: 'Mayotte', dialCode: '+262', flag: '๐Ÿ‡พ๐Ÿ‡น' }, - { code: 'MX', name: 'Mexico', dialCode: '+52', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ' }, - { code: 'FM', name: 'Micronesia', dialCode: '+691', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ' }, - { code: 'MD', name: 'Moldova', dialCode: '+373', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ' }, - { code: 'MC', name: 'Monaco', dialCode: '+377', flag: '๐Ÿ‡ฒ๐Ÿ‡จ' }, - { code: 'MN', name: 'Mongolia', dialCode: '+976', flag: '๐Ÿ‡ฒ๐Ÿ‡ณ' }, - { code: 'ME', name: 'Montenegro', dialCode: '+382', flag: '๐Ÿ‡ฒ๐Ÿ‡ช' }, - { code: 'MS', name: 'Montserrat', dialCode: '+1664', flag: '๐Ÿ‡ฒ๐Ÿ‡ธ' }, - { code: 'MA', name: 'Morocco', dialCode: '+212', flag: '๐Ÿ‡ฒ๐Ÿ‡ฆ' }, - { code: 'MZ', name: 'Mozambique', dialCode: '+258', flag: '๐Ÿ‡ฒ๐Ÿ‡ฟ' }, - { code: 'MM', name: 'Myanmar', dialCode: '+95', flag: '๐Ÿ‡ฒ๐Ÿ‡ฒ' }, - { code: 'NA', name: 'Namibia', dialCode: '+264', flag: '๐Ÿ‡ณ๐Ÿ‡ฆ' }, - { code: 'NR', name: 'Nauru', dialCode: '+674', flag: '๐Ÿ‡ณ๐Ÿ‡ท' }, - { code: 'NP', name: 'Nepal', dialCode: '+977', flag: '๐Ÿ‡ณ๐Ÿ‡ต' }, - { code: 'NL', name: 'Netherlands', dialCode: '+31', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ' }, - { code: 'NC', name: 'New Caledonia', dialCode: '+687', flag: '๐Ÿ‡ณ๐Ÿ‡จ' }, - { code: 'NZ', name: 'New Zealand', dialCode: '+64', flag: '๐Ÿ‡ณ๐Ÿ‡ฟ' }, - { code: 'NI', name: 'Nicaragua', dialCode: '+505', flag: '๐Ÿ‡ณ๐Ÿ‡ฎ' }, - { code: 'NE', name: 'Niger', dialCode: '+227', flag: '๐Ÿ‡ณ๐Ÿ‡ช' }, - { code: 'NG', name: 'Nigeria', dialCode: '+234', flag: '๐Ÿ‡ณ๐Ÿ‡ฌ' }, - { code: 'NU', name: 'Niue', dialCode: '+683', flag: '๐Ÿ‡ณ๐Ÿ‡บ' }, - { code: 'NF', name: 'Norfolk Island', dialCode: '+672', flag: '๐Ÿ‡ณ๐Ÿ‡ซ' }, - { code: 'MP', name: 'Northern Mariana Islands', dialCode: '+1670', flag: '๐Ÿ‡ฒ๐Ÿ‡ต' }, - { code: 'NO', name: 'Norway', dialCode: '+47', flag: '๐Ÿ‡ณ๐Ÿ‡ด' }, - { code: 'OM', name: 'Oman', dialCode: '+968', flag: '๐Ÿ‡ด๐Ÿ‡ฒ' }, - { code: 'PK', name: 'Pakistan', dialCode: '+92', flag: '๐Ÿ‡ต๐Ÿ‡ฐ' }, - { code: 'PW', name: 'Palau', dialCode: '+680', flag: '๐Ÿ‡ต๐Ÿ‡ผ' }, - { code: 'PS', name: 'Palestine', dialCode: '+970', flag: '๐Ÿ‡ต๐Ÿ‡ธ' }, - { code: 'PA', name: 'Panama', dialCode: '+507', flag: '๐Ÿ‡ต๐Ÿ‡ฆ' }, - { code: 'PG', name: 'Papua New Guinea', dialCode: '+675', flag: '๐Ÿ‡ต๐Ÿ‡ฌ' }, - { code: 'PY', name: 'Paraguay', dialCode: '+595', flag: '๐Ÿ‡ต๐Ÿ‡พ' }, - { code: 'PE', name: 'Peru', dialCode: '+51', flag: '๐Ÿ‡ต๐Ÿ‡ช' }, - { code: 'PH', name: 'Philippines', dialCode: '+63', flag: '๐Ÿ‡ต๐Ÿ‡ญ' }, - { code: 'PN', name: 'Pitcairn Islands', dialCode: '+64', flag: '๐Ÿ‡ต๐Ÿ‡ณ' }, - { code: 'PL', name: 'Poland', dialCode: '+48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ' }, - { code: 'PT', name: 'Portugal', dialCode: '+351', flag: '๐Ÿ‡ต๐Ÿ‡น' }, - { code: 'PR', name: 'Puerto Rico', dialCode: '+1787', flag: '๐Ÿ‡ต๐Ÿ‡ท' }, - { code: 'QA', name: 'Qatar', dialCode: '+974', flag: '๐Ÿ‡ถ๐Ÿ‡ฆ' }, - { code: 'RE', name: 'Rรฉunion', dialCode: '+262', flag: '๐Ÿ‡ท๐Ÿ‡ช' }, - { code: 'RO', name: 'Romania', dialCode: '+40', flag: '๐Ÿ‡ท๐Ÿ‡ด' }, - { code: 'RU', name: 'Russia', dialCode: '+7', flag: '๐Ÿ‡ท๐Ÿ‡บ' }, - { code: 'RW', name: 'Rwanda', dialCode: '+250', flag: '๐Ÿ‡ท๐Ÿ‡ผ' }, - { code: 'BL', name: 'Saint Barthรฉlemy', dialCode: '+590', flag: '๐Ÿ‡ง๐Ÿ‡ฑ' }, - { code: 'SH', name: 'Saint Helena', dialCode: '+290', flag: '๐Ÿ‡ธ๐Ÿ‡ญ' }, - { code: 'KN', name: 'Saint Kitts and Nevis', dialCode: '+1869', flag: '๐Ÿ‡ฐ๐Ÿ‡ณ' }, - { code: 'LC', name: 'Saint Lucia', dialCode: '+1758', flag: '๐Ÿ‡ฑ๐Ÿ‡จ' }, - { code: 'MF', name: 'Saint Martin', dialCode: '+590', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ' }, - { code: 'PM', name: 'Saint Pierre and Miquelon', dialCode: '+508', flag: '๐Ÿ‡ต๐Ÿ‡ฒ' }, - { code: 'VC', name: 'Saint Vincent and the Grenadines', dialCode: '+1784', flag: '๐Ÿ‡ป๐Ÿ‡จ' }, - { code: 'WS', name: 'Samoa', dialCode: '+685', flag: '๐Ÿ‡ผ๐Ÿ‡ธ' }, - { code: 'SM', name: 'San Marino', dialCode: '+378', flag: '๐Ÿ‡ธ๐Ÿ‡ฒ' }, - { code: 'ST', name: 'Sรฃo Tomรฉ and Prรญncipe', dialCode: '+239', flag: '๐Ÿ‡ธ๐Ÿ‡น' }, - { code: 'SA', name: 'Saudi Arabia', dialCode: '+966', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ' }, - { code: 'SN', name: 'Senegal', dialCode: '+221', flag: '๐Ÿ‡ธ๐Ÿ‡ณ' }, - { code: 'RS', name: 'Serbia', dialCode: '+381', flag: '๐Ÿ‡ท๐Ÿ‡ธ' }, - { code: 'SC', name: 'Seychelles', dialCode: '+248', flag: '๐Ÿ‡ธ๐Ÿ‡จ' }, - { code: 'SL', name: 'Sierra Leone', dialCode: '+232', flag: '๐Ÿ‡ธ๐Ÿ‡ฑ' }, - { code: 'SG', name: 'Singapore', dialCode: '+65', flag: '๐Ÿ‡ธ๐Ÿ‡ฌ' }, - { code: 'SX', name: 'Sint Maarten', dialCode: '+1721', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ' }, - { code: 'SK', name: 'Slovakia', dialCode: '+421', flag: '๐Ÿ‡ธ๐Ÿ‡ฐ' }, - { code: 'SI', name: 'Slovenia', dialCode: '+386', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ' }, - { code: 'SB', name: 'Solomon Islands', dialCode: '+677', flag: '๐Ÿ‡ธ๐Ÿ‡ง' }, - { code: 'SO', name: 'Somalia', dialCode: '+252', flag: '๐Ÿ‡ธ๐Ÿ‡ด' }, - { code: 'ZA', name: 'South Africa', dialCode: '+27', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ' }, - { code: 'GS', name: 'South Georgia and the South Sandwich Islands', dialCode: '+500', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ' }, - { code: 'SS', name: 'South Sudan', dialCode: '+211', flag: '๐Ÿ‡ธ๐Ÿ‡ธ' }, - { code: 'ES', name: 'Spain', dialCode: '+34', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, - { code: 'LK', name: 'Sri Lanka', dialCode: '+94', flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ' }, - { code: 'SD', name: 'Sudan', dialCode: '+249', flag: '๐Ÿ‡ธ๐Ÿ‡ฉ' }, - { code: 'SR', name: 'Suriname', dialCode: '+597', flag: '๐Ÿ‡ธ๐Ÿ‡ท' }, - { code: 'SJ', name: 'Svalbard and Jan Mayen', dialCode: '+47', flag: '๐Ÿ‡ธ๐Ÿ‡ฏ' }, - { code: 'SZ', name: 'Eswatini', dialCode: '+268', flag: '๐Ÿ‡ธ๐Ÿ‡ฟ' }, - { code: 'SE', name: 'Sweden', dialCode: '+46', flag: '๐Ÿ‡ธ๐Ÿ‡ช' }, - { code: 'CH', name: 'Switzerland', dialCode: '+41', flag: '๐Ÿ‡จ๐Ÿ‡ญ' }, - { code: 'SY', name: 'Syria', dialCode: '+963', flag: '๐Ÿ‡ธ๐Ÿ‡พ' }, - { code: 'TW', name: 'Taiwan', dialCode: '+886', flag: '๐Ÿ‡น๐Ÿ‡ผ' }, - { code: 'TJ', name: 'Tajikistan', dialCode: '+992', flag: '๐Ÿ‡น๐Ÿ‡ฏ' }, - { code: 'TZ', name: 'Tanzania', dialCode: '+255', flag: '๐Ÿ‡น๐Ÿ‡ฟ' }, - { code: 'TH', name: 'Thailand', dialCode: '+66', flag: '๐Ÿ‡น๐Ÿ‡ญ' }, - { code: 'TL', name: 'Timor-Leste', dialCode: '+670', flag: '๐Ÿ‡น๐Ÿ‡ฑ' }, - { code: 'TG', name: 'Togo', dialCode: '+228', flag: '๐Ÿ‡น๐Ÿ‡ฌ' }, - { code: 'TK', name: 'Tokelau', dialCode: '+690', flag: '๐Ÿ‡น๐Ÿ‡ฐ' }, - { code: 'TO', name: 'Tonga', dialCode: '+676', flag: '๐Ÿ‡น๐Ÿ‡ด' }, - { code: 'TT', name: 'Trinidad and Tobago', dialCode: '+1868', flag: '๐Ÿ‡น๐Ÿ‡น' }, - { code: 'TN', name: 'Tunisia', dialCode: '+216', flag: '๐Ÿ‡น๐Ÿ‡ณ' }, - { code: 'TR', name: 'Turkey', dialCode: '+90', flag: '๐Ÿ‡น๐Ÿ‡ท' }, - { code: 'TM', name: 'Turkmenistan', dialCode: '+993', flag: '๐Ÿ‡น๐Ÿ‡ฒ' }, - { code: 'TC', name: 'Turks and Caicos Islands', dialCode: '+1649', flag: '๐Ÿ‡น๐Ÿ‡จ' }, - { code: 'TV', name: 'Tuvalu', dialCode: '+688', flag: '๐Ÿ‡น๐Ÿ‡ป' }, - { code: 'UG', name: 'Uganda', dialCode: '+256', flag: '๐Ÿ‡บ๐Ÿ‡ฌ' }, - { code: 'UA', name: 'Ukraine', dialCode: '+380', flag: '๐Ÿ‡บ๐Ÿ‡ฆ' }, - { code: 'AE', name: 'United Arab Emirates', dialCode: '+971', flag: '๐Ÿ‡ฆ๐Ÿ‡ช' }, - { code: 'GB', name: 'United Kingdom', dialCode: '+44', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, - { code: 'US', name: 'United States', dialCode: '+1', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, - { code: 'UM', name: 'United States Minor Outlying Islands', dialCode: '+1', flag: '๐Ÿ‡บ๐Ÿ‡ฒ' }, - { code: 'UY', name: 'Uruguay', dialCode: '+598', flag: '๐Ÿ‡บ๐Ÿ‡พ' }, - { code: 'UZ', name: 'Uzbekistan', dialCode: '+998', flag: '๐Ÿ‡บ๐Ÿ‡ฟ' }, - { code: 'VU', name: 'Vanuatu', dialCode: '+678', flag: '๐Ÿ‡ป๐Ÿ‡บ' }, - { code: 'VE', name: 'Venezuela', dialCode: '+58', flag: '๐Ÿ‡ป๐Ÿ‡ช' }, - { code: 'VN', name: 'Vietnam', dialCode: '+84', flag: '๐Ÿ‡ป๐Ÿ‡ณ' }, - { code: 'VG', name: 'British Virgin Islands', dialCode: '+1284', flag: '๐Ÿ‡ป๐Ÿ‡ฌ' }, - { code: 'VI', name: 'U.S. Virgin Islands', dialCode: '+1340', flag: '๐Ÿ‡ป๐Ÿ‡ฎ' }, - { code: 'WF', name: 'Wallis and Futuna', dialCode: '+681', flag: '๐Ÿ‡ผ๐Ÿ‡ซ' }, - { code: 'EH', name: 'Western Sahara', dialCode: '+212', flag: '๐Ÿ‡ช๐Ÿ‡ญ' }, - { code: 'YE', name: 'Yemen', dialCode: '+967', flag: '๐Ÿ‡พ๐Ÿ‡ช' }, - { code: 'ZM', name: 'Zambia', dialCode: '+260', flag: '๐Ÿ‡ฟ๐Ÿ‡ฒ' }, - { code: 'ZW', name: 'Zimbabwe', dialCode: '+263', flag: '๐Ÿ‡ฟ๐Ÿ‡ผ' } - ]; - - ngOnInit(): void { - super.ngOnInit(); - this.configureFromWidgetParams(); - this.initializePhoneNumber(); - this.initializeAutocomplete(); - } - - configureFromWidgetParams(): void { - if (this.widgetStructure?.widget_params) { - const params = this.widgetStructure.widget_params; - - if (params.preferred_countries && Array.isArray(params.preferred_countries)) { - this.preferredCountries = params.preferred_countries; - } - - if (typeof params.enable_placeholder === 'boolean') { - this.enablePlaceholder = params.enable_placeholder; - } - - - if (typeof params.phone_validation === 'boolean') { - this.phoneValidation = params.phone_validation; - } - } - } - - private initializePhoneNumber(): void { - if (this.value) { - this.parseExistingPhoneNumber(this.value); - } else { - // Set default country - this.setDefaultCountry(); - this.displayPhoneNumber = ''; - } - } - - private parseExistingPhoneNumber(fullNumber: string): void { - let phoneNumber; - - try { - // First try to parse as international number - phoneNumber = parsePhoneNumber(fullNumber); - } catch (_error) { - // Will try with default country below - } - - // If that failed or didn't detect country, try with default country - if (!phoneNumber || !phoneNumber.country) { - try { - const defaultCountryCode = this.preferredCountries[0] || 'US'; - phoneNumber = parsePhoneNumber(fullNumber, defaultCountryCode as LibPhoneCountryCode); - } catch (error) { - console.warn('Failed to parse with default country as well:', error); - } - } - - if (phoneNumber?.country) { - // Find the country in our list - exact match by country code - const country = this.countries.find(c => c.code === phoneNumber.country); - if (country) { - this.selectedCountry = country; - this.countryControl.setValue(country); - this.phoneNumber = phoneNumber.nationalNumber; - this.displayPhoneNumber = phoneNumber.formatNational(); - this.initializeFormatter(); - return; - } else { - console.warn('Country not found in list:', phoneNumber.country); - } - } - - // Fallback: use default country and original number - this.setDefaultCountry(); - this.phoneNumber = fullNumber.replace(/\D/g, ''); - - // Try to format with default country formatter - if (this.formatter && this.phoneNumber) { - this.formatter.reset(); - this.displayPhoneNumber = this.formatter.input(this.phoneNumber); - } else { - this.displayPhoneNumber = fullNumber; - } - } - - private setDefaultCountry(): void { - this.selectedCountry = this.countries.find(c => c.code === this.preferredCountries[0]) || this.countries[0]; - this.countryControl.setValue(this.selectedCountry); - this.initializeFormatter(); - } - - private initializeAutocomplete(): void { - this.filteredCountries$ = this.countryControl.valueChanges.pipe( - startWith(this.selectedCountry), - map(value => { - if (typeof value === 'string') { - return this._filterCountries(value); - } else if (value && typeof value === 'object') { - return this.sortedCountries; - } - return this.sortedCountries; - }) - ); - } - - _filterCountries(value: string): CountryCode[] { - const filterValue = value.toLowerCase(); - return this.sortedCountries.filter(country => - country.name.toLowerCase().includes(filterValue) || - country.code.toLowerCase().includes(filterValue) || - country.dialCode.includes(filterValue) - ); - } - - displayCountryFn(country: CountryCode): string { - return country ? `${country.flag} ${country.name} ${country.dialCode}` : ''; - } - - onCountrySelected(country: CountryCode): void { - this.selectedCountry = country; - this.initializeFormatter(); - this.formatAndUpdatePhoneNumber(); - } - - initializeFormatter(): void { - if (this.selectedCountry) { - this.formatter = new AsYouType(this.selectedCountry.code as LibPhoneCountryCode); - } - } - - onCountryChange(): void { - this.initializeFormatter(); - this.formatAndUpdatePhoneNumber(); - } - - onPhoneNumberChange(): void { - // Check if user entered a full international number (starts with +) - if (this.displayPhoneNumber.startsWith('+')) { - this.detectCountryFromInput(); - } else { - this.formatAndUpdatePhoneNumber(); - } - } - - private formatAndUpdatePhoneNumber(): void { - if (!this.displayPhoneNumber) { - this.phoneNumber = ''; - this.value = ''; - this.onFieldChange.emit(this.value); - return; - } - - if (this.formatter && !this.displayPhoneNumber.startsWith('+')) { - this.formatter.reset(); - const formatted = this.formatter.input(this.displayPhoneNumber); - this.displayPhoneNumber = formatted; - - // Extract raw number for storage - this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); - } else { - this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); - } - - this.updateFullPhoneNumber(); - } - - private detectCountryFromInput(): void { - if (!this.displayPhoneNumber.startsWith('+')) { - return; - } - - try { - const phoneNumber = parsePhoneNumber(this.displayPhoneNumber); - if (phoneNumber?.country) { - const detectedCountry = this.countries.find(c => c.code === phoneNumber.country); - if (detectedCountry) { - this.selectedCountry = detectedCountry; - this.countryControl.setValue(detectedCountry); - this.phoneNumber = phoneNumber.nationalNumber; - this.displayPhoneNumber = phoneNumber.formatNational(); - this.initializeFormatter(); - this.updateFullPhoneNumber(); - return; - } - } - } catch (error) { - console.warn('Could not detect country from input:', this.displayPhoneNumber, error); - } - - // If detection failed, update with current input - this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); - this.updateFullPhoneNumber(); - } - - private updateFullPhoneNumber(): void { - if (!this.displayPhoneNumber && !this.phoneNumber) { - this.value = ''; - this.onFieldChange.emit(this.value); - return; - } - - try { - let phoneNumber; - - if (this.displayPhoneNumber.startsWith('+')) { - // User entered full international number - phoneNumber = parsePhoneNumber(this.displayPhoneNumber); - } else if (this.selectedCountry && this.displayPhoneNumber) { - // User entered local number, parse with selected country - phoneNumber = parsePhoneNumber(this.displayPhoneNumber, this.selectedCountry.code as LibPhoneCountryCode); - } - - if (phoneNumber?.isValid()) { - // Store in international format without spaces (E164) - this.value = phoneNumber.number; // E164 format: +380671111111 - } else { - // Fallback: clean the display number - this.value = this.displayPhoneNumber.replace(/\s/g, ''); - } - } catch (error) { - console.warn('Error formatting phone number:', error); - // Fallback: clean the display number - this.value = this.displayPhoneNumber.replace(/\s/g, ''); - } - - this.onFieldChange.emit(this.value); - } - - get sortedCountries(): CountryCode[] { - const preferred = this.countries.filter(c => this.preferredCountries.includes(c.code)); - const others = this.countries.filter(c => !this.preferredCountries.includes(c.code)); - return [...preferred, ...others]; - } - - get placeholder(): string { - if (!this.enablePlaceholder) return ''; - return this.selectedCountry ? `Phone number for ${this.selectedCountry.name}` : 'Phone number'; - } - - getPhoneNumberPlaceholder(): string { - if (!this.enablePlaceholder) return ''; - if (this.selectedCountry) { - return `Local number or ${this.selectedCountry.dialCode}1234567890`; - } - return 'Enter +1234567890 or select country'; - } - - getExamplePhoneNumber(): string { - if (!this.selectedCountry) return ''; - - // Generate example phone number based on country - const exampleNumbers: { [key: string]: string } = { - 'US': '(202) 456-1111', - 'GB': '020 7946 0958', - 'CA': '(416) 555-1234', - 'AU': '(02) 1234 5678', - 'DE': '030 12345678', - 'FR': '01 23 45 67 89', - 'IT': '06 1234 5678', - 'ES': '91 123 45 67', - 'NL': '020 123 4567', - 'BE': '02 123 45 67', - 'CH': '044 123 45 67', - 'AT': '01 12345678', - 'SE': '08-123 456 78', - 'NO': '22 12 34 56', - 'DK': '32 12 34 56', - 'FI': '09 1234 5678', - 'PL': '12 123 45 67', - 'CZ': '224 123 456', - 'HU': '(06 1) 123 4567', - 'SK': '2 1234 5678', - 'SI': '1 123 45 67', - 'HR': '1 123 4567', - 'RO': '021 123 4567', - 'BG': '02 123 4567', - 'GR': '21 1234 5678', - 'PT': '21 123 4567', - 'IE': '01 123 4567', - 'LU': '621 123 456', - 'MT': '2123 4567', - 'CY': '22 123456', - 'EE': '372 1234', - 'LV': '2123 4567', - 'LT': '8 612 34567', - 'RU': '8 (495) 123-45-67', - 'UA': '044 123 4567', - 'BY': '8 017 123-45-67', - 'MD': '22 123456', - 'JP': '03-1234-5678', - 'KR': '02-123-4567', - 'CN': '010 1234 5678', - 'HK': '2123 4567', - 'TW': '02 1234 5678', - 'SG': '6123 4567', - 'MY': '03-1234 5678', - 'TH': '02 123 4567', - 'PH': '02 1234 5678', - 'ID': '021 1234 5678', - 'VN': '28 1234 5678', - 'IN': '011 1234 5678', - 'PK': '21 1234 5678', - 'BD': '2 1234 5678', - 'LK': '11 234 5678', - 'NP': '1 123 4567', - 'AF': '20 123 4567', - 'IR': '021 1234 5678', - 'IQ': '1 123 4567', - 'SA': '011 123 4567', - 'AE': '4 123 4567', - 'QA': '4412 3456', - 'KW': '2221 2345', - 'BH': '1712 3456', - 'OM': '2412 3456', - 'JO': '6 123 4567', - 'LB': '1 123 456', - 'SY': '11 123 4567', - 'IL': '2-123-4567', - 'PS': '59 123 4567', - 'TR': '(0212) 123 45 67', - 'GE': '32 123 45 67', - 'AM': '10 123456', - 'AZ': '12 123 45 67', - 'KZ': '8 (7172) 12 34 56', - 'KG': '312 123456', - 'TJ': '372 123456', - 'UZ': '71 123 45 67', - 'TM': '12 123456', - 'MN': '11 123456', - 'ZA': '011 123 4567', - 'EG': '02 12345678', - 'MA': '522 123456', - 'TN': '71 123 456', - 'DZ': '21 12 34 56', - 'LY': '21 123 4567', - 'SD': '15 123 4567', - 'ET': '11 123 4567', - 'KE': '20 123 4567', - 'UG': '41 123 4567', - 'TZ': '22 123 4567', - 'RW': '78 123 4567', - 'BI': '22 12 34 56', - 'DJ': '77 12 34 56', - 'SO': '1 123456', - 'ER': '1 123 456', - 'SS': '95 123 4567', - 'CF': '70 12 34 56', - 'TD': '22 12 34 56', - 'CM': '6 71 23 45 67', - 'GQ': '222 123456', - 'GA': '06 12 34 56', - 'CG': '06 612 3456', - 'CD': '12 123 4567', - 'AO': '222 123456', - 'ZM': '21 123 4567', - 'ZW': '4 123456', - 'BW': '71 123 456', - 'NA': '61 123 4567', - 'SZ': '2505 1234', - 'LS': '2212 3456', - 'MZ': '21 123456', - 'MW': '1 123 456', - 'MG': '20 12 345 67', - 'MU': '212 3456', - 'SC': '4 123 456', - 'KM': '773 1234', - 'YT': '269 61 23 45', - 'RE': '262 12 34 56', - 'MV': '330 1234', - 'BR': '(11) 1234-5678', - 'AR': '011 1234-5678', - 'CL': '2 1234 5678', - 'CO': '(601) 234 5678', - 'PE': '1 123 4567', - 'VE': '0212-1234567', - 'EC': '2 123 4567', - 'BO': '2 123 4567', - 'PY': '21 123 456', - 'UY': '2 123 4567', - 'GY': '222 1234', - 'SR': '421234', - 'GF': '594 12 34 56', - 'FK': '41234', - 'MX': '55 1234 5678', - 'GT': '2 123 4567', - 'BZ': '223 1234', - 'SV': '2123 4567', - 'HN': '2 123 4567', - 'NI': '2 123 4567', - 'CR': '2 123 4567', - 'PA': '123 4567' - }; - - return exampleNumbers[this.selectedCountry.code] || `${this.selectedCountry.dialCode} 123 4567`; - } - - isValidPhoneNumber(): boolean { - if (!this.phoneValidation) return true; - if (!this.displayPhoneNumber) return true; // Empty is valid (let required validation handle it) - - try { - let phoneNumber; - - if (this.displayPhoneNumber.startsWith('+')) { - phoneNumber = parsePhoneNumber(this.displayPhoneNumber); - } else if (this.selectedCountry) { - phoneNumber = parsePhoneNumber(this.displayPhoneNumber, this.selectedCountry.code as LibPhoneCountryCode); - } else { - return false; - } - - return phoneNumber ? phoneNumber.isValid() : false; - } catch (_error) { - return false; - } - } -} \ No newline at end of file + @Input() value: string = ''; + + static type = 'phone'; + + preferredCountries: string[] = ['US', 'GB']; + enablePlaceholder: boolean = true; + phoneValidation: boolean = true; + + selectedCountry: CountryCode; + phoneNumber: string = ''; + displayPhoneNumber: string = ''; + formatter: AsYouType | null = null; + + countryControl = new FormControl(null); + + private _countryControlValue = toSignal(this.countryControl.valueChanges, { + initialValue: null as CountryCode | string | null, + }); + + filteredCountries = computed(() => { + const value = this._countryControlValue(); + if (typeof value === 'string') { + return this._filterCountries(value); + } else if (value && typeof value === 'object') { + return this.sortedCountries; + } + return this.sortedCountries; + }); + + countries: CountryCode[] = [ + { code: 'AF', name: 'Afghanistan', dialCode: '+93', flag: '๐Ÿ‡ฆ๐Ÿ‡ซ' }, + { code: 'AL', name: 'Albania', dialCode: '+355', flag: '๐Ÿ‡ฆ๐Ÿ‡ฑ' }, + { code: 'DZ', name: 'Algeria', dialCode: '+213', flag: '๐Ÿ‡ฉ๐Ÿ‡ฟ' }, + { code: 'AS', name: 'American Samoa', dialCode: '+1684', flag: '๐Ÿ‡ฆ๐Ÿ‡ธ' }, + { code: 'AD', name: 'Andorra', dialCode: '+376', flag: '๐Ÿ‡ฆ๐Ÿ‡ฉ' }, + { code: 'AO', name: 'Angola', dialCode: '+244', flag: '๐Ÿ‡ฆ๐Ÿ‡ด' }, + { code: 'AI', name: 'Anguilla', dialCode: '+1264', flag: '๐Ÿ‡ฆ๐Ÿ‡ฎ' }, + { code: 'AQ', name: 'Antarctica', dialCode: '+672', flag: '๐Ÿ‡ฆ๐Ÿ‡ถ' }, + { code: 'AG', name: 'Antigua and Barbuda', dialCode: '+1268', flag: '๐Ÿ‡ฆ๐Ÿ‡ฌ' }, + { code: 'AR', name: 'Argentina', dialCode: '+54', flag: '๐Ÿ‡ฆ๐Ÿ‡ท' }, + { code: 'AM', name: 'Armenia', dialCode: '+374', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ' }, + { code: 'AW', name: 'Aruba', dialCode: '+297', flag: '๐Ÿ‡ฆ๐Ÿ‡ผ' }, + { code: 'AU', name: 'Australia', dialCode: '+61', flag: '๐Ÿ‡ฆ๐Ÿ‡บ' }, + { code: 'AT', name: 'Austria', dialCode: '+43', flag: '๐Ÿ‡ฆ๐Ÿ‡น' }, + { code: 'AZ', name: 'Azerbaijan', dialCode: '+994', flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ' }, + { code: 'BS', name: 'Bahamas', dialCode: '+1242', flag: '๐Ÿ‡ง๐Ÿ‡ธ' }, + { code: 'BH', name: 'Bahrain', dialCode: '+973', flag: '๐Ÿ‡ง๐Ÿ‡ญ' }, + { code: 'BD', name: 'Bangladesh', dialCode: '+880', flag: '๐Ÿ‡ง๐Ÿ‡ฉ' }, + { code: 'BB', name: 'Barbados', dialCode: '+1246', flag: '๐Ÿ‡ง๐Ÿ‡ง' }, + { code: 'BY', name: 'Belarus', dialCode: '+375', flag: '๐Ÿ‡ง๐Ÿ‡พ' }, + { code: 'BE', name: 'Belgium', dialCode: '+32', flag: '๐Ÿ‡ง๐Ÿ‡ช' }, + { code: 'BZ', name: 'Belize', dialCode: '+501', flag: '๐Ÿ‡ง๐Ÿ‡ฟ' }, + { code: 'BJ', name: 'Benin', dialCode: '+229', flag: '๐Ÿ‡ง๐Ÿ‡ฏ' }, + { code: 'BM', name: 'Bermuda', dialCode: '+1441', flag: '๐Ÿ‡ง๐Ÿ‡ฒ' }, + { code: 'BT', name: 'Bhutan', dialCode: '+975', flag: '๐Ÿ‡ง๐Ÿ‡น' }, + { code: 'BO', name: 'Bolivia', dialCode: '+591', flag: '๐Ÿ‡ง๐Ÿ‡ด' }, + { code: 'BA', name: 'Bosnia and Herzegovina', dialCode: '+387', flag: '๐Ÿ‡ง๐Ÿ‡ฆ' }, + { code: 'BW', name: 'Botswana', dialCode: '+267', flag: '๐Ÿ‡ง๐Ÿ‡ผ' }, + { code: 'BR', name: 'Brazil', dialCode: '+55', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, + { code: 'IO', name: 'British Indian Ocean Territory', dialCode: '+246', flag: '๐Ÿ‡ฎ๐Ÿ‡ด' }, + { code: 'BN', name: 'Brunei', dialCode: '+673', flag: '๐Ÿ‡ง๐Ÿ‡ณ' }, + { code: 'BG', name: 'Bulgaria', dialCode: '+359', flag: '๐Ÿ‡ง๐Ÿ‡ฌ' }, + { code: 'BF', name: 'Burkina Faso', dialCode: '+226', flag: '๐Ÿ‡ง๐Ÿ‡ซ' }, + { code: 'BI', name: 'Burundi', dialCode: '+257', flag: '๐Ÿ‡ง๐Ÿ‡ฎ' }, + { code: 'KH', name: 'Cambodia', dialCode: '+855', flag: '๐Ÿ‡ฐ๐Ÿ‡ญ' }, + { code: 'CM', name: 'Cameroon', dialCode: '+237', flag: '๐Ÿ‡จ๐Ÿ‡ฒ' }, + { code: 'CA', name: 'Canada', dialCode: '+1', flag: '๐Ÿ‡จ๐Ÿ‡ฆ' }, + { code: 'CV', name: 'Cape Verde', dialCode: '+238', flag: '๐Ÿ‡จ๐Ÿ‡ป' }, + { code: 'KY', name: 'Cayman Islands', dialCode: '+1345', flag: '๐Ÿ‡ฐ๐Ÿ‡พ' }, + { code: 'CF', name: 'Central African Republic', dialCode: '+236', flag: '๐Ÿ‡จ๐Ÿ‡ซ' }, + { code: 'TD', name: 'Chad', dialCode: '+235', flag: '๐Ÿ‡น๐Ÿ‡ฉ' }, + { code: 'CL', name: 'Chile', dialCode: '+56', flag: '๐Ÿ‡จ๐Ÿ‡ฑ' }, + { code: 'CN', name: 'China', dialCode: '+86', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, + { code: 'CX', name: 'Christmas Island', dialCode: '+61', flag: '๐Ÿ‡จ๐Ÿ‡ฝ' }, + { code: 'CC', name: 'Cocos Islands', dialCode: '+61', flag: '๐Ÿ‡จ๐Ÿ‡จ' }, + { code: 'CO', name: 'Colombia', dialCode: '+57', flag: '๐Ÿ‡จ๐Ÿ‡ด' }, + { code: 'KM', name: 'Comoros', dialCode: '+269', flag: '๐Ÿ‡ฐ๐Ÿ‡ฒ' }, + { code: 'CG', name: 'Congo', dialCode: '+242', flag: '๐Ÿ‡จ๐Ÿ‡ฌ' }, + { code: 'CD', name: 'Congo (DRC)', dialCode: '+243', flag: '๐Ÿ‡จ๐Ÿ‡ฉ' }, + { code: 'CK', name: 'Cook Islands', dialCode: '+682', flag: '๐Ÿ‡จ๐Ÿ‡ฐ' }, + { code: 'CR', name: 'Costa Rica', dialCode: '+506', flag: '๐Ÿ‡จ๐Ÿ‡ท' }, + { code: 'CI', name: "Cรดte d'Ivoire", dialCode: '+225', flag: '๐Ÿ‡จ๐Ÿ‡ฎ' }, + { code: 'HR', name: 'Croatia', dialCode: '+385', flag: '๐Ÿ‡ญ๐Ÿ‡ท' }, + { code: 'CU', name: 'Cuba', dialCode: '+53', flag: '๐Ÿ‡จ๐Ÿ‡บ' }, + { code: 'CW', name: 'Curaรงao', dialCode: '+599', flag: '๐Ÿ‡จ๐Ÿ‡ผ' }, + { code: 'CY', name: 'Cyprus', dialCode: '+357', flag: '๐Ÿ‡จ๐Ÿ‡พ' }, + { code: 'CZ', name: 'Czech Republic', dialCode: '+420', flag: '๐Ÿ‡จ๐Ÿ‡ฟ' }, + { code: 'DK', name: 'Denmark', dialCode: '+45', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ' }, + { code: 'DJ', name: 'Djibouti', dialCode: '+253', flag: '๐Ÿ‡ฉ๐Ÿ‡ฏ' }, + { code: 'DM', name: 'Dominica', dialCode: '+1767', flag: '๐Ÿ‡ฉ๐Ÿ‡ฒ' }, + { code: 'DO', name: 'Dominican Republic', dialCode: '+1', flag: '๐Ÿ‡ฉ๐Ÿ‡ด' }, + { code: 'EC', name: 'Ecuador', dialCode: '+593', flag: '๐Ÿ‡ช๐Ÿ‡จ' }, + { code: 'EG', name: 'Egypt', dialCode: '+20', flag: '๐Ÿ‡ช๐Ÿ‡ฌ' }, + { code: 'SV', name: 'El Salvador', dialCode: '+503', flag: '๐Ÿ‡ธ๐Ÿ‡ป' }, + { code: 'GQ', name: 'Equatorial Guinea', dialCode: '+240', flag: '๐Ÿ‡ฌ๐Ÿ‡ถ' }, + { code: 'ER', name: 'Eritrea', dialCode: '+291', flag: '๐Ÿ‡ช๐Ÿ‡ท' }, + { code: 'EE', name: 'Estonia', dialCode: '+372', flag: '๐Ÿ‡ช๐Ÿ‡ช' }, + { code: 'ET', name: 'Ethiopia', dialCode: '+251', flag: '๐Ÿ‡ช๐Ÿ‡น' }, + { code: 'FK', name: 'Falkland Islands', dialCode: '+500', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ' }, + { code: 'FO', name: 'Faroe Islands', dialCode: '+298', flag: '๐Ÿ‡ซ๐Ÿ‡ด' }, + { code: 'FJ', name: 'Fiji', dialCode: '+679', flag: '๐Ÿ‡ซ๐Ÿ‡ฏ' }, + { code: 'FI', name: 'Finland', dialCode: '+358', flag: '๐Ÿ‡ซ๐Ÿ‡ฎ' }, + { code: 'FR', name: 'France', dialCode: '+33', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, + { code: 'GF', name: 'French Guiana', dialCode: '+594', flag: '๐Ÿ‡ฌ๐Ÿ‡ซ' }, + { code: 'PF', name: 'French Polynesia', dialCode: '+689', flag: '๐Ÿ‡ต๐Ÿ‡ซ' }, + { code: 'GA', name: 'Gabon', dialCode: '+241', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ' }, + { code: 'GM', name: 'Gambia', dialCode: '+220', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ' }, + { code: 'GE', name: 'Georgia', dialCode: '+995', flag: '๐Ÿ‡ฌ๐Ÿ‡ช' }, + { code: 'DE', name: 'Germany', dialCode: '+49', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, + { code: 'GH', name: 'Ghana', dialCode: '+233', flag: '๐Ÿ‡ฌ๐Ÿ‡ญ' }, + { code: 'GI', name: 'Gibraltar', dialCode: '+350', flag: '๐Ÿ‡ฌ๐Ÿ‡ฎ' }, + { code: 'GR', name: 'Greece', dialCode: '+30', flag: '๐Ÿ‡ฌ๐Ÿ‡ท' }, + { code: 'GL', name: 'Greenland', dialCode: '+299', flag: '๐Ÿ‡ฌ๐Ÿ‡ฑ' }, + { code: 'GD', name: 'Grenada', dialCode: '+1473', flag: '๐Ÿ‡ฌ๐Ÿ‡ฉ' }, + { code: 'GP', name: 'Guadeloupe', dialCode: '+590', flag: '๐Ÿ‡ฌ๐Ÿ‡ต' }, + { code: 'GU', name: 'Guam', dialCode: '+1671', flag: '๐Ÿ‡ฌ๐Ÿ‡บ' }, + { code: 'GT', name: 'Guatemala', dialCode: '+502', flag: '๐Ÿ‡ฌ๐Ÿ‡น' }, + { code: 'GG', name: 'Guernsey', dialCode: '+44', flag: '๐Ÿ‡ฌ๐Ÿ‡ฌ' }, + { code: 'GN', name: 'Guinea', dialCode: '+224', flag: '๐Ÿ‡ฌ๐Ÿ‡ณ' }, + { code: 'GW', name: 'Guinea-Bissau', dialCode: '+245', flag: '๐Ÿ‡ฌ๐Ÿ‡ผ' }, + { code: 'GY', name: 'Guyana', dialCode: '+592', flag: '๐Ÿ‡ฌ๐Ÿ‡พ' }, + { code: 'HT', name: 'Haiti', dialCode: '+509', flag: '๐Ÿ‡ญ๐Ÿ‡น' }, + { code: 'VA', name: 'Holy See', dialCode: '+379', flag: '๐Ÿ‡ป๐Ÿ‡ฆ' }, + { code: 'HN', name: 'Honduras', dialCode: '+504', flag: '๐Ÿ‡ญ๐Ÿ‡ณ' }, + { code: 'HK', name: 'Hong Kong', dialCode: '+852', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ' }, + { code: 'HU', name: 'Hungary', dialCode: '+36', flag: '๐Ÿ‡ญ๐Ÿ‡บ' }, + { code: 'IS', name: 'Iceland', dialCode: '+354', flag: '๐Ÿ‡ฎ๐Ÿ‡ธ' }, + { code: 'IN', name: 'India', dialCode: '+91', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, + { code: 'ID', name: 'Indonesia', dialCode: '+62', flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ' }, + { code: 'IR', name: 'Iran', dialCode: '+98', flag: '๐Ÿ‡ฎ๐Ÿ‡ท' }, + { code: 'IQ', name: 'Iraq', dialCode: '+964', flag: '๐Ÿ‡ฎ๐Ÿ‡ถ' }, + { code: 'IE', name: 'Ireland', dialCode: '+353', flag: '๐Ÿ‡ฎ๐Ÿ‡ช' }, + { code: 'IM', name: 'Isle of Man', dialCode: '+44', flag: '๐Ÿ‡ฎ๐Ÿ‡ฒ' }, + { code: 'IL', name: 'Israel', dialCode: '+972', flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ' }, + { code: 'IT', name: 'Italy', dialCode: '+39', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, + { code: 'JM', name: 'Jamaica', dialCode: '+1876', flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ' }, + { code: 'JP', name: 'Japan', dialCode: '+81', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + { code: 'JE', name: 'Jersey', dialCode: '+44', flag: '๐Ÿ‡ฏ๐Ÿ‡ช' }, + { code: 'JO', name: 'Jordan', dialCode: '+962', flag: '๐Ÿ‡ฏ๐Ÿ‡ด' }, + { code: 'KZ', name: 'Kazakhstan', dialCode: '+7', flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ' }, + { code: 'KE', name: 'Kenya', dialCode: '+254', flag: '๐Ÿ‡ฐ๐Ÿ‡ช' }, + { code: 'KI', name: 'Kiribati', dialCode: '+686', flag: '๐Ÿ‡ฐ๐Ÿ‡ฎ' }, + { code: 'KP', name: 'North Korea', dialCode: '+850', flag: '๐Ÿ‡ฐ๐Ÿ‡ต' }, + { code: 'KR', name: 'South Korea', dialCode: '+82', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, + { code: 'KW', name: 'Kuwait', dialCode: '+965', flag: '๐Ÿ‡ฐ๐Ÿ‡ผ' }, + { code: 'KG', name: 'Kyrgyzstan', dialCode: '+996', flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ' }, + { code: 'LA', name: 'Laos', dialCode: '+856', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ' }, + { code: 'LV', name: 'Latvia', dialCode: '+371', flag: '๐Ÿ‡ฑ๐Ÿ‡ป' }, + { code: 'LB', name: 'Lebanon', dialCode: '+961', flag: '๐Ÿ‡ฑ๐Ÿ‡ง' }, + { code: 'LS', name: 'Lesotho', dialCode: '+266', flag: '๐Ÿ‡ฑ๐Ÿ‡ธ' }, + { code: 'LR', name: 'Liberia', dialCode: '+231', flag: '๐Ÿ‡ฑ๐Ÿ‡ท' }, + { code: 'LY', name: 'Libya', dialCode: '+218', flag: '๐Ÿ‡ฑ๐Ÿ‡พ' }, + { code: 'LI', name: 'Liechtenstein', dialCode: '+423', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ' }, + { code: 'LT', name: 'Lithuania', dialCode: '+370', flag: '๐Ÿ‡ฑ๐Ÿ‡น' }, + { code: 'LU', name: 'Luxembourg', dialCode: '+352', flag: '๐Ÿ‡ฑ๐Ÿ‡บ' }, + { code: 'MO', name: 'Macau', dialCode: '+853', flag: '๐Ÿ‡ฒ๐Ÿ‡ด' }, + { code: 'MK', name: 'North Macedonia', dialCode: '+389', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ' }, + { code: 'MG', name: 'Madagascar', dialCode: '+261', flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ' }, + { code: 'MW', name: 'Malawi', dialCode: '+265', flag: '๐Ÿ‡ฒ๐Ÿ‡ผ' }, + { code: 'MY', name: 'Malaysia', dialCode: '+60', flag: '๐Ÿ‡ฒ๐Ÿ‡พ' }, + { code: 'MV', name: 'Maldives', dialCode: '+960', flag: '๐Ÿ‡ฒ๐Ÿ‡ป' }, + { code: 'ML', name: 'Mali', dialCode: '+223', flag: '๐Ÿ‡ฒ๐Ÿ‡ฑ' }, + { code: 'MT', name: 'Malta', dialCode: '+356', flag: '๐Ÿ‡ฒ๐Ÿ‡น' }, + { code: 'MH', name: 'Marshall Islands', dialCode: '+692', flag: '๐Ÿ‡ฒ๐Ÿ‡ญ' }, + { code: 'MQ', name: 'Martinique', dialCode: '+596', flag: '๐Ÿ‡ฒ๐Ÿ‡ถ' }, + { code: 'MR', name: 'Mauritania', dialCode: '+222', flag: '๐Ÿ‡ฒ๐Ÿ‡ท' }, + { code: 'MU', name: 'Mauritius', dialCode: '+230', flag: '๐Ÿ‡ฒ๐Ÿ‡บ' }, + { code: 'YT', name: 'Mayotte', dialCode: '+262', flag: '๐Ÿ‡พ๐Ÿ‡น' }, + { code: 'MX', name: 'Mexico', dialCode: '+52', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ' }, + { code: 'FM', name: 'Micronesia', dialCode: '+691', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ' }, + { code: 'MD', name: 'Moldova', dialCode: '+373', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ' }, + { code: 'MC', name: 'Monaco', dialCode: '+377', flag: '๐Ÿ‡ฒ๐Ÿ‡จ' }, + { code: 'MN', name: 'Mongolia', dialCode: '+976', flag: '๐Ÿ‡ฒ๐Ÿ‡ณ' }, + { code: 'ME', name: 'Montenegro', dialCode: '+382', flag: '๐Ÿ‡ฒ๐Ÿ‡ช' }, + { code: 'MS', name: 'Montserrat', dialCode: '+1664', flag: '๐Ÿ‡ฒ๐Ÿ‡ธ' }, + { code: 'MA', name: 'Morocco', dialCode: '+212', flag: '๐Ÿ‡ฒ๐Ÿ‡ฆ' }, + { code: 'MZ', name: 'Mozambique', dialCode: '+258', flag: '๐Ÿ‡ฒ๐Ÿ‡ฟ' }, + { code: 'MM', name: 'Myanmar', dialCode: '+95', flag: '๐Ÿ‡ฒ๐Ÿ‡ฒ' }, + { code: 'NA', name: 'Namibia', dialCode: '+264', flag: '๐Ÿ‡ณ๐Ÿ‡ฆ' }, + { code: 'NR', name: 'Nauru', dialCode: '+674', flag: '๐Ÿ‡ณ๐Ÿ‡ท' }, + { code: 'NP', name: 'Nepal', dialCode: '+977', flag: '๐Ÿ‡ณ๐Ÿ‡ต' }, + { code: 'NL', name: 'Netherlands', dialCode: '+31', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ' }, + { code: 'NC', name: 'New Caledonia', dialCode: '+687', flag: '๐Ÿ‡ณ๐Ÿ‡จ' }, + { code: 'NZ', name: 'New Zealand', dialCode: '+64', flag: '๐Ÿ‡ณ๐Ÿ‡ฟ' }, + { code: 'NI', name: 'Nicaragua', dialCode: '+505', flag: '๐Ÿ‡ณ๐Ÿ‡ฎ' }, + { code: 'NE', name: 'Niger', dialCode: '+227', flag: '๐Ÿ‡ณ๐Ÿ‡ช' }, + { code: 'NG', name: 'Nigeria', dialCode: '+234', flag: '๐Ÿ‡ณ๐Ÿ‡ฌ' }, + { code: 'NU', name: 'Niue', dialCode: '+683', flag: '๐Ÿ‡ณ๐Ÿ‡บ' }, + { code: 'NF', name: 'Norfolk Island', dialCode: '+672', flag: '๐Ÿ‡ณ๐Ÿ‡ซ' }, + { code: 'MP', name: 'Northern Mariana Islands', dialCode: '+1670', flag: '๐Ÿ‡ฒ๐Ÿ‡ต' }, + { code: 'NO', name: 'Norway', dialCode: '+47', flag: '๐Ÿ‡ณ๐Ÿ‡ด' }, + { code: 'OM', name: 'Oman', dialCode: '+968', flag: '๐Ÿ‡ด๐Ÿ‡ฒ' }, + { code: 'PK', name: 'Pakistan', dialCode: '+92', flag: '๐Ÿ‡ต๐Ÿ‡ฐ' }, + { code: 'PW', name: 'Palau', dialCode: '+680', flag: '๐Ÿ‡ต๐Ÿ‡ผ' }, + { code: 'PS', name: 'Palestine', dialCode: '+970', flag: '๐Ÿ‡ต๐Ÿ‡ธ' }, + { code: 'PA', name: 'Panama', dialCode: '+507', flag: '๐Ÿ‡ต๐Ÿ‡ฆ' }, + { code: 'PG', name: 'Papua New Guinea', dialCode: '+675', flag: '๐Ÿ‡ต๐Ÿ‡ฌ' }, + { code: 'PY', name: 'Paraguay', dialCode: '+595', flag: '๐Ÿ‡ต๐Ÿ‡พ' }, + { code: 'PE', name: 'Peru', dialCode: '+51', flag: '๐Ÿ‡ต๐Ÿ‡ช' }, + { code: 'PH', name: 'Philippines', dialCode: '+63', flag: '๐Ÿ‡ต๐Ÿ‡ญ' }, + { code: 'PN', name: 'Pitcairn Islands', dialCode: '+64', flag: '๐Ÿ‡ต๐Ÿ‡ณ' }, + { code: 'PL', name: 'Poland', dialCode: '+48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ' }, + { code: 'PT', name: 'Portugal', dialCode: '+351', flag: '๐Ÿ‡ต๐Ÿ‡น' }, + { code: 'PR', name: 'Puerto Rico', dialCode: '+1787', flag: '๐Ÿ‡ต๐Ÿ‡ท' }, + { code: 'QA', name: 'Qatar', dialCode: '+974', flag: '๐Ÿ‡ถ๐Ÿ‡ฆ' }, + { code: 'RE', name: 'Rรฉunion', dialCode: '+262', flag: '๐Ÿ‡ท๐Ÿ‡ช' }, + { code: 'RO', name: 'Romania', dialCode: '+40', flag: '๐Ÿ‡ท๐Ÿ‡ด' }, + { code: 'RU', name: 'Russia', dialCode: '+7', flag: '๐Ÿ‡ท๐Ÿ‡บ' }, + { code: 'RW', name: 'Rwanda', dialCode: '+250', flag: '๐Ÿ‡ท๐Ÿ‡ผ' }, + { code: 'BL', name: 'Saint Barthรฉlemy', dialCode: '+590', flag: '๐Ÿ‡ง๐Ÿ‡ฑ' }, + { code: 'SH', name: 'Saint Helena', dialCode: '+290', flag: '๐Ÿ‡ธ๐Ÿ‡ญ' }, + { code: 'KN', name: 'Saint Kitts and Nevis', dialCode: '+1869', flag: '๐Ÿ‡ฐ๐Ÿ‡ณ' }, + { code: 'LC', name: 'Saint Lucia', dialCode: '+1758', flag: '๐Ÿ‡ฑ๐Ÿ‡จ' }, + { code: 'MF', name: 'Saint Martin', dialCode: '+590', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ' }, + { code: 'PM', name: 'Saint Pierre and Miquelon', dialCode: '+508', flag: '๐Ÿ‡ต๐Ÿ‡ฒ' }, + { code: 'VC', name: 'Saint Vincent and the Grenadines', dialCode: '+1784', flag: '๐Ÿ‡ป๐Ÿ‡จ' }, + { code: 'WS', name: 'Samoa', dialCode: '+685', flag: '๐Ÿ‡ผ๐Ÿ‡ธ' }, + { code: 'SM', name: 'San Marino', dialCode: '+378', flag: '๐Ÿ‡ธ๐Ÿ‡ฒ' }, + { code: 'ST', name: 'Sรฃo Tomรฉ and Prรญncipe', dialCode: '+239', flag: '๐Ÿ‡ธ๐Ÿ‡น' }, + { code: 'SA', name: 'Saudi Arabia', dialCode: '+966', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ' }, + { code: 'SN', name: 'Senegal', dialCode: '+221', flag: '๐Ÿ‡ธ๐Ÿ‡ณ' }, + { code: 'RS', name: 'Serbia', dialCode: '+381', flag: '๐Ÿ‡ท๐Ÿ‡ธ' }, + { code: 'SC', name: 'Seychelles', dialCode: '+248', flag: '๐Ÿ‡ธ๐Ÿ‡จ' }, + { code: 'SL', name: 'Sierra Leone', dialCode: '+232', flag: '๐Ÿ‡ธ๐Ÿ‡ฑ' }, + { code: 'SG', name: 'Singapore', dialCode: '+65', flag: '๐Ÿ‡ธ๐Ÿ‡ฌ' }, + { code: 'SX', name: 'Sint Maarten', dialCode: '+1721', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ' }, + { code: 'SK', name: 'Slovakia', dialCode: '+421', flag: '๐Ÿ‡ธ๐Ÿ‡ฐ' }, + { code: 'SI', name: 'Slovenia', dialCode: '+386', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ' }, + { code: 'SB', name: 'Solomon Islands', dialCode: '+677', flag: '๐Ÿ‡ธ๐Ÿ‡ง' }, + { code: 'SO', name: 'Somalia', dialCode: '+252', flag: '๐Ÿ‡ธ๐Ÿ‡ด' }, + { code: 'ZA', name: 'South Africa', dialCode: '+27', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ' }, + { code: 'GS', name: 'South Georgia and the South Sandwich Islands', dialCode: '+500', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ' }, + { code: 'SS', name: 'South Sudan', dialCode: '+211', flag: '๐Ÿ‡ธ๐Ÿ‡ธ' }, + { code: 'ES', name: 'Spain', dialCode: '+34', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, + { code: 'LK', name: 'Sri Lanka', dialCode: '+94', flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ' }, + { code: 'SD', name: 'Sudan', dialCode: '+249', flag: '๐Ÿ‡ธ๐Ÿ‡ฉ' }, + { code: 'SR', name: 'Suriname', dialCode: '+597', flag: '๐Ÿ‡ธ๐Ÿ‡ท' }, + { code: 'SJ', name: 'Svalbard and Jan Mayen', dialCode: '+47', flag: '๐Ÿ‡ธ๐Ÿ‡ฏ' }, + { code: 'SZ', name: 'Eswatini', dialCode: '+268', flag: '๐Ÿ‡ธ๐Ÿ‡ฟ' }, + { code: 'SE', name: 'Sweden', dialCode: '+46', flag: '๐Ÿ‡ธ๐Ÿ‡ช' }, + { code: 'CH', name: 'Switzerland', dialCode: '+41', flag: '๐Ÿ‡จ๐Ÿ‡ญ' }, + { code: 'SY', name: 'Syria', dialCode: '+963', flag: '๐Ÿ‡ธ๐Ÿ‡พ' }, + { code: 'TW', name: 'Taiwan', dialCode: '+886', flag: '๐Ÿ‡น๐Ÿ‡ผ' }, + { code: 'TJ', name: 'Tajikistan', dialCode: '+992', flag: '๐Ÿ‡น๐Ÿ‡ฏ' }, + { code: 'TZ', name: 'Tanzania', dialCode: '+255', flag: '๐Ÿ‡น๐Ÿ‡ฟ' }, + { code: 'TH', name: 'Thailand', dialCode: '+66', flag: '๐Ÿ‡น๐Ÿ‡ญ' }, + { code: 'TL', name: 'Timor-Leste', dialCode: '+670', flag: '๐Ÿ‡น๐Ÿ‡ฑ' }, + { code: 'TG', name: 'Togo', dialCode: '+228', flag: '๐Ÿ‡น๐Ÿ‡ฌ' }, + { code: 'TK', name: 'Tokelau', dialCode: '+690', flag: '๐Ÿ‡น๐Ÿ‡ฐ' }, + { code: 'TO', name: 'Tonga', dialCode: '+676', flag: '๐Ÿ‡น๐Ÿ‡ด' }, + { code: 'TT', name: 'Trinidad and Tobago', dialCode: '+1868', flag: '๐Ÿ‡น๐Ÿ‡น' }, + { code: 'TN', name: 'Tunisia', dialCode: '+216', flag: '๐Ÿ‡น๐Ÿ‡ณ' }, + { code: 'TR', name: 'Turkey', dialCode: '+90', flag: '๐Ÿ‡น๐Ÿ‡ท' }, + { code: 'TM', name: 'Turkmenistan', dialCode: '+993', flag: '๐Ÿ‡น๐Ÿ‡ฒ' }, + { code: 'TC', name: 'Turks and Caicos Islands', dialCode: '+1649', flag: '๐Ÿ‡น๐Ÿ‡จ' }, + { code: 'TV', name: 'Tuvalu', dialCode: '+688', flag: '๐Ÿ‡น๐Ÿ‡ป' }, + { code: 'UG', name: 'Uganda', dialCode: '+256', flag: '๐Ÿ‡บ๐Ÿ‡ฌ' }, + { code: 'UA', name: 'Ukraine', dialCode: '+380', flag: '๐Ÿ‡บ๐Ÿ‡ฆ' }, + { code: 'AE', name: 'United Arab Emirates', dialCode: '+971', flag: '๐Ÿ‡ฆ๐Ÿ‡ช' }, + { code: 'GB', name: 'United Kingdom', dialCode: '+44', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, + { code: 'US', name: 'United States', dialCode: '+1', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + { code: 'UM', name: 'United States Minor Outlying Islands', dialCode: '+1', flag: '๐Ÿ‡บ๐Ÿ‡ฒ' }, + { code: 'UY', name: 'Uruguay', dialCode: '+598', flag: '๐Ÿ‡บ๐Ÿ‡พ' }, + { code: 'UZ', name: 'Uzbekistan', dialCode: '+998', flag: '๐Ÿ‡บ๐Ÿ‡ฟ' }, + { code: 'VU', name: 'Vanuatu', dialCode: '+678', flag: '๐Ÿ‡ป๐Ÿ‡บ' }, + { code: 'VE', name: 'Venezuela', dialCode: '+58', flag: '๐Ÿ‡ป๐Ÿ‡ช' }, + { code: 'VN', name: 'Vietnam', dialCode: '+84', flag: '๐Ÿ‡ป๐Ÿ‡ณ' }, + { code: 'VG', name: 'British Virgin Islands', dialCode: '+1284', flag: '๐Ÿ‡ป๐Ÿ‡ฌ' }, + { code: 'VI', name: 'U.S. Virgin Islands', dialCode: '+1340', flag: '๐Ÿ‡ป๐Ÿ‡ฎ' }, + { code: 'WF', name: 'Wallis and Futuna', dialCode: '+681', flag: '๐Ÿ‡ผ๐Ÿ‡ซ' }, + { code: 'EH', name: 'Western Sahara', dialCode: '+212', flag: '๐Ÿ‡ช๐Ÿ‡ญ' }, + { code: 'YE', name: 'Yemen', dialCode: '+967', flag: '๐Ÿ‡พ๐Ÿ‡ช' }, + { code: 'ZM', name: 'Zambia', dialCode: '+260', flag: '๐Ÿ‡ฟ๐Ÿ‡ฒ' }, + { code: 'ZW', name: 'Zimbabwe', dialCode: '+263', flag: '๐Ÿ‡ฟ๐Ÿ‡ผ' }, + ]; + + ngOnInit(): void { + super.ngOnInit(); + this.configureFromWidgetParams(); + this.initializePhoneNumber(); + } + + configureFromWidgetParams(): void { + if (this.widgetStructure?.widget_params) { + const params = this.widgetStructure.widget_params; + + if (params.preferred_countries && Array.isArray(params.preferred_countries)) { + this.preferredCountries = params.preferred_countries; + } + + if (typeof params.enable_placeholder === 'boolean') { + this.enablePlaceholder = params.enable_placeholder; + } + + if (typeof params.phone_validation === 'boolean') { + this.phoneValidation = params.phone_validation; + } + } + } + + _filterCountries(value: string): CountryCode[] { + const filterValue = value.toLowerCase(); + return this.sortedCountries.filter( + (country) => + country.name.toLowerCase().includes(filterValue) || + country.code.toLowerCase().includes(filterValue) || + country.dialCode.includes(filterValue), + ); + } + + displayCountryFn(country: CountryCode): string { + return country ? `${country.flag} ${country.name} ${country.dialCode}` : ''; + } + + onCountrySelected(country: CountryCode): void { + this.selectedCountry = country; + this.initializeFormatter(); + this.formatAndUpdatePhoneNumber(); + } + + initializeFormatter(): void { + if (this.selectedCountry) { + this.formatter = new AsYouType(this.selectedCountry.code as LibPhoneCountryCode); + } + } + + onCountryChange(): void { + this.initializeFormatter(); + this.formatAndUpdatePhoneNumber(); + } + + onPhoneNumberChange(): void { + if (this.displayPhoneNumber.startsWith('+')) { + this.detectCountryFromInput(); + } else { + this.formatAndUpdatePhoneNumber(); + } + } + + get sortedCountries(): CountryCode[] { + const preferred = this.countries.filter((c) => this.preferredCountries.includes(c.code)); + const others = this.countries.filter((c) => !this.preferredCountries.includes(c.code)); + return [...preferred, ...others]; + } + + get placeholder(): string { + if (!this.enablePlaceholder) return ''; + return this.selectedCountry ? `Phone number for ${this.selectedCountry.name}` : 'Phone number'; + } + + getPhoneNumberPlaceholder(): string { + if (!this.enablePlaceholder) return ''; + if (this.selectedCountry) { + return `Local number or ${this.selectedCountry.dialCode}1234567890`; + } + return 'Enter +1234567890 or select country'; + } + + getExamplePhoneNumber(): string { + if (!this.selectedCountry) return ''; + + const exampleNumbers: { [key: string]: string } = { + US: '(202) 456-1111', + GB: '020 7946 0958', + CA: '(416) 555-1234', + AU: '(02) 1234 5678', + DE: '030 12345678', + FR: '01 23 45 67 89', + IT: '06 1234 5678', + ES: '91 123 45 67', + NL: '020 123 4567', + BE: '02 123 45 67', + CH: '044 123 45 67', + AT: '01 12345678', + SE: '08-123 456 78', + NO: '22 12 34 56', + DK: '32 12 34 56', + FI: '09 1234 5678', + PL: '12 123 45 67', + CZ: '224 123 456', + HU: '(06 1) 123 4567', + SK: '2 1234 5678', + SI: '1 123 45 67', + HR: '1 123 4567', + RO: '021 123 4567', + BG: '02 123 4567', + GR: '21 1234 5678', + PT: '21 123 4567', + IE: '01 123 4567', + LU: '621 123 456', + MT: '2123 4567', + CY: '22 123456', + EE: '372 1234', + LV: '2123 4567', + LT: '8 612 34567', + RU: '8 (495) 123-45-67', + UA: '044 123 4567', + BY: '8 017 123-45-67', + MD: '22 123456', + JP: '03-1234-5678', + KR: '02-123-4567', + CN: '010 1234 5678', + HK: '2123 4567', + TW: '02 1234 5678', + SG: '6123 4567', + MY: '03-1234 5678', + TH: '02 123 4567', + PH: '02 1234 5678', + ID: '021 1234 5678', + VN: '28 1234 5678', + IN: '011 1234 5678', + PK: '21 1234 5678', + BD: '2 1234 5678', + LK: '11 234 5678', + NP: '1 123 4567', + AF: '20 123 4567', + IR: '021 1234 5678', + IQ: '1 123 4567', + SA: '011 123 4567', + AE: '4 123 4567', + QA: '4412 3456', + KW: '2221 2345', + BH: '1712 3456', + OM: '2412 3456', + JO: '6 123 4567', + LB: '1 123 456', + SY: '11 123 4567', + IL: '2-123-4567', + PS: '59 123 4567', + TR: '(0212) 123 45 67', + GE: '32 123 45 67', + AM: '10 123456', + AZ: '12 123 45 67', + KZ: '8 (7172) 12 34 56', + KG: '312 123456', + TJ: '372 123456', + UZ: '71 123 45 67', + TM: '12 123456', + MN: '11 123456', + ZA: '011 123 4567', + EG: '02 12345678', + MA: '522 123456', + TN: '71 123 456', + DZ: '21 12 34 56', + LY: '21 123 4567', + SD: '15 123 4567', + ET: '11 123 4567', + KE: '20 123 4567', + UG: '41 123 4567', + TZ: '22 123 4567', + RW: '78 123 4567', + BI: '22 12 34 56', + DJ: '77 12 34 56', + SO: '1 123456', + ER: '1 123 456', + SS: '95 123 4567', + CF: '70 12 34 56', + TD: '22 12 34 56', + CM: '6 71 23 45 67', + GQ: '222 123456', + GA: '06 12 34 56', + CG: '06 612 3456', + CD: '12 123 4567', + AO: '222 123456', + ZM: '21 123 4567', + ZW: '4 123456', + BW: '71 123 456', + NA: '61 123 4567', + SZ: '2505 1234', + LS: '2212 3456', + MZ: '21 123456', + MW: '1 123 456', + MG: '20 12 345 67', + MU: '212 3456', + SC: '4 123 456', + KM: '773 1234', + YT: '269 61 23 45', + RE: '262 12 34 56', + MV: '330 1234', + BR: '(11) 1234-5678', + AR: '011 1234-5678', + CL: '2 1234 5678', + CO: '(601) 234 5678', + PE: '1 123 4567', + VE: '0212-1234567', + EC: '2 123 4567', + BO: '2 123 4567', + PY: '21 123 456', + UY: '2 123 4567', + GY: '222 1234', + SR: '421234', + GF: '594 12 34 56', + FK: '41234', + MX: '55 1234 5678', + GT: '2 123 4567', + BZ: '223 1234', + SV: '2123 4567', + HN: '2 123 4567', + NI: '2 123 4567', + CR: '2 123 4567', + PA: '123 4567', + }; + + return exampleNumbers[this.selectedCountry.code] || `${this.selectedCountry.dialCode} 123 4567`; + } + + isValidPhoneNumber(): boolean { + if (!this.phoneValidation) return true; + if (!this.displayPhoneNumber) return true; + + try { + let phoneNumber; + + if (this.displayPhoneNumber.startsWith('+')) { + phoneNumber = parsePhoneNumber(this.displayPhoneNumber); + } else if (this.selectedCountry) { + phoneNumber = parsePhoneNumber(this.displayPhoneNumber, this.selectedCountry.code as LibPhoneCountryCode); + } else { + return false; + } + + return phoneNumber ? phoneNumber.isValid() : false; + } catch (_error) { + return false; + } + } + + private initializePhoneNumber(): void { + if (this.value) { + this.parseExistingPhoneNumber(this.value); + } else { + this.setDefaultCountry(); + this.displayPhoneNumber = ''; + } + } + + private parseExistingPhoneNumber(fullNumber: string): void { + let phoneNumber; + + try { + phoneNumber = parsePhoneNumber(fullNumber); + } catch (_error) { + // Will try with default country below + } + + if (!phoneNumber || !phoneNumber.country) { + try { + const defaultCountryCode = this.preferredCountries[0] || 'US'; + phoneNumber = parsePhoneNumber(fullNumber, defaultCountryCode as LibPhoneCountryCode); + } catch (error) { + console.warn('Failed to parse with default country as well:', error); + } + } + + if (phoneNumber?.country) { + const country = this.countries.find((c) => c.code === phoneNumber.country); + if (country) { + this.selectedCountry = country; + this.countryControl.setValue(country); + this.phoneNumber = phoneNumber.nationalNumber; + this.displayPhoneNumber = phoneNumber.formatNational(); + this.initializeFormatter(); + return; + } else { + console.warn('Country not found in list:', phoneNumber.country); + } + } + + this.setDefaultCountry(); + this.phoneNumber = fullNumber.replace(/\D/g, ''); + + if (this.formatter && this.phoneNumber) { + this.formatter.reset(); + this.displayPhoneNumber = this.formatter.input(this.phoneNumber); + } else { + this.displayPhoneNumber = fullNumber; + } + } + + private setDefaultCountry(): void { + this.selectedCountry = this.countries.find((c) => c.code === this.preferredCountries[0]) || this.countries[0]; + this.countryControl.setValue(this.selectedCountry); + this.initializeFormatter(); + } + + private formatAndUpdatePhoneNumber(): void { + if (!this.displayPhoneNumber) { + this.phoneNumber = ''; + this.value = ''; + this.onFieldChange.emit(this.value); + return; + } + + if (this.formatter && !this.displayPhoneNumber.startsWith('+')) { + this.formatter.reset(); + const formatted = this.formatter.input(this.displayPhoneNumber); + this.displayPhoneNumber = formatted; + + this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); + } else { + this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); + } + + this.updateFullPhoneNumber(); + } + + private detectCountryFromInput(): void { + if (!this.displayPhoneNumber.startsWith('+')) { + return; + } + + try { + const phoneNumber = parsePhoneNumber(this.displayPhoneNumber); + if (phoneNumber?.country) { + const detectedCountry = this.countries.find((c) => c.code === phoneNumber.country); + if (detectedCountry) { + this.selectedCountry = detectedCountry; + this.countryControl.setValue(detectedCountry); + this.phoneNumber = phoneNumber.nationalNumber; + this.displayPhoneNumber = phoneNumber.formatNational(); + this.initializeFormatter(); + this.updateFullPhoneNumber(); + return; + } + } + } catch (error) { + console.warn('Could not detect country from input:', this.displayPhoneNumber, error); + } + + this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); + this.updateFullPhoneNumber(); + } + + private updateFullPhoneNumber(): void { + if (!this.displayPhoneNumber && !this.phoneNumber) { + this.value = ''; + this.onFieldChange.emit(this.value); + return; + } + + try { + let phoneNumber; + + if (this.displayPhoneNumber.startsWith('+')) { + phoneNumber = parsePhoneNumber(this.displayPhoneNumber); + } else if (this.selectedCountry && this.displayPhoneNumber) { + phoneNumber = parsePhoneNumber(this.displayPhoneNumber, this.selectedCountry.code as LibPhoneCountryCode); + } + + if (phoneNumber?.isValid()) { + this.value = phoneNumber.number; + } else { + this.value = this.displayPhoneNumber.replace(/\s/g, ''); + } + } catch (error) { + console.warn('Error formatting phone number:', error); + this.value = this.displayPhoneNumber.replace(/\s/g, ''); + } + + this.onFieldChange.emit(this.value); + } +}