Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mui): editable Data Grid #5744 #5989

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7f4cbb7
feat(useDataGrid): enhance data grid manipulation with CRUD operation…
May 26, 2024
1fbce4e
test(useDataGrid): enhance tests with advanced filtering, pagination,…
May 26, 2024
2286c30
feat(PostList): enable title column editing in data grid
May 26, 2024
a81a401
feat(cypress-tests): add tests for cell update functionality in data …
May 26, 2024
438e415
docs(useDataGrid): update properties and add editing section
May 26, 2024
9d7b49c
chore: create changeset for editable MUI Data Grid feature
May 26, 2024
7fe92ff
feat(useDataGrid): replace useForm with useUpdate for enhanced state …
Jun 1, 2024
463c09e
refactor(useDataGrid): enhance processRowUpdate with explicit promise…
Jun 1, 2024
aa2c505
docs(useDataGrid): update documentation to reflect useUpdate integration
Jun 1, 2024
29ac185
docs(useDataGrid): enhance documentation with links and code clarity
Jun 1, 2024
f19498b
refactor(refine): streamline useDataGrid hook integration with MUI Da…
Jun 3, 2024
2a3c94b
feat(posts): integrate server-side updates in PostList data grid example
Jun 7, 2024
0467887
Merge branch 'master' into editable-MUI-data-grid
aliemir Jun 7, 2024
bb921df
feat(mui): improve typing for updateMutationOptions and useUpdate in …
Jun 13, 2024
7f6cddc
chore: merge main branch into feature branch
Jun 13, 2024
1c3a83c
fix(mui): correct import of UseUpdateProps to use import type for lin…
Jun 13, 2024
4ecd9b9
chore(editable-data-grid): update example data grid
aliemir Jun 14, 2024
a3df4e7
chore(core): export data hook props and return types
aliemir Jun 14, 2024
d7a270e
chore: add changeset
aliemir Jun 14, 2024
8cb1394
fix(use-data-grid): fix editable mutation
aliemir Jun 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/honest-beds-rhyme.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,40 @@ 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 hook can further extend editing provided in [`<DataGrid>`](https://mui.com/x/react-data-grid/editing/) component. To enable column editing, set `editable: "true"` on specific column definition.

`useDataGrid` will utilize `processRowUpdate` from `<DataGrid>` component, which in turn will call [`useForm`](https://refine.dev/docs/data/hooks/use-form/) to attempt updating the row with the new value.

```tsx
const columns = React.useMemo<GridColDef<IPost>[]>(
() => [
{
field: "title",
headerName: "Title",
minWidth: 400,
flex: 1,
editable: true,
},
],
[],
);
```

Properties from [`useForm`](https://refine.dev/docs/data/hooks/use-form/), with exception of `autoSave`, `action` and `redirect`, are available in `useDataGrid`.

The hook returns `onFinish`, `setId`, `id` and `formLoading` from `useForm`, which can be used to extend functionality.

```tsx
const {
dataGridProps,
formProps: { onFinish, setId, id, formLoading },
} = useDataGrid<IPost>();
```

By default, the `onCellEditStart` and `onRowEditStart` callbacks from `<DataGrid>` will set the `id` on the form, and `processRowUpdate` will call `onFinish`.

## Properties

### resource
Expand Down
48 changes: 48 additions & 0 deletions examples/table-material-ui-use-data-grid/cypress/e2e/all.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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",
Expand Down
75 changes: 75 additions & 0 deletions packages/mui/src/hooks/useDataGrid/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MockJSONServer, TestWrapper } from "@test";
import { useDataGrid } from "./";
import { 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 () => {
Expand Down Expand Up @@ -198,4 +199,78 @@ 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",
formProps: {
resource: "posts",
},
}),
{
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 () => {
await result.current.dataGridProps.processRowUpdate(
newPost,
postToUpdate,
);
});

expect(newPost).toEqual(postToUpdate);
});

it("when update fails, return old data to row", async () => {
const { result } = renderHook(
() =>
useDataGrid({
resource: "posts",
formProps: {
resource: "posts",
},
}),
{
wrapper: TestWrapper({
dataProvider: {
...MockJSONServer,
update: () => Promise.reject(),
},
}),
},
);

const oldPost = posts[0];

const newPost = {
...oldPost,
title: "New title",
};

await act(async () => {
const returnValue = await result.current.dataGridProps.processRowUpdate(
newPost,
oldPost,
);
expect(returnValue).toEqual(oldPost);
});
});
});
106 changes: 76 additions & 30 deletions packages/mui/src/hooks/useDataGrid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
Pagination,
pickNotDeprecated,
Prettify,
useForm,
UseFormProps,
lnikitadobrenkol marked this conversation as resolved.
Show resolved Hide resolved
UseFormReturnType,
useLiveMode,
useTable as useTableCore,
useTableProps as useTablePropsCore,
Expand Down Expand Up @@ -45,49 +48,65 @@ type DataGridPropsType = Required<
| "disableRowSelectionOnClick"
| "onStateChange"
| "paginationMode"
| "processRowUpdate"
| "onCellEditStart"
| "onRowEditStart"
>
> &
Pick<
DataGridProps,
"paginationModel" | "onPaginationModelChange" | "filterModel"
>;

