Skip to content

Commit

Permalink
Merge pull request novuhq#4012 from novuhq/nv-2703-sales-create-produ…
Browse files Browse the repository at this point in the history
…ct-lead-generation-assist

feat: add product lead component
  • Loading branch information
davidsoderberg authored Aug 23, 2023
2 parents 42be279 + 5bfb8d5 commit d2fbfac
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 78 deletions.
125 changes: 69 additions & 56 deletions apps/web/cypress/tests/integrations-list-modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ Cypress.on('window:before:load', (win) => {
});

describe('Integrations List Modal', function () {
let session: any;

beforeEach(function () {
cy.initializeSession().as('session');
cy.initializeSession()
.then((result) => {
session = result;
})
.as('session');
});

const navigateToGetStarted = () => {
Expand Down Expand Up @@ -301,63 +307,70 @@ describe('Integrations List Modal', function () {
});

it('should show the select provider sidebar', () => {
cy.intercept('*/integrations', async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
}).as('getIntegrations');
cy.intercept('*/environments').as('getEnvironments');

navigateToGetStarted();

cy.wait('@getIntegrations');
cy.wait('@getEnvironments');

cy.getByTestId('add-provider').should('be.enabled').click();

cy.getByTestId('select-provider-sidebar').should('be.visible').as('selectProviderSidebar');

cy.get('@selectProviderSidebar').getByTestId('sidebar-close').should('be.visible');
cy.get('@selectProviderSidebar').contains('Select a provider');
cy.get('@selectProviderSidebar').contains('Select a provider to create instance for a channel');
cy.get('@selectProviderSidebar')
.find('input[type="search"]')
.should('have.attr', 'placeholder', 'Search a provider...');

cy.get('@selectProviderSidebar').find('[role="tablist"]').as('channelTabs');
cy.get('@channelTabs').find('[data-active="true"]').contains('Email');
cy.get('@channelTabs').contains('In-App');
cy.get('@channelTabs').contains('Email');
cy.get('@channelTabs').contains('Chat');
cy.get('@channelTabs').contains('Push');
cy.get('@channelTabs').contains('SMS');

cy.getByTestId('providers-group-in_app').contains('In-App').as('inAppGroup');
cy.getByTestId('providers-group-email').contains('Email').as('emailGroup');
cy.getByTestId('providers-group-chat').contains('Chat').as('chatGroup');
cy.getByTestId('providers-group-push').contains('Push').as('pushGroup');
cy.getByTestId('providers-group-sms').contains('SMS').as('smsGroup');

inAppProviders.forEach((provider) => {
cy.get('@inAppGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});
emailProviders
.filter((provider) => provider.id !== EmailProviderIdEnum.Novu)
.forEach((provider) => {
cy.get('@emailGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
cy.task('deleteProvider', {
providerId: InAppProviderIdEnum.Novu,
channel: ChannelTypeEnum.IN_APP,
environmentId: session.environment.id,
organizationId: session.organization.id,
}).then(() => {
cy.intercept('*/integrations', async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
}).as('getIntegrations');
cy.intercept('*/environments').as('getEnvironments');

navigateToGetStarted();

cy.wait('@getIntegrations');
cy.wait('@getEnvironments');

cy.getByTestId('add-provider').should('be.enabled').click();

cy.getByTestId('select-provider-sidebar').should('be.visible').as('selectProviderSidebar');

cy.get('@selectProviderSidebar').getByTestId('sidebar-close').should('be.visible');
cy.get('@selectProviderSidebar').contains('Select a provider');
cy.get('@selectProviderSidebar').contains('Select a provider to create instance for a channel');
cy.get('@selectProviderSidebar')
.find('input[type="search"]')
.should('have.attr', 'placeholder', 'Search a provider...');

cy.get('@selectProviderSidebar').find('[role="tablist"]').as('channelTabs');
cy.get('@channelTabs').find('[data-active="true"]').contains('Email');
cy.get('@channelTabs').contains('In-App');
cy.get('@channelTabs').contains('Email');
cy.get('@channelTabs').contains('Chat');
cy.get('@channelTabs').contains('Push');
cy.get('@channelTabs').contains('SMS');

cy.getByTestId('providers-group-in_app').contains('In-App').as('inAppGroup');
cy.getByTestId('providers-group-email').contains('Email').as('emailGroup');
cy.getByTestId('providers-group-chat').contains('Chat').as('chatGroup');
cy.getByTestId('providers-group-push').contains('Push').as('pushGroup');
cy.getByTestId('providers-group-sms').contains('SMS').as('smsGroup');

inAppProviders.forEach((provider) => {
cy.get('@inAppGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});
chatProviders.forEach((provider) => {
cy.get('@chatGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});
pushProviders.forEach((provider) => {
cy.get('@pushGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});
smsProviders
.filter((provider) => provider.id !== SmsProviderIdEnum.Novu)
.forEach((provider) => {
cy.get('@smsGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
emailProviders
.filter((provider) => provider.id !== EmailProviderIdEnum.Novu)
.forEach((provider) => {
cy.get('@emailGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});
chatProviders.forEach((provider) => {
cy.get('@chatGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});

cy.getByTestId('select-provider-sidebar-cancel').contains('Cancel');
cy.getByTestId('select-provider-sidebar-next').should('be.disabled').contains('Next');
pushProviders.forEach((provider) => {
cy.get('@pushGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});
smsProviders
.filter((provider) => provider.id !== SmsProviderIdEnum.Novu)
.forEach((provider) => {
cy.get('@smsGroup').getByTestId(`provider-${provider.id}`).contains(provider.displayName);
});

cy.getByTestId('select-provider-sidebar-cancel').contains('Cancel');
cy.getByTestId('select-provider-sidebar-next').should('be.disabled').contains('Next');
});
});

it('should allow for searching', () => {
Expand Down
140 changes: 140 additions & 0 deletions apps/web/src/components/utils/ProductLead.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ActionIcon, Group, Title, useMantineTheme } from '@mantine/core';
import { useLocalStorage } from '@mantine/hooks';
import { CSSProperties, ReactNode, useEffect } from 'react';
import { IS_DOCKER_HOSTED } from '../../config';
import { Button, colors, Text } from '../../design-system';
import { Calendar, Close } from '../../design-system/icons';
import { useAuthContext } from '../providers/AuthProvider';
import { useSegment } from '../providers/SegmentProvider';
import { When } from './When';

export enum ProductLeadVariants {
DEFAULT = 'default',
COLUMN = 'column',
}

const Wrapper = ({ children, variant, id }: { children: any; variant: ProductLeadVariants; id: string }) => {
const segment = useSegment();

useEffect(() => {
segment.track('Banner seen - [Product lead]', {
id,
});
}, []);

return variant === ProductLeadVariants.COLUMN ? (
<Group position="apart" align="center">
{children}
</Group>
) : (
children
);
};

export const ProductLead = ({
title,
text,
closeable = true,
icon = null,
id,
variant = ProductLeadVariants.DEFAULT,
style = {},
}: {
title: string;
text: string;
closeable?: boolean;
icon?: ReactNode;
id: string;
variant?: ProductLeadVariants;
style?: CSSProperties;
}) => {
const { currentUser } = useAuthContext();
const [open, setOpen] = useLocalStorage<boolean>({
key: id,
defaultValue: true,
getInitialValueInEffect: true,
});

const theme = useMantineTheme();
const dark = theme.colorScheme === 'dark';
const isSelfHosted = IS_DOCKER_HOSTED;
const segment = useSegment();

if (open === false) {
return null;
}

return (
<div
style={{
padding: 24,
background: dark ? colors.B20 : colors.B98,
borderRadius: 8,
color: colors.B60,
...style,
}}
>
<Wrapper variant={variant} id={id}>
<div>
<Group position="apart">
<Group spacing={8}>
{icon}
<Title size={18} color={colors.B60}>
{title}
</Title>
</Group>
<When truthy={closeable && variant === ProductLeadVariants.COLUMN}>
<ActionIcon
variant={'transparent'}
onClick={() => {
setOpen(false);
segment.track('Banner hidden - [Product lead]', {
id,
});
}}
>
<Close color={colors.B60} />
</ActionIcon>
</When>
</Group>
<Text mt={4} color={colors.B60}>
{text}
</Text>
</div>
<Group spacing={24}>
<Button
mt={variant === ProductLeadVariants.COLUMN ? 16 : undefined}
onClick={() => {
segment.track('Scheduled call clicked - [Product lead]', {
id,
});
window.open(
`https://calendly.com/novuhq/novu-meeting?full_name=${currentUser?.firstName}&email=${
currentUser?.email
}&utm_campaign=${id}&utm_source=${isSelfHosted ? 'self-hosted' : 'cloud'}`
);
}}
variant="outline"
>
<Group spacing={8}>
<Calendar color={dark ? theme.white : colors.B60} /> Schedule a call
</Group>
</Button>
<When truthy={closeable && variant === ProductLeadVariants.DEFAULT}>
<ActionIcon
variant={'transparent'}
onClick={() => {
setOpen(false);
segment.track('Banner hidden - [Product lead]', {
id,
});
}}
>
<Close color={colors.B60} />
</ActionIcon>
</When>
</Group>
</Wrapper>
</div>
);
};
11 changes: 11 additions & 0 deletions apps/web/src/design-system/icons/general/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function Calendar(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" fill="none" {...props}>
<path
// eslint-disable-next-line max-len
d="M4.40377 10.6C3.95683 10.6 3.5778 10.4457 3.26669 10.1371C2.95558 9.82846 2.80002 9.45068 2.80002 9.00373C2.80002 8.55678 2.95433 8.17775 3.26294 7.86664C3.57154 7.55553 3.94932 7.39998 4.39627 7.39998C4.84322 7.39998 5.22225 7.55428 5.53336 7.86289C5.84447 8.17149 6.00002 8.54927 6.00002 8.99623C6.00002 9.44317 5.84572 9.8222 5.53711 10.1333C5.22851 10.4444 4.85073 10.6 4.40377 10.6ZM1.60002 13.4C1.27002 13.4 0.987524 13.2805 0.752524 13.0416C0.517524 12.8028 0.400024 12.5222 0.400024 12.2V3.39998C0.400024 3.07775 0.517524 2.7972 0.752524 2.55831C0.987524 2.31942 1.27002 2.19998 1.60002 2.19998H2.80002V0.599976H4.00002V2.19998H8.00002V0.599976H9.20002V2.19998H10.4C10.73 2.19998 11.0125 2.31942 11.2475 2.55831C11.4825 2.7972 11.6 3.07775 11.6 3.39998V12.2C11.6 12.5222 11.4825 12.8028 11.2475 13.0416C11.0125 13.2805 10.73 13.4 10.4 13.4H1.60002ZM1.60002 12.2H10.4V6.19998H1.60002V12.2ZM1.60002 4.99998H10.4V3.39998H1.60002V4.99998Z"
fill="currentColor"
/>
</svg>
);
}
11 changes: 11 additions & 0 deletions apps/web/src/design-system/icons/general/Cloud.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function Cloud(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="16" viewBox="0 0 22 16" fill="none" {...props}>
<path
// eslint-disable-next-line max-len
d="M5.5 16C3.98333 16 2.6875 15.475 1.6125 14.425C0.5375 13.375 0 12.0917 0 10.575C0 9.275 0.391667 8.11667 1.175 7.1C1.95833 6.08333 2.98333 5.43333 4.25 5.15C4.66667 3.61667 5.5 2.375 6.75 1.425C8 0.475 9.41667 0 11 0C12.95 0 14.6042 0.679167 15.9625 2.0375C17.3208 3.39583 18 5.05 18 7C19.15 7.13333 20.1042 7.62917 20.8625 8.4875C21.6208 9.34583 22 10.35 22 11.5C22 12.75 21.5625 13.8125 20.6875 14.6875C19.8125 15.5625 18.75 16 17.5 16H5.5ZM5.5 14H17.5C18.2 14 18.7917 13.7583 19.275 13.275C19.7583 12.7917 20 12.2 20 11.5C20 10.8 19.7583 10.2083 19.275 9.725C18.7917 9.24167 18.2 9 17.5 9H16V7C16 5.61667 15.5125 4.4375 14.5375 3.4625C13.5625 2.4875 12.3833 2 11 2C9.61667 2 8.4375 2.4875 7.4625 3.4625C6.4875 4.4375 6 5.61667 6 7H5.5C4.53333 7 3.70833 7.34167 3.025 8.025C2.34167 8.70833 2 9.53333 2 10.5C2 11.4667 2.34167 12.2917 3.025 12.975C3.70833 13.6583 4.53333 14 5.5 14Z"
fill="#828299"
/>
</svg>
);
}
18 changes: 18 additions & 0 deletions apps/web/src/design-system/icons/general/SSO.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function SSO(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 18" fill="none" {...props}>
<path
// eslint-disable-next-line max-len
d="M1.6125 14.425C2.6875 15.475 4 16 5 16V14C4.25 14 3.70833 13.6583 3.025 12.975C2.34167 12.2917 2 11.4667 2 10.5C2 9.53333 2.34167 8.70833 3.025 8.025C3.70833 7.34167 4.53333 7 5.5 7H6C6 5.61667 6.4875 4.4375 7.4625 3.4625C8.4375 2.4875 9.61667 2 11 2C12.3833 2 13.5625 2.4875 14.5375 3.4625C15.5125 4.4375 16 5.61667 16 7V9H17.5C18.2 9 18.7917 9.24167 19.275 9.725C19.7583 10.2083 20 10.8 20 11.5H22C22 10.35 21.6208 9.34583 20.8625 8.4875C20.1042 7.62917 19.15 7.13333 18 7C18 5.05 17.3208 3.39583 15.9625 2.0375C14.6042 0.679167 12.95 0 11 0C9.41667 0 8 0.475 6.75 1.425C5.5 2.375 4.66667 3.61667 4.25 5.15C2.98333 5.43333 1.95833 6.08333 1.175 7.1C0.391667 8.11667 0 9.275 0 10.575C0 12.0917 0.5375 13.375 1.6125 14.425Z"
fill="#828299"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
// eslint-disable-next-line max-len
d="M8.05281 16.9792C8.75468 17.6597 9.60695 18 10.6096 18C11.4218 18 12.1312 17.7788 12.7378 17.3365C13.3444 16.8941 13.768 16.3375 14.0087 15.6667H19.3904V18H21.7968V15.6667H24V13.3333H14.0087C13.768 12.6625 13.3444 12.1059 12.7378 11.6635C12.1312 11.2212 11.4218 11 10.6096 11C9.60695 11 8.75468 11.3403 8.05281 12.0208C7.35094 12.7014 7 13.5278 7 14.5C7 15.4722 7.35094 16.2986 8.05281 16.9792ZM10.6001 16.6C11.7047 16.6 12.6001 15.7046 12.6001 14.6C12.6001 13.4954 11.7047 12.6 10.6001 12.6C9.49553 12.6 8.6001 13.4954 8.6001 14.6C8.6001 15.7046 9.49553 16.6 10.6001 16.6Z"
fill="#828299"
/>
</svg>
);
}
11 changes: 11 additions & 0 deletions apps/web/src/design-system/icons/general/Translate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function Translate(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="16" viewBox="0 0 18 16" fill="none" {...props}>
<path
// eslint-disable-next-line max-len
d="M9.16667 16L12.7292 6.5H14.4375L18 16H16.3542L15.5 13.5625H11.6667L10.8125 16H9.16667ZM12.1667 12.1875H15L13.625 8.25H13.5417L12.1667 12.1875ZM2.52083 13.5L1.47917 12.4375L5.47917 8.47917C4.97917 7.95139 4.51389 7.40278 4.08333 6.83333C3.65278 6.26389 3.27778 5.64583 2.95833 4.97917H4.66667C4.91667 5.42361 5.20139 5.84028 5.52083 6.22917C5.84028 6.61806 6.16667 7 6.5 7.375C7.02778 6.79167 7.51389 6.18403 7.95833 5.55208C8.40278 4.92014 8.76389 4.23611 9.04167 3.5H0V2H5.75V0H7.25V2H13V3.5H10.625C10.3333 4.45833 9.90972 5.34375 9.35417 6.15625C8.79861 6.96875 8.1875 7.74306 7.52083 8.47917L9.41667 10.3542L8.83333 11.8958L6.5 9.5625L2.52083 13.5Z"
fill="#828299"
/>
</svg>
);
}
11 changes: 11 additions & 0 deletions apps/web/src/design-system/icons/general/UserAccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function UserAccess(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="17" viewBox="0 0 18 17" fill="none" {...props}>
<path
// eslint-disable-next-line max-len
d="M8 8C6.9 8 5.95833 7.60833 5.175 6.825C4.39167 6.04167 4 5.1 4 4C4 2.9 4.39167 1.95833 5.175 1.175C5.95833 0.391667 6.9 0 8 0C9.1 0 10.0417 0.391667 10.825 1.175C11.6083 1.95833 12 2.9 12 4C12 5.1 11.6083 6.04167 10.825 6.825C10.0417 7.60833 9.1 8 8 8ZM15.25 17L14 15.5V11.825C13.4 11.5917 12.9167 11.2208 12.55 10.7125C12.1833 10.2042 12 9.63333 12 9C12 8.16667 12.2917 7.45833 12.875 6.875C13.4583 6.29167 14.1667 6 15 6C15.8333 6 16.5417 6.29167 17.125 6.875C17.7083 7.45833 18 8.16667 18 9C18 9.63333 17.8167 10.2042 17.45 10.7125C17.0833 11.2208 16.6 11.5917 16 11.825V12L17 13L16 14L17 15L15.25 17ZM15 10.5C15.4167 10.5 15.7708 10.3542 16.0625 10.0625C16.3542 9.77083 16.5 9.41667 16.5 9C16.5 8.58333 16.3542 8.22917 16.0625 7.9375C15.7708 7.64583 15.4167 7.5 15 7.5C14.5833 7.5 14.2292 7.64583 13.9375 7.9375C13.6458 8.22917 13.5 8.58333 13.5 9C13.5 9.41667 13.6458 9.77083 13.9375 10.0625C14.2292 10.3542 14.5833 10.5 15 10.5ZM10.025 9.15C10.0583 9.91667 10.2375 10.6375 10.5625 11.3125C10.8875 11.9875 11.3667 12.5417 12 12.975V16H0V13.225C0 12.6583 0.141667 12.1333 0.425 11.65C0.708333 11.1667 1.1 10.8 1.6 10.55C2.6 10.05 3.6375 9.66667 4.7125 9.4C5.7875 9.13333 6.88333 9 8 9C8.33333 9 8.67083 9.0125 9.0125 9.0375C9.35417 9.0625 9.69167 9.1 10.025 9.15Z"
fill="#828299"
/>
</svg>
);
}
5 changes: 5 additions & 0 deletions apps/web/src/design-system/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export { BoltFilled } from './general/BoltFilled';
export { BoltOffFilled } from './general/BoltOffFilled';
export { Question } from './general/Question';
export { ProviderMissing } from './general/ProviderMissing';
export { Calendar } from './general/Calendar';
export { Translate } from './general/Translate';
export { UserAccess } from './general/UserAccess';
export { SSO } from './general/SSO';
export { Cloud } from './general/Cloud';

export { Copy } from './actions/Copy';
export { Close } from './actions/Close';
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/pages/invites/MembersInvitePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import {
} from '../../api/organization';
import { MembersTable } from './components/MembersTable';
import { Button, Input } from '../../design-system';
import { Invite } from '../../design-system/icons';
import { Invite, UserAccess } from '../../design-system/icons';
import { useAuthContext } from '../../components/providers/AuthProvider';
import { parseUrl } from '../../utils/routeUtils';
import { ROUTES } from '../../constants/routes.enum';
import { ProductLead } from '../../components/utils/ProductLead';

export function MembersInvitePage() {
const [form] = Form.useForm();
Expand Down Expand Up @@ -197,6 +198,12 @@ export function MembersInvitePage() {
onResendInviteMember={resendInviteMemberClick}
onChangeMemberRole={changeMemberRoleClick}
/>
<ProductLead
icon={<UserAccess />}
id="rbac-team-page"
title="Role-based access control"
text="Securely manage users' permissions to access system resources."
/>
</Container>
</PageContainer>
);
Expand Down
Loading

0 comments on commit d2fbfac

Please sign in to comment.