From 92507f0064c20a8e0087df674ab2d5016afd1e60 Mon Sep 17 00:00:00 2001 From: deepshekhardas Date: Sun, 19 Jul 2026 13:33:00 +0530 Subject: [PATCH] feat: apply PRs #7501, #7495, #7473 - chore: use official @module-federation/vite (#7501) - feat(core): add namespace option to useTranslation/useTranslate (#7495) - feat(antd): add onParse support to useTable and useSimpleList (#7473) --- .changeset/add-onparse-to-antd.md | 5 + .../apps/blog-posts/package.json | 2 +- .../apps/blog-posts/vite.config.ts | 7 +- .../apps/categories/package.json | 2 +- .../apps/categories/vite.config.ts | 7 +- .../apps/host/.env.example | 2 +- .../apps/host/package.json | 2 +- .../apps/host/src/App.tsx | 15 +-- .../apps/host/src/types.ts | 3 - .../apps/host/vite.config.ts | 85 +++++++------- .../hooks/table/useTable/useTable.spec.tsx | 58 ++++++++++ .../antd/src/hooks/table/useTable/useTable.ts | 51 +++++---- .../hooks/useSimpleList/useSimpleList.spec.ts | 108 +++++++++++++++++- .../src/hooks/useSimpleList/useSimpleList.ts | 37 +++++- packages/core/src/hooks/i18n/useTranslate.ts | 30 ++++- .../core/src/hooks/i18n/useTranslation.tsx | 8 +- .../react-router/src/use-document-title.ts | 40 ++++--- 17 files changed, 349 insertions(+), 113 deletions(-) create mode 100644 .changeset/add-onparse-to-antd.md delete mode 100644 examples/monorepo-module-federation/apps/host/src/types.ts diff --git a/.changeset/add-onparse-to-antd.md b/.changeset/add-onparse-to-antd.md new file mode 100644 index 0000000000000..a693626bf5f2a --- /dev/null +++ b/.changeset/add-onparse-to-antd.md @@ -0,0 +1,5 @@ +--- +"@refinedev/antd": minor +--- + +Added `onParse` callback to `useTable` and `useSimpleList` hooks to support custom transformation/mapping of URL-synced filters back into search form fields. diff --git a/examples/monorepo-module-federation/apps/blog-posts/package.json b/examples/monorepo-module-federation/apps/blog-posts/package.json index 423152dda741e..752783a319af4 100644 --- a/examples/monorepo-module-federation/apps/blog-posts/package.json +++ b/examples/monorepo-module-federation/apps/blog-posts/package.json @@ -37,7 +37,7 @@ "react-router": "^7.0.2" }, "devDependencies": { - "@originjs/vite-plugin-federation": "^1.2.3", + "@module-federation/vite": "^1.16.8", "@types/node": "^20", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", diff --git a/examples/monorepo-module-federation/apps/blog-posts/vite.config.ts b/examples/monorepo-module-federation/apps/blog-posts/vite.config.ts index f0fb0ea3c513f..ccb79f9d7bda1 100644 --- a/examples/monorepo-module-federation/apps/blog-posts/vite.config.ts +++ b/examples/monorepo-module-federation/apps/blog-posts/vite.config.ts @@ -1,4 +1,4 @@ -import federation from "@originjs/vite-plugin-federation"; +import { federation } from "@module-federation/vite"; import react from "@vitejs/plugin-react"; import * as dns from "dns"; import { defineConfig } from "vite"; @@ -12,7 +12,8 @@ export default defineConfig({ react(), federation({ name: "blog_posts", - filename: "blog_posts.js", + filename: "remoteEntry.js", + dts: false, exposes: { "./BlogPostList": "./src/pages/blog-posts/list.tsx", "./BlogPostShow": "./src/pages/blog-posts/show.tsx", @@ -22,7 +23,7 @@ export default defineConfig({ shared: [ "react", "react-dom", - "react-router-dom", + "react-router", "@refinedev/core", "@refinedev/antd", "antd", diff --git a/examples/monorepo-module-federation/apps/categories/package.json b/examples/monorepo-module-federation/apps/categories/package.json index 5af4ec06ab8e9..66747e44eeeb6 100644 --- a/examples/monorepo-module-federation/apps/categories/package.json +++ b/examples/monorepo-module-federation/apps/categories/package.json @@ -37,7 +37,7 @@ "react-router": "^7.0.2" }, "devDependencies": { - "@originjs/vite-plugin-federation": "^1.2.3", + "@module-federation/vite": "^1.16.8", "@types/node": "^20", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", diff --git a/examples/monorepo-module-federation/apps/categories/vite.config.ts b/examples/monorepo-module-federation/apps/categories/vite.config.ts index 514a89a241326..0203b84252de0 100644 --- a/examples/monorepo-module-federation/apps/categories/vite.config.ts +++ b/examples/monorepo-module-federation/apps/categories/vite.config.ts @@ -1,4 +1,4 @@ -import federation from "@originjs/vite-plugin-federation"; +import { federation } from "@module-federation/vite"; import react from "@vitejs/plugin-react"; import * as dns from "dns"; import { defineConfig } from "vite"; @@ -12,7 +12,8 @@ export default defineConfig({ react(), federation({ name: "categories", - filename: "categories.js", + filename: "remoteEntry.js", + dts: false, exposes: { "./CategoryList": "./src/pages/categories/list.tsx", "./CategoryShow": "./src/pages/categories/show.tsx", @@ -22,7 +23,7 @@ export default defineConfig({ shared: [ "react", "react-dom", - "react-router-dom", + "react-router", "@refinedev/core", "@refinedev/antd", "antd", diff --git a/examples/monorepo-module-federation/apps/host/.env.example b/examples/monorepo-module-federation/apps/host/.env.example index 7295664406274..97348f1323754 100644 --- a/examples/monorepo-module-federation/apps/host/.env.example +++ b/examples/monorepo-module-federation/apps/host/.env.example @@ -1 +1 @@ -VITE_CATEGORIES_URL="http://localhost:4002/assets/categories.js" +VITE_CATEGORIES_URL="http://localhost:4002/remoteEntry.js" diff --git a/examples/monorepo-module-federation/apps/host/package.json b/examples/monorepo-module-federation/apps/host/package.json index f7223b535c5d6..99f70f7b3f541 100644 --- a/examples/monorepo-module-federation/apps/host/package.json +++ b/examples/monorepo-module-federation/apps/host/package.json @@ -35,7 +35,7 @@ "react-router": "^7.0.2" }, "devDependencies": { - "@originjs/vite-plugin-federation": "^1.2.3", + "@module-federation/vite": "^1.16.8", "@types/node": "^20", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", diff --git a/examples/monorepo-module-federation/apps/host/src/App.tsx b/examples/monorepo-module-federation/apps/host/src/App.tsx index 36b849d3f13f4..914c74d308403 100644 --- a/examples/monorepo-module-federation/apps/host/src/App.tsx +++ b/examples/monorepo-module-federation/apps/host/src/App.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useEffect } from "react"; +import React, { Suspense } from "react"; import { Authenticated, GitHubBanner, Refine } from "@refinedev/core"; import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar"; @@ -25,7 +25,6 @@ import { ColorModeContextProvider } from "./contexts/color-mode"; import { ForgotPassword } from "./pages/forgotPassword"; import { Login } from "./pages/login"; import { Register } from "./pages/register"; -import type { ExtendedWindow } from "./types"; const BlogPostList = React.lazy(() => import("blog_posts/BlogPostList")); const BlogPostShow = React.lazy(() => import("blog_posts/BlogPostShow")); @@ -37,19 +36,7 @@ const CategoryShow = React.lazy(() => import("categories/CategoryShow")); const CategoryEdit = React.lazy(() => import("categories/CategoryEdit")); const CategoryCreate = React.lazy(() => import("categories/CategoryCreate")); -declare let window: ExtendedWindow; - function App() { - useEffect(() => { - // This is not as elegant as production code as we cannot use such in production. - // But lets assume these envs are coming from a environment handler. - // And this is only for sample case. - if (import.meta.env.VITE_CATEGORIES_URL) { - // This is where we set the payment remote's URL. - window.categoriesUrl = import.meta.env.VITE_CATEGORIES_URL; - } - }, []); - return ( diff --git a/examples/monorepo-module-federation/apps/host/src/types.ts b/examples/monorepo-module-federation/apps/host/src/types.ts deleted file mode 100644 index be530d246faf6..0000000000000 --- a/examples/monorepo-module-federation/apps/host/src/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ExtendedWindow extends Window { - categoriesUrl: string; -} diff --git a/examples/monorepo-module-federation/apps/host/vite.config.ts b/examples/monorepo-module-federation/apps/host/vite.config.ts index e384d251291f1..eec14e436caa3 100644 --- a/examples/monorepo-module-federation/apps/host/vite.config.ts +++ b/examples/monorepo-module-federation/apps/host/vite.config.ts @@ -1,53 +1,60 @@ -import federation from "@originjs/vite-plugin-federation"; +import { federation } from "@module-federation/vite"; import react from "@vitejs/plugin-react"; import * as dns from "dns"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; dns.setDefaultResultOrder("verbatim"); // https://vitejs.dev/config/ -export default defineConfig({ - plugins: [ - react(), - federation({ - name: "host", - remotes: [ - { +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, __dirname, ""); + + return { + plugins: [ + react(), + federation({ + name: "host", + dts: false, + remotes: { blog_posts: { - external: "http://localhost:4001/assets/blog_posts.js", - from: "vite", - externalType: "url", + type: "module", + name: "blog_posts", + entry: "http://localhost:4001/remoteEntry.js", + entryGlobalName: "blog_posts", + shareScope: "default", }, - }, - { categories: { - external: "Promise.resolve(window.categoriesUrl)", - from: "vite", - externalType: "promise", + type: "module", + name: "categories", + entry: + env.VITE_CATEGORIES_URL || "http://localhost:4002/remoteEntry.js", + entryGlobalName: "categories", + shareScope: "default", }, }, - ], - shared: [ - "react", - "react-dom", - "react-router-dom", - "@refinedev/core", - "@refinedev/antd", - "antd", - ], - }), - tsconfigPaths({ root: __dirname }), - ], + filename: "remoteEntry.js", + shared: [ + "react", + "react-dom", + "react-router", + "@refinedev/core", + "@refinedev/antd", + "antd", + ], + }), + tsconfigPaths({ root: __dirname }), + ], - preview: { - host: "localhost", - port: 4000, - strictPort: true, - }, - build: { - target: "esnext", - minify: false, - cssCodeSplit: false, - }, + preview: { + host: "localhost", + port: 4000, + strictPort: true, + }, + build: { + target: "esnext", + minify: false, + cssCodeSplit: false, + }, + }; }); diff --git a/packages/antd/src/hooks/table/useTable/useTable.spec.tsx b/packages/antd/src/hooks/table/useTable/useTable.spec.tsx index 67ed2cbc18a6a..65c455d8ac491 100644 --- a/packages/antd/src/hooks/table/useTable/useTable.spec.tsx +++ b/packages/antd/src/hooks/table/useTable/useTable.spec.tsx @@ -355,4 +355,62 @@ describe("useTable Hook", () => { expect(result.current.tableQuery).toEqual(result.current.tableQuery); }); + + it("should pass form values parsed by onParse to search form from params (syncWithLocation)", async () => { + const Component = () => { + const { searchFormProps } = useTable({ + resource: "categories", + syncWithLocation: true, + onParse: (filters) => { + const nameFilter = filters.find( + (f) => "field" in f && f.field === "name", + ); + return { + name: nameFilter?.value ? `Parsed: ${nameFilter.value}` : "", + }; + }, + }); + + return ( +
+ + + +
+ ); + }; + + const { getByDisplayValue } = render(, { + wrapper: TestWrapper({ + routerProvider: { + parse: () => { + return () => ({ + resource: { + name: "posts", + }, + params: { + filters: [ + { + field: "name", + operator: "contains", + value: "Some Name To Look For", + }, + ], + }, + }); + }, + }, + }), + }); + + await waitFor(() => { + expect( + getByDisplayValue("Parsed: Some Name To Look For"), + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/antd/src/hooks/table/useTable/useTable.ts b/packages/antd/src/hooks/table/useTable/useTable.ts index cbe97ec2a7954..94ce04af6bfea 100644 --- a/packages/antd/src/hooks/table/useTable/useTable.ts +++ b/packages/antd/src/hooks/table/useTable/useTable.ts @@ -29,6 +29,7 @@ import type { FilterValue, SorterResult } from "../../../definitions/table"; export type useTableProps = useTablePropsCore & { onSearch?: (data: TSearchVariables) => CrudFilters | Promise; + onParse?: (filters: CrudFilters) => TSearchVariables; }; export type useTableReturnType< @@ -61,6 +62,7 @@ export const useTable = < TData extends BaseRecord = TQueryFnData, >({ onSearch, + onParse, pagination: paginationFromProp, filters: filtersFromProp, sorters: sortersFromProp, @@ -126,28 +128,33 @@ export const useTable = < React.useEffect(() => { if (shouldSyncWithLocation) { - // get registered fields of form - const registeredFields = formSF.form.getFieldsValue() as Record< - string, - any - >; - // map `filters` for registered fields - const filterFilterMap = Object.keys(registeredFields).reduce( - (acc, curr) => { - // find filter for current field - const filter = filters.find( - (filter) => "field" in filter && filter.field === curr, - ); - // if filter exists, set value to filter value - if (filter) { - acc[curr] = filter?.value; - } - return acc; - }, - {} as Record, - ); - // set values to form - formSF.form.setFieldsValue(filterFilterMap as any); + if (onParse) { + const parsedValues = onParse(filters); + formSF.form.setFieldsValue(parsedValues as any); + } else { + // get registered fields of form + const registeredFields = formSF.form.getFieldsValue() as Record< + string, + any + >; + // map `filters` for registered fields + const filterFilterMap = Object.keys(registeredFields).reduce( + (acc, curr) => { + // find filter for current field + const filter = filters.find( + (filter) => "field" in filter && filter.field === curr, + ); + // if filter exists, set value to filter value + if (filter) { + acc[curr] = filter?.value; + } + return acc; + }, + {} as Record, + ); + // set values to form + formSF.form.setFieldsValue(filterFilterMap as any); + } } }, [shouldSyncWithLocation]); diff --git a/packages/antd/src/hooks/useSimpleList/useSimpleList.spec.ts b/packages/antd/src/hooks/useSimpleList/useSimpleList.spec.ts index 25b4f26004b53..5adb203439998 100644 --- a/packages/antd/src/hooks/useSimpleList/useSimpleList.spec.ts +++ b/packages/antd/src/hooks/useSimpleList/useSimpleList.spec.ts @@ -1,6 +1,8 @@ +import { createElement } from "react"; import { renderHook, waitFor } from "@testing-library/react"; +import { Form, Input } from "antd"; -import { MockJSONServer, TestWrapper } from "@test"; +import { MockJSONServer, TestWrapper, render } from "@test"; import { useSimpleList } from "./useSimpleList"; @@ -188,4 +190,108 @@ describe("useSimpleList Hook", () => { expect(result.current.query).toEqual(result.current.query); }); + + it("should pass form values to search form from params (syncWithLocation)", async () => { + const Component = () => { + const { searchFormProps } = useSimpleList({ + resource: "categories", + syncWithLocation: true, + }); + + return createElement( + Form, + searchFormProps, + createElement( + Form.Item, + { name: "name", noStyle: true }, + createElement(Input), + ), + ); + }; + + const { getByDisplayValue } = render(createElement(Component), { + wrapper: TestWrapper({ + dataProvider: MockJSONServer, + routerProvider: { + parse: () => { + return () => ({ + resource: { + name: "posts", + }, + params: { + filters: [ + { + field: "name", + operator: "contains", + value: "Some Name To Look For", + }, + ], + }, + }); + }, + }, + }), + }); + + await waitFor(() => { + expect(getByDisplayValue("Some Name To Look For")).toBeInTheDocument(); + }); + }); + + it("should pass form values parsed by onParse to search form from params (syncWithLocation)", async () => { + const Component = () => { + const { searchFormProps } = useSimpleList({ + resource: "categories", + syncWithLocation: true, + onParse: (filters) => { + const nameFilter = filters.find( + (f) => "field" in f && f.field === "name", + ); + return { + name: nameFilter?.value ? `Parsed: ${nameFilter.value}` : "", + }; + }, + }); + + return createElement( + Form, + searchFormProps, + createElement( + Form.Item, + { name: "name", noStyle: true }, + createElement(Input), + ), + ); + }; + + const { getByDisplayValue } = render(createElement(Component), { + wrapper: TestWrapper({ + dataProvider: MockJSONServer, + routerProvider: { + parse: () => { + return () => ({ + resource: { + name: "posts", + }, + params: { + filters: [ + { + field: "name", + operator: "contains", + value: "Some Name To Look For", + }, + ], + }, + }); + }, + }, + }), + }); + + await waitFor(() => { + expect( + getByDisplayValue("Parsed: Some Name To Look For"), + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/antd/src/hooks/useSimpleList/useSimpleList.ts b/packages/antd/src/hooks/useSimpleList/useSimpleList.ts index 40f567415538d..e60029fffb2a4 100644 --- a/packages/antd/src/hooks/useSimpleList/useSimpleList.ts +++ b/packages/antd/src/hooks/useSimpleList/useSimpleList.ts @@ -1,4 +1,4 @@ -import { Children, createElement, Fragment } from "react"; +import { Children, createElement, Fragment, useEffect } from "react"; import { type ListProps, type FormProps, Form, Grid } from "antd"; import { @@ -8,6 +8,7 @@ import { useTable as useTableCore, type useTableProps as useTablePropsCore, type useTableReturnType, + useSyncWithLocation, } from "@refinedev/core"; import { useLiveMode } from "@refinedev/core"; import { PaginationLink } from "@hooks/table/useTable/paginationLink"; @@ -16,6 +17,7 @@ import type { PaginationConfig } from "antd/lib/pagination"; export type useSimpleListProps = useTablePropsCore & { onSearch?: (data: TSearchVariables) => CrudFilters | Promise; + onParse?: (filters: CrudFilters) => TSearchVariables; }; export type useSimpleListReturnType< @@ -53,6 +55,7 @@ export const useSimpleList = < filters: filtersFromProp, sorters: sortersFromProp, onSearch, + onParse, queryOptions, syncWithLocation, successNotification, @@ -106,6 +109,38 @@ export const useSimpleList = < const [form] = Form.useForm(); + const { syncWithLocation: defaultSyncWithLocation } = useSyncWithLocation(); + const shouldSyncWithLocation = syncWithLocation ?? defaultSyncWithLocation; + + useEffect(() => { + if (shouldSyncWithLocation) { + if (onParse) { + const parsedValues = onParse(filters); + form.setFieldsValue(parsedValues as any); + } else { + // get registered fields of form + const registeredFields = form.getFieldsValue() as Record; + // map `filters` for registered fields + const filterFilterMap = Object.keys(registeredFields).reduce( + (acc, curr) => { + // find filter for current field + const filter = filters.find( + (filter) => "field" in filter && filter.field === curr, + ); + // if filter exists, set value to filter value + if (filter) { + acc[curr] = filter?.value; + } + return acc; + }, + {} as Record, + ); + // set values to form + form.setFieldsValue(filterFilterMap as any); + } + } + }, [shouldSyncWithLocation]); + const { data, isFetched, isLoading } = tableQuery; const onChange = (page: number, pageSize?: number): void => { diff --git a/packages/core/src/hooks/i18n/useTranslate.ts b/packages/core/src/hooks/i18n/useTranslate.ts index b0888e26e2299..447363bf065d9 100644 --- a/packages/core/src/hooks/i18n/useTranslate.ts +++ b/packages/core/src/hooks/i18n/useTranslate.ts @@ -1,6 +1,7 @@ import { useContext, useMemo } from "react"; import { I18nContext } from "@contexts/i18n"; +import type { UseTranslationProps } from "./useTranslation"; /** * If you need to translate the texts in your own components, refine provides the `useTranslate` hook. @@ -8,7 +9,7 @@ import { I18nContext } from "@contexts/i18n"; * * @see {@link https://refine.dev/docs/api-reference/core/hooks/translate/useTranslate} for more details. */ -export const useTranslate = () => { +export const useTranslate = ({ ns }: UseTranslationProps = {}) => { const { i18nProvider } = useContext(I18nContext); const fn = useMemo(() => { @@ -21,20 +22,37 @@ export const useTranslate = () => { function translate( key: string, - options?: string | any, - defaultMessage?: string, + _options?: string | any, + _defaultMessage?: string, ) { + let options = _options; + let defaultMessage = _defaultMessage; + + if (typeof options === "string" && defaultMessage === undefined) { + defaultMessage = options; + + if (ns) { + options = { ns }; + } else { + options = undefined; + } + } + + if (ns && options && typeof options !== "string") { + options = { ns, ...options }; + } + return ( i18nProvider?.translate(key, options, defaultMessage) ?? defaultMessage ?? - (typeof options === "string" && typeof defaultMessage === "undefined" - ? options + (typeof _options === "string" && typeof _defaultMessage === "undefined" + ? _options : key) ); } return translate; - }, [i18nProvider]); + }, [i18nProvider, ns]); return fn; }; diff --git a/packages/core/src/hooks/i18n/useTranslation.tsx b/packages/core/src/hooks/i18n/useTranslation.tsx index 2d2a5dbb4297e..44b5aa51278f0 100644 --- a/packages/core/src/hooks/i18n/useTranslation.tsx +++ b/packages/core/src/hooks/i18n/useTranslation.tsx @@ -2,6 +2,10 @@ import { useGetLocale } from "./useGetLocale"; import { useSetLocale } from "./useSetLocale"; import { useTranslate } from "./useTranslate"; +export interface UseTranslationProps { + ns?: string | string[]; +} + /** * It combines `useTranslate`, `useSetLocale` and `useGetLocale` hooks for a better developer experience. * It returns `i18nProvider` methods under the hood. @@ -11,8 +15,8 @@ import { useTranslate } from "./useTranslate"; * * @see {@link https://refine.dev/docs/i18n/i18n-provider/} for more details. */ -export const useTranslation = () => { - const translate = useTranslate(); +export const useTranslation = ({ ns }: UseTranslationProps = {}) => { + const translate = useTranslate({ ns }); const changeLocale = useSetLocale(); const getLocale = useGetLocale(); diff --git a/packages/react-router/src/use-document-title.ts b/packages/react-router/src/use-document-title.ts index d17597d3e21fe..b9475d4c1cb39 100644 --- a/packages/react-router/src/use-document-title.ts +++ b/packages/react-router/src/use-document-title.ts @@ -1,26 +1,36 @@ -import { useTranslate } from "@refinedev/core"; -import { useEffect } from "react"; +import { useTranslate, type UseTranslationProps } from "@refinedev/core"; +import { useCallback, useEffect } from "react"; type Title = string | { i18nKey: string }; -export const useDocumentTitle = (title?: Title) => { - const translate = useTranslate(); +interface useDocumentTitleOptions { + ns?: UseTranslationProps["ns"]; + + defaultTitle?: string; +} + +export const useDocumentTitle = ( + title?: Title, + options?: useDocumentTitleOptions, +) => { + const translate = useTranslate({ ns: options?.ns }); + + const getTitle = useCallback( + (title: Title) => { + const key = typeof title === "string" ? title : title.i18nKey; + + return translate(key, options?.defaultTitle); + }, + [translate, options?.defaultTitle], + ); useEffect(() => { if (!title) return; - if (typeof title === "string") { - document.title = translate(title); - } else { - document.title = translate(title.i18nKey); - } - }, [title]); + document.title = getTitle(title); + }, [title, getTitle]); return (title: Title) => { - if (typeof title === "string") { - document.title = translate(title); - } else { - document.title = translate(title.i18nKey); - } + document.title = getTitle(title); }; };