@applica-software-guru/react-admin 1.1.113 → 1.2.115
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/bitbucket-pipelines.yml +14 -15
- package/dist/components/ra-forms/LongForm/BaseForm.d.ts +8 -0
- package/dist/components/ra-forms/LongForm/BaseForm.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/Content.d.ts +8 -0
- package/dist/components/ra-forms/LongForm/Content.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/Form.d.ts +11 -0
- package/dist/components/ra-forms/LongForm/Form.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/NavMenu.d.ts +19 -0
- package/dist/components/ra-forms/LongForm/NavMenu.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/Provider.d.ts +15 -0
- package/dist/components/ra-forms/LongForm/Provider.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/Sidebar.d.ts +23 -0
- package/dist/components/ra-forms/LongForm/Sidebar.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/Tab.d.ts +11 -0
- package/dist/components/ra-forms/LongForm/Tab.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/hooks.d.ts +5 -0
- package/dist/components/ra-forms/LongForm/hooks.d.ts.map +1 -0
- package/dist/components/ra-forms/LongForm/index.d.ts +30 -2
- package/dist/components/ra-forms/LongForm/index.d.ts.map +1 -1
- package/dist/components/ra-forms/LongForm/types.d.ts +12 -9
- package/dist/components/ra-forms/LongForm/types.d.ts.map +1 -1
- package/dist/components/ra-forms/LongForm/utils.d.ts +8 -0
- package/dist/components/ra-forms/LongForm/utils.d.ts.map +1 -0
- package/dist/components/ra-forms/TableForm/TableFormIterator.d.ts +2 -0
- package/dist/components/ra-forms/TableForm/TableFormIterator.d.ts.map +1 -1
- package/dist/react-admin.cjs.js +65 -65
- package/dist/react-admin.cjs.js.map +1 -1
- package/dist/react-admin.es.js +9206 -9131
- package/dist/react-admin.es.js.map +1 -1
- package/dist/react-admin.umd.js +65 -65
- package/dist/react-admin.umd.js.map +1 -1
- package/dist/themes/overrides/ListItemText.d.ts +12 -0
- package/dist/themes/overrides/ListItemText.d.ts.map +1 -0
- package/dist/themes/overrides/index.d.ts.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ra-forms/LongForm/BaseForm.tsx +54 -0
- package/src/components/ra-forms/LongForm/Content.tsx +19 -0
- package/src/components/ra-forms/LongForm/Form.tsx +37 -0
- package/src/components/ra-forms/LongForm/NavMenu.tsx +128 -0
- package/src/components/ra-forms/LongForm/Provider.tsx +196 -0
- package/src/components/ra-forms/LongForm/Sidebar.tsx +71 -0
- package/src/components/ra-forms/LongForm/Tab.tsx +67 -0
- package/src/components/ra-forms/LongForm/hooks.tsx +25 -0
- package/src/components/ra-forms/LongForm/index.ts +52 -0
- package/src/components/ra-forms/LongForm/types.ts +13 -9
- package/src/components/ra-forms/LongForm/utils.ts +22 -0
- package/src/components/ra-forms/TableForm/TableFormIterator.tsx +32 -11
- package/src/themes/overrides/ListItemText.jsx +15 -0
- package/src/themes/overrides/index.jsx +2 -0
- package/src/types.ts +5 -0
- package/dist/components/ra-forms/LongForm/LongForm.d.ts +0 -98
- package/dist/components/ra-forms/LongForm/LongForm.d.ts.map +0 -1
- package/dist/components/ra-forms/LongForm/LongFormSidebar.d.ts +0 -34
- package/dist/components/ra-forms/LongForm/LongFormSidebar.d.ts.map +0 -1
- package/dist/components/ra-forms/LongForm/LongFormTab.d.ts +0 -45
- package/dist/components/ra-forms/LongForm/LongFormTab.d.ts.map +0 -1
- package/dist/components/ra-forms/LongForm/LongFormTabs.d.ts +0 -23
- package/dist/components/ra-forms/LongForm/LongFormTabs.d.ts.map +0 -1
- package/dist/components/ra-forms/LongForm/LongFormView.d.ts +0 -42
- package/dist/components/ra-forms/LongForm/LongFormView.d.ts.map +0 -1
- package/dist/components/ra-forms/LongForm/useFormRootPath.d.ts +0 -6
- package/dist/components/ra-forms/LongForm/useFormRootPath.d.ts.map +0 -1
- package/src/components/ra-forms/LongForm/LongForm.tsx +0 -59
- package/src/components/ra-forms/LongForm/LongFormSidebar.tsx +0 -44
- package/src/components/ra-forms/LongForm/LongFormTab.tsx +0 -122
- package/src/components/ra-forms/LongForm/LongFormTabs.tsx +0 -72
- package/src/components/ra-forms/LongForm/LongFormView.tsx +0 -161
- package/src/components/ra-forms/LongForm/index.tsx +0 -2
- package/src/components/ra-forms/LongForm/useFormRootPath.ts +0 -21
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListItemText.d.ts","sourceRoot":"","sources":["../../../../src/themes/overrides/ListItemText.jsx"],"names":[],"mappings":"AAEA;;;;;;;;;;EAYC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/themes/overrides/index.jsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/themes/overrides/index.jsx"],"names":[],"mappings":"AAmDA,6DAgDC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -51,4 +51,8 @@ export type MenuItemProps = {
|
|
|
51
51
|
* Indica come deve essere configurato il menu dell'applicazione.
|
|
52
52
|
*/
|
|
53
53
|
export type MenuProps = MenuItemProps[];
|
|
54
|
+
/**
|
|
55
|
+
* Consente di trasformare dei type rendendone opzionali solo alcune chiavi
|
|
56
|
+
*/
|
|
57
|
+
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
54
58
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IACpC;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,aAAa,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IACpC;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,aAAa,EAAE,CAAC;AAExC;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { Form, FormProps } from 'react-admin';
|
|
3
|
+
import { Grid } from '@mui/material';
|
|
4
|
+
import { styled } from '@mui/material/styles';
|
|
5
|
+
import { useThemeConfig } from '../../../hooks';
|
|
6
|
+
|
|
7
|
+
type IBaseFormProps = FormProps & {
|
|
8
|
+
spacing?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const StyledGrid = styled(Grid, {
|
|
12
|
+
name: 'ApplicaLongFormView',
|
|
13
|
+
slot: 'Root'
|
|
14
|
+
})(({ theme }) => ({
|
|
15
|
+
'& .MuiToolbar-root': {
|
|
16
|
+
position: 'initial',
|
|
17
|
+
marginTop: theme.spacing(2),
|
|
18
|
+
marginLeft: `-${theme.spacing(3)}`,
|
|
19
|
+
marginRight: `-${theme.spacing(3)}`,
|
|
20
|
+
marginBottom: `-${theme.spacing(2)}`,
|
|
21
|
+
borderTop: `1px solid ${theme.palette.divider}`,
|
|
22
|
+
[theme.breakpoints.down('sm')]: {
|
|
23
|
+
position: 'initial !important',
|
|
24
|
+
width: 'auto !important'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
'& form > .MuiToolbar-root': {
|
|
28
|
+
// È molto importante: questa regola serve per poter inserire form, all'interno di altri form, evitando
|
|
29
|
+
// che la toolbar del form interno recepisca gli stili della regola precedente (che genera un padding strano).
|
|
30
|
+
margin: 0,
|
|
31
|
+
marginRight: `-${theme.spacing(0.5)}`,
|
|
32
|
+
[theme.breakpoints.down('sm')]: {
|
|
33
|
+
marginRight: theme.spacing(0.5)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
function BaseForm(props: IBaseFormProps) {
|
|
39
|
+
const { spacing: themeSpacing } = useThemeConfig(),
|
|
40
|
+
{ spacing = themeSpacing } = props,
|
|
41
|
+
formProps = _.omit(props, ['spacing']);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Form {...formProps}>
|
|
45
|
+
<StyledGrid container spacing={spacing * 2}>
|
|
46
|
+
{props.children}
|
|
47
|
+
</StyledGrid>
|
|
48
|
+
</Form>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type { IBaseFormProps };
|
|
53
|
+
|
|
54
|
+
export { BaseForm };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { Grid, GridProps } from '@mui/material';
|
|
3
|
+
import { Disposition } from './types';
|
|
4
|
+
|
|
5
|
+
type IContentProps = GridProps & {
|
|
6
|
+
disposition?: Disposition;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function Content(props: IContentProps) {
|
|
10
|
+
const { disposition = { xl: 9, lg: 9, md: 8, sm: 8, xs: 12 } } = props,
|
|
11
|
+
gridProps = _.omit(props, ['disposition']);
|
|
12
|
+
return (
|
|
13
|
+
<Grid {...gridProps} item {...disposition}>
|
|
14
|
+
{props.children}
|
|
15
|
+
</Grid>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Content };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { BaseForm, IBaseFormProps } from './BaseForm';
|
|
3
|
+
import { IProviderProps, Provider } from './Provider';
|
|
4
|
+
import { Disposition } from './types';
|
|
5
|
+
import { Sidebar, useSidebarChildren } from './Sidebar';
|
|
6
|
+
import { Content } from './Content';
|
|
7
|
+
import { NavMenu } from './NavMenu';
|
|
8
|
+
import { useBaseItemChildren } from './Tab';
|
|
9
|
+
|
|
10
|
+
type IFormProps = IProviderProps &
|
|
11
|
+
IBaseFormProps & {
|
|
12
|
+
spacing?: number;
|
|
13
|
+
tabsDisposition: Disposition;
|
|
14
|
+
contentDisposition: Disposition;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function Form(props: IFormProps) {
|
|
18
|
+
const { tabsDisposition, contentDisposition, spacing = 2 } = props,
|
|
19
|
+
providerProps = _.pick(props, ['syncWithLocation']),
|
|
20
|
+
baseFormProps = _.omit(props, ['tabsDisposition', 'contentDisposition', 'syncWithLocation']),
|
|
21
|
+
{ top: sidebarTop, bottom: sidebarBottom } = useSidebarChildren(props),
|
|
22
|
+
contentChildren = useBaseItemChildren(props);
|
|
23
|
+
return (
|
|
24
|
+
<Provider {...providerProps}>
|
|
25
|
+
<BaseForm {...baseFormProps} spacing={spacing}>
|
|
26
|
+
<Sidebar disposition={tabsDisposition} spacing={spacing}>
|
|
27
|
+
{sidebarTop}
|
|
28
|
+
<NavMenu />
|
|
29
|
+
{sidebarBottom}
|
|
30
|
+
</Sidebar>
|
|
31
|
+
<Content disposition={contentDisposition}>{contentChildren}</Content>
|
|
32
|
+
</BaseForm>
|
|
33
|
+
</Provider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { Form };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { Avatar } from '../../@extended';
|
|
3
|
+
import { Card, Collapse, List, ListItem, ListItemProps, ListItemIcon, ListItemText, ListItemButton, ListItemAvatar } from '@mui/material';
|
|
4
|
+
import { useSetActiveItem, useSyncWithLocation } from './Provider';
|
|
5
|
+
import { IItem } from './types';
|
|
6
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
7
|
+
import { useNavigate } from 'react-router';
|
|
8
|
+
import { useChildren, useIsActive } from './hooks';
|
|
9
|
+
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
|
10
|
+
import { getLevel } from './utils';
|
|
11
|
+
import { useThemeConfig } from '../../../hooks';
|
|
12
|
+
|
|
13
|
+
type IBaseNavMenuItemProps = React.PropsWithChildren<{
|
|
14
|
+
id: string;
|
|
15
|
+
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
|
16
|
+
}>;
|
|
17
|
+
type INavMenuItemProps = IItem & ListItemProps & IBaseNavMenuItemProps;
|
|
18
|
+
|
|
19
|
+
function NavMenu() {
|
|
20
|
+
const items = useChildren();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card variant="outlined">
|
|
24
|
+
<List
|
|
25
|
+
sx={{
|
|
26
|
+
'& .MuiListItemButton-root.Mui-selected': {
|
|
27
|
+
borderColor: 'palette.primary.main',
|
|
28
|
+
borderRightStyle: 'solid',
|
|
29
|
+
borderRightWidth: 2
|
|
30
|
+
},
|
|
31
|
+
'& .MuiListItemAvatar-root': {
|
|
32
|
+
minWidth: 24
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{_.map(items, (item, index) => {
|
|
37
|
+
return <NavMenuItem key={index} {...item} />;
|
|
38
|
+
})}
|
|
39
|
+
</List>
|
|
40
|
+
</Card>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function NavMenuItem(props: INavMenuItemProps) {
|
|
45
|
+
const navMenuItemProps = useNavMenuItem(props),
|
|
46
|
+
{ selected, label, icon, onClick: _onClick, badge, items, isGroup, level } = navMenuItemProps,
|
|
47
|
+
{ spacing } = useThemeConfig(),
|
|
48
|
+
[open, setOpen] = useState(selected),
|
|
49
|
+
onClick = useCallback(
|
|
50
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
51
|
+
setOpen(!open);
|
|
52
|
+
_onClick(e);
|
|
53
|
+
},
|
|
54
|
+
[_onClick, open]
|
|
55
|
+
),
|
|
56
|
+
hasIcon = icon !== undefined,
|
|
57
|
+
listItemProps = _.omit(props, ['label', 'icon', 'selected', 'badge']);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<ListItem {...listItemProps} disablePadding>
|
|
62
|
+
<ListItemButton selected={selected && !isGroup} onClick={onClick} sx={{ pl: spacing * level }}>
|
|
63
|
+
{hasIcon && <ListItemIcon>{icon}</ListItemIcon>}
|
|
64
|
+
<ListItemText inset={!hasIcon} primary={label} />
|
|
65
|
+
{badge && (
|
|
66
|
+
<ListItemAvatar>
|
|
67
|
+
<Avatar type="filled" size="xs" color={badge.color ?? 'default'}>
|
|
68
|
+
{badge.content}
|
|
69
|
+
</Avatar>
|
|
70
|
+
</ListItemAvatar>
|
|
71
|
+
)}
|
|
72
|
+
{isGroup && (open ? <ExpandLess /> : <ExpandMore />)}
|
|
73
|
+
</ListItemButton>
|
|
74
|
+
</ListItem>
|
|
75
|
+
{isGroup && (
|
|
76
|
+
<Collapse in={open} timeout="auto">
|
|
77
|
+
<List>
|
|
78
|
+
{_.map(items, (item: IItem, index: number) => {
|
|
79
|
+
return <NavMenuItem key={index} {...item} />;
|
|
80
|
+
})}
|
|
81
|
+
</List>
|
|
82
|
+
</Collapse>
|
|
83
|
+
)}
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function useNavMenuItem(
|
|
89
|
+
item: INavMenuItemProps
|
|
90
|
+
): IItem & { selected: boolean; onClick: React.MouseEventHandler<HTMLDivElement>; items: Array<IItem>; isGroup: boolean; level: number } {
|
|
91
|
+
const { id } = item,
|
|
92
|
+
isActive = useIsActive(id),
|
|
93
|
+
level = getLevel(id),
|
|
94
|
+
navigate = useNavigate(),
|
|
95
|
+
syncWithLocation = useSyncWithLocation(),
|
|
96
|
+
setActiveItem = useSetActiveItem(),
|
|
97
|
+
items = useChildren(id),
|
|
98
|
+
isGroup = !_.isEmpty(items),
|
|
99
|
+
onClick = useCallback(
|
|
100
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
101
|
+
if (!isGroup) {
|
|
102
|
+
if (syncWithLocation) {
|
|
103
|
+
navigate(item.id);
|
|
104
|
+
} else {
|
|
105
|
+
setActiveItem(item.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (_.isFunction(item.onClick)) {
|
|
109
|
+
item.onClick(e);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[setActiveItem, navigate, item, syncWithLocation, isGroup]
|
|
113
|
+
),
|
|
114
|
+
navMenuItem = useMemo(() => {
|
|
115
|
+
return {
|
|
116
|
+
...item,
|
|
117
|
+
selected: isActive,
|
|
118
|
+
onClick: onClick,
|
|
119
|
+
items: items,
|
|
120
|
+
isGroup: isGroup,
|
|
121
|
+
level: level
|
|
122
|
+
};
|
|
123
|
+
}, [item, isActive, onClick, items, isGroup]);
|
|
124
|
+
|
|
125
|
+
return navMenuItem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { NavMenu, NavMenuItem, useNavMenuItem };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from 'react';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import { IItem } from './types';
|
|
4
|
+
import { useLocation } from 'react-router';
|
|
5
|
+
import { getItemsIds } from './utils';
|
|
6
|
+
|
|
7
|
+
enum ActionType {
|
|
8
|
+
SET_SYNC_WITH_LOCATION = 'setSyncWithLocation',
|
|
9
|
+
SET_ACTIVE_ITEM = 'setActiveItem',
|
|
10
|
+
ADD_ITEM = 'addItem',
|
|
11
|
+
REMOVE_ITEM = 'removeItem'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type IProviderProps = React.PropsWithChildren<{
|
|
15
|
+
syncWithLocation?: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
type IState = {
|
|
18
|
+
syncWithLocation: boolean;
|
|
19
|
+
items: Array<IItem>;
|
|
20
|
+
activeItem?: string;
|
|
21
|
+
};
|
|
22
|
+
type IAction =
|
|
23
|
+
| {
|
|
24
|
+
type: ActionType.SET_SYNC_WITH_LOCATION;
|
|
25
|
+
payload: boolean;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
type: ActionType.SET_ACTIVE_ITEM;
|
|
29
|
+
payload?: string;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
type: ActionType.ADD_ITEM;
|
|
33
|
+
payload: IItem;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
type: ActionType.REMOVE_ITEM;
|
|
37
|
+
payload: string | IItem;
|
|
38
|
+
};
|
|
39
|
+
type IDispatch = React.Dispatch<IAction>;
|
|
40
|
+
type IContext = {
|
|
41
|
+
state: IState;
|
|
42
|
+
dispatch: IDispatch;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function reducer(state: IState, action: IAction): IState {
|
|
46
|
+
const newState = _.clone(state),
|
|
47
|
+
{ type, payload } = action;
|
|
48
|
+
switch (type) {
|
|
49
|
+
case ActionType.SET_SYNC_WITH_LOCATION:
|
|
50
|
+
return _.extend(newState, { syncWithLocation: payload });
|
|
51
|
+
case ActionType.SET_ACTIVE_ITEM:
|
|
52
|
+
return _.includes(getItemsIds(state.items), payload) ? _.extend(newState, { activeItem: payload }) : newState;
|
|
53
|
+
case ActionType.ADD_ITEM: {
|
|
54
|
+
const id = payload.id,
|
|
55
|
+
items = _.reject(newState.items, (item: IItem) => item.id === id);
|
|
56
|
+
items.push(payload);
|
|
57
|
+
_.extend(newState, { items: items });
|
|
58
|
+
if (newState.activeItem === undefined) {
|
|
59
|
+
_.extend(newState, { activeItem: payload.id });
|
|
60
|
+
}
|
|
61
|
+
return newState;
|
|
62
|
+
}
|
|
63
|
+
case ActionType.REMOVE_ITEM: {
|
|
64
|
+
const id = _.isString(payload) ? payload : payload.id,
|
|
65
|
+
items = _.reject(newState.items, (item: IItem) => item.id === id);
|
|
66
|
+
_.extend(newState, { items: items });
|
|
67
|
+
if (newState.activeItem === id) {
|
|
68
|
+
_.extend(newState, { activeItem: _.first(items)?.id ?? undefined });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return newState;
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return newState;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const DefaultState: IState = {
|
|
79
|
+
syncWithLocation: true,
|
|
80
|
+
items: [],
|
|
81
|
+
activeItem: undefined
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const Context = createContext<IContext | undefined>(undefined);
|
|
85
|
+
|
|
86
|
+
function Provider(props: IProviderProps) {
|
|
87
|
+
const syncWithLocation = Boolean(props.syncWithLocation ?? true),
|
|
88
|
+
{ pathname } = useLocation(),
|
|
89
|
+
[state, dispatch] = useReducer(reducer, _.cloneDeep(DefaultState)),
|
|
90
|
+
{ items } = state,
|
|
91
|
+
value = useMemo(() => ({ state: state, dispatch: dispatch }), [state, dispatch]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
dispatch({
|
|
95
|
+
type: ActionType.SET_SYNC_WITH_LOCATION,
|
|
96
|
+
payload: Boolean(syncWithLocation)
|
|
97
|
+
});
|
|
98
|
+
}, [syncWithLocation, dispatch]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (syncWithLocation) {
|
|
102
|
+
const match = pathname.match(/([^/]+)$/);
|
|
103
|
+
if (match && match[0]) {
|
|
104
|
+
dispatch({
|
|
105
|
+
type: ActionType.SET_ACTIVE_ITEM,
|
|
106
|
+
payload: match[0]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, [dispatch, pathname, syncWithLocation, items]);
|
|
111
|
+
|
|
112
|
+
return <Context.Provider value={value}>{props.children}</Context.Provider>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function useFormContext(): IContext {
|
|
116
|
+
const context = useContext(Context);
|
|
117
|
+
|
|
118
|
+
if (context === undefined) {
|
|
119
|
+
throw new Error('[LongForm] useFormContext: context undefined. Please provide a valid Context using LongForm.Provider');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return context;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function useFormState(): IState {
|
|
126
|
+
const context = useFormContext();
|
|
127
|
+
return context.state;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function useFormDispatch(): IDispatch {
|
|
131
|
+
const context = useFormContext();
|
|
132
|
+
return context.dispatch;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function useActiveItem(): string | undefined {
|
|
136
|
+
const state = useFormState();
|
|
137
|
+
return state.activeItem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function useItems(): Array<IItem> {
|
|
141
|
+
const state = useFormState();
|
|
142
|
+
return state.items;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function useSyncWithLocation(): boolean {
|
|
146
|
+
const state = useFormState();
|
|
147
|
+
return state.syncWithLocation;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function useSetActiveItem(): (activeItem: string) => void {
|
|
151
|
+
const dispatch = useFormDispatch(),
|
|
152
|
+
setActiveItem = useCallback(
|
|
153
|
+
(activeItem: string) => {
|
|
154
|
+
dispatch({
|
|
155
|
+
type: ActionType.SET_ACTIVE_ITEM,
|
|
156
|
+
payload: activeItem
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
[dispatch]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return setActiveItem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function useAddItem(): (item: IItem) => void {
|
|
166
|
+
const dispatch = useFormDispatch(),
|
|
167
|
+
addItem = useCallback(
|
|
168
|
+
(item: IItem) => {
|
|
169
|
+
dispatch({
|
|
170
|
+
type: ActionType.ADD_ITEM,
|
|
171
|
+
payload: item
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
[dispatch]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return addItem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function useRemoveItem(): (item: string | IItem) => void {
|
|
181
|
+
const dispatch = useFormDispatch(),
|
|
182
|
+
removeItem = useCallback(
|
|
183
|
+
(item: string | IItem) => {
|
|
184
|
+
dispatch({
|
|
185
|
+
type: ActionType.REMOVE_ITEM,
|
|
186
|
+
payload: item
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
[dispatch]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return removeItem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { Provider, useItems, useSyncWithLocation, useActiveItem, useSetActiveItem, useAddItem, useRemoveItem };
|
|
196
|
+
export type { IProviderProps };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { Children, isValidElement, useMemo } from 'react';
|
|
3
|
+
import StickyBox from 'react-sticky-box';
|
|
4
|
+
import { Disposition } from './types';
|
|
5
|
+
import { Box, Grid, GridProps, Stack } from '@mui/material';
|
|
6
|
+
|
|
7
|
+
enum SidebarSectionPosition {
|
|
8
|
+
TOP = 'top',
|
|
9
|
+
BOTTOM = 'bottom'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type ISidebarProps = {
|
|
13
|
+
sticky?: boolean;
|
|
14
|
+
disposition?: Disposition;
|
|
15
|
+
} & GridProps;
|
|
16
|
+
|
|
17
|
+
type ISidebarSectionProps = React.PropsWithChildren<{
|
|
18
|
+
position: SidebarSectionPosition;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
function Sidebar(props: ISidebarProps) {
|
|
22
|
+
const { sticky = true, disposition = { xl: 3, lg: 3, md: 4, sm: 4, xs: 12 }, spacing = 2 } = props,
|
|
23
|
+
gridProps = _.omit(props, ['sticky', 'disposition', 'spacing']),
|
|
24
|
+
Wrapper = sticky ? StickyBox : Box,
|
|
25
|
+
wrapperProps = useMemo(() => {
|
|
26
|
+
return sticky ? { offsetTop: 74, offsetBottom: (spacing as number) * 2 } : {};
|
|
27
|
+
}, [sticky, spacing]);
|
|
28
|
+
return (
|
|
29
|
+
<Grid {...gridProps} item {...disposition}>
|
|
30
|
+
<Wrapper {...wrapperProps}>
|
|
31
|
+
<Stack spacing={spacing}>{props.children}</Stack>
|
|
32
|
+
</Wrapper>
|
|
33
|
+
</Grid>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function SidebarSection(props: ISidebarSectionProps) {
|
|
38
|
+
return <>{props.children}</>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ISidebarChildren = {
|
|
42
|
+
top: Array<React.ReactNode>;
|
|
43
|
+
bottom: Array<React.ReactNode>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function useSidebarChildren(props: React.PropsWithChildren): ISidebarChildren {
|
|
47
|
+
const { children } = props,
|
|
48
|
+
result = useMemo<ISidebarChildren>(() => {
|
|
49
|
+
const result: ISidebarChildren = {
|
|
50
|
+
top: [],
|
|
51
|
+
bottom: []
|
|
52
|
+
};
|
|
53
|
+
Children.forEach<React.ReactNode>(children, (Child) => {
|
|
54
|
+
if (isValidElement(Child) && Child?.type === SidebarSection) {
|
|
55
|
+
const { position } = Child.props;
|
|
56
|
+
switch (position) {
|
|
57
|
+
case SidebarSectionPosition.TOP:
|
|
58
|
+
result.top.push(Child);
|
|
59
|
+
break;
|
|
60
|
+
case SidebarSectionPosition.BOTTOM:
|
|
61
|
+
result.bottom.push(Child);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
}, [children]);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { Sidebar, SidebarSection, SidebarSectionPosition, useSidebarChildren };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { Box } from '@mui/material';
|
|
3
|
+
import React, { Children, cloneElement, isValidElement, useEffect, useMemo } from 'react';
|
|
4
|
+
import { useAddItem, useRemoveItem } from './Provider';
|
|
5
|
+
import { Optional } from 'src/types';
|
|
6
|
+
import { IItem } from './types';
|
|
7
|
+
import { useIsActive } from './hooks';
|
|
8
|
+
import { getId } from './utils';
|
|
9
|
+
|
|
10
|
+
type IBaseItemProps = React.PropsWithChildren<Optional<IItem, 'id'>>;
|
|
11
|
+
type ITabProps = IBaseItemProps;
|
|
12
|
+
type IGroupProps = IBaseItemProps;
|
|
13
|
+
|
|
14
|
+
function BaseItem(props: IBaseItemProps) {
|
|
15
|
+
const { label, icon, badge } = props,
|
|
16
|
+
id = getId(props),
|
|
17
|
+
addItem = useAddItem(),
|
|
18
|
+
removeItem = useRemoveItem(),
|
|
19
|
+
visible = useIsActive(id);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
addItem({
|
|
23
|
+
id: id,
|
|
24
|
+
label: label,
|
|
25
|
+
icon: icon,
|
|
26
|
+
badge: badge
|
|
27
|
+
});
|
|
28
|
+
return () => {
|
|
29
|
+
removeItem(id);
|
|
30
|
+
};
|
|
31
|
+
}, [addItem, removeItem, label, icon, id, badge]);
|
|
32
|
+
|
|
33
|
+
/* All tabs are rendered (not only the one in focus), to allow validation
|
|
34
|
+
on tabs not in focus. The tabs receive a `hidden` property, which they'll
|
|
35
|
+
use to hide the tab using CSS if it's not the one in focus.
|
|
36
|
+
See https://github.com/marmelab/react-admin/issues/1866 */
|
|
37
|
+
return <Box display={visible ? 'unset' : 'none'}>{props.children}</Box>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Group(props: IGroupProps) {
|
|
41
|
+
const filteredChildren = useBaseItemChildren(props),
|
|
42
|
+
groupId = getId(props);
|
|
43
|
+
return (
|
|
44
|
+
<BaseItem {...props} id={groupId}>
|
|
45
|
+
{Children.map(filteredChildren, (Child) => {
|
|
46
|
+
const childId = getId(Child.props);
|
|
47
|
+
return cloneElement(Child, { id: `${groupId}.${childId}` });
|
|
48
|
+
})}
|
|
49
|
+
</BaseItem>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Tab(props: ITabProps) {
|
|
54
|
+
return <BaseItem {...props} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function useBaseItemChildren(props: React.PropsWithChildren): Array<React.FunctionComponentElement<IBaseItemProps>> {
|
|
58
|
+
const { children } = props,
|
|
59
|
+
result = useMemo<Array<React.FunctionComponentElement<IBaseItemProps>>>(
|
|
60
|
+
//@ts-ignore
|
|
61
|
+
() => _.filter(Children.toArray(children), (Child) => isValidElement(Child) && (Child?.type === Tab || Child?.type === Group)),
|
|
62
|
+
[children]
|
|
63
|
+
);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Tab, Group, useBaseItemChildren };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useActiveItem, useItems } from './Provider';
|
|
4
|
+
import { IItem } from './types';
|
|
5
|
+
import { getLevel, isChild } from './utils';
|
|
6
|
+
|
|
7
|
+
function useIsActive(id: string): boolean {
|
|
8
|
+
const activeItem = useActiveItem();
|
|
9
|
+
return isChild(id, activeItem ?? '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function useChildren(id?: string): Array<IItem> {
|
|
13
|
+
const items = useItems(),
|
|
14
|
+
level = id !== undefined ? getLevel(id) : 0,
|
|
15
|
+
children = useMemo(() => {
|
|
16
|
+
return _.chain(items)
|
|
17
|
+
.filter((item) => (id !== undefined ? isChild(id, item.id) : true))
|
|
18
|
+
.filter((item) => getLevel(item.id) === level + 1)
|
|
19
|
+
.value();
|
|
20
|
+
}, [items, level, id]);
|
|
21
|
+
|
|
22
|
+
return children;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { useChildren, useIsActive };
|