From 7d86c8d5ca2b8f492740749f6c59ade0cc33ee06 Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:14:02 +0200 Subject: [PATCH] feat: add infinite scroll support to InputSelect component and use it in FiltersBar --- packages/app-elements/package.json | 1 + .../InputSelect/AsyncPaginateComponent.tsx | 173 +++++++++++++++++ .../forms/InputSelect/CreatableComponent.tsx | 4 +- .../InputSelect/GenericAsyncComponent.tsx | 10 +- .../forms/InputSelect/HookedInputSelect.tsx | 15 +- .../ui/forms/InputSelect/InputSelect.test.tsx | 96 +++++++++ .../src/ui/forms/InputSelect/InputSelect.tsx | 80 +++++++- .../ui/forms/InputSelect/SelectComponent.tsx | 4 +- .../src/ui/forms/InputSelect/index.tsx | 4 + .../src/ui/forms/InputSelect/overrides.tsx | 14 ++ .../useResourceFilters/FieldOptionsSelect.tsx | 17 +- .../useResourceFilters/FiltersBarSelect.tsx | 32 ++- .../useResourceSelectOptions.ts | 70 +++++-- .../stories/forms/ui/InputSelect.stories.tsx | 45 ++++- pnpm-lock.yaml | 183 ++++++++++++------ 15 files changed, 637 insertions(+), 111 deletions(-) create mode 100644 packages/app-elements/src/ui/forms/InputSelect/AsyncPaginateComponent.tsx diff --git a/packages/app-elements/package.json b/packages/app-elements/package.json index bb8386de5..4c68299e6 100644 --- a/packages/app-elements/package.json +++ b/packages/app-elements/package.json @@ -72,6 +72,7 @@ "react-hook-form": "7.62.0", "react-i18next": "^15.7.4", "react-select": "^5.10.2", + "react-select-async-paginate": "^0.7.11", "react-toastify": "^11.1.0", "react-tooltip": "^5.30.1", "stable-hash": "^0.0.6", diff --git a/packages/app-elements/src/ui/forms/InputSelect/AsyncPaginateComponent.tsx b/packages/app-elements/src/ui/forms/InputSelect/AsyncPaginateComponent.tsx new file mode 100644 index 000000000..921cdd289 --- /dev/null +++ b/packages/app-elements/src/ui/forms/InputSelect/AsyncPaginateComponent.tsx @@ -0,0 +1,173 @@ +import { forwardRef } from "react" +import type { + GroupBase, + OptionsOrGroups, + SelectInstance, + StylesConfig, +} from "react-select" +import { + AsyncPaginate, + type LoadOptions, + type ReduceOptions, + wrapMenuList, +} from "react-select-async-paginate" +import type { + InputSelectBaseProps, + InputSelectValue, + LoadAsyncValuesPaginated, +} from "./InputSelect" +import overrides from "./overrides" + +/** + * What the loader carries from one page to the next. + * + * The library calls it `additional` and hands it back untouched on the following + * request, which is where the page cursor lives. + */ +interface Additional { + page: number +} + +type Options = OptionsOrGroups> + +export interface AsyncPaginateSelectComponentProps + extends Omit< + InputSelectBaseProps, + "label" | "hint" | "asTextSearch" | "isCreatable" + > { + loadAsyncValues: LoadAsyncValuesPaginated + styles: StylesConfig +} + +/** + * The library wraps `MenuList` to watch its scroll position, but its own + * wrapping runs *before* the components we pass in, so ours would replace it and + * take the scroll listener with it. Wrapping ours here keeps both: the footer and + * the styling we override, plus the scroll detection paging depends on. + */ +const components = { + ...overrides, + MenuList: wrapMenuList(overrides.MenuList), +} + +const componentsWithoutDropdownIndicator = { + ...components, + DropdownIndicator: null, +} + +/** + * react-select hands any prop it does not know to `selectProps`, which is how the + * `MenuList` override tells "loading the next page" — a footer under the options + * already shown — apart from "loading the first one", which react-select covers + * on its own. Spread rather than written as an attribute, so it is not rejected + * as an unknown JSX prop. + */ +const paginatedSelectProps = { isPaginated: true } + +/** + * The async select that loads the next page as its menu is scrolled, instead of + * leaving everything past the first page reachable only by typing. + * + * Options are always what the server returned — client-side filtering stays off, + * as filtering a list that is only partly loaded would quietly hide matches + * sitting on a page that has not been fetched yet. + */ +export const AsyncPaginateSelectComponent = forwardRef< + SelectInstance>, + AsyncPaginateSelectComponentProps +>( + ( + { + onSelect, + noOptionsMessage, + // deliberately dropped: see the note on paging from the first page below + initialValues: _initialValues, + isOptionDisabled, + loadAsyncValues, + hideDropdownIndicator = false, + debounceMs = 500, + ...rest + }, + ref, + ) => { + const loadOptions: LoadOptions< + InputSelectValue, + GroupBase, + Additional + > = async (inputValue, _loadedOptions, additional) => { + const page = additional?.page ?? 1 + const { options, hasMore } = await loadAsyncValues(inputValue, { page }) + + return { + options, + hasMore, + additional: { page: page + 1 }, + } + } + + return ( + , + Additional, + boolean + > + {...rest} + {...paginatedSelectProps} + selectRef={ref} + loadOptions={loadOptions} + // Paging always starts at the first page, and `initialValues` seeds + // nothing. + // + // Seeding would mean claiming those values *are* the first page, which + // the component cannot know: a caller composes them freely, and even the + // filters bar can hand over a lone selected record that resolved before + // the list did. Starting from the second page in that case would skip the + // first one for good. So the menu fetches page one when it opens, and + // `initialValues` keeps to what it is elsewhere — the labels the closed + // control needs. + additional={{ page: 1 }} + reduceOptions={reduceOptions} + debounceTimeout={debounceMs} + closeMenuOnSelect={rest.isMulti !== true} + isOptionDisabled={isOptionDisabled} + onChange={onSelect} + noOptionsMessage={() => noOptionsMessage} + components={ + hideDropdownIndicator + ? componentsWithoutDropdownIndicator + : components + } + classNames={{ + control: (state) => (state.isFocused ? "z-[101]" : ""), + }} + /> + ) + }, +) + +/** + * Appends a page to what the menu already shows, dropping anything already + * there. + * + * A duplicate is not hypothetical: the selected option is fetched on its own so + * that its label resolves, and it turns up again once its own page is reached. + */ +const reduceOptions: ReduceOptions< + InputSelectValue, + GroupBase, + Additional +> = (prevOptions: Options, loadedOptions: Options): Options => { + const seen = new Set( + prevOptions.map((option) => ("value" in option ? option.value : undefined)), + ) + + return [ + ...prevOptions, + ...loadedOptions.filter( + (option) => !("value" in option) || !seen.has(option.value), + ), + ] +} + +AsyncPaginateSelectComponent.displayName = "AsyncPaginateSelectComponent" diff --git a/packages/app-elements/src/ui/forms/InputSelect/CreatableComponent.tsx b/packages/app-elements/src/ui/forms/InputSelect/CreatableComponent.tsx index fa593335a..0526e8c78 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/CreatableComponent.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/CreatableComponent.tsx @@ -1,11 +1,11 @@ import { forwardRef } from "react" import type { GroupBase, SelectInstance, StylesConfig } from "react-select" import CreatableSelect from "react-select/creatable" -import type { InputSelectProps, InputSelectValue } from "./InputSelect" +import type { InputSelectBaseProps, InputSelectValue } from "./InputSelect" import components from "./overrides" export interface CreatableComponentProps - extends Omit { + extends Omit { styles: StylesConfig } diff --git a/packages/app-elements/src/ui/forms/InputSelect/GenericAsyncComponent.tsx b/packages/app-elements/src/ui/forms/InputSelect/GenericAsyncComponent.tsx index 2798726f9..774871565 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/GenericAsyncComponent.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/GenericAsyncComponent.tsx @@ -15,11 +15,11 @@ import type { } from "react-select" import type AsyncSelect from "react-select/async" import type AsyncCreatableSelect from "react-select/async-creatable" -import type { SetRequired } from "type-fest" import type { GroupedSelectValues, - InputSelectProps, + InputSelectBaseProps, InputSelectValue, + LoadAsyncValues, } from "./InputSelect" import components from "./overrides" import { isSingleValueSelected } from "./utils" @@ -52,10 +52,8 @@ interface AsyncAdditionalProps> { } export interface GenericAsyncSelectComponentProps - extends Omit< - SetRequired, - "label" | "hint" - > { + extends Omit { + loadAsyncValues: LoadAsyncValues styles: StylesConfig } diff --git a/packages/app-elements/src/ui/forms/InputSelect/HookedInputSelect.tsx b/packages/app-elements/src/ui/forms/InputSelect/HookedInputSelect.tsx index 8c7554efb..a5d3379dc 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/HookedInputSelect.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/HookedInputSelect.tsx @@ -4,14 +4,15 @@ import type { GroupBase, SelectInstance } from "react-select" import { useValidationFeedback } from "../ReactHookForm" import { InputSelect, - type InputSelectProps, + type InputSelectAsyncProps, + type InputSelectBaseProps, type InputSelectValue, type PossibleSelectValue, } from "./InputSelect" import { flatSelectValues, getDefaultValueFromFlatten } from "./utils" -export interface HookedInputSelectProps - extends Omit { +interface HookedInputSelectOwnProps + extends Omit { /** * field name to match hook-form state */ @@ -40,6 +41,14 @@ export interface HookedInputSelectProps onSelect?: (value: PossibleSelectValue) => void } +/** + * The async half is intersected in rather than derived with `Omit`/`Pick`, as + * both collapse a union — and with it the rule that `infiniteScroll` requires the + * paginated loader. + */ +export type HookedInputSelectProps = HookedInputSelectOwnProps & + InputSelectAsyncProps + /** * `InputSelect` component ready to be used with the `react-hook-form` context. * Value to be stored in the field can be controlled from the `pathToValue` prop. diff --git a/packages/app-elements/src/ui/forms/InputSelect/InputSelect.test.tsx b/packages/app-elements/src/ui/forms/InputSelect/InputSelect.test.tsx index faf4f4627..c73e3896d 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/InputSelect.test.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/InputSelect.test.tsx @@ -147,4 +147,100 @@ describe("InputSelect", () => { container.querySelector('[class*="indicatorContainer"] svg'), ).not.toBeInTheDocument() }) + + describe("with infinite scroll", () => { + // jsdom gives every element a zero height, which the library reads as "the + // menu is already scrolled to its bottom" — so simply opening the menu is + // what asks for the next page here + const openMenu = (getByText: (text: string) => HTMLElement): void => { + fireEvent.keyDown(getByText("Please select an option"), { + key: "ArrowDown", + }) + } + + test("loads the following pages as the menu is scrolled", async () => { + const loadAsyncValues = vi.fn(async (_hint: string, { page }) => ({ + options: [{ value: `page-${page}`, label: `Page ${page}` }], + hasMore: page < 2, + })) + + const { getByText, queryByText } = render( + {}} + initialValues={[]} + placeholder="Please select an option" + infiniteScroll + loadAsyncValues={loadAsyncValues} + debounceMs={0} + />, + ) + + openMenu(getByText) + + await waitFor(() => { + expect(queryByText("Page 2")).toBeVisible() + }) + expect(queryByText("Page 1")).toBeVisible() + expect(loadAsyncValues.mock.calls.map(([, meta]) => meta.page)).toEqual([ + 1, 2, + ]) + }) + + // `initialValues` is whatever the caller composed — the labels the closed + // control needs — and never a claim about which page they are, so taking them + // for the first page could skip it for good + test("still starts from the first page when given initial values", async () => { + const loadAsyncValues = vi.fn(async (_hint: string, { page }) => ({ + options: [{ value: `page-${page}`, label: `Page ${page}` }], + hasMore: false, + })) + + const { getByText, queryByText } = render( + {}} + initialValues={[{ value: "chosen", label: "Chosen" }]} + placeholder="Please select an option" + infiniteScroll + loadAsyncValues={loadAsyncValues} + debounceMs={0} + />, + ) + + openMenu(getByText) + + await waitFor(() => { + expect(queryByText("Page 1")).toBeVisible() + }) + expect(loadAsyncValues.mock.calls.map(([, meta]) => meta.page)).toEqual([ + 1, + ]) + }) + + // the selected option is fetched on its own so its label resolves, and shows + // up again once its own page is reached + test("does not repeat an option already in the menu", async () => { + const loadAsyncValues = vi.fn(async (_hint: string, { page }) => ({ + options: [{ value: "paris", label: "Paris" }], + hasMore: page < 2, + })) + + const { getByText, queryAllByText } = render( + {}} + initialValues={[]} + placeholder="Please select an option" + infiniteScroll + loadAsyncValues={loadAsyncValues} + debounceMs={0} + />, + ) + + openMenu(getByText) + + await waitFor(() => { + expect(loadAsyncValues).toHaveBeenCalledTimes(2) + }) + expect(queryAllByText("Paris")).toHaveLength(1) + }) + }) }) diff --git a/packages/app-elements/src/ui/forms/InputSelect/InputSelect.tsx b/packages/app-elements/src/ui/forms/InputSelect/InputSelect.tsx index 9e74e8800..05cf5509b 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/InputSelect.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/InputSelect.tsx @@ -13,6 +13,7 @@ import { } from "#ui/internals/InputWrapper" import { AsyncSelectComponent } from "./AsyncComponent" import { AsyncCreatableSelectComponent } from "./AsyncCreatableComponent" +import { AsyncPaginateSelectComponent } from "./AsyncPaginateComponent" import { CreatableComponent, type CreatableComponentProps, @@ -37,7 +38,7 @@ export type PossibleSelectValue = | MultiValue | SingleValue -export interface InputSelectProps extends InputWrapperBaseProps { +export interface InputSelectBaseProps extends InputWrapperBaseProps { /** * Initial values to populate the select options. It can be a flat array of values or a grouped array. */ @@ -122,12 +123,6 @@ export interface InputSelectProps extends InputWrapperBaseProps { * CSS class name */ className?: string - /** - * Function to load async values on search - */ - loadAsyncValues?: ( - inputValue: string, - ) => Promise /** * Optional text to display at the bottom of the dropdown menu */ @@ -164,6 +159,58 @@ export interface InputSelectProps extends InputWrapperBaseProps { menuPortalTarget?: HTMLElement | null } +/** + * Loads the options matching what has been typed, all in one go. + */ +export type LoadAsyncValues = ( + inputValue: string, +) => Promise + +/** + * Loads one page of the options matching what has been typed. + * + * `hasMore` is what stops the paging: while it is `true` the menu asks for the + * page after the current one as it is scrolled to the bottom. + */ +export type LoadAsyncValuesPaginated = ( + inputValue: string, + meta: { page: number }, +) => Promise<{ options: InputSelectValue[]; hasMore: boolean }> + +/** + * How the options are loaded, which also decides which select is mounted. + * + * The two are kept apart rather than folded into one optional flag so that + * turning `infiniteScroll` on makes the paginated loader mandatory — a loader + * that ignores the page it is handed would silently return the first page + * forever. + */ +export type InputSelectAsyncProps = + | { + /** + * Load the next page of options as the menu is scrolled, so that a list + * longer than one page can be browsed instead of only searched. + * + * Opt-in: without it the select keeps loading options the way it always + * has, one non-paginated request per search. + */ + infiniteScroll?: false + /** + * Function to load async values on search + */ + loadAsyncValues?: LoadAsyncValues + } + | { + infiniteScroll: true + /** + * Function to load one page of async values. Called again with the next + * page as the menu is scrolled to the bottom. + */ + loadAsyncValues: LoadAsyncValuesPaginated + } + +export type InputSelectProps = InputSelectBaseProps & InputSelectAsyncProps + /** * Advanced select component with support for async options loading and multi-select. * It's a wrapper around `react-select` with a subset of props exposed. @@ -202,6 +249,7 @@ export const InputSelect = forwardRef< name, className, loadAsyncValues, + infiniteScroll, debounceMs, noOptionsMessage = t("common.no_results_found"), menuFooterText, @@ -242,12 +290,24 @@ export const InputSelect = forwardRef< name={name} {...rest} > - {loadAsyncValues != null && isCreatable === true ? ( + {loadAsyncValues != null && infiniteScroll === true ? ( + + ) : loadAsyncValues != null && isCreatable === true ? ( { + extends Omit { styles: StylesConfig } diff --git a/packages/app-elements/src/ui/forms/InputSelect/index.tsx b/packages/app-elements/src/ui/forms/InputSelect/index.tsx index dce7b7bd9..de0e4518f 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/index.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/index.tsx @@ -5,8 +5,12 @@ export { export { type GroupedSelectValues, InputSelect, + type InputSelectAsyncProps, + type InputSelectBaseProps, type InputSelectProps, type InputSelectValue, + type LoadAsyncValues, + type LoadAsyncValuesPaginated, type PossibleSelectValue, } from "./InputSelect" export { diff --git a/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx b/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx index 8cabf946a..fd1c5ef44 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx @@ -15,6 +15,7 @@ import { type SingleValueProps, type ValueContainerProps, } from "react-select" +import { t } from "#providers/I18NProvider" import { Hr } from "#ui/atoms/Hr" import { Spacer } from "#ui/atoms/Spacer" import { Tag } from "#ui/atoms/Tag" @@ -144,10 +145,23 @@ function MenuList(props: MenuListProps): JSX.Element { const isLoading = Boolean(props.isLoading) // @ts-expect-error I found no way to enhance `props.selectProps` definitions with custom ones specified in our wrapped `InputSelect` component const menuFooterText = props.selectProps.menuFooterText as string | undefined + // @ts-expect-error same as above: set by the paginated async select + const isPaginated = props.selectProps.isPaginated === true + + // A paginated menu keeps the options it already has while the next page is on + // its way, so the control's spinner is the only sign anything is happening and + // it sits far from where the user is looking. This says it at the bottom, right + // under the row that has just been scrolled past. + const isLoadingMore = isPaginated && isLoading && props.options.length > 0 return ( {props.children} + {isLoadingMore ? ( +
+ {t("common.loading")} +
+ ) : null} {menuFooterText != null && !isLoading && props.options.length > 0 ? (
{menuFooterText}
) : null} diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FieldOptionsSelect.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FieldOptionsSelect.tsx index 8c5747ab0..5884c3154 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/FieldOptionsSelect.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FieldOptionsSelect.tsx @@ -100,8 +100,13 @@ function ResourceOptionsSelect({ String(value), ) - const { initialValues, isLoading, recordCount, loadAsyncValues } = - useResourceSelectOptions({ props, selectedValues }) + const { + initialValues, + isLoading, + recordCount, + hasMorePages, + loadAsyncValues, + } = useResourceSelectOptions({ props, selectedValues }) // parity with `inputResourceGroup`: a filter over a single possible value is // not worth showing, unless the user already picked something @@ -113,6 +118,12 @@ function ResourceOptionsSelect({ return null } + // a list that fits in its first page is entirely loaded already, so it filters + // in place and never asks the server for anything + const paginationProps = hasMorePages + ? ({ infiniteScroll: true, loadAsyncValues } as const) + : {} + return ( ) } diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersBarSelect.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersBarSelect.tsx index 1a2708221..aae15ceec 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersBarSelect.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersBarSelect.tsx @@ -84,6 +84,8 @@ export function FiltersBarSelect({ isLoading, recordCount, hasResolvedSelection, + hasMorePages, + isSearchable, loadAsyncValues, } = useResourceSelectOptions({ props, @@ -115,16 +117,38 @@ export function FiltersBarSelect({ const allOption = { value: "", label: props.placeholder ?? item.label } const options = [allOption, ...initialValues] + // Only a list that runs past its first page needs to fetch: with everything + // already loaded the select filters what it holds, and no request is made just + // to open the menu. + // + // Where it does page, "All" rides on the first page of the resting list rather + // than on every response: it belongs to the list one browses, not to search + // results, so it heads the untouched menu and steps aside as soon as one types. + const paginationProps = hasMorePages + ? ({ + infiniteScroll: true, + loadAsyncValues: async (hint: string, meta: { page: number }) => { + const { options, hasMore } = await loadAsyncValues(hint, meta) + + return { + options: + hint === "" && meta.page === 1 + ? [allOption, ...options] + : options, + hasMore, + } + }, + } as const) + : {} + return (
Promise + /** + * Whether anything exists beyond `initialValues`. When it does not, the whole + * list is already in hand and the select needs no request of its own. + */ + hasMorePages: boolean + /** Whether typing narrows the list server-side, i.e. `searchBy` is set. */ + isSearchable: boolean + /** + * Loads one page of options, narrowed by what has been typed when the + * instruction sets `searchBy`. Feeds the select's infinite scroll. + */ + loadAsyncValues: ( + hint: string, + meta: { page: number }, + ) => Promise<{ options: InputSelectValue[]; hasMore: boolean }> } /** @@ -106,33 +119,48 @@ export function useResourceSelectOptions({ "value", ) + const recordCount = firstPage?.meta?.recordCount + const pageCount = firstPage?.meta?.pageCount + return { initialValues, isLoading, - recordCount: firstPage?.meta?.recordCount, + recordCount, hasResolvedSelection: selectedValues.length === 0 || selectedValues.every((value) => initialValues.some((option) => option.value === value), ), - loadAsyncValues: - searchBy == null - ? undefined - : async (hint: string) => { - // the sdk resource is only known at runtime, so the `list` shape - // cannot be inferred from the union of all listable resources - const results = await ( - sdkClient[resource] as unknown as { - list: ( - params: QueryParamsList, - ) => Promise>> - } - ).list({ - ...listQuery, - filters: { ...filters, [searchBy]: hint }, - }) + hasMorePages: pageCount != null && pageCount > 1, + isSearchable: searchBy != null, + loadAsyncValues: async (hint: string, { page }: { page: number }) => { + // the sdk resource is only known at runtime, so the `list` shape cannot be + // inferred from the union of all listable resources + const results = await ( + sdkClient[resource] as unknown as { + list: (params: QueryParamsList) => Promise< + Array> & { + meta?: { pageCount?: number } + } + > + } + ).list({ + ...listQuery, + pageNumber: page, + // an empty hint is every record, and `searchBy` is what makes narrowing + // possible at all — without it the menu can still be paged, just not + // searched + filters: + searchBy == null || hint === "" + ? filters + : { ...filters, [searchBy]: hint }, + }) - return results.map(toOption) - }, + return { + options: results.map(toOption), + hasMore: + results.meta?.pageCount != null && results.meta.pageCount > page, + } + }, } } diff --git a/packages/docs/src/stories/forms/ui/InputSelect.stories.tsx b/packages/docs/src/stories/forms/ui/InputSelect.stories.tsx index 9a5810bc1..27abda625 100644 --- a/packages/docs/src/stories/forms/ui/InputSelect.stories.tsx +++ b/packages/docs/src/stories/forms/ui/InputSelect.stories.tsx @@ -149,7 +149,7 @@ Async.args = { icon: "lightbulbFilament", text: "Try to search some of the following values: customer, SKU, price, tax", }, - loadAsyncValues: async (hint) => { + loadAsyncValues: async (hint: string) => { return await new Promise((resolve) => { setTimeout(() => { resolve(fakeSearch(hint)) @@ -158,6 +158,43 @@ Async.args = { }, } +/** + * With `infiniteScroll`, the menu loads the page after the one it is showing as it + * is scrolled to the bottom, so a list longer than a single page can be browsed + * instead of only searched. + * + * `loadAsyncValues` is handed the page to fetch and answers with `hasMore`, which + * is what stops the paging. Anything passed as `initialValues` counts as the first + * page, so scrolling continues from the second one. + */ +export const AsyncInfiniteScroll = Template.bind({}) +AsyncInfiniteScroll.args = { + label: "Search resource", + placeholder: "Scroll for more...", + isSearchable: true, + isClearable: false, + debounceMs: 200, + hint: { + icon: "lightbulbFilament", + text: "Open the menu and scroll to the bottom to load more options", + }, + infiniteScroll: true, + loadAsyncValues: async (hint, { page }) => { + const pageSize = 5 + const matches = fakeSearch(hint) + const options = matches.slice((page - 1) * pageSize, page * pageSize) + + return await new Promise<{ + options: InputSelectValue[] + hasMore: boolean + }>((resolve) => { + setTimeout(() => { + resolve({ options, hasMore: matches.length > page * pageSize }) + }, 1000) + }) + }, +} + /** * It's possible to specify a footer text that will be rendered at the bottom of the dropdown list. */ @@ -173,7 +210,7 @@ MenuFooterText.args = { text: "Try to search some of the following values: customer, SKU, price, tax", }, initialValues: fullList.slice(0, 5), - loadAsyncValues: async (hint) => { + loadAsyncValues: async (hint: string) => { return await new Promise((resolve) => { setTimeout(() => { resolve(fakeSearch(hint)) @@ -243,7 +280,7 @@ AsyncCreatable.args = { icon: "lightbulbFilament", text: "Try to search some of the following values: customer, SKU, price, tax or any other text", }, - loadAsyncValues: async (hint) => { + loadAsyncValues: async (hint: string) => { return await new Promise((resolve) => { setTimeout(() => { resolve(fakeSearch(hint)) @@ -266,7 +303,7 @@ AsTextSearch.args = { isClearable: false, asTextSearch: true, debounceMs: 200, - loadAsyncValues: async (hint) => { + loadAsyncValues: async (hint: string) => { const defaultValues = await new Promise((resolve) => { setTimeout(() => { resolve(fakeSearch(hint)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f7e05c0e..d85b5d7e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: react-select: specifier: ^5.10.2 version: 5.10.2(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-select-async-paginate: + specifier: ^0.7.11 + version: 0.7.11(@types/react@19.2.13)(react-select@5.10.2(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) react-toastify: specifier: ^11.1.0 version: 11.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -152,7 +155,7 @@ importers: version: 2.0.4 '@vitejs/plugin-react': specifier: ^5.2.0 - version: 5.2.0(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) + version: 5.2.0(supports-color@7.2.0)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) cross-fetch: specifier: ^4.1.0 version: 4.1.0(encoding@0.1.13) @@ -164,7 +167,7 @@ importers: version: 3.2.0(date-fns@4.4.0) jsdom: specifier: ^27.4.0 - version: 27.4.0 + version: 27.4.0(supports-color@7.2.0) msw: specifier: ^2.14.6 version: 2.15.0(@types/node@22.20.1)(typescript@5.9.3) @@ -185,10 +188,10 @@ importers: version: 7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@22.20.1)(rollup@4.62.4)(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) + version: 4.5.4(@types/node@22.20.1)(rollup@4.62.4)(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) vitest: specifier: ^3.2.6 - version: 3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(jiti@2.7.0)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.15.0(@types/node@22.20.1)(typescript@5.9.3))(terser@5.50.0)(yaml@2.9.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(jiti@2.7.0)(jsdom@27.4.0(supports-color@7.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@22.20.1)(typescript@5.9.3))(supports-color@7.2.0)(terser@5.50.0)(yaml@2.9.0) wouter: specifier: ^3.10.0 version: 3.10.0(react@19.2.4) @@ -255,7 +258,7 @@ importers: version: 19.2.3(@types/react@19.2.13) '@vitejs/plugin-react': specifier: ^5.2.0 - version: 5.2.0(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) + version: 5.2.0(supports-color@7.2.0)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) babel-loader: specifier: ^10.1.1 version: 10.1.1(@babel/core@8.0.1)(webpack@5.109.2(esbuild@0.28.2)(lightningcss@1.32.0)) @@ -276,7 +279,7 @@ importers: version: 5.10.2(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) remark-gfm: specifier: ^4.0.1 - version: 4.0.1 + version: 4.0.1(supports-color@7.2.0) storybook: specifier: ^10.5.5 version: 10.5.9(@types/react@19.2.13)(prettier@3.9.6)(react@19.2.4) @@ -291,7 +294,7 @@ importers: version: 7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) + version: 5.1.4(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)) zod: specifier: ^3.25.76 version: 3.25.76 @@ -2264,6 +2267,9 @@ packages: '@rushstack/ts-command-line@5.3.12': resolution: {integrity: sha512-Vg2n24arSf7JvUNga2DMHYTbxnVq9L5OtVCp4Gfr8YC/kmL/bmdR8FQhGcpMzMYNY9Vdw3+TaimG11Sf+z1Tpw==} + '@seznam/compose-react-refs@1.0.6': + resolution: {integrity: sha512-izzOXQfeQLonzrIQb8u6LQ8dk+ymz3WXTIXjvOlTXHq6sbzROg3NWU+9TTAOpEoK9Bth24/6F/XrfHJ5yR5n6Q==} + '@sigstore/bundle@4.0.0': resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} engines: {node: ^20.17.0 || >=22.9.0} @@ -2748,6 +2754,11 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@vtaits/use-lazy-ref@0.1.4': + resolution: {integrity: sha512-pdHe8k2WLIm8ccVfNw3HzeTCkifKKjVQ3hpiM7/rMynCp8nev715wrY2RCYnbeowNvekWqpGdHtrWKfCDocC6g==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@vue/compiler-core@3.5.41': resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} @@ -4079,6 +4090,9 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + krustykrab@1.1.0: + resolution: {integrity: sha512-xpX9MPbw+nJseewe6who9Oq46RQwrBfps+dO/N4fSjJhsf2+y4XWC2kz46oBGX8yzMHyYJj35ug0X5s5yxB6tA==} + lerna@10.0.0: resolution: {integrity: sha512-U1Rkz2lMZEGstg7h6vw2LfuGNTomXANZ+mX80+3pR0L2RWWcXI/gZJya9EDq4wTY+1AoUzhNNUnlAEWdFugisw==} engines: {node: ^22.13.0 || ^24.0.0 || ^26.0.0} @@ -4989,6 +5003,12 @@ packages: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} + react-select-async-paginate@0.7.11: + resolution: {integrity: sha512-AjtCLPMk5DLNgygwQprEPC0gfVIjkou+QYvXM+2gm/LeRpY1Gv5KNT79EYB37H1uMCrwA+HL9BY7OtlaNWtYNg==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-select: ^5.0.0 + react-select@5.10.2: resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} peerDependencies: @@ -5214,6 +5234,9 @@ packages: resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==} engines: {node: ^20.17.0 || >=22.9.0} + sleep-promise@9.1.0: + resolution: {integrity: sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==} + slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -5593,6 +5616,11 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + use-is-mounted-ref@1.5.0: + resolution: {integrity: sha512-p5FksHf/ospZUr5KU9ese6u3jp9fzvZ3wuSb50i0y6fdONaHWgmOqQtxR/PUcwi6hnhQDbNxWSg3eTK3N6m+dg==} + peerDependencies: + react: '>=16.0.0' + use-isomorphic-layout-effect@1.2.1: resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} peerDependencies: @@ -5602,6 +5630,15 @@ packages: '@types/react': optional: true + use-latest@1.3.0: + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -5946,7 +5983,7 @@ snapshots: '@babel/compat-data@8.0.0': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@7.2.0)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 @@ -6028,7 +6065,7 @@ snapshots: '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-member-expression-to-functions': 7.29.7 '@babel/helper-optimise-call-expression': 7.29.7 @@ -6094,7 +6131,7 @@ snapshots: '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.29.8 @@ -6131,7 +6168,7 @@ snapshots: '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-member-expression-to-functions': 7.29.7 '@babel/helper-optimise-call-expression': 7.29.7 '@babel/traverse': 7.29.8 @@ -6228,17 +6265,17 @@ snapshots: '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-arrow-functions@8.0.1(@babel/core@8.0.1)': @@ -6272,7 +6309,7 @@ snapshots: '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: @@ -6350,7 +6387,7 @@ snapshots: '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7) @@ -6394,7 +6431,7 @@ snapshots: '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: @@ -6432,7 +6469,7 @@ snapshots: '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-nullish-coalescing-operator@8.0.1(@babel/core@8.0.1)': @@ -6466,7 +6503,7 @@ snapshots: '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: @@ -6485,7 +6522,7 @@ snapshots: '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: @@ -6511,12 +6548,12 @@ snapshots: '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-transform-regenerator@8.0.2(@babel/core@8.0.1)': @@ -6563,7 +6600,7 @@ snapshots: '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 @@ -6666,7 +6703,7 @@ snapshots: '@babel/preset-flow@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7) @@ -6682,7 +6719,7 @@ snapshots: '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) @@ -6693,7 +6730,7 @@ snapshots: '@babel/register@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -7323,8 +7360,8 @@ snapshots: '@npmcli/agent@4.0.2': dependencies: agent-base: 7.1.4 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) lru-cache: 11.5.2 socks-proxy-agent: 8.0.5 transitivePeerDependencies: @@ -7840,6 +7877,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@seznam/compose-react-refs@1.0.6': {} + '@sigstore/bundle@4.0.0': dependencies: '@sigstore/protobuf-specs': 0.5.1 @@ -8298,9 +8337,9 @@ snapshots: '@types/unist@3.0.3': {} - '@vitejs/plugin-react@5.2.0(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(supports-color@7.2.0)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.0-rc.3 @@ -8387,6 +8426,10 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 + '@vtaits/use-lazy-ref@0.1.4(react@19.2.4)': + dependencies: + react: 19.2.4 + '@vue/compiler-core@3.5.41': dependencies: '@babel/parser': 7.29.8 @@ -8612,7 +8655,7 @@ snapshots: babel-core@7.0.0-bridge.0(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) babel-loader@10.1.1(@babel/core@8.0.1)(webpack@5.109.2(esbuild@0.28.2)(lightningcss@1.32.0)): dependencies: @@ -9407,7 +9450,7 @@ snapshots: http-cache-semantics@4.2.0: {} - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@7.2.0): dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@7.2.0) @@ -9421,7 +9464,7 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@7.2.0): dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@7.2.0) @@ -9602,7 +9645,7 @@ snapshots: jscodeshift@0.15.2(@babel/preset-env@8.0.2(@babel/core@8.0.1)): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/parser': 7.29.8 '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) @@ -9627,7 +9670,7 @@ snapshots: transitivePeerDependencies: - supports-color - jsdom@27.4.0: + jsdom@27.4.0(supports-color@7.2.0): dependencies: '@acemir/cssom': 0.9.31 '@asamuzakjp/dom-selector': 6.8.1 @@ -9636,8 +9679,8 @@ snapshots: data-urls: 6.0.1 decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) is-potential-custom-element-name: 1.0.1 parse5: 8.0.1 saxes: 6.0.0 @@ -9689,6 +9732,8 @@ snapshots: kolorist@1.8.0: {} + krustykrab@1.1.0: {} + lerna@10.0.0(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(typescript@5.9.3): dependencies: '@npmcli/arborist': 9.1.6 @@ -9953,14 +9998,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3: + mdast-util-from-markdown@2.0.3(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@7.2.0) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -9982,7 +10027,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: @@ -9991,7 +10036,7 @@ snapshots: mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -10001,7 +10046,7 @@ snapshots: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -10010,14 +10055,14 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0: + mdast-util-gfm@3.1.0(supports-color@7.2.0): dependencies: - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) mdast-util-gfm-autolink-literal: 2.0.1 mdast-util-gfm-footnote: 2.1.0 mdast-util-gfm-strikethrough: 2.0.0 @@ -10223,7 +10268,7 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@7.2.0): dependencies: '@types/debug': 4.1.13 debug: 4.4.3(supports-color@7.2.0) @@ -10976,7 +11021,7 @@ snapshots: react-docgen@8.0.3: dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/traverse': 7.29.8 '@babel/types': 7.29.8 '@types/babel__core': 7.20.5 @@ -11016,6 +11061,19 @@ snapshots: react-refresh@0.18.0: {} + react-select-async-paginate@0.7.11(@types/react@19.2.13)(react-select@5.10.2(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + dependencies: + '@seznam/compose-react-refs': 1.0.6 + '@vtaits/use-lazy-ref': 0.1.4(react@19.2.4) + krustykrab: 1.1.0 + react: 19.2.4 + react-select: 5.10.2(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + sleep-promise: 9.1.0 + use-is-mounted-ref: 1.5.0(react@19.2.4) + use-latest: 1.3.0(@types/react@19.2.13)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + react-select@5.10.2(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.29.7 @@ -11107,10 +11165,10 @@ snapshots: dependencies: jsesc: 3.1.0 - remark-gfm@4.0.1: + remark-gfm@4.0.1(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 + mdast-util-gfm: 3.1.0(supports-color@7.2.0) micromark-extension-gfm: 3.0.0 remark-parse: 11.0.0 remark-stringify: 11.0.0 @@ -11121,7 +11179,7 @@ snapshots: remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -11272,6 +11330,8 @@ snapshots: transitivePeerDependencies: - supports-color + sleep-promise@9.1.0: {} + slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -11629,12 +11689,23 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + use-is-mounted-ref@1.5.0(react@19.2.4): + dependencies: + react: 19.2.4 + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.13)(react@19.2.4): dependencies: react: 19.2.4 optionalDependencies: '@types/react': 19.2.13 + use-latest@1.3.0(@types/react@19.2.13)(react@19.2.4): + dependencies: + react: 19.2.4 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.13)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + use-sync-external-store@1.6.0(react@19.2.4): dependencies: react: 19.2.4 @@ -11658,7 +11729,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0): + vite-node@3.2.4(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0)(terser@5.50.0)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@7.2.0) @@ -11679,7 +11750,7 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.5.4(@types/node@22.20.1)(rollup@4.62.4)(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)): + vite-plugin-dts@4.5.4(@types/node@22.20.1)(rollup@4.62.4)(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)): dependencies: '@microsoft/api-extractor': 7.58.12(@types/node@22.20.1) '@rollup/pluginutils': 5.4.0(rollup@4.62.4) @@ -11698,7 +11769,7 @@ snapshots: - rollup - supports-color - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)): + vite-tsconfig-paths@5.1.4(supports-color@7.2.0)(typescript@5.9.3)(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0)): dependencies: debug: 4.4.3(supports-color@7.2.0) globrex: 0.1.2 @@ -11725,7 +11796,7 @@ snapshots: terser: 5.50.0 yaml: 2.9.0 - vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(jiti@2.7.0)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.15.0(@types/node@22.20.1)(typescript@5.9.3))(terser@5.50.0)(yaml@2.9.0): + vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(jiti@2.7.0)(jsdom@27.4.0(supports-color@7.2.0))(lightningcss@1.32.0)(msw@2.15.0(@types/node@22.20.1)(typescript@5.9.3))(supports-color@7.2.0)(terser@5.50.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 @@ -11748,12 +11819,12 @@ snapshots: tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.50.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(supports-color@7.2.0)(terser@5.50.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 '@types/node': 22.20.1 - jsdom: 27.4.0 + jsdom: 27.4.0(supports-color@7.2.0) transitivePeerDependencies: - jiti - less