@toolpad/core
Version:
Dashboard framework powered by Material UI.
380 lines (379 loc) • 13.1 kB
JavaScript
'use client';
var _EditIcon, _DeleteIcon, _RefreshIcon, _AddIcon;
import * as React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import { DataGrid, GridToolbar, GridActionsCellItem, gridClasses } from '@mui/x-data-grid';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import invariant from 'invariant';
import { useDialogs } from "../useDialogs/index.js";
import { useNotifications } from "../useNotifications/index.js";
import { NoSsr } from "../shared/NoSsr.js";
import { CrudContext, RouterContext, WindowContext } from "../shared/context.js";
import { useLocaleText } from "../AppProvider/LocalizationProvider.js";
import { DataSourceCache } from "./cache.js";
import { useCachedDataSource } from "./useCachedDataSource.js";
import { CRUD_DEFAULT_LOCALE_TEXT } from "./localeText.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const ErrorOverlay = styled('div')(({
theme
}) => ({
position: 'absolute',
backgroundColor: theme.palette.error.light,
borderRadius: '4px',
top: 0,
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
p: 1,
zIndex: 10
}));
/**
*
* Demos:
*
* - [CRUD](https://mui.com/toolpad/core/react-crud/)
*
* API:
*
* - [List API](https://mui.com/toolpad/core/api/list)
*/
function List(props) {
const {
initialPageSize = 100,
onRowClick,
onCreateClick,
onEditClick,
onDelete,
dataSourceCache,
slots,
slotProps,
localeText: propsLocaleText
} = props;
const globalLocaleText = useLocaleText();
const localeText = {
...CRUD_DEFAULT_LOCALE_TEXT,
...globalLocaleText,
...propsLocaleText
};
const crudContext = React.useContext(CrudContext);
const dataSource = props.dataSource ?? crudContext.dataSource;
invariant(dataSource, 'No data source found.');
const cache = React.useMemo(() => {
const manualCache = dataSourceCache ?? crudContext.dataSourceCache;
return typeof manualCache !== 'undefined' ? manualCache : new DataSourceCache();
}, [crudContext.dataSourceCache, dataSourceCache]);
const cachedDataSource = useCachedDataSource(dataSource, cache);
const {
fields,
validate,
...methods
} = cachedDataSource;
const {
getMany,
deleteOne
} = methods;
const routerContext = React.useContext(RouterContext);
const appWindowContext = React.useContext(WindowContext);
const appWindow = appWindowContext ?? (typeof window !== 'undefined' ? window : null);
const dialogs = useDialogs();
const notifications = useNotifications();
const [rowsState, setRowsState] = React.useState({
rows: [],
rowCount: 0
});
const [paginationModel, setPaginationModel] = React.useState({
page: routerContext?.searchParams.get('page') ? Number(routerContext?.searchParams.get('page')) : 0,
pageSize: routerContext?.searchParams.get('pageSize') ? Number(routerContext?.searchParams.get('pageSize')) : initialPageSize
});
const [filterModel, setFilterModel] = React.useState(routerContext?.searchParams.get('filter') ? JSON.parse(routerContext?.searchParams.get('filter') ?? '') : {
items: []
});
const [sortModel, setSortModel] = React.useState(routerContext?.searchParams.get('sort') ? JSON.parse(routerContext?.searchParams.get('sort') ?? '') : []);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
if (appWindow) {
const url = new URL(appWindow.location.href);
url.searchParams.set('page', String(paginationModel.page));
url.searchParams.set('pageSize', String(paginationModel.pageSize));
if (!appWindow.frameElement) {
appWindow.history.pushState({}, '', url);
}
}
}, [appWindow, paginationModel.page, paginationModel.pageSize]);
React.useEffect(() => {
if (appWindow) {
const url = new URL(appWindow.location.href);
if (filterModel.items.length > 0 || filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0) {
url.searchParams.set('filter', JSON.stringify(filterModel));
} else {
url.searchParams.delete('filter');
}
if (!appWindow.frameElement) {
appWindow.history.pushState({}, '', url);
}
}
}, [appWindow, filterModel]);
React.useEffect(() => {
if (appWindow) {
const url = new URL(appWindow.location.href);
if (sortModel.length > 0) {
url.searchParams.set('sort', JSON.stringify(sortModel));
} else {
url.searchParams.delete('sort');
}
if (!appWindow.frameElement) {
appWindow.history.pushState({}, '', url);
}
}
}, [appWindow, sortModel]);
const loadData = React.useCallback(async () => {
setError(null);
setIsLoading(true);
try {
const listData = await getMany({
paginationModel,
sortModel,
filterModel
});
setRowsState({
rows: listData.items,
rowCount: listData.itemCount
});
} catch (listDataError) {
setError(listDataError);
}
setIsLoading(false);
}, [filterModel, getMany, paginationModel, sortModel]);
React.useEffect(() => {
loadData();
}, [filterModel, getMany, loadData, paginationModel, sortModel]);
const handleRefresh = React.useCallback(() => {
if (!isLoading) {
cache?.clear();
loadData();
}
}, [cache, isLoading, loadData]);
const handleRowClick = React.useCallback(({
row
}) => {
if (onRowClick) {
onRowClick(row.id);
}
}, [onRowClick]);
const handleItemEdit = React.useCallback(itemId => () => {
if (onEditClick) {
onEditClick(itemId);
}
}, [onEditClick]);
const handleItemDelete = React.useCallback(itemId => async () => {
const confirmed = await dialogs.confirm(localeText.deleteConfirmMessage, {
title: localeText.deleteConfirmTitle,
severity: 'error',
okText: localeText.deleteConfirmLabel,
cancelText: localeText.deleteCancelLabel
});
if (confirmed) {
setIsLoading(true);
try {
await deleteOne?.(itemId);
if (onDelete) {
onDelete(itemId);
}
notifications.show(localeText.deleteSuccessMessage, {
severity: 'success',
autoHideDuration: 3000
});
loadData();
} catch (deleteError) {
notifications.show(`${localeText.deleteErrorMessage} ${deleteError.message}`, {
severity: 'error',
autoHideDuration: 3000
});
}
setIsLoading(false);
}
}, [deleteOne, dialogs, loadData, localeText.deleteCancelLabel, localeText.deleteConfirmLabel, localeText.deleteConfirmMessage, localeText.deleteConfirmTitle, localeText.deleteErrorMessage, localeText.deleteSuccessMessage, notifications, onDelete]);
const DataGridSlot = slots?.dataGrid ?? DataGrid;
const initialState = React.useMemo(() => ({
pagination: {
paginationModel: {
pageSize: initialPageSize
}
}
}), [initialPageSize]);
const columns = React.useMemo(() => {
return [...fields, {
field: 'actions',
type: 'actions',
flex: 1,
align: 'right',
getActions: ({
id
}) => [...(onEditClick ? [/*#__PURE__*/_jsx(GridActionsCellItem, {
icon: _EditIcon || (_EditIcon = /*#__PURE__*/_jsx(EditIcon, {})),
label: localeText.editLabel,
onClick: handleItemEdit(id)
}, "edit-item")] : []), ...(deleteOne ? [/*#__PURE__*/_jsx(GridActionsCellItem, {
icon: _DeleteIcon || (_DeleteIcon = /*#__PURE__*/_jsx(DeleteIcon, {})),
label: localeText.deleteLabel,
onClick: handleItemDelete(id)
}, "delete-item")] : [])]
}];
}, [deleteOne, fields, handleItemDelete, handleItemEdit, localeText.deleteLabel, localeText.editLabel, onEditClick]);
return /*#__PURE__*/_jsxs(Stack, {
sx: {
flex: 1,
width: '100%'
},
children: [/*#__PURE__*/_jsxs(Stack, {
direction: "row",
alignItems: "center",
justifyContent: "space-between",
sx: {
mb: 1
},
children: [/*#__PURE__*/_jsx(Tooltip, {
title: localeText.reloadButtonLabel,
placement: "right",
enterDelay: 1000,
children: /*#__PURE__*/_jsx("div", {
children: /*#__PURE__*/_jsx(IconButton, {
"aria-label": "refresh",
onClick: handleRefresh,
children: _RefreshIcon || (_RefreshIcon = /*#__PURE__*/_jsx(RefreshIcon, {}))
})
})
}), onCreateClick ? /*#__PURE__*/_jsx(Button, {
variant: "contained",
onClick: onCreateClick,
startIcon: _AddIcon || (_AddIcon = /*#__PURE__*/_jsx(AddIcon, {})),
children: localeText.createNewButtonLabel
}) : null]
}), /*#__PURE__*/_jsxs(Box, {
sx: {
flex: 1,
position: 'relative',
width: '100%'
},
children: [/*#__PURE__*/_jsx(NoSsr, {
children: /*#__PURE__*/_jsx(DataGridSlot, {
rows: rowsState.rows,
rowCount: rowsState.rowCount,
columns: columns,
pagination: true,
sortingMode: "server",
filterMode: "server",
paginationMode: "server",
paginationModel: paginationModel,
onPaginationModelChange: setPaginationModel,
sortModel: sortModel,
onSortModelChange: setSortModel,
filterModel: filterModel,
onFilterModelChange: setFilterModel,
onRowClick: handleRowClick,
loading: isLoading,
initialState: initialState,
slots: {
toolbar: GridToolbar
}
// Prevent type conflicts if slotProps don't match DataGrid used for dataGrid slot
,
...slotProps?.dataGrid,
sx: {
[`& .${gridClasses.columnHeader}, & .${gridClasses.cell}`]: {
outline: 'transparent'
},
[`& .${gridClasses.columnHeader}:focus-within, & .${gridClasses.cell}:focus-within`]: {
outline: 'none'
},
...(onRowClick ? {
[`& .${gridClasses.row}:hover`]: {
cursor: 'pointer'
}
} : {}),
...slotProps?.dataGrid?.sx
}
})
}), error && /*#__PURE__*/_jsx(ErrorOverlay, {
children: /*#__PURE__*/_jsx(Typography, {
variant: "body1",
children: error.message
})
})]
})]
});
}
process.env.NODE_ENV !== "production" ? List.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources).
*/
dataSource: PropTypes.object,
/**
* [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source.
*/
dataSourceCache: PropTypes.shape({
cache: PropTypes.object.isRequired,
clear: PropTypes.func.isRequired,
get: PropTypes.func.isRequired,
set: PropTypes.func.isRequired,
ttl: PropTypes.number.isRequired
}),
/**
* Initial number of rows to show per page.
* @default 100
*/
initialPageSize: PropTypes.number,
/**
* Locale text for the component.
*/
localeText: PropTypes.object,
/**
* Callback fired when the "Create" button is clicked.
*/
onCreateClick: PropTypes.func,
/**
* Callback fired when the item is successfully deleted.
*/
onDelete: PropTypes.func,
/**
* Callback fired when the "Edit" button is clicked.
*/
onEditClick: PropTypes.func,
/**
* Callback fired when a row is clicked. Not called if the target clicked is an interactive element added by the built-in columns.
*/
onRowClick: PropTypes.func,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
dataGrid: PropTypes.object
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
dataGrid: PropTypes.func
})
} : void 0;
export { List };