diff --git a/.changeset/fix-create-modal-cached-data.md b/.changeset/fix-create-modal-cached-data.md new file mode 100644 index 0000000000000..be590b110d299 --- /dev/null +++ b/.changeset/fix-create-modal-cached-data.md @@ -0,0 +1,11 @@ +--- +"@refinedev/refine-core": patch +"@refinedev/refine-react-hook-form": patch +--- + +fix(core, react-hook-form): prevent cached show-page data from overwriting create modal defaultValues + +When opening a create modal on a show page for the same resource, the form's `defaultValues` were overwritten by cached data from the show page's `useOne` query. This happened because `useForm` passed the URL-derived `id` to `useOne` even for create actions, causing a query key collision with the cached entry. + +- **core:** Don't pass `id` to `useOne` for create actions, preventing the cache key collision at the source. +- **react-hook-form:** Guard the `useModalForm` visibility reset effect against create actions. diff --git a/.changeset/safe-route-id-decoding.md b/.changeset/safe-route-id-decoding.md new file mode 100644 index 0000000000000..aadb4c9cbc108 --- /dev/null +++ b/.changeset/safe-route-id-decoding.md @@ -0,0 +1,7 @@ +--- +"@refinedev/core": patch +"@refinedev/react-router": patch +"@refinedev/remix-router": patch +--- + +Safely handle malformed URI-encoded route params when parsing route ids and query targets. diff --git a/packages/antd/src/components/themedLayout/sider/index.tsx b/packages/antd/src/components/themedLayout/sider/index.tsx index bbee40e82fa10..76040558df718 100644 --- a/packages/antd/src/components/themedLayout/sider/index.tsx +++ b/packages/antd/src/components/themedLayout/sider/index.tsx @@ -5,6 +5,7 @@ import { Grid, Drawer, Button, + Tooltip, theme, ConfigProvider, } from "antd"; @@ -82,7 +83,11 @@ export const ThemedSider: React.FC = ({ } - title={label} + title={ + + {label} + + } > {renderTreeView(children, selectedKey)} @@ -109,9 +114,11 @@ export const ThemedSider: React.FC = ({ icon={icon ?? (isRoute && )} style={linkStyle} > - - {label} - + + + {label} + + {!siderCollapsed && isSelected && (
)} @@ -145,7 +152,9 @@ export const ThemedSider: React.FC = ({ onClick={() => handleLogout()} icon={} > - {translate("buttons.logout", "Logout")} + + {translate("buttons.logout", "Logout")} + ); diff --git a/packages/core/src/definitions/helpers/handleUseParams/index.tsx b/packages/core/src/definitions/helpers/handleUseParams/index.tsx index d71cd1f8e1486..4dc1aff4c55be 100644 --- a/packages/core/src/definitions/helpers/handleUseParams/index.tsx +++ b/packages/core/src/definitions/helpers/handleUseParams/index.tsx @@ -1,8 +1,16 @@ +const safeDecodeURIComponent = (value: string) => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; + export const handleUseParams = (params: any = {}): any => { if (params?.id) { return { ...params, - id: decodeURIComponent(params.id), + id: safeDecodeURIComponent(params.id), }; } return params; diff --git a/packages/core/src/hooks/form/index.ts b/packages/core/src/hooks/form/index.ts index 10facf3d0e7c5..2e41bfd8a807d 100644 --- a/packages/core/src/hooks/form/index.ts +++ b/packages/core/src/hooks/form/index.ts @@ -156,7 +156,7 @@ export const useForm = < const queryResult = useOne({ resource: identifier, - id, + id: isCreate ? undefined : id, queryOptions: { // Only enable the query if it's not a create action and the `id` is defined ...props.queryOptions, diff --git a/packages/react-hook-form/src/useModalForm/index.ts b/packages/react-hook-form/src/useModalForm/index.ts index 3895c50aad1c9..aa028d7e0a1ee 100644 --- a/packages/react-hook-form/src/useModalForm/index.ts +++ b/packages/react-hook-form/src/useModalForm/index.ts @@ -187,7 +187,7 @@ export const useModalForm = < // compensate for setting of initial form values in useForm since it doesnt track modal visibility React.useEffect(() => { - if (!visible || !query?.data?.data) return; + if (!visible || !query?.data?.data || action === "create") return; const formData = query.data.data; if (!formData) return; @@ -197,7 +197,7 @@ export const useModalForm = < keepDirtyValues: true, }), }); - }, [visible, query?.data?.data, autoResetFormWhenClose]); + }, [visible, query?.data?.data, autoResetFormWhenClose, action]); React.useEffect(() => { if (initiallySynced === false && syncWithLocationKey) { diff --git a/packages/react-hook-form/test/index.tsx b/packages/react-hook-form/test/index.tsx index 0c2ce0fb2e8de..54cb490cb530f 100644 --- a/packages/react-hook-form/test/index.tsx +++ b/packages/react-hook-form/test/index.tsx @@ -6,6 +6,7 @@ import { type IResourceItem, type I18nProvider, type IRefineOptions, + type RouterProvider, } from "@refinedev/core"; import { MockJSONServer, mockRouterProvider } from "./dataMocks"; @@ -15,6 +16,7 @@ interface ITestWrapperProps { dataProvider?: DataProvider; resources?: IResourceItem[]; routerInitialEntries?: string[]; + routerProvider?: RouterProvider; i18nProvider?: I18nProvider; options?: IRefineOptions; } @@ -25,6 +27,7 @@ export const TestWrapper: ( dataProvider, resources, routerInitialEntries, + routerProvider, i18nProvider, options, }) => { @@ -34,12 +37,12 @@ export const TestWrapper: ( { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; diff --git a/packages/remix-router/src/bindings.tsx b/packages/remix-router/src/bindings.tsx index d010b15212a39..5b477a43743b4 100644 --- a/packages/remix-router/src/bindings.tsx +++ b/packages/remix-router/src/bindings.tsx @@ -11,6 +11,7 @@ import qs from "qs"; import React, { type ComponentProps, useCallback, useContext } from "react"; import { paramsFromCurrentPath } from "./params-from-current-path"; import { convertToNumberIfPossible } from "./convert-to-number-if-possible"; +import { safeDecodeURIComponent } from "./safe-decode-uri-component"; export const stringifyConfig = { addQueryPrefix: true, @@ -116,8 +117,8 @@ export const routerProvider: RouterProvider = { const response: ParseResponse = { ...(resource && { resource }), ...(action && { action }), - ...(inferredId && { id: decodeURIComponent(inferredId) }), - ...(params?.id && { id: decodeURIComponent(params.id) }), + ...(inferredId && { id: safeDecodeURIComponent(inferredId) }), + ...(params?.id && { id: safeDecodeURIComponent(params.id) }), // ...(params?.action && { action: params.action }), // lets see if there is a need for this pathname, params: { @@ -129,7 +130,7 @@ export const routerProvider: RouterProvider = { combinedParams.pageSize as string, ) as number | undefined, to: combinedParams.to - ? decodeURIComponent(combinedParams.to as string) + ? safeDecodeURIComponent(combinedParams.to as string) : undefined, }, }; diff --git a/packages/remix-router/src/safe-decode-uri-component.ts b/packages/remix-router/src/safe-decode-uri-component.ts new file mode 100644 index 0000000000000..b13259acebb4b --- /dev/null +++ b/packages/remix-router/src/safe-decode-uri-component.ts @@ -0,0 +1,7 @@ +export const safeDecodeURIComponent = (value: string) => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +};