UNPKG

@toolpad/core

Version:

Dashboard framework powered by Material UI.

380 lines (379 loc) 13.1 kB
'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 };