feat: add custom additional drawer link (#1586)

This commit is contained in:
Ali BARIN
2024-02-08 16:33:12 +01:00
committed by GitHub
parent 6bba2c82fe
commit 24451892ff
8 changed files with 134 additions and 44 deletions

View File

@@ -18,7 +18,9 @@ const port = process.env.PORT || '3000';
const serveWebAppSeparately = const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false; process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let apiUrl = new URL(process.env.API_URL || `${protocol}://${host}:${port}`).toString(); let apiUrl = new URL(
process.env.API_URL || `${protocol}://${host}:${port}`
).toString();
apiUrl = apiUrl.substring(0, apiUrl.length - 1); apiUrl = apiUrl.substring(0, apiUrl.length - 1);
// use apiUrl by default, which has less priority over the following cases // use apiUrl by default, which has less priority over the following cases
@@ -90,6 +92,8 @@ const appConfig = {
CI: process.env.CI === 'true', CI: process.env.CI === 'true',
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true', disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true', disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
}; };
if (!appConfig.encryptionKey) { if (!appConfig.encryptionKey) {

View File

@@ -8,6 +8,8 @@ const getConfig = async (_parent, params) => {
const defaultConfig = { const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage, disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon, disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
}; };
const configQuery = Config.query(); const configQuery = Config.query();

View File

@@ -59,6 +59,8 @@ describe('graphQL getConfig query', () => {
[configThree.key]: configThree.value.data, [configThree.key]: configThree.value.data,
disableNotificationsPage: false, disableNotificationsPage: false,
disableFavicon: false, disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
}, },
}, },
}; };
@@ -87,6 +89,8 @@ describe('graphQL getConfig query', () => {
[configTwo.key]: configTwo.value.data, [configTwo.key]: configTwo.value.data,
disableNotificationsPage: false, disableNotificationsPage: false,
disableFavicon: false, disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
}, },
}, },
}; };
@@ -101,6 +105,12 @@ describe('graphQL getConfig query', () => {
true true
); );
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true); vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
}); });
it('should return custom config', async () => { it('should return custom config', async () => {
@@ -117,6 +127,8 @@ describe('graphQL getConfig query', () => {
[configThree.key]: configThree.value.data, [configThree.key]: configThree.value.data,
disableNotificationsPage: true, disableNotificationsPage: true,
disableFavicon: true, disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
}, },
}, },
}; };

View File

