@smartbooks-ai/layout 0.0.3
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/package/tsconfig.layout.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/src/components/PageHeader/PageHeader.tsx +15 -0
- package/src/components/PageHeader/index.ts +1 -0
- package/src/components/PageHeader/styles.ts +34 -0
- package/src/components/PageWithMenuLayout/AppSelect/index.tsx +66 -0
- package/src/components/PageWithMenuLayout/AppSelect/styles.ts +33 -0
- package/src/components/PageWithMenuLayout/LogoHeaderImage.tsx +44 -0
- package/src/components/PageWithMenuLayout/LogoHeaderText.tsx +48 -0
- package/src/components/PageWithMenuLayout/MenuItemWithChildren/MenuItemWithChildren.tsx +149 -0
- package/src/components/PageWithMenuLayout/MenuItemWithChildren/styles.ts +179 -0
- package/src/components/PageWithMenuLayout/MenuSelect/index.tsx +78 -0
- package/src/components/PageWithMenuLayout/MenuSelect/styles.ts +97 -0
- package/src/components/PageWithMenuLayout/MultiSubscriptionsMenuItems/ConsolidationIcon.tsx +6 -0
- package/src/components/PageWithMenuLayout/MultiSubscriptionsMenuItems/MultiSubscriptionsMenuItems.tsx +120 -0
- package/src/components/PageWithMenuLayout/MultiSubscriptionsMenuItems/consolidation.svg +8 -0
- package/src/components/PageWithMenuLayout/MultiSubscriptionsMenuItems/index.ts +1 -0
- package/src/components/PageWithMenuLayout/MultiSubscriptionsMenuItems/styles.ts +10 -0
- package/src/components/PageWithMenuLayout/PageWithMenuLayout.tsx +106 -0
- package/src/components/PageWithMenuLayout/UserProfileSelect/index.tsx +64 -0
- package/src/components/PageWithMenuLayout/UserProfileSelect/styles.ts +8 -0
- package/src/components/PageWithMenuLayout/index.ts +8 -0
- package/src/components/PageWithMenuLayout/styles.ts +110 -0
- package/src/components/PageWithMenuLayout/types.ts +7 -0
- package/src/components/index.ts +3 -0
- package/src/emotion.d.ts +76 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useIsAuthorized.ts +35 -0
- package/src/hooks/useToggle.ts +27 -0
- package/src/index.ts +7 -0
- package/src/package-isolation.test.ts +60 -0
- package/src/security/AuthorizedContent/index.tsx +77 -0
- package/src/security/AuthorizedContent/state.ts +8 -0
- package/src/security/AuthorizedContent/useAuthorizationState.ts +42 -0
- package/src/security/ProfileContext/ProfileContext.tsx +37 -0
- package/src/security/ProfileContext/index.ts +4 -0
- package/src/security/ProfileContext/types.ts +7 -0
- package/src/security/ProfileContext/useProfile.tsx +7 -0
- package/src/security/UserProfile.ts +48 -0
- package/src/security/index.ts +2 -0
- package/src/theme/colorPrimitives.ts +107 -0
- package/src/theme/colors.ts +78 -0
- package/src/theme/font.ts +27 -0
- package/src/theme/globalStyles.tsx +55 -0
- package/src/theme/index.tsx +228 -0
- package/src/theme/radius.ts +12 -0
- package/src/theme/spacing.ts +12 -0
- package/src/theme/typography.ts +40 -0
- package/src/utils/assertNever.ts +14 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/shouldNotForwardPropsWithKeys.ts +7 -0
- package/tsconfig.json +34 -0
- package/tsconfig.layout.tsbuildinfo +1 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Fade } from '@mui/material';
|
|
4
|
+
import useResizeObserver from 'use-resize-observer';
|
|
5
|
+
|
|
6
|
+
import * as Styled from './styles';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
isExpanded: boolean;
|
|
10
|
+
renderMenuItems: (params: { closeMenu: () => void }) => React.ReactElement[];
|
|
11
|
+
startIcon?: React.ReactNode;
|
|
12
|
+
selectedOptionText?: string;
|
|
13
|
+
menuPosition?: 'top' | 'bottom';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const MenuSelect: React.FC<Props> = ({
|
|
17
|
+
isExpanded,
|
|
18
|
+
renderMenuItems,
|
|
19
|
+
startIcon,
|
|
20
|
+
selectedOptionText,
|
|
21
|
+
menuPosition = 'bottom',
|
|
22
|
+
}) => {
|
|
23
|
+
const openMenu: React.MouseEventHandler<HTMLElement> = (event) => {
|
|
24
|
+
setAnchorEl(event.currentTarget);
|
|
25
|
+
};
|
|
26
|
+
const closeMenu = () => {
|
|
27
|
+
setAnchorEl(undefined);
|
|
28
|
+
};
|
|
29
|
+
const [anchorEl, setAnchorEl] = useState<HTMLElement>();
|
|
30
|
+
const isMenuOpen = Boolean(anchorEl);
|
|
31
|
+
|
|
32
|
+
const menuItems = renderMenuItems({ closeMenu });
|
|
33
|
+
|
|
34
|
+
const { height: bodyHeight } = useResizeObserver({ ref: document.body });
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<Styled.SelectorButton
|
|
39
|
+
isMenuExpanded={isExpanded}
|
|
40
|
+
variant="outlined"
|
|
41
|
+
aria-haspopup="true"
|
|
42
|
+
disableElevation={true}
|
|
43
|
+
onClick={openMenu}
|
|
44
|
+
endIcon={<Styled.UnfoldMoreIcon />}
|
|
45
|
+
fullWidth={true}
|
|
46
|
+
startIcon={startIcon}
|
|
47
|
+
disabled={!menuItems.length}
|
|
48
|
+
>
|
|
49
|
+
<Fade in={isExpanded}>
|
|
50
|
+
<Styled.SelectedOptionText>{selectedOptionText}</Styled.SelectedOptionText>
|
|
51
|
+
</Fade>
|
|
52
|
+
</Styled.SelectorButton>
|
|
53
|
+
|
|
54
|
+
<Styled.Menu
|
|
55
|
+
anchorEl={anchorEl}
|
|
56
|
+
open={isMenuOpen}
|
|
57
|
+
onClose={closeMenu}
|
|
58
|
+
sx={{ top: menuPosition === 'top' ? -4 : 4 }}
|
|
59
|
+
slotProps={{
|
|
60
|
+
paper: {
|
|
61
|
+
style: {
|
|
62
|
+
maxHeight:
|
|
63
|
+
anchorEl && bodyHeight ? bodyHeight - anchorEl?.getBoundingClientRect().bottom - 16 : undefined,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}}
|
|
67
|
+
anchorOrigin={
|
|
68
|
+
menuPosition === 'top' ? { horizontal: 'left', vertical: 'top' } : { horizontal: 'left', vertical: 'bottom' }
|
|
69
|
+
}
|
|
70
|
+
transformOrigin={
|
|
71
|
+
menuPosition === 'top' ? { horizontal: 'left', vertical: 'bottom' } : { horizontal: 'left', vertical: 'top' }
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{menuItems}
|
|
75
|
+
</Styled.Menu>
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import MuiUnfoldMoreIcon from '@mui/icons-material/UnfoldMoreRounded';
|
|
4
|
+
import Button, { buttonClasses } from '@mui/material/Button';
|
|
5
|
+
import MuiMenu, { menuClasses } from '@mui/material/Menu';
|
|
6
|
+
|
|
7
|
+
import { shouldNotForwardPropsWithKeys } from '../../../utils/shouldNotForwardPropsWithKeys';
|
|
8
|
+
|
|
9
|
+
type SelectorButtonProps = {
|
|
10
|
+
isMenuExpanded: boolean;
|
|
11
|
+
};
|
|
12
|
+
export const SelectorButton = styled(
|
|
13
|
+
Button,
|
|
14
|
+
shouldNotForwardPropsWithKeys<SelectorButtonProps>(['isMenuExpanded']),
|
|
15
|
+
)<SelectorButtonProps>(
|
|
16
|
+
({ theme, isMenuExpanded, disabled }) => css`
|
|
17
|
+
display: ${disabled && 'none'};
|
|
18
|
+
|
|
19
|
+
justify-content: flex-start;
|
|
20
|
+
|
|
21
|
+
min-width: 0;
|
|
22
|
+
height: 2.375rem;
|
|
23
|
+
|
|
24
|
+
padding-right: calc(${theme.my.spacing.xxxs} - 0.0625rem);
|
|
25
|
+
padding-left: calc(${theme.my.spacing.xxxs} - 0.0625rem);
|
|
26
|
+
|
|
27
|
+
text-overflow: ellipsis;
|
|
28
|
+
|
|
29
|
+
color: ${theme.my.colors.primitives.common.white};
|
|
30
|
+
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
|
|
33
|
+
border-radius: ${theme.my.radius.md};
|
|
34
|
+
|
|
35
|
+
transition:
|
|
36
|
+
height 0.3s,
|
|
37
|
+
padding 0.3s;
|
|
38
|
+
|
|
39
|
+
:hover {
|
|
40
|
+
&.${buttonClasses.outlined} {
|
|
41
|
+
border-color: ${theme.my.colors.primitives.common.white};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
${isMenuExpanded &&
|
|
46
|
+
css`
|
|
47
|
+
height: 3rem;
|
|
48
|
+
|
|
49
|
+
padding-right: ${theme.my.spacing.xxs};
|
|
50
|
+
padding-left: ${theme.my.spacing.xxs};
|
|
51
|
+
`}
|
|
52
|
+
|
|
53
|
+
.${buttonClasses.startIcon} {
|
|
54
|
+
width: 1.75rem;
|
|
55
|
+
height: 1.75rem;
|
|
56
|
+
|
|
57
|
+
margin-right: 0;
|
|
58
|
+
margin-left: 0;
|
|
59
|
+
|
|
60
|
+
transition: 0.3s;
|
|
61
|
+
|
|
62
|
+
${isMenuExpanded &&
|
|
63
|
+
css`
|
|
64
|
+
width: 2rem;
|
|
65
|
+
height: 2rem;
|
|
66
|
+
`}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.${buttonClasses.endIcon} {
|
|
70
|
+
margin-right: ${theme.my.spacing.xxxs};
|
|
71
|
+
}
|
|
72
|
+
`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export const SelectedOptionText = styled.span`
|
|
76
|
+
flex: 1;
|
|
77
|
+
|
|
78
|
+
padding-left: ${({ theme }) => theme.my.spacing.xxs};
|
|
79
|
+
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
text-overflow: ellipsis;
|
|
82
|
+
|
|
83
|
+
font: ${({ theme }) => theme.my.font.highlight3};
|
|
84
|
+
|
|
85
|
+
text-align: left;
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
export const Menu = styled(MuiMenu)`
|
|
89
|
+
.${menuClasses.root} {
|
|
90
|
+
padding: 0;
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
export const UnfoldMoreIcon = styled(MuiUnfoldMoreIcon)`
|
|
95
|
+
width: 1rem;
|
|
96
|
+
height: 1rem;
|
|
97
|
+
`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const ConsolidationIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 36 36" {...props}>
|
|
3
|
+
<path d="M9.8 18.8h16.4v3.08h1.6V17.2h-9V14h-1.6v3.2h-9v4.68h1.6zM14 23H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2M4 31v-6h10v6ZM32 23H22a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2m-10 8v-6h10v6ZM13 13h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H13a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2m0-8h10v6H13Z" />
|
|
4
|
+
</svg>
|
|
5
|
+
);
|
|
6
|
+
export default ConsolidationIcon;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Fragment, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Business as BusinessIcon,
|
|
5
|
+
ExpandLess as ExpandLessIcon,
|
|
6
|
+
ExpandMore as ExpandMoreIcon,
|
|
7
|
+
} from '@mui/icons-material';
|
|
8
|
+
import { Collapse, Divider, List, ListItemIcon, MenuItem, Tooltip } from '@mui/material';
|
|
9
|
+
import { AvailableCompany, AvailableTenant } from '@smartbooks-ai/api-client';
|
|
10
|
+
|
|
11
|
+
import ConsolidationIcon from './ConsolidationIcon';
|
|
12
|
+
import * as Styled from './styles';
|
|
13
|
+
|
|
14
|
+
import { assertNever } from '../../../utils/assertNever';
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
selectedTenant?: AvailableTenant;
|
|
18
|
+
tenants: AvailableTenant[];
|
|
19
|
+
onCompanySelected: (company: AvailableCompany) => void;
|
|
20
|
+
selectedCompanyCode: string | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const MultiSubscriptionsMenuItems: React.FC<Props> = ({
|
|
24
|
+
selectedTenant,
|
|
25
|
+
tenants,
|
|
26
|
+
onCompanySelected,
|
|
27
|
+
selectedCompanyCode,
|
|
28
|
+
}) => {
|
|
29
|
+
const [openTenants, setOpenTenants] = useState<string[]>([]);
|
|
30
|
+
|
|
31
|
+
const toggleTenant = (tenantCode: string) => {
|
|
32
|
+
if (openTenants.includes(tenantCode)) {
|
|
33
|
+
setOpenTenants(openTenants.filter((a) => a !== tenantCode));
|
|
34
|
+
} else {
|
|
35
|
+
setOpenTenants([...openTenants, tenantCode]);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (selectedTenant) {
|
|
41
|
+
setOpenTenants([selectedTenant.code]);
|
|
42
|
+
}
|
|
43
|
+
}, [selectedTenant]);
|
|
44
|
+
|
|
45
|
+
const sortedTenants = tenants.toSorted((a, b) => (a.description || '').localeCompare(b.description || ''));
|
|
46
|
+
|
|
47
|
+
const [highlightedCompanyMenuItem, setHighlightedCompanyMenuItem] = useState<HTMLElement | null>(null);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
{sortedTenants.map((tenant) => {
|
|
52
|
+
const sortedCompanies = tenant.companies.toSorted((a, b) =>
|
|
53
|
+
a.companyType === 'consolidation' && b.companyType !== 'consolidation'
|
|
54
|
+
? -1
|
|
55
|
+
: a.companyType !== 'consolidation' && b.companyType === 'consolidation'
|
|
56
|
+
? 1
|
|
57
|
+
: (a.description || '').localeCompare(b.description || ''),
|
|
58
|
+
);
|
|
59
|
+
const isOpen = tenants.length === 1 || openTenants.includes(tenant.code);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Fragment key={tenant.code}>
|
|
63
|
+
{tenants.length > 1 && (
|
|
64
|
+
<MenuItem onClick={() => toggleTenant(tenant.code)}>
|
|
65
|
+
{tenant.description}
|
|
66
|
+
|
|
67
|
+
<ListItemIcon sx={{ marginLeft: '1em' }}>
|
|
68
|
+
{isOpen ? <ExpandLessIcon fontSize="small" /> : <ExpandMoreIcon fontSize="small" />}
|
|
69
|
+
</ListItemIcon>
|
|
70
|
+
</MenuItem>
|
|
71
|
+
)}
|
|
72
|
+
<Collapse
|
|
73
|
+
in={isOpen}
|
|
74
|
+
timeout="auto"
|
|
75
|
+
unmountOnExit={true}
|
|
76
|
+
onEntered={() => {
|
|
77
|
+
if (tenant.code === selectedTenant?.code)
|
|
78
|
+
highlightedCompanyMenuItem?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<List component="div" disablePadding={true}>
|
|
82
|
+
<Divider />
|
|
83
|
+
|
|
84
|
+
{sortedCompanies.map((company) => (
|
|
85
|
+
<MenuItem
|
|
86
|
+
key={company.code}
|
|
87
|
+
onClick={() => onCompanySelected(company)}
|
|
88
|
+
selected={company.code === selectedCompanyCode}
|
|
89
|
+
autoFocus={company.code === selectedCompanyCode}
|
|
90
|
+
ref={(el) => {
|
|
91
|
+
if (company.code === selectedCompanyCode) setHighlightedCompanyMenuItem(el);
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<ListItemIcon>
|
|
95
|
+
{company.companyType === 'consolidation' ? (
|
|
96
|
+
<Tooltip title="Consolidation" disableInteractive={true}>
|
|
97
|
+
<Styled.IconContainer>
|
|
98
|
+
<ConsolidationIcon fontSize="inherit" />
|
|
99
|
+
</Styled.IconContainer>
|
|
100
|
+
</Tooltip>
|
|
101
|
+
) : company.companyType === 'regular' ? (
|
|
102
|
+
<Styled.IconContainer>
|
|
103
|
+
<BusinessIcon fontSize="inherit" />
|
|
104
|
+
</Styled.IconContainer>
|
|
105
|
+
) : (
|
|
106
|
+
assertNever(company.companyType)
|
|
107
|
+
)}
|
|
108
|
+
</ListItemIcon>
|
|
109
|
+
|
|
110
|
+
{company.description}
|
|
111
|
+
</MenuItem>
|
|
112
|
+
))}
|
|
113
|
+
</List>
|
|
114
|
+
</Collapse>
|
|
115
|
+
</Fragment>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg fill="currentColor" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<polygon
|
|
3
|
+
points="9.8 18.8 26.2 18.8 26.2 21.88 27.8 21.88 27.8 17.2 18.8 17.2 18.8 14 17.2 14 17.2 17.2 8.2 17.2 8.2 21.88 9.8 21.88 9.8 18.8">
|
|
4
|
+
</polygon>
|
|
5
|
+
<path d="M14,23H4a2,2,0,0,0-2,2v6a2,2,0,0,0,2,2H14a2,2,0,0,0,2-2V25A2,2,0,0,0,14,23ZM4,31V25H14v6Z"></path>
|
|
6
|
+
<path d="M32,23H22a2,2,0,0,0-2,2v6a2,2,0,0,0,2,2H32a2,2,0,0,0,2-2V25A2,2,0,0,0,32,23ZM22,31V25H32v6Z"></path>
|
|
7
|
+
<path d="M13,13H23a2,2,0,0,0,2-2V5a2,2,0,0,0-2-2H13a2,2,0,0,0-2,2v6A2,2,0,0,0,13,13Zm0-8H23v6H13Z"></path>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MultiSubscriptionsMenuItems } from './MultiSubscriptionsMenuItems';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
|
2
|
+
import Fade from '@mui/material/Fade';
|
|
3
|
+
import MenuList from '@mui/material/MenuList';
|
|
4
|
+
import { AvailableCompany } from '@smartbooks-ai/api-client';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import { NavLink, Outlet } from 'react-router';
|
|
7
|
+
|
|
8
|
+
import LogoHeaderImage from './LogoHeaderImage';
|
|
9
|
+
import LogoHeaderText from './LogoHeaderText';
|
|
10
|
+
import { AppSelect, MenuItemWithChildren, UserProfileSelect } from './index';
|
|
11
|
+
import * as Styled from './styles';
|
|
12
|
+
import { MenuStructure } from './types';
|
|
13
|
+
|
|
14
|
+
import { UserProfile } from '../../security/UserProfile';
|
|
15
|
+
|
|
16
|
+
export type PageWithMenuLayoutProps = {
|
|
17
|
+
isExpanded: boolean;
|
|
18
|
+
toggleIsExpanded: () => void;
|
|
19
|
+
resolvedCompanyCode?: string;
|
|
20
|
+
topMenu?: MenuStructure;
|
|
21
|
+
bottomMenu: MenuStructure;
|
|
22
|
+
profile: UserProfile | null;
|
|
23
|
+
tenantCode?: string;
|
|
24
|
+
switchToCompany: (company: AvailableCompany) => Promise<void>;
|
|
25
|
+
openMenu: () => void;
|
|
26
|
+
logout: () => void;
|
|
27
|
+
additionalOutletContent?: React.ReactNode;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const PageWithMenuLayout: React.FC<PageWithMenuLayoutProps> = ({
|
|
31
|
+
isExpanded,
|
|
32
|
+
toggleIsExpanded,
|
|
33
|
+
resolvedCompanyCode,
|
|
34
|
+
topMenu,
|
|
35
|
+
bottomMenu,
|
|
36
|
+
profile,
|
|
37
|
+
tenantCode,
|
|
38
|
+
switchToCompany,
|
|
39
|
+
openMenu,
|
|
40
|
+
logout,
|
|
41
|
+
additionalOutletContent,
|
|
42
|
+
}) => {
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<Styled.LockMenuButton onClick={toggleIsExpanded} isExpanded={isExpanded}>
|
|
46
|
+
<KeyboardArrowLeftIcon fontSize="inherit" />
|
|
47
|
+
</Styled.LockMenuButton>
|
|
48
|
+
|
|
49
|
+
{createPortal(
|
|
50
|
+
<Styled.Drawer open={isExpanded} variant="permanent">
|
|
51
|
+
<Styled.Header>
|
|
52
|
+
<Styled.LogoContainer>
|
|
53
|
+
<NavLink to="/">
|
|
54
|
+
<LogoHeaderImage height={24} />
|
|
55
|
+
</NavLink>
|
|
56
|
+
</Styled.LogoContainer>
|
|
57
|
+
|
|
58
|
+
<Fade in={isExpanded}>
|
|
59
|
+
<NavLink to="/">
|
|
60
|
+
<LogoHeaderText height={12} />
|
|
61
|
+
</NavLink>
|
|
62
|
+
</Fade>
|
|
63
|
+
</Styled.Header>
|
|
64
|
+
<AppSelect
|
|
65
|
+
tenantCode={tenantCode}
|
|
66
|
+
companyCode={resolvedCompanyCode}
|
|
67
|
+
isExpanded={isExpanded}
|
|
68
|
+
profile={profile}
|
|
69
|
+
onCompanyClicked={switchToCompany}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<Styled.TopMenuList>
|
|
73
|
+
{topMenu?.map((firstLevelItem) => (
|
|
74
|
+
<MenuItemWithChildren
|
|
75
|
+
key={firstLevelItem.title}
|
|
76
|
+
menuItem={firstLevelItem}
|
|
77
|
+
isMenuOpen={isExpanded}
|
|
78
|
+
openMenu={openMenu}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
</Styled.TopMenuList>
|
|
82
|
+
|
|
83
|
+
<MenuList disablePadding={true}>
|
|
84
|
+
{bottomMenu?.map((firstLevelItem) => (
|
|
85
|
+
<MenuItemWithChildren
|
|
86
|
+
key={firstLevelItem.title}
|
|
87
|
+
menuItem={firstLevelItem}
|
|
88
|
+
isMenuOpen={isExpanded}
|
|
89
|
+
openMenu={openMenu}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</MenuList>
|
|
93
|
+
|
|
94
|
+
<UserProfileSelect isTextVisible={isExpanded} profile={profile} logout={logout} />
|
|
95
|
+
</Styled.Drawer>,
|
|
96
|
+
document.body,
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
<Styled.Main isLocked={isExpanded}>
|
|
100
|
+
<Outlet />
|
|
101
|
+
|
|
102
|
+
{additionalOutletContent}
|
|
103
|
+
</Styled.Main>
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded';
|
|
4
|
+
import { Fade } from '@mui/material';
|
|
5
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
6
|
+
import { GlobalRole } from '@smartbooks-ai/api-client';
|
|
7
|
+
|
|
8
|
+
import * as Styled from './styles';
|
|
9
|
+
|
|
10
|
+
import { useIsAuthorized } from '../../../hooks/useIsAuthorized';
|
|
11
|
+
import { useToggle } from '../../../hooks/useToggle';
|
|
12
|
+
import { UserProfile } from '../../../security/ProfileContext';
|
|
13
|
+
import * as MenuItemWithChildrenStyled from '../MenuItemWithChildren/styles';
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
isTextVisible: boolean;
|
|
17
|
+
profile: UserProfile | null;
|
|
18
|
+
logout: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const UserProfileSelect: React.FC<Props> = ({ isTextVisible, profile, logout }) => {
|
|
22
|
+
const { value: isMenuOpen, switchOn: openMenu, switchOff: closeMenu } = useToggle();
|
|
23
|
+
const getIsAuthorized = useIsAuthorized();
|
|
24
|
+
|
|
25
|
+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<MenuItemWithChildrenStyled.MenuItem
|
|
30
|
+
isTitle={false}
|
|
31
|
+
isMenuOpen={isTextVisible}
|
|
32
|
+
ref={setAnchorEl}
|
|
33
|
+
isHighlighted={false}
|
|
34
|
+
onClick={openMenu}
|
|
35
|
+
>
|
|
36
|
+
<MenuItemWithChildrenStyled.ListItemIcon>
|
|
37
|
+
<AccountCircleRoundedIcon fontSize="small" />
|
|
38
|
+
</MenuItemWithChildrenStyled.ListItemIcon>
|
|
39
|
+
|
|
40
|
+
<Fade in={isTextVisible}>
|
|
41
|
+
<MenuItemWithChildrenStyled.ListItemText elevation={0} isHighlighted={false} isTitle={false}>
|
|
42
|
+
{profile?.displayName ?? 'My account'}
|
|
43
|
+
</MenuItemWithChildrenStyled.ListItemText>
|
|
44
|
+
</Fade>
|
|
45
|
+
|
|
46
|
+
<MenuItemWithChildrenStyled.ArrowDownIcon isExpanded={isMenuOpen} isVisible={isTextVisible} />
|
|
47
|
+
</MenuItemWithChildrenStyled.MenuItem>
|
|
48
|
+
|
|
49
|
+
<Styled.Menu
|
|
50
|
+
anchorOrigin={{ horizontal: 'left', vertical: 'top' }}
|
|
51
|
+
transformOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
|
52
|
+
anchorEl={anchorEl}
|
|
53
|
+
open={isMenuOpen}
|
|
54
|
+
onClose={closeMenu}
|
|
55
|
+
>
|
|
56
|
+
<MenuItem disabled={true}>{profile?.email}</MenuItem>
|
|
57
|
+
{getIsAuthorized({ globalRole: GlobalRole.GlobalAdmin }) && (
|
|
58
|
+
<MenuItem onClick={() => window.open('/ac/ga', '_blank')}>System admin</MenuItem>
|
|
59
|
+
)}
|
|
60
|
+
<MenuItem onClick={logout}>Logout</MenuItem>
|
|
61
|
+
</Styled.Menu>
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AppSelect } from './AppSelect';
|
|
2
|
+
export { MenuSelect } from './MenuSelect';
|
|
3
|
+
export { MultiSubscriptionsMenuItems } from './MultiSubscriptionsMenuItems';
|
|
4
|
+
export { MenuItemWithChildren } from './MenuItemWithChildren/MenuItemWithChildren';
|
|
5
|
+
export { UserProfileSelect } from './UserProfileSelect';
|
|
6
|
+
export { PageWithMenuLayout } from './PageWithMenuLayout';
|
|
7
|
+
export { default as LogoHeaderImage } from './LogoHeaderImage';
|
|
8
|
+
export { default as LogoHeaderText } from './LogoHeaderText';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import MuiDrawer, { drawerClasses } from '@mui/material/Drawer';
|
|
4
|
+
import MuiIconButton from '@mui/material/IconButton';
|
|
5
|
+
import MenuList from '@mui/material/MenuList';
|
|
6
|
+
|
|
7
|
+
import { shouldNotForwardPropsWithKeys } from '../../utils/shouldNotForwardPropsWithKeys';
|
|
8
|
+
|
|
9
|
+
const drawerOpenWidthRem = 15.625;
|
|
10
|
+
const drawerClosedWidthRem = 3.75;
|
|
11
|
+
|
|
12
|
+
export const Drawer = styled(MuiDrawer)`
|
|
13
|
+
flex-shrink: 0;
|
|
14
|
+
|
|
15
|
+
overflow-x: hidden;
|
|
16
|
+
.${drawerClasses.paper} {
|
|
17
|
+
width: ${({ open }) => (open ? drawerOpenWidthRem : drawerClosedWidthRem)}rem;
|
|
18
|
+
|
|
19
|
+
padding: ${({ theme }) => theme.my.spacing.xs};
|
|
20
|
+
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
|
|
23
|
+
background-color: ${({ theme }) => theme.my.colors.primary.main};
|
|
24
|
+
border: unset;
|
|
25
|
+
|
|
26
|
+
transition: width 0.3s;
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export const Header = styled.div`
|
|
31
|
+
display: flex;
|
|
32
|
+
|
|
33
|
+
column-gap: ${({ theme }) => theme.my.spacing.xxs};
|
|
34
|
+
align-items: center;
|
|
35
|
+
|
|
36
|
+
width: max-content;
|
|
37
|
+
min-width: 100%;
|
|
38
|
+
|
|
39
|
+
padding-bottom: ${({ theme }) => theme.my.spacing.xs};
|
|
40
|
+
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
export const LogoContainer = styled.div`
|
|
45
|
+
display: flex;
|
|
46
|
+
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
|
|
50
|
+
width: 2.25rem;
|
|
51
|
+
height: 2.25rem;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
type LockMenuButtonProps = {
|
|
55
|
+
isExpanded: boolean;
|
|
56
|
+
};
|
|
57
|
+
export const LockMenuButton = styled(
|
|
58
|
+
MuiIconButton,
|
|
59
|
+
shouldNotForwardPropsWithKeys<LockMenuButtonProps>(['isExpanded']),
|
|
60
|
+
)<LockMenuButtonProps>(
|
|
61
|
+
({ theme, isExpanded }) => css`
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: ${theme.my.spacing.sm};
|
|
64
|
+
left: ${isExpanded ? drawerOpenWidthRem : drawerClosedWidthRem}rem;
|
|
65
|
+
z-index: ${(theme.zIndex?.drawer ?? 0) + 1};
|
|
66
|
+
|
|
67
|
+
padding: ${theme.my.spacing.xxxs};
|
|
68
|
+
|
|
69
|
+
font-size: 1rem;
|
|
70
|
+
|
|
71
|
+
color: ${theme.my.colors.text.white};
|
|
72
|
+
|
|
73
|
+
background-color: ${theme.my.colors.primitives.darkNavy[700]};
|
|
74
|
+
border-radius: ${theme.my.radius.xxl};
|
|
75
|
+
|
|
76
|
+
transform: translateX(-50%) rotate(${isExpanded ? 0 : 180}deg);
|
|
77
|
+
|
|
78
|
+
transition: 0.3s;
|
|
79
|
+
|
|
80
|
+
&:hover {
|
|
81
|
+
background-color: ${theme.my.colors.primitives.darkNavy[600]};
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
export const TopMenuList = styled(MenuList)`
|
|
87
|
+
display: flex;
|
|
88
|
+
|
|
89
|
+
flex: 1;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
|
|
92
|
+
gap: ${({ theme }) => theme.my.spacing.xxxs};
|
|
93
|
+
|
|
94
|
+
overflow: hidden auto;
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
type MainProps = {
|
|
98
|
+
isLocked: boolean;
|
|
99
|
+
};
|
|
100
|
+
export const Main = styled('div', shouldNotForwardPropsWithKeys<MainProps>(['isLocked']))<MainProps>`
|
|
101
|
+
position: relative;
|
|
102
|
+
|
|
103
|
+
flex-grow: 1;
|
|
104
|
+
|
|
105
|
+
margin-left: ${({ isLocked }) => (isLocked ? drawerOpenWidthRem : drawerClosedWidthRem)}rem;
|
|
106
|
+
|
|
107
|
+
overflow: auto;
|
|
108
|
+
|
|
109
|
+
transition: margin 0.3s;
|
|
110
|
+
`;
|