diff --git a/.changeset/loud-zebras-lie.md b/.changeset/loud-zebras-lie.md new file mode 100644 index 000000000000..1f6b1255af02 --- /dev/null +++ b/.changeset/loud-zebras-lie.md @@ -0,0 +1,11 @@ +--- +"@refinedev/core": minor +"@refinedev/antd": minor +"@refinedev/mui": minor +--- + +feat: add `selectedOptionsOrder` in `useSelect` + +Now with `selectedOptionsOrder`, you can sort `selectedOptions` at the top of list when use `useSelect` with `defaultValue`. + +Resolves [#6061](https://github.com/refinedev/refine/issues/6061) diff --git a/documentation/docs/data/hooks/use-select/index.md b/documentation/docs/data/hooks/use-select/index.md index a805b9fc84d8..0f31ae3a2cb2 100644 --- a/documentation/docs/data/hooks/use-select/index.md +++ b/documentation/docs/data/hooks/use-select/index.md @@ -160,6 +160,20 @@ useSelect({ }); ``` +### selectedOptionsOrder + +`selectedOptionsOrder` allows us to sort `selectedOptions` on `defaultValue`. It can be: + +- `"in-place"`: sort `selectedOptions` at the bottom. It is by default. +- `"selected-first"`: sort `selectedOptions` at the top. + +```tsx +useSelect({ + defaultValue: 1, // or [1, 2] + selectedOptionsOrder: "selected-first", // in-place | selected-first +}); +``` + > For more information, refer to the [`useMany` documentation→](/docs/data/hooks/use-many) ### debounce diff --git a/documentation/docs/ui-integrations/ant-design/hooks/use-checkbox-group/index.md b/documentation/docs/ui-integrations/ant-design/hooks/use-checkbox-group/index.md index 9d39c5526a6d..68952f61a718 100644 --- a/documentation/docs/ui-integrations/ant-design/hooks/use-checkbox-group/index.md +++ b/documentation/docs/ui-integrations/ant-design/hooks/use-checkbox-group/index.md @@ -91,6 +91,23 @@ const { selectProps } = useCheckboxGroup({ }); ``` +### selectedOptionsOrder + +`selectedOptionsOrder` allows us to sort `selectedOptions` on `defaultValue`. It can be: + +- `"in-place"`: sort `selectedOptions` at the bottom. It is by default. +- `"selected-first"`: sort `selectedOptions` at the top. + +```tsx +const { selectProps } = useCheckboxGroup({ + resource: "languages", + // highlight-next-line + defaultValue: [1, 2], + // highlight-next-line + selectedOptionsOrder: "selected-first", // in-place | selected-first +}); +``` + The easiest way to select default values for checkbox fields is by passing in `defaultValue`. ### optionLabel and optionValue @@ -260,3 +277,7 @@ Use `sorters` instead. [baserecord]: /docs/core/interface-references#baserecord [httperror]: /docs/core/interface-references#httperror + +``` + +``` diff --git a/documentation/docs/ui-integrations/ant-design/hooks/use-radio-group/index.md b/documentation/docs/ui-integrations/ant-design/hooks/use-radio-group/index.md index 7772cbdc8e2b..15daf7aa228d 100644 --- a/documentation/docs/ui-integrations/ant-design/hooks/use-radio-group/index.md +++ b/documentation/docs/ui-integrations/ant-design/hooks/use-radio-group/index.md @@ -90,6 +90,23 @@ const { selectProps } = useRadioGroup({ }); ``` +### selectedOptionsOrder + +`selectedOptionsOrder` allows us to sort `selectedOptions` on `defaultValue`. It can be: + +- `"in-place"`: sort `selectedOptions` at the bottom. It is by default. +- `"selected-first"`: sort `selectedOptions` at the top. + +```tsx +const { selectProps } = useRadioGroup({ + resource: "languages", + // highlight-next-line + defaultValue: 1, + // highlight-next-line + selectedOptionsOrder: "selected-first", // in-place | selected-first +}); +``` + The easiest way to selecting a default value for an radio button field is by passing in `defaultValue`. ### optionLabel and optionValue diff --git a/documentation/docs/ui-integrations/ant-design/hooks/use-select/index.md b/documentation/docs/ui-integrations/ant-design/hooks/use-select/index.md index 3ed3b3fc4e2e..c7ca3ebb2b50 100644 --- a/documentation/docs/ui-integrations/ant-design/hooks/use-select/index.md +++ b/documentation/docs/ui-integrations/ant-design/hooks/use-select/index.md @@ -157,6 +157,20 @@ useSelect({ }); ``` +### selectedOptionsOrder + +`selectedOptionsOrder` allows us to sort `selectedOptions` on `defaultValue`. It can be: + +- `"in-place"`: sort `selectedOptions` at the bottom. It is by default. +- `"selected-first"`: sort `selectedOptions` at the top. + +```tsx +useSelect({ + defaultValue: 1, // or [1, 2] + selectedOptionsOrder: "selected-first", // in-place | selected-first +}); +``` + > For more information, refer to the [`useMany` documentation →](/docs/data/hooks/use-many) ### debounce diff --git a/documentation/docs/ui-integrations/mantine/hooks/use-select/index.md b/documentation/docs/ui-integrations/mantine/hooks/use-select/index.md index c38f30b136cf..edfbeec475fb 100644 --- a/documentation/docs/ui-integrations/mantine/hooks/use-select/index.md +++ b/documentation/docs/ui-integrations/mantine/hooks/use-select/index.md @@ -149,6 +149,20 @@ useSelect({ > For more information, refer to the [`useMany` documentation →](/docs/data/hooks/use-many) +### selectedOptionsOrder + +`selectedOptionsOrder` allows us to sort `selectedOptions` on `defaultValue`. It can be: + +- `"in-place"`: sort `selectedOptions` at the bottom. It is by default. +- `"selected-first"`: sort `selectedOptions` at the top. + +```tsx +useSelect({ + defaultValue: 1, // or [1, 2] + selectedOptionsOrder: "selected-first", // in-place | selected-first +}); +``` + ### debounce `debounce` allows us to `debounce` the `onSearch` function. diff --git a/documentation/docs/ui-integrations/material-ui/hooks/use-auto-complete/index.md b/documentation/docs/ui-integrations/material-ui/hooks/use-auto-complete/index.md index c267509f7daa..bfaee58f1a76 100644 --- a/documentation/docs/ui-integrations/material-ui/hooks/use-auto-complete/index.md +++ b/documentation/docs/ui-integrations/material-ui/hooks/use-auto-complete/index.md @@ -90,6 +90,20 @@ useAutocomplete({ }); ``` +### selectedOptionsOrder + +`selectedOptionsOrder` allows us to sort `selectedOptions` on `defaultValue`. It can be: + +- `"in-place"`: sort `selectedOptions` at the bottom. It is by default. +- `"selected-first"`: sort `selectedOptions` at the top. + +```tsx +useAutocomplete({ + defaultValue: 1, // or [1, 2] + selectedOptionsOrder: "selected-first", // in-place | selected-first +}); +``` + > For more information, refer to the [`useMany` documentation →](/docs/data/hooks/use-many) ### debounce diff --git a/packages/antd/src/hooks/fields/useCheckboxGroup/index.ts b/packages/antd/src/hooks/fields/useCheckboxGroup/index.ts index 6e5640dc69ab..0013aabf0ef2 100644 --- a/packages/antd/src/hooks/fields/useCheckboxGroup/index.ts +++ b/packages/antd/src/hooks/fields/useCheckboxGroup/index.ts @@ -63,6 +63,7 @@ export const useCheckboxGroup = < pagination, liveMode, defaultValue, + selectedOptionsOrder, onLiveEvent, liveParams, meta, @@ -90,6 +91,7 @@ export const useCheckboxGroup = < pagination, liveMode, defaultValue, + selectedOptionsOrder, onLiveEvent, liveParams, meta: pickNotDeprecated(meta, metaData), diff --git a/packages/antd/src/hooks/fields/useRadioGroup/index.ts b/packages/antd/src/hooks/fields/useRadioGroup/index.ts index 3e15fd7732bc..f7f02a16e2f3 100644 --- a/packages/antd/src/hooks/fields/useRadioGroup/index.ts +++ b/packages/antd/src/hooks/fields/useRadioGroup/index.ts @@ -60,6 +60,7 @@ export const useRadioGroup = < pagination, liveMode, defaultValue, + selectedOptionsOrder, onLiveEvent, liveParams, meta, @@ -86,6 +87,7 @@ export const useRadioGroup = < pagination, liveMode, defaultValue, + selectedOptionsOrder, onLiveEvent, liveParams, meta: pickNotDeprecated(meta, metaData), diff --git a/packages/core/src/hooks/useSelect/index.spec.ts b/packages/core/src/hooks/useSelect/index.spec.ts index aa6f85a2b4fc..f58df9b8fe6d 100644 --- a/packages/core/src/hooks/useSelect/index.spec.ts +++ b/packages/core/src/hooks/useSelect/index.spec.ts @@ -2,6 +2,7 @@ import { waitFor } from "@testing-library/react"; import { renderHook } from "@testing-library/react-hooks"; import { MockJSONServer, TestWrapper, act, mockRouterProvider } from "@test"; +import { posts } from "@test/dataMocks"; import type { CrudFilter, @@ -440,6 +441,47 @@ describe("useSelect Hook", () => { expect(mockFunc).toBeCalledTimes(2); }); + it("should sort default data first with selectedOptionsOrder for defaultValue", async () => { + const { result } = renderHook( + () => + useSelect({ + resource: "posts", + defaultValue: ["2"], + selectedOptionsOrder: "selected-first", + }), + { + wrapper: TestWrapper({ + dataProvider: { + default: { + ...MockJSONServer.default, + // Default `getMany` mock returns all posts, we need to update it to return appropriate posts + getMany: ({ ids }) => { + return Promise.resolve({ + data: posts.filter((post) => ids.includes(post.id)) as any, + }); + }, + }, + }, + resources: [{ name: "posts" }], + }), + }, + ); + + await waitFor(() => { + expect(result.current.queryResult.isSuccess).toBeTruthy(); + }); + + expect(result.current.options).toHaveLength(2); + expect(result.current.options).toEqual([ + { label: "Recusandae consectetur aut atque est.", value: "2" }, + { + label: + "Necessitatibus necessitatibus id et cupiditate provident est qui amet.", + value: "1", + }, + ]); + }); + it("should invoke queryOptions methods for default value successfully", async () => { const mockFunc = jest.fn(); diff --git a/packages/core/src/hooks/useSelect/index.ts b/packages/core/src/hooks/useSelect/index.ts index ffde0140b813..3a08f8fde0bf 100644 --- a/packages/core/src/hooks/useSelect/index.ts +++ b/packages/core/src/hooks/useSelect/index.ts @@ -34,6 +34,8 @@ import { useLoadingOvertime, } from "../useLoadingOvertime"; +export type SelectedOptionsOrder = "in-place" | "selected-first"; + export type UseSelectProps = { /** * Resource name for API data interactions @@ -84,6 +86,11 @@ export type UseSelectProps = { * Adds extra `options` */ defaultValue?: BaseKey | BaseKey[]; + /** + * Allow us to sort the selection options + * @default `in-place` + */ + selectedOptionsOrder?: SelectedOptionsOrder; /** * The number of milliseconds to delay * @default `300` @@ -213,6 +220,7 @@ export const useSelect = < hasPagination = false, liveMode, defaultValue = [], + selectedOptionsOrder = "in-place", onLiveEvent, onSearch: onSearchFromProp, liveParams, @@ -362,7 +370,13 @@ export const useSelect = < }); const combinedOptions = useMemo( - () => uniqBy([...options, ...selectedOptions], "value"), + () => + uniqBy( + selectedOptionsOrder === "in-place" + ? [...options, ...selectedOptions] + : [...selectedOptions, ...options], + "value", + ), [options, selectedOptions], ); diff --git a/packages/mui/src/hooks/useAutocomplete/index.ts b/packages/mui/src/hooks/useAutocomplete/index.ts index 2e88377e6955..81fa8e55ee44 100644 --- a/packages/mui/src/hooks/useAutocomplete/index.ts +++ b/packages/mui/src/hooks/useAutocomplete/index.ts @@ -60,11 +60,18 @@ export const useAutocomplete = < return { autocompleteProps: { - options: unionWith( - queryResult.data?.data || [], - defaultValueQueryResult.data?.data || [], - isEqual, - ), + options: + props.selectedOptionsOrder === "selected-first" + ? unionWith( + defaultValueQueryResult.data?.data || [], + queryResult.data?.data || [], + isEqual, + ) + : unionWith( + queryResult.data?.data || [], + defaultValueQueryResult.data?.data || [], + isEqual, + ), loading: queryResult.isFetching || defaultValueQueryResult.isFetching, onInputChange: (event, value) => { if (event?.type === "change") {