Skip to content

Commit

Permalink
feat(web): Local sidebar (#5673)
Browse files Browse the repository at this point in the history
* feat: Add New EnvironmentSelector
  • Loading branch information
Joel Anton authored and SokratisVidros committed Jun 13, 2024
1 parent 1d178d7 commit fa55ff5
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 246 deletions.
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@
"@faker-js/faker": "^6.0.0",
"@novu/dal": "workspace:*",
"@novu/testing": "workspace:*",
"@pandacss/dev": "^0.38.0",
"@pandacss/studio": "^0.38.0",
"@pandacss/dev": "^0.40.1",
"@pandacss/studio": "^0.40.1",
"@playwright/test": "^1.44.0",
"@storybook/addon-actions": "^7.4.2",
"@storybook/addon-essentials": "^7.4.2",
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ export const AppRoutes = () => {
{isInformationArchitectureEnabled && (
<Route path={ROUTES.BRAND} element={<Navigate to={ROUTES.BRAND_SETTINGS} replace />} />
)}
<Route path={ROUTES.STUDIO}>
<Route path="" element={<Navigate to={ROUTES.STUDIO_FLOWS} replace />} />
<Route path={ROUTES.STUDIO_FLOWS} element={<WorkflowListPage />} />
</Route>

<Route path="/translations/*" element={<TranslationRoutes />} />
<Route path={ROUTES.ANY} element={<HomePage />} />
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/components/nav/MainNav.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { ROUTES } from '@novu/shared-web';
import { FC } from 'react';
import { LocalNavMenu } from '../../studio/components/LocalNavMenu';
import { RootNavMenu } from './RootNavMenu';
import { SettingsNavMenu } from './SettingsNavMenu';
import { SidebarNav } from './SidebarNav';

export const MainNav: FC = () => {
return <SidebarNav root={<RootNavMenu />} routeMenus={{ [ROUTES.SETTINGS]: <SettingsNavMenu /> }} />;
return (
<SidebarNav
root={<RootNavMenu />}
routeMenus={{ [ROUTES.SETTINGS]: <SettingsNavMenu />, [ROUTES.STUDIO]: <LocalNavMenu /> }}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { IIconProps } from '@novu/design-system';
import { LocalizedMessage } from '@novu/shared-web';
import { ReactNode } from 'react';
import { css } from '@novu/novui/css';
import { styled } from '@novu/novui/jsx';
import { text } from '@novu/novui/recipes';
import { CoreProps } from '@novu/novui';

export type RightSideTrigger = 'hover';

Expand All @@ -12,7 +11,7 @@ export interface INavMenuButtonRightSideConfig {
tooltip?: LocalizedMessage;
triggerOn?: RightSideTrigger;
}
export interface INavMenuButtonProps {
export interface INavMenuButtonProps extends CoreProps {
icon: React.ReactElement<IIconProps>;
label: LocalizedMessage;
rightSide?: INavMenuButtonRightSideConfig;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, PropsWithChildren, useState } from 'react';
import { NavLink } from 'react-router-dom';
import { css } from '@novu/novui/css';
import { css, cx } from '@novu/novui/css';
import { HStack } from '@novu/novui/jsx';
import { INavMenuButtonProps, rawButtonBaseStyles } from './NavMenuButton.shared';
import { NavMenuRightSide } from './NavMenuButtonRightSide';
Expand Down Expand Up @@ -33,6 +33,7 @@ export const NavMenuLinkButton: FC<PropsWithChildren<INavMenuLinkButtonProps>> =
link,
label,
isVisible = true,
className,
}) => {
const [isHovered, setIsHovered] = useState<boolean>(false);

Expand All @@ -42,7 +43,7 @@ export const NavMenuLinkButton: FC<PropsWithChildren<INavMenuLinkButtonProps>> =

return isVisible ? (
<NavLink
className={css(rawButtonBaseStyles, rawLinkButtonStyles)}
className={cx(css(rawButtonBaseStyles, rawLinkButtonStyles), className)}
to={link}
data-test-id={testId}
onMouseEnter={() => setIsHovered(true)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IconArrowDropUp, IconArrowDropDown } from '@novu/design-system';
import { FC, PropsWithChildren, useState } from 'react';
import { css } from '@novu/novui/css';
import { css, cx } from '@novu/novui/css';
import { Flex, HStack } from '@novu/novui/jsx';
import { INavMenuButtonProps, rawButtonBaseStyles } from './NavMenuButton.shared';

Expand All @@ -11,6 +11,7 @@ export const NavMenuToggleButton: FC<PropsWithChildren<INavMenuToggleButtonProps
icon,
label,
children,
className,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);

Expand All @@ -20,7 +21,7 @@ export const NavMenuToggleButton: FC<PropsWithChildren<INavMenuToggleButtonProps

return (
<>
<button className={css(rawButtonBaseStyles)} data-test-id={testId} onClick={handleClick}>
<button className={cx(css(rawButtonBaseStyles), className)} data-test-id={testId} onClick={handleClick}>
<HStack justifyContent={'space-between'} w="inherit">
<HStack gap="75">
{icon}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Select, When } from '@novu/design-system';
import { css } from '@novu/novui/css';
import { navSelectStyles } from '../../../components/nav/NavSelect.styles';
import { EnvironmentPopover } from '../../../components/nav/EnvironmentSelect/EnvironmentPopover';
import { useEnvironmentSelect } from './useEnvironmentSelect';

export const EnvironmentSelectRenderer: React.FC<ReturnType<typeof useEnvironmentSelect>> = ({
icon,
isPopoverOpened,
setIsPopoverOpened,
handlePopoverLinkClick,
...selectProps
}) => {
return (
<EnvironmentPopover
isPopoverOpened={isPopoverOpened}
setIsPopoverOpened={setIsPopoverOpened}
handlePopoverLinkClick={handlePopoverLinkClick}
>
<Select
className={navSelectStyles}
data-test-id="environment-switch"
allowDeselect={false}
icon={
<When truthy={!selectProps.loading}>
<span
className={css({
p: '50',
borderRadius: '50',
bg: 'surface.page',
'& svg': {
fill: 'typography.text.main',
},
_after: {
width: '100',
},
})}
>
{icon}
</span>
</When>
}
{...selectProps}
/>
</EnvironmentPopover>
);
};

export const EnvironmentSelect = () => {
const props = useEnvironmentSelect();

return <EnvironmentSelectRenderer {...props} />;
};
1 change: 1 addition & 0 deletions apps/web/src/studio/components/EnvironmentSelect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './EnvironmentSelect';
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { type ISelectProps } from '@novu/design-system';
import { IconComputer, IconConstruction, IconRocketLaunch, type IIconProps } from '@novu/novui/icons';
import { ROUTES, useEnvController } from '@novu/shared-web';
import { useState } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { EnvironmentEnum } from '../../constants/EnvironmentEnum';

const ENVIRONMENT_ICON_LOOKUP: Record<EnvironmentEnum, React.ReactElement<IIconProps>> = {
[EnvironmentEnum.LOCAL]: <IconComputer />,
[EnvironmentEnum.DEVELOPMENT]: <IconConstruction />,
[EnvironmentEnum.PRODUCTION]: <IconRocketLaunch />,
};

export const useEnvironmentSelect = () => {
const [isPopoverOpened, setIsPopoverOpened] = useState<boolean>(false);
const location = useLocation();

const { setEnvironment, isLoading, environment, readonly } = useEnvController({
onSuccess: (newEnvironment) => {
setIsPopoverOpened(!!newEnvironment?._parentId);
},
});

async function handlePopoverLinkClick(e) {
e.preventDefault();

await setEnvironment(EnvironmentEnum.DEVELOPMENT, { route: ROUTES.CHANGES });
}

const onChange: ISelectProps['onChange'] = async (value) => {
if (typeof value !== 'string') {
return;
}

/*
* this navigates users to the "base" page of the application to avoid sub-pages opened with data from other
* environments -- unless the path itself is based on a specific environment (e.g. API Keys)
*/
const urlParts = location.pathname.replace('/', '').split('/');
const redirectRoute: string | undefined = checkIfEnvBasedRoute() ? undefined : urlParts[0];
await setEnvironment(value as EnvironmentEnum, { route: redirectRoute });
};

return {
loading: isLoading,
data: Object.values(EnvironmentEnum).map((value) => ({
label: value,
value,
})),
value: environment?.name,
onChange,
readonly,
icon: environment?.name ? ENVIRONMENT_ICON_LOOKUP[environment.name] : null,
isPopoverOpened,
setIsPopoverOpened,
handlePopoverLinkClick,
};
};

/** Determine if the current pathname is dependent on the current env */
function checkIfEnvBasedRoute() {
return [ROUTES.API_KEYS, ROUTES.WEBHOOK].some((route) => matchPath(route, window.location.pathname));
}
32 changes: 32 additions & 0 deletions apps/web/src/studio/components/LocalNavMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IconRoute, IconSettings } from '@novu/novui/icons';
import { ROUTES } from '@novu/shared-web';
import { EnvironmentSelect } from './EnvironmentSelect/index';
import { NavMenu } from '../../components/nav/NavMenu';
import { NavMenuLinkButton } from '../../components/nav/NavMenuButton/NavMenuLinkButton';
import { NavMenuSection } from '../../components/nav/NavMenuSection';
import { OrganizationSelect } from '../../components/nav/OrganizationSelect/v2/index';
import { RootNavMenuFooter } from '../../components/nav/RootNavMenuFooter';

export const LocalNavMenu: React.FC = () => {
return (
<NavMenu variant="root">
<NavMenuSection>
<OrganizationSelect />
<NavMenuLinkButton
label="Settings"
icon={<IconSettings />}
link={ROUTES.PROFILE}
testId="side-nav-settings-link"
/>
<EnvironmentSelect />
<NavMenuLinkButton
label="Workflows"
icon={<IconRoute />}
link={ROUTES.STUDIO_FLOWS}
testId="side-nav-templates-link"
/>
</NavMenuSection>
<RootNavMenuFooter />
</NavMenu>
);
};
5 changes: 5 additions & 0 deletions apps/web/src/studio/constants/EnvironmentEnum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum EnvironmentEnum {
LOCAL = 'Local',
DEVELOPMENT = 'Development',
PRODUCTION = 'Production',
}
1 change: 1 addition & 0 deletions apps/web/src/studio/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './EnvironmentEnum';
1 change: 1 addition & 0 deletions libs/novui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './panda-preset';
export * from './components';
export { type CoreProps } from './types';
4 changes: 4 additions & 0 deletions libs/shared-web/src/constants/routes.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export enum ROUTES {
ORGANIZATION = '/settings/organization',
SECURITY = '/settings/security',
BILLING = '/settings/billing',

/** Novu V2 routes */
STUDIO = '/studio',
STUDIO_FLOWS = '/studio/flows',
}

export const PUBLIC_ROUTES_PREFIXES = new Set<string>(['/auth', '/partner-integrations']);
Loading

0 comments on commit fa55ff5

Please sign in to comment.