diff --git a/projects/ngx-formentry/src/components/ngx-remote-select/ngx-remote-select.component.ts b/projects/ngx-formentry/src/components/ngx-remote-select/ngx-remote-select.component.ts index 108d8b57..a28efc41 100644 --- a/projects/ngx-formentry/src/components/ngx-remote-select/ngx-remote-select.component.ts +++ b/projects/ngx-formentry/src/components/ngx-remote-select/ngx-remote-select.component.ts @@ -5,13 +5,14 @@ import { forwardRef, Output, EventEmitter, - Renderer2 + Renderer2, OnDestroy } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { concat, Observable, of, Subject } from 'rxjs'; import { catchError, distinctUntilChanged, + finalize, switchMap, tap } from 'rxjs/operators'; @@ -32,7 +33,7 @@ import { TranslateService } from '@ngx-translate/core'; } ] }) -export class RemoteSelectComponent implements OnInit, ControlValueAccessor { +export class RemoteSelectComponent implements OnInit, ControlValueAccessor, OnDestroy { // @Input() dataSource: DataSource; remoteOptions$: Observable; remoteOptionsLoading = false; @@ -138,19 +139,37 @@ export class RemoteSelectComponent implements OnInit, ControlValueAccessor { private loadOptions() { this.remoteOptions$ = concat( - of([]), // default items + this.dataSource + .searchOptions('', this.dataSource?.dataSourceOptions ?? {}) + ?.pipe( + catchError((error) => { + console.error('Error loading initial options:', error); + return of([]); + }) + ) ?? of([]), // default items this.remoteOptionInput$.pipe( distinctUntilChanged(), tap(() => { this.loading = true; }), switchMap((term) => - this.dataSource.searchOptions(term).pipe( - catchError(() => of([])), // empty list on error - tap(() => (this.loading = false)) - ) + this.dataSource + .searchOptions(term, this.dataSource?.dataSourceOptions ?? {}) + .pipe( + catchError((error) => { + console.error('Error loading options:', error); + return of([]); + }), + finalize(() => { + this.loading = false; + }) + ) ) ) ); } + + ngOnDestroy() { + this.remoteOptionInput$.complete(); + } } diff --git a/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts b/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts index 9b5ec941..12672fe9 100644 --- a/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts +++ b/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts @@ -33,6 +33,7 @@ import { MinLengthValidationModel } from '../question-models/min-length-validati import { WorkspaceLauncherQuestion } from '../question-models'; import { DecimalValidationModel } from '../question-models/decimal-validation.model'; import { DisallowDecimalsValidationModel } from '../question-models/disallow-decimals-validation.model'; +import { RemoteSelectQuestion } from '../question-models/remote-select-question'; @Injectable() export class QuestionFactory { dataSources: any = {}; @@ -745,6 +746,52 @@ export class QuestionFactory { return question; } + toRemoteSelectQuestion(schemaQuestion: any): RemoteSelectQuestion { + const dataSource = this.getDataSourceConfig(schemaQuestion); + const question = new RemoteSelectQuestion({ + dataSource: dataSource.name, + dataSourceOptions: dataSource.options, + type: '', + key: '' + }); + question.questionIndex = this.quetionIndex; + question.label = schemaQuestion.label; + question.prefix = schemaQuestion.prefix; + question.key = schemaQuestion.id; + question.renderingType = 'remote-select'; + question.validators = this.addValidators(schemaQuestion); + question.extras = schemaQuestion; + + const mappings: Record = { + label: 'label', + required: 'required', + id: 'key' + }; + + question.componentConfigs = schemaQuestion.componentConfigs || []; + this.copyProperties(mappings, schemaQuestion, question); + this.addDisableOrHideProperty(schemaQuestion, question); + this.addAlertProperty(schemaQuestion, question); + this.addHistoricalExpressions(schemaQuestion, question); + this.addCalculatorProperty(schemaQuestion, question); + + return question; + } + + private getDataSourceConfig( + schemaQuestion: any + ): { name: string; options: Record } { + const dataSourceName = schemaQuestion.questionOptions?.dataSource; + const dataSourceOptions = schemaQuestion.questionOptions?.dataSourceOptions; + // See https://github.com/openmrs/openmrs-contrib-json-schemas/blob/main/form.schema.json + const openmrs3DataSource = schemaQuestion.questionOptions?.datasource; + + return { + name: dataSourceName ?? openmrs3DataSource?.name ?? '', + options: dataSourceOptions ?? openmrs3DataSource?.config ?? {} + }; + } + toDecimalQuestion(schemaQuestion: any): TextInputQuestion { const question = new TextInputQuestion({ placeholder: '', @@ -938,6 +985,8 @@ export class QuestionFactory { return this.toFileUploadQuestion(schema); case 'workspace-launcher': return this.toWorkspaceLauncher(schema); + case 'remote-select': + return this.toRemoteSelectQuestion(schema); default: console.warn('New Schema Question Type found.........' + renderType); return this.toTextQuestion(schema); diff --git a/projects/ngx-formentry/src/form-entry/question-models/interfaces/data-source.ts b/projects/ngx-formentry/src/form-entry/question-models/interfaces/data-source.ts index e7db19ab..df5e9b14 100644 --- a/projects/ngx-formentry/src/form-entry/question-models/interfaces/data-source.ts +++ b/projects/ngx-formentry/src/form-entry/question-models/interfaces/data-source.ts @@ -2,10 +2,13 @@ import { SelectOption } from './select-option'; import { Observable } from 'rxjs'; export interface DataSource { - dataSourceOptions?: any; + dataSourceOptions?: Record; dataFromSourceChanged?: Observable; resolveSelectedValue(value): Observable; - searchOptions(searchText): Observable; + searchOptions( + searchText, + dataSourceOptions?: Record + ): Observable; fileUpload(data): Observable; fetchFile(url: string, fileType?: string): Observable; } diff --git a/projects/ngx-formentry/src/form-entry/question-models/interfaces/remote-select-question-options.ts b/projects/ngx-formentry/src/form-entry/question-models/interfaces/remote-select-question-options.ts new file mode 100644 index 00000000..bc9e7286 --- /dev/null +++ b/projects/ngx-formentry/src/form-entry/question-models/interfaces/remote-select-question-options.ts @@ -0,0 +1,6 @@ +import { BaseOptions } from '../interfaces/base-options'; + +export interface RemoteSelectQuestionOptions extends BaseOptions { + dataSource: string; + dataSourceOptions?: Record; +} diff --git a/projects/ngx-formentry/src/form-entry/question-models/question-base.ts b/projects/ngx-formentry/src/form-entry/question-models/question-base.ts index 927fa72c..59dfac9d 100755 --- a/projects/ngx-formentry/src/form-entry/question-models/question-base.ts +++ b/projects/ngx-formentry/src/form-entry/question-models/question-base.ts @@ -30,7 +30,7 @@ export class QuestionBase implements BaseOptions { historicalDataValue?: any; extras?: any; dataSource?: string; - dataSourceOptions?: any; + dataSourceOptions?: Record; controlType?: AfeControlType; validators?: Array; diff --git a/projects/ngx-formentry/src/form-entry/question-models/remote-select-question.ts b/projects/ngx-formentry/src/form-entry/question-models/remote-select-question.ts new file mode 100644 index 00000000..50bb230e --- /dev/null +++ b/projects/ngx-formentry/src/form-entry/question-models/remote-select-question.ts @@ -0,0 +1,16 @@ +import { QuestionBase } from './question-base'; +import { AfeControlType } from '../../abstract-controls-extension/afe-control-type'; +import { RemoteSelectQuestionOptions } from './interfaces/remote-select-question-options'; + +export class RemoteSelectQuestion extends QuestionBase { + rendering: string; + options: any[]; + + constructor(options: RemoteSelectQuestionOptions) { + super(options); + this.renderingType = 'select'; + this.controlType = AfeControlType.AfeFormControl; + this.dataSource = options.dataSource || ''; + this.dataSourceOptions = options.dataSourceOptions || {}; + } +} diff --git a/src/app/adult-1.6.json b/src/app/adult-1.6.json index f52d697a..93fb690a 100644 --- a/src/app/adult-1.6.json +++ b/src/app/adult-1.6.json @@ -318,6 +318,23 @@ "rendering": "ui-select-extended" } }, + { + "id": "admitToLocation", + "type": "obs", + "required": true, + "label": "Admit to location", + "questionOptions": { + "rendering": "remote-select", + "required": true, + "concept": "CIEL:169403", + "datasource": { + "name": "location_datasource", + "config": { + "tag": "Admission Location" + } + } + } + }, { "type": "encounterLocation", "label": "Facility name (site/satellite clinic required):", @@ -3362,7 +3379,9 @@ "type": "obs", "hide": { "field": "tb_current", - "value": ["b8aa06ca-93c6-40ea-b144-c74f841926f4"] + "value": [ + "b8aa06ca-93c6-40ea-b144-c74f841926f4" + ] }, "id": "__ptxCzFD2s" } @@ -6991,7 +7010,9 @@ "type": "obs", "hide": { "field": "q26f", - "value": ["b8aa06ca-93c6-40ea-b144-c74f841926f4"] + "value": [ + "b8aa06ca-93c6-40ea-b144-c74f841926f4" + ] }, "id": "__Jywyp94Lw" } @@ -8012,7 +8033,16 @@ "questionOptions": { "concept": "318a5e8b-218c-4f66-9106-cd581dec1f95", "rendering": "date", - "weeksList": [2, 4, 6, 8, 12, 16, 24, 36] + "weeksList": [ + 2, + 4, + 6, + 8, + 12, + 16, + 24, + 36 + ] }, "validators": [ { @@ -8042,7 +8072,17 @@ "questionOptions": { "concept": "a8a666ba-1350-11df-a1f1-0026b9348838", "rendering": "date", - "weeksList": [2, 4, 6, 8, 12, 16, 20, 24, 36] + "weeksList": [ + 2, + 4, + 6, + 8, + 12, + 16, + 20, + 24, + 36 + ] }, "validators": [ { @@ -8178,4 +8218,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 215e7ac9..51e7914d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -72,6 +72,10 @@ export class AppComponent implements OnInit { searchOptions: this.sampleSearch, resolveSelectedValue: this.sampleResolve }); + this.dataSources.registerDataSource('location_datasource', { + searchOptions: this.sampleLocationSearch, + resolveSelectedValue: this.sampleResolve + }); this.dataSources.registerDataSource('provider', { searchOptions: this.sampleSearch, resolveSelectedValue: this.sampleResolve @@ -350,7 +354,42 @@ export class AppComponent implements OnInit { }); } - public sampleSearch(): Observable { + public sampleLocationSearch( + searchText: string + ): Observable>> { + const locations = [ + { + value: 'ba685651-ed3b-4e63-9b35-78893060758a', + label: 'Inpatient Ward' + }, + { + value: '184ac7d9-225a-41f8-bac7-c87b1327e1b0', + label: 'Ward 1' + }, + { + value: '5a7f3c53-6bb4-448b-a966-5e65b397b9f3', + label: 'Ward 2' + }, + { + value: '2272b8cd-b690-4878-a50c-40d22235b3f3', + label: 'Ward 3' + }, + { + value: 'db0253bb-8e2e-4b2c-b60c-6c88110e3c2e', + label: 'Duplix' + } + ]; + if (!searchText) { + return of(locations); + } + return of( + locations.filter((location) => + location.label.toLowerCase().includes(searchText.toLowerCase()) + ) + ); + } + + public sampleSearch(searchText: string): Observable { const items: Array = [ { value: '0', label: 'Aech' }, { value: '5b6e58ea-1359-11df-a1f1-0026b9348838', label: 'Art3mis' }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 74e69e30..46c2538b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,8 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -7,11 +10,17 @@ import { FormEntryModule } from '@openmrs/ngx-formentry'; import { AppComponent } from './app.component'; import { NgxTranslateModule } from './translate/translate.module'; -@NgModule({ declarations: [AppComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - bootstrap: [AppComponent], imports: [BrowserModule, - BrowserAnimationsModule, - FormEntryModule, - ReactiveFormsModule, - NgxTranslateModule], providers: [provideHttpClient(withInterceptorsFromDi())] }) +@NgModule({ + declarations: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + bootstrap: [AppComponent], + imports: [ + BrowserModule, + BrowserAnimationsModule, + FormEntryModule, + ReactiveFormsModule, + NgxTranslateModule + ], + providers: [provideHttpClient(withInterceptorsFromDi())] +}) export class AppModule {}