Skip to content

Commit

Permalink
Support serverside sorting/filtering (#2982)
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Potoms <2109932+Janpot@users.noreply.github.com>
Co-authored-by: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com>
  • Loading branch information
Janpot and apedroferreira authored Dec 13, 2023
1 parent 08f31fe commit 1d39974
Show file tree
Hide file tree
Showing 19 changed files with 374 additions and 103 deletions.
68 changes: 58 additions & 10 deletions docs/data/toolpad/concepts/data-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,69 @@ export default createDataProvider({
});
```

## Filtering 🚧
## Filtering

:::warning
This feature isn't implemented yet.
Toolpad data sources support server-side filtering. You can implement a server-side filter by reading the `filterModel` property that is passed to the `getRecords` function. This model contains an `items` property and a `logicOperator`. By combining them you can achieve complex serverside filters.

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

## Sorting 🚧
For example, this could print the following if the corresponding column filters were applied in the data grid:

:::warning
This feature isn't implemented yet.
```tsx
{
logicOperator: 'and',
items: [
{ field: 'first_name', operator: 'startsWith', value: 'L' },
{ field: 'last_name', operator: 'equals', value: 'Skywalker' },
]
}
```

👍 Upvote [issue #2539](https://github.com/mui/mui-toolpad/issues/2539) if you want to see it land faster.
:::
Now the data grid filter UI will be hooked up to your backend function in the data provider.

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

Uncheck the column option "filterable" if you want to disable filtering for a certain column:

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

## Sorting

Toolpad data sources support server-side sorting. To achieve this you'll have to consume the `sortModel` property that is passed to the `getRecords` method:

```tsx
export default createDataProvider({
async getRecords({ sortModel }) {
console.log(sortModel);
},
});
```

Depending on which column has been set to sort by, this will result in:

```tsx
[{ field: 'name', sort: 'asc' }];
```

Now the data grid sorting UI will be hooked up to your backend function in the data provider.

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

Uncheck the column option "sortable" if you want to disable sorting for a certain column:

{{"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 🚧

Expand Down
17 changes: 14 additions & 3 deletions docs/data/toolpad/reference/api/create-data-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ Describes the capabilities of the data provider.

**Properties**

| Name | Type | Description |
| :----------------- | :---------------- | :------------------------------------------------------- |
| `paginationModel?` | `PaginationModel` | The pagination model that describes the requested slice. |
| Name | Type | Description |
| :----------------- | :------------------------------------------ | :---------------------------------------------------------------------------- |
| `paginationModel?` | `PaginationModel` | The pagination model that describes the requested slice. |
| `filterModel` | `FilterModel` | The filtering model that describes the serverside filter applied to the data. |
| `sortModel` | `{ field: string; sort: 'asc' \| 'desc'}[]` | The sort model that describes the desired ordering of the result set. |

### PaginationModel

Expand All @@ -76,6 +78,15 @@ Describes the capabilities of the data provider.
| `cursor` | `number` | The cursor addressing the requested slice. `null` for the initial page. |
| `pageSize` | `number` | The length of the requested slice. |

### FilterModel

**Properties**

| Name | Type | Description |
| :-------------- | :------------------------------------------------------ | :---------------------------------------------------------------------------------------------- |
| `logicOperator` | `'and' \| 'or'` | The operator that is applied to the filtering operation. |
| `items` | `{ field: string; operator: string; value: unknown }[]` | The constituents of the filter, each describes an operation applied to a field in the data set. |

### GetRecordsResult

| Name | Type | Description |
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.39/docs/schemas/v1/definitions.json#properties/Page
# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.40/docs/schemas/v1/definitions.json#properties/Page

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

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

apiVersion: v1
kind: page
Expand Down
4 changes: 3 additions & 1 deletion examples/with-prisma-data-provider/toolpad/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { PrismaClient } from '@prisma/client';
import { PrismaClient, Prisma } 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;

export { Prisma };
86 changes: 82 additions & 4 deletions examples/with-prisma-data-provider/toolpad/resources/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,92 @@
import { createDataProvider } from '@mui/toolpad/server';
import prisma from '../prisma';

function parseOperator(operator: string) {
switch (operator) {
case '=':
case 'equals':
return 'equals';
case '!=':
return 'not';
case '>':
return 'gt';
case '>=':
return 'gte';
case '<':
return 'lt';
case '<=':
return 'lte';
case 'isAnyOf':
return 'in';
case 'contains':
case 'isEmpty':
case 'isNotEmpty':
case 'startsWith':
case 'endsWith':
return operator;
default:
throw new Error(`Unknown operator: ${operator}`);
}
}

function parseValue(typeName: string, value: unknown) {
if (value === undefined || value === null) {
return null;
}
switch (typeName) {
case 'Boolean':
return Boolean(value);
case 'String':
return String(value);
case 'Int':
case 'BigInt':
case 'Float':
case 'Decimal':
return Number(value);
default:
return value;
}
}

const model: typeof prisma.user | typeof prisma.post = prisma.user;
export default createDataProvider({
async getRecords({ paginationModel: { start, pageSize } }) {
async getRecords({ paginationModel: { start, pageSize }, sortModel, filterModel }) {
const [userRecords, totalCount] = await Promise.all([
prisma.user.findMany({
model.findMany({
skip: start,
take: pageSize,

where:
filterModel.items.length <= 0
? undefined
: {
[filterModel.logicOperator.toUpperCase()]: filterModel.items.map(
({ field, operator, value }) => {
operator = parseOperator(operator);
switch (operator) {
case 'isEmpty':
return { [field]: null };
case 'isNotEmpty':
return { [field]: { not: null } };
default: {
const typeName = (model.fields as any)[field]?.typeName;
if (operator === 'in') {
value = (value as unknown[]).map((val) => parseValue(typeName, val));
} else {
value = parseValue(typeName, value);
}
return { [field]: { [operator]: value } };
}
}
},
),
},

orderBy: sortModel.map(({ field, sort }) => ({
[field]: sort,
})),
}),
prisma.user.count(),
model.count(),
]);
return {
records: userRecords,
Expand All @@ -22,7 +100,7 @@ export default createDataProvider({
},

async deleteRecord(id) {
await prisma.user.delete({
await model.delete({
where: { id: Number(id) },
});
},
Expand Down
24 changes: 15 additions & 9 deletions packages/toolpad-app/src/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import * as React from 'react';
import invariant from 'invariant';
import { throttle } from 'lodash-es';
import { CanvasEventsContext } from '@mui/toolpad-core/runtime';
import ToolpadApp from '../runtime/ToolpadApp';
import ToolpadApp, { IS_RENDERED_IN_CANVAS } from '../runtime/ToolpadApp';
import { queryClient } from '../runtime/api';
import { AppCanvasState } from '../types';
import getPageViewState from './getPageViewState';
import { rectContainsPoint } from '../utils/geometry';
import { CanvasHooks, CanvasHooksContext } from '../runtime/CanvasHooksContext';
import { bridge, setCommandHandler } from './ToolpadBridge';
import { ToolpadBridge, bridge, setCommandHandler } from './ToolpadBridge';

const handleScreenUpdate = throttle(
() => {
Expand All @@ -25,6 +25,7 @@ export interface AppCanvasProps {

export default function AppCanvas({ basename, state: initialState }: AppCanvasProps) {
const [state, setState] = React.useState<AppCanvasState>(initialState);
const [readyBridge, setReadyBridge] = React.useState<ToolpadBridge>();

const appRootRef = React.useRef<HTMLDivElement>();
const appRootCleanupRef = React.useRef<() => void>();
Expand Down Expand Up @@ -114,6 +115,7 @@ export default function AppCanvas({ basename, state: initialState }: AppCanvasPr
});

bridge.canvasEvents.emit('ready', {});
setReadyBridge(bridge);
}, []);

const savedNodes = state?.savedNodes;
Expand All @@ -123,11 +125,15 @@ export default function AppCanvas({ basename, state: initialState }: AppCanvasPr
};
}, [savedNodes]);

return (
<CanvasHooksContext.Provider value={editorHooks}>
<CanvasEventsContext.Provider value={bridge?.canvasEvents || null}>
<ToolpadApp rootRef={onAppRoot} basename={basename} state={state} />
</CanvasEventsContext.Provider>
</CanvasHooksContext.Provider>
);
if (IS_RENDERED_IN_CANVAS) {
return readyBridge ? (
<CanvasHooksContext.Provider value={editorHooks}>
<CanvasEventsContext.Provider value={readyBridge.canvasEvents}>
<ToolpadApp rootRef={onAppRoot} basename={basename} state={state} />
</CanvasEventsContext.Provider>
</CanvasHooksContext.Provider>
) : null;
}

return <ToolpadApp basename={basename} state={state} />;
}
2 changes: 1 addition & 1 deletion packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ import { AuthenticationProvider, RequireAuthorization, User } from './auth';

const browserJsRuntime = getBrowserRuntime();

const IS_RENDERED_IN_CANVAS =
export const IS_RENDERED_IN_CANVAS =
typeof window === 'undefined'
? false
: !!(window.frameElement as HTMLIFrameElement)?.dataset?.toolpadCanvas;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
Box,
Button,
Checkbox,
FormControlLabel,
IconButton,
List,
ListItem,
Expand Down Expand Up @@ -200,6 +202,38 @@ function GridColumnEditor({
))}
</TextField>

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

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

<Box sx={{ ml: 1, pl: 1, borderLeft: 1, borderColor: 'divider' }}>
{editedColumn.type === 'number' ? (
<NumberFormatEditor
Expand Down Expand Up @@ -281,7 +315,9 @@ function GridColumnsPropEditor({
setMenuAnchorEl(null);
};

const rawRows: unknown = nodeId && nodeData[nodeId]?.rawRows;
const gridNodeData = nodeId && nodeData[nodeId];

const rawRows: unknown = gridNodeData && gridNodeData.rawRows;

const inferredColumns = React.useMemo(
() => inferColumns(Array.isArray(rawRows) ? rawRows : []),
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-components/src/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function Chart({ data = [], loading, error, height, sx }: ChartProps) {

return (
<Box sx={{ ...sx, position: 'relative', minHeight: height, width: '100%' }} aria-busy={loading}>
<ErrorOverlay error={displayError} />
{displayError ? <ErrorOverlay error={displayError} /> : null}
{loading && !error ? (
<div
style={{
Expand Down
Loading

0 comments on commit 1d39974

Please sign in to comment.