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

Support updating rows in the data provider #3001

Merged
merged 14 commits into from
Dec 20, 2023
29 changes: 24 additions & 5 deletions docs/data/toolpad/concepts/data-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,32 @@ Uncheck the column option "sortable" if you want to disable sorting for a certai

{{"component": "modules/components/DocsImage.tsx", "src": "/static/toolpad/docs/concepts/data-providers/disable-sortable.png", "alt": "Disable sortable", "caption": "Disable sortable", "zoom": false, "width": 325 }}

## Row editing 🚧
## Row editing

:::warning
This feature isn't implemented yet.
The data provider can be extended to automatically support row editing. To enable this, you'll have to add a `updateRecord` method to the data provider interface that accepts the `id` of the row that is to be deleted, and an object containing all the updated fields from the row editing operation.

👍 Upvote [issue #2887](https://github.com/mui/mui-toolpad/issues/2887) if you want to see it land faster.
:::
```tsx
export default createDataProvider({
async getRecords() {
return prisma.users.findMany();
},

async updateRecord(id, data) {
return prisma.users.update({ where: { id }, data });
},
});
```

When this method is available in the data provider, each row will have an edit button. This edit button brings the row in edit mode. To commit the changes press the save button on the row that is in edit mode. To discard the changes use the cancel button.

<video controls width="auto" height="100%" style="contain" alt="component-library">
<source src="/static/toolpad/docs/concepts/data-providers/editing.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

You can disable the editing functionality for specific columns by unchecking the **Editable** option in the column definition.

{{"component": "modules/components/DocsImage.tsx", "src": "/static/toolpad/docs/concepts/data-providers/disable-editable.png", "alt": "Disable editable", "caption": "Disable editable", "zoom": false, "width": 308 }}

## Row creation 🚧

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
8 changes: 5 additions & 3 deletions examples/with-prisma-data-provider/toolpad/resources/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ export default createDataProvider({
},

async deleteRecord(id) {
await model.delete({
where: { id: Number(id) },
});
await model.delete({ where: { id: Number(id) } });
},

async updateRecord(id, data) {
await model.update({ where: { id: Number(id) }, data });
},
});
14 changes: 14 additions & 0 deletions packages/toolpad-app/src/runtime/useDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ export const useDataProvider: UseDataProviderHook = (id) => {
return api.methods.deleteDataProviderRecord(filePath, name, recordId);
}
: undefined,
updateRecord: introspection.hasUpdateRecord
? async (recordId: GridRowId, values: Record<string, unknown>) => {
invariant(id, 'id is required');
const [filePath, name] = id.split(':');
return api.methods.updateDataProviderRecord(filePath, name, recordId, values);
}
: undefined,
createRecord: introspection.hasCreateRecord
? async (values: Record<string, unknown>) => {
invariant(id, 'id is required');
const [filePath, name] = id.split(':');
return api.methods.createDataProviderRecord(filePath, name, values);
}
: undefined,
};
}, [id, introspection]);

Expand Down
21 changes: 21 additions & 0 deletions packages/toolpad-app/src/server/FunctionsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,25 @@ export default class FunctionsManager {
invariant(this.devWorker, 'devWorker must be initialized');
return this.devWorker.deleteDataProviderRecord(fullPath, exportName, id);
}

async updateDataProviderRecord(
fileName: string,
exportName: string,
id: GridRowId,
values: Record<string, unknown>,
): Promise<void> {
const fullPath = await this.getBuiltOutputFilePath(fileName);
invariant(this.devWorker, 'devWorker must be initialized');
return this.devWorker.updateDataProviderRecord(fullPath, exportName, id, values);
}

async createDataProviderRecord(
fileName: string,
exportName: string,
values: Record<string, unknown>,
): Promise<void> {
const fullPath = await this.getBuiltOutputFilePath(fileName);
invariant(this.devWorker, 'devWorker must be initialized');
return this.devWorker.createDataProviderRecord(fullPath, exportName, values);
}
}
52 changes: 33 additions & 19 deletions packages/toolpad-app/src/server/functionsDevWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ 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(),
updateRecord: z.function(z.tuple([z.any(), 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 @@ -161,6 +161,8 @@ async function introspectDataProvider(
return {
paginationMode: dataProvider.paginationMode,
hasDeleteRecord: !!dataProvider.deleteRecord,
hasUpdateRecord: !!dataProvider.updateRecord,
hasCreateRecord: !!dataProvider.createRecord,
};
}

Expand All @@ -184,11 +186,34 @@ async function deleteDataProviderRecord(
return dataProvider.deleteRecord(id);
}

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

async function createDataProviderRecord(
filePath: string,
name: string,
values: Record<string, unknown>,
): Promise<void> {
const dataProvider = await loadDataProvider(filePath, name);
invariant(dataProvider.createRecord, 'DataProvider does not support createRecord');
return dataProvider.createRecord(values);
}

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

if (!isMainThread && parentPort) {
Expand All @@ -197,6 +222,8 @@ if (!isMainThread && parentPort) {
introspectDataProvider,
getDataProviderRecords,
deleteDataProviderRecord,
updateDataProviderRecord,
createDataProviderRecord,
});
}

Expand Down Expand Up @@ -238,24 +265,11 @@ export function createWorker(env: Record<string, any>) {
return result;
},

async introspectDataProvider(
filePath: string,
name: string,
): Promise<ToolpadDataProviderIntrospection> {
return client.introspectDataProvider(filePath, name);
},

async getDataProviderRecords<R, P extends PaginationMode>(
filePath: string,
name: string,
params: GetRecordsParams<R, P>,
): 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);
},
introspectDataProvider: client.introspectDataProvider.bind(client),
getDataProviderRecords: client.getDataProviderRecords.bind(client),
deleteDataProviderRecord: client.deleteDataProviderRecord.bind(client),
updateDataProviderRecord: client.updateDataProviderRecord.bind(client),
createDataProviderRecord: client.createDataProviderRecord.bind(client),
};
}

Expand Down
10 changes: 10 additions & 0 deletions packages/toolpad-app/src/server/runtimeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ export function createRpcServer(project: ToolpadProject) {
>(({ params }) => {
return project.functionsManager.deleteDataProviderRecord(...params);
}),
updateDataProviderRecord: createMethod<
typeof project.functionsManager.updateDataProviderRecord
>(({ params }) => {
return project.functionsManager.updateDataProviderRecord(...params);
}),
createDataProviderRecord: createMethod<
typeof project.functionsManager.createDataProviderRecord
>(({ params }) => {
return project.functionsManager.createDataProviderRecord(...params);
}),
execQuery: createMethod<typeof project.dataManager.execQuery>(({ params }) => {
return project.dataManager.execQuery(...params);
}),
Expand Down
16 changes: 16 additions & 0 deletions packages/toolpad-app/src/toolpad/propertyControls/GridColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,22 @@ function GridColumnEditor({
label="Filterable"
/>

<FormControlLabel
control={
<Checkbox
checked={editedColumn.editable ?? true}
disabled={disabled}
onChange={(event) =>
handleColumnChange({
...editedColumn,
editable: event.target.checked,
})
}
/>
}
label="Editable"
/>

<Box sx={{ ml: 1, pl: 1, borderLeft: 1, borderColor: 'divider' }}>
{editedColumn.type === 'number' ? (
<NumberFormatEditor
Expand Down
Loading
Loading