Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inventory product select update #1484

Merged
merged 4 commits into from
Oct 11, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { Form, Button, Input, DatePicker, Space, Switch } from 'antd';
import {
PaginatedAsyncSelect,
formItemLayout,
tailLayout,
SelectOption as ProductSelectOption,
ValueSetAsyncSelect,
ClientSideActionsSelect,
} from '@opensrp/react-utils';
import { useTranslation } from '../../mls';
import { useQueryClient, useMutation } from 'react-query';
@@ -162,12 +162,12 @@ const AddLocationInventoryForm = (props: LocationInventoryFormProps) => {
initialValues={initialValues}
>
<FormItem id={product} name={product} label={t('Product name')}>
<PaginatedAsyncSelect<IGroup>
baseUrl={fhirBaseURL}
<ClientSideActionsSelect<IGroup>
fhirBaseUrl={fhirBaseURL}
resourceType={groupResourceType}
transformOption={processProductOptions}
extraQueryParams={productQueryFilters}
showSearch={false}
showSearch={true}
placeholder={t('Select product')}
getFullOptionOnChange={productChangeHandler}
disabled={editMode}
Original file line number Diff line number Diff line change
@@ -152,7 +152,13 @@ test('creates new inventory as expected', async () => {
const preFetchScope = nock(props.fhirBaseURL)
.get(`/${groupResourceType}/_search`)
.query({
_getpagesoffset: 0,
_summary: 'count',
code: 'http://snomed.info/sct|386452003',
'_has:List:item:_id': props.commodityListId,
})
.reply(200, { total: 20 })
.get(`/${groupResourceType}/_search`)
.query({
_count: 20,
code: 'http://snomed.info/sct|386452003',
'_has:List:item:_id': props.commodityListId,
@@ -194,29 +200,32 @@ test('creates new inventory as expected', async () => {
render(<AppWrapper {...thisProps}></AppWrapper>);

await waitFor(() => {
expect(preFetchScope.isDone()).toBeTruthy();
expect(preFetchScope.pendingMocks()).toEqual([]);
});

// simulate value selection for product
const productSelectComponent = document.querySelector(`input#${product}`)!;
fireEvent.mouseDown(productSelectComponent);

const optionTexts = [
...document.querySelectorAll(
`#${product}_list+div.rc-virtual-list .ant-select-item-option-content`
),
].map((option) => {
return option.textContent;
await waitFor(() => {
// simulate value selection for product
const productSelectComponent = document.querySelector(`input#${product}`)!;
fireEvent.mouseDown(productSelectComponent);

const optionTexts = [
...document.querySelectorAll(
`#${product}_list+div.rc-virtual-list .ant-select-item-option-content`
),
].map((option) => {
return option.textContent;
});

expect(optionTexts).toEqual([
'Yellow sunshine',
'Fig tree',
'Lumpy nuts',
'Happy Feet',
'Lilly Flowers',
'Smartphone TEST',
]);
});

expect(optionTexts).toEqual([
'Yellow sunshine',
'Fig tree',
'Lumpy nuts',
'Happy Feet',
'Lilly Flowers',
'Smartphone TEST',
]);
fireEvent.click(document.querySelector(`[title="${'Lumpy nuts'}"]`)!);

const quantity = screen.getByLabelText('Quantity');
@@ -269,7 +278,13 @@ test('#1384 - correctly updates location inventory', async () => {
const preFetchScope = nock(props.fhirBaseURL)
.get(`/${groupResourceType}/_search`)
.query({
_getpagesoffset: 0,
_summary: 'count',
code: 'http://snomed.info/sct|386452003',
'_has:List:item:_id': props.commodityListId,
})
.reply(200, { total: 20 })
.get(`/${groupResourceType}/_search`)
.query({
_count: 20,
code: 'http://snomed.info/sct|386452003',
'_has:List:item:_id': props.commodityListId,
@@ -322,26 +337,28 @@ test('#1384 - correctly updates location inventory', async () => {
// serial number is initially not shown on the form
expect(screen.queryByText('Serial number')).not.toBeInTheDocument();

// simulate value selection for product
const productSelectComponent = document.querySelector(`input#${product}`)!;
fireEvent.mouseDown(productSelectComponent);

const optionTexts = [
...document.querySelectorAll(
`#${product}_list+div.rc-virtual-list .ant-select-item-option-content`
),
].map((option) => {
return option.textContent;
await waitFor(() => {
// simulate value selection for product
const productSelectComponent = document.querySelector(`input#${product}`)!;
fireEvent.mouseDown(productSelectComponent);

const optionTexts = [
...document.querySelectorAll(
`#${product}_list+div.rc-virtual-list .ant-select-item-option-content`
),
].map((option) => {
return option.textContent;
});

expect(optionTexts).toEqual([
'Yellow sunshine',
'Fig tree',
'Lumpy nuts',
'Happy Feet',
'Lilly Flowers',
'Smartphone TEST',
]);
});

expect(optionTexts).toEqual([
'Yellow sunshine',
'Fig tree',
'Lumpy nuts',
'Happy Feet',
'Lilly Flowers',
'Smartphone TEST',
]);
fireEvent.click(document.querySelector(`[title="${'Lumpy nuts'}"]`)!);

const quantity = screen.getByLabelText('Quantity');
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import { URLParams } from '@opensrp/server-service';
import { useQuery } from 'react-query';
import { Divider, Select, Empty, Spin, Alert } from 'antd';
import { IResource } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IResource';
import { getResourcesFromBundle } from '../../../helpers/utils';
import { useTranslation } from '../../../mls';
import { loadAllResources } from '../../../helpers/fhir-utils';
import {
AbstractedSelectOptions,
defaultSelectFilterFunction,
SelectOption,
TransformOptions,
} from '../utils';

export interface ClientSideActionsSelectProps<ResourceT extends IResource>
extends AbstractedSelectOptions<ResourceT> {
fhirBaseUrl: string;
resourceType: string;
extraQueryParams?: URLParams;
transformOption: TransformOptions<ResourceT>;
getFullOptionOnChange?: (obj: SelectOption<ResourceT> | SelectOption<ResourceT>[]) => void;
}

/**
* Select component that loads all options as a single resource
*
* @param props - component props
*/
export function ClientSideActionsSelect<ResourceT extends IResource>(
props: ClientSideActionsSelectProps<ResourceT>
) {
const {
fhirBaseUrl,
resourceType,
extraQueryParams = {},
transformOption,
onChange,
getFullOptionOnChange,
...restProps
} = props;

const { t } = useTranslation();

const {
data: options,
isLoading,
error,
} = useQuery({
queryKey: [ClientSideActionsSelect.name, resourceType],
queryFn: async () => {
return await loadAllResources(fhirBaseUrl, resourceType, extraQueryParams);
},
refetchOnWindowFocus: false,
select: (bundle) => {
const options = getResourcesFromBundle<ResourceT>(bundle).map((resource) =>
transformOption(resource)
);
return options as SelectOption<ResourceT>[];
},
});

const changeHandler = (
value: string,
fullOption: SelectOption<ResourceT> | SelectOption<ResourceT>[]
) => {
const saneFullOption = Array.isArray(fullOption) ? fullOption.slice() : fullOption;
props.onChange?.(value, saneFullOption);
getFullOptionOnChange?.(saneFullOption);
};

const propsToSelect = {
className: 'asyncSelect',
filterOption: defaultSelectFilterFunction,
...restProps,
onChange: changeHandler,
loading: isLoading,
notFoundContent: isLoading ? <Spin size="small" /> : <Empty description={t('No data')} />,
options,
dropdownRender: (menu: React.ReactNode) => (
<>
{!error && options?.length && menu}
<Divider style={{ margin: '8px 0' }} />
{error && <Alert message={t('Unable to load dropdown options.')} type="error" showIcon />}
</>
),
};

return <Select {...propsToSelect}></Select>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
cleanup,
fireEvent,
render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react';
import React from 'react';
import * as reactQuery from 'react-query';
import { ClientSideActionsSelect } from '../index';
import { IOrganization } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IOrganization';
import nock from 'nock';
import { store } from '@opensrp/store';
import { authenticateUser } from '@onaio/session-reducer';
import flushPromises from 'flush-promises';
import {
organizationsPage1,
organizationsPage1Summary,
} from '../../PaginatedAsyncSelect/tests/fixtures';
import userEvent from '@testing-library/user-event';

const organizationResourceType = 'Organization';

jest.mock('fhirclient', () => {
return jest.requireActual('fhirclient/lib/entry/browser');
});

const { QueryClient, QueryClientProvider } = reactQuery;

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});

export const QueryWrapper = ({ children }: { children: JSX.Element }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

beforeAll(() => {
nock.disableNetConnect();
store.dispatch(
authenticateUser(
true,
{
email: '[email protected]',
name: 'Bobbie',
username: 'RobertBaratheon',
},
{ api_token: 'hunter2', oAuth2Data: { access_token: 'sometoken', state: 'abcde' } }
)
);
});

afterAll(() => {
nock.enableNetConnect();
});

afterEach(() => {
nock.cleanAll();
cleanup();
jest.resetAllMocks();
});

const commonProps = {
fhirBaseUrl: 'https://sample.com',
resourceType: organizationResourceType,
transformOption: (resource: IOrganization) => {
const { name } = resource;
const id = resource.id as string;
return {
label: name ?? id,
value: id,
ref: resource,
};
},
};

test('works correctly nominal case', async () => {
nock(commonProps.fhirBaseUrl)
.get(`/${organizationResourceType}/_search`)
.query({ _count: '10' })
.reply(200, organizationsPage1);

nock(commonProps.fhirBaseUrl)
.get(`/${organizationResourceType}/_search`)
.query({ _summary: 'count' })
.reply(200, organizationsPage1Summary);

const changeMock = jest.fn();
const fullOptionHandlerMock = jest.fn();

const props = {
...commonProps,
onChange: changeMock,
getFullOptionOnChange: fullOptionHandlerMock,
};

render(
<QueryWrapper>
<ClientSideActionsSelect<IOrganization> {...props}></ClientSideActionsSelect>
</QueryWrapper>
);

await waitForElementToBeRemoved(document.querySelector('.anticon-spin'));

// click on input. - should see the first 5 records by default
const input = document.querySelector('.ant-select-selector') as Element;

// simulate click on select - to show dropdown items
fireEvent.mouseDown(input);

// find antd select options
const selectOptions = document.querySelectorAll('.ant-select-item-option-content');

await flushPromises();
// expect all practitioners (except inactive ones)
expect([...selectOptions].map((opt) => opt.textContent)).toStrictEqual([
'高雄榮民總醫院',
'Blok Operacyjny Chirurgii Naczyń',
'Volunteer virtual hospital 志工虛擬醫院',
'Volunteer virtual hospital 志工虛擬醫院',
'Volunteer virtual hospital 志工虛擬醫院',
]);

// search and then select.
userEvent.type(input.querySelector('input') as Element, 'Blok');

fireEvent.click(screen.getByTitle('Blok Operacyjny Chirurgii Naczyń') as Element);

const blokOrgId = '22332';
const blokOrganizationFullOption = {
value: '22332',
ref: organizationsPage1.entry[1].resource,
label: 'Blok Operacyjny Chirurgii Naczyń',
};

expect(changeMock).toHaveBeenCalledWith(blokOrgId, blokOrganizationFullOption);
expect(fullOptionHandlerMock).toHaveBeenCalledWith(blokOrganizationFullOption);
});
Loading