Skip to content

Commit

Permalink
(feat) O3-3050: add support for rendering select-concept-answers (#214)
Browse files Browse the repository at this point in the history
* feat: add support for rendering select-concept-answers

* refactor: use concet-set-member-ds

* chore: add unit tests

* chore: rebase branch to main

* fix: fix failed build

* refactor: user ui-select-ext input

* refactor: fix formating prettier

* Fine-tune datasource config during schema preprocessing

---------

Co-authored-by: samuelmale <samuelsmalek@gmail.com>
  • Loading branch information
usamaidrsk and samuelmale authored May 8, 2024
1 parent ae36027 commit ab88063
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 42 deletions.
2 changes: 1 addition & 1 deletion src/components/inputs/select/dropdown.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
117 changes: 82 additions & 35 deletions src/components/inputs/ui-select-extended/ui-select-extended.test.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -54,12 +72,13 @@ const renderForm = (initialValues) => {
obsGroupsToVoid: [],
setObsGroupsToVoid: jest.fn(),
encounterContext: encounterContext,
fields: [question],
fields: questions,
isFieldInitializationComplete: true,
isSubmitting: false,
formFieldHandlers: { obs: ObsSubmissionHandler },
}}>
<UiSelectExtended question={question} onChange={jest.fn()} handler={ObsSubmissionHandler} />
<UiSelectExtended question={questions[0]} onChange={jest.fn()} handler={ObsSubmissionHandler} />
<UiSelectExtended question={questions[1]} onChange={jest.fn()} handler={ObsSubmissionHandler} />
</FormContext.Provider>
)}
</Formik>,
Expand All @@ -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,
}),
}));
Expand All @@ -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
Expand All @@ -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({});
Expand All @@ -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',
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/datasources/concept-data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
17 changes: 17 additions & 0 deletions src/datasources/concept-set-members-data-source.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>): Promise<any[]> {
let apiUrl = this.url;
let urlParts = apiUrl.split('conceptUuid');
apiUrl = `${urlParts[0]}/${config.concept}${urlParts[1]}`;
return openmrsFetch(apiUrl).then(({ data }) => {
return data['setMembers'];
});
}
}
9 changes: 9 additions & 0 deletions src/registry/inbuilt-components/control-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export const controlTemplates: Array<ControlTemplate> = [
},
},
},
{
name: 'select-concept-answers',
datasource: {
name: 'select_concept_answers_datasource',
config: {
concept: '',
},
},
},
{
name: 'encounter-provider',
datasource: {
Expand Down
15 changes: 10 additions & 5 deletions src/registry/inbuilt-components/inbuiltDataSources.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,6 +21,10 @@ export const inbuiltDataSources: Array<RegistryItem<DataSource<any>>> = [
name: 'problem_datasource',
component: new ConceptDataSource(),
},
{
name: 'select_concept_answers_datasource',
component: new ConceptSetMembersDataSource(),
},
{
name: 'provider_datasource',
component: new ProviderDataSource(),
Expand Down
4 changes: 4 additions & 0 deletions src/registry/inbuilt-components/template-component-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ export const templateToComponentMap = [
name: 'encounter-location',
baseControlComponent: UiSelectExtended,
},
{
name: 'select-concept-answers',
baseControlComponent: UiSelectExtended,
},
];
14 changes: 14 additions & 0 deletions src/transformers/angular-fe-schema-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
},
};
}
}
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ export type RenderType =
| 'ui-select-extended'
| 'workspace-launcher'
| 'fixed-value'
| 'file';
| 'file'
| 'select-concept-answers';

export interface PostSubmissionAction {
applyAction(
Expand Down

0 comments on commit ab88063

Please sign in to comment.