@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 +18 -0
- package/README.md +28 -6
- package/dist/index.d.mts +18 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +42 -44
- package/dist/index.mjs +42 -44
- package/package.json +8 -6
- package/src/lib/AsyncAutocomplete.stories.tsx +29 -23
- package/src/lib/AsyncAutocomplete.test.tsx +22 -7
- package/src/lib/AsyncAutocomplete.tsx +20 -29
- package/src/lib/OrganizationAutocomplete.stories.tsx +14 -1
- package/src/lib/OrganizationAutocomplete.test.tsx +9 -1
- package/src/lib/OrganizationAutocomplete.tsx +27 -20
- package/src/lib/ProviderAutocomplete.stories.tsx +51 -0
- package/src/lib/ProviderAutocomplete.test.tsx +55 -0
- package/src/lib/ProviderAutocomplete.tsx +64 -0
- package/src/lib/util.ts +2 -0
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.
|
|
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 (
|
|
131
|
-
const response = await callApi(
|
|
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
|
|
17
|
-
loadOptions: (
|
|
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
|
|
17
|
-
loadOptions: (
|
|
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
|
|
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
|
-
|
|
138
|
+
queryKey,
|
|
139
|
+
ListboxProps,
|
|
140
|
+
queryOptions
|
|
139
141
|
} = _b, rest = __objRest(_b, [
|
|
140
142
|
"loadOptions",
|
|
141
143
|
"limit",
|
|
142
|
-
"
|
|
144
|
+
"queryKey",
|
|
145
|
+
"ListboxProps",
|
|
146
|
+
"queryOptions"
|
|
143
147
|
]);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 && !
|
|
171
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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, {
|
|
203
|
-
|
|
204
|
-
|
|
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)(
|
|
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 {
|
|
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
|
-
|
|
105
|
+
queryKey,
|
|
106
|
+
ListboxProps,
|
|
107
|
+
queryOptions
|
|
106
108
|
} = _b, rest = __objRest(_b, [
|
|
107
109
|
"loadOptions",
|
|
108
110
|
"limit",
|
|
109
|
-
"
|
|
111
|
+
"queryKey",
|
|
112
|
+
"ListboxProps",
|
|
113
|
+
"queryOptions"
|
|
110
114
|
]);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 && !
|
|
138
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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, {
|
|
170
|
-
|
|
171
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
40
|
-
"@availity/mui-form-utils": "^0.11.
|
|
41
|
-
"@availity/mui-textfield": "^0.5.
|
|
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.
|
|
51
|
-
"@availity/mui-textfield": "^0.5.
|
|
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 (
|
|
39
|
-
const offset = page * limit;
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 (
|
|
62
|
-
const { options, totalCount
|
|
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
|
|
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 (
|
|
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 (
|
|
45
|
-
const { options, totalCount
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
17
|
-
loadOptions: (
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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={
|
|
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 && !
|
|
68
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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<
|
|
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 = ({
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
+
};
|
package/src/lib/util.ts
ADDED