/* eslint-disable filenames/match-regex */ import { Error } from '@/components/Layout/Error'; import { Card, CardContent, CircularProgress } from '@mui/material'; import { styled, useTheme } from '@mui/material/styles'; import { SxProps } from '@mui/system'; import clsx from 'clsx'; import { RaRecord, useListContext } from 'ra-core'; import { ElementType, ReactElement, ReactNode, cloneElement } from 'react'; import * as React from 'react'; import { CreateButton, Pagination as DefaultPagination, FilterButton, SavedQueriesListClasses, Title, TopToolbar } from 'react-admin'; import { ExportButton } from '@/components/ra-buttons/ExportButton'; import { ListToolbar } from './ListToolbar'; import { FilterSidebar } from './FilterSidebar'; import { MainCard } from '@/components/MainCard'; import { Empty } from '@/components/ra-lists/Empty'; import { type ListTabToolbarConfig } from './ListTabsToolbar'; import { FilterPicker } from './FilterPicker'; interface DefaultListActionsProps { filters?: ReactElement | ReactElement[]; } function DefaultListActions({ filters }: DefaultListActionsProps) { const filterPickerFilters = Array.isArray(filters) ? filters : undefined; return ( {filterPickerFilters ? : null} {filters && !filterPickerFilters ? : null} ); } function getDefaultActions(filters?: ReactElement | ReactElement[]) { return ; } const defaultPagination = ; const defaultEmpty = ; const DefaultComponent = Card; function ListView(props: ListViewProps): ReactElement | null { const { actions = undefined, aside, filters, bulkActionButtons, emptyWhileLoading, hasCreate, pagination = defaultPagination, children, className, component: Content = DefaultComponent, title, empty = defaultEmpty, tabs, ...rest } = props; const { defaultTitle, data, error, isLoading, filterValues, resource } = useListContext(props); // Se actions non รจ passato, usa quello di default con filters const resolvedActions = actions === undefined ? getDefaultActions(filters) : actions; const theme = useTheme() as any; const hasAsideSidebar = React.isValidElement(aside) && aside?.type !== FilterSidebar; const hasFilterSidebar = React.isValidElement(aside) && aside?.type === FilterSidebar; if (!children || (!data && isLoading && emptyWhileLoading)) { return null; } function renderList(shouldRenderEmptyList: boolean) { return (
{filters || resolvedActions ? ( ) : null} {!error && ( {bulkActionButtons && children && React.isValidElement(children) ? cloneElement(children, { bulkActionButtons }) : children} )} {error ? ( ) : ( pagination !== false && pagination )}
); } function renderEmpty() { return empty !== false && cloneElement(empty, { className: ListClasses.noResults, hasCreate }); } const shouldRenderEmptyList = !isLoading && data?.length === 0; const shouldRenderEmptyPage = !isLoading && data?.length === 0 && !Object.keys(filterValues).length && empty !== false; // Check if 'aside' is a valid React element const isAsideValid = React.isValidElement(aside); // Determine if 'aside' is a FilterSidebar component const isFilterSidebar = isAsideValid && aside?.type === FilterSidebar; // Use React.Fragment if there's a generic 'aside', otherwise use MainCard const Wrapper = isLoading || shouldRenderEmptyPage || (isAsideValid && !isFilterSidebar) ? React.Fragment : MainCard; // Define props for the ListWrapper based on the presence of 'aside' const wrapperProps = !isLoading && !shouldRenderEmptyPage && (!aside || isFilterSidebar) ? { content: false } : undefined; return ( <Wrapper {...wrapperProps}> {isLoading ? ( <CircularProgress sx={{ position: 'absolute', top: '50%', left: '50%', marginTop: '-20px', marginLeft: '-20px' }} /> ) : shouldRenderEmptyPage ? ( renderEmpty() ) : ( renderList(shouldRenderEmptyList) )} </Wrapper> {hasAsideSidebar ? <AsideCard aside={aside} /> : null} {/* @ts-ignore */} {hasFilterSidebar ? React.cloneElement(aside, { filters }) : null} </Root> ); } interface AsideCardProps { aside: React.ReactElement; } function AsideCard({ aside }: AsideCardProps): ReactElement { const order = (aside?.props as { order?: number })?.order ?? -1; const theme = useTheme() as any; return ( <Card className={ListClasses.asideCard} sx={{ order, mr: order === -1 ? 2 : 0, ml: order !== -1 ? 2 : 0, width: 200, border: '1px solid', borderRadius: 1, borderColor: theme.palette.mode === 'dark' ? theme.palette.divider : theme.palette.grey.A800, backgroundColor: theme.palette.background.paper, backgroundImage: 'none' }} > <CardContent sx={{ pt: 1 }}>{cloneElement(aside, {})}</CardContent> </Card> ); } const PREFIX = 'RaList'; const ListClasses = { main: `${PREFIX}-main`, content: `${PREFIX}-content`, actions: `${PREFIX}-actions`, noResults: `${PREFIX}-noResults`, asideCard: `${PREFIX}-asideCard` }; interface ListViewProps { /** * Tabs to display in the toolbar. Each tab is a filter object with a label, key, icon, filter, and an optional default property. * * @example * import { List, ListTabToolbarConfig } from 'react-admin'; * const tabs: ListTabToolbarConfig[] = [ * { label: 'Published', key: 'published', icon: <IconPublished />, filter: { status: 'published' }, default: true }, * { label: 'Drafts', key: 'drafts', icon: <IconDrafts />, filter: { status: 'draft' } }, * ]; * const PostList = () => ( * <List tabs={tabs}> * ... * </List> * ); */ tabs?: ListTabToolbarConfig[]; /** * A unique key to identify the tab group. Used to persist the selected tab in session storage. * Must be stable across renders and unique among different List components. * * @example * import { List, ListTabToolbarConfig } from 'react-admin'; * const tabs: ListTabToolbarConfig[] = [ * { label: 'Published', key: 'published', icon: <IconPublished />, filter: { status: 'published' }, default: true }, * { label: 'Drafts', key: 'drafts', icon: <IconDrafts />, filter: { status: 'draft' } }, * ]; * const PostList = () => ( * <List tabs={tabs} tabGroupKey="post-list-tabs"> * ... * </List> * ); */ tabGroupKey?: string; /** * The actions to display in the toolbar. defaults to Filter + Create + Export. * * @see https://marmelab.com/react-admin/List.html#actions * @example * import { * CreateButton, * DatagridConfigurable, * ExportButton, * FilterButton, * List, * SelectColumnsButton, * TopToolbar, * } from 'react-admin'; * import IconEvent from '@mui/icons-material/Event'; * * const ListActions = () => ( * <TopToolbar> * <SelectColumnsButton /> * <FilterButton/> * <CreateButton/> * <ExportButton/> * </TopToolbar> * ); * * export const PostList = () => ( * <List actions={<ListActions/>}> * <DatagridConfigurable> * ... * </DatagridConfigurable> * </List> * ); */ actions?: ReactElement | false; /** * The content to render as a sidebar. * @see https://marmelab.com/react-admin/List.html#aside * @example * import { List, useListContext } from 'react-admin'; * import { Typography } from '@mui/material'; * * const Aside = () => { * const { data, isLoading } = useListContext(); * if (isLoading) return null; * return ( * <div style={{ width: 200, margin: '4em 1em' }}> * <Typography variant="h6">Posts stats</Typography> * <Typography variant="body2"> * Total views: {data.reduce((sum, post) => sum + post.views, 0)} * </Typography> * </div> * ); * }; * * const PostList = () => ( * <List aside={<Aside />}> * ... * </List> * ); */ aside?: ReactElement; /** * @deprecated pass the bulkActionButtons prop to the List child (Datagrid or SimpleList) instead */ bulkActionButtons?: ReactElement | false; /** * A class name to apply to the root div element */ className?: string; /** * The components rendering the list of records. Usually a <Datagrid> or <SimpleList>. * * @see https://marmelab.com/react-admin/List.html#children * @example * import { List, Datagrid, TextField, DateField, NumberField, BooleanField, ReferenceManyCount } from 'react-admin'; * * export const BookList = () => ( * <List> * <Datagrid rowClick="edit"> * <TextField source="id" /> * <TextField source="title" /> * <DateField source="published_at" /> * <ReferenceManyCount label="Nb comments" reference="comments" target="post_id" link /> * <BooleanField source="commentable" label="Com." /> * <NumberField source="nb_views" label="Views" /> * </Datagrid> * </List> * ); */ children: ReactNode; /** * The component used to display the list. Defaults to <Card>. * * @see https://marmelab.com/react-admin/List.html#component * @example * import { List } from 'react-admin'; * * const PostList = () => ( * <List component="div"> * ... * </List> * ); */ component?: ElementType; /** * The component to display when the list is empty. * * @see https://marmelab.com/react-admin/List.html#empty * @example * import { CreateButton, List } from 'react-admin'; * import { Box, Button, Typography } from '@mui/material'; * * const Empty = () => ( * <Box textAlign="center" m={1}> * <Typography variant="h4" paragraph> * No products available * </Typography> * <Typography variant="body1"> * Create one or import products from a file * </Typography> * <CreateButton /> * <Button onClick={...}>Import</Button> * </Box> * ); * * const ProductList = () => ( * <List empty={<Empty />}> * ... * </List> * ); */ empty?: ReactElement | false; /** * Set to true to return null while the list is loading. * * @see https://marmelab.com/react-admin/List.html#emptywhileloading * @example * import { List } from 'react-admin'; * import { SimpleBookList } from './BookList'; * * const BookList = () => ( * <List emptyWhileLoading> * <SimpleBookList /> * </List> * ); */ emptyWhileLoading?: boolean; /** * The filter inputs to display in the toolbar. * * @see https://marmelab.com/react-admin/List.html#filters * @example * import { List, TextInput } from 'react-admin'; * * const postFilters = [ * <TextInput label="Search" source="q" alwaysOn />, * <TextInput label="Title" source="title" defaultValue="Hello, World!" />, * ]; * * export const PostList = () => ( * <List filters={postFilters}> * ... * </List> * ); */ filters?: ReactElement | ReactElement[]; /** * Set to true to force a Create button in the toolbar, even if there is no create view declared in Resource * * @see https://marmelab.com/react-admin/List.html#hascreate * @example * import { List } from 'react-admin'; * * export const PostList = () => ( * <List hasCreate={false}> * ... * </List> * ); */ hasCreate?: boolean; /** * The pagination component to display. defaults to <Pagination /> * * @see https://marmelab.com/react-admin/List.html#pagination * @example * import { Pagination, List } from 'react-admin'; * * const PostPagination = props => <Pagination rowsPerPageOptions={[10, 25, 50, 100]} {...props} />; * * export const PostList = () => ( * <List pagination={<PostPagination />}> * ... * </List> * ); */ pagination?: ReactElement | false; /** * The page title (main title) to display above the data. Defaults to the humanized resource name. * * @see https://marmelab.com/react-admin/List.html#title * @example * import { List } from 'react-admin'; * * export const PostList = () => ( * <List title="List of posts"> * ... * </List> * ); */ title?: string | ReactElement; /** * The CSS styles to apply to the component. * * @see https://marmelab.com/react-admin/List.html#sx-css-api * @example * const PostList = () => ( * <List * sx={{ * backgroundColor: 'yellow', * '& .RaList-content': { * backgroundColor: 'red', * }, * }} * > * ... * </List> * ); */ sx?: SxProps; } const Root = styled('div', { name: PREFIX, overridesResolver: (_, styles) => styles.root })(({ theme }) => ({ display: 'flex', '& >.MuiPaper-root': { flex: 1 }, [`& .${ListClasses.main}`]: { flex: '1 1 auto', display: 'flex', flexDirection: 'column' }, [`& .${ListClasses.content}`]: { position: 'relative', [theme.breakpoints.down('sm')]: { boxShadow: 'none' }, overflow: 'inherit' }, [`& .${ListClasses.actions}`]: {}, [`& .${ListClasses.noResults}`]: { alignContent: 'center' }, [`& .${ListClasses.asideCard}`]: { [theme.breakpoints.down('md')]: { display: 'none' }, [`& .${SavedQueriesListClasses.floatingIcon}`]: { fontSize: '1.125rem' }, [`& .${SavedQueriesListClasses.floatingTooltip}`]: { top: '-1.2em' }, '& form': { marginTop: theme.spacing(2) } } })); export { ListView }; export type { ListViewProps };