feat: add custom additional drawer link (#1586)
This commit is contained in:
@@ -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) {
|
||||||
|
@@ -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();
|
||||||
|
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -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
|
||||||
|
@@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -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}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user