-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems like should be used inside the
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
}; | ||
}; |
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, | ||
}; |
This file was deleted.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 🙃