@availity/mui-autocomplete 0.5.1 → 0.6.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,24 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [0.6.0](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.5.2...@availity/mui-autocomplete@0.6.0) (2024-07-01)
6
+
7
+
8
+ ### Features
9
+
10
+ * **mui-autocomplete:** add ProviderAutocomplete component ([5ed1450](https://github.com/Availity/element/commit/5ed1450a6daca47231a030b516b5bf950c190f08))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **mui-autocomplete:** fix unit test ([50e2b52](https://github.com/Availity/element/commit/50e2b52f2a0b7799fe63d4d140259255d905a96f))
16
+
17
+ ## [0.5.2](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.5.1...@availity/mui-autocomplete@0.5.2) (2024-06-27)
18
+
19
+ ### Dependency Updates
20
+
21
+ * `mui-form-utils` updated to version `0.5.1`
22
+ * `mui-textfield` updated to version `0.5.1`
5
23
  ## [0.5.1](https://github.com/Availity/element/compare/@availity/mui-autocomplete@0.5.0...@availity/mui-autocomplete@0.5.1) (2024-06-21)
6
24
 
7
25
 
package/README.md CHANGED
@@ -48,8 +48,6 @@ yarn add @availity/mui-autocomplete
48
48
 
49
49
  ### Usage
50
50
 
51
- #### Import through @availity/element
52
-
53
51
  The `Autcomplete` component can be used standalone or with a form state library like [react-hook-form](https://react-hook-form.com/).
54
52
 
55
53
  `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.
@@ -121,18 +119,24 @@ const Form = () => {
121
119
 
122
120
  #### `AsyncAutocomplete` Usage
123
121
 
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.
122
+ An `AsyncAutocomplete` component is exported for use cases that require fetching paginated results from an api. It uses the `useInfiniteQuery` from [@tanstack/react-query](https://tanstack.com/query/v4/docs/framework/react/guides/infinite-queries). The component requires two props to work properly: `loadOptions` and `queryKey`. The `loadOptions` prop is the function that is called to fetch the options. The `queryKey` is used as the key to cache the results. You can use this key to interact with the data in the query client.
123
+
124
+ The `loadOptions` function will be called when the user scrolls to the bottom of the dropdown. It will be passed the current offset 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`, a boolean `hasMore` property, and the `offset`. The returned `options` will be concatenated to the existing options array. `hasMore` tells the `AsyncAutocomplete` component whether or not it should call `loadOptions` again. Finally, the returned `offset` will be passed in the subsequent call to get the next set of options.
125
+
126
+ The `queryOptions` prop is available for passing in options to the `useInfiniteQuery` hook. One example of how this can be used is by using the `enabled` property. This can be used in cases where you would like to render the autocomplete, but are waiting on fetching the options. For example, if you need the user to fill out a section of the form before fetching the options for the autocomplete.
125
127
 
126
128
  ```jsx
127
129
  import { Autocomplete } from '@availity/element';
130
+ import { callApi } from '../api';
128
131
 
129
132
  const Example = () => {
130
- const loadOptions = async (page: number) => {
131
- const response = await callApi(page);
133
+ const loadOptions = async (offset: number, limit: number) => {
134
+ const response = await callApi(offset, limit);
132
135
 
133
136
  return {
134
137
  options: repsonse.data,
135
- hasMore: response.totalCount > response.count,
138
+ hasMore: response.data.totalCount > response.data.count,
139
+ offset,
136
140
  };
137
141
  };
138
142
 
@@ -146,6 +150,8 @@ The `OrganizationAutocomplete` component is an extension of the `AsyncAutocomple
146
150
 
147
151
  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
152
 
153
+ The `queryKey` by default is `org-autocomplete`.
154
+
149
155
  ```jsx
150
156
  import { OrganizationAutocomplete } from '@availity/element';
151
157
 
@@ -153,3 +159,19 @@ const Example = () => {
153
159
  return <OrganizationAutocomplete FieldProps={{ label: 'Organization Select', placeholder: 'Select...' }} />;
154
160
  };
155
161
  ```
162
+
163
+ #### `ProviderAutocomplete` Usage
164
+
165
+ The `ProviderAutocomplete` component is an extension of the `AsyncAutocomplete` component which calls our Providers endpoint. The props are the same except you do not need to pass a function to `loadOptions`. This has already been done for you. The component uses the `uiDisplayName` as the default label for the options. This can be changed through the `getOptionLabel` function.
166
+
167
+ `ProviderAutocomplete` requires a `customerId` to call the api. You can pass it in as prop that the component will then use in the api call. There is also an `apiConfig` prop available for further customizing the call.
168
+
169
+ The `queryKey` by default is `prov-autocomplete`.
170
+
171
+ ```jsx
172
+ import { ProviderAutocomplete } from '@availity/element';
173
+
174
+ const Example = () => {
175
+ return <ProviderAutocomplete customerId="1234" FieldProps={{ label: 'Provider Select', placeholder: 'Select...' }} />;
176
+ };
177
+ ```
package/dist/index.d.mts CHANGED
@@ -3,6 +3,7 @@ import { AutocompleteProps as AutocompleteProps$1 } from '@mui/material/Autocomp
3
3
  import { ChipTypeMap } from '@mui/material/Chip';
4
4
  import { TextFieldProps } from '@availity/mui-textfield';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
+ import { UseInfiniteQueryOptions } from '@tanstack/react-query';
6
7
  import { ApiConfig } from '@availity/api-axios';
7
8
 
8
9
  interface AutocompleteProps<T, Multiple extends boolean | undefined, DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AutocompleteProps$1<T, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'clearIcon' | 'clearText' | 'closeText' | 'componentsProps' | 'disabledItemsFocusable' | 'forcePopupIcon' | 'fullWidth' | 'handleHomeEndKeys' | 'includeInputInList' | 'openOnFocus' | 'openText' | 'PaperComponent' | 'PopperComponent' | 'popupIcon' | 'selectOnFocus' | 'size' | 'renderInput' | 'slotProps'> {
@@ -10,19 +11,30 @@ interface AutocompleteProps<T, Multiple extends boolean | undefined, DisableClea
10
11
  FieldProps?: TextFieldProps;
11
12
  name?: string;
12
13
  }
13
- declare const Autocomplete: <T, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any> = "div">({ FieldProps, ...props }: AutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>) => JSX.Element;
14
+ declare const Autocomplete: <T, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any, keyof react.JSX.IntrinsicElements> = "div">({ FieldProps, ...props }: AutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>) => JSX.Element;
14
15
 
15
16
  interface AsyncAutocompleteProps<Option, Multiple extends boolean | undefined, DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'options' | 'disableListWrap' | 'loading'> {
16
- /** Function that returns a promise with options and hasMore */
17
- loadOptions: (page: number, limit: number) => Promise<{
17
+ /** Function that is called to fetch the options for the list. Returns a promise with options, hasMore, and offset */
18
+ loadOptions: (offset: number, limit: number) => Promise<{
18
19
  options: Option[];
19
20
  hasMore: boolean;
21
+ offset: number;
20
22
  }>;
23
+ /** The key used by @tanstack/react-query to cache the response */
24
+ queryKey: string;
21
25
  /** The number of options to request from the api
22
26
  * @default 50 */
23
27
  limit?: number;
28
+ /** Config options for the useInfiniteQuery hook */
29
+ queryOptions?: UseInfiniteQueryOptions<{
30
+ options: Option[];
31
+ hasMore: boolean;
32
+ offset: number;
33
+ }>;
24
34
  }
25
- declare const AsyncAutocomplete: <Option, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any> = "div">({ loadOptions, limit, ListboxProps, ...rest }: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => react_jsx_runtime.JSX.Element;
35
+ declare const AsyncAutocomplete: <Option, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any, keyof react.JSX.IntrinsicElements> = "div">({ loadOptions, limit, queryKey, ListboxProps, queryOptions, ...rest }: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => react_jsx_runtime.JSX.Element;
36
+
37
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
26
38
 
27
39
  type Organization = {
28
40
  customerId: string;
@@ -31,9 +43,9 @@ type Organization = {
31
43
  createDate: string;
32
44
  links: Record<string, Record<string, string>>;
33
45
  };
34
- interface OrgAutocompleteProps<Option = Organization, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'loadOptions'> {
46
+ interface OrgAutocompleteProps<Option = Organization, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<Optional<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'queryKey'>, 'loadOptions'> {
35
47
  apiConfig?: ApiConfig;
36
48
  }
37
- declare const OrganizationAutocomplete: ({ apiConfig, ...rest }: OrgAutocompleteProps) => react_jsx_runtime.JSX.Element;
49
+ declare const OrganizationAutocomplete: ({ apiConfig, queryKey, ...rest }: OrgAutocompleteProps) => react_jsx_runtime.JSX.Element;
38
50
 
39
51
  export { AsyncAutocomplete, type AsyncAutocompleteProps, Autocomplete, type AutocompleteProps, type OrgAutocompleteProps, type Organization, OrganizationAutocomplete };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { AutocompleteProps as AutocompleteProps$1 } from '@mui/material/Autocomp
3
3
  import { ChipTypeMap } from '@mui/material/Chip';
4
4
  import { TextFieldProps } from '@availity/mui-textfield';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
+ import { UseInfiniteQueryOptions } from '@tanstack/react-query';
6
7
  import { ApiConfig } from '@availity/api-axios';
7
8
 
8
9
  interface AutocompleteProps<T, Multiple extends boolean | undefined, DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AutocompleteProps$1<T, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'clearIcon' | 'clearText' | 'closeText' | 'componentsProps' | 'disabledItemsFocusable' | 'forcePopupIcon' | 'fullWidth' | 'handleHomeEndKeys' | 'includeInputInList' | 'openOnFocus' | 'openText' | 'PaperComponent' | 'PopperComponent' | 'popupIcon' | 'selectOnFocus' | 'size' | 'renderInput' | 'slotProps'> {
@@ -10,19 +11,30 @@ interface AutocompleteProps<T, Multiple extends boolean | undefined, DisableClea
10
11
  FieldProps?: TextFieldProps;
11
12
  name?: string;
12
13
  }
13
- declare const Autocomplete: <T, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any> = "div">({ FieldProps, ...props }: AutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>) => JSX.Element;
14
+ declare const Autocomplete: <T, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any, keyof react.JSX.IntrinsicElements> = "div">({ FieldProps, ...props }: AutocompleteProps<T, Multiple, DisableClearable, FreeSolo, ChipComponent>) => JSX.Element;
14
15
 
15
16
  interface AsyncAutocompleteProps<Option, Multiple extends boolean | undefined, DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'options' | 'disableListWrap' | 'loading'> {
16
- /** Function that returns a promise with options and hasMore */
17
- loadOptions: (page: number, limit: number) => Promise<{
17
+ /** Function that is called to fetch the options for the list. Returns a promise with options, hasMore, and offset */
18
+ loadOptions: (offset: number, limit: number) => Promise<{
18
19
  options: Option[];
19
20
  hasMore: boolean;
21
+ offset: number;
20
22
  }>;
23
+ /** The key used by @tanstack/react-query to cache the response */
24
+ queryKey: string;
21
25
  /** The number of options to request from the api
22
26
  * @default 50 */
23
27
  limit?: number;
28
+ /** Config options for the useInfiniteQuery hook */
29
+ queryOptions?: UseInfiniteQueryOptions<{
30
+ options: Option[];
31
+ hasMore: boolean;
32
+ offset: number;
33
+ }>;
24
34
  }
25
- declare const AsyncAutocomplete: <Option, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any> = "div">({ loadOptions, limit, ListboxProps, ...rest }: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => react_jsx_runtime.JSX.Element;
35
+ declare const AsyncAutocomplete: <Option, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends react.ElementType<any, keyof react.JSX.IntrinsicElements> = "div">({ loadOptions, limit, queryKey, ListboxProps, queryOptions, ...rest }: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => react_jsx_runtime.JSX.Element;
36
+
37
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
26
38
 
27
39
  type Organization = {
28
40
  customerId: string;
@@ -31,9 +43,9 @@ type Organization = {
31
43
  createDate: string;
32
44
  links: Record<string, Record<string, string>>;
33
45
  };
34
- interface OrgAutocompleteProps<Option = Organization, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'loadOptions'> {
46
+ interface OrgAutocompleteProps<Option = Organization, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false, ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent']> extends Omit<Optional<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'queryKey'>, 'loadOptions'> {
35
47
  apiConfig?: ApiConfig;
36
48
  }
37
- declare const OrganizationAutocomplete: ({ apiConfig, ...rest }: OrgAutocompleteProps) => react_jsx_runtime.JSX.Element;
49
+ declare const OrganizationAutocomplete: ({ apiConfig, queryKey, ...rest }: OrgAutocompleteProps) => react_jsx_runtime.JSX.Element;
38
50
 
39
51
  export { AsyncAutocomplete, type AsyncAutocompleteProps, Autocomplete, type AutocompleteProps, type OrgAutocompleteProps, type Organization, OrganizationAutocomplete };
package/dist/index.js CHANGED
@@ -129,51 +129,42 @@ var Autocomplete = (_a) => {
129
129
  };
130
130
 
131
131
  // src/lib/AsyncAutocomplete.tsx
132
- var import_react2 = require("react");
132
+ var import_react_query = require("@tanstack/react-query");
133
133
  var import_jsx_runtime2 = require("react/jsx-runtime");
134
134
  var AsyncAutocomplete = (_a) => {
135
135
  var _b = _a, {
136
136
  loadOptions,
137
137
  limit = 50,
138
- ListboxProps
138
+ queryKey,
139
+ ListboxProps,
140
+ queryOptions
139
141
  } = _b, rest = __objRest(_b, [
140
142
  "loadOptions",
141
143
  "limit",
142
- "ListboxProps"
144
+ "queryKey",
145
+ "ListboxProps",
146
+ "queryOptions"
143
147
  ]);
144
- const [page, setPage] = (0, import_react2.useState)(0);
145
- const [options, setOptions] = (0, import_react2.useState)([]);
146
- const [loading, setLoading] = (0, import_react2.useState)(false);
147
- const [hasMore, setHasMore] = (0, import_react2.useState)(true);
148
- (0, import_react2.useEffect)(() => {
149
- const getInitialOptions = () => __async(void 0, null, function* () {
150
- setLoading(true);
151
- const result = yield loadOptions(page, limit);
152
- setOptions(result.options);
153
- setHasMore(result.hasMore);
154
- setPage((prev) => prev + 1);
155
- setLoading(false);
156
- });
157
- if (!loading && hasMore && page === 0) {
158
- getInitialOptions();
159
- }
160
- }, [page, loading, loadOptions]);
148
+ const { isLoading, isFetching, data, hasNextPage, fetchNextPage } = (0, import_react_query.useInfiniteQuery)(__spreadValues({
149
+ queryKey: [queryKey, limit],
150
+ queryFn: (_0) => __async(void 0, [_0], function* ({ pageParam = 0 }) {
151
+ return loadOptions(pageParam, limit);
152
+ }),
153
+ staleTime: 1e4,
154
+ getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.offset + limit : false
155
+ }, queryOptions));
156
+ const options = (data == null ? void 0 : data.pages) ? data.pages.map((page) => page.options).flat() : [];
161
157
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
162
158
  Autocomplete,
163
159
  __spreadProps(__spreadValues({}, rest), {
164
- loading,
160
+ loading: isFetching,
165
161
  options,
166
162
  ListboxProps: __spreadProps(__spreadValues({}, ListboxProps), {
167
163
  onScroll: (event) => __async(void 0, null, function* () {
168
164
  const listboxNode = event.currentTarget;
169
165
  const difference = listboxNode.scrollHeight - (listboxNode.scrollTop + listboxNode.clientHeight);
170
- if (difference <= 5 && !loading && hasMore) {
171
- setLoading(true);
172
- const result = yield loadOptions(page, limit);
173
- setOptions([...options, ...result.options]);
174
- setHasMore(result.hasMore);
175
- setPage((prev) => prev + 1);
176
- setLoading(false);
166
+ if (difference <= 5 && !isLoading && !isFetching && hasNextPage) {
167
+ fetchNextPage();
177
168
  }
178
169
  })
179
170
  })
@@ -185,28 +176,35 @@ var AsyncAutocomplete = (_a) => {
185
176
  var import_api_axios = require("@availity/api-axios");
186
177
  var import_jsx_runtime3 = require("react/jsx-runtime");
187
178
  var fetchOrgs = (config) => __async(void 0, null, function* () {
188
- try {
189
- const resp = yield import_api_axios.avOrganizationsApi.getOrganizations(config);
190
- return {
191
- options: resp.data.organizations,
192
- hasMore: config.params.offset + config.params.limit < resp.data.totalCount
193
- };
194
- } catch (e) {
195
- return {
196
- options: [],
197
- hasMore: false
198
- };
199
- }
179
+ const resp = yield import_api_axios.avOrganizationsApi.getOrganizations(config);
180
+ return {
181
+ options: resp.data.organizations,
182
+ hasMore: config.params.offset + config.params.limit < resp.data.totalCount,
183
+ offset: config.params.offset
184
+ };
200
185
  });
201
186
  var OrganizationAutocomplete = (_a) => {
202
- var _b = _a, { apiConfig = {} } = _b, rest = __objRest(_b, ["apiConfig"]);
203
- const handleLoadOptions = (page, limit) => __async(void 0, null, function* () {
204
- const offset = page * limit;
187
+ var _b = _a, {
188
+ apiConfig = {},
189
+ queryKey = "org-autocomplete"
190
+ } = _b, rest = __objRest(_b, [
191
+ "apiConfig",
192
+ "queryKey"
193
+ ]);
194
+ const handleLoadOptions = (offset, limit) => __async(void 0, null, function* () {
205
195
  const resp = yield fetchOrgs(__spreadProps(__spreadValues({}, apiConfig), { params: __spreadProps(__spreadValues({ dropdown: true }, apiConfig.params), { offset, limit }) }));
206
196
  return resp;
207
197
  });
208
198
  const handleGetOptionLabel = (org) => org.name;
209
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AsyncAutocomplete, __spreadProps(__spreadValues({ getOptionLabel: handleGetOptionLabel }, rest), { loadOptions: handleLoadOptions }));
199
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
200
+ AsyncAutocomplete,
201
+ __spreadProps(__spreadValues({
202
+ getOptionLabel: handleGetOptionLabel,
203
+ queryKey
204
+ }, rest), {
205
+ loadOptions: handleLoadOptions
206
+ })
207
+ );
210
208
  };
211
209
  // Annotate the CommonJS export names for ESM import in node:
212
210
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -96,51 +96,42 @@ var Autocomplete = (_a) => {
96
96
  };
97
97
 
98
98
  // src/lib/AsyncAutocomplete.tsx
99
- import { useState, useEffect } from "react";
99
+ import { useInfiniteQuery } from "@tanstack/react-query";
100
100
  import { jsx as jsx2 } from "react/jsx-runtime";
101
101
  var AsyncAutocomplete = (_a) => {
102
102
  var _b = _a, {
103
103
  loadOptions,
104
104
  limit = 50,
105
- ListboxProps
105
+ queryKey,
106
+ ListboxProps,
107
+ queryOptions
106
108
  } = _b, rest = __objRest(_b, [
107
109
  "loadOptions",
108
110
  "limit",
109
- "ListboxProps"
111
+ "queryKey",
112
+ "ListboxProps",
113
+ "queryOptions"
110
114
  ]);
111
- const [page, setPage] = useState(0);
112
- const [options, setOptions] = useState([]);
113
- const [loading, setLoading] = useState(false);
114
- const [hasMore, setHasMore] = useState(true);
115
- useEffect(() => {
116
- const getInitialOptions = () => __async(void 0, null, function* () {
117
- setLoading(true);
118
- const result = yield loadOptions(page, limit);
119
- setOptions(result.options);
120
- setHasMore(result.hasMore);
121
- setPage((prev) => prev + 1);
122
- setLoading(false);
123
- });
124
- if (!loading && hasMore && page === 0) {
125
- getInitialOptions();
126
- }
127
- }, [page, loading, loadOptions]);
115
+ const { isLoading, isFetching, data, hasNextPage, fetchNextPage } = useInfiniteQuery(__spreadValues({
116
+ queryKey: [queryKey, limit],
117
+ queryFn: (_0) => __async(void 0, [_0], function* ({ pageParam = 0 }) {
118
+ return loadOptions(pageParam, limit);
119
+ }),
120
+ staleTime: 1e4,
121
+ getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.offset + limit : false
122
+ }, queryOptions));
123
+ const options = (data == null ? void 0 : data.pages) ? data.pages.map((page) => page.options).flat() : [];
128
124
  return /* @__PURE__ */ jsx2(
129
125
  Autocomplete,
130
126
  __spreadProps(__spreadValues({}, rest), {
131
- loading,
127
+ loading: isFetching,
132
128
  options,
133
129
  ListboxProps: __spreadProps(__spreadValues({}, ListboxProps), {
134
130
  onScroll: (event) => __async(void 0, null, function* () {
135
131
  const listboxNode = event.currentTarget;
136
132
  const difference = listboxNode.scrollHeight - (listboxNode.scrollTop + listboxNode.clientHeight);
137
- if (difference <= 5 && !loading && hasMore) {
138
- setLoading(true);
139
- const result = yield loadOptions(page, limit);
140
- setOptions([...options, ...result.options]);
141
- setHasMore(result.hasMore);
142
- setPage((prev) => prev + 1);
143
- setLoading(false);
133
+ if (difference <= 5 && !isLoading && !isFetching && hasNextPage) {
134
+ fetchNextPage();
144
135
  }
145
136
  })
146
137
  })
@@ -152,28 +143,35 @@ var AsyncAutocomplete = (_a) => {
152
143
  import { avOrganizationsApi } from "@availity/api-axios";
153
144
  import { jsx as jsx3 } from "react/jsx-runtime";
154
145
  var fetchOrgs = (config) => __async(void 0, null, function* () {
155
- try {
156
- const resp = yield avOrganizationsApi.getOrganizations(config);
157
- return {
158
- options: resp.data.organizations,
159
- hasMore: config.params.offset + config.params.limit < resp.data.totalCount
160
- };
161
- } catch (e) {
162
- return {
163
- options: [],
164
- hasMore: false
165
- };
166
- }
146
+ const resp = yield avOrganizationsApi.getOrganizations(config);
147
+ return {
148
+ options: resp.data.organizations,
149
+ hasMore: config.params.offset + config.params.limit < resp.data.totalCount,
150
+ offset: config.params.offset
151
+ };
167
152
  });
168
153
  var OrganizationAutocomplete = (_a) => {
169
- var _b = _a, { apiConfig = {} } = _b, rest = __objRest(_b, ["apiConfig"]);
170
- const handleLoadOptions = (page, limit) => __async(void 0, null, function* () {
171
- const offset = page * limit;
154
+ var _b = _a, {
155
+ apiConfig = {},
156
+ queryKey = "org-autocomplete"
157
+ } = _b, rest = __objRest(_b, [
158
+ "apiConfig",
159
+ "queryKey"
160
+ ]);
161
+ const handleLoadOptions = (offset, limit) => __async(void 0, null, function* () {
172
162
  const resp = yield fetchOrgs(__spreadProps(__spreadValues({}, apiConfig), { params: __spreadProps(__spreadValues({ dropdown: true }, apiConfig.params), { offset, limit }) }));
173
163
  return resp;
174
164
  });
175
165
  const handleGetOptionLabel = (org) => org.name;
176
- return /* @__PURE__ */ jsx3(AsyncAutocomplete, __spreadProps(__spreadValues({ getOptionLabel: handleGetOptionLabel }, rest), { loadOptions: handleLoadOptions }));
166
+ return /* @__PURE__ */ jsx3(
167
+ AsyncAutocomplete,
168
+ __spreadProps(__spreadValues({
169
+ getOptionLabel: handleGetOptionLabel,
170
+ queryKey
171
+ }, rest), {
172
+ loadOptions: handleLoadOptions
173
+ })
174
+ );
177
175
  };
178
176
  export {
179
177
  AsyncAutocomplete,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@availity/mui-autocomplete",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Availity MUI Autocomplete Component - part of the @availity/element design system",
5
5
  "keywords": [
6
6
  "react",
@@ -36,10 +36,11 @@
36
36
  "@mui/types": "^7.2.14"
37
37
  },
38
38
  "devDependencies": {
39
- "@availity/api-axios": "^8.0.7",
40
- "@availity/mui-form-utils": "^0.11.2",
41
- "@availity/mui-textfield": "^0.5.20",
39
+ "@availity/api-axios": "^8.0.8",
40
+ "@availity/mui-form-utils": "^0.11.3",
41
+ "@availity/mui-textfield": "^0.5.21",
42
42
  "@mui/material": "^5.15.15",
43
+ "@tanstack/react-query": "^4.36.1",
43
44
  "react": "18.2.0",
44
45
  "react-dom": "18.2.0",
45
46
  "tsup": "^8.0.2",
@@ -47,9 +48,10 @@
47
48
  },
48
49
  "peerDependencies": {
49
50
  "@availity/api-axios": "^8.0.7",
50
- "@availity/mui-form-utils": "^0.11.2",
51
- "@availity/mui-textfield": "^0.5.20",
51
+ "@availity/mui-form-utils": "^0.11.3",
52
+ "@availity/mui-textfield": "^0.5.21",
52
53
  "@mui/material": "^5.11.9",
54
+ "@tanstack/react-query": "^4.36.1",
53
55
  "react": ">=16.3.0"
54
56
  },
55
57
  "publishConfig": {
@@ -1,6 +1,7 @@
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 AvApi, { ApiConfig } from '@availity/api-axios';
4
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
5
 
5
6
  import { AsyncAutocomplete } from './AsyncAutocomplete';
6
7
 
@@ -35,42 +36,46 @@ type ExampleResponse = {
35
36
  count: number;
36
37
  };
37
38
 
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: {} });
39
+ const getResults = async (offset: number, limit: number) => {
40
+ // const offset = page * limit;
41
+ const resp = await api.post<ExampleResponse>({ offset, limit }, { params: {} });
42
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
- }
43
+ return {
44
+ totalCount: resp.data.totalCount,
45
+ offset,
46
+ limit,
47
+ options: resp.data.options,
48
+ count: resp.data.count,
49
+ };
59
50
  };
60
51
 
61
- const loadOptions = async (page: number, limit: number) => {
62
- const { options, totalCount, offset } = await getResults(page, limit);
52
+ const loadOptions = async (offset: number, limit: number) => {
53
+ const { options, totalCount } = await getResults(offset, limit);
63
54
 
64
55
  return {
65
56
  options,
66
57
  hasMore: offset + limit < totalCount,
58
+ offset,
67
59
  };
68
60
  };
69
61
 
62
+ const client = new QueryClient({
63
+ defaultOptions: {
64
+ queries: {
65
+ refetchOnWindowFocus: false,
66
+ },
67
+ },
68
+ });
69
+
70
70
  export const _Async: StoryObj<typeof AsyncAutocomplete> = {
71
71
  render: (args) => {
72
- return <AsyncAutocomplete {...args} />;
72
+ return (
73
+ <QueryClientProvider client={client}>
74
+ <AsyncAutocomplete {...args} />
75
+ </QueryClientProvider>
76
+ );
73
77
  },
78
+ decorators: [],
74
79
  parameters: {
75
80
  controls: {
76
81
  exclude: /loading(?!Text)|options/,
@@ -81,5 +86,6 @@ export const _Async: StoryObj<typeof AsyncAutocomplete> = {
81
86
  getOptionLabel: (val: Option) => val.label,
82
87
  loadOptions,
83
88
  limit: 10,
89
+ queryKey: 'example',
84
90
  },
85
91
  };
@@ -2,11 +2,14 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
2
2
  import AvApi, { ApiConfig } from '@availity/api-axios';
3
3
  /* eslint-disable @nx/enforce-module-boundaries */
4
4
  import { server } from '@availity/mock/src/lib/server';
5
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
6
 
6
7
  import { AsyncAutocomplete } from './AsyncAutocomplete';
7
8
 
8
9
  const api = new AvApi({ name: 'example' } as ApiConfig);
9
10
 
11
+ const client = new QueryClient();
12
+
10
13
  type Option = {
11
14
  label: string;
12
15
  value: number;
@@ -18,8 +21,7 @@ type ExampleResponse = {
18
21
  count: number;
19
22
  };
20
23
 
21
- const getResults = async (page: number, limit: number) => {
22
- const offset = page * limit;
24
+ const getResults = async (offset: number, limit: number) => {
23
25
  try {
24
26
  const resp = await api.post<ExampleResponse>({ offset, limit }, { params: {} });
25
27
 
@@ -41,12 +43,13 @@ const getResults = async (page: number, limit: number) => {
41
43
  }
42
44
  };
43
45
 
44
- const loadOptions = async (page: number, limit: number) => {
45
- const { options, totalCount, offset } = await getResults(page, limit);
46
+ const loadOptions = async (offset: number, limit: number) => {
47
+ const { options, totalCount } = await getResults(offset, limit);
46
48
 
47
49
  return {
48
50
  options,
49
51
  hasMore: offset + limit < totalCount,
52
+ offset,
50
53
  };
51
54
  };
52
55
 
@@ -64,13 +67,21 @@ describe('AsyncAutocomplete', () => {
64
67
  });
65
68
 
66
69
  test('should render successfully', () => {
67
- const { getByLabelText } = render(<AsyncAutocomplete FieldProps={{ label: 'Test' }} loadOptions={loadOptions} />);
70
+ const { getByLabelText } = render(
71
+ <QueryClientProvider client={client}>
72
+ <AsyncAutocomplete queryKey="test" FieldProps={{ label: 'Test' }} loadOptions={loadOptions} />
73
+ </QueryClientProvider>
74
+ );
68
75
 
69
76
  expect(getByLabelText('Test')).toBeTruthy();
70
77
  });
71
78
 
72
79
  test('options should be available', async () => {
73
- render(<AsyncAutocomplete loadOptions={loadOptions} FieldProps={{ label: 'Test' }} />);
80
+ render(
81
+ <QueryClientProvider client={client}>
82
+ <AsyncAutocomplete queryKey="test1" loadOptions={loadOptions} FieldProps={{ label: 'Test' }} />
83
+ </QueryClientProvider>
84
+ );
74
85
 
75
86
  const input = screen.getByRole('combobox');
76
87
  fireEvent.click(input);
@@ -88,7 +99,11 @@ describe('AsyncAutocomplete', () => {
88
99
  });
89
100
 
90
101
  test('should call loadOptions when scroll to the bottom', async () => {
91
- render(<AsyncAutocomplete loadOptions={loadOptions} limit={10} FieldProps={{ label: 'Test' }} />);
102
+ render(
103
+ <QueryClientProvider client={client}>
104
+ <AsyncAutocomplete queryKey="test2" loadOptions={loadOptions} limit={10} FieldProps={{ label: 'Test' }} />
105
+ </QueryClientProvider>
106
+ );
92
107
 
93
108
  const input = screen.getByRole('combobox');
94
109
  fireEvent.click(input);
@@ -1,5 +1,5 @@
1
- import { useState, useEffect } from 'react';
2
1
  import type { ChipTypeMap } from '@mui/material/Chip';
2
+ import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query';
3
3
 
4
4
  import { Autocomplete, AutocompleteProps } from './Autocomplete';
5
5
 
@@ -13,11 +13,15 @@ export interface AsyncAutocompleteProps<
13
13
  AutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>,
14
14
  'options' | 'disableListWrap' | 'loading'
15
15
  > {
16
- /** Function that returns a promise with options and hasMore */
17
- loadOptions: (page: number, limit: number) => Promise<{ options: Option[]; hasMore: boolean }>;
16
+ /** Function that is called to fetch the options for the list. Returns a promise with options, hasMore, and offset */
17
+ loadOptions: (offset: number, limit: number) => Promise<{ options: Option[]; hasMore: boolean; offset: number }>;
18
+ /** The key used by @tanstack/react-query to cache the response */
19
+ queryKey: string;
18
20
  /** The number of options to request from the api
19
21
  * @default 50 */
20
22
  limit?: number;
23
+ /** Config options for the useInfiniteQuery hook */
24
+ queryOptions?: UseInfiniteQueryOptions<{ options: Option[]; hasMore: boolean; offset: number }>;
21
25
  }
22
26
 
23
27
  export const AsyncAutocomplete = <
@@ -29,33 +33,25 @@ export const AsyncAutocomplete = <
29
33
  >({
30
34
  loadOptions,
31
35
  limit = 50,
36
+ queryKey,
32
37
  ListboxProps,
38
+ queryOptions,
33
39
  ...rest
34
40
  }: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => {
35
- const [page, setPage] = useState(0);
36
- const [options, setOptions] = useState<Option[]>([]);
37
- const [loading, setLoading] = useState(false);
38
- const [hasMore, setHasMore] = useState(true);
41
+ const { isLoading, isFetching, data, hasNextPage, fetchNextPage } = useInfiniteQuery({
42
+ queryKey: [queryKey, limit],
43
+ queryFn: async ({ pageParam = 0 }) => loadOptions(pageParam, limit),
44
+ staleTime: 10000,
45
+ getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.offset + limit : false),
46
+ ...queryOptions,
47
+ });
39
48
 
40
- useEffect(() => {
41
- const getInitialOptions = async () => {
42
- setLoading(true);
43
- const result = await loadOptions(page, limit);
44
- setOptions(result.options);
45
- setHasMore(result.hasMore);
46
- setPage((prev) => prev + 1);
47
- setLoading(false);
48
- };
49
-
50
- if (!loading && hasMore && page === 0) {
51
- getInitialOptions();
52
- }
53
- }, [page, loading, loadOptions]);
49
+ const options = data?.pages ? data.pages.map((page) => page.options).flat() : [];
54
50
 
55
51
  return (
56
52
  <Autocomplete
57
53
  {...rest}
58
- loading={loading}
54
+ loading={isFetching}
59
55
  options={options}
60
56
  ListboxProps={{
61
57
  ...ListboxProps,
@@ -64,13 +60,8 @@ export const AsyncAutocomplete = <
64
60
  const difference = listboxNode.scrollHeight - (listboxNode.scrollTop + listboxNode.clientHeight);
65
61
 
66
62
  // Only fetch if we are near the bottom, not already fetching, and there are more results
67
- if (difference <= 5 && !loading && hasMore) {
68
- setLoading(true);
69
- const result = await loadOptions(page, limit);
70
- setOptions([...options, ...result.options]);
71
- setHasMore(result.hasMore);
72
- setPage((prev) => prev + 1);
73
- setLoading(false);
63
+ if (difference <= 5 && !isLoading && !isFetching && hasNextPage) {
64
+ fetchNextPage();
74
65
  }
75
66
  },
76
67
  }}
@@ -1,5 +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
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
4
 
4
5
  import { OrganizationAutocomplete } from './OrganizationAutocomplete';
5
6
 
@@ -21,9 +22,21 @@ const meta: Meta<typeof OrganizationAutocomplete> = {
21
22
 
22
23
  export default meta;
23
24
 
25
+ const client = new QueryClient({
26
+ defaultOptions: {
27
+ queries: {
28
+ refetchOnWindowFocus: false,
29
+ },
30
+ },
31
+ });
32
+
24
33
  export const _OrganizationAutocomplete: StoryObj<typeof OrganizationAutocomplete> = {
25
34
  render: (args) => {
26
- return <OrganizationAutocomplete {...args} />;
35
+ return (
36
+ <QueryClientProvider client={client}>
37
+ <OrganizationAutocomplete {...args} />
38
+ </QueryClientProvider>
39
+ );
27
40
  },
28
41
  args: {
29
42
  FieldProps: {
@@ -1,9 +1,12 @@
1
1
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
2
  /* eslint-disable @nx/enforce-module-boundaries */
3
3
  import { server } from '@availity/mock/src/lib/server';
4
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
5
 
5
6
  import { OrganizationAutocomplete } from './OrganizationAutocomplete';
6
7
 
8
+ const client = new QueryClient();
9
+
7
10
  describe('OrganizationAutocomplete', () => {
8
11
  beforeAll(() => {
9
12
  // Start the interception.
@@ -15,10 +18,15 @@ describe('OrganizationAutocomplete', () => {
15
18
  // in individual tests (runtime handlers).
16
19
  server.resetHandlers();
17
20
  jest.restoreAllMocks();
21
+ client.clear();
18
22
  });
19
23
 
20
24
  test('organizations are fetched and displayed by name', async () => {
21
- render(<OrganizationAutocomplete FieldProps={{ label: 'Test' }} />);
25
+ render(
26
+ <QueryClientProvider client={client}>
27
+ <OrganizationAutocomplete FieldProps={{ label: 'Test' }} />
28
+ </QueryClientProvider>
29
+ );
22
30
 
23
31
  const input = screen.getByRole('combobox');
24
32
  fireEvent.click(input);
@@ -2,6 +2,7 @@ import { avOrganizationsApi, ApiConfig } from '@availity/api-axios';
2
2
  import type { ChipTypeMap } from '@mui/material/Chip';
3
3
 
4
4
  import { AsyncAutocomplete, AsyncAutocompleteProps } from './AsyncAutocomplete';
5
+ import type { Optional } from './util';
5
6
 
6
7
  export type Organization = {
7
8
  customerId: string;
@@ -11,20 +12,14 @@ export type Organization = {
11
12
  links: Record<string, Record<string, string>>;
12
13
  };
13
14
 
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
- }
15
+ const fetchOrgs = async (config: ApiConfig): Promise<{ options: Organization[]; hasMore: boolean; offset: number }> => {
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
+ offset: config.params.offset,
22
+ };
28
23
  };
29
24
 
30
25
  export interface OrgAutocompleteProps<
@@ -33,14 +28,19 @@ export interface OrgAutocompleteProps<
33
28
  DisableClearable extends boolean | undefined = false,
34
29
  FreeSolo extends boolean | undefined = false,
35
30
  ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
36
- > extends Omit<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'loadOptions'> {
31
+ > extends Omit<
32
+ Optional<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'queryKey'>,
33
+ 'loadOptions'
34
+ > {
37
35
  apiConfig?: ApiConfig;
38
36
  }
39
37
 
40
- export const OrganizationAutocomplete = ({ apiConfig = {}, ...rest }: OrgAutocompleteProps) => {
41
- const handleLoadOptions = async (page: number, limit: number) => {
42
- const offset = page * limit;
43
-
38
+ export const OrganizationAutocomplete = ({
39
+ apiConfig = {},
40
+ queryKey = 'org-autocomplete',
41
+ ...rest
42
+ }: OrgAutocompleteProps) => {
43
+ const handleLoadOptions = async (offset: number, limit: number) => {
44
44
  const resp = await fetchOrgs({ ...apiConfig, params: { dropdown: true, ...apiConfig.params, offset, limit } });
45
45
 
46
46
  return resp;
@@ -48,5 +48,12 @@ export const OrganizationAutocomplete = ({ apiConfig = {}, ...rest }: OrgAutocom
48
48
 
49
49
  const handleGetOptionLabel = (org: Organization) => org.name;
50
50
 
51
- return <AsyncAutocomplete getOptionLabel={handleGetOptionLabel} {...rest} loadOptions={handleLoadOptions} />;
51
+ return (
52
+ <AsyncAutocomplete
53
+ getOptionLabel={handleGetOptionLabel}
54
+ queryKey={queryKey}
55
+ {...rest}
56
+ loadOptions={handleLoadOptions}
57
+ />
58
+ );
52
59
  };
@@ -0,0 +1,51 @@
1
+ // Each exported component in the package should have its own stories file
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+
5
+ import { ProviderAutocomplete } from './ProviderAutocomplete';
6
+
7
+ const meta: Meta<typeof ProviderAutocomplete> = {
8
+ title: 'Form Components/Autocomplete/ProviderAutocomplete',
9
+ component: ProviderAutocomplete,
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 client = new QueryClient({
26
+ defaultOptions: {
27
+ queries: {
28
+ refetchOnWindowFocus: false,
29
+ },
30
+ },
31
+ });
32
+
33
+ export const _ProviderAutocomplete: StoryObj<typeof ProviderAutocomplete> = {
34
+ render: (args) => {
35
+ return (
36
+ <QueryClientProvider client={client}>
37
+ <ProviderAutocomplete {...args} />
38
+ </QueryClientProvider>
39
+ );
40
+ },
41
+ args: {
42
+ FieldProps: {
43
+ label: 'Provider Select',
44
+ helperText: 'Select a Provider from the list',
45
+ placeholder: 'Select...',
46
+ fullWidth: false,
47
+ },
48
+ limit: 10,
49
+ customerId: '1234',
50
+ },
51
+ };
@@ -0,0 +1,55 @@
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
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
+
6
+ import { ProviderAutocomplete } from './ProviderAutocomplete';
7
+
8
+ const client = new QueryClient();
9
+
10
+ describe('ProviderAutocomplete', () => {
11
+ beforeAll(() => {
12
+ // Start the interception.
13
+ server.listen();
14
+ });
15
+
16
+ afterEach(() => {
17
+ // Remove any handlers you may have added
18
+ // in individual tests (runtime handlers).
19
+ server.resetHandlers();
20
+ jest.restoreAllMocks();
21
+ client.clear();
22
+ });
23
+
24
+ test('providers are fetched and displayed by name', async () => {
25
+ render(
26
+ <QueryClientProvider client={client}>
27
+ <ProviderAutocomplete customerId="123" FieldProps={{ label: 'Test' }} />
28
+ </QueryClientProvider>
29
+ );
30
+
31
+ const input = screen.getByRole('combobox');
32
+ fireEvent.click(input);
33
+ fireEvent.keyDown(input, { key: 'ArrowDown' });
34
+
35
+ await waitFor(() => {
36
+ expect(screen.getByText('Provider 1')).toBeDefined();
37
+ });
38
+ });
39
+
40
+ test('providers are not fetched when customerId is not present', async () => {
41
+ render(
42
+ <QueryClientProvider client={client}>
43
+ <ProviderAutocomplete customerId="" FieldProps={{ label: 'Test' }} />
44
+ </QueryClientProvider>
45
+ );
46
+
47
+ const input = screen.getByRole('combobox');
48
+ fireEvent.click(input);
49
+ fireEvent.keyDown(input, { key: 'ArrowDown' });
50
+
51
+ await waitFor(() => {
52
+ expect(screen.getByText('No options')).toBeDefined();
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,64 @@
1
+ import { avProvidersApi, ApiConfig } from '@availity/api-axios';
2
+ import type { ChipTypeMap } from '@mui/material/Chip';
3
+
4
+ import { AsyncAutocomplete, AsyncAutocompleteProps } from './AsyncAutocomplete';
5
+ import type { Optional } from './util';
6
+
7
+ export type Provider = {
8
+ id: string;
9
+ businessName: string;
10
+ uiDisplayName: string;
11
+ aytypical: boolean;
12
+ };
13
+
14
+ const fetchProviders = async (
15
+ customerId: string,
16
+ config: ApiConfig
17
+ ): Promise<{ options: Provider[]; hasMore: boolean; offset: number }> => {
18
+ const resp = await avProvidersApi.getProviders(customerId, config);
19
+
20
+ return {
21
+ options: resp.data.providers as Provider[],
22
+ hasMore: config.params.offset + config.params.limit < resp.data.totalCount,
23
+ offset: config.params.offset,
24
+ };
25
+ };
26
+
27
+ export interface ProviderAutocompleteProps<
28
+ Option = Provider,
29
+ Multiple extends boolean | undefined = false,
30
+ DisableClearable extends boolean | undefined = false,
31
+ FreeSolo extends boolean | undefined = false,
32
+ ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
33
+ > extends Omit<
34
+ Optional<AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'queryKey'>,
35
+ 'loadOptions'
36
+ > {
37
+ customerId: string;
38
+ apiConfig?: ApiConfig;
39
+ }
40
+
41
+ export const ProviderAutocomplete = ({
42
+ apiConfig = {},
43
+ customerId,
44
+ queryKey = 'prov-autocomplete',
45
+ ...rest
46
+ }: ProviderAutocompleteProps) => {
47
+ const handleLoadOptions = async (offset: number, limit: number) => {
48
+ const resp = await fetchProviders(customerId, { ...apiConfig, params: { ...apiConfig.params, offset, limit } });
49
+
50
+ return resp;
51
+ };
52
+
53
+ const handleGetOptionLabel = (option: Provider) => option.uiDisplayName;
54
+
55
+ return (
56
+ <AsyncAutocomplete
57
+ getOptionLabel={handleGetOptionLabel}
58
+ queryOptions={{ enabled: !!customerId }}
59
+ queryKey={queryKey}
60
+ {...rest}
61
+ loadOptions={handleLoadOptions}
62
+ />
63
+ );
64
+ };
@@ -0,0 +1,2 @@
1
+ // Mark a field as optional in an already defined type
2
+ export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;