Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Onboarding] - Refactor tabs and routing #5184

Merged
merged 1 commit into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions apps/web/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Cloud, SSO, UserAccess } from '@novu/design-system';
import { FeatureFlagsKeysEnum, ProductUseCasesEnum } from '@novu/shared';
import { FeatureFlagsKeysEnum } from '@novu/shared';
import { Route, Routes } from 'react-router-dom';
import { AppLayout } from './components/layout/AppLayout';
import { RequiredAuth } from './components/layout/RequiredAuth';
Expand All @@ -15,7 +15,6 @@ import { BrandPage } from './pages/brand/BrandPage';
import { BrandingForm, LayoutsListPage } from './pages/brand/tabs';
import { PromoteChangesPage } from './pages/changes/PromoteChangesPage';
import { GetStartedPage } from './pages/get-started/GetStartedPage';
import { GetStartedTab } from './pages/get-started/layout/GetStartedTab';
import HomePage from './pages/HomePage';
import { SelectProviderPage } from './pages/integrations/components/SelectProviderPage';
import { CreateProviderPage } from './pages/integrations/CreateProviderPage';
Expand Down Expand Up @@ -100,10 +99,7 @@ export const AppRoutes = () => {
<Route path=":identifier" element={<UpdateTenantPage />} />
</Route>
{isImprovedOnboardingEnabled ? (
<Route path={ROUTES.GET_STARTED} element={<GetStartedPage />}>
<Route path="" element={<GetStartedTab usecase={ProductUseCasesEnum.IN_APP} />} />
<Route path=":usecase" element={<GetStartedTab />} />
</Route>
<Route path={ROUTES.GET_STARTED} element={<GetStartedPage />} />
) : (
<Route path={ROUTES.GET_STARTED} element={<GetStarted />} />
)}
Comment on lines +102 to 105
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional (that requires a re-render) is why search params don't persist across refresh. Once we remove this, they will work as expected

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this scope
I wonder if we could/want to create a "loading" indication on our FF's so in such cases we would create a loading state, so we would avoid unnecessary rerenders and screen changes.

@antonjoel82 would love to hear your thoughts on it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@djabarovgeorge What is FF in this context?

Sounds like a good UX improvement! We could do a temporary one, but overall my preference would be to upgrade our React version to use Suspense which will be a huge improvement overall.

Alternatively, one way I've done this in the past to avoid the re-routing issue is to route to a container page (just a page component that uses the hook to determine which content to show). Then, we'd delete that page later and point directly to the new functionality

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry by i meant feature flag by FF.
Still had not the chance to use Suspense wonder how good is it.
Agree that a middleware component could help.

thanks for the feedback 🙃

Expand Down
17 changes: 14 additions & 3 deletions apps/web/src/pages/get-started/GetStartedPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import React from 'react';

import { HeaderLayout } from './layout/HeaderLayout';
import PageContainer from '../../components/layout/components/PageContainer';
import PageHeader from '../../components/layout/components/PageHeader';
import { GetStartedTabs } from './components/get-started-tabs/GetStartedTabs';
import { useAuthContext } from '../../components/providers/AuthProvider';
import { Center, Loader } from '@mantine/core';
import { colors } from '@novu/design-system';
import { useTabSearchParams } from './components/get-started-tabs/useGetStartedTabs';

const PAGE_TITLE = 'Get started';

export function GetStartedPage() {
const { currentOrganization } = useAuthContext();
const { currentTab, setTab } = useTabSearchParams();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like should be used inside the GetStartedTabs:

  • otherwise, it will rerender this whole component (but it's fine in this case)
  • child controls the parent state (nah)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I will merge it as is for now, but we can memoize the tab behaviors so that they don't re-render the whole component tree if it becomes necessary.

return (
<PageContainer title={PAGE_TITLE}>
<HeaderLayout>
<PageHeader title={PAGE_TITLE} />
</HeaderLayout>
<GetStartedTabs />
{currentOrganization ? (
<GetStartedTabs currentTab={currentTab} setTab={setTab} />
) : (
<Center>
<Loader color={colors.error} size={32} />
</Center>
)}
</PageContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RingingBell, MultiChannel, Digest, HalfClock, Translation } from '@novu/design-system';
import { CSSProperties } from 'react';
import { OnboardingUseCasesTabsEnum } from '../../consts/OnboardingUseCasesTabsEnum';

export interface GetStartedTabConfig {
value: OnboardingUseCasesTabsEnum;
icon: JSX.Element;
title: string;
}

const ICON_STYLE: Partial<CSSProperties> = { height: 20, width: 20, marginBottom: '12px' };
export const TAB_CONFIGS: GetStartedTabConfig[] = [
{
value: OnboardingUseCasesTabsEnum.IN_APP,
icon: <RingingBell style={ICON_STYLE} />,
title: 'In-app',
},
{
value: OnboardingUseCasesTabsEnum.MULTI_CHANNEL,
icon: <MultiChannel style={ICON_STYLE} />,
title: 'Multi-channel',
},
{
value: OnboardingUseCasesTabsEnum.DIGEST,
icon: <Digest style={ICON_STYLE} />,
title: 'Digest',
},
{
value: OnboardingUseCasesTabsEnum.DELAY,
icon: <HalfClock style={ICON_STYLE} />,
title: 'Delay',
},
{
value: OnboardingUseCasesTabsEnum.TRANSLATION,
icon: <Translation style={ICON_STYLE} />,
title: 'Translate',
},
];
Original file line number Diff line number Diff line change
@@ -1,63 +1,47 @@
import React from 'react';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import { Center, Container, Loader, Tabs } from '@mantine/core';

import { colors, Digest, HalfClock, MultiChannel, RingingBell, Translation } from '@novu/design-system';

import { useAuthContext } from '../../../../components/providers/AuthProvider';
import { OnboardingParams } from '../../types';
import { ROUTES } from '../../../../constants/routes.enum';
import { OnboardingUseCasesTabsEnum } from '../../../../constants/onboarding-tabs';
import { Container, Tabs } from '@mantine/core';
import { Outlet } from 'react-router-dom';
import { OnboardingUseCasesTabsEnum } from '../../consts/OnboardingUseCasesTabsEnum';
import { UseCasesConst } from '../../consts/UseCases.const';
import { GetStartedTab } from '../../layout/GetStartedTab';
import { GetStartedTabConfig, TAB_CONFIGS } from './GetStartedTabs.const';
import useStyles from './GetStartedTabs.style';

export function GetStartedTabs() {
const { currentOrganization } = useAuthContext();
const { classes } = useStyles();
const navigate = useNavigate();
const { usecase } = useParams<OnboardingParams>();

const iconStyle = { height: 20, width: 20, marginBottom: '12px' };
interface IGetStartedTabsProps {
tabConfigs?: GetStartedTabConfig[];
currentTab: OnboardingUseCasesTabsEnum;
setTab: (tab: OnboardingUseCasesTabsEnum) => void;
}

if (!currentOrganization) {
return (
<Center>
<Loader color={colors.error} size={32} />
</Center>
);
}
export const GetStartedTabs: React.FC<IGetStartedTabsProps> = ({ tabConfigs = TAB_CONFIGS, currentTab, setTab }) => {
const { classes } = useStyles();

return (
<Container fluid mt={15} ml={5}>
<Tabs
orientation="horizontal"
keepMounted={true}
onTabChange={(tabValue) => {
navigate(`${ROUTES.GET_STARTED}/${tabValue}`);
onTabChange={(tabValue: OnboardingUseCasesTabsEnum) => {
setTab(tabValue);
}}
variant="default"
value={usecase ?? OnboardingUseCasesTabsEnum.IN_APP}
value={currentTab}
classNames={classes}
mb={15}
>
<Tabs.List>
<Tabs.Tab value={OnboardingUseCasesTabsEnum.IN_APP} icon={<RingingBell style={iconStyle} />}>
In-app
</Tabs.Tab>
<Tabs.Tab value={OnboardingUseCasesTabsEnum.MULTI_CHANNEL} icon={<MultiChannel style={iconStyle} />}>
Multi-channel
</Tabs.Tab>
<Tabs.Tab value={OnboardingUseCasesTabsEnum.DIGEST} icon={<Digest style={iconStyle} />}>
Digest
</Tabs.Tab>
<Tabs.Tab value={OnboardingUseCasesTabsEnum.DELAY} icon={<HalfClock style={iconStyle} />}>
Delay
</Tabs.Tab>
<Tabs.Tab value={OnboardingUseCasesTabsEnum.TRANSLATION} icon={<Translation style={iconStyle} />}>
Translate
</Tabs.Tab>
{tabConfigs.map(({ value, icon, title }) => (
<Tabs.Tab key={`tab-${value}`} value={value} icon={icon}>
{title}
</Tabs.Tab>
))}
</Tabs.List>
{tabConfigs.map(({ value }) => (
<Tabs.Panel key={`tab-panel-${value}`} value={value}>
{<GetStartedTab {...UseCasesConst[value]} />}
</Tabs.Panel>
))}
</Tabs>
<Outlet />
</Container>
);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { URLSearchParamsInit, useSearchParams } from 'react-router-dom';
import { OnboardingUseCasesTabsEnum } from '../../consts/OnboardingUseCasesTabsEnum';

const TAB_SEARCH_PARAM_NAME = 'tab';
const DEFAULT_TAB: OnboardingUseCasesTabsEnum = OnboardingUseCasesTabsEnum.IN_APP;

interface GetStartedTabSearchParams {
[TAB_SEARCH_PARAM_NAME]: OnboardingUseCasesTabsEnum;
}

const DEFAULT_PARAMS: GetStartedTabSearchParams = {
[TAB_SEARCH_PARAM_NAME]: DEFAULT_TAB,
} satisfies URLSearchParamsInit;

export const useTabSearchParams = () => {
const [params, setParams] = useSearchParams(DEFAULT_PARAMS as unknown as URLSearchParamsInit);

const setTab = (tab: OnboardingUseCasesTabsEnum) => {
// replace is used so that changing the search params isn't considered a change in page
setParams({ [TAB_SEARCH_PARAM_NAME]: tab }, { replace: true });
};

const currentTab = (params.get(TAB_SEARCH_PARAM_NAME) as OnboardingUseCasesTabsEnum) ?? DEFAULT_TAB;

return {
currentTab,
setTab,
};
};
13 changes: 6 additions & 7 deletions apps/web/src/pages/get-started/consts/UseCases.const.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { ProductUseCasesEnum } from '@novu/shared';

import { OnboardingUseCases } from './types';
import { InAppUseCaseConst } from './InAppUseCase.const';
import { MultiChannelUseCaseConst } from './MultiChannelUseCase.const';
import { DelayUseCaseConst } from './DelayUseCase.const';
import { TranslationUseCaseConst } from './TranslationUseCase.const';
import { DigestUseCaseConst } from './DigestUseCase.const';
import { OnboardingUseCasesTabsEnum } from './OnboardingUseCasesTabsEnum';

export const UseCasesConst: OnboardingUseCases = {
[ProductUseCasesEnum.IN_APP]: InAppUseCaseConst,
[ProductUseCasesEnum.MULTI_CHANNEL]: MultiChannelUseCaseConst,
[ProductUseCasesEnum.DELAY]: DelayUseCaseConst,
[ProductUseCasesEnum.TRANSLATION]: TranslationUseCaseConst,
[ProductUseCasesEnum.DIGEST]: DigestUseCaseConst,
[OnboardingUseCasesTabsEnum.IN_APP]: InAppUseCaseConst,
[OnboardingUseCasesTabsEnum.MULTI_CHANNEL]: MultiChannelUseCaseConst,
[OnboardingUseCasesTabsEnum.DELAY]: DelayUseCaseConst,
[OnboardingUseCasesTabsEnum.TRANSLATION]: TranslationUseCaseConst,
[OnboardingUseCasesTabsEnum.DIGEST]: DigestUseCaseConst,
};
6 changes: 2 additions & 4 deletions apps/web/src/pages/get-started/consts/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ProductUseCasesEnum } from '@novu/shared';
import { OnboardingUseCasesTabsEnum } from './OnboardingUseCasesTabsEnum';

export type OnboardingUseCases = {
[key in ProductUseCasesEnum]: OnboardingUseCase;
};
export type OnboardingUseCases = Record<OnboardingUseCasesTabsEnum, OnboardingUseCase>;

export interface IOnboardingStep {
title: string;
Expand Down
37 changes: 4 additions & 33 deletions apps/web/src/pages/get-started/layout/GetStartedTab.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,15 @@
import React, { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Grid } from '@mantine/core';
import styled from '@emotion/styled';
import { Grid } from '@mantine/core';

import { ProductUseCasesEnum } from '@novu/shared';
import { colors, Text } from '@novu/design-system';

import { OnboardingParams } from '../types';
import { ROUTES } from '../../../constants/routes.enum';
import { OnboardingUseCasesTabsEnum } from '../../../constants/onboarding-tabs';
import Card from '../../../components/layout/components/Card';
import { Timeline } from '../components/timeline/Timeline';
import { UseCasesConst } from '../consts/UseCases.const';

interface IGetStartedTabProps {
usecase?: ProductUseCasesEnum;
}

export function GetStartedTab(props: IGetStartedTabProps) {
const navigate = useNavigate();
const { usecase: usecaseParam } = useParams<Record<OnboardingParams, OnboardingUseCasesTabsEnum | undefined>>();

const usecase = (props.usecase || usecaseParam?.replace('-', '_')) as ProductUseCasesEnum | undefined;

/*
* This will redirect to the in-app tab if the use case is not provided in the parameters and component input.
* * This state should not occur; it was added as a precautionary measure.
*/
useEffect(() => {
if (!usecase) {
navigate(`${ROUTES.GET_STARTED}/${OnboardingUseCasesTabsEnum.IN_APP}`);
}
}, [navigate, usecase]);

if (!usecase) {
return null;
}
import { OnboardingUseCase } from '../consts/types';

const { steps, Demo, title, description } = UseCasesConst[usecase];
type IGetStartedTabProps = OnboardingUseCase;

export function GetStartedTab({ steps, Demo, title, description }: IGetStartedTabProps) {
return (
<Grid align="stretch" justify={'space-between'}>
<Grid.Col span={3} mt={12}>
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/pages/get-started/types.ts

This file was deleted.

Loading