@availity/mui-autocomplete 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [0.5.0](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.4.6...@availity/mui-autocomplete@0.5.0) (2024-06-20)
6
+
7
+
8
+ ### Features
9
+
10
+ * **mui-autocomplete:** add OrganizationAutocomplete component ([9f3ea07](https://github.com/Availity/element/commit/9f3ea0753e0147d7e1aeaf73230716046caba01e))
11
+
5
12
  ## [0.4.6](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.4.5...@availity/mui-autocomplete@0.4.6) (2024-06-14)
6
13
 
7
14
  ### Dependency Updates
package/README.md CHANGED
@@ -54,7 +54,7 @@ The `Autcomplete` component can be used standalone or with a form state library
54
54
 
55
55
  `Autocomplete` uses the `TextField` component to render the input. You must pass your field related props: `label`, `helperText`, `error`, etc. to the the `FieldProps` prop.
56
56
 
57
- ```tsx
57
+ ```jsx
58
58
  import { Autocomplete } from '@availity/element';
59
59
 
60
60
  const MyAutocomplete = () => {
@@ -74,13 +74,13 @@ const MyAutocomplete = () => {
74
74
 
75
75
  #### Direct import
76
76
 
77
- ```tsx
77
+ ```jsx
78
78
  import { Autocomplete } from '@availity/mui-autocomplete';
79
79
  ```
80
80
 
81
81
  #### Usage with `react-hook-form`
82
82
 
83
- ```tsx
83
+ ```jsx
84
84
  import { useForm, Controller } from 'react-hook-form';
85
85
  import { Autocomplete, Button } from '@availity/element';
86
86
 
@@ -123,7 +123,7 @@ const Form = () => {
123
123
 
124
124
  An `AsyncAutocomplete` component is exported for use cases that require fetching paginated results from an api. You will need to use the `loadOptions` prop. The `loadOptions` function will be called when the user scrolls to the bottom of the dropdown. It will be passed the current page and limit. The `limit` prop controls what is passed to `loadOptions` and is defaulted to `50`. The `loadOptions` function must return an object that has an array of `options` and a `hasMore` property. `hasMore` tells the `AsyncAutocomplete` component whether or not it should call `loadOptions` again. The returned `options` will be concatenated to the existing options array.
125
125
 
126
- ```tsx
126
+ ```jsx
127
127
  import { Autocomplete } from '@availity/element';
128
128
 
129
129
  const Example = () => {
@@ -139,3 +139,17 @@ const Example = () => {
139
139
  return <Autocomplete FieldProps={{ label: 'Async Dropdown' }} loadOptions={loadOptions} />;
140
140
  };
141
141
  ```
142
+
143
+ #### `OrganizationAutocomplete` Usage
144
+
145
+ The `OrganizationAutocomplete` component is an extension of the `AsyncAutocomplete` component which calls our Organizations endpoint. The props are the same as `AsyncAutocomplete` except you do not need to pass a function to `loadOptions`. This has already been done for you. The component uses the `name` from the returned organizations as the label for the option. Pass in your own `getOptionLabel` function if you would like to change the label.
146
+
147
+ If you need to add params, headers, or other data to the api call then the `apiConfig` prop is available. This allows for passing in the same options you would to the `getOrganizations`. For example, `permissionIds` or `resourceIds`.
148
+
149
+ ```jsx
150
+ import { OrganizationAutocomplete } from '@availity/element';
151
+
152
+ const Example = () => {
153
+ return <OrganizationAutocomplete FieldProps={{ label: 'Organization Select', placeholder: 'Select...' }} />;
154
+ };
155
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@availity/mui-autocomplete",
3
- "version": "0.4.6",
3
+ "version": "0.5.0",
4
4
  "description": "Availity MUI Autocomplete Component - part of the @availity/element design system",
5
5
  "keywords": [
6
6
  "react",
@@ -0,0 +1,85 @@
1
+ // Each exported component in the package should have its own stories file
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import AvApi, { ApiConfig } from '@availity/api-axios';
4
+
5
+ import { AsyncAutocomplete } from './AsyncAutocomplete';
6
+
7
+ const meta: Meta<typeof AsyncAutocomplete> = {
8
+ title: 'Form Components/Autocomplete/AsyncAutocomplete',
9
+ component: AsyncAutocomplete,
10
+ tags: ['autodocs'],
11
+ args: {
12
+ id: 'example',
13
+ },
14
+ argTypes: {
15
+ multiple: {
16
+ table: {
17
+ disable: true,
18
+ },
19
+ },
20
+ },
21
+ };
22
+
23
+ export default meta;
24
+
25
+ const api = new AvApi({ name: 'example' } as ApiConfig);
26
+
27
+ type Option = {
28
+ label: string;
29
+ value: number;
30
+ };
31
+
32
+ type ExampleResponse = {
33
+ totalCount: number;
34
+ options: Option[];
35
+ count: number;
36
+ };
37
+
38
+ const getResults = async (page: number, limit: number) => {
39
+ const offset = page * limit;
40
+ try {
41
+ const resp = await api.post<ExampleResponse>({ offset, limit }, { params: {} });
42
+
43
+ return {
44
+ totalCount: resp.data.totalCount,
45
+ offset,
46
+ limit,
47
+ options: resp.data.options,
48
+ count: resp.data.count,
49
+ };
50
+ } catch {
51
+ return {
52
+ totalCount: 0,
53
+ offset,
54
+ limit,
55
+ options: [],
56
+ count: 0,
57
+ };
58
+ }
59
+ };
60
+
61
+ const loadOptions = async (page: number, limit: number) => {
62
+ const { options, totalCount, offset } = await getResults(page, limit);
63
+
64
+ return {
65
+ options,
66
+ hasMore: offset + limit < totalCount,
67
+ };
68
+ };
69
+
70
+ export const _Async: StoryObj<typeof AsyncAutocomplete> = {
71
+ render: (args) => {
72
+ return <AsyncAutocomplete {...args} />;
73
+ },
74
+ parameters: {
75
+ controls: {
76
+ exclude: /loading(?!Text)|options/,
77
+ },
78
+ },
79
+ args: {
80
+ FieldProps: { label: 'Async Select', helperText: 'Helper Text', fullWidth: false },
81
+ getOptionLabel: (val: Option) => val.label,
82
+ loadOptions,
83
+ limit: 10,
84
+ },
85
+ };
@@ -1,28 +1,75 @@
1
1
  import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import AvApi, { ApiConfig } from '@availity/api-axios';
3
+ /* eslint-disable @nx/enforce-module-boundaries */
4
+ import { server } from '@availity/mock/src/lib/server';
5
+
2
6
  import { AsyncAutocomplete } from './AsyncAutocomplete';
3
7
 
8
+ const api = new AvApi({ name: 'example' } as ApiConfig);
9
+
10
+ type Option = {
11
+ label: string;
12
+ value: number;
13
+ };
14
+
15
+ type ExampleResponse = {
16
+ totalCount: number;
17
+ options: Option[];
18
+ count: number;
19
+ };
20
+
21
+ const getResults = async (page: number, limit: number) => {
22
+ const offset = page * limit;
23
+ try {
24
+ const resp = await api.post<ExampleResponse>({ offset, limit }, { params: {} });
25
+
26
+ return {
27
+ totalCount: resp.data.totalCount,
28
+ offset,
29
+ limit,
30
+ options: resp.data.options,
31
+ count: resp.data.count,
32
+ };
33
+ } catch {
34
+ return {
35
+ totalCount: 0,
36
+ offset,
37
+ limit,
38
+ options: [],
39
+ count: 0,
40
+ };
41
+ }
42
+ };
43
+
44
+ const loadOptions = async (page: number, limit: number) => {
45
+ const { options, totalCount, offset } = await getResults(page, limit);
46
+
47
+ return {
48
+ options,
49
+ hasMore: offset + limit < totalCount,
50
+ };
51
+ };
52
+
4
53
  describe('AsyncAutocomplete', () => {
54
+ beforeAll(() => {
55
+ // Start the interception.
56
+ server.listen();
57
+ });
58
+
59
+ afterEach(() => {
60
+ // Remove any handlers you may have added
61
+ // in individual tests (runtime handlers).
62
+ server.resetHandlers();
63
+ jest.restoreAllMocks();
64
+ });
65
+
5
66
  test('should render successfully', () => {
6
- const { getByLabelText } = render(
7
- <AsyncAutocomplete
8
- FieldProps={{ label: 'Test' }}
9
- loadOptions={async () => ({
10
- options: ['1', '2', '3'],
11
- hasMore: false,
12
- })}
13
- />
14
- );
67
+ const { getByLabelText } = render(<AsyncAutocomplete FieldProps={{ label: 'Test' }} loadOptions={loadOptions} />);
68
+
15
69
  expect(getByLabelText('Test')).toBeTruthy();
16
70
  });
17
71
 
18
72
  test('options should be available', async () => {
19
- const loadOptions = () =>
20
- Promise.resolve({
21
- options: [{ label: 'Option 1' }],
22
- getOptionLabel: (option: { label: string }) => option.label,
23
- hasMore: false,
24
- });
25
-
26
73
  render(<AsyncAutocomplete loadOptions={loadOptions} FieldProps={{ label: 'Test' }} />);
27
74
 
28
75
  const input = screen.getByRole('combobox');
@@ -41,19 +88,7 @@ describe('AsyncAutocomplete', () => {
41
88
  });
42
89
 
43
90
  test('should call loadOptions when scroll to the bottom', async () => {
44
- const loadOptions = jest.fn();
45
- loadOptions.mockResolvedValueOnce({
46
- options: [
47
- { label: 'Option 1' },
48
- { label: 'Option 2' },
49
- { label: 'Option 3' },
50
- { label: 'Option 4' },
51
- { label: 'Option 5' },
52
- { label: 'Option 6' },
53
- ],
54
- hasMore: true,
55
- });
56
- render(<AsyncAutocomplete loadOptions={loadOptions} FieldProps={{ label: 'Test' }} />);
91
+ render(<AsyncAutocomplete loadOptions={loadOptions} limit={10} FieldProps={{ label: 'Test' }} />);
57
92
 
58
93
  const input = screen.getByRole('combobox');
59
94
  fireEvent.click(input);
@@ -63,24 +98,14 @@ describe('AsyncAutocomplete', () => {
63
98
  expect(screen.getByText('Option 1')).toBeDefined();
64
99
  });
65
100
 
66
- expect(loadOptions).toHaveBeenCalled();
67
- expect(loadOptions).toHaveBeenCalledTimes(1);
68
- expect(loadOptions).toHaveBeenCalledWith(0, 50);
69
-
70
- loadOptions.mockResolvedValueOnce({
71
- options: [{ label: 'Option 7' }],
72
- hasMore: false,
73
- });
74
-
75
101
  await act(async () => {
76
102
  const options = await screen.findByRole('listbox');
77
103
  fireEvent.scroll(options, { target: { scrollTop: options.scrollHeight } });
78
104
  });
79
105
 
80
106
  await waitFor(() => {
81
- expect(loadOptions).toHaveBeenCalled();
82
- expect(loadOptions).toHaveBeenCalledTimes(2);
83
- expect(loadOptions).toHaveBeenLastCalledWith(1, 50);
107
+ expect(screen.getByText('Option 10')).toBeDefined();
108
+ expect(() => screen.getByText('Option 20')).toThrowError();
84
109
  });
85
110
  });
86
111
  });
@@ -1,7 +1,6 @@
1
1
  // Each exported component in the package should have its own stories file
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
  import { Autocomplete } from './Autocomplete';
4
- import { AsyncAutocomplete } from './AsyncAutocomplete';
5
4
 
6
5
  const meta: Meta<typeof Autocomplete> = {
7
6
  title: 'Form Components/Autocomplete/Autocomplete',
@@ -43,116 +42,3 @@ export const _Multi: StoryObj<typeof Autocomplete> = {
43
42
  multiple: true,
44
43
  },
45
44
  };
46
-
47
- type Org = {
48
- id: string;
49
- name: string;
50
- };
51
-
52
- const organizations: Org[] = [
53
- {
54
- id: '1',
55
- name: 'Org 1',
56
- },
57
- {
58
- id: '2',
59
- name: 'Org 2',
60
- },
61
- {
62
- id: '3',
63
- name: 'Org 3',
64
- },
65
- {
66
- id: '4',
67
- name: 'Org 4',
68
- },
69
- {
70
- id: '5',
71
- name: 'Org 5',
72
- },
73
- {
74
- id: '6',
75
- name: 'Org 6',
76
- },
77
- {
78
- id: '7',
79
- name: 'Org 7',
80
- },
81
- {
82
- id: '8',
83
- name: 'Org 8',
84
- },
85
- {
86
- id: '9',
87
- name: 'Org 9',
88
- },
89
- {
90
- id: '10',
91
- name: 'Org 10',
92
- },
93
- {
94
- id: '11',
95
- name: 'Org 11',
96
- },
97
- {
98
- id: '12',
99
- name: 'Org 12',
100
- },
101
- {
102
- id: '13',
103
- name: 'Org 13',
104
- },
105
- {
106
- id: '14',
107
- name: 'Org 14',
108
- },
109
- {
110
- id: '15',
111
- name: 'Org 15',
112
- },
113
- ];
114
-
115
- async function sleep(duration = 2500) {
116
- await new Promise((resolve) => setTimeout(resolve, duration));
117
- }
118
-
119
- const getResults = (page: number, limit: number) => {
120
- const offset = page * limit;
121
- const orgs = organizations.slice(page * offset, page * offset + limit);
122
-
123
- return {
124
- totalCount: organizations.length,
125
- offset,
126
- limit,
127
- orgs,
128
- count: orgs.length,
129
- };
130
- };
131
-
132
- const loadOptions = async (page: number, limit: number) => {
133
- await sleep(1000);
134
-
135
- const { orgs, totalCount, offset } = getResults(page, limit);
136
-
137
- return {
138
- options: orgs,
139
- hasMore: offset + limit < totalCount,
140
- };
141
- };
142
-
143
- export const _Async: StoryObj<typeof AsyncAutocomplete> = {
144
- render: (args) => {
145
- return <AsyncAutocomplete {...args} />;
146
- },
147
- parameters: {
148
- controls: {
149
- exclude: /loading(?!Text)|options/,
150
- },
151
- },
152
- args: {
153
- FieldProps: { label: 'Async Select', helperText: 'Helper Text', fullWidth: false },
154
- getOptionLabel: (val: Org) => val.name,
155
- loadOptions,
156
- limit: 10,
157
- },
158
- };
@@ -0,0 +1,37 @@
1
+ // Each exported component in the package should have its own stories file
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { OrganizationAutocomplete } from './OrganizationAutocomplete';
5
+
6
+ const meta: Meta<typeof OrganizationAutocomplete> = {
7
+ title: 'Form Components/Autocomplete/OrganizationAutocomplete',
8
+ component: OrganizationAutocomplete,
9
+ tags: ['autodocs'],
10
+ args: {
11
+ id: 'example',
12
+ },
13
+ argTypes: {
14
+ multiple: {
15
+ table: {
16
+ disable: true,
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+
24
+ export const _OrganizationAutocomplete: StoryObj<typeof OrganizationAutocomplete> = {
25
+ render: (args) => {
26
+ return <OrganizationAutocomplete {...args} />;
27
+ },
28
+ args: {
29
+ FieldProps: {
30
+ label: 'Organization Select',
31
+ helperText: 'Select an Organization from the list',
32
+ placeholder: 'Select...',
33
+ fullWidth: false,
34
+ },
35
+ limit: 15,
36
+ },
37
+ };
@@ -0,0 +1,31 @@
1
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
+ /* eslint-disable @nx/enforce-module-boundaries */
3
+ import { server } from '@availity/mock/src/lib/server';
4
+
5
+ import { OrganizationAutocomplete } from './OrganizationAutocomplete';
6
+
7
+ describe('OrganizationAutocomplete', () => {
8
+ beforeAll(() => {
9
+ // Start the interception.
10
+ server.listen();
11
+ });
12
+
13
+ afterEach(() => {
14
+ // Remove any handlers you may have added
15
+ // in individual tests (runtime handlers).
16
+ server.resetHandlers();
17
+ jest.restoreAllMocks();
18
+ });
19
+
20
+ test('organizations are fetched and displayed by name', async () => {
21
+ render(<OrganizationAutocomplete FieldProps={{ label: 'Test' }} />);
22
+
23
+ const input = screen.getByRole('combobox');
24
+ fireEvent.click(input);
25
+ fireEvent.keyDown(input, { key: 'ArrowDown' });
26
+
27
+ await waitFor(() => {
28
+ expect(screen.getByText('Organization 1')).toBeDefined();
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,52 @@
1
+ import { avOrganizationsApi, ApiConfig } from '@availity/api-axios';
2
+ import type { ChipTypeMap } from '@mui/material/Chip';
3
+
4
+ import { AsyncAutocomplete, AsyncAutocompleteProps } from './AsyncAutocomplete';
5
+
6
+ export type Organization = {
7
+ customerId: string;
8
+ name: string;
9
+ id: string;
10
+ createDate: string;
11
+ links: Record<string, Record<string, string>>;
12
+ };
13
+
14
+ const fetchOrgs = async (config: ApiConfig): Promise<{ options: Organization[]; hasMore: boolean }> => {
15
+ try {
16
+ const resp = await avOrganizationsApi.getOrganizations(config);
17
+
18
+ return {
19
+ options: resp.data.organizations as Organization[],
20
+ hasMore: config.params.offset + config.params.limit < resp.data.totalCount,
21
+ };
22
+ } catch {
23
+ return {
24
+ options: [],
25
+ hasMore: false,
26
+ };
27
+ }
28
+ };
29
+
30
+ export type OrgAutocompleteProps<
31
+ Option = Organization,
32
+ Multiple extends boolean | undefined = false,
33
+ DisableClearable extends boolean | undefined = false,
34
+ FreeSolo extends boolean | undefined = false,
35
+ ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
36
+ > = {
37
+ apiConfig?: ApiConfig;
38
+ } & Omit<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'loadOptions'>;
39
+
40
+ export const OrganizationAutocomplete = ({ apiConfig = {}, ...rest }: OrgAutocompleteProps) => {
41
+ const handleLoadOptions = async (page: number, limit: number) => {
42
+ const offset = page * limit;
43
+
44
+ const resp = await fetchOrgs({ ...apiConfig, params: { dropdown: true, ...apiConfig.params, offset, limit } });
45
+
46
+ return resp;
47
+ };
48
+
49
+ const handleGetOptionLabel = (org: Organization) => org.name;
50
+
51
+ return <AsyncAutocomplete getOptionLabel={handleGetOptionLabel} {...rest} loadOptions={handleLoadOptions} />;
52
+ };