diff --git a/docs/data/toolpad/concepts/data-providers.md b/docs/data/toolpad/concepts/data-providers.md index 9dbd21ebc68..4da860a5249 100644 --- a/docs/data/toolpad/concepts/data-providers.md +++ b/docs/data/toolpad/concepts/data-providers.md @@ -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. + + + +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. + + + +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 🚧 diff --git a/docs/data/toolpad/reference/api/create-data-provider.md b/docs/data/toolpad/reference/api/create-data-provider.md index d73645bfea6..c61f1eeaced 100644 --- a/docs/data/toolpad/reference/api/create-data-provider.md +++ b/docs/data/toolpad/reference/api/create-data-provider.md @@ -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 @@ -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 | diff --git a/docs/public/static/toolpad/docs/concepts/data-providers/disable-filterable.png b/docs/public/static/toolpad/docs/concepts/data-providers/disable-filterable.png new file mode 100644 index 00000000000..519a0558b52 Binary files /dev/null and b/docs/public/static/toolpad/docs/concepts/data-providers/disable-filterable.png differ diff --git a/docs/public/static/toolpad/docs/concepts/data-providers/disable-sortable.png b/docs/public/static/toolpad/docs/concepts/data-providers/disable-sortable.png new file mode 100644 index 00000000000..c8d9b69e9da Binary files /dev/null and b/docs/public/static/toolpad/docs/concepts/data-providers/disable-sortable.png differ diff --git a/docs/public/static/toolpad/docs/concepts/data-providers/filtering.mp4 b/docs/public/static/toolpad/docs/concepts/data-providers/filtering.mp4 new file mode 100644 index 00000000000..71e69e92801 Binary files /dev/null and b/docs/public/static/toolpad/docs/concepts/data-providers/filtering.mp4 differ diff --git a/docs/public/static/toolpad/docs/concepts/data-providers/sorting.mp4 b/docs/public/static/toolpad/docs/concepts/data-providers/sorting.mp4 new file mode 100644 index 00000000000..8762c33c3b7 Binary files /dev/null and b/docs/public/static/toolpad/docs/concepts/data-providers/sorting.mp4 differ diff --git a/examples/with-prisma-data-provider/toolpad/pages/crud/page.yml b/examples/with-prisma-data-provider/toolpad/pages/crud/page.yml index 5dcf68c3c85..6e10f9d7ff8 100644 --- a/examples/with-prisma-data-provider/toolpad/pages/crud/page.yml +++ b/examples/with-prisma-data-provider/toolpad/pages/crud/page.yml @@ -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 diff --git a/examples/with-prisma-data-provider/toolpad/pages/cursorBased/page.yml b/examples/with-prisma-data-provider/toolpad/pages/cursorBased/page.yml index 2fbd2f9782e..27d5778e8ca 100644 --- a/examples/with-prisma-data-provider/toolpad/pages/cursorBased/page.yml +++ b/examples/with-prisma-data-provider/toolpad/pages/cursorBased/page.yml @@ -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 diff --git a/examples/with-prisma-data-provider/toolpad/pages/indexBased/page.yml b/examples/with-prisma-data-provider/toolpad/pages/indexBased/page.yml index 03d439f6758..8fa5e6a9d8c 100644 --- a/examples/with-prisma-data-provider/toolpad/pages/indexBased/page.yml +++ b/examples/with-prisma-data-provider/toolpad/pages/indexBased/page.yml @@ -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 diff --git a/examples/with-prisma-data-provider/toolpad/prisma.ts b/examples/with-prisma-data-provider/toolpad/prisma.ts index 5d171c2fb92..31bfbe43c20 100644 --- a/examples/with-prisma-data-provider/toolpad/prisma.ts +++ b/examples/with-prisma-data-provider/toolpad/prisma.ts @@ -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 }; diff --git a/examples/with-prisma-data-provider/toolpad/resources/crud.ts b/examples/with-prisma-data-provider/toolpad/resources/crud.ts index 0e54173b8af..8681f2212e5 100644 --- a/examples/with-prisma-data-provider/toolpad/resources/crud.ts +++ b/examples/with-prisma-data-provider/toolpad/resources/crud.ts @@ -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, @@ -22,7 +100,7 @@ export default createDataProvider({ }, async deleteRecord(id) { - await prisma.user.delete({ + await model.delete({ where: { id: Number(id) }, }); }, diff --git a/packages/toolpad-app/src/canvas/index.tsx b/packages/toolpad-app/src/canvas/index.tsx index 5b53872b7ee..eed519dcb18 100644 --- a/packages/toolpad-app/src/canvas/index.tsx +++ b/packages/toolpad-app/src/canvas/index.tsx @@ -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( () => { @@ -25,6 +25,7 @@ export interface AppCanvasProps { export default function AppCanvas({ basename, state: initialState }: AppCanvasProps) { const [state, setState] = React.useState(initialState); + const [readyBridge, setReadyBridge] = React.useState(); const appRootRef = React.useRef(); const appRootCleanupRef = React.useRef<() => void>(); @@ -114,6 +115,7 @@ export default function AppCanvas({ basename, state: initialState }: AppCanvasPr }); bridge.canvasEvents.emit('ready', {}); + setReadyBridge(bridge); }, []); const savedNodes = state?.savedNodes; @@ -123,11 +125,15 @@ export default function AppCanvas({ basename, state: initialState }: AppCanvasPr }; }, [savedNodes]); - return ( - - - - - - ); + if (IS_RENDERED_IN_CANVAS) { + return readyBridge ? ( + + + + + + ) : null; + } + + return ; } diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index d6201a52bc6..e66d43939ed 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -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; diff --git a/packages/toolpad-app/src/toolpad/propertyControls/GridColumns.tsx b/packages/toolpad-app/src/toolpad/propertyControls/GridColumns.tsx index d0f061a66fc..433a81ddb5d 100644 --- a/packages/toolpad-app/src/toolpad/propertyControls/GridColumns.tsx +++ b/packages/toolpad-app/src/toolpad/propertyControls/GridColumns.tsx @@ -1,6 +1,8 @@ import { Box, Button, + Checkbox, + FormControlLabel, IconButton, List, ListItem, @@ -200,6 +202,38 @@ function GridColumnEditor({ ))} + + handleColumnChange({ + ...editedColumn, + sortable: event.target.checked, + }) + } + /> + } + label="Sortable" + /> + + + handleColumnChange({ + ...editedColumn, + filterable: event.target.checked, + }) + } + /> + } + label="Filterable" + /> + {editedColumn.type === 'number' ? ( inferColumns(Array.isArray(rawRows) ? rawRows : []), diff --git a/packages/toolpad-components/src/Chart.tsx b/packages/toolpad-components/src/Chart.tsx index a6defbd1d33..28e8d100496 100644 --- a/packages/toolpad-components/src/Chart.tsx +++ b/packages/toolpad-components/src/Chart.tsx @@ -168,7 +168,7 @@ function Chart({ data = [], loading, error, height, sx }: ChartProps) { return ( - + {displayError ? : null} {loading && !error ? (
= { }; export interface SerializableGridColumn - extends Pick { + extends Pick< + GridColDef, + 'field' | 'type' | 'align' | 'width' | 'headerName' | 'sortable' | 'filterable' + > { numberFormat?: NumberFormat; dateFormat?: DateFormat; dateTimeFormat?: DateFormat; @@ -487,8 +494,6 @@ interface ToolpadDataGridProps extends Omit void; hideToolbar?: boolean; - rawRows?: GridRowsProp; - onRawRowsChange?: (rows: GridRowsProp) => void; } interface DeleteActionProps { @@ -524,7 +529,7 @@ function DeleteAction({ id, dataProvider, refetch }: DeleteActionProps) { } interface DataProviderDataGridProps extends Partial { - error?: unknown; + rowLoadingError?: unknown; getActions?: GridActionsColDef['getActions']; } @@ -534,60 +539,90 @@ function useDataProviderDataGridProps( const useDataProvider = useNonNullableContext(UseDataProviderContext); const { dataProvider } = useDataProvider(dataProviderId || null); - const [paginationModel, setPaginationModel] = React.useState({ + const [rawPaginationModel, setRawPaginationModel] = React.useState({ page: 0, pageSize: 100, }); - const { page, pageSize } = paginationModel; - const mapPageToNextCursor = React.useRef(new Map()); - const { data, isFetching, isPlaceholderData, isLoading, error, refetch } = useQuery({ + const paginationModel = React.useMemo(() => { + const page = rawPaginationModel.page; + const pageSize = rawPaginationModel.pageSize; + if (dataProvider?.paginationMode === 'cursor') { + // cursor based pagination + let cursor: string | null = null; + if (page !== 0) { + cursor = mapPageToNextCursor.current.get(page - 1) ?? null; + if (cursor === null) { + throw new Error(`No cursor found for page ${page - 1}`); + } + } + return { + cursor, + pageSize, + }; + // TODO: when docs are on ts>5, replace with + // } satisfies CursorPaginationModel; + } + + // index based pagination + return { + start: page * pageSize, + pageSize, + }; + // TODO: when docs are on ts>5, replace with + // } satisfies IndexPaginationModel; + }, [dataProvider?.paginationMode, rawPaginationModel.page, rawPaginationModel.pageSize]); + + const [rawFilterModel, setRawFilterModel] = React.useState(); + + const filterModel = React.useMemo( + () => ({ + items: + rawFilterModel?.items.map(({ field, operator, value }) => ({ field, operator, value })) ?? + [], + logicOperator: rawFilterModel?.logicOperator ?? 'and', + }), + [rawFilterModel], + ); + + const [rawSortModel, setRawSortModel] = React.useState(); + + const sortModel = React.useMemo( + () => rawSortModel?.map(({ field, sort }) => ({ field, sort: sort ?? 'asc' })) ?? [], + [rawSortModel], + ); + + const { + data, + isFetching, + isPlaceholderData, + isLoading, + error: rowLoadingError, + refetch, + } = useQuery({ enabled: !!dataProvider, - queryKey: ['toolpadDataProvider', dataProviderId, page, pageSize], + queryKey: ['toolpadDataProvider', dataProviderId, paginationModel, filterModel, sortModel], placeholderData: keepPreviousData, queryFn: async () => { invariant(dataProvider, 'dataProvider must be defined'); - let dataProviderPaginationModel: IndexPaginationModel | CursorPaginationModel; - if (dataProvider.paginationMode === 'cursor') { - // cursor based pagination - let cursor: string | null = null; - if (page !== 0) { - cursor = mapPageToNextCursor.current.get(page - 1) ?? null; - if (cursor === null) { - throw new Error(`No cursor found for page ${page - 1}`); - } - } - dataProviderPaginationModel = { - cursor, - pageSize, - }; - // TODO: when docs are on ts>5, replace with - // } satisfies CursorPaginationModel; - } else { - // index based pagination - dataProviderPaginationModel = { - start: page * pageSize, - pageSize, - }; - // TODO: when docs are on ts>5, replace with - // } satisfies IndexPaginationModel; - } const result = await dataProvider.getRecords({ - paginationModel: dataProviderPaginationModel, + paginationModel, + filterModel, + sortModel, }); if (dataProvider.paginationMode === 'cursor') { if (typeof result.cursor === 'undefined') { throw new Error( - `No cursor returned for page ${page}. Return \`null\` to signal the end of the data.`, + `No cursor returned for page ${rawPaginationModel.page}. Return \`null\` to signal the end of the data.`, ); } if (typeof result.cursor === 'string') { - mapPageToNextCursor.current.set(page, result.cursor); + mapPageToNextCursor.current.set(rawPaginationModel.page, result.cursor); } } @@ -597,7 +632,9 @@ function useDataProviderDataGridProps( const rowCount = data?.totalCount ?? - (data?.hasNextPage ? (paginationModel.page + 1) * paginationModel.pageSize + 1 : undefined) ?? + (data?.hasNextPage + ? (rawPaginationModel.page + 1) * rawPaginationModel.pageSize + 1 + : undefined) ?? 0; const getActions = React.useMemo(() => { @@ -624,23 +661,41 @@ function useDataProviderDataGridProps( return { loading: isLoading || (isPlaceholderData && isFetching), paginationMode: 'server', + filterMode: 'server', + sortingMode: 'server', pagination: true, - paginationModel, rowCount, + paginationModel: rawPaginationModel, onPaginationModelChange(model) { - setPaginationModel((prevModel) => { + setRawPaginationModel((prevModel) => { if (prevModel.pageSize !== model.pageSize) { return { ...model, page: 0 }; } return model; }); }, + filterModel: rawFilterModel, + onFilterModelChange: setRawFilterModel, + sortModel: rawSortModel, + onSortModelChange: setRawSortModel, rows: data?.records ?? [], - error, + rowLoadingError, getActions, }; } +interface NoRowsOverlayProps extends React.ComponentProps { + error: Error; +} + +function NoRowsOverlay(props: NoRowsOverlayProps) { + if (props.error) { + return ; + } + + return ; +} + function dataGridFallbackRender({ error }: FallbackProps) { return ; } @@ -657,7 +712,6 @@ const DataGridComponent = React.forwardRef(function DataGridComponent( hideToolbar, rowsSource, dataProviderId, - onRawRowsChange, ...props }: ToolpadDataGridProps, ref: React.ForwardedRef, @@ -786,11 +840,11 @@ const DataGridComponent = React.forwardRef(function DataGridComponent( [getRowId, columns], ); - let error: Error | null = null; - if (dataProviderProps?.error) { - error = errorFrom(dataProviderProps.error); + let rowLoadingError: Error | null = null; + if (dataProviderProps?.rowLoadingError) { + rowLoadingError = errorFrom(dataProviderProps.rowLoadingError); } else if (errorProp) { - error = errorFrom(errorProp); + rowLoadingError = errorFrom(errorProp); } React.useEffect(() => { @@ -833,13 +887,10 @@ const DataGridComponent = React.forwardRef(function DataGridComponent( ref={ref} style={{ height: heightProp, minHeight: '100%', width: '100%', position: 'relative' }} > - -
@@ -849,6 +900,12 @@ const DataGridComponent = React.forwardRef(function DataGridComponent( slots={{ toolbar: hideToolbar ? null : GridToolbar, loadingOverlay: SkeletonLoadingOverlay, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + noRowsOverlay: { + error: rowLoadingError, + } as any, }} onColumnResize={handleResize} onColumnOrderChange={handleColumnOrderChange} diff --git a/packages/toolpad-components/src/Image.tsx b/packages/toolpad-components/src/Image.tsx index 23f49908b35..6f7713d81dc 100644 --- a/packages/toolpad-components/src/Image.tsx +++ b/packages/toolpad-components/src/Image.tsx @@ -65,7 +65,7 @@ function Image({ const error = errorProp || imgError; return ( - + {error ? : null} {loading && !error ? : null} ({ - position: 'absolute', - inset: '0 0 0 0', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - borderWidth: 1, - borderStyle: 'solid', - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.divider, + padding: theme.spacing(2), })); -interface ErrorOverlayProps { - error?: unknown; +interface ErrorContentProps { + sx?: SxProps; + error: NonNullable; } -export default function ErrorOverlay({ error }: ErrorOverlayProps) { - const errMessage = error ? errorFrom(error).message : null; - return errMessage ? ( - +export function ErrorContent({ sx, error }: ErrorContentProps) { + const errMessage = errorFrom(error).message; + return ( + Error {errMessage} - ) : null; + ); } + +const ErrorOverlay = styled(ErrorContent)(({ theme }) => ({ + position: 'absolute', + inset: '0 0 0 0', + borderWidth: 1, + borderStyle: 'solid', + borderRadius: theme.shape.borderRadius, + borderColor: theme.palette.divider, +})); + +export default ErrorOverlay; diff --git a/packages/toolpad-core/src/types.ts b/packages/toolpad-core/src/types.ts index e2c09483324..09fe514fcca 100644 --- a/packages/toolpad-core/src/types.ts +++ b/packages/toolpad-core/src/types.ts @@ -499,13 +499,39 @@ export interface CursorPaginationModel { pageSize: number; } +export interface FilterModelItem { + field: string; + operator: string; + value: unknown; +} + +export type LogicOperator = 'and' | 'or'; + +export interface FilterModel { + items: FilterModelItem[]; + logicOperator: LogicOperator; +} + +export type SortDirection = 'asc' | 'desc'; + +export interface SortItem { + field: string; + sort: SortDirection; +} + +export type SortModel = SortItem[]; + export type PaginationMode = 'index' | 'cursor'; +export type PaginationModel = M extends 'cursor' + ? CursorPaginationModel + : IndexPaginationModel; + // eslint-disable-next-line @typescript-eslint/no-unused-vars export interface GetRecordsParams { - paginationModel: P extends 'cursor' ? CursorPaginationModel : IndexPaginationModel; - // filterModel: FilterModel; - // sortModel: SortModel; + paginationModel: PaginationModel

; + filterModel: FilterModel; + sortModel: SortModel; } export interface GetRecordsResult {