Skip to content

Commit

Permalink
Support delete in data providers (mui#2978)
Browse files Browse the repository at this point in the history
  • Loading branch information
Janpot authored Dec 7, 2023
1 parent c21fccc commit a5890b5
Show file tree
Hide file tree
Showing 26 changed files with 336 additions and 47 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ module.exports = {
files: ['examples/**/*'],
rules: {
'no-console': 'off',
'no-underscore-dangle': 'off',
},
},
{
Expand Down
22 changes: 17 additions & 5 deletions docs/data/toolpad/concepts/data-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,25 @@ This feature isn't implemented yet.
👍 Upvote [issue #2888](https://github.com/mui/mui-toolpad/issues/2888) if you want to see it land faster.
:::

## Deleting rows 🚧
## Deleting rows

:::warning
This feature isn't implemented yet.
The data provider can be extended to automatically support row deletion. To enable this, you'll have to add a `deleteRecord` method to the data provider interface that accepts the `id` of the row that is to be deleted.

👍 Upvote [issue #2889](https://github.com/mui/mui-toolpad/issues/2889) if you want to see it land faster.
:::
```tsx
export default createDataProvider({
async getRecords({ paginationModel: { start = 0, pageSize } }) {
return db.query(`SELECT * FROM users`);
},

async deleteRecord(id) {
await db.query(`DELETE FROM users WHERE id = ?`, [id]);
},
});
```

When a data provider contains a `deleteRecord` method, each row will have a delete button. When the user clicks that delete button, the delete method will be called with the id of that row and after successful deletion, the data will be reloaded.

{{"component": "modules/components/DocsImage.tsx", "src": "/static/toolpad/docs/concepts/connecting-to-data/data-providers-delete.png", "alt": "Data provider delete", "caption": "Delete action in data provider" }}

## API

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions examples/with-prisma-data-provider/toolpad/pages/crud/page.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.39/docs/schemas/v1/definitions.json#properties/Page

apiVersion: v1
kind: page
spec:
title: Prisma data source CRUD example
content:
- component: Text
name: text
layout:
columnSize: 1
horizontalAlign: start
props:
variant: h2
value: Users
mode: text
- component: DataGrid
name: usersDataGrid
layout:
columnSize: 1
props:
rows: null
columns:
- field: id
type: number
width: 77
- field: name
type: string
width: 120
- field: email
type: string
width: 335
height: 336
rowsSource: dataProvider
dataProviderId: crud.ts:default
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.39/docs/schemas/v1/definitions.json#properties/Page

apiVersion: v1
kind: page
spec:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.39/docs/schemas/v1/definitions.json#properties/Page

apiVersion: v1
kind: page
spec:
Expand Down
7 changes: 7 additions & 0 deletions examples/with-prisma-data-provider/toolpad/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PrismaClient } from '@prisma/client';

// Reuse existing PrismaClient instance during development
(globalThis as any).__prisma ??= new PrismaClient();
const prisma: PrismaClient = (globalThis as any).__prisma;

export default prisma;
29 changes: 29 additions & 0 deletions examples/with-prisma-data-provider/toolpad/resources/crud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Toolpad data provider file.
* See: https://mui.com/toolpad/concepts/data-providers/
*/

import { createDataProvider } from '@mui/toolpad/server';
import prisma from '../prisma';

export default createDataProvider({
async getRecords({ paginationModel: { start, pageSize } }) {
const [userRecords, totalCount] = await Promise.all([
prisma.user.findMany({
skip: start,
take: pageSize,
}),
prisma.user.count(),
]);
return {
records: userRecords,
totalCount,
};
},

async deleteRecord(id) {
await prisma.user.delete({
where: { id: Number(id) },
});
},
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
/* eslint-disable no-underscore-dangle */
/**
* Toolpad data provider file.
* See: https://mui.com/toolpad/concepts/data-providers/
*/

import { createDataProvider } from '@mui/toolpad/server';
import { PrismaClient } from '@prisma/client';

// Reuse existing PrismaClient instance during development
(globalThis as any).__prisma ??= new PrismaClient();
const prisma: PrismaClient = (globalThis as any).__prisma;
import prisma from '../prisma';

export default createDataProvider({
paginationMode: 'cursor',

async getRecords({ paginationModel: { cursor, pageSize } }) {
const userRecords = await prisma.user.findMany({
cursor: cursor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
/* eslint-disable no-underscore-dangle */
/**
* Toolpad data provider file.
* See: https://mui.com/toolpad/concepts/data-providers/
*/

import { createDataProvider } from '@mui/toolpad/server';
import { PrismaClient } from '@prisma/client';

// Reuse existing PrismaClient instance during development
(globalThis as any).__prisma ??= new PrismaClient();
const prisma: PrismaClient = (globalThis as any).__prisma;
import prisma from '../prisma';

export default createDataProvider({
async getRecords({ paginationModel: { start, pageSize } }) {
Expand Down
1 change: 0 additions & 1 deletion examples/with-prisma/toolpad/resources/functions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-underscore-dangle */
import { PrismaClient, Prisma } from '@prisma/client';

// Reuse existing PrismaClient instance during development
Expand Down
8 changes: 8 additions & 0 deletions packages/toolpad-app/src/runtime/useDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { UseDataProviderHook } from '@mui/toolpad-core/runtime';
import { useQuery } from '@tanstack/react-query';
import invariant from 'invariant';
import { ToolpadDataProviderBase } from '@mui/toolpad-core';
import { GridRowId } from '@mui/x-data-grid';
import api from './api';

export const useDataProvider: UseDataProviderHook = (id) => {
Expand Down Expand Up @@ -31,6 +32,13 @@ export const useDataProvider: UseDataProviderHook = (id) => {
const [filePath, name] = id.split(':');
return api.methods.getDataProviderRecords(filePath, name, ...args);
},
deleteRecord: introspection.hasDeleteRecord
? async (recordId: GridRowId) => {
invariant(id, 'id is required');
const [filePath, name] = id.split(':');
return api.methods.deleteDataProviderRecord(filePath, name, recordId);
}
: undefined,
};
}, [id, introspection]);

Expand Down
11 changes: 11 additions & 0 deletions packages/toolpad-app/src/server/FunctionsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { errorFrom } from '@mui/toolpad-utils/errors';
import { ToolpadDataProviderIntrospection } from '@mui/toolpad-core/runtime';
import * as url from 'node:url';
import invariant from 'invariant';
import { GridRowId } from '@mui/x-data-grid';
import EnvManager from './EnvManager';
import { ProjectEvents, ToolpadProjectOptions } from '../types';
import { createWorker as createDevWorker } from './functionsDevWorker';
Expand Down Expand Up @@ -408,4 +409,14 @@ export default class FunctionsManager {
invariant(this.devWorker, 'devWorker must be initialized');
return this.devWorker.getDataProviderRecords(fullPath, exportName, params);
}

async deleteDataProviderRecord(
fileName: string,
exportName: string,
id: GridRowId,
): Promise<void> {
const fullPath = await this.getBuiltOutputFilePath(fileName);
invariant(this.devWorker, 'devWorker must be initialized');
return this.devWorker.deleteDataProviderRecord(fullPath, exportName, id);
}
}
22 changes: 22 additions & 0 deletions packages/toolpad-app/src/server/functionsDevWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { TOOLPAD_DATA_PROVIDER_MARKER, ToolpadDataProvider } from '@mui/toolpad-
import * as z from 'zod';
import { fromZodError } from 'zod-validation-error';
import { GetRecordsParams, GetRecordsResult, PaginationMode } from '@mui/toolpad-core';
import invariant from 'invariant';
import { GridRowId } from '@mui/x-data-grid';

import.meta.url ??= url.pathToFileURL(__filename).toString();
const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url));
Expand Down Expand Up @@ -123,6 +125,9 @@ async function execute(msg: ExecuteParams): Promise<ExecuteResult> {
const dataProviderSchema: z.ZodType<ToolpadDataProvider<any, any>> = z.object({
paginationMode: z.enum(['index', 'cursor']).optional().default('index'),
getRecords: z.function(z.tuple([z.any()]), z.any()),
deleteRecord: z.function(z.tuple([z.any()]), z.any()).optional(),
updateRecord: z.function(z.tuple([z.any()]), z.any()).optional(),
createRecord: z.function(z.tuple([z.any()]), z.any()).optional(),
[TOOLPAD_DATA_PROVIDER_MARKER]: z.literal(true),
});

Expand Down Expand Up @@ -154,6 +159,7 @@ async function introspectDataProvider(

return {
paginationMode: dataProvider.paginationMode,
hasDeleteRecord: !!dataProvider.deleteRecord,
};
}

Expand All @@ -167,17 +173,29 @@ async function getDataProviderRecords<R, P extends PaginationMode>(
return dataProvider.getRecords(params);
}

async function deleteDataProviderRecord(
filePath: string,
name: string,
id: GridRowId,
): Promise<void> {
const dataProvider = await loadDataProvider(filePath, name);
invariant(dataProvider.deleteRecord, 'DataProvider does not support deleteRecord');
return dataProvider.deleteRecord(id);
}

type WorkerRpcServer = {
execute: typeof execute;
introspectDataProvider: typeof introspectDataProvider;
getDataProviderRecords: typeof getDataProviderRecords;
deleteDataProviderRecord: typeof deleteDataProviderRecord;
};

if (!isMainThread && parentPort) {
serveRpc<WorkerRpcServer>(workerData.workerRpcPort, {
execute,
introspectDataProvider,
getDataProviderRecords,
deleteDataProviderRecord,
});
}

Expand Down Expand Up @@ -233,6 +251,10 @@ export function createWorker(env: Record<string, any>) {
): Promise<GetRecordsResult<R, P>> {
return client.getDataProviderRecords(filePath, name, params);
},

async deleteDataProviderRecord(filePath: string, name: string, id: GridRowId): Promise<void> {
return client.deleteDataProviderRecord(filePath, name, id);
},
};
}

Expand Down
5 changes: 5 additions & 0 deletions packages/toolpad-app/src/server/runtimeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export function createRpcServer(project: ToolpadProject) {
return project.functionsManager.getDataProviderRecords(...params);
},
),
deleteDataProviderRecord: createMethod<
typeof project.functionsManager.deleteDataProviderRecord
>(({ params }) => {
return project.functionsManager.deleteDataProviderRecord(...params);
}),
execQuery: createMethod<typeof project.dataManager.execQuery>(({ params }) => {
return project.dataManager.execQuery(...params);
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import { createProvidedContext } from '@mui/toolpad-utils/react';
import { TabContext, TabList } from '@mui/lab';
import useDebounced from '@mui/toolpad-utils/hooks/useDebounced';
import { errorFrom } from '@mui/toolpad-utils/errors';
import useLatest from '@mui/toolpad-utils/hooks/useLatest';
import { JsExpressionEditor } from './PageEditor/JsExpressionEditor';
import JsonView from '../../components/JsonView';
import useLatest from '../../utils/useLatest';
import { useEvaluateLiveBinding } from './useEvaluateLiveBinding';
import GlobalScopeExplorer from './GlobalScopeExplorer';
import { WithControlledProp, Maybe } from '../../utils/types';
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/src/toolpad/AppEditor/NodeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import DeleteIcon from '@mui/icons-material/Delete';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ModeEditIcon from '@mui/icons-material/ModeEdit';
import { NodeId } from '@mui/toolpad-core';
import useLatest from '@mui/toolpad-utils/hooks/useLatest';
import * as appDom from '../../appDom';
import { useAppState } from '../AppState';
import useLatest from '../../utils/useLatest';
import { ConfirmDialog } from '../../components/SystemDialogs';
import useMenu from '../../utils/useMenu';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { BindableAttrValue } from '@mui/toolpad-core';
import { useBrowserJsRuntime } from '@mui/toolpad-core/jsBrowserRuntime';
import invariant from 'invariant';
import useEventCallback from '@mui/utils/useEventCallback';
import useLatest from '../../../../utils/useLatest';
import useLatest from '@mui/toolpad-utils/hooks/useLatest';
import { usePageEditorState } from '../PageEditorProvider';
import * as appDom from '../../../../appDom';
import dataSources from '../../../../toolpadDataSources/client';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import * as React from 'react';
import invariant from 'invariant';
import CloseIcon from '@mui/icons-material/Close';
import useEventCallback from '@mui/utils/useEventCallback';
import useLatest from '@mui/toolpad-utils/hooks/useLatest';
import * as appDom from '../../../appDom';
import { useAppState } from '../../AppState';
import DialogForm from '../../../components/DialogForm';
import { useNodeNameValidation } from './validation';
import { useProjectApi } from '../../../projectApi';
import useLatest from '../../../utils/useLatest';
import OpenCodeEditorButton from '../../OpenCodeEditor';

function handleInputFocus(event: React.FocusEvent<HTMLInputElement>) {
Expand Down
Loading

0 comments on commit a5890b5

Please sign in to comment.