= {
};
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 {