diff --git a/src/components/inputs/select/dropdown.test.tsx b/src/components/inputs/select/dropdown.test.tsx index de49702e2..3f35e6a41 100644 --- a/src/components/inputs/select/dropdown.test.tsx +++ b/src/components/inputs/select/dropdown.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent, screen, cleanup, act } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import { Formik } from 'formik'; import { type EncounterContext, FormContext } from '../../../form-context'; import Dropdown from './dropdown.component'; diff --git a/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx b/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx index 47f3b18d5..7cb412c3b 100644 --- a/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +++ b/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx @@ -1,27 +1,45 @@ import React from 'react'; -import { render, fireEvent, waitFor, act, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import UiSelectExtended from './ui-select-extended.component'; import { type EncounterContext, FormContext } from '../../../form-context'; import { Formik } from 'formik'; import { ObsSubmissionHandler } from '../../../submission-handlers/base-handlers'; -import { type FormField } from '../../../types'; - -const question: FormField = { - label: 'Transfer Location', - type: 'obs', - questionOptions: { - rendering: 'ui-select-extended', - concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - datasource: { - name: 'location_datasource', - config: { - tag: 'test-tag', +import { type FormField, type RenderType } from '../../../types'; + +const questions: FormField[] = [ + { + label: 'Transfer Location', + type: 'obs', + questionOptions: { + rendering: 'ui-select-extended', + concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + datasource: { + name: 'location_datasource', + config: { + tag: 'test-tag', + }, }, }, + value: null, + id: 'patient_transfer_location', }, - value: null, - id: 'patient_transfer_location', -}; + { + label: 'Select criteria for new WHO stage:', + type: 'obs', + questionOptions: { + concept: '250e87b6-beb7-44a1-93a1-d3dd74d7e372', + rendering: 'select-concept-answers', + datasource: { + name: 'select_concept_answers_datasource', + config: { + concept: '250e87b6-beb7-44a1-93a1-d3dd74d7e372', + }, + }, + }, + validators: [], + id: '__sq5ELJr7p', + }, +]; const encounterContext: EncounterContext = { patient: { @@ -54,12 +72,13 @@ const renderForm = (initialValues) => { obsGroupsToVoid: [], setObsGroupsToVoid: jest.fn(), encounterContext: encounterContext, - fields: [question], + fields: questions, isFieldInitializationComplete: true, isSubmitting: false, formFieldHandlers: { obs: ObsSubmissionHandler }, }}> - + + )} , @@ -69,20 +88,35 @@ const renderForm = (initialValues) => { // Mock the data source fetch behavior jest.mock('../../../registry/registry', () => ({ getRegisteredDataSource: jest.fn().mockResolvedValue({ - fetchData: jest.fn().mockResolvedValue([ - { - uuid: 'aaa-1', - display: 'Kololo', - }, - { - uuid: 'aaa-2', - display: 'Naguru', - }, - { - uuid: 'aaa-3', - display: 'Muyenga', - }, - ]), + fetchData: jest.fn().mockImplementation((...args) => { + if (args[1].concept) { + return Promise.resolve([ + { + uuid: 'stage-1-uuid', + display: 'stage 1', + }, + { + uuid: 'stage-2-uuid', + display: 'stage 2', + }, + ]); + } + + return Promise.resolve([ + { + uuid: 'aaa-1', + display: 'Kololo', + }, + { + uuid: 'aaa-2', + display: 'Naguru', + }, + { + uuid: 'aaa-3', + display: 'Muyenga', + }, + ]); + }), toUuidAndDisplay: (data) => data, }), })); @@ -98,7 +132,7 @@ describe('UiSelectExtended Component', () => { // assert initial values await act(async () => { - expect(question.value).toBe(null); + expect(questions[0].value).toBe(null); }); //Click on the UiSelectExtendedWidget to open the dropdown @@ -110,6 +144,19 @@ describe('UiSelectExtended Component', () => { expect(screen.getByText('Muyenga')).toBeInTheDocument(); }); + it('renders with items from the datasource of select-concept-answers rendering', async () => { + await act(async () => { + await renderForm({}); + }); + + const uiSelectExtendedWidget = screen.getByLabelText(/Select criteria for new WHO stage:/i); + fireEvent.click(uiSelectExtendedWidget); + + // Assert that all items are displayed + expect(screen.getByText('stage 1')).toBeInTheDocument(); + expect(screen.getByText('stage 2')).toBeInTheDocument(); + }); + it('Selects a value from the list', async () => { await act(async () => { await renderForm({}); @@ -127,7 +174,7 @@ describe('UiSelectExtended Component', () => { // verify await act(async () => { - expect(question.value).toEqual({ + expect(questions[0].value).toEqual({ person: '833db896-c1f0-11eb-8529-0242ac130003', obsDatetime: new Date(2023, 8, 29), concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', @@ -184,7 +231,7 @@ describe('UiSelectExtended Component', () => { await act(async () => { await renderForm({}); }); - const config = question.questionOptions.datasource.config; + const config = questions[0].questionOptions.datasource.config; // Assert that the config is set with the expected configuration value expect(config).toEqual(expectedConfigValue); diff --git a/src/datasources/concept-data-source.ts b/src/datasources/concept-data-source.ts index 5bb8bf53a..4bdaca3e4 100644 --- a/src/datasources/concept-data-source.ts +++ b/src/datasources/concept-data-source.ts @@ -20,6 +20,16 @@ export class ConceptDataSource extends BaseOpenMRSDataSource { }); } } + + if (config?.useSetMembersByConcept) { + let urlParts = apiUrl.split('?name=&searchType=fuzzy&v='); + apiUrl = `${urlParts[0]}/${config.concept}?v=custom:(uuid,setMembers:(uuid,display))`; + return openmrsFetch(searchTerm ? `${apiUrl}&q=${searchTerm}` : apiUrl).then(({ data }) => { + // return the setMembers from the retrieved concept object + return data['setMembers']; + }); + } + return openmrsFetch(searchTerm ? `${apiUrl}&q=${searchTerm}` : apiUrl).then(({ data }) => { return data.results; }); diff --git a/src/datasources/concept-set-members-data-source.ts b/src/datasources/concept-set-members-data-source.ts new file mode 100644 index 000000000..2290b3141 --- /dev/null +++ b/src/datasources/concept-set-members-data-source.ts @@ -0,0 +1,17 @@ +import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { BaseOpenMRSDataSource } from './data-source'; + +export class ConceptSetMembersDataSource extends BaseOpenMRSDataSource { + constructor() { + super(`${restBaseUrl}/concept/conceptUuid?v=custom:(uuid,setMembers:(uuid,display))`); + } + + fetchData(searchTerm: string, config?: Record): Promise { + let apiUrl = this.url; + let urlParts = apiUrl.split('conceptUuid'); + apiUrl = `${urlParts[0]}/${config.concept}${urlParts[1]}`; + return openmrsFetch(apiUrl).then(({ data }) => { + return data['setMembers']; + }); + } +} diff --git a/src/registry/inbuilt-components/control-templates.ts b/src/registry/inbuilt-components/control-templates.ts index 79e9baf04..9dce18084 100644 --- a/src/registry/inbuilt-components/control-templates.ts +++ b/src/registry/inbuilt-components/control-templates.ts @@ -10,6 +10,15 @@ export const controlTemplates: Array = [ }, }, }, + { + name: 'select-concept-answers', + datasource: { + name: 'select_concept_answers_datasource', + config: { + concept: '', + }, + }, + }, { name: 'encounter-provider', datasource: { diff --git a/src/registry/inbuilt-components/inbuiltDataSources.ts b/src/registry/inbuilt-components/inbuiltDataSources.ts index d32d67d74..2cdbe5acc 100644 --- a/src/registry/inbuilt-components/inbuiltDataSources.ts +++ b/src/registry/inbuilt-components/inbuiltDataSources.ts @@ -1,8 +1,9 @@ -import { type DataSource } from '../../types'; -import { type RegistryItem } from '../registry'; -import { ConceptDataSource } from '../../datasources/concept-data-source'; -import { LocationDataSource } from '../../datasources/location-data-source'; -import { ProviderDataSource } from '../../datasources/provider-datasource'; +import {type DataSource} from '../../types'; +import {type RegistryItem} from '../registry'; +import {ConceptDataSource} from '../../datasources/concept-data-source'; +import {LocationDataSource} from '../../datasources/location-data-source'; +import {ProviderDataSource} from '../../datasources/provider-datasource'; +import {ConceptSetMembersDataSource} from '../../datasources/concept-set-members-data-source'; /** * @internal @@ -20,6 +21,10 @@ export const inbuiltDataSources: Array>> = [ name: 'problem_datasource', component: new ConceptDataSource(), }, + { + name: 'select_concept_answers_datasource', + component: new ConceptSetMembersDataSource(), + }, { name: 'provider_datasource', component: new ProviderDataSource(), diff --git a/src/registry/inbuilt-components/template-component-map.ts b/src/registry/inbuilt-components/template-component-map.ts index d1787d9c0..10d037a5c 100644 --- a/src/registry/inbuilt-components/template-component-map.ts +++ b/src/registry/inbuilt-components/template-component-map.ts @@ -17,4 +17,8 @@ export const templateToComponentMap = [ name: 'encounter-location', baseControlComponent: UiSelectExtended, }, + { + name: 'select-concept-answers', + baseControlComponent: UiSelectExtended, + }, ]; diff --git a/src/transformers/angular-fe-schema-transformer.ts b/src/transformers/angular-fe-schema-transformer.ts index c44da8ec9..6fd4ede04 100644 --- a/src/transformers/angular-fe-schema-transformer.ts +++ b/src/transformers/angular-fe-schema-transformer.ts @@ -44,6 +44,9 @@ function transformByRendering(question: FormField) { case 'numeric': question.questionOptions.rendering = 'number'; break; + case 'select-concept-answers': + handleSelectConceptAnswers(question); + break; case 'repeating': case 'group': handleLabOrders(question); @@ -66,3 +69,14 @@ function updateQuestionAnswers(question: FormField) { question.questionOptions.answers = question.questionOptions.selectableOrders || []; delete question.questionOptions.selectableOrders; } + +function handleSelectConceptAnswers(question: FormField) { + if (!question.questionOptions.datasource?.config) { + question.questionOptions.datasource = { + name: 'select_concept_answers_datasource', + config: { + concept: question.questionOptions.concept, + }, + }; + } +} diff --git a/src/types.ts b/src/types.ts index ebb7ca75a..df1e31938 100644 --- a/src/types.ts +++ b/src/types.ts @@ -251,7 +251,8 @@ export type RenderType = | 'ui-select-extended' | 'workspace-launcher' | 'fixed-value' - | 'file'; + | 'file' + | 'select-concept-answers'; export interface PostSubmissionAction { applyAction(