From b86648f42cd849a506e4c32d740de26b72681f72 Mon Sep 17 00:00:00 2001 From: Nikita Dobrenko Date: Thu, 20 Jun 2024 11:48:23 +0300 Subject: [PATCH 01/13] feat(mui): editable Data Grid #5744 (#5989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: FXC Intelligence Co-authored-by: Ali Emir Şen --- .changeset/honest-beds-rhyme.md | 10 ++ .changeset/orange-seals-brush.md | 7 ++ .../material-ui/hooks/use-data-grid/index.md | 65 ++++++++++ .../cypress/e2e/all.cy.ts | 48 +++++++ .../src/pages/posts/list.tsx | 9 +- packages/core/src/hooks/data/index.ts | 40 ++++-- packages/core/src/hooks/data/useUpdateMany.ts | 2 +- .../mui/src/hooks/useDataGrid/index.spec.ts | 39 ++++++ packages/mui/src/hooks/useDataGrid/index.ts | 117 +++++++++++++----- 9 files changed, 291 insertions(+), 46 deletions(-) create mode 100644 .changeset/honest-beds-rhyme.md create mode 100644 .changeset/orange-seals-brush.md diff --git a/.changeset/honest-beds-rhyme.md b/.changeset/honest-beds-rhyme.md new file mode 100644 index 000000000000..c799643dcf2d --- /dev/null +++ b/.changeset/honest-beds-rhyme.md @@ -0,0 +1,10 @@ +--- +"@refinedev/mui": minor +--- + +feat: editable feature for MUI Data Grid #5656 + +It is now possible to make MUI Data Grid editable by +setting editable property on specific column. + +Resolves #5656 diff --git a/.changeset/orange-seals-brush.md b/.changeset/orange-seals-brush.md new file mode 100644 index 000000000000..f451bf63312f --- /dev/null +++ b/.changeset/orange-seals-brush.md @@ -0,0 +1,7 @@ +--- +"@refinedev/core": patch +--- + +chore(core): add missing types of data hooks + +Added missing props and return types of data hooks. diff --git a/documentation/docs/ui-integrations/material-ui/hooks/use-data-grid/index.md b/documentation/docs/ui-integrations/material-ui/hooks/use-data-grid/index.md index b521faca3cc9..8209096cf6d7 100644 --- a/documentation/docs/ui-integrations/material-ui/hooks/use-data-grid/index.md +++ b/documentation/docs/ui-integrations/material-ui/hooks/use-data-grid/index.md @@ -270,6 +270,71 @@ const MyComponent = () => { When the `useDataGrid` hook is mounted, it will call the `subscribe` method from the `liveProvider` with some parameters such as `channel`, `resource` etc. It is useful when you want to subscribe to live updates. +## Editing + +The `useDataGrid` hook extends the editing capabilities provided by the [``](https://mui.com/x/react-data-grid/editing/) component from MUI. To enable column editing, set `editable: "true"` on specific column definitions. + +`useDataGrid` leverages [`useUpdate`](https://refine.dev/docs/data/hooks/use-update/) for direct integration with update operations. This change enhances performance and simplifies the interaction model by directly using the update mechanisms provided by Refine. + +Here is how you can define columns to be editable: + +```tsx +const columns = React.useMemo[]>( + () => [ + { + field: "title", + headerName: "Title", + minWidth: 400, + flex: 1, + editable: true, + }, + ], + [], +); +``` + +### Handling Updates + +With the integration of `useUpdate`, processRowUpdate from [``](https://mui.com/x/react-data-grid/editing/) directly interacts with the backend. This method attempts to update the row with the new values, handling the update logic internally. + +The hook now simplifies the handling of updates by removing the need for managing form state transitions explicitly: + +```tsx +const { + dataGridProps, + formProps: { processRowUpdate, formLoading }, +} = useDataGrid(); +``` + +By default, when a cell edit is initiated and completed, the processRowUpdate function will be triggered, which will now use the mutate function from useUpdate to commit changes. + +```tsx +const processRowUpdate = async (newRow: TData, oldRow: TData) => { + try { + await new Promise((resolve, reject) => { + mutate( + { + resource: resourceFromProp as string, + id: newRow.id as string, + values: newRow, + }, + { + onError: (error) => { + reject(error); + }, + onSuccess: (data) => { + resolve(data); + }, + }, + ); + }); + return newRow; + } catch (error) { + return oldRow; + } +}; +``` + ## Properties ### resource diff --git a/examples/table-material-ui-use-data-grid/cypress/e2e/all.cy.ts b/examples/table-material-ui-use-data-grid/cypress/e2e/all.cy.ts index a34bd49aa939..fa21ddb5d93a 100644 --- a/examples/table-material-ui-use-data-grid/cypress/e2e/all.cy.ts +++ b/examples/table-material-ui-use-data-grid/cypress/e2e/all.cy.ts @@ -171,4 +171,52 @@ describe("table-material-ui-use-data-grid", () => { cy.url().should("include", "current=1"); }); + + it("should update a cell", () => { + cy.getMaterialUILoadingCircular().should("not.exist"); + + cy.intercept("/posts/*").as("patchRequest"); + + cy.getMaterialUIColumnHeader(1).click(); + + cy.get(".MuiDataGrid-cell").eq(1).dblclick(); + + cy.get( + ".MuiDataGrid-cell--editing > .MuiInputBase-root > .MuiInputBase-input", + ) + .clear() + .type("Lorem ipsum refine!") + .type("{enter}"); + + cy.wait("@patchRequest"); + + cy.get(".MuiDataGrid-cell").eq(1).should("contain", "Lorem ipsum refine!"); + }); + + it("should not update a cell", () => { + cy.getMaterialUILoadingCircular().should("not.exist"); + + cy.intercept("PATCH", "/posts/*", (request) => { + request.reply({ + statusCode: 500, + }); + }).as("patchRequest"); + + cy.getMaterialUIColumnHeader(1).click(); + + cy.get(".MuiDataGrid-cell").eq(1).dblclick(); + + cy.get( + ".MuiDataGrid-cell--editing > .MuiInputBase-root > .MuiInputBase-input", + ) + .clear() + .type("Lorem ipsum fail!") + .type("{enter}"); + + cy.wait("@patchRequest"); + + cy.get(".MuiDataGrid-cell") + .eq(1) + .should("not.contain", "Lorem ipsum fail!"); + }); }); diff --git a/examples/table-material-ui-use-data-grid/src/pages/posts/list.tsx b/examples/table-material-ui-use-data-grid/src/pages/posts/list.tsx index 1043a77e16fc..0c743d1c4dd1 100644 --- a/examples/table-material-ui-use-data-grid/src/pages/posts/list.tsx +++ b/examples/table-material-ui-use-data-grid/src/pages/posts/list.tsx @@ -14,6 +14,7 @@ export const PostList: React.FC = () => { const { dataGridProps } = useDataGrid({ initialCurrent: 1, initialPageSize: 10, + editable: true, initialSorter: [ { field: "title", @@ -45,7 +46,13 @@ export const PostList: React.FC = () => { type: "number", width: 50, }, - { field: "title", headerName: "Title", minWidth: 400, flex: 1 }, + { + field: "title", + headerName: "Title", + minWidth: 400, + flex: 1, + editable: true, + }, { field: "category.id", headerName: "Category", diff --git a/packages/core/src/hooks/data/index.ts b/packages/core/src/hooks/data/index.ts index 94be2377487f..a6df2ba9040b 100644 --- a/packages/core/src/hooks/data/index.ts +++ b/packages/core/src/hooks/data/index.ts @@ -1,18 +1,34 @@ -export { useList } from "./useList"; -export { useOne } from "./useOne"; -export { useMany } from "./useMany"; +export { useList, UseListProps } from "./useList"; +export { useOne, UseOneProps } from "./useOne"; +export { useMany, UseManyProps } from "./useMany"; -export { useUpdate } from "./useUpdate"; -export { useCreate, UseCreateReturnType } from "./useCreate"; -export { useDelete } from "./useDelete"; +export { useUpdate, UseUpdateProps, UseUpdateReturnType } from "./useUpdate"; +export { useCreate, UseCreateProps, UseCreateReturnType } from "./useCreate"; +export { useDelete, UseDeleteProps, UseDeleteReturnType } from "./useDelete"; -export { useCreateMany, UseCreateManyReturnType } from "./useCreateMany"; -export { useUpdateMany } from "./useUpdateMany"; -export { useDeleteMany } from "./useDeleteMany"; +export { + useCreateMany, + UseCreateManyProps, + UseCreateManyReturnType, +} from "./useCreateMany"; +export { + useUpdateMany, + UseUpdateManyProps, + UseUpdateManyReturnType, +} from "./useUpdateMany"; +export { + useDeleteMany, + UseDeleteManyProps, + UseDeleteManyReturnType, +} from "./useDeleteMany"; export { useApiUrl } from "./useApiUrl"; -export { useCustom } from "./useCustom"; -export { useCustomMutation } from "./useCustomMutation"; +export { useCustom, UseCustomProps } from "./useCustom"; +export { + useCustomMutation, + UseCustomMutationProps, + UseCustomMutationReturnType, +} from "./useCustomMutation"; export { useDataProvider } from "./useDataProvider"; -export { useInfiniteList } from "./useInfiniteList"; +export { useInfiniteList, UseInfiniteListProps } from "./useInfiniteList"; diff --git a/packages/core/src/hooks/data/useUpdateMany.ts b/packages/core/src/hooks/data/useUpdateMany.ts index c2fbbaffa578..d1824217137d 100644 --- a/packages/core/src/hooks/data/useUpdateMany.ts +++ b/packages/core/src/hooks/data/useUpdateMany.ts @@ -134,7 +134,7 @@ type UpdateManyParams = { { ids: BaseKey[]; values: TVariables } >; -type UseUpdateManyReturnType< +export type UseUpdateManyReturnType< TData extends BaseRecord = BaseRecord, TError extends HttpError = HttpError, TVariables = {}, diff --git a/packages/mui/src/hooks/useDataGrid/index.spec.ts b/packages/mui/src/hooks/useDataGrid/index.spec.ts index cb109274e3d4..2ec32456d714 100644 --- a/packages/mui/src/hooks/useDataGrid/index.spec.ts +++ b/packages/mui/src/hooks/useDataGrid/index.spec.ts @@ -5,6 +5,7 @@ import { MockJSONServer, TestWrapper } from "@test"; import { useDataGrid } from "./"; import type { CrudFilters } from "@refinedev/core"; import { act } from "react-dom/test-utils"; +import { posts } from "@test/dataMocks"; describe("useDataGrid Hook", () => { it("controlled filtering with 'onSubmit' and 'onSearch'", async () => { @@ -198,4 +199,42 @@ describe("useDataGrid Hook", () => { expect(result.current.overtime.elapsedTime).toBeUndefined(); }); }); + + it("when processRowUpdate is called, update data", async () => { + let postToUpdate: any = posts[0]; + + const { result } = renderHook( + () => + useDataGrid({ + resource: "posts", + editable: true, + }), + { + wrapper: TestWrapper({ + dataProvider: { + ...MockJSONServer, + update: async (data) => { + const resolvedData = await Promise.resolve({ data }); + postToUpdate = resolvedData.data.variables; + }, + }, + }), + }, + ); + const newPost = { + ...postToUpdate, + title: "New title", + }; + + await act(async () => { + if (result.current.dataGridProps.processRowUpdate) { + await result.current.dataGridProps.processRowUpdate( + newPost, + postToUpdate, + ); + } + }); + + expect(newPost).toEqual(postToUpdate); + }); }); diff --git a/packages/mui/src/hooks/useDataGrid/index.ts b/packages/mui/src/hooks/useDataGrid/index.ts index f7b3373f161c..a6c0f561cc06 100644 --- a/packages/mui/src/hooks/useDataGrid/index.ts +++ b/packages/mui/src/hooks/useDataGrid/index.ts @@ -1,14 +1,17 @@ import { + useUpdate, + useLiveMode, + pickNotDeprecated, + useTable as useTableCore, type BaseRecord, type CrudFilters, type HttpError, type Pagination, - pickNotDeprecated, type Prettify, - useLiveMode, - useTable as useTableCore, + type UseUpdateProps, type useTableProps as useTablePropsCore, type useTableReturnType as useTableReturnTypeCore, + useResourceParams, } from "@refinedev/core"; import { useState } from "react"; @@ -49,37 +52,50 @@ type DataGridPropsType = Required< > & Pick< DataGridProps, - "paginationModel" | "onPaginationModelChange" | "filterModel" + | "paginationModel" + | "onPaginationModelChange" + | "filterModel" + | "processRowUpdate" >; -export type UseDataGridProps = - Omit< - useTablePropsCore, - "pagination" | "filters" - > & { - onSearch?: (data: TSearchVariables) => CrudFilters | Promise; - pagination?: Prettify< - Omit & { - /** - * Initial number of items per page - * @default 25 - */ - pageSize?: number; - } - >; - filters?: Prettify< - Omit< - NonNullable["filters"]>, - "defaultBehavior" - > & { - /** - * Default behavior of the `setFilters` function - * @default "replace" - */ - defaultBehavior?: "replace" | "merge"; - } - >; - }; +export type UseDataGridProps< + TQueryFnData, + TError extends HttpError, + TSearchVariables, + TData extends BaseRecord, +> = Omit< + useTablePropsCore, + "pagination" | "filters" +> & { + onSearch?: (data: TSearchVariables) => CrudFilters | Promise; + pagination?: Prettify< + Omit & { + /** + * Initial number of items per page + * @default 25 + */ + pageSize?: number; + } + >; + filters?: Prettify< + Omit< + NonNullable["filters"]>, + "defaultBehavior" + > & { + /** + * Default behavior of the `setFilters` function + * @default "replace" + */ + defaultBehavior?: "replace" | "merge"; + } + >; + editable?: boolean; + updateMutationOptions?: UseUpdateProps< + TData, + TError, + TData + >["mutationOptions"]; +}; export type UseDataGridReturnType< TData extends BaseRecord = BaseRecord, @@ -134,6 +150,8 @@ export function useDataGrid< metaData, dataProviderName, overtimeOptions, + editable = false, + updateMutationOptions, }: UseDataGridProps< TQueryFnData, TError, @@ -145,6 +163,8 @@ export function useDataGrid< const [columnsTypes, setColumnsType] = useState>(); + const { identifier } = useResourceParams({ resource: resourceFromProp }); + const { tableQueryResult, current, @@ -263,6 +283,38 @@ export function useDataGrid< }; }; + const { mutate } = useUpdate({ + mutationOptions: updateMutationOptions, + }); + + const processRowUpdate = async (newRow: TData, oldRow: TData) => { + if (!editable) { + return Promise.resolve(oldRow); + } + + if (!identifier) { + return Promise.reject(new Error("Resource is not defined")); + } + + return new Promise((resolve, reject) => { + mutate( + { + resource: identifier, + id: newRow.id as string, + values: newRow, + }, + { + onError: (error) => { + reject(error); + }, + onSuccess: (data) => { + resolve(newRow); + }, + }, + ); + }); + }; + return { tableQueryResult, dataGridProps: { @@ -310,6 +362,7 @@ export function useDataGrid< )}`, }, }, + processRowUpdate: editable ? processRowUpdate : undefined, }, current, setCurrent, From 82170288209653b096b996cf31854434d19c01cd Mon Sep 17 00:00:00 2001 From: YAMADA Yutaka Date: Thu, 20 Jun 2024 17:49:01 +0900 Subject: [PATCH 02/13] fix(nestjs-query): can specify `0` as filter value (#6023) --- .changeset/sharp-lies-wash.md | 19 ++++++++ packages/nestjs-query/src/utils/index.ts | 6 ++- .../test/utils/generateFilters.spec.ts | 45 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 .changeset/sharp-lies-wash.md diff --git a/.changeset/sharp-lies-wash.md b/.changeset/sharp-lies-wash.md new file mode 100644 index 000000000000..b1d0f6b15b34 --- /dev/null +++ b/.changeset/sharp-lies-wash.md @@ -0,0 +1,19 @@ +--- +"@refinedev/nestjs-query": patch +--- + +fix: can specify 0 as filter value + +The following values do not apply to the filter. + +- null +- undefined +- NaN +- Infinity / -Infinity + +The following values can apply to the filter. + +- 0 +- "" + +Resolves #6022 diff --git a/packages/nestjs-query/src/utils/index.ts b/packages/nestjs-query/src/utils/index.ts index f049c1056ab4..d5614c9ff4d7 100644 --- a/packages/nestjs-query/src/utils/index.ts +++ b/packages/nestjs-query/src/utils/index.ts @@ -161,8 +161,12 @@ export const generateFilters = (filters: LogicalFilter[]) => { if (Array.isArray(f.value) && f.value.length === 0) { return false; } + if (typeof f.value === "number") { + return Number.isFinite(f.value); + } - return !!f.value; + // If the value is null or undefined, it returns false. + return !(f.value == null); }) .map((filter: LogicalFilter | CrudFilter) => { if (filter.operator === "and" || filter.operator === "or") { diff --git a/packages/nestjs-query/test/utils/generateFilters.spec.ts b/packages/nestjs-query/test/utils/generateFilters.spec.ts index 2029805f660d..0741a11289a5 100644 --- a/packages/nestjs-query/test/utils/generateFilters.spec.ts +++ b/packages/nestjs-query/test/utils/generateFilters.spec.ts @@ -151,6 +151,51 @@ describe("generateFilters", () => { }, ]; + testCases.forEach(({ filters, expected }) => { + const result = generateFilters(filters as LogicalFilter[]); + expect(result).toEqual(expected); + }); + }); + it("should generate filter when value is valid", () => { + const testCases: { filters: CrudFilter[]; expected: any }[] = [ + { + filters: [{ operator: "eq", field: "name", value: "" }], + expected: { name: { eq: "" } }, + }, + { + filters: [{ operator: "eq", field: "name", value: null }], + expected: {}, + }, + { + filters: [{ operator: "eq", field: "name", value: undefined }], + expected: {}, + }, + { + filters: [{ operator: "eq", field: "age", value: 0 }], + expected: { age: { eq: 0 } }, + }, + { + filters: [{ operator: "eq", field: "age", value: Number.NaN }], + expected: {}, + }, + { + filters: [ + { operator: "eq", field: "age", value: Number.POSITIVE_INFINITY }, + ], + expected: {}, + }, + { + filters: [ + { operator: "eq", field: "age", value: Number.NEGATIVE_INFINITY }, + ], + expected: {}, + }, + { + filters: [{ operator: "between", field: "age", value: [] }], + expected: {}, + }, + ]; + testCases.forEach(({ filters, expected }) => { const result = generateFilters(filters as LogicalFilter[]); expect(result).toEqual(expected); From 24db047aea42e307a9662c46fde50ea69ca8c381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Emir=20=C5=9Een?= Date: Thu, 20 Jun 2024 11:49:17 +0300 Subject: [PATCH 03/13] fix(cli): type imports in swizzle command (#6039) --- .changeset/slow-cougars-sit.md | 9 +++ packages/cli/src/utils/swizzle/import.test.ts | 65 +++++++++++++++++-- packages/cli/src/utils/swizzle/import.ts | 30 +++++---- 3 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 .changeset/slow-cougars-sit.md diff --git a/.changeset/slow-cougars-sit.md b/.changeset/slow-cougars-sit.md new file mode 100644 index 000000000000..aa24b6b46276 --- /dev/null +++ b/.changeset/slow-cougars-sit.md @@ -0,0 +1,9 @@ +--- +"@refinedev/cli": patch +--- + +fix(cli): type imports are breaking the code structure on swizzle + +When exporting elements with `swizzle` command, it will try to replace and combine imports from Refine packages. This process was broken if the target file was using `import type` syntax. This PR updates swizzle command to handle `import type` syntax separately. + +Resolves [#6035](https://github.com/refinedev/refine/issues/6035) diff --git a/packages/cli/src/utils/swizzle/import.test.ts b/packages/cli/src/utils/swizzle/import.test.ts index 3d6841733bfd..2274ad999249 100644 --- a/packages/cli/src/utils/swizzle/import.test.ts +++ b/packages/cli/src/utils/swizzle/import.test.ts @@ -9,39 +9,60 @@ describe("getImports", () => { import * as Antd from "antd"; import { Button as AntButton, TextInput } from "antd"; import { Button as AntButton, TextInput as AntTextInput } from "antd"; + import type { IAuthProvider } from "@refinedev/core"; + import { type BaseRecord } from "@refinedev/core"; + `; const expected = [ { + isType: false, statement: 'import React from "react";', importPath: "react", defaultImport: "React", }, { + isType: false, statement: 'import { Button } from "antd";', importPath: "antd", - namedImports: " { Button }", + namedImports: "{ Button }", }, { + isType: false, statement: 'import { TextInput as AntTextInput } from "antd";', importPath: "antd", - namedImports: " { TextInput as AntTextInput }", + namedImports: "{ TextInput as AntTextInput }", }, { + isType: false, statement: 'import * as Antd from "antd";', importPath: "antd", namespaceImport: "Antd", }, { + isType: false, statement: 'import { Button as AntButton, TextInput } from "antd";', importPath: "antd", - namedImports: " { Button as AntButton, TextInput }", + namedImports: "{ Button as AntButton, TextInput }", }, { + isType: false, statement: 'import { Button as AntButton, TextInput as AntTextInput } from "antd";', importPath: "antd", - namedImports: " { Button as AntButton, TextInput as AntTextInput }", + namedImports: "{ Button as AntButton, TextInput as AntTextInput }", + }, + { + isType: true, + statement: 'import type { IAuthProvider } from "@refinedev/core";', + importPath: "@refinedev/core", + namedImports: "{ IAuthProvider }", + }, + { + isType: false, + statement: 'import { type BaseRecord } from "@refinedev/core";', + importPath: "@refinedev/core", + namedImports: "{ type BaseRecord }", }, ]; @@ -52,7 +73,7 @@ describe("getImports", () => { describe("getNameChangeInImport", () => { it("should get all name changes", () => { const statement = ` - { Button as AntButton, TextInput as AntTextInput } + { Button as AntButton, TextInput as AntTextInput, type ButtonProps, type TextInputProps as AntTextInputProps } `; const expected = [ @@ -63,10 +84,16 @@ describe("getNameChangeInImport", () => { afterCharacter: ",", }, { - statement: " TextInput as AntTextInput ", + statement: " TextInput as AntTextInput,", fromName: "TextInput", toName: "AntTextInput", + afterCharacter: ",", + }, + { afterCharacter: undefined, + fromName: "type TextInputProps", + statement: " type TextInputProps as AntTextInputProps ", + toName: "AntTextInputProps", }, ]; @@ -196,6 +223,32 @@ import { Button, TextInput } from "antd"; import React from "react"; import { Button, TextInput } from "antd"; import type { Layout } from "antd"; +`; + + expect(reorderImports(content).trim()).toEqual(expected.trim()); + }); + + it("should keep type imports with content", () => { + const content = ` +import type { AxiosInstance } from "axios"; +import { stringify } from "query-string"; +import type { DataProvider } from "@refinedev/core"; +import { axiosInstance, generateSort, generateFilter } from "./utils"; + +type MethodTypes = "get" | "delete" | "head" | "options"; +type MethodTypesWithBody = "post" | "put" | "patch"; +`; + + const expected = ` +import { axiosInstance, generateSort, generateFilter } from "./utils"; +import { stringify } from "query-string"; +import type { AxiosInstance } from "axios"; +import type { DataProvider } from "@refinedev/core"; + + + +type MethodTypes = "get" | "delete" | "head" | "options"; +type MethodTypesWithBody = "post" | "put" | "patch"; `; expect(reorderImports(content).trim()).toEqual(expected.trim()); diff --git a/packages/cli/src/utils/swizzle/import.ts b/packages/cli/src/utils/swizzle/import.ts index e9e7f3fa3f78..db1c9d93537d 100644 --- a/packages/cli/src/utils/swizzle/import.ts +++ b/packages/cli/src/utils/swizzle/import.ts @@ -1,5 +1,5 @@ const packageRegex = - /import(?:(?:(?:[ \n\t]+([^ *\n\t\{\},]+)[ \n\t]*(?:,|[ \n\t]+))?([ \n\t]*\{(?:[ \n\t]*[^ \n\t"'\{\}]+[ \n\t]*,?)+\})?[ \n\t]*)|[ \n\t]*\*[ \n\t]*as[ \n\t]+([^ \n\t\{\}]+)[ \n\t]+)from[ \n\t]*(?:['"])([^'"\n]+)(?:['"])(?:;?)/g; + /import(?:\s+(type))?\s*(?:([^\s\{\},]+)\s*(?:,\s*)?)?(\{[^}]+\})?\s*(?:\*\s*as\s+([^\s\{\}]+)\s*)?from\s*['"]([^'"]+)['"];?/g; const nameChangeRegex = /((?:\w|\s|_)*)( as )((?:\w|\s|_)*)( |,)?/g; @@ -9,6 +9,7 @@ export type ImportMatch = { defaultImport?: string; namedImports?: string; namespaceImport?: string; + isType?: boolean; }; export type NameChangeMatch = { @@ -26,6 +27,7 @@ export const getImports = (content: string): Array => { for (const match of matches) { const [ statement, + typePrefix, defaultImport, namedImports, namespaceImport, @@ -33,6 +35,7 @@ export const getImports = (content: string): Array => { ] = match; imports.push({ + isType: typePrefix === "type", statement, importPath, ...(defaultImport && { defaultImport }), @@ -114,17 +117,20 @@ export const reorderImports = (content: string): string => { const allImports = getImports(content); // remove `import type` imports const allModuleImports = allImports.filter( - (importMatch) => !importMatch.statement.includes("import type "), - ); - const typeImports = allImports.filter((importMatch) => - importMatch.statement.includes("import type"), + (importMatch) => !importMatch.isType, ); + const typeImports = allImports.filter((importMatch) => importMatch.isType); const importsWithBeforeContent: ImportMatch[] = []; const importsWithoutBeforeContent: ImportMatch[] = []; + // // remove all type imports + typeImports.forEach((importMatch) => { + newContent = newContent.replace(`${importMatch.statement}\n`, ""); + }); + allModuleImports.forEach((importMatch) => { - if (isImportHasBeforeContent(content, importMatch)) { + if (isImportHasBeforeContent(newContent, importMatch)) { importsWithBeforeContent.push(importMatch); } else { importsWithoutBeforeContent.push(importMatch); @@ -141,11 +147,6 @@ export const reorderImports = (content: string): string => { newContent = newContent.replace(importMatch.statement, ""); }); - // remove all type imports - typeImports.forEach((importMatch) => { - newContent = newContent.replace(importMatch.statement, ""); - }); - // we need to merge the imports from the same package unless one of them is a namespace import] const importsByPackage = importsWithoutBeforeContent.reduce( (acc, importMatch) => { @@ -250,9 +251,10 @@ export const reorderImports = (content: string): string => { const joinedModuleImports = sortedImports .map(([, importLine]) => importLine) .join(""); - const joinedTypeImports = typeImports - .map((importMatch) => importMatch.statement) - .join("\n"); + const joinedTypeImports = [ + ...typeImports.map((importMatch) => importMatch.statement), + "", + ].join("\n"); newContent = newContent.substring(0, insertionPoint) + From 658891c413b1fc83b75905919eabc94f08482e61 Mon Sep 17 00:00:00 2001 From: Apurva Singh <32512696+ApsMJ23@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:25:33 +0530 Subject: [PATCH 04/13] =?UTF-8?q?fix(rtl-customisation):=20changed=20the?= =?UTF-8?q?=20header=20back=20button=20orientations=20o=E2=80=A6=20(#5984)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ali Emir Şen --- .changeset/neat-lobsters-return.md | 24 +++++++++++ .../src/components/pageHeader/index.spec.tsx | 33 +++++++++++++++ .../antd/src/components/pageHeader/index.tsx | 21 +++++----- .../themedLayoutV2/sider/index.spec.tsx | 40 +++++++++++++++++++ .../components/themedLayoutV2/sider/index.tsx | 38 ++++++++++-------- 5 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 .changeset/neat-lobsters-return.md create mode 100644 packages/antd/src/components/pageHeader/index.spec.tsx diff --git a/.changeset/neat-lobsters-return.md b/.changeset/neat-lobsters-return.md new file mode 100644 index 000000000000..7434128a9fc3 --- /dev/null +++ b/.changeset/neat-lobsters-return.md @@ -0,0 +1,24 @@ +--- +"@refinedev/antd": patch +--- + +fix(antd): use appropriate icons for RTL direction layouts + +Previously CRUD components and `` component used hardcoded icons which doesn't fit well for RTL layouts. This PR uses Ant Design's `ConfigProvider` context to use `direction` to determine the appropriate icons for RTL layouts. + +**Example** + +```tsx +import { ConfigProvider } from 'antd'; +import { Refine } from '@refinedev/antd'; + +const App = () => ( + + + +); +``` + +When any CRUD component or `` component is rendered, the icons will be rendered with respect to the `direction` prop of `ConfigProvider`. diff --git a/packages/antd/src/components/pageHeader/index.spec.tsx b/packages/antd/src/components/pageHeader/index.spec.tsx new file mode 100644 index 000000000000..b85cd62df681 --- /dev/null +++ b/packages/antd/src/components/pageHeader/index.spec.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { render, TestWrapper } from "@test"; +import { PageHeader } from "./"; +import { ConfigProvider } from "antd"; + +describe("PageHeader", () => { + it("should render default back button with respect to direction from config", async () => { + const { rerender, queryByLabelText } = render( + + 0} title="title"> + content + + , + { + wrapper: TestWrapper({}), + }, + ); + + expect(queryByLabelText("arrow-left")).toBeTruthy(); + expect(queryByLabelText("arrow-right")).toBeFalsy(); + + rerender( + + 0} title="title"> + content + + , + ); + + expect(queryByLabelText("arrow-left")).toBeFalsy(); + expect(queryByLabelText("arrow-right")).toBeTruthy(); + }); +}); diff --git a/packages/antd/src/components/pageHeader/index.tsx b/packages/antd/src/components/pageHeader/index.tsx index 6e3a8ee3d269..1ffd58d63ad5 100644 --- a/packages/antd/src/components/pageHeader/index.tsx +++ b/packages/antd/src/components/pageHeader/index.tsx @@ -1,22 +1,25 @@ -import React, { type FC } from "react"; +import React, { useContext, type FC } from "react"; import { PageHeader as AntdPageHeader, type PageHeaderProps as AntdPageHeaderProps, } from "@ant-design/pro-layout"; -import { Button, Typography } from "antd"; -import { ArrowLeftOutlined } from "@ant-design/icons"; +import { Button, ConfigProvider, Typography } from "antd"; +import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons"; import { RefinePageHeaderClassNames } from "@refinedev/ui-types"; export type PageHeaderProps = AntdPageHeaderProps; export const PageHeader: FC = ({ children, ...props }) => { + const direction = useContext(ConfigProvider.ConfigContext)?.direction; + const renderBackButton = () => { + const BackIcon = + direction === "rtl" ? ArrowRightOutlined : ArrowLeftOutlined; + + // @ts-expect-error Ant Design Icon's v5.0.1 has an issue with @types/react@^18.2.66 + return } > From 55cd0662b1e3ff8f8410eba812e80130afe75d14 Mon Sep 17 00:00:00 2001 From: Jay Bhensdadia <101461017+JayBhensdadia@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:59:48 +0530 Subject: [PATCH 05/13] fix(core): ensure consistent casing in resource names for access control (#6021) --- .changeset/eleven-maps-glow.md | 12 ++++ .changeset/ten-eyes-hunt.md | 9 +++ .../docs/advanced-tutorials/real-time.md | 4 +- .../app-crm/src/components/layout/sider.tsx | 4 +- .../src/components/sider/index.tsx | 2 +- .../src/components/sider/index.tsx | 2 +- .../src/components/sider/index.tsx | 2 +- .../src/components/layout/sider/index.tsx | 4 +- .../src/components/layout/sider/index.tsx | 4 +- .../components/themedLayout/sider/index.tsx | 4 +- .../components/themedLayoutV2/sider/index.tsx | 4 +- .../src/components/layout/sider/index.tsx | 2 +- .../components/themedLayout/sider/index.tsx | 2 +- .../components/themedLayoutV2/sider/index.tsx | 2 +- .../src/components/layout/sider/index.tsx | 2 +- .../components/themedLayout/sider/index.tsx | 2 +- .../components/themedLayoutV2/sider/index.tsx | 2 +- .../mui/src/components/layout/sider/index.tsx | 4 +- .../components/themedLayout/sider/index.tsx | 4 +- .../components/themedLayoutV2/sider/index.tsx | 4 +- packages/ui-tests/src/tests/layout/sider.tsx | 68 +++++++++++++++++++ 21 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 .changeset/eleven-maps-glow.md create mode 100644 .changeset/ten-eyes-hunt.md diff --git a/.changeset/eleven-maps-glow.md b/.changeset/eleven-maps-glow.md new file mode 100644 index 000000000000..38adaa13cf44 --- /dev/null +++ b/.changeset/eleven-maps-glow.md @@ -0,0 +1,12 @@ +--- +"@refinedev/chakra-ui": patch +"@refinedev/mantine": patch +"@refinedev/antd": patch +"@refinedev/mui": patch +--- + +fix: ensure Sider component handles various resource name formats correctly + +Updated Sider component to correctly handle lowercase and camelcased resource names, enhancing usability and functionality. + +Fixes #6004 diff --git a/.changeset/ten-eyes-hunt.md b/.changeset/ten-eyes-hunt.md new file mode 100644 index 000000000000..fb21374ddeb0 --- /dev/null +++ b/.changeset/ten-eyes-hunt.md @@ -0,0 +1,9 @@ +--- +"@refinedev/ui-tests": patch +--- + +fix: update tests to handle lowercase and camelcased resource names correctly + +This update ensures that resource names are correctly handled in both lowercase and camelcased formats, improving test coverage and accuracy. + +Fixes #6004 diff --git a/documentation/docs/advanced-tutorials/real-time.md b/documentation/docs/advanced-tutorials/real-time.md index 1377a03d9120..d408a25a3ee5 100644 --- a/documentation/docs/advanced-tutorials/real-time.md +++ b/documentation/docs/advanced-tutorials/real-time.md @@ -285,7 +285,7 @@ export const CustomSider: typeof Sider = ({ render }) => { return ( @@ -514,7 +514,7 @@ export const CustomSider: typeof Sider = ({ render }) => { return ( diff --git a/examples/app-crm/src/components/layout/sider.tsx b/examples/app-crm/src/components/layout/sider.tsx index 4a8e5b279178..5b6fc40005af 100644 --- a/examples/app-crm/src/components/layout/sider.tsx +++ b/examples/app-crm/src/components/layout/sider.tsx @@ -64,7 +64,7 @@ export const Sider: React.FC = () => { return ( { return ( { return ( diff --git a/examples/customization-sider/src/components/sider/index.tsx b/examples/customization-sider/src/components/sider/index.tsx index 09cd54865b4a..e28d86564a19 100644 --- a/examples/customization-sider/src/components/sider/index.tsx +++ b/examples/customization-sider/src/components/sider/index.tsx @@ -67,7 +67,7 @@ export const CustomSider: typeof Sider = ({ render }) => { return ( diff --git a/examples/live-provider-ably/src/components/sider/index.tsx b/examples/live-provider-ably/src/components/sider/index.tsx index 9402cec9cc6c..2fd54ce136fd 100644 --- a/examples/live-provider-ably/src/components/sider/index.tsx +++ b/examples/live-provider-ably/src/components/sider/index.tsx @@ -76,7 +76,7 @@ export const CustomSider: typeof Sider = ({ render }) => { return ( diff --git a/examples/mern-dashboard-client/src/components/layout/sider/index.tsx b/examples/mern-dashboard-client/src/components/layout/sider/index.tsx index 159524005f03..f2077f577bbc 100644 --- a/examples/mern-dashboard-client/src/components/layout/sider/index.tsx +++ b/examples/mern-dashboard-client/src/components/layout/sider/index.tsx @@ -86,7 +86,7 @@ export const Sider: typeof DefaultSider = ({ render }) => { return ( { return ( diff --git a/packages/antd/src/components/layout/sider/index.tsx b/packages/antd/src/components/layout/sider/index.tsx index 781b590abdd0..cee77df0fad0 100644 --- a/packages/antd/src/components/layout/sider/index.tsx +++ b/packages/antd/src/components/layout/sider/index.tsx @@ -74,7 +74,7 @@ export const Sider: React.FC = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( diff --git a/packages/mui/src/components/themedLayout/sider/index.tsx b/packages/mui/src/components/themedLayout/sider/index.tsx index 7d4921798c65..20d75f024e09 100644 --- a/packages/mui/src/components/themedLayout/sider/index.tsx +++ b/packages/mui/src/components/themedLayout/sider/index.tsx @@ -111,7 +111,7 @@ export const ThemedSider: React.FC = ({ return ( = ({ return ( diff --git a/packages/mui/src/components/themedLayoutV2/sider/index.tsx b/packages/mui/src/components/themedLayoutV2/sider/index.tsx index 0c133ea3157d..e66cac496e0e 100644 --- a/packages/mui/src/components/themedLayoutV2/sider/index.tsx +++ b/packages/mui/src/components/themedLayoutV2/sider/index.tsx @@ -111,7 +111,7 @@ export const ThemedSiderV2: React.FC = ({ return ( = ({ return ( diff --git a/packages/ui-tests/src/tests/layout/sider.tsx b/packages/ui-tests/src/tests/layout/sider.tsx index 3dbc3ae5f0b1..4e861efb8d22 100644 --- a/packages/ui-tests/src/tests/layout/sider.tsx +++ b/packages/ui-tests/src/tests/layout/sider.tsx @@ -235,5 +235,73 @@ export const layoutSiderTests = ( return expect(postLink).toHaveStyle("pointer-events: none"); }); }); + + it("should handle lowercase resource names correctly", async () => { + const { getByText, getAllByText } = render(, { + wrapper: TestWrapper({ + resources: [ + { + name: "posts", + list: "/posts", + }, + { + name: "users", + list: "/users", + }, + ], + accessControlProvider: { + can: ({ action, resource }) => { + if (action === "list" && resource === "posts") { + return Promise.resolve({ can: true }); + } + if (action === "list" && resource === "users") { + return Promise.resolve({ can: false }); + } + return Promise.resolve({ can: false }); + }, + }, + }), + }); + + const postsElements = await waitFor(() => getAllByText("Posts")); + postsElements.forEach((element) => { + expect(element).toBeInTheDocument(); + }); + expect(() => getByText("Users")).toThrow(); + }); + + it("should handle camelcased resource names correctly", async () => { + const { getByText, getAllByText } = render(, { + wrapper: TestWrapper({ + resources: [ + { + name: "blogPosts", + list: "/blog-posts", + }, + { + name: "userProfiles", + list: "/user-profiles", + }, + ], + accessControlProvider: { + can: ({ action, resource }) => { + if (action === "list" && resource === "blogPosts") { + return Promise.resolve({ can: true }); + } + if (action === "list" && resource === "userProfiles") { + return Promise.resolve({ can: false }); + } + return Promise.resolve({ can: false }); + }, + }, + }), + }); + + const blogPostsElements = await waitFor(() => getAllByText("Blog posts")); + blogPostsElements.forEach((element) => { + expect(element).toBeInTheDocument(); + }); + expect(() => getByText("User profiles")).toThrow(); + }); }); }; From 50d21076928ca738ec54cc5bcd17fad2683653dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Emir=20=C5=9Een?= Date: Thu, 20 Jun 2024 16:30:03 +0300 Subject: [PATCH 06/13] fix: replace lodash imports from root with subpath imports (#6052) Co-authored-by: Batuhan Wilhelm --- .changeset/smooth-maps-arrive.md | 9 +++++++++ .changeset/tall-geckos-help.md | 7 +++++++ .changeset/twenty-insects-eat.md | 7 +++++++ packages/devtools-server/src/reload-on-change.ts | 2 +- packages/hasura/src/utils/upperCaseValues.ts | 2 +- packages/inferencer/src/use-relation-fetch/index.ts | 2 +- 6 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 .changeset/smooth-maps-arrive.md create mode 100644 .changeset/tall-geckos-help.md create mode 100644 .changeset/twenty-insects-eat.md diff --git a/.changeset/smooth-maps-arrive.md b/.changeset/smooth-maps-arrive.md new file mode 100644 index 000000000000..40ba010c07fc --- /dev/null +++ b/.changeset/smooth-maps-arrive.md @@ -0,0 +1,9 @@ +--- +"@refinedev/hasura": patch +--- + +fix(hasura): broken lodash import in bundle + +ESM bundle of `@refinedev/hasura` was broken due to incorrect lodash import. Import has been replaced with subdirectory import to get handled properly in the bundling process. + +Fixes [#6044](https://github.com/refinedev/refine/issues/6044) diff --git a/.changeset/tall-geckos-help.md b/.changeset/tall-geckos-help.md new file mode 100644 index 000000000000..287359105095 --- /dev/null +++ b/.changeset/tall-geckos-help.md @@ -0,0 +1,7 @@ +--- +"@refinedev/devtools-server": patch +--- + +fix(devtools-server): lodash import from root + +`@refinedev/devtools-server` was using `lodash` imports from root which are interpreted as CJS imports in the ESM bundle. To avoid any future issues, lodash imports have been replaced with subdirectory imports. diff --git a/.changeset/twenty-insects-eat.md b/.changeset/twenty-insects-eat.md new file mode 100644 index 000000000000..d48b0162416e --- /dev/null +++ b/.changeset/twenty-insects-eat.md @@ -0,0 +1,7 @@ +--- +"@refinedev/inferencer": patch +--- + +fix(inferencer): broken lodash import in bundle + +ESM bundle of `@refinedev/inferencer` was broken due to incorrect lodash import. Import has been replaced with subdirectory import to get handled properly in the bundling process. diff --git a/packages/devtools-server/src/reload-on-change.ts b/packages/devtools-server/src/reload-on-change.ts index 1b95c2b7496b..2f2280e53d29 100644 --- a/packages/devtools-server/src/reload-on-change.ts +++ b/packages/devtools-server/src/reload-on-change.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import { debounce } from "lodash"; +import debounce from "lodash/debounce"; import { DevtoolsEvent, send } from "@refinedev/devtools-shared"; import type { Server } from "ws"; diff --git a/packages/hasura/src/utils/upperCaseValues.ts b/packages/hasura/src/utils/upperCaseValues.ts index 3334b46d4dc6..4d9cf1139ed1 100644 --- a/packages/hasura/src/utils/upperCaseValues.ts +++ b/packages/hasura/src/utils/upperCaseValues.ts @@ -1,4 +1,4 @@ -import { mapValues } from "lodash"; +import mapValues from "lodash/mapValues"; export const upperCaseValues = (obj: any): any => { if (!obj) return undefined; diff --git a/packages/inferencer/src/use-relation-fetch/index.ts b/packages/inferencer/src/use-relation-fetch/index.ts index 74abb2aa62a1..9320e4d0912c 100644 --- a/packages/inferencer/src/use-relation-fetch/index.ts +++ b/packages/inferencer/src/use-relation-fetch/index.ts @@ -13,7 +13,7 @@ import type { InferencerComponentProps, ResourceInferenceAttempt, } from "../types"; -import { get } from "lodash"; +import get from "lodash/get"; import { pickMeta } from "../utilities/get-meta-props"; type UseRelationFetchProps = { From b516c18b828ba8823561d0fefc4afe02b45ce332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Emir=20=C5=9Een?= Date: Wed, 26 Jun 2024 11:06:44 +0300 Subject: [PATCH 07/13] fix(auto-save-indicator): rename key prop to translationKey (#6064) Co-authored-by: Batuhan Wilhelm --- .changeset/witty-papayas-look.md | 13 +++++++++ .../components/autoSaveIndicator/index.tsx | 14 +++++----- .../components/autoSaveIndicator/index.tsx | 14 +++++----- .../components/autoSaveIndicator/index.tsx | 28 ++++++++++++++----- .../components/autoSaveIndicator/index.tsx | 14 +++++----- .../components/autoSaveIndicator/index.tsx | 14 +++++----- 6 files changed, 62 insertions(+), 35 deletions(-) create mode 100644 .changeset/witty-papayas-look.md diff --git a/.changeset/witty-papayas-look.md b/.changeset/witty-papayas-look.md new file mode 100644 index 000000000000..783648fed8db --- /dev/null +++ b/.changeset/witty-papayas-look.md @@ -0,0 +1,13 @@ +--- +"@refinedev/chakra-ui": patch +"@refinedev/mantine": patch +"@refinedev/antd": patch +"@refinedev/core": patch +"@refinedev/mui": patch +--- + +fix(auto-save-indicator): replace reserved `key` prop with `translationKey` in components + +`` components from UI libraries have been using a `` component internally that uses a `key` prop. Since `key` is a reserved prop in React, it was causing a warning in the console. This change replaces the `key` prop with `translationKey` to avoid the warning. + +Resolves [#6067](https://github.com/refinedev/refine/issues/6067) diff --git a/packages/antd/src/components/autoSaveIndicator/index.tsx b/packages/antd/src/components/autoSaveIndicator/index.tsx index 70183b6196a5..bf361446a84b 100644 --- a/packages/antd/src/components/autoSaveIndicator/index.tsx +++ b/packages/antd/src/components/autoSaveIndicator/index.tsx @@ -17,7 +17,7 @@ export const AutoSaveIndicator: React.FC = ({ elements: { success = ( } @@ -25,7 +25,7 @@ export const AutoSaveIndicator: React.FC = ({ ), error = ( } @@ -33,7 +33,7 @@ export const AutoSaveIndicator: React.FC = ({ ), loading = ( } @@ -41,7 +41,7 @@ export const AutoSaveIndicator: React.FC = ({ ), idle = ( } @@ -63,11 +63,11 @@ export const AutoSaveIndicator: React.FC = ({ }; const Message = ({ - key, + translationKey, defaultMessage, icon, }: { - key: string; + translationKey: string; defaultMessage: string; icon: React.ReactNode; }) => { @@ -82,7 +82,7 @@ const Message = ({ fontSize: ".8rem", }} > - {translate(key, defaultMessage)} + {translate(translationKey, defaultMessage)} {icon} ); diff --git a/packages/chakra-ui/src/components/autoSaveIndicator/index.tsx b/packages/chakra-ui/src/components/autoSaveIndicator/index.tsx index 0617646c55dd..a5ccd0ffe3e6 100644 --- a/packages/chakra-ui/src/components/autoSaveIndicator/index.tsx +++ b/packages/chakra-ui/src/components/autoSaveIndicator/index.tsx @@ -17,28 +17,28 @@ export const AutoSaveIndicator: React.FC = ({ elements: { success = ( } /> ), error = ( } /> ), loading = ( } /> ), idle = ( } /> @@ -59,11 +59,11 @@ export const AutoSaveIndicator: React.FC = ({ }; const Message = ({ - key, + translationKey, defaultMessage, icon, }: { - key: string; + translationKey: string; defaultMessage: string; icon: React.ReactNode; }) => { @@ -78,7 +78,7 @@ const Message = ({ marginRight="2" fontSize="sm" > - {translate(key, defaultMessage)} + {translate(translationKey, defaultMessage)} {icon} ); diff --git a/packages/core/src/components/autoSaveIndicator/index.tsx b/packages/core/src/components/autoSaveIndicator/index.tsx index 1a95f21fe5ba..599db8f1618f 100644 --- a/packages/core/src/components/autoSaveIndicator/index.tsx +++ b/packages/core/src/components/autoSaveIndicator/index.tsx @@ -32,10 +32,24 @@ export type AutoSaveIndicatorProps< export const AutoSaveIndicator: React.FC = ({ status, elements: { - success = , - error = , - loading = , - idle = , + success = ( + + ), + error = ( + + ), + loading = ( + + ), + idle = ( + + ), } = {}, }) => { switch (status) { @@ -51,13 +65,13 @@ export const AutoSaveIndicator: React.FC = ({ }; const Message = ({ - key, + translationKey, defaultMessage, }: { - key: string; + translationKey: string; defaultMessage: string; }) => { const translate = useTranslate(); - return {translate(key, defaultMessage)}; + return {translate(translationKey, defaultMessage)}; }; diff --git a/packages/mantine/src/components/autoSaveIndicator/index.tsx b/packages/mantine/src/components/autoSaveIndicator/index.tsx index 036c64227408..19de37e0fea7 100644 --- a/packages/mantine/src/components/autoSaveIndicator/index.tsx +++ b/packages/mantine/src/components/autoSaveIndicator/index.tsx @@ -17,28 +17,28 @@ export const AutoSaveIndicator: React.FC = ({ elements: { success = ( } /> ), error = ( } /> ), loading = ( } /> ), idle = ( } /> @@ -59,11 +59,11 @@ export const AutoSaveIndicator: React.FC = ({ }; const Message = ({ - key, + translationKey, defaultMessage, icon, }: { - key: string; + translationKey: string; defaultMessage: string; icon: React.ReactNode; }) => { @@ -71,7 +71,7 @@ const Message = ({ return ( - {translate(key, defaultMessage)} + {translate(translationKey, defaultMessage)} {icon} diff --git a/packages/mui/src/components/autoSaveIndicator/index.tsx b/packages/mui/src/components/autoSaveIndicator/index.tsx index ff2e887bc5e2..6a4a9f925eaa 100644 --- a/packages/mui/src/components/autoSaveIndicator/index.tsx +++ b/packages/mui/src/components/autoSaveIndicator/index.tsx @@ -15,28 +15,28 @@ export const AutoSaveIndicator: React.FC = ({ elements: { success = ( } /> ), error = ( } /> ), loading = ( } /> ), idle = ( } /> @@ -57,11 +57,11 @@ export const AutoSaveIndicator: React.FC = ({ }; const Message = ({ - key, + translationKey, defaultMessage, icon, }: { - key: string; + translationKey: string; defaultMessage: string; icon: React.ReactNode; }) => { @@ -77,7 +77,7 @@ const Message = ({ flexWrap="wrap" marginRight=".3rem" > - {translate(key, defaultMessage)} + {translate(translationKey, defaultMessage)} {icon} From f4e61b0fae7e78d0c63c09453f4bd11b4c6f8b09 Mon Sep 17 00:00:00 2001 From: Youssef Siam Date: Wed, 26 Jun 2024 11:17:21 +0300 Subject: [PATCH 08/13] fix(supabase): nested embedded resources sorting (#6025) Co-authored-by: Batuhan Wilhelm --- .changeset/cuddly-brooms-leave.md | 5 +++++ packages/supabase/src/dataProvider/index.ts | 2 +- packages/supabase/test/getList/index.spec.ts | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .changeset/cuddly-brooms-leave.md diff --git a/.changeset/cuddly-brooms-leave.md b/.changeset/cuddly-brooms-leave.md new file mode 100644 index 000000000000..09b39faef694 --- /dev/null +++ b/.changeset/cuddly-brooms-leave.md @@ -0,0 +1,5 @@ +--- +"@refinedev/supabase": patch +--- + +fix supabase sorting with nested embedded resources diff --git a/packages/supabase/src/dataProvider/index.ts b/packages/supabase/src/dataProvider/index.ts index 6a624f84180d..d334b9804d72 100644 --- a/packages/supabase/src/dataProvider/index.ts +++ b/packages/supabase/src/dataProvider/index.ts @@ -22,7 +22,7 @@ export const dataProvider = ( } sorters?.map((item) => { - const [foreignTable, field] = item.field.split(/\.(.*)/); + const [foreignTable, field] = item.field.split(/\.(?=[^.]+$)/); if (foreignTable && field) { query diff --git a/packages/supabase/test/getList/index.spec.ts b/packages/supabase/test/getList/index.spec.ts index 39c5910827c8..3ce3bcf3961e 100644 --- a/packages/supabase/test/getList/index.spec.ts +++ b/packages/supabase/test/getList/index.spec.ts @@ -81,6 +81,23 @@ describe("getList", () => { foreignTable: "categories", }); }); + + it("correct sorting object with nested foreignTable", async () => { + await dataProvider(mockSupabaseClient).getList({ + resource: "posts", + sorters: [ + { + field: "categories.tags.title", + order: "asc", + }, + ], + }); + expect(mockSupabaseOrder).toHaveBeenCalledWith("title", { + ascending: true, + foreignTable: "categories.tags", + }); + }); + it("correct sorting object without foreignTable", async () => { await dataProvider(mockSupabaseClient).getList({ resource: "posts", From 4265ae2509f79af9dbca8d52daf5c2f1b4a50a51 Mon Sep 17 00:00:00 2001 From: FatimaSaleem21 <111277464+FatimaSaleem21@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:04:48 +0500 Subject: [PATCH 09/13] fix(core): add unexported types (#6070) Co-authored-by: Batuhan Wilhelm --- .changeset/tough-elephants-teach.md | 9 +++++++ packages/core/src/index.tsx | 39 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .changeset/tough-elephants-teach.md diff --git a/.changeset/tough-elephants-teach.md b/.changeset/tough-elephants-teach.md new file mode 100644 index 000000000000..4f6104a64491 --- /dev/null +++ b/.changeset/tough-elephants-teach.md @@ -0,0 +1,9 @@ +--- +"@refinedev/core": patch +--- + +fix(core): add unexported types in `index.tsx` + +The `refinedev/core` package has many unexported types that are not accessible for use outside the package. This change aims to address this limitation by exporting those missing types. + +Resolves [#6041](https://github.com/refinedev/refine/issues/6041) diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index bb5d413387a6..28b47a3411c3 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -47,22 +47,32 @@ export { AccessControlProvider, AccessControlProvider as AccessControlBindings, CanParams, + CanResponse, CanReturnType, IAccessControlContext, + IAccessControlContextReturnType, } from "./contexts/accessControl/types.js"; export { AuditLogProvider, + IAuditLogContext, ILog, ILogData, LogParams, } from "./contexts/auditLog/types.js"; export { + AuthActionResponse, AuthBindings, AuthProvider, + CheckResponse, + IAuthContext, + IdentityResponse, ILegacyAuthContext, LegacyAuthProvider, + OnErrorResponse, + PermissionResponse, + SuccessNotificationResponse, } from "./contexts/auth/types.js"; export { @@ -107,6 +117,20 @@ export { BaseOption, IQueryKeys, Prettify, + Context, + ContextQuery, + DataProviders, + IDataContext, + GraphQLQueryOptions, + Fields, + NestedField, + PrevContext, + PreviousQuery, + QueryBuilderOptions, + QueryResponse, + RefineError, + ValidationErrors, + VariableOptions, } from "./contexts/data/types.js"; export { @@ -123,6 +147,7 @@ export { export { ILiveContext, + ILiveModeContextProvider, LiveEvent, LiveCommonParams, LiveManyParams, @@ -141,6 +166,7 @@ export { } from "./contexts/notification/types.js"; export { + DashboardPageProps, IRefineContext, IRefineContextOptions, IRefineContextProvider, @@ -158,7 +184,14 @@ export { IResourceContext, IResourceItem, ITreeMenu, + IMenuItem, + ResourceAuditLogPermissions, ResourceBindings, + RouteableProperties, + ResourceRouteComponent, + ResourceRouteComposition, + ResourceRouteDefinition, + ResourceRoutePath, } from "./contexts/resource/types.js"; export { @@ -184,4 +217,10 @@ export { RouterProvider as RouterBindings, } from "./contexts/router/types.js"; +export { + ActionTypes, + IUndoableQueue, + IUndoableQueueContext, +} from "./contexts/undoableQueue/types.js"; + export { IUnsavedWarnContext } from "./contexts/unsavedWarn/types.js"; From 853bef97ed7baf59e74c98fc54c0ed11624fb491 Mon Sep 17 00:00:00 2001 From: Dominic Preap Date: Mon, 1 Jul 2024 17:51:30 +0700 Subject: [PATCH 10/13] feat(core): add `selectedOptionsOrder` in `useSelect` (#6071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Batuhan Wilhelm Co-authored-by: Ali Emir Şen --- .changeset/loud-zebras-lie.md | 11 +++++ .../docs/data/hooks/use-select/index.md | 14 +++++++ .../hooks/use-checkbox-group/index.md | 21 ++++++++++ .../ant-design/hooks/use-radio-group/index.md | 17 ++++++++ .../ant-design/hooks/use-select/index.md | 14 +++++++ .../mantine/hooks/use-select/index.md | 14 +++++++ .../hooks/use-auto-complete/index.md | 14 +++++++ .../hooks/fields/useCheckboxGroup/index.ts | 2 + .../src/hooks/fields/useRadioGroup/index.ts | 2 + .../core/src/hooks/useSelect/index.spec.ts | 42 +++++++++++++++++++ packages/core/src/hooks/useSelect/index.ts | 16 ++++++- .../mui/src/hooks/useAutocomplete/index.ts | 17 +++++--- 12 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 .changeset/loud-zebras-lie.md 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") { From 311dcdc454ee6914218a59198b5d423a4f8e5456 Mon Sep 17 00:00:00 2001 From: Alican Erdurmaz Date: Mon, 1 Jul 2024 15:49:31 +0300 Subject: [PATCH 11/13] fix(antd): useDrawerForm has unused props (#6074) --- .changeset/friendly-papayas-greet.md | 11 ++ .changeset/silver-carrots-sell.md | 29 ++++ .../ant-design/hooks/use-drawer-form/index.md | 57 ++++++-- .../ant-design/hooks/use-form/index.md | 59 ++++++-- .../ant-design/hooks/use-modal-form/index.md | 23 +++- .../ant-design/hooks/use-steps-form/index.md | 36 ++++- .../hooks/form/useDrawerForm/useDrawerForm.ts | 3 +- packages/antd/src/hooks/form/useForm.spec.tsx | 126 +++++++++++++++++- packages/antd/src/hooks/form/useForm.ts | 15 ++- 9 files changed, 313 insertions(+), 46 deletions(-) create mode 100644 .changeset/friendly-papayas-greet.md create mode 100644 .changeset/silver-carrots-sell.md diff --git a/.changeset/friendly-papayas-greet.md b/.changeset/friendly-papayas-greet.md new file mode 100644 index 000000000000..b89fc8143b5a --- /dev/null +++ b/.changeset/friendly-papayas-greet.md @@ -0,0 +1,11 @@ +--- +"@refinedev/antd": minor +--- + +fix: [`useDrawerForm`](https://refine.dev/docs/ui-integrations/ant-design/hooks/use-drawer-form/)'s `submit` and `form` props are not working (#6082). + +- `submit` prop is removed from `useDrawerForm` hook. Instead, you can use `onFinish` prop to handle the form submission. + https://refine.dev/docs/guides-concepts/forms/#modifying-data-before-submission + +- `form` prop is removed from `useDrawerForm` hook. + The purpose of `useDrawerForm` is to create a `form` instance. Because of that `form` instance cannot be passed as a prop. diff --git a/.changeset/silver-carrots-sell.md b/.changeset/silver-carrots-sell.md new file mode 100644 index 000000000000..701357da15f3 --- /dev/null +++ b/.changeset/silver-carrots-sell.md @@ -0,0 +1,29 @@ +--- +"@refinedev/antd": minor +--- + +fix: `useForm`'s `defaultFormValues` prop is not working (#5727). + +From now on, `useForm`, `useDrawerForm`, and `useModalForm` hooks accept the `defaultFormValues` prop to pre-populate the form with data that needs to be displayed. + +```tsx +useForm({ + defaultFormValues: { + title: "Hello World", + }, +}); +``` + +Also, it can be provided as an async function to fetch the default values. The loading state can be tracked using the `defaultFormValuesLoading` state returned from the hook. + +> 🚨 When `action` is "edit" or "clone" a race condition with `async defaultFormValues` may occur. In this case, the form values will be the result of the last completed operation. + +```tsx +const { defaultFormValuesLoading } = useForm({ + defaultFormValues: async () => { + const response = await fetch("https://my-api.com/posts/1"); + const data = await response.json(); + return data; + }, +}); +``` diff --git a/documentation/docs/ui-integrations/ant-design/hooks/use-drawer-form/index.md b/documentation/docs/ui-integrations/ant-design/hooks/use-drawer-form/index.md index 0d2c1c7617fd..88fd5f5f696f 100644 --- a/documentation/docs/ui-integrations/ant-design/hooks/use-drawer-form/index.md +++ b/documentation/docs/ui-integrations/ant-design/hooks/use-drawer-form/index.md @@ -414,6 +414,32 @@ useDrawerForm({ }); ``` +### defaultFormValues + +Default values for the form. Use this to pre-populate the form with data that needs to be displayed. + +```tsx +useForm({ + defaultFormValues: { + title: "Hello World", + }, +}); +``` + +Also, it can be provided as an async function to fetch the default values. The loading state can be tracked using the [`defaultFormValuesLoading`](#defaultformvaluesloading) state returned from the hook. + +> 🚨 When `action` is "edit" or "clone" a race condition with `async defaultFormValues` may occur. In this case, the form values will be the result of the last completed operation. + +```tsx +const { defaultFormValuesLoading } = useForm({ + defaultFormValues: async () => { + const response = await fetch("https://my-api.com/posts/1"); + const data = await response.json(); + return data; + }, +}); +``` + ## Return values ### show @@ -473,6 +499,10 @@ console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... If `autoSave` is enabled, this hook returns `autoSaveProps` object with `data`, `error`, and `status` properties from mutation. +### defaultFormValuesLoading + +If [`defaultFormValues`](#defaultformvalues) is an async function, `defaultFormValuesLoading` will be `true` until the function is resolved. + ## FAQ ### How can I change the form data before submitting it to the API? @@ -548,19 +578,20 @@ export const UserCreate: React.FC = () => { | mutationMode? | [`MutationMode`](#mutationmode) | | hideText? | `boolean` | -| Key | Description | Type | -| ----------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -| show | A function that opens the drawer | `(id?: BaseKey) => void` | -| form | Ant Design form instance | [`FormInstance`](https://ant.design/components/form/#FormInstance) | -| formProps | Ant Design form props | [`FormProps`](/docs/ui-integrations/ant-design/hooks/use-form#properties) | -| drawerProps | Props for managed drawer | [`DrawerProps`](#drawerprops) | -| saveButtonProps | Props for a submit button | `{ disabled: boolean; onClick: () => void; loading: boolean; }` | -| deleteButtonProps | Adds props for delete button | `{ resourceName?: string; recordItemId?: BaseKey; onSuccess?: (data: TData) => void; mutationMode?: MutationMode; hideText?: boolean; }` | -| submit | Submit method, the parameter is the value of the form fields | `() => void` | -| open | Whether the drawer is open or not | `boolean` | -| close | Specify a function that can close the drawer | `() => void` | -| overtime | Overtime loading props | `{ elapsedTime?: number }` | -| autoSaveProps | Auto save props | `{ data: UpdateResponse` \| `undefined, error: HttpError` \| `null, status: "loading"` \| `"error"` \| `"idle"` \| `"success" }` | +| Key | Description | Type | +| ------------------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| show | A function that opens the drawer | `(id?: BaseKey) => void` | +| form | Ant Design form instance | [`FormInstance`](https://ant.design/components/form/#FormInstance) | +| formProps | Ant Design form props | [`FormProps`](/docs/ui-integrations/ant-design/hooks/use-form#properties) | +| drawerProps | Props for managed drawer | [`DrawerProps`](#drawerprops) | +| saveButtonProps | Props for a submit button | `{ disabled: boolean; onClick: () => void; loading: boolean; }` | +| deleteButtonProps | Adds props for delete button | `{ resourceName?: string; recordItemId?: BaseKey; onSuccess?: (data: TData) => void; mutationMode?: MutationMode; hideText?: boolean; }` | +| submit | Submit method, the parameter is the value of the form fields | `() => void` | +| open | Whether the drawer is open or not | `boolean` | +| close | Specify a function that can close the drawer | `() => void` | +| overtime | Overtime loading props | `{ elapsedTime?: number }` | +| autoSaveProps | Auto save props | `{ data: UpdateResponse` \| `undefined, error: HttpError` \| `null, status: "loading"` \| `"error"` \| `"idle"` \| `"success" }` | +| defaultFormValuesLoading | DefaultFormValues loading status of form | `boolean` | ## Example diff --git a/documentation/docs/ui-integrations/ant-design/hooks/use-form/index.md b/documentation/docs/ui-integrations/ant-design/hooks/use-form/index.md index c4d5609b5a1a..38d7c85f4e9a 100644 --- a/documentation/docs/ui-integrations/ant-design/hooks/use-form/index.md +++ b/documentation/docs/ui-integrations/ant-design/hooks/use-form/index.md @@ -871,6 +871,32 @@ useForm({ }); ``` +### defaultFormValues + +Default values for the form. Use this to pre-populate the form with data that needs to be displayed. + +```tsx +useForm({ + defaultFormValues: { + title: "Hello World", + }, +}); +``` + +Also, it can be provided as an async function to fetch the default values. The loading state can be tracked using the [`defaultFormValuesLoading`](#defaultformvaluesloading) state returned from the hook. + +> 🚨 When `action` is "edit" or "clone" a race condition with `async defaultFormValues` may occur. In this case, the form values will be the result of the last completed operation. + +```tsx +const { defaultFormValuesLoading } = useForm({ + defaultFormValues: async () => { + const response = await fetch("https://my-api.com/posts/1"); + const data = await response.json(); + return data; + }, +}); +``` + ## Return Values All [`Refine Core's useForm`](/docs/data/hooks/use-form/) return values also available in `useForm`. @@ -988,6 +1014,10 @@ console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... If `autoSave` is enabled, this hook returns `autoSaveProps` object with `data`, `error`, and `status` properties from mutation. +### defaultFormValuesLoading + +If [`defaultFormValues`](#defaultformvalues) is an async function, `defaultFormValuesLoading` will be `true` until the function is resolved. + ## FAQ ### How can Invalidate other resources? @@ -1083,20 +1113,21 @@ You can use the `meta` property to pass common values to the mutation and the qu ### Return values -| Property | Description | Type | -| --------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| onFinish | Triggers the mutation | `(values?: TVariables) => Promise` \| `UpdateResponse` \| `void`> | -| form | Ant Design form instance | [`FormInstance`](https://ant.design/components/form/#FormInstance) | -| formProps | Ant Design form props | [`FormProps`](https://ant.design/components/form/#Form) | -| saveButtonProps | Props for a submit button | `{ disabled: boolean; onClick: () => void; loading?:boolean; }` | -| redirect | Redirect function for custom redirections | `(redirect:` `"list"`\|`"edit"`\|`"show"`\|`"create"`\| `false` ,`idFromFunction?:` [`BaseKey`](/docs/core/interface-references#basekey)\|`undefined`) => `data` | -| queryResult | Result of the query of a record | [`QueryObserverResult`](https://react-query.tanstack.com/reference/useQuery) | -| mutationResult | Result of the mutation triggered by submitting the form | [`UseMutationResult`](https://react-query.tanstack.com/reference/useMutation) | -| formLoading | Loading state of form request | `boolean` | -| id | Record id for `clone` and `create` action | [`BaseKey`](/docs/core/interface-references#basekey) | -| setId | `id` setter | `Dispatch>` | -| overtime | Overtime loading props | `{ elapsedTime?: number }` | -| autoSaveProps | Auto save props | `{ data: UpdateResponse` \| `undefined, error: HttpError` \| `null, status: "loading"` \| `"error"` \| `"idle"` \| `"success" }` | +| Property | Description | Type | +| ------------------------ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| onFinish | Triggers the mutation | `(values?: TVariables) => Promise` \| `UpdateResponse` \| `void`> | +| form | Ant Design form instance | [`FormInstance`](https://ant.design/components/form/#FormInstance) | +| formProps | Ant Design form props | [`FormProps`](https://ant.design/components/form/#Form) | +| saveButtonProps | Props for a submit button | `{ disabled: boolean; onClick: () => void; loading?:boolean; }` | +| redirect | Redirect function for custom redirections | `(redirect:` `"list"`\|`"edit"`\|`"show"`\|`"create"`\| `false` ,`idFromFunction?:` [`BaseKey`](/docs/core/interface-references#basekey)\|`undefined`) => `data` | +| queryResult | Result of the query of a record | [`QueryObserverResult`](https://react-query.tanstack.com/reference/useQuery) | +| mutationResult | Result of the mutation triggered by submitting the form | [`UseMutationResult`](https://react-query.tanstack.com/reference/useMutation) | +| formLoading | Loading state of form request | `boolean` | +| id | Record id for `clone` and `create` action | [`BaseKey`](/docs/core/interface-references#basekey) | +| setId | `id` setter | `Dispatch>` | +| overtime | Overtime loading props | `{ elapsedTime?: number }` | +| autoSaveProps | Auto save props | `{ data: UpdateResponse` \| `undefined, error: HttpError` \| `null, status: "loading"` \| `"error"` \| `"idle"` \| `"success" }` | +| defaultFormValuesLoading | DefaultFormValues loading status of form | `boolean` | ## Example diff --git a/documentation/docs/ui-integrations/ant-design/hooks/use-modal-form/index.md b/documentation/docs/ui-integrations/ant-design/hooks/use-modal-form/index.md index efdeb365bd48..59883a1d6208 100644 --- a/documentation/docs/ui-integrations/ant-design/hooks/use-modal-form/index.md +++ b/documentation/docs/ui-integrations/ant-design/hooks/use-modal-form/index.md @@ -421,16 +421,30 @@ const modalForm = useModalForm({ ### defaultFormValues -Default values for the form. Use this to pre-populate the form with data that needs to be displayed. This property is only available for `"create"` action. +Default values for the form. Use this to pre-populate the form with data that needs to be displayed. ```tsx -const modalForm = useModalForm({ +useForm({ defaultFormValues: { title: "Hello World", }, }); ``` +Also, it can be provided as an async function to fetch the default values. The loading state can be tracked using the [`defaultFormValuesLoading`](#defaultformvaluesloading) state returned from the hook. + +> 🚨 When `action` is "edit" or "clone" a race condition with `async defaultFormValues` may occur. In this case, the form values will be the result of the last completed operation. + +```tsx +const { defaultFormValuesLoading } = useForm({ + defaultFormValues: async () => { + const response = await fetch("https://my-api.com/posts/1"); + const data = await response.json(); + return data; + }, +}); +``` + ### defaultVisible When `defaultVisible` is `true`, the modal will be visible by default. It is `false` by default. @@ -763,6 +777,10 @@ console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... If `autoSave` is enabled, this hook returns `autoSaveProps` object with `data`, `error`, and `status` properties from mutation. +### defaultFormValuesLoading + +If [`defaultFormValues`](#defaultformvalues) is an async function, `defaultFormValuesLoading` will be `true` until the function is resolved. + ## FAQ ### How can I change the form data before submitting it to the API? @@ -847,6 +865,7 @@ export const UserCreate: React.FC = () => { | mutationResult | Result of the mutation triggered by submitting the form | [`UseMutationResult<{ data: TData }, TError, { resource: string; values: TVariables; }, unknown>`](https://react-query.tanstack.com/reference/useMutation) | | overtime | Overtime loading props | `{ elapsedTime?: number }` | | autoSaveProps | Auto save props | `{ data: UpdateResponse` \| `undefined, error: HttpError` \| `null, status: "loading"` \| `"error"` \| `"idle"` \| `"success" }` | +| defaultFormValuesLoading | DefaultFormValues loading status of form | `boolean` | ## Example diff --git a/documentation/docs/ui-integrations/ant-design/hooks/use-steps-form/index.md b/documentation/docs/ui-integrations/ant-design/hooks/use-steps-form/index.md index a789f68a24cd..36c5106067d5 100644 --- a/documentation/docs/ui-integrations/ant-design/hooks/use-steps-form/index.md +++ b/documentation/docs/ui-integrations/ant-design/hooks/use-steps-form/index.md @@ -1108,6 +1108,32 @@ useStepsForm({ }); ``` +### defaultFormValues + +Default values for the form. Use this to pre-populate the form with data that needs to be displayed. + +```tsx +useForm({ + defaultFormValues: { + title: "Hello World", + }, +}); +``` + +Also, it can be provided as an async function to fetch the default values. The loading state can be tracked using the [`defaultFormValuesLoading`](#defaultformvaluesloading) state returned from the hook. + +> 🚨 When `action` is "edit" or "clone" a race condition with `async defaultFormValues` may occur. In this case, the form values will be the result of the last completed operation. + +```tsx +const { defaultFormValuesLoading } = useForm({ + defaultFormValues: async () => { + const response = await fetch("https://my-api.com/posts/1"); + const data = await response.json(); + return data; + }, +}); +``` + ## Return Values All [`useForm`](/docs/ui-integrations/ant-design/hooks/use-form) return values also available in `useStepsForm`. You can find descriptions on [`useForm`](/docs/ui-integrations/ant-design/hooks/use-form#return-values) docs. @@ -1137,10 +1163,6 @@ It takes in one argument, step, which is a number representing the index of the `submit` is a function that can submit the form. It's useful when you want to submit the form manually. -### defaultFormValuesLoading - -When `action` is `"edit"` or `"clone"`, `useStepsForm` will fetch the data from the API and set it as default values. This prop is `true` when the data is being fetched. - ### overtime `overtime` object is returned from this hook. `elapsedTime` is the elapsed time in milliseconds. It becomes `undefined` when the request is completed. @@ -1155,6 +1177,10 @@ console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... If `autoSave` is enabled, this hook returns `autoSaveProps` object with `data`, `error`, and `status` properties from mutation. +### defaultFormValuesLoading + +If [`defaultFormValues`](#defaultformvalues) is an async function, `defaultFormValuesLoading` will be `true` until the function is resolved. + ## FAQ ### How can I change the form data before submitting it to the API? @@ -1210,10 +1236,10 @@ const { current, gotoStep, stepsProps, formProps, saveButtonProps, onFinish } = | gotoStep | Go to the target step | `(step: number) => void` | | formProps | Ant Design form props | [`FormProps`](/docs/ui-integrations/ant-design/hooks/use-form#formprops) | | form | Ant Design form instance | [`FormInstance`](https://ant.design/components/form/#FormInstance) | -| defaultFormValuesLoading | DefaultFormValues loading status of form | `boolean` | | submit | Submit method, the parameter is the value of the form fields | `() => void` | | overtime | Overtime loading props | `{ elapsedTime?: number }` | | autoSaveProps | Auto save props | `{ data: UpdateResponse` \| `undefined, error: HttpError` \| `null, status: "loading"` \| `"error"` \| `"idle"` \| `"success" }` | +| defaultFormValuesLoading | DefaultFormValues loading status of form | `boolean` | ## Example diff --git a/packages/antd/src/hooks/form/useDrawerForm/useDrawerForm.ts b/packages/antd/src/hooks/form/useDrawerForm/useDrawerForm.ts index 1e676c0ae3c3..e7e24193312c 100644 --- a/packages/antd/src/hooks/form/useDrawerForm/useDrawerForm.ts +++ b/packages/antd/src/hooks/form/useDrawerForm/useDrawerForm.ts @@ -1,5 +1,4 @@ import React, { useCallback } from "react"; -import type { UseFormConfig } from "sunflower-antd"; import type { FormInstance, FormProps, DrawerProps, ButtonProps } from "antd"; import { useTranslate, @@ -20,7 +19,7 @@ import { import { useForm, type UseFormProps, type UseFormReturnType } from "../useForm"; import type { DeleteButtonProps } from "../../../components"; -export interface UseDrawerFormConfig extends UseFormConfig { +export interface UseDrawerFormConfig { action: "show" | "edit" | "create" | "clone"; } diff --git a/packages/antd/src/hooks/form/useForm.spec.tsx b/packages/antd/src/hooks/form/useForm.spec.tsx index dc7aeb38af81..fbe63c622f9f 100644 --- a/packages/antd/src/hooks/form/useForm.spec.tsx +++ b/packages/antd/src/hooks/form/useForm.spec.tsx @@ -5,7 +5,15 @@ import type { IRefineOptions, HttpError } from "@refinedev/core"; import { Form, Input, Select } from "antd"; import { useForm, useSelect } from ".."; -import { MockJSONServer, TestWrapper, render, waitFor, fireEvent } from "@test"; +import { + MockJSONServer, + TestWrapper, + render, + waitFor, + fireEvent, + renderHook, + act, +} from "@test"; import { mockRouterBindings } from "@test/dataMocks"; import { SaveButton } from "@components/buttons"; @@ -25,11 +33,13 @@ const renderForm = ({ refineOptions?: IRefineOptions; }) => { const Page = () => { - const { formProps, saveButtonProps, queryResult, formLoading } = useForm< - IPost, - HttpError, - IPost - >(formParams); + const { + formProps, + saveButtonProps, + queryResult, + formLoading, + defaultFormValuesLoading, + } = useForm(formParams); const postData = queryResult?.data?.data; const { selectProps: categorySelectProps } = useSelect({ @@ -42,7 +52,8 @@ const renderForm = ({ return ( <> - {formLoading &&
loading...
} + {formLoading &&
formLoading
} + {defaultFormValuesLoading &&
defaultFormValuesLoading
}
@@ -245,4 +256,105 @@ describe("useForm hook", () => { expect(queryByText("Field is not valid.")).not.toBeInTheDocument(); }); }); + + it("should accept defaultFormValues", async () => { + const { getByLabelText } = renderForm({ + formParams: { + resource: "posts", + action: "create", + defaultFormValues: { + title: "Default Title", + content: "Default Content", + }, + }, + }); + + await waitFor(() => { + expect(getByLabelText("Title")).toHaveValue("Default Title"); + expect(getByLabelText("Content")).toHaveValue("Default Content"); + }); + }); + + it("should accept defaultFormValues as promise", async () => { + const { getByLabelText } = renderForm({ + formParams: { + resource: "posts", + action: "create", + defaultFormValues: async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + return { + title: "Default Title", + content: "Default Content", + }; + }, + }, + }); + + await waitFor(() => { + expect(getByLabelText("Title")).toHaveValue(""); + expect(getByLabelText("Content")).toHaveValue(""); + }); + + await waitFor(() => { + expect(getByLabelText("Title")).toHaveValue("Default Title"); + expect(getByLabelText("Content")).toHaveValue("Default Content"); + }); + }); + + it("formLoading and defaultFormValuesLoading should work", async () => { + jest.useFakeTimers(); + + const { result } = renderHook( + () => { + return useForm({ + resource: "posts", + action: "edit", + id: "1", + defaultFormValues: async () => { + await new Promise((resolve) => setTimeout(resolve, 200)); + return { + title: "Default Title", + content: "Default Content", + }; + }, + }); + }, + { + wrapper: TestWrapper({ + dataProvider: { + ...MockJSONServer, + getOne: async () => { + await new Promise((resolve) => setTimeout(resolve, 600)); + return { + data: { + id: 1, + title: + "Necessitatibus necessitatibus id et cupiditate provident est qui amet.", + content: "Content", + category: { + id: 1, + }, + tags: ["tag1", "tag2"], + }, + }; + }, + }, + }), + }, + ); + + expect(result.current.formLoading).toBe(true); + expect(result.current.defaultFormValuesLoading).toBe(true); + await act(async () => { + jest.advanceTimersByTime(400); + }); + expect(result.current.formLoading).toBe(true); + expect(result.current.defaultFormValuesLoading).toBe(false); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + expect(result.current.formLoading).toBe(false); + expect(result.current.defaultFormValuesLoading).toBe(false); + }); }); diff --git a/packages/antd/src/hooks/form/useForm.ts b/packages/antd/src/hooks/form/useForm.ts index 4fc277d3c156..33174d4951e6 100644 --- a/packages/antd/src/hooks/form/useForm.ts +++ b/packages/antd/src/hooks/form/useForm.ts @@ -5,7 +5,7 @@ import { Form, type ButtonProps, } from "antd"; -import { useForm as useFormSF } from "sunflower-antd"; +import { useForm as useFormSF, type UseFormConfig } from "sunflower-antd"; import { type AutoSaveProps, flattenObjectKeys, @@ -52,7 +52,8 @@ export type UseFormProps< * @see {@link https://refine.dev/docs/advanced-tutorials/forms/server-side-form-validation/} */ disableServerSideValidation?: boolean; -} & AutoSaveProps; +} & AutoSaveProps & + Pick; export type UseFormReturnType< TQueryFnData extends BaseRecord = BaseRecord, @@ -77,7 +78,10 @@ export type UseFormReturnType< onFinish: ( values?: TVariables, ) => Promise | UpdateResponse | void>; -}; +} & Pick< + ReturnType>, + "defaultFormValuesLoading" + >; /** * `useForm` is used to manage forms. It uses Ant Design {@link https://ant.design/components/form/ Form} data scope management under the hood and returns the required props for managing the form actions. @@ -128,6 +132,7 @@ export const useForm = < id: idFromProps, overtimeOptions, optimisticUpdateMap, + defaultFormValues, disableServerSideValidation: disableServerSideValidationProp = false, }: UseFormProps< TQueryFnData, @@ -153,6 +158,7 @@ export const useForm = < const [formAnt] = Form.useForm(); const formSF = useFormSF({ form: formAnt, + defaultFormValues, }); const { form } = formSF; @@ -264,6 +270,8 @@ export const useForm = < const warnWhenUnsavedChanges = warnWhenUnsavedChangesProp ?? warnWhenUnsavedChangesRefine; + // populate form with data when queryResult is ready or id changes + // form populated via initialValues prop React.useEffect(() => { form.resetFields(); }, [queryResult?.data?.data, id]); @@ -310,6 +318,7 @@ export const useForm = < initialValues: queryResult?.data?.data, }, saveButtonProps, + defaultFormValuesLoading: formSF.defaultFormValuesLoading, ...useFormCoreResult, onFinish: async (values?: TVariables) => { return await onFinish(values ?? formSF.form.getFieldsValue(true)); From 8bc2c1c6790d1e098ce0d98e01f608e3310f7b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Emir=20=C5=9Een?= Date: Thu, 4 Jul 2024 17:11:13 +0300 Subject: [PATCH 12/13] refactor(devtools): updated authentication flow (#6098) --- .changeset/cuddly-wasps-boil.md | 8 + .changeset/curly-peas-swim.md | 7 + .changeset/cyan-paws-press.md | 7 + .changeset/funny-schools-draw.md | 7 + .changeset/short-rabbits-flash.md | 8 + .changeset/small-mangos-matter.md | 8 + .changeset/spotty-turkeys-grab.md | 7 + examples/data-provider-strapi-v4/package.json | 1 - examples/data-provider-strapi/package.json | 1 - examples/store/package.json | 2 +- packages/cli/package.json | 1 - packages/cli/src/cli.ts | 2 - packages/cli/src/commands/proxy/index.ts | 147 --------- packages/devtools-server/package.json | 3 +- packages/devtools-server/src/constants.ts | 8 + packages/devtools-server/src/index.ts | 13 + packages/devtools-server/src/serve-api.ts | 12 +- packages/devtools-server/src/serve-proxy.ts | 287 +++++++----------- packages/devtools-server/tsup.config.ts | 74 +++-- packages/devtools-shared/src/context.tsx | 12 +- packages/devtools-shared/src/event-types.ts | 10 + .../src/components/feature-slide.tsx | 8 +- .../{ => components}/reload-on-changes.tsx | 0 packages/devtools-ui/src/devtools.tsx | 6 +- .../devtools-ui/src/pages/after-login.tsx | 26 +- packages/devtools-ui/src/pages/login.tsx | 97 +++--- packages/devtools-ui/src/utils/constants.ts | 3 + packages/devtools-ui/src/utils/me.ts | 13 +- packages/devtools-ui/src/utils/ory.ts | 5 +- packages/devtools-ui/src/utils/project-id.ts | 21 +- .../devtools/src/utilities/use-selector.tsx | 4 +- pnpm-lock.yaml | 53 ++-- 32 files changed, 388 insertions(+), 473 deletions(-) create mode 100644 .changeset/cuddly-wasps-boil.md create mode 100644 .changeset/curly-peas-swim.md create mode 100644 .changeset/cyan-paws-press.md create mode 100644 .changeset/funny-schools-draw.md create mode 100644 .changeset/short-rabbits-flash.md create mode 100644 .changeset/small-mangos-matter.md create mode 100644 .changeset/spotty-turkeys-grab.md delete mode 100644 packages/cli/src/commands/proxy/index.ts rename packages/devtools-ui/src/{ => components}/reload-on-changes.tsx (100%) create mode 100644 packages/devtools-ui/src/utils/constants.ts diff --git a/.changeset/cuddly-wasps-boil.md b/.changeset/cuddly-wasps-boil.md new file mode 100644 index 000000000000..796ab2c815cd --- /dev/null +++ b/.changeset/cuddly-wasps-boil.md @@ -0,0 +1,8 @@ +--- +"@refinedev/devtools-server": patch +"@refinedev/devtools-ui": patch +--- + +refactor(devtools): updated flow for login callbacks + +Previously, when the login flow had an error, the Devtools UI was displaying it in the secondary window, which was not user-friendly and lead to multiple clients to connect unnecessarily. This change updates the flow to display the error message in the main Devtools window. diff --git a/.changeset/curly-peas-swim.md b/.changeset/curly-peas-swim.md new file mode 100644 index 000000000000..ad3a1629e851 --- /dev/null +++ b/.changeset/curly-peas-swim.md @@ -0,0 +1,7 @@ +--- +"@refinedev/devtools-ui": patch +--- + +chore(devtools-ui): fix slider image loader + +In the welcome page of the Devtools UI, feature slider was re-mounting every image at transition, causing polluted network tab in the browser even though the images were cached and loaded already. This change fixes the issue by loading the images only once and reusing them on transition. diff --git a/.changeset/cyan-paws-press.md b/.changeset/cyan-paws-press.md new file mode 100644 index 000000000000..c9724a9e2a29 --- /dev/null +++ b/.changeset/cyan-paws-press.md @@ -0,0 +1,7 @@ +--- +"@refinedev/cli": patch +--- + +chore(cli): remove unused command + +Previously `@refinedev/cli` had a `proxy` command that is no longer in use and not required in any of the projects. This change removes the command from the CLI without a fallback. diff --git a/.changeset/funny-schools-draw.md b/.changeset/funny-schools-draw.md new file mode 100644 index 000000000000..f6cbe5b0d5ca --- /dev/null +++ b/.changeset/funny-schools-draw.md @@ -0,0 +1,7 @@ +--- +"@refinedev/devtools-shared": patch +--- + +chore(devtools-shared): add login callback events + +Added new events to handle login errors on the main devtools window rather than external windows. This change is accompanied by new event handlers in the `@refinedev/devtools-ui` and `@refinedev/devtools-server` packages. diff --git a/.changeset/short-rabbits-flash.md b/.changeset/short-rabbits-flash.md new file mode 100644 index 000000000000..df1bd32b198f --- /dev/null +++ b/.changeset/short-rabbits-flash.md @@ -0,0 +1,8 @@ +--- +"@refinedev/devtools-shared": patch +"@refinedev/devtools": patch +--- + +chore(devtools): update devtools url fallback values + +Updated fallback values for the Devtools URL and use single fallback value until its provided by the `@refinedev/devtools-server` when client is connected. diff --git a/.changeset/small-mangos-matter.md b/.changeset/small-mangos-matter.md new file mode 100644 index 000000000000..80b042ebed40 --- /dev/null +++ b/.changeset/small-mangos-matter.md @@ -0,0 +1,8 @@ +--- +"@refinedev/devtools-server": patch +"@refinedev/devtools-ui": patch +--- + +refactor(devtools): updated auth flow + +Previously, a proxy in the Devtools Server was used as an auth server to handle sign-ins in the localhost (Devtools Server). This change updates the flow and moves the authentication flow to `https://auth.refine.dev` to handle sign-ins and sign-outs. Now the Devtools Server is only responsible for the connection between the auth server and the user interface while also managing the user's session. diff --git a/.changeset/spotty-turkeys-grab.md b/.changeset/spotty-turkeys-grab.md new file mode 100644 index 000000000000..2178ea9c28a5 --- /dev/null +++ b/.changeset/spotty-turkeys-grab.md @@ -0,0 +1,7 @@ +--- +"@refinedev/devtools-server": patch +--- + +refactor(devtools-server): handle project id without polluting user console + +When project ID is missing in the project, Devtools Server was returning with `400` and `404` status codes, which leads to unwanted logs in the user console. To avoid this, the server now returns a `200` status code with an error message in the response body. This change is accompanied by a new error handler in the `@refinedev/devtools-ui` package to handle the error message and display it in the user interface. diff --git a/examples/data-provider-strapi-v4/package.json b/examples/data-provider-strapi-v4/package.json index f2074eb62736..4e1ccabf4c18 100644 --- a/examples/data-provider-strapi-v4/package.json +++ b/examples/data-provider-strapi-v4/package.json @@ -43,7 +43,6 @@ "@types/react-dom": "^18.0.0", "@vitejs/plugin-react": "^4.2.1", "cypress": "^13.6.3", - "http-proxy-middleware": "^2.0.6", "typescript": "^5.4.2", "vite": "^5.1.6" } diff --git a/examples/data-provider-strapi/package.json b/examples/data-provider-strapi/package.json index 80fbdbc0ca29..317d67965101 100644 --- a/examples/data-provider-strapi/package.json +++ b/examples/data-provider-strapi/package.json @@ -44,7 +44,6 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@vitejs/plugin-react": "^4.2.1", - "http-proxy-middleware": "^2.0.6", "typescript": "^5.4.2", "vite": "^5.1.6" } diff --git a/examples/store/package.json b/examples/store/package.json index d74efe211bdc..c4f2f951b833 100644 --- a/examples/store/package.json +++ b/examples/store/package.json @@ -26,7 +26,7 @@ "body-scroll-lock": "^4.0.0-beta.0", "clsx": "^1.1.1", "cross-env": "^7.0.3", - "http-proxy-middleware": "^2.0.6", + "http-proxy-middleware": "^3.0.0", "keen-slider": "^6.6.3", "lodash": "^4.17.21", "medusa-react": "^0.3.5", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6c69bee792d0..f31b01335cab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,7 +52,6 @@ "globby": "^11.1.0", "gray-matter": "^4.0.3", "handlebars": "^4.7.7", - "http-proxy-middleware": "^2.0.6", "inquirer": "^8.2.5", "inquirer-autocomplete-prompt": "^2.0.0", "jscodeshift": "0.15.2", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 7d9ea00f27fc..e0218b105212 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -5,7 +5,6 @@ import figlet from "figlet"; import checkUpdates from "@commands/check-updates"; import createResource from "@commands/create-resource"; -import proxy from "@commands/proxy"; import { build, dev, run, start } from "@commands/runner"; import swizzle from "@commands/swizzle"; import update from "@commands/update"; @@ -49,7 +48,6 @@ const bootstrap = () => { run(program); checkUpdates(program); whoami(program); - proxy(program); devtools(program); add(program); diff --git a/packages/cli/src/commands/proxy/index.ts b/packages/cli/src/commands/proxy/index.ts deleted file mode 100644 index bd2db1900977..000000000000 --- a/packages/cli/src/commands/proxy/index.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { ENV } from "@utils/env"; -import type { Command } from "commander"; -import express from "express"; -import { createProxyMiddleware } from "http-proxy-middleware"; -import type { OnProxyResCallback } from "http-proxy-middleware/dist/types"; - -const load = (program: Command) => { - return program - .command("proxy") - .description("Manage proxy settings") - .action(action) - .option( - "-p, --port [port]", - "Port to serve the proxy server. You can also set this with the `REFINE_PROXY_PORT` environment variable.", - ENV.REFINE_PROXY_PORT, - ) - .option( - "-t, --target [target]", - "Target to proxy. You can also set this with the `REFINE_PROXY_TARGET` environment variable.", - ENV.REFINE_PROXY_TARGET, - ) - .option( - "-d, --domain [domain]", - "Domain to proxy. You can also set this with the `REFINE_PROXY_DOMAIN` environment variable.", - ENV.REFINE_PROXY_DOMAIN, - ) - .option( - "-r, --rewrite-url [rewrite URL]", - "Rewrite URL for redirects. You can also set this with the `REFINE_PROXY_REWRITE_URL` environment variable.", - ENV.REFINE_PROXY_REWRITE_URL, - ); -}; - -const action = async ({ - port, - target, - domain, - rewriteUrl, -}: { - port: string; - target: string; - domain: string; - rewriteUrl: string; -}) => { - const app = express(); - - const targetUrl = new URL(target); - - const onProxyRes: OnProxyResCallback | undefined = - targetUrl.protocol === "http:" - ? (proxyRes) => { - if (proxyRes.headers["set-cookie"]) { - proxyRes.headers["set-cookie"]?.forEach((cookie, i) => { - if (proxyRes?.headers?.["set-cookie"]) { - proxyRes.headers["set-cookie"][i] = cookie.replace( - "Secure;", - "", - ); - } - }); - } - } - : undefined; - - app.use( - "/.refine", - createProxyMiddleware({ - target: `${domain}/.refine`, - changeOrigin: true, - pathRewrite: { "^/.refine": "" }, - logProvider: () => ({ - log: console.log, - info: (msg) => { - if (`${msg}`.includes("Proxy rewrite rule created")) return; - - if (`${msg}`.includes("Proxy created")) { - console.log( - `Proxying localhost:${port}/.refine to ${domain}/.refine`, - ); - } else if (msg) { - console.log(msg); - } - }, - warn: console.warn, - debug: console.debug, - error: console.error, - }), - }), - ); - - app.use( - "/.auth", - createProxyMiddleware({ - target: `${domain}/.auth`, - changeOrigin: true, - cookieDomainRewrite: { - "refine.dev": "", - }, - headers: { - "auth-base-url-rewrite": `${rewriteUrl}/.auth`, - }, - pathRewrite: { "^/.auth": "" }, - logProvider: () => ({ - log: console.log, - info: (msg) => { - if (`${msg}`.includes("Proxy rewrite rule created")) return; - - if (`${msg}`.includes("Proxy created")) { - console.log(`Proxying localhost:${port}/.auth to ${domain}/.auth`); - } else if (msg) { - console.log(msg); - } - }, - warn: console.warn, - debug: console.debug, - error: console.error, - }), - onProxyRes, - }), - ); - - app.use( - "*", - createProxyMiddleware({ - target: `${target}`, - changeOrigin: true, - ws: true, - logProvider: () => ({ - log: console.log, - info: (msg) => { - if (`${msg}`.includes("Proxy created")) { - console.log(`Proxying localhost:${port} to ${target}`); - } else if (msg) { - console.log(msg); - } - }, - warn: console.warn, - debug: console.debug, - error: console.error, - }), - }), - ); - - app.listen(Number(port)); -}; - -export default load; diff --git a/packages/devtools-server/package.json b/packages/devtools-server/package.json index 6363a170a2ab..0dfbcff8c782 100644 --- a/packages/devtools-server/package.json +++ b/packages/devtools-server/package.json @@ -46,7 +46,6 @@ "types": "node ../shared/generate-declarations.js" }, "dependencies": { - "@ory/client": "^1.5.2", "@refinedev/devtools-shared": "1.1.9", "body-parser": "^1.20.2", "boxen": "^5.1.2", @@ -58,7 +57,7 @@ "fs-extra": "^10.1.0", "globby": "^11.1.0", "gray-matter": "^4.0.3", - "http-proxy-middleware": "^2.0.6", + "http-proxy-middleware": "^3.0.0", "jscodeshift": "0.15.2", "lodash": "^4.17.21", "lodash-es": "^4.17.21", diff --git a/packages/devtools-server/src/constants.ts b/packages/devtools-server/src/constants.ts index d99f874d7f07..6989a7e67649 100644 --- a/packages/devtools-server/src/constants.ts +++ b/packages/devtools-server/src/constants.ts @@ -1,9 +1,17 @@ export const DEFAULT_SERVER_PORT = 5001; export const SERVER_PORT = DEFAULT_SERVER_PORT; +export const AUTH_SERVER_URL = __DEVELOPMENT__ + ? "https://develop.auth.refine.dev" + : "https://auth.refine.dev"; + export const REFINE_API_URL = __DEVELOPMENT__ ? "https://develop.cloud.refine.dev" : "https://cloud2.refine.dev"; +export const AUTH_TRIGGER_API_PATH = "/api/login"; +export const AUTH_CALLBACK_API_PATH = "/api/login-callback"; +export const AUTH_CALLBACK_UI_PATH = "/after-login"; + export const FEED_MD_URL = "https://raw.githubusercontent.com/refinedev/refine/master/packages/devtools-server/FEED.md"; diff --git a/packages/devtools-server/src/index.ts b/packages/devtools-server/src/index.ts index 6c8f31f810fe..4d1aa52fc3f4 100644 --- a/packages/devtools-server/src/index.ts +++ b/packages/devtools-server/src/index.ts @@ -112,6 +112,19 @@ export const server = async ({ }); }); + receive( + client as any, + DevtoolsEvent.DEVTOOLS_LOGIN_FAILURE, + ({ error, code }) => { + ws.clients.forEach((c) => { + send(c as any, DevtoolsEvent.DEVTOOLS_DISPLAY_LOGIN_FAILURE, { + error, + code, + }); + }); + }, + ); + // close connected app if client disconnects client.on("close", (_, reason) => { if (__DEVELOPMENT__) { diff --git a/packages/devtools-server/src/serve-api.ts b/packages/devtools-server/src/serve-api.ts index b9ba39cfe004..30507a4213a7 100644 --- a/packages/devtools-server/src/serve-api.ts +++ b/packages/devtools-server/src/serve-api.ts @@ -131,17 +131,23 @@ export const serveApi = (app: Express, db: Data) => { }); app.get("/api/project-id/status", async (_, res) => { + const CODES = { + OK: 0, + NOT_FOUND: 1, + ERROR: 2, + }; + const projectId = await getProjectIdFromPackageJson(); if (projectId) { - res.status(200).json({ projectId }); + res.status(200).json({ projectId, status: CODES.OK }); return; } if (projectId === false) { - res.status(404).json({ projectId: null }); + res.status(200).json({ projectId: null, status: CODES.NOT_FOUND }); return; } - res.status(500).json({ projectId: null }); + res.status(200).json({ projectId: null, status: CODES.ERROR }); return; }); diff --git a/packages/devtools-server/src/serve-proxy.ts b/packages/devtools-server/src/serve-proxy.ts index 5c8a9dd9a1cb..614dda64de79 100644 --- a/packages/devtools-server/src/serve-proxy.ts +++ b/packages/devtools-server/src/serve-proxy.ts @@ -1,67 +1,22 @@ -import { readJSON, writeJSON } from "fs-extra"; -import { FrontendApi } from "@ory/client"; -import { createProxyMiddleware, type Options } from "http-proxy-middleware"; import path from "path"; -import { REFINE_API_URL, SERVER_PORT } from "./constants"; +import { readJSON, writeJSON } from "fs-extra"; +import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware"; +import { + REFINE_API_URL, + AUTH_SERVER_URL, + AUTH_CALLBACK_API_PATH, + AUTH_CALLBACK_UI_PATH, + AUTH_TRIGGER_API_PATH, +} from "./constants"; import { getProjectIdFromPackageJson } from "./project-id/get-project-id-from-package-json"; import type { Express, RequestHandler } from "express"; -let currentProjectId: string | null | false = null; -const projectIdAppender: RequestHandler = async (req, res, next) => { - if (!currentProjectId) { - currentProjectId = await getProjectIdFromPackageJson(); - } - - if (currentProjectId) { - req.headers["x-project-id"] = currentProjectId; - } - - next(); -}; - -const restream: Options["onProxyReq"] = (proxyReq, req) => { - if (req.body) { - const bodyData = JSON.stringify(req.body); - // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json - proxyReq.setHeader("Content-Type", "application/json"); - proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData)); - // stream the content - proxyReq.write(bodyData); - } -}; - -const tokenize = async (token: string) => { - try { - const ORY_URL = `${REFINE_API_URL}/.auth`; - - const ory = new FrontendApi({ - isJsonMime: () => true, - basePath: ORY_URL, - baseOptions: { - withCredentials: true, - }, - }); - - const { data } = await ory.toSession({ - xSessionToken: token, - tokenizeAs: "jwt_template_1", - }); - - return data?.tokenized; - } catch (err) { - // - } - - return undefined; -}; +const persistPath = path.join(__dirname, "..", ".persist.json"); const saveAuth = async (token?: string, jwt?: string) => { try { - writeJSON(path.join(__dirname, "..", ".persist.json"), { - token: token, - jwt: jwt, - }); + await writeJSON(persistPath, { token, jwt }); } catch (error) { // } @@ -69,138 +24,128 @@ const saveAuth = async (token?: string, jwt?: string) => { const loadAuth = async () => { try { - const persist = await readJSON(path.join(__dirname, "..", ".persist.json")); - return persist as { token?: string; jwt?: string }; + return (await readJSON(persistPath)) as { token?: string; jwt?: string }; } catch (error) { // } - return { - token: undefined, - jwt: undefined, - }; -}; - -const handleLogoutToken: ( - token?: string, -) => NonNullable = (token) => { - return (proxyReq, req) => { - if (req.url.includes("self-service/logout/api")) { - const bodyData = JSON.stringify({ - session_token: token, - }); - proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData)); - // stream the content - proxyReq.write(bodyData); - } - }; -}; - -const handleSignInCallbacks: ( - onToken: (token?: string, jwt?: string) => void, -) => NonNullable = (onToken) => { - return (proxyRes, req, res) => { - let body = ""; - proxyRes.on("data", (chunk) => { - body += chunk; - }); - proxyRes.on("end", () => { - let sessionToken: string | undefined = undefined; - try { - const parsed = JSON.parse(body); - sessionToken = parsed.session_token; - } catch (err) { - // - } - if (!sessionToken) { - if (body?.includes?.("an+account+with+the+same+identifier")) { - res.redirect( - "/after-login?error=An+account+with+the+same+identifier+exists+already", - ); - return; - } - res.redirect("/after-login?error=Invalid-session-token"); - return; - } - - // After grabbing the session_token, convert it to JWT, then redirect to /after-login - tokenize(sessionToken).then((tokenized) => { - onToken(sessionToken, tokenized ?? ""); - res.redirect("/after-login"); - }); - }); - }; + return {}; }; export const serveProxy = async (app: Express) => { let { token, jwt } = await loadAuth(); const authProxy = createProxyMiddleware({ - target: REFINE_API_URL, + target: `${AUTH_SERVER_URL}/api/.auth`, + secure: false, changeOrigin: true, - pathRewrite: { "^/api/.auth": "/.auth" }, - cookieDomainRewrite: { - "refine.dev": "localhost", - }, - logLevel: __DEVELOPMENT__ ? "debug" : "silent", - headers: { - "auth-base-url-rewrite": `http://localhost:${SERVER_PORT}/api/.auth`, - }, - selfHandleResponse: true, - onProxyReq: (proxyReq, req, ...rest) => { - if (token) { - proxyReq.setHeader("X-Session-Token", token ?? ""); - - handleLogoutToken(token)(proxyReq, req, ...rest); - } - }, - onProxyRes: (proxyRes, req, res) => { - const newSetCookie = proxyRes.headers["set-cookie"]?.map((cookie) => - cookie - .replace("Domain=refine.dev;", "Domain=localhost;") - .replace(" HttpOnly; Secure; SameSite=Lax", ""), - ); - if (newSetCookie) proxyRes.headers["set-cookie"] = newSetCookie; - - if (req.url.includes("self-service/methods/oidc/callback")) { - return handleSignInCallbacks((_token, _jwt) => { - token = _token; - jwt = _jwt; - saveAuth(token, jwt); - })(proxyRes, req, res); - } - - if (proxyRes.statusCode === 401) { - res.writeHead(200, { - ...proxyRes.headers, - "Refine-Is-Authenticated": "false", - "Access-Control-Expose-Headers": `Refine-Is-Authenticated, ${proxyRes.headers["Access-Control-Expose-Headers"]}`, - }); - } else { - res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); - } - - proxyRes.pipe(res, { end: true }); + logger: __DEVELOPMENT__ ? console : undefined, + on: { + proxyReq: fixRequestBody, + proxyRes: (_proxyRes, req) => { + if (req.url?.includes("self-service/logout/api")) { + token = undefined; + jwt = undefined; + saveAuth(); + } + }, }, }); - app.use("/api/.auth", authProxy); - - const refineApiProxy = createProxyMiddleware({ - target: REFINE_API_URL, + const refineProxy = createProxyMiddleware({ + target: `${REFINE_API_URL}/.refine`, secure: false, changeOrigin: true, - logLevel: __DEVELOPMENT__ ? "debug" : "silent", - pathRewrite: { "^/api/.refine": "/.refine" }, - onProxyReq: (proxyReq, ...rest) => { - if (jwt) { - proxyReq.setHeader("Authorization", `Bearer ${jwt}`); - proxyReq.removeHeader("cookie"); - } - - restream(proxyReq, ...rest); + logger: __DEVELOPMENT__ ? console : undefined, + on: { + proxyReq: fixRequestBody, }, }); - app.use("/api/.refine", projectIdAppender, refineApiProxy); + let currentProjectId: string | null | false = null; + const projectIdAppender: RequestHandler = async (req, _res, next) => { + if (!currentProjectId) { + currentProjectId = await getProjectIdFromPackageJson(); + } + + if (currentProjectId) { + req.headers["x-project-id"] = currentProjectId; + } + + next(); + }; + + const appendAuth: RequestHandler = async (req, _res, next) => { + if (token) { + req.headers["X-Session-Token"] = token; + } + if (req.url?.includes("self-service/logout/api")) { + req.body = { + session_token: token, + }; + + req.headers["Content-Length"] = Buffer.byteLength( + JSON.stringify(req.body), + ).toString(); + } + + next(); + }; + + const appendJwt: RequestHandler = async (req, _res, next) => { + if (jwt) { + req.headers["Authorization"] = `Bearer ${jwt}`; + delete req.headers["cookie"]; + } + + next(); + }; + + const loginCallback: RequestHandler = async (req, res, _next) => { + const query = req.query; + + if (query.token && query.jwt) { + token = query.token as string; + jwt = query.jwt as string; + await saveAuth(query.token as string, query.jwt as string); + } + + const errorParams = new URLSearchParams(); + if (query.error) { + errorParams.set("error", query.error as string); + } + if (query.code) { + errorParams.set("code", query.code as string); + } + + res.redirect(`${AUTH_CALLBACK_UI_PATH}?${errorParams.toString()}`); + }; + + const loginTrigger: RequestHandler = async (req, res, _next) => { + const query = req.query; + const protocol = req.secure ? "https" : "http"; + const host = req.headers.host; + + if (!host) { + res.redirect(`${AUTH_CALLBACK_API_PATH}?error=Missing%20Host`); + return; + } + + const callbackUrl = `${protocol}://${host}${AUTH_CALLBACK_API_PATH}`; + + const params = new URLSearchParams({ + provider: query.provider as string, + returnUrl: encodeURIComponent(callbackUrl), + }); + + res.redirect(`${AUTH_SERVER_URL}/login?${params.toString()}`); + }; + + app.use(AUTH_TRIGGER_API_PATH, loginTrigger); + + app.use(AUTH_CALLBACK_API_PATH, loginCallback); + + app.use("/api/.auth", appendAuth, authProxy); + + app.use("/api/.refine", projectIdAppender, appendJwt, refineProxy); }; diff --git a/packages/devtools-server/tsup.config.ts b/packages/devtools-server/tsup.config.ts index 272d05456da5..d6ae939fcd61 100644 --- a/packages/devtools-server/tsup.config.ts +++ b/packages/devtools-server/tsup.config.ts @@ -2,34 +2,46 @@ import { defineConfig } from "tsup"; import { NodeResolvePlugin } from "@esbuild-plugins/node-resolve"; import { lodashReplacePlugin } from "../shared/lodash-replace-plugin"; -export default defineConfig((tsupOptions) => ({ - entry: ["src/index.ts", "src/cli.ts"], - splitting: false, - sourcemap: true, - clean: false, - minify: true, - format: ["cjs", "esm"], - outExtension: ({ format }) => ({ js: format === "cjs" ? ".cjs" : ".mjs" }), - platform: "node", - esbuildOptions: (options) => { - options.define = { - ...options.define, - __DEVELOPMENT__: tsupOptions.watch ? "true" : "false", - }; - }, - esbuildPlugins: [ - lodashReplacePlugin, - NodeResolvePlugin({ - extensions: [".js", "ts", "tsx", "jsx"], - onResolved: (resolved) => { - if (resolved.includes("node_modules")) { - return { - external: true, - }; - } - return resolved; - }, - }), - ], - onSuccess: tsupOptions.watch ? "pnpm types" : undefined, -})); +export default defineConfig((tsupOptions) => { + const onSuccess = [ + ...(tsupOptions.watch ? ["pnpm types"] : []), + ...(process.env.STANDALONE_DEVTOOLS_SERVER === "true" && tsupOptions.watch + ? ["pnpm start:server"] + : []), + ].join(" && "); + + return { + entry: ["src/index.ts", "src/cli.ts"], + splitting: false, + sourcemap: true, + clean: false, + minify: true, + format: ["cjs", "esm"], + outExtension: ({ format }) => ({ js: format === "cjs" ? ".cjs" : ".mjs" }), + platform: "node", + esbuildOptions: (options) => { + options.define = { + ...options.define, + __DEVELOPMENT__: + process.env.USE_DEV_ENV === "true" || tsupOptions.watch + ? "true" + : "false", + }; + }, + esbuildPlugins: [ + lodashReplacePlugin, + NodeResolvePlugin({ + extensions: [".js", "ts", "tsx", "jsx"], + onResolved: (resolved) => { + if (resolved.includes("node_modules")) { + return { + external: true, + }; + } + return resolved; + }, + }), + ], + onSuccess: onSuccess ? onSuccess : undefined, + }; +}); diff --git a/packages/devtools-shared/src/context.tsx b/packages/devtools-shared/src/context.tsx index 2cfcdb470df7..602ee979f1e5 100644 --- a/packages/devtools-shared/src/context.tsx +++ b/packages/devtools-shared/src/context.tsx @@ -35,11 +35,13 @@ export const DevToolsContextProvider: React.FC = ({ url: "localhost", secure: false, ws: null, + devtoolsUrl: "http://localhost:5001", }); const [ws, setWs] = React.useState(null); React.useEffect(() => { + let timeout: NodeJS.Timeout | null = null; const wsInstance = new WebSocket( `${values.secure ? "wss" : "ws"}://localhost:${values.port}`, ); @@ -58,9 +60,11 @@ export const DevToolsContextProvider: React.FC = ({ wsInstance.addEventListener("open", () => { if (!values.__devtools) { - send(wsInstance, DevtoolsEvent.DEVTOOLS_INIT, { - url: window.location.origin, - }); + timeout = setTimeout(() => { + send(wsInstance, DevtoolsEvent.DEVTOOLS_INIT, { + url: window.location.origin, + }); + }, 300); } }); @@ -69,6 +73,8 @@ export const DevToolsContextProvider: React.FC = ({ return () => { unsubscribe(); + if (timeout) clearTimeout(timeout); + // In strict mode, the WebSocket instance might not be connected yet // so we need to wait for it to connect before closing it // otherwise it will log an unnecessary error in the console diff --git a/packages/devtools-shared/src/event-types.ts b/packages/devtools-shared/src/event-types.ts index f78799cd6d6e..63364e728fc8 100644 --- a/packages/devtools-shared/src/event-types.ts +++ b/packages/devtools-shared/src/event-types.ts @@ -20,6 +20,8 @@ export enum DevtoolsEvent { DEVTOOLS_HIGHLIGHT_IN_MONITOR = "devtools:highlight-in-monitor", DEVTOOLS_HIGHLIGHT_IN_MONITOR_ACTION = "devtools:highlight-in-monitor-action", DEVTOOLS_LOGIN_SUCCESS = "devtools:login-success", + DEVTOOLS_DISPLAY_LOGIN_FAILURE = "devtools:display-login-failure", + DEVTOOLS_LOGIN_FAILURE = "devtools:login-failure", DEVTOOLS_RELOAD_AFTER_LOGIN = "devtools:reload-after-login", DEVTOOLS_INVALIDATE_QUERY = "devtools:invalidate-query", DEVTOOLS_INVALIDATE_QUERY_ACTION = "devtools:invalidate-query-action", @@ -71,6 +73,14 @@ export type DevtoolsEventPayloads = { [DevtoolsEvent.DEVTOOLS_HIGHLIGHT_IN_MONITOR]: { name: string }; [DevtoolsEvent.DEVTOOLS_HIGHLIGHT_IN_MONITOR_ACTION]: { name: string }; [DevtoolsEvent.DEVTOOLS_LOGIN_SUCCESS]: {}; + [DevtoolsEvent.DEVTOOLS_LOGIN_FAILURE]: { + error: string | null; + code: string | null; + }; + [DevtoolsEvent.DEVTOOLS_DISPLAY_LOGIN_FAILURE]: { + error: string | null; + code: string | null; + }; [DevtoolsEvent.DEVTOOLS_RELOAD_AFTER_LOGIN]: {}; [DevtoolsEvent.DEVTOOLS_INVALIDATE_QUERY]: { queryKey: QueryKey }; [DevtoolsEvent.DEVTOOLS_INVALIDATE_QUERY_ACTION]: { queryKey: QueryKey }; diff --git a/packages/devtools-ui/src/components/feature-slide.tsx b/packages/devtools-ui/src/components/feature-slide.tsx index 41f23b41703d..585f78530e62 100644 --- a/packages/devtools-ui/src/components/feature-slide.tsx +++ b/packages/devtools-ui/src/components/feature-slide.tsx @@ -89,14 +89,14 @@ export const FeatureSlide = (props: { className?: string }) => { "re-relative", )} > - {slides.map((_slide, index) => { + {slides.map((slide, index) => { const active = index === slideIndex; return ( {slides[slideIndex].title} { @@ -17,10 +17,24 @@ export const AfterLogin = () => { }; const Failure = () => { + const { ws } = React.useContext(DevToolsContext); + const [searchParams] = useSearchParams(); const errorParam = searchParams.get("error"); + const errorCode = searchParams.get("code"); + + React.useEffect(() => { + if (ws) { + send(ws, DevtoolsEvent.DEVTOOLS_LOGIN_FAILURE, { + error: errorParam, + code: errorCode, + }).then(() => { + window.close(); + }); + } + }, [ws, errorCode, errorParam]); - return ; + return Login failed.; }; const Success = () => { @@ -34,6 +48,10 @@ const Success = () => { } }, [ws]); + return Logged in successfully, you can close this window.; +}; + +const Wrapper = ({ children }: { children: React.ReactNode }) => { return (
{ "re-text-sm", )} > - Logged in successfully, you can close this window. + {children}
); diff --git a/packages/devtools-ui/src/pages/login.tsx b/packages/devtools-ui/src/pages/login.tsx index 65959986ddbc..f7c361d550d0 100644 --- a/packages/devtools-ui/src/pages/login.tsx +++ b/packages/devtools-ui/src/pages/login.tsx @@ -1,4 +1,3 @@ -import type { LoginFlow } from "@ory/client"; import { DevToolsContext, DevtoolsEvent, @@ -12,7 +11,7 @@ import { FeatureSlide, FeatureSlideMobile } from "src/components/feature-slide"; import { GithubIcon } from "src/components/icons/github"; import { GoogleIcon } from "src/components/icons/google"; import { LogoIcon } from "src/components/icons/logo"; -import { ory } from "src/utils/ory"; +import { AUTH_TRIGGER_API_PATH } from "src/utils/constants"; export const Login = () => { return ( @@ -70,28 +69,10 @@ const providers = [ const LoginForm = (props: { className?: string }) => { const { ws } = React.useContext(DevToolsContext); const [searchParams] = useSearchParams(); - const [flowData, setFlowData] = React.useState(null); + const errorParam = searchParams.get("error"); + const [sentError, setSentError] = React.useState(null); - const error = searchParams.get("error"); - - const generateAuthFlow = React.useCallback(async () => { - try { - const redirectUrl = `${window.location.origin}/after-login`; - - const { data } = await ory.createNativeLoginFlow({ - refresh: true, - returnTo: redirectUrl, - }); - - setFlowData(data); - } catch (_error) { - console.error(_error); - } - }, [typeof window]); - - React.useEffect(() => { - generateAuthFlow(); - }, [generateAuthFlow]); + const error = errorParam || sentError; React.useEffect(() => { if (ws) { @@ -107,6 +88,20 @@ const LoginForm = (props: { className?: string }) => { return () => 0; }, [ws]); + React.useEffect(() => { + if (ws) { + const unsub = receive( + ws, + DevtoolsEvent.DEVTOOLS_DISPLAY_LOGIN_FAILURE, + (payload) => { + setSentError(payload.error); + }, + ); + return unsub; + } + return () => 0; + }, [ws]); + return (
{ )} > - { "re-justify-center", "re-gap-4", )} - action={flowData?.ui?.action} - method={flowData?.ui?.method} - target="_blank" > - {providers.map(({ name, icon, label }) => ( - + ))} - {error && ( -
- {error} -
- )} - +
+ {error && ( +
+ {error} +
+ )} ); }; diff --git a/packages/devtools-ui/src/utils/constants.ts b/packages/devtools-ui/src/utils/constants.ts new file mode 100644 index 000000000000..41462e32f226 --- /dev/null +++ b/packages/devtools-ui/src/utils/constants.ts @@ -0,0 +1,3 @@ +export const AUTH_SERVER_URL = "/api/.auth"; +export const REFINE_API_URL = "/api/.refine"; +export const AUTH_TRIGGER_API_PATH = "/api/login"; diff --git a/packages/devtools-ui/src/utils/me.ts b/packages/devtools-ui/src/utils/me.ts index aeee6715d584..2c1d19f0158e 100644 --- a/packages/devtools-ui/src/utils/me.ts +++ b/packages/devtools-ui/src/utils/me.ts @@ -1,3 +1,5 @@ +import { REFINE_API_URL } from "./constants"; + import type { MeResponse, MeUpdateVariables, @@ -6,7 +8,7 @@ import type { export const getMe = async () => { try { - const response = await fetch("/api/.refine/users/me"); + const response = await fetch(`${REFINE_API_URL}/users/me`); if (response.ok) { const data = (await response.json()) as MeResponse; @@ -24,7 +26,7 @@ export const getMe = async () => { export const updateMe = async (variables: MeUpdateVariables) => { try { - const { status } = await fetch("/api/.refine/users/me", { + const { status } = await fetch(`${REFINE_API_URL}/users/me`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -45,10 +47,7 @@ export const updateMe = async (variables: MeUpdateVariables) => { export const raffle = async (): Promise => { try { - const response = await fetch( - // TODO: Change to real endpoint - "/api/.refine/users/me/raffle", - ); + const response = await fetch(`${REFINE_API_URL}/users/me/raffle`); const data = (await response.json()) as RaffleResponse; @@ -61,7 +60,7 @@ export const raffle = async (): Promise => { export const acknowledgeRaffle = async () => { try { - await fetch("/api/.refine/users/me/raffle/acknowledge"); + await fetch(`${REFINE_API_URL}/users/me/raffle/acknowledge`); } catch (_) { // } diff --git a/packages/devtools-ui/src/utils/ory.ts b/packages/devtools-ui/src/utils/ory.ts index b5d522f70042..4cf79f37193b 100644 --- a/packages/devtools-ui/src/utils/ory.ts +++ b/packages/devtools-ui/src/utils/ory.ts @@ -1,10 +1,9 @@ import { FrontendApi } from "@ory/client"; - -const ORY_URL = "/api/.auth"; +import { AUTH_SERVER_URL } from "./constants"; export const ory = new FrontendApi({ isJsonMime: () => true, - basePath: ORY_URL, + basePath: AUTH_SERVER_URL, baseOptions: { withCredentials: true, }, diff --git a/packages/devtools-ui/src/utils/project-id.ts b/packages/devtools-ui/src/utils/project-id.ts index 9b50965d518c..a5cf2d017be4 100644 --- a/packages/devtools-ui/src/utils/project-id.ts +++ b/packages/devtools-ui/src/utils/project-id.ts @@ -1,8 +1,15 @@ import type { ProjectIdResponse } from "src/interfaces/api"; +import { REFINE_API_URL } from "./constants"; + +const CODES = { + OK: 0, + NOT_FOUND: 1, + ERROR: 2, +}; export const fetchNewProjectId = async () => { try { - const response = await fetch("/api/.refine/projects", { + const response = await fetch(`${REFINE_API_URL}/projects`, { method: "POST", headers: { "Content-Type": "application/json", @@ -25,17 +32,19 @@ export const fetchNewProjectId = async () => { export const getCurrentProjectIdStatus = async () => { try { const response = await fetch("/api/project-id/status"); + const body: { projectId: string | null; status: 0 | 1 | 2 } = + await response.json(); - if (response.status === 400) { - return undefined; + if (body.status === CODES.OK) { + return true; } - if (response.status === 404) { + if (body.status === CODES.NOT_FOUND) { return false; } - if (response.status === 200) { - return true; + if (body.status === CODES.ERROR) { + return undefined; } } catch (_) { // diff --git a/packages/devtools/src/utilities/use-selector.tsx b/packages/devtools/src/utilities/use-selector.tsx index f5cc1c124922..174b5193c131 100644 --- a/packages/devtools/src/utilities/use-selector.tsx +++ b/packages/devtools/src/utilities/use-selector.tsx @@ -23,9 +23,7 @@ export const useSelector = (active: boolean) => { >([]); const fetchTraceItems = React.useCallback(async () => { - const response = await fetch( - `${devtoolsUrl ?? "http://localhost:5001"}/api/unique-trace-items`, - ); + const response = await fetch(`${devtoolsUrl}/api/unique-trace-items`); const data = await response.json(); return data.data as string[]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d7fe21c3d9d..b7865e74ba4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5297,9 +5297,6 @@ importers: '@vitejs/plugin-react': specifier: ^4.2.1 version: 4.2.1(vite@5.2.10(@types/node@18.19.31)(sass@1.75.0)(terser@5.30.4)) - http-proxy-middleware: - specifier: ^2.0.6 - version: 2.0.6(@types/express@4.17.21) typescript: specifier: ^5.4.2 version: 5.4.5 @@ -5361,9 +5358,6 @@ importers: cypress: specifier: ^13.6.3 version: 13.8.1 - http-proxy-middleware: - specifier: ^2.0.6 - version: 2.0.6(@types/express@4.17.21) typescript: specifier: ^5.4.2 version: 5.4.5 @@ -9655,8 +9649,8 @@ importers: specifier: ^7.0.3 version: 7.0.3 http-proxy-middleware: - specifier: ^2.0.6 - version: 2.0.6(@types/express@4.17.21) + specifier: ^3.0.0 + version: 3.0.0 keen-slider: specifier: ^6.6.3 version: 6.8.6 @@ -14270,9 +14264,6 @@ importers: handlebars: specifier: ^4.7.7 version: 4.7.8 - http-proxy-middleware: - specifier: ^2.0.6 - version: 2.0.6(@types/express@4.17.21) inquirer: specifier: ^8.2.5 version: 8.2.6 @@ -14765,9 +14756,6 @@ importers: packages/devtools-server: dependencies: - '@ory/client': - specifier: ^1.5.2 - version: 1.9.0 '@refinedev/devtools-shared': specifier: 1.1.9 version: link:../devtools-shared @@ -14808,8 +14796,8 @@ importers: specifier: ^4.0.3 version: 4.0.3 http-proxy-middleware: - specifier: ^2.0.6 - version: 2.0.6(@types/express@4.17.21) + specifier: ^3.0.0 + version: 3.0.0 jscodeshift: specifier: 0.15.2 version: 0.15.2(@babel/preset-env@7.24.4(@babel/core@7.24.4)) @@ -28966,6 +28954,10 @@ packages: '@types/express': optional: true + http-proxy-middleware@3.0.0: + resolution: {integrity: sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -51667,19 +51659,19 @@ snapshots: axios@0.21.4: dependencies: - follow-redirects: 1.15.6 + follow-redirects: 1.15.6(debug@4.3.4) transitivePeerDependencies: - debug axios@0.24.0: dependencies: - follow-redirects: 1.15.6 + follow-redirects: 1.15.6(debug@4.3.4) transitivePeerDependencies: - debug axios@1.6.8: dependencies: - follow-redirects: 1.15.6 + follow-redirects: 1.15.6(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -55560,7 +55552,9 @@ snapshots: dependencies: tslib: 2.6.2 - follow-redirects@1.15.6: {} + follow-redirects@1.15.6(debug@4.3.4): + optionalDependencies: + debug: 4.3.4(supports-color@5.5.0) fontkit@2.0.2: dependencies: @@ -55849,7 +55843,7 @@ snapshots: get-it@8.4.27: dependencies: decompress-response: 7.0.0 - follow-redirects: 1.15.6 + follow-redirects: 1.15.6(debug@4.3.4) into-stream: 6.0.0 is-retry-allowed: 2.2.0 is-stream: 2.0.1 @@ -56769,7 +56763,7 @@ snapshots: http-proxy-middleware@2.0.6(@types/express@4.17.21): dependencies: '@types/http-proxy': 1.17.14 - http-proxy: 1.18.1 + http-proxy: 1.18.1(debug@4.3.4) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.5 @@ -56778,10 +56772,21 @@ snapshots: transitivePeerDependencies: - debug - http-proxy@1.18.1: + http-proxy-middleware@3.0.0: + dependencies: + '@types/http-proxy': 1.17.14 + debug: 4.3.4(supports-color@5.5.0) + http-proxy: 1.18.1(debug@4.3.4) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 + transitivePeerDependencies: + - supports-color + + http-proxy@1.18.1(debug@4.3.4): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.6 + follow-redirects: 1.15.6(debug@4.3.4) requires-port: 1.0.0 transitivePeerDependencies: - debug From ba8117f6060253dc1d589d69acba79d7c89e94c6 Mon Sep 17 00:00:00 2001 From: Batuhan Wilhelm Date: Thu, 4 Jul 2024 17:43:34 +0300 Subject: [PATCH 13/13] chore(inferencer): remove `graphql-tag` from peer dependencies (#6099) --- .changeset/itchy-schools-push.md | 5 +++++ packages/inferencer/package.json | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 .changeset/itchy-schools-push.md diff --git a/.changeset/itchy-schools-push.md b/.changeset/itchy-schools-push.md new file mode 100644 index 000000000000..03a20f112895 --- /dev/null +++ b/.changeset/itchy-schools-push.md @@ -0,0 +1,5 @@ +--- +"@refinedev/inferencer": patch +--- + +chore: remove graphql-tag from peer dependencies. fixes #6100 diff --git a/packages/inferencer/package.json b/packages/inferencer/package.json index 8baed50cfa5a..616d14789daa 100644 --- a/packages/inferencer/package.json +++ b/packages/inferencer/package.json @@ -179,7 +179,6 @@ "@types/react-dom": "^17.0.0 || ^18.0.0", "antd": "^5.0.3", "dayjs": "^1.10.7", - "graphql-tag": "^2.12.6", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", "react-hook-form": "^7.30.0" @@ -242,9 +241,6 @@ "dayjs": { "optional": true }, - "graphql-tag": { - "optional": true - }, "react-hook-form": { "optional": true }