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,