feat: ensure to access dashboard user's subscription is active.

This commit is contained in:
a.bouhuolia
2021-08-30 10:42:39 +02:00
parent 47da64e625
commit 9dca9f3317
59 changed files with 1299 additions and 546 deletions

View File

@@ -0,0 +1,17 @@
import React from 'react';
import clsx from 'classnames';
import Style from './style.module.scss';
export function Alert({ title, description, intent }) {
return (
<div
className={clsx(Style.root, {
[`${Style['root_' + intent]}`]: intent,
})}
>
{title && <h3 className={clsx(Style.title)}>{title}</h3>}
{description && <p class={clsx(Style.description)}>{description}</p>}
</div>
);
}

View File

@@ -0,0 +1,32 @@
.root {
border: 1px solid rgb(223, 227, 230);
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
&_danger {
border-color: rgb(249, 198, 198);
background: rgb(255, 248, 248);
.description {
color: #d95759;
}
.title {
color: rgb(205, 43, 49);
}
}
}
.title {
color: rgb(17, 24, 28);
margin-bottom: 4px;
font-size: 14px;
font-weight: 600;
}
.description {
color: rgb(104, 112, 118);
margin: 0;
}

View File

@@ -12,6 +12,33 @@ import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
import GlobalHotkeys from './GlobalHotkeys';
import DashboardProvider from './DashboardProvider';
import DrawersContainer from 'components/DrawersContainer';
import EnsureSubscriptionIsActive from '../Guards/EnsureSubscriptionIsActive';
/**
* Dashboard preferences.
*/
function DashboardPreferences() {
return (
<EnsureSubscriptionIsActive>
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
</EnsureSubscriptionIsActive>
);
}
/**
* Dashboard other routes.
*/
function DashboardAnyPage() {
return (
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
);
}
/**
* Dashboard page.
@@ -20,19 +47,8 @@ export default function Dashboard() {
return (
<DashboardProvider>
<Switch>
<Route path="/preferences">
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
</Route>
<Route path="/">
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
</Route>
<Route path="/preferences" component={DashboardPreferences} />
<Route path="/" component={DashboardAnyPage} />
</Switch>
<DashboardUniversalSearch />

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
import DashboardContentRoute from 'components/Dashboard/DashboardContentRoute';
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
import DashboardFooter from 'components/Dashboard/DashboardFooter';
import DashboardErrorBoundary from './DashboardErrorBoundary';
@@ -10,7 +10,7 @@ export default React.forwardRef(({}, ref) => {
<ErrorBoundary FallbackComponent={DashboardErrorBoundary}>
<div className="dashboard-content" id="dashboard" ref={ref}>
<DashboardTopbar />
<DashboardContentRoute />
<DashboardContentRoutes />
<DashboardFooter />
</div>
</ErrorBoundary>

View File

@@ -2,8 +2,43 @@ import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { getDashboardRoutes } from 'routes/dashboard';
import EnsureSubscriptionsIsActive from '../Guards/EnsureSubscriptionsIsActive';
import EnsureSubscriptionsIsInactive from '../Guards/EnsureSubscriptionsIsInactive';
import DashboardPage from './DashboardPage';
/**
* Dashboard inner route content.
*/
function DashboardContentRouteContent({ route }) {
const content = (
<DashboardPage
name={route.name}
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
hint={route.hint}
sidebarExpand={route.sidebarExpand}
pageType={route.pageType}
defaultSearchResource={route.defaultSearchResource}
/>
);
return route.subscriptionActive ? (
<EnsureSubscriptionsIsInactive
subscriptionTypes={route.subscriptionActive}
children={content}
redirectTo={'/billing'}
/>
) : route.subscriptionInactive ? (
<EnsureSubscriptionsIsActive
subscriptionTypes={route.subscriptionInactive}
children={content}
redirectTo={'/'}
/>
) : (
content
);
}
/**
* Dashboard content route.
*/
@@ -14,21 +49,8 @@ export default function DashboardContentRoute() {
<Route pathname="/">
<Switch>
{routes.map((route, index) => (
<Route
exact={route.exact}
key={index}
path={`${route.path}`}
>
<DashboardPage
name={route.name}
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
hint={route.hint}
sidebarExpand={route.sidebarExpand}
pageType={route.pageType}
defaultSearchResource={route.defaultSearchResource}
/>
<Route exact={route.exact} key={index} path={`${route.path}`}>
<DashboardContentRouteContent route={route} />
</Route>
))}
</Switch>

View File

@@ -1,15 +1,12 @@
import React from 'react';
import DashboardLoadingIndicator from './DashboardLoadingIndicator';
import { useSettings } from 'hooks/query';
/**
* Dashboard provider.
*/
export default function DashboardProvider({ children }) {
const { isLoading } = useSettings();
return (
<DashboardLoadingIndicator isLoading={isLoading}>
<DashboardLoadingIndicator isLoading={false}>
{ children }
</DashboardLoadingIndicator>
)

View File

@@ -23,6 +23,43 @@ import withSettings from 'containers/Settings/withSettings';
import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function DashboardTopbarSubscriptionMessage() {
return (
<div class="dashboard__topbar-subscription-msg">
<span>
<T id={'dashboard.subscription_msg.period_over'} />
</span>
</div>
);
}
function DashboardHamburgerButton({ ...props }) {
return (
<Button minimal={true} {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
role="img"
focusable="false"
>
<title>
<T id={'menu'} />
</title>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="5"
stroke-width="2"
d="M4 7h15M4 12h15M4 17h15"
></path>
</svg>
</Button>
);
}
/**
* Dashboard topbar.
@@ -44,6 +81,10 @@ function DashboardTopbar({
// #withGlobalSearch
openGlobalSearch,
// #withSubscriptions
isSubscriptionActive,
isSubscriptionInactive,
}) {
const history = useHistory();
@@ -69,27 +110,7 @@ function DashboardTopbar({
}
position={Position.RIGHT}
>
<Button minimal={true} onClick={handleSidebarToggleBtn}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
role="img"
focusable="false"
>
<title>
<T id={'menu'} />
</title>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="5"
stroke-width="2"
d="M4 7h15M4 12h15M4 17h15"
></path>
</svg>
</Button>
<DashboardHamburgerButton onClick={handleSidebarToggleBtn} />
</Tooltip>
</div>
@@ -114,29 +135,36 @@ function DashboardTopbar({
<div class="dashboard__breadcrumbs">
<DashboardBreadcrumbs />
</div>
<DashboardBackLink />
</div>
<div class="dashboard__topbar-right">
<If condition={isSubscriptionInactive}>
<DashboardTopbarSubscriptionMessage />
</If>
<Navbar class="dashboard__topbar-navbar">
<NavbarGroup>
<Button
onClick={() => openGlobalSearch(true)}
className={Classes.MINIMAL}
icon={<Icon icon={'search-24'} iconSize={20} />}
text={<T id={'quick_find'} />}
/>
<QuickNewDropdown />
<Tooltip
content={<T id={'notifications'} />}
position={Position.BOTTOM}
>
<If condition={isSubscriptionActive}>
<Button
onClick={() => openGlobalSearch(true)}
className={Classes.MINIMAL}
icon={<Icon icon={'notification-24'} iconSize={20} />}
icon={<Icon icon={'search-24'} iconSize={20} />}
text={<T id={'quick_find'} />}
/>
</Tooltip>
<QuickNewDropdown />
<Tooltip
content={<T id={'notifications'} />}
position={Position.BOTTOM}
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'notification-24'} iconSize={20} />}
/>
</Tooltip>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'help-24'} iconSize={20} />}
@@ -166,4 +194,11 @@ export default compose(
organizationName: organizationSettings.name,
})),
withDashboardActions,
withSubscriptions(
({ isSubscriptionActive, isSubscriptionInactive }) => ({
isSubscriptionActive,
isSubscriptionInactive,
}),
'main',
),
)(DashboardTopbar);

View File

@@ -8,15 +8,21 @@ import {
Popover,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { If, FormattedMessage as T } from 'components';
import { firstLettersArgs } from 'utils';
import { useAuthActions, useAuthUser } from 'hooks/state';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function DashboardTopbarUser({ openDialog }) {
function DashboardTopbarUser({
openDialog,
// #withSubscriptions
isSubscriptionActive
}) {
const history = useHistory();
const { setLogout } = useAuthActions();
const user = useAuthUser();
@@ -48,14 +54,16 @@ function DashboardTopbarUser({ openDialog }) {
}
/>
<MenuDivider />
<MenuItem
text={<T id={'keyboard_shortcuts'} />}
onClick={onKeyboardShortcut}
/>
<MenuItem
text={<T id={'preferences'} />}
onClick={() => history.push('/preferences')}
/>
<If condition={isSubscriptionActive}>
<MenuItem
text={<T id={'keyboard_shortcuts'} />}
onClick={onKeyboardShortcut}
/>
<MenuItem
text={<T id={'preferences'} />}
onClick={() => history.push('/preferences')}
/>
</If>
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
</Menu>
}
@@ -69,4 +77,10 @@ function DashboardTopbarUser({ openDialog }) {
</Popover>
);
}
export default compose(withDialogActions)(DashboardTopbarUser);
export default compose(
withDialogActions,
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(DashboardTopbarUser);

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { includes } from 'lodash';
import { compose } from 'utils';
import { Redirect } from 'react-router-dom';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
/**
* Ensures the given subscription type is active or redirect to the given route.
*/
function EnsureSubscriptionIsActive({
children,
subscriptionType = 'main',
redirectTo = '/billing',
routePath,
exclude,
isSubscriptionActive,
}) {
return isSubscriptionActive || includes(exclude, routePath) ? (
children
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}
export default compose(
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(EnsureSubscriptionIsActive);

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { includes } from 'lodash';
import { compose } from 'utils';
import { Redirect } from 'react-router-dom';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptionss';
/**
* Ensures the given subscription type is active or redirect to the given route.
*/
function EnsureSubscriptionsIsActive({
children,
subscriptionType = 'main',
redirectTo = '/billing',
routePath,
exclude,
isSubscriptionsActive,
}) {
return !isSubscriptionsActive || includes(exclude, routePath) ? (
children
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}
export default compose(
withSubscriptions(
({ isSubscriptionsActive }) => ({ isSubscriptionsActive }),
'main',
),
)(EnsureSubscriptionsIsActive);

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { includes } from 'lodash';
import { compose } from 'utils';
import { Redirect } from 'react-router-dom';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptionss';
/**
* Ensures the given subscription type is active or redirect to the given route.
*/
function EnsureSubscriptionsIsInactive({
children,
subscriptionType = 'main',
redirectTo = '/billing',
routePath,
exclude,
isSubscriptionsInactive,
}) {
return !isSubscriptionsInactive || includes(exclude, routePath) ? (
children
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}
export default compose(
withSubscriptions(
({ isSubscriptionsInactive }) => ({ isSubscriptionsInactive }),
'main',
),
)(EnsureSubscriptionsIsInactive);

View File

@@ -1,22 +1,20 @@
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import preferencesRoutes from 'routes/preferences'
import { Route, Switch } from 'react-router-dom';
import preferencesRoutes from 'routes/preferences';
export default function DashboardContentRoute() {
return (
<Route pathname="/preferences">
<Switch>
{ preferencesRoutes.map((route, index) => (
{preferencesRoutes.map((route, index) => (
<Route
key={index}
path={`${route.path}`}
exact={route.exact}
component={route.component}
/>
/>
))}
</Switch>
</Route>
);
}
}

View File

@@ -5,6 +5,7 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function SidebarContainer({
// #ownProps
@@ -15,6 +16,9 @@ function SidebarContainer({
// #withDashboard
sidebarExpended,
// #withSubscription
isSubscriptionActive,
}) {
const sidebarScrollerRef = React.useRef();
@@ -30,8 +34,8 @@ function SidebarContainer({
}, [sidebarExpended]);
const handleSidebarMouseLeave = () => {
if (!sidebarExpended && sidebarScrollerRef.current) {
sidebarScrollerRef.current.scrollTo({ top: 0, left: 0, });
if (!sidebarExpended && sidebarScrollerRef.current) {
sidebarScrollerRef.current.scrollTo({ top: 0, left: 0 });
}
};
@@ -43,6 +47,7 @@ function SidebarContainer({
<div
className={classNames('sidebar', {
'sidebar--mini-sidebar': !sidebarExpended,
'is-subscription-inactive': !isSubscriptionActive,
})}
id="sidebar"
onMouseLeave={handleSidebarMouseLeave}
@@ -64,4 +69,8 @@ export default compose(
withDashboard(({ sidebarExpended }) => ({
sidebarExpended,
})),
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(SidebarContainer);

View File

@@ -8,6 +8,8 @@ import MenuItem from 'components/MenuItem';
import { MenuItemLabel } from 'components';
import classNames from 'classnames';
import SidebarOverlay from 'components/SidebarOverlay';
import { compose } from 'redux';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
const DEFAULT_ITEM = {
text: '',
@@ -19,12 +21,10 @@ function matchPath(pathname, path, matchExact) {
}
function SidebarMenuItemSpace({ space }) {
return (
<div class="bp3-menu-spacer" style={{ height: `${space}px` }} />
)
return <div class="bp3-menu-spacer" style={{ height: `${space}px` }} />;
}
export default function SidebarMenu() {
function SidebarMenu({ isSubscriptionActive }) {
const history = useHistory();
const location = useLocation();
@@ -92,7 +92,11 @@ export default function SidebarMenu() {
);
});
};
const items = menuItemsMapper(sidebarMenuList);
const filterItems = sidebarMenuList.filter(
(item) => isSubscriptionActive || item.enableBilling,
);
const items = menuItemsMapper(filterItems);
const handleSidebarOverlayClose = () => {
setIsOpen(false);
@@ -110,3 +114,10 @@ export default function SidebarMenu() {
</div>
);
}
export default compose(
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(SidebarMenu);

View File

@@ -0,0 +1,135 @@
import React from 'react';
import classNames from 'classnames';
import { T } from 'components';
import { saveInvoke } from 'utils';
import 'style/pages/Subscription/PlanRadio.scss';
import 'style/pages/Subscription/PlanPeriodRadio.scss';
export function SubscriptionPlans({ value, plans, onSelect }) {
const handleSelect = (value) => {
onSelect && onSelect(value);
};
return (
<div className={'plan-radios'}>
{plans.map((plan) => (
<SubscriptionPlan
name={plan.name}
description={plan.description}
slug={plan.slug}
price={plan.price}
currencyCode={plan.currencyCode}
value={plan.slug}
onSelected={handleSelect}
selectedOption={value}
/>
))}
</div>
);
}
export function SubscriptionPlan({
name,
description,
price,
currencyCode,
value,
selectedOption,
onSelected,
}) {
const handlePlanClick = () => {
saveInvoke(onSelected, value);
};
return (
<div
id={'basic-plan'}
className={classNames('plan-radio', {
'is-selected': selectedOption === value,
})}
onClick={handlePlanClick}
>
<div className={'plan-radio__header'}>
<div className={'plan-radio__name'}>{name}</div>
</div>
<div className={'plan-radio__description'}>
<ul>
{description.map((line) => (
<li>{line}</li>
))}
</ul>
</div>
<div className={'plan-radio__price'}>
<span className={'plan-radio__amount'}>
{price} {currencyCode}
</span>
<span className={'plan-radio__period'}>
<T id={'monthly'} />
</span>
</div>
</div>
);
}
/**
* Subscription periods.
*/
export function SubscriptionPeriods({ periods, selectedPeriod, onPeriodSelect }) {
const handleSelected = (value) => {
saveInvoke(onPeriodSelect, value);
};
return (
<div className={'plan-periods'}>
{periods.map((period) => (
<SubscriptionPeriod
period={period.slug}
label={period.label}
onSelected={handleSelected}
price={period.price}
selectedPeriod={selectedPeriod}
/>
))}
</div>
);
}
/**
* Billing period.
*/
export function SubscriptionPeriod({
// #ownProps
label,
selectedPeriod,
onSelected,
period,
price,
currencyCode,
}) {
const handlePeriodClick = () => {
saveInvoke(onSelected, period);
};
return (
<div
id={`plan-period-${period}`}
className={classNames(
{ 'is-selected': period === selectedPeriod },
'period-radio',
)}
onClick={handlePeriodClick}
>
<span className={'period-radio__label'}>{label}</span>
<div className={'period-radio__price'}>
<span className={'period-radio__amount'}>
{price} {currencyCode}
</span>
<span className={'period-radio__period'}>{label}</span>
</div>
</div>
);
}

View File

@@ -72,6 +72,8 @@ export * from './Details';
export * from './Drawer/DrawerInsider';
export * from './Drawer/DrawerMainTabs';
export * from './TotalLines/index'
export * from './Alert';
export * from './Subscriptions';
const Hint = FieldHint;