@@ -15,6 +15,7 @@ import { SvgIconComponent } from '@mui/icons-material';
import AppBar from 'components/AppBar'; import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer'; import Drawer from 'components/Drawer';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type SettingsLayoutProps = { type SettingsLayoutProps = {
@@ -86,19 +87,11 @@ function createDrawerLinks({
return items; return items;
} }
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: 'adminSettingsDrawer.goBack',
to: '/',
dataTest: 'go-back-drawer-link',
},
];
export default function SettingsLayout({ export default function SettingsLayout({
children, children,
}: SettingsLayoutProps): React.ReactElement { }: SettingsLayoutProps): React.ReactElement {
const theme = useTheme(); const theme = useTheme();
const formatMessage = useFormatMessage();
const currentUserAbility = useCurrentUserAbility(); const currentUserAbility = useCurrentUserAbility();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -116,6 +109,15 @@ export default function SettingsLayout({
canUpdateApp: currentUserAbility.can('update', 'App'), canUpdateApp: currentUserAbility.can('update', 'App'),
}); });
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: formatMessage('adminSettingsDrawer.goBack'),
to: '/',
dataTest: 'go-back-drawer-link',
},
];
return ( return (
<> <>
<AppBar <AppBar

View File

@@ -19,6 +19,7 @@ type DrawerLink = {
Icon: React.ElementType; Icon: React.ElementType;
primary: string; primary: string;
to: string; to: string;
target?: '_blank';
badgeContent?: React.ReactNode; badgeContent?: React.ReactNode;
dataTest?: string; dataTest?: string;
}; };
@@ -69,7 +70,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<List sx={{ py: 0, mt: 3 }}> <List sx={{ py: 0, mt: 3 }}>
{bottomLinks.map( {bottomLinks.map(
({ Icon, badgeContent, primary, to, dataTest }, index) => ( ({ Icon, badgeContent, primary, to, dataTest, target }, index) => (
<ListItemLink <ListItemLink
key={`${to}-${index}`} key={`${to}-${index}`}
icon={ icon={
@@ -77,9 +78,10 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<Icon htmlColor={theme.palette.primary.main} /> <Icon htmlColor={theme.palette.primary.main} />
</Badge> </Badge>
} }
primary={formatMessage(primary)} primary={primary}
to={to} to={to}
onClick={closeOnClick} onClick={closeOnClick}
target={target}
data-test={dataTest} data-test={dataTest}
/> />
) )

View File

@@ -7,8 +7,10 @@ import AppsIcon from '@mui/icons-material/Apps';
import SwapCallsIcon from '@mui/icons-material/SwapCalls'; import SwapCallsIcon from '@mui/icons-material/SwapCalls';
import HistoryIcon from '@mui/icons-material/History'; import HistoryIcon from '@mui/icons-material/History';
import NotificationsIcon from '@mui/icons-material/Notifications'; import NotificationsIcon from '@mui/icons-material/Notifications';
import ArrowBackIosNew from '@mui/icons-material/ArrowBackIosNew';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useVersion from 'hooks/useVersion'; import useVersion from 'hooks/useVersion';
import AppBar from 'components/AppBar'; import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer'; import Drawer from 'components/Drawer';
@@ -41,46 +43,93 @@ const drawerLinks = [
type GenerateDrawerBottomLinksOptions = { type GenerateDrawerBottomLinksOptions = {
disableNotificationsPage: boolean; disableNotificationsPage: boolean;
loading: boolean;
notificationBadgeContent: number; notificationBadgeContent: number;
additionalDrawerLink?: string;
additionalDrawerLinkText?: string;
additionalDrawerLinkIcon?: string;
formatMessage: ReturnType<typeof useFormatMessage>;
}; };
const generateDrawerBottomLinks = ({ const generateDrawerBottomLinks = async ({
disableNotificationsPage, disableNotificationsPage,
loading,
notificationBadgeContent = 0, notificationBadgeContent = 0,
additionalDrawerLink,
additionalDrawerLinkText,
formatMessage,
}: GenerateDrawerBottomLinksOptions) => { }: GenerateDrawerBottomLinksOptions) => {
if (loading || disableNotificationsPage) { const notificationsPageLinkObject = {
return []; Icon: NotificationsIcon,
primary: formatMessage('settingsDrawer.notifications'),
to: URLS.UPDATES,
badgeContent: notificationBadgeContent,
};
const hasAdditionalDrawerLink =
additionalDrawerLink && additionalDrawerLinkText;
const additionalDrawerLinkObject = {
Icon: ArrowBackIosNew,
primary: additionalDrawerLinkText || '',
to: additionalDrawerLink || '',
target: '_blank' as const,
};
const links = [];
if (!disableNotificationsPage) {
links.push(notificationsPageLinkObject);
} }
return [ if (hasAdditionalDrawerLink) {
{ links.push(additionalDrawerLinkObject);
Icon: NotificationsIcon, }
primary: 'settingsDrawer.notifications',
to: URLS.UPDATES, return links;
badgeContent: notificationBadgeContent, };
},
]; type Link = {
Icon: React.ElementType;
primary: string;
target?: '_blank';
to: string;
badgeContent?: React.ReactNode;
}; };
export default function PublicLayout({ export default function PublicLayout({
children, children,
}: PublicLayoutProps): React.ReactElement { }: PublicLayoutProps): React.ReactElement {
const version = useVersion(); const version = useVersion();
const { config, loading } = useConfig(['disableNotificationsPage']); const { config, loading } = useConfig([
'disableNotificationsPage',
'additionalDrawerLink',
'additionalDrawerLinkText',
]);
const theme = useTheme(); const theme = useTheme();
const formatMessage = useFormatMessage();
const [bottomLinks, setBottomLinks] = React.useState<Link[]>([]);
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
const openDrawer = () => setDrawerOpen(true); const openDrawer = () => setDrawerOpen(true);
const closeDrawer = () => setDrawerOpen(false); const closeDrawer = () => setDrawerOpen(false);
const drawerBottomLinks = generateDrawerBottomLinks({ React.useEffect(() => {
notificationBadgeContent: version.newVersionCount, async function perform() {
loading, const newBottomLinks = await generateDrawerBottomLinks({
disableNotificationsPage: config?.disableNotificationsPage as boolean, notificationBadgeContent: version.newVersionCount,
}); disableNotificationsPage: config?.disableNotificationsPage as boolean,
additionalDrawerLink: config?.additionalDrawerLink as string,
additionalDrawerLinkText: config?.additionalDrawerLinkText as string,
formatMessage,
});
setBottomLinks(newBottomLinks);
}
if (loading) return;
perform();
}, [config, loading, version.newVersionCount]);
return ( return (
<> <>
@@ -93,7 +142,7 @@ export default function PublicLayout({
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Drawer <Drawer
links={drawerLinks} links={drawerLinks}
bottomLinks={drawerBottomLinks} bottomLinks={bottomLinks}
open={isDrawerOpen} open={isDrawerOpen}
onOpen={openDrawer} onOpen={openDrawer}
onClose={closeDrawer} onClose={closeDrawer}

View File

@@ -9,6 +9,7 @@ type ListItemLinkProps = {
icon: React.ReactNode; icon: React.ReactNode;
primary: string; primary: string;
to: string; to: string;
target?: '_blank';
onClick?: (event: React.SyntheticEvent) => void; onClick?: (event: React.SyntheticEvent) => void;
'data-test'?: string; 'data-test'?: string;
}; };
@@ -16,14 +17,29 @@ type ListItemLinkProps = {
export default function ListItemLink( export default function ListItemLink(
props: ListItemLinkProps props: ListItemLinkProps
): React.ReactElement { ): React.ReactElement {
const { icon, primary, to, onClick, 'data-test': dataTest } = props; const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
const selected = useMatch({ path: to, end: true }); const selected = useMatch({ path: to, end: true });
const CustomLink = React.useMemo( const CustomLink = React.useMemo(
() => () =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>( React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
function InLineLink(linkProps, ref) { function InLineLink(linkProps, ref) {
return <Link ref={ref} to={to} {...linkProps} />; try {
// challenge the link to check if it's absolute URL
new URL(to); // should throw an error if it's not an absolute URL
return (
<a
{...linkProps}
ref={ref}
href={to}
target={target}
rel="noopener noreferrer"
/>
);
} catch {
return <Link ref={ref} {...linkProps} to={to} />;
}
} }
), ),
[to] [to]
@@ -37,6 +53,7 @@ export default function ListItemLink(
selected={!!selected} selected={!!selected}
onClick={onClick} onClick={onClick}
data-test={dataTest} data-test={dataTest}
target={target}
> >
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon> <ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
<ListItemText <ListItemText

View File

@@ -9,6 +9,7 @@ import PaymentIcon from '@mui/icons-material/Payment';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import useAutomatischInfo from 'hooks/useAutomatischInfo'; import useAutomatischInfo from 'hooks/useAutomatischInfo';
import useFormatMessage from 'hooks/useFormatMessage';
import AppBar from 'components/AppBar'; import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer'; import Drawer from 'components/Drawer';
@@ -22,8 +23,8 @@ function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
Icon: AccountCircleIcon, Icon: AccountCircleIcon,
primary: 'settingsDrawer.myProfile', primary: 'settingsDrawer.myProfile',
to: URLS.SETTINGS_PROFILE, to: URLS.SETTINGS_PROFILE,
} },
] ];
if (isCloud) { if (isCloud) {
items.push({ items.push({
@@ -36,19 +37,12 @@ function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
return items; return items;
} }
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: 'settingsDrawer.goBack',
to: '/',
},
];
export default function SettingsLayout({ export default function SettingsLayout({
children, children,
}: SettingsLayoutProps): React.ReactElement { }: SettingsLayoutProps): React.ReactElement {
const { isCloud } = useAutomatischInfo(); const { isCloud } = useAutomatischInfo();
const theme = useTheme(); const theme = useTheme();
const formatMessage = useFormatMessage();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -56,6 +50,14 @@ export default function SettingsLayout({
const closeDrawer = () => setDrawerOpen(false); const closeDrawer = () => setDrawerOpen(false);
const drawerLinks = createDrawerLinks({ isCloud }); const drawerLinks = createDrawerLinks({ isCloud });
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: formatMessage('settingsDrawer.goBack'),
to: '/',
},
];
return ( return (
<> <>
<AppBar <AppBar