export type UseDataGridProps<TQueryFnData, TError, TSearchVariables, TData> =
Omit<
useTablePropsCore<TQueryFnData, TError, TData>,
"pagination" | "filters"
> & {
onSearch?: (data: TSearchVariables) => CrudFilters | Promise<CrudFilters>;
pagination?: Prettify<
Omit<Pagination, "pageSize"> & {
/**
* Initial number of items per page
* @default 25
*/
pageSize?: number;
}
>;
filters?: Prettify<
Omit<
NonNullable<useTablePropsCore<TQueryFnData, TError, TData>["filters"]>,
"defaultBehavior"
> & {
/**
* Default behavior of the `setFilters` function
* @default "replace"
*/
defaultBehavior?: "replace" | "merge";
}
>;
};
export type UseDataGridProps<
TQueryFnData extends BaseRecord,
TError extends HttpError,
lnikitadobrenkol marked this conversation as resolved.
Show resolved Hide resolved
TSearchVariables,
TData,
> = Omit<
useTablePropsCore<TQueryFnData, TError, TData>,
"pagination" | "filters"
> & {
onSearch?: (data: TSearchVariables) => CrudFilters | Promise<CrudFilters>;
pagination?: Prettify<
Omit<Pagination, "pageSize"> & {
/**
* Initial number of items per page
* @default 25
*/
pageSize?: number;
}
>;
filters?: Prettify<
Omit<
NonNullable<useTablePropsCore<TQueryFnData, TError, TData>["filters"]>,
"defaultBehavior"
> & {
/**
* Default behavior of the `setFilters` function
* @default "replace"
*/
defaultBehavior?: "replace" | "merge";
}
>;
formProps?: Omit<
UseFormProps<TQueryFnData, TError, TData>,
"autoSave" | "action" | "redirect"
>;
lnikitadobrenkol marked this conversation as resolved.
Show resolved Hide resolved
};

export type UseDataGridReturnType<
TData extends BaseRecord = BaseRecord,
TQueryFnData extends BaseRecord = BaseRecord,
TError extends HttpError = HttpError,
TSearchVariables = unknown,
TData extends BaseRecord = TQueryFnData,
> = useTableReturnTypeCore<TData, TError> & {
dataGridProps: DataGridPropsType;
search: (value: TSearchVariables) => Promise<void>;
formProps: Pick<
UseFormReturnType<TQueryFnData, TError, TData>,
"onFinish" | "setId" | "id" | "formLoading"
>;
};

/**
Expand Down Expand Up @@ -134,12 +153,13 @@ export function useDataGrid<
metaData,
dataProviderName,
overtimeOptions,
formProps,
}: UseDataGridProps<
TQueryFnData,
TError,
TSearchVariables,
TData
> = {}): UseDataGridReturnType<TData, TError, TSearchVariables> {
> = {}): UseDataGridReturnType<TQueryFnData, TError, TSearchVariables, TData> {
const theme = useTheme();
const liveMode = useLiveMode(liveModeFromProp);

Expand Down Expand Up @@ -263,6 +283,16 @@ export function useDataGrid<
};
};

const { setId, onFinish, id, formLoading } = useForm<
TQueryFnData,
TError,
TData
>({
...formProps,
action: "edit",
redirect: false,
});

return {
tableQueryResult,
dataGridProps: {
Expand Down Expand Up @@ -310,6 +340,16 @@ export function useDataGrid<
)}`,
},
},
processRowUpdate: async (newRow, oldRow) => {
try {
await onFinish(newRow);
return newRow;
} catch {
return oldRow;
}
},
onCellEditStart: (params) => setId(params?.id),
onRowEditStart: (params) => setId(params?.id),
},
current,
setCurrent,
Expand All @@ -325,5 +365,11 @@ export function useDataGrid<
search,
createLinkForSyncWithLocation,
overtime,
formProps: {
onFinish,
setId,
id,
formLoading,
},
};
}
Loading