@applica-software-guru/react-admin 1.5.301 → 1.5.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +1 -1
- package/dist/components/Layout/Sidebar/Content.d.ts +8 -0
- package/dist/components/Layout/Sidebar/Content.d.ts.map +1 -0
- package/dist/components/Layout/Sidebar/Drawer.d.ts +11 -0
- package/dist/components/Layout/Sidebar/Drawer.d.ts.map +1 -0
- package/dist/components/Layout/Sidebar/Footer.d.ts +7 -0
- package/dist/components/Layout/Sidebar/Footer.d.ts.map +1 -0
- package/dist/components/Layout/Sidebar/Header.d.ts +16 -0
- package/dist/components/Layout/Sidebar/Header.d.ts.map +1 -0
- package/dist/components/Layout/Sidebar/index.d.ts +14 -0
- package/dist/components/Layout/Sidebar/index.d.ts.map +1 -0
- package/dist/components/Layout/TopToolbar.d.ts +11 -0
- package/dist/components/Layout/TopToolbar.d.ts.map +1 -0
- package/dist/components/Layout/index.d.ts +2 -0
- package/dist/components/Layout/index.d.ts.map +1 -1
- package/dist/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/components/ra-lists/FilterSidebar/ActiveFiltersChips.d.ts +4 -0
- package/dist/components/ra-lists/FilterSidebar/ActiveFiltersChips.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/FilterSidebar.d.ts +7 -0
- package/dist/components/ra-lists/FilterSidebar/FilterSidebar.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/FilterSidebarButton.d.ts +3 -0
- package/dist/components/ra-lists/FilterSidebar/FilterSidebarButton.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/FilterSidebarContext.d.ts +13 -0
- package/dist/components/ra-lists/FilterSidebar/FilterSidebarContext.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/RemoveSavedQueryDialog.d.ts +11 -0
- package/dist/components/ra-lists/FilterSidebar/RemoveSavedQueryDialog.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/SaveFiltersButton.d.ts +3 -0
- package/dist/components/ra-lists/FilterSidebar/SaveFiltersButton.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/SaveFiltersDialog.d.ts +7 -0
- package/dist/components/ra-lists/FilterSidebar/SaveFiltersDialog.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/SavedFiltersList.d.ts +6 -0
- package/dist/components/ra-lists/FilterSidebar/SavedFiltersList.d.ts.map +1 -0
- package/dist/components/ra-lists/FilterSidebar/index.d.ts +9 -0
- package/dist/components/ra-lists/FilterSidebar/index.d.ts.map +1 -0
- package/dist/components/ra-lists/List.d.ts.map +1 -1
- package/dist/components/ra-lists/ListToolbar.d.ts +25 -0
- package/dist/components/ra-lists/ListToolbar.d.ts.map +1 -0
- package/dist/components/ra-lists/ListView.d.ts.map +1 -1
- package/dist/components/ra-lists/ListViewProvider.d.ts +11 -0
- package/dist/components/ra-lists/ListViewProvider.d.ts.map +1 -0
- package/dist/components/ra-lists/index.d.ts +3 -0
- package/dist/components/ra-lists/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/react-admin.cjs.js +58 -58
- package/dist/react-admin.cjs.js.gz +0 -0
- package/dist/react-admin.cjs.js.map +1 -1
- package/dist/react-admin.es.js +12072 -11478
- package/dist/react-admin.es.js.gz +0 -0
- package/dist/react-admin.es.js.map +1 -1
- package/dist/react-admin.umd.js +58 -58
- package/dist/react-admin.umd.js.gz +0 -0
- package/dist/react-admin.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Layout/Sidebar/Content.tsx +13 -0
- package/src/components/Layout/Sidebar/Drawer.tsx +50 -0
- package/src/components/Layout/Sidebar/Footer.tsx +20 -0
- package/src/components/Layout/Sidebar/Header.tsx +54 -0
- package/src/components/Layout/Sidebar/index.ts +22 -0
- package/src/components/Layout/TopToolbar.tsx +43 -0
- package/src/components/Layout/index.ts +2 -0
- package/src/components/Pagination/Pagination.tsx +3 -0
- package/src/components/ra-lists/FilterSidebar/ActiveFiltersChips.tsx +108 -0
- package/src/components/ra-lists/FilterSidebar/FilterSidebar.tsx +164 -0
- package/src/components/ra-lists/FilterSidebar/FilterSidebarButton.tsx +44 -0
- package/src/components/ra-lists/FilterSidebar/FilterSidebarContext.tsx +53 -0
- package/src/components/ra-lists/FilterSidebar/RemoveSavedQueryDialog.tsx +72 -0
- package/src/components/ra-lists/FilterSidebar/SaveFiltersButton.tsx +28 -0
- package/src/components/ra-lists/FilterSidebar/SaveFiltersDialog.tsx +138 -0
- package/src/components/ra-lists/FilterSidebar/SavedFiltersList.tsx +118 -0
- package/src/components/ra-lists/FilterSidebar/index.ts +8 -0
- package/src/components/ra-lists/List.tsx +23 -6
- package/src/components/ra-lists/ListToolbar.tsx +101 -0
- package/src/components/ra-lists/ListView.tsx +18 -8
- package/src/components/ra-lists/ListViewProvider.tsx +23 -0
- package/src/components/ra-lists/index.ts +3 -0
- package/src/index.ts +3 -2
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Box } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
type ContentProps = {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
sx?: Record<string, any>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function Content(props: ContentProps) {
|
|
9
|
+
const { sx } = props;
|
|
10
|
+
return <Box sx={{ ...sx, flex: '1 1 auto', backgroundColor: 'background.paper' }}>{props.children}</Box>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Content };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Drawer as MUIDrawer, useMediaQuery, useTheme } from '@mui/material';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { useSx } from '@/hooks';
|
|
4
|
+
import { useListViewContext } from '@/components/ra-lists';
|
|
5
|
+
import { isFunction } from 'lodash';
|
|
6
|
+
|
|
7
|
+
type DrawerProps = {
|
|
8
|
+
PaperProps?: Record<string, any>;
|
|
9
|
+
anchor?: 'left' | 'right' | 'top' | 'bottom';
|
|
10
|
+
variant?: 'temporary' | 'persistent' | 'permanent';
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
open?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function Drawer(props: DrawerProps) {
|
|
16
|
+
const { PaperProps, anchor = 'right', variant = 'temporary', open = false } = props;
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
19
|
+
const anchorPosition = isMobile ? 'bottom' : anchor;
|
|
20
|
+
const { setSidebarOpen } = useListViewContext();
|
|
21
|
+
const handleClose = useCallback(() => {
|
|
22
|
+
if (isFunction(setSidebarOpen)) {
|
|
23
|
+
setSidebarOpen(false);
|
|
24
|
+
}
|
|
25
|
+
}, [setSidebarOpen]);
|
|
26
|
+
const sx = useSx(PaperProps ?? {}, {
|
|
27
|
+
backgroundColor: 'background.default',
|
|
28
|
+
boxShadow: '-10px 4px 42px 0px rgba(0, 0, 0, 0.25)',
|
|
29
|
+
width: {
|
|
30
|
+
xs: window.innerWidth,
|
|
31
|
+
sm: 480,
|
|
32
|
+
md: 600
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<MUIDrawer
|
|
38
|
+
{...props}
|
|
39
|
+
anchor={anchorPosition}
|
|
40
|
+
onClose={handleClose}
|
|
41
|
+
open={open}
|
|
42
|
+
PaperProps={{ sx: sx }}
|
|
43
|
+
variant={variant}
|
|
44
|
+
>
|
|
45
|
+
{props.children}
|
|
46
|
+
</MUIDrawer>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { Drawer };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Box, Divider } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface FooterProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function Footer(props: FooterProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Box>
|
|
12
|
+
<Divider />
|
|
13
|
+
<Box bgcolor={'background.paper'} px={2} py={2.5}>
|
|
14
|
+
{props.children}
|
|
15
|
+
</Box>
|
|
16
|
+
</Box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Footer };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Box, Divider, Stack, Typography } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
interface HeaderProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function Header(props: HeaderProps) {
|
|
8
|
+
return (
|
|
9
|
+
<Box>
|
|
10
|
+
<Stack
|
|
11
|
+
alignItems="center"
|
|
12
|
+
bgcolor="background.paper"
|
|
13
|
+
direction="row"
|
|
14
|
+
px={2.5}
|
|
15
|
+
py={2}
|
|
16
|
+
spacing={2}
|
|
17
|
+
minHeight={'64px'}
|
|
18
|
+
>
|
|
19
|
+
{props.children}
|
|
20
|
+
</Stack>
|
|
21
|
+
<Divider sx={{ opacity: 0.6 }} />
|
|
22
|
+
</Box>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HeaderTextProps {
|
|
27
|
+
primary?: React.ReactNode;
|
|
28
|
+
secondary?: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function HeaderText(props: HeaderTextProps) {
|
|
32
|
+
const { primary, secondary } = props;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Box display="inline-flex" flexGrow={1} flexShrink={1} flexDirection={'column'}>
|
|
36
|
+
{primary ? <Typography variant="h5">{primary}</Typography> : null}
|
|
37
|
+
{secondary ? <Typography variant="h6">{secondary}</Typography> : null}
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface HeaderActionProps {
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function HeaderAction(props: HeaderActionProps) {
|
|
47
|
+
return (
|
|
48
|
+
<Box display="inline-flex" flexGrow={0} flexShrink={0}>
|
|
49
|
+
{props.children}
|
|
50
|
+
</Box>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { Header, HeaderAction, HeaderText };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Content } from './Content';
|
|
2
|
+
import { Drawer } from './Drawer';
|
|
3
|
+
import { Footer } from './Footer';
|
|
4
|
+
import { Header, HeaderAction, HeaderText } from './Header';
|
|
5
|
+
|
|
6
|
+
type ISidebar = typeof Drawer & {
|
|
7
|
+
Content: typeof Content;
|
|
8
|
+
Footer: typeof Footer;
|
|
9
|
+
Header: typeof Header;
|
|
10
|
+
HeaderAction: typeof HeaderAction;
|
|
11
|
+
HeaderText: typeof HeaderText;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DefaultSidebar = Drawer as ISidebar;
|
|
15
|
+
|
|
16
|
+
DefaultSidebar.Content = Content;
|
|
17
|
+
DefaultSidebar.Footer = Footer;
|
|
18
|
+
DefaultSidebar.Header = Header;
|
|
19
|
+
DefaultSidebar.HeaderText = HeaderText;
|
|
20
|
+
DefaultSidebar.HeaderAction = HeaderAction;
|
|
21
|
+
|
|
22
|
+
export { DefaultSidebar as Sidebar };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Theme, Toolbar, ToolbarProps, styled, useMediaQuery } from '@mui/material';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
|
|
4
|
+
function TopToolbar(props: ToolbarProps) {
|
|
5
|
+
const isXSmall = useMediaQuery<Theme>((theme) => theme.breakpoints.down('sm'));
|
|
6
|
+
return <StyledToolbar disableGutters variant={isXSmall ? 'regular' : 'dense'} {...sanitizeToolbarRestProps(props)} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
TopToolbar.propTypes = {
|
|
10
|
+
children: PropTypes.node,
|
|
11
|
+
className: PropTypes.string
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const PREFIX = 'RaTopToolbar';
|
|
15
|
+
|
|
16
|
+
const StyledToolbar = styled(Toolbar, {
|
|
17
|
+
name: PREFIX,
|
|
18
|
+
overridesResolver: (styles) => styles.root
|
|
19
|
+
})(({ theme }) => ({
|
|
20
|
+
display: 'flex',
|
|
21
|
+
justifyContent: 'flex-end',
|
|
22
|
+
alignItems: 'flex-end',
|
|
23
|
+
gap: theme.spacing(1),
|
|
24
|
+
whiteSpace: 'nowrap',
|
|
25
|
+
flex: '0 1 auto',
|
|
26
|
+
paddingTop: theme.spacing(0.5),
|
|
27
|
+
paddingBottom: theme.spacing(0.5),
|
|
28
|
+
paddingInline: theme.spacing(2),
|
|
29
|
+
[theme.breakpoints.down('md')]: {
|
|
30
|
+
flex: '0 1 100%'
|
|
31
|
+
},
|
|
32
|
+
[theme.breakpoints.down('sm')]: {
|
|
33
|
+
backgroundColor: theme.palette.background.paper,
|
|
34
|
+
justifyContent: 'space-between',
|
|
35
|
+
alignItems: 'center'
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
function sanitizeToolbarRestProps({ hasCreate, ...props }: any): any {
|
|
40
|
+
return props;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { TopToolbar };
|
|
@@ -9,7 +9,9 @@ export * from './MenuProvider';
|
|
|
9
9
|
export * from './NavMenu';
|
|
10
10
|
export * from './Navigation';
|
|
11
11
|
export * from './Provider';
|
|
12
|
+
export * as Sidebar from './Sidebar';
|
|
12
13
|
export * from './ThemeColor';
|
|
13
14
|
export * from './ThemeProvider';
|
|
14
15
|
export * from './ThemeToggler';
|
|
16
|
+
export * from './TopToolbar';
|
|
15
17
|
export * from './Wrapper';
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ListPaginationContextValue,
|
|
7
7
|
sanitizeListRestProps,
|
|
8
8
|
useListPaginationContext,
|
|
9
|
+
useResourceDefinition,
|
|
9
10
|
useTranslate
|
|
10
11
|
} from 'ra-core';
|
|
11
12
|
|
|
@@ -18,6 +19,7 @@ const Pagination: FC<PaginationProps> = memo((props) => {
|
|
|
18
19
|
const translate = useTranslate();
|
|
19
20
|
const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'));
|
|
20
21
|
const [currentPage, setCurrentPage] = useState(page - 1); // Stato per la UI
|
|
22
|
+
const { hasCreate } = useResourceDefinition(props);
|
|
21
23
|
|
|
22
24
|
const totalPages = useMemo(() => {
|
|
23
25
|
return total != null ? Math.ceil(total / perPage) : undefined;
|
|
@@ -125,6 +127,7 @@ const Pagination: FC<PaginationProps> = memo((props) => {
|
|
|
125
127
|
rowsPerPageOptions={emptyArray}
|
|
126
128
|
component="span"
|
|
127
129
|
labelDisplayedRows={labelDisplayedRows}
|
|
130
|
+
sx={{ justifyContent: hasCreate ? 'flex-start' : 'flex-end', display: hasCreate ? 'flex' : 'block' }}
|
|
128
131
|
{...sanitizeListRestProps(rest)}
|
|
129
132
|
/>
|
|
130
133
|
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Button, Chip, Stack, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
2
|
+
import { useListContext, useTranslate } from 'ra-core';
|
|
3
|
+
import { Close } from '@mui/icons-material';
|
|
4
|
+
import { ReactElement, useCallback } from 'react';
|
|
5
|
+
import { isEmpty } from 'lodash';
|
|
6
|
+
import { SaveFiltersButton } from './SaveFiltersButton';
|
|
7
|
+
import { useGetChipValue } from './FilterSidebarContext';
|
|
8
|
+
|
|
9
|
+
interface ChipItemProps {
|
|
10
|
+
onDelete: () => void;
|
|
11
|
+
value: unknown;
|
|
12
|
+
source: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ChipItem(props: ChipItemProps) {
|
|
16
|
+
const { onDelete, value, source } = props;
|
|
17
|
+
const label = useGetChipValue({ source, value });
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
label && (
|
|
21
|
+
<Chip
|
|
22
|
+
label={label}
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
variant="combined"
|
|
25
|
+
color="primary"
|
|
26
|
+
sx={{
|
|
27
|
+
height: '36px',
|
|
28
|
+
'& .MuiChip-deleteIcon': {
|
|
29
|
+
fontSize: '1rem !important',
|
|
30
|
+
mr: 1
|
|
31
|
+
}
|
|
32
|
+
}}
|
|
33
|
+
deleteIcon={<Close />}
|
|
34
|
+
onDelete={onDelete}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Chips(): ReactElement {
|
|
41
|
+
const { filterValues, setFilters } = useListContext();
|
|
42
|
+
const theme = useTheme();
|
|
43
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
44
|
+
const translate = useTranslate();
|
|
45
|
+
|
|
46
|
+
const resetAllFilters = useCallback(() => {
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
setFilters({});
|
|
49
|
+
}, [setFilters]);
|
|
50
|
+
|
|
51
|
+
const removeFilter = useCallback(
|
|
52
|
+
(key: string) => {
|
|
53
|
+
const newFilters = { ...filterValues };
|
|
54
|
+
delete newFilters[key];
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
setFilters(newFilters);
|
|
57
|
+
},
|
|
58
|
+
[filterValues, setFilters]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const Buttons = useCallback(() => {
|
|
62
|
+
return (
|
|
63
|
+
<Stack flexDirection={'row'} gap={2}>
|
|
64
|
+
<SaveFiltersButton />
|
|
65
|
+
<Button variant="outlined" onClick={resetAllFilters}>
|
|
66
|
+
{translate('ra.action.reset_filters')}
|
|
67
|
+
</Button>
|
|
68
|
+
</Stack>
|
|
69
|
+
);
|
|
70
|
+
}, [resetAllFilters, translate]);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Stack gap={2} flexDirection={isMobile ? 'column' : 'row'}>
|
|
74
|
+
<Stack
|
|
75
|
+
flexDirection={'row'}
|
|
76
|
+
flexWrap={isMobile ? 'nowrap' : 'wrap'}
|
|
77
|
+
gap={2}
|
|
78
|
+
pb={0.5}
|
|
79
|
+
overflow={'auto'}
|
|
80
|
+
width={isMobile ? window.innerWidth - (24 + 34) : 'auto'}
|
|
81
|
+
>
|
|
82
|
+
{Object.entries(filterValues).map(([key, value]) => {
|
|
83
|
+
return <ChipItem key={key} source={key} value={value} onDelete={() => removeFilter(key)} />;
|
|
84
|
+
})}
|
|
85
|
+
{!isMobile ? <Buttons /> : null}
|
|
86
|
+
</Stack>
|
|
87
|
+
{isMobile ? <Buttons /> : null}
|
|
88
|
+
</Stack>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ActiveFiltersChips(): ReactElement | null {
|
|
93
|
+
const { filterValues } = useListContext();
|
|
94
|
+
const theme = useTheme();
|
|
95
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
96
|
+
const translate = useTranslate();
|
|
97
|
+
|
|
98
|
+
return filterValues && !isEmpty(filterValues) ? (
|
|
99
|
+
<Stack flexDirection={isMobile ? 'column' : 'row'} alignItems={'baseline'} gap={2} m={isMobile ? 2 : 0}>
|
|
100
|
+
<Typography variant="h6" color="secondary" whiteSpace={'nowrap'}>
|
|
101
|
+
{translate('ra.title.active_filters')}
|
|
102
|
+
</Typography>
|
|
103
|
+
<Chips />
|
|
104
|
+
</Stack>
|
|
105
|
+
) : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { ActiveFiltersChips };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { LoadingButton } from '@/components/@extended';
|
|
2
|
+
import { Box, Button, Stack } from '@mui/material';
|
|
3
|
+
import { useListContext, useResourceContext, useTranslate } from 'ra-core';
|
|
4
|
+
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { FilterContext, getFilterFormValues } from 'react-admin';
|
|
6
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
7
|
+
import { ReloadOutlined } from '@ant-design/icons';
|
|
8
|
+
import { isEmpty, isEqual, isFunction } from 'lodash';
|
|
9
|
+
import { SavedFiltersList } from './SavedFiltersList';
|
|
10
|
+
import { useListViewContext } from '@/components/ra-lists';
|
|
11
|
+
import { Sidebar } from '@/components/Layout/Sidebar';
|
|
12
|
+
|
|
13
|
+
interface FiltersInputProps {
|
|
14
|
+
form: ReturnType<typeof useForm>;
|
|
15
|
+
filters?: ReactElement | ReactElement[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function FiltersInput(props: FiltersInputProps): ReactElement {
|
|
19
|
+
const { form, filters } = props;
|
|
20
|
+
const resource = useResourceContext();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<FilterContext.Provider value={Array.isArray(filters) ? filters : [filters]}>
|
|
24
|
+
<FormProvider {...form}>
|
|
25
|
+
<Box px={'20px'}>
|
|
26
|
+
{filters
|
|
27
|
+
? (Array.isArray(filters) ? filters : [filters]).map((filter: ReactElement, i) => {
|
|
28
|
+
const props = { ...filter.props, display: 'label' };
|
|
29
|
+
return React.isValidElement(filter) ? (
|
|
30
|
+
<Box key={i} sx={{ mb: 1 }}>
|
|
31
|
+
{React.cloneElement(filter, { ...props, resource })}
|
|
32
|
+
</Box>
|
|
33
|
+
) : null;
|
|
34
|
+
})
|
|
35
|
+
: null}
|
|
36
|
+
</Box>
|
|
37
|
+
</FormProvider>
|
|
38
|
+
</FilterContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface FilterSidebarProps {
|
|
43
|
+
filters?: ReactElement | ReactElement[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function FilterSidebar(props: FilterSidebarProps): ReactElement {
|
|
47
|
+
const { filters } = props;
|
|
48
|
+
const [loading, setLoading] = useState(false);
|
|
49
|
+
const { setSidebarOpen, sidebarOpen } = useListViewContext();
|
|
50
|
+
const { filterValues, setFilters } = useListContext();
|
|
51
|
+
const translate = useTranslate();
|
|
52
|
+
const form = useForm({
|
|
53
|
+
defaultValues: filterValues
|
|
54
|
+
});
|
|
55
|
+
const { handleSubmit, getValues, reset, watch } = form;
|
|
56
|
+
const watchValues = watch();
|
|
57
|
+
// values containes all the filters values and the default ones
|
|
58
|
+
const values = useMemo(() => {
|
|
59
|
+
const values: any = {};
|
|
60
|
+
if (filters) {
|
|
61
|
+
(Array.isArray(filters) ? filters : [filters]).forEach((filter) => {
|
|
62
|
+
values[filter.props.source] = filterValues?.[filter.props.source] ?? '';
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return values;
|
|
66
|
+
}, [filters, filterValues]);
|
|
67
|
+
|
|
68
|
+
// Reapply filterValues when the URL changes or a user removes a filter
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const newValues = getFilterFormValues(getValues(), filterValues);
|
|
71
|
+
const previousValues = getValues();
|
|
72
|
+
if (!isEqual(newValues, previousValues)) {
|
|
73
|
+
reset(newValues);
|
|
74
|
+
}
|
|
75
|
+
// The reference to the filterValues object is not updated when it changes,
|
|
76
|
+
// so we must stringify it to compare it by value and also compare the reference.
|
|
77
|
+
// This makes it work for both input values and filters applied directly through
|
|
78
|
+
// the ListContext.setFilter (e.g. QuickFilter in the simple example)
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, [JSON.stringify(filterValues), filterValues, getValues, reset]);
|
|
81
|
+
|
|
82
|
+
const closeSidebar = useCallback(() => {
|
|
83
|
+
if (isFunction(setSidebarOpen)) {
|
|
84
|
+
setSidebarOpen(false);
|
|
85
|
+
}
|
|
86
|
+
}, [setSidebarOpen]);
|
|
87
|
+
|
|
88
|
+
const onSubmit = React.useCallback(
|
|
89
|
+
(data: any) => {
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
setFilters(data);
|
|
92
|
+
closeSidebar();
|
|
93
|
+
},
|
|
94
|
+
[setFilters, closeSidebar]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const resetAllFields = useCallback(() => {
|
|
98
|
+
reset({});
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
setFilters({});
|
|
101
|
+
}, [reset, setFilters]);
|
|
102
|
+
|
|
103
|
+
const toggleLoading = useCallback(() => {
|
|
104
|
+
setLoading((prev) => !prev);
|
|
105
|
+
resetAllFields();
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}, 1000);
|
|
109
|
+
}, [resetAllFields]);
|
|
110
|
+
|
|
111
|
+
const isApplyDisabled = useMemo(() => {
|
|
112
|
+
const currentFormValues = getValues();
|
|
113
|
+
return isEmpty(values) || isEqual(currentFormValues, values);
|
|
114
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
+
}, [values, getValues, watchValues]);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Sidebar PaperProps={{ sx: { height: '100%' } }} open={sidebarOpen}>
|
|
119
|
+
<Sidebar.Header>
|
|
120
|
+
<Sidebar.HeaderText primary={translate('ra.action.add_filter')} />
|
|
121
|
+
<Sidebar.HeaderAction>
|
|
122
|
+
<LoadingButton
|
|
123
|
+
variant="text"
|
|
124
|
+
loading={loading}
|
|
125
|
+
loadingPosition="start"
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
startIcon={<ReloadOutlined />}
|
|
128
|
+
onClick={toggleLoading}
|
|
129
|
+
>
|
|
130
|
+
{translate('ra.action.reset_filters')}
|
|
131
|
+
</LoadingButton>
|
|
132
|
+
</Sidebar.HeaderAction>
|
|
133
|
+
</Sidebar.Header>
|
|
134
|
+
<Sidebar.Content>
|
|
135
|
+
<SavedFiltersList closeSidebar={closeSidebar} />
|
|
136
|
+
<FiltersInput form={form} filters={filters} />
|
|
137
|
+
</Sidebar.Content>
|
|
138
|
+
<Sidebar.Footer>
|
|
139
|
+
<Stack direction="row" spacing={2}>
|
|
140
|
+
<Button
|
|
141
|
+
fullWidth
|
|
142
|
+
onClick={() => {
|
|
143
|
+
reset({});
|
|
144
|
+
closeSidebar();
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{translate('ra.action.close')}
|
|
148
|
+
</Button>
|
|
149
|
+
<Button
|
|
150
|
+
fullWidth
|
|
151
|
+
disabled={isApplyDisabled}
|
|
152
|
+
variant="contained"
|
|
153
|
+
color="primary"
|
|
154
|
+
onClick={handleSubmit(onSubmit)}
|
|
155
|
+
>
|
|
156
|
+
{translate('ra.action.apply_filters')}
|
|
157
|
+
</Button>
|
|
158
|
+
</Stack>
|
|
159
|
+
</Sidebar.Footer>
|
|
160
|
+
</Sidebar>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { FilterSidebar };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useCallback, useContext } from 'react';
|
|
2
|
+
import { FilterList } from '@mui/icons-material';
|
|
3
|
+
import { FilterContext, useTranslate } from 'react-admin';
|
|
4
|
+
import { useIsEnabledSidebarFilter } from '@/components/ra-lists/FilterSidebar';
|
|
5
|
+
import { Button as MUIButton, Typography, useMediaQuery, useTheme } from '@mui/material';
|
|
6
|
+
import { Button } from '@/components/ra-buttons';
|
|
7
|
+
import { useListViewContext } from '@/components/ra-lists';
|
|
8
|
+
import { isFunction } from 'lodash';
|
|
9
|
+
|
|
10
|
+
function FilterSidebarButton() {
|
|
11
|
+
const { setSidebarOpen } = useListViewContext();
|
|
12
|
+
const filters = useContext(FilterContext);
|
|
13
|
+
const hasFilterSidebar = useIsEnabledSidebarFilter();
|
|
14
|
+
const translate = useTranslate();
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
17
|
+
|
|
18
|
+
if (!hasFilterSidebar) {
|
|
19
|
+
throw new Error('The <FilterSidebarButton> component requires the <List aside={<FilterSidebar />}> prop to be set');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (filters === undefined) {
|
|
23
|
+
throw new Error('The <FilterSidebarButton> component requires the <List filters> prop to be set');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const handleFilterClick = useCallback(() => {
|
|
27
|
+
if (isFunction(setSidebarOpen)) {
|
|
28
|
+
setSidebarOpen(true);
|
|
29
|
+
}
|
|
30
|
+
}, [setSidebarOpen]);
|
|
31
|
+
|
|
32
|
+
return isMobile ? (
|
|
33
|
+
<MUIButton className="add-filter" onClick={handleFilterClick}>
|
|
34
|
+
<FilterList sx={{ fontSize: '1rem', mr: 0.5 }} />
|
|
35
|
+
<Typography variant="h6">{translate('ra.action.add_filter')}</Typography>
|
|
36
|
+
</MUIButton>
|
|
37
|
+
) : (
|
|
38
|
+
<Button label="ra.action.add_filter" onClick={handleFilterClick}>
|
|
39
|
+
<FilterList />
|
|
40
|
+
</Button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { FilterSidebarButton };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { FilterContext, useResourceContext } from 'react-admin';
|
|
3
|
+
|
|
4
|
+
interface FiltersContextType {
|
|
5
|
+
hasFilterSidebar?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const FilterSidebarContext = createContext<FiltersContextType>({
|
|
9
|
+
hasFilterSidebar: false
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function useIsEnabledSidebarFilter(): boolean {
|
|
13
|
+
return useContext(FilterSidebarContext)?.hasFilterSidebar ?? false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ChipItemProps {
|
|
17
|
+
source: string;
|
|
18
|
+
value: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useGetChipValue({ source, value }: ChipItemProps): JSX.Element | null {
|
|
22
|
+
const filters = useContext(FilterContext);
|
|
23
|
+
const resource = useResourceContext();
|
|
24
|
+
|
|
25
|
+
const currentSourceFilter = useMemo(() => {
|
|
26
|
+
return filters.find((filter) => React.isValidElement(filter) && filter.props?.source === source);
|
|
27
|
+
}, [filters, source]);
|
|
28
|
+
const currentSourceFilterProps = useMemo(
|
|
29
|
+
() => (React.isValidElement(currentSourceFilter) ? currentSourceFilter.props : {}),
|
|
30
|
+
[currentSourceFilter]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const ChipComponent = React.isValidElement(currentSourceFilter) ? currentSourceFilter.props?.chip : null;
|
|
34
|
+
|
|
35
|
+
if (!ChipComponent) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`No chip component found for filter source "${source}". Ensure your filter components define a 'chip' prop.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const label = useMemo(() => {
|
|
42
|
+
return React.cloneElement(ChipComponent as React.ReactElement<any>, {
|
|
43
|
+
...currentSourceFilterProps,
|
|
44
|
+
record: { [source]: value },
|
|
45
|
+
value,
|
|
46
|
+
resource
|
|
47
|
+
});
|
|
48
|
+
}, [value, ChipComponent, source, resource, currentSourceFilterProps]);
|
|
49
|
+
|
|
50
|
+
return label;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { FilterSidebarContext, useGetChipValue, useIsEnabledSidebarFilter };
|