3865-Add-Integrations (#3870)

* initial commit setup

* ui done

* added links

* changed brand logos

* Twenty logo fix

* Windmill logo fix

* Fix typo

* Add feature flag

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Kanav Arora 2024-02-14 03:41:05 +05:30 committed by GitHub
parent 15a5fec545
commit 7b88e5bdaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 356 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="96px" height="96px" viewBox="0 0 96 96" version="1.1">
<defs>
<clipPath id="clip1">
<path d="M 11.292969 0 L 84.707031 0 C 90.945312 0 96 5.054688 96 11.292969 L 96 84.707031 C 96 90.945312 90.945312 96 84.707031 96 L 11.292969 96 C 5.054688 96 0 90.945312 0 84.707031 L 0 11.292969 C 0 5.054688 5.054688 0 11.292969 0 Z M 11.292969 0 "/>
</clipPath>
<clipPath id="clip2">
<path d="M 19 26 L 48 26 L 48 45 L 19 45 Z M 19 26 "/>
</clipPath>
<clipPath id="clip3">
<path d="M 11.292969 0 L 84.707031 0 C 90.945312 0 96 5.054688 96 11.292969 L 96 84.707031 C 96 90.945312 90.945312 96 84.707031 96 L 11.292969 96 C 5.054688 96 0 90.945312 0 84.707031 L 0 11.292969 C 0 5.054688 5.054688 0 11.292969 0 Z M 11.292969 0 "/>
</clipPath>
<clipPath id="clip4">
<path d="M 19 26 L 77 26 L 77 70 L 19 70 Z M 19 26 "/>
</clipPath>
<clipPath id="clip5">
<path d="M 11.292969 0 L 84.707031 0 C 90.945312 0 96 5.054688 96 11.292969 L 96 84.707031 C 96 90.945312 90.945312 96 84.707031 96 L 11.292969 96 C 5.054688 96 0 90.945312 0 84.707031 L 0 11.292969 C 0 5.054688 5.054688 0 11.292969 0 Z M 11.292969 0 "/>
</clipPath>
</defs>
<g id="surface1">
<g clip-path="url(#clip1)" clip-rule="nonzero">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 96 0 L 0 0 L 0 96 L 96 96 Z M 19.25 35.746094 C 19.25 30.5 23.507812 26.246094 28.75 26.246094 L 47.039062 26.246094 C 47.308594 26.246094 47.554688 26.40625 47.667969 26.652344 C 47.78125 26.902344 47.730469 27.191406 47.546875 27.394531 L 43.539062 31.75 C 42.839844 32.507812 41.859375 32.945312 40.828125 32.945312 L 28.800781 32.945312 C 27.226562 32.945312 25.949219 34.222656 25.949219 35.796875 L 25.949219 42.980469 C 25.949219 43.90625 25.199219 44.652344 24.273438 44.652344 L 20.929688 44.652344 C 20.003906 44.652344 19.257812 43.90625 19.257812 42.980469 L 19.257812 35.746094 Z M 76.152344 60.253906 C 76.152344 65.5 71.894531 69.753906 66.648438 69.753906 L 58.878906 69.753906 C 53.632812 69.753906 49.375 65.5 49.375 60.253906 L 49.375 46.652344 C 49.375 45.726562 49.722656 44.835938 50.351562 44.152344 L 54.882812 39.234375 C 55.074219 39.027344 55.371094 38.957031 55.636719 39.054688 C 55.898438 39.164062 56.074219 39.410156 56.074219 39.691406 L 56.074219 60.210938 C 56.074219 61.785156 57.351562 63.0625 58.925781 63.0625 L 66.605469 63.0625 C 68.179688 63.0625 69.457031 61.785156 69.457031 60.210938 L 69.457031 35.796875 C 69.457031 34.222656 68.179688 32.945312 66.605469 32.945312 L 57.675781 32.945312 C 56.652344 32.945312 55.679688 33.375 54.980469 34.121094 L 28.347656 63.0625 L 44.351562 63.0625 C 45.273438 63.0625 46.023438 63.8125 46.023438 64.738281 L 46.023438 68.082031 C 46.023438 69.007812 45.273438 69.753906 44.351562 69.753906 L 22.785156 69.753906 C 20.832031 69.753906 19.242188 68.167969 19.242188 66.210938 L 19.242188 64.441406 C 19.242188 63.550781 19.574219 62.695312 20.179688 62.039062 L 50.039062 29.605469 C 52.015625 27.457031 54.789062 26.246094 57.707031 26.246094 L 66.640625 26.246094 C 71.886719 26.246094 76.144531 30.5 76.144531 35.746094 L 76.144531 60.253906 Z M 76.152344 60.253906 "/>
</g>
<g clip-path="url(#clip2)" clip-rule="nonzero">
<g clip-path="url(#clip3)" clip-rule="nonzero">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 19.25 35.746094 C 19.25 30.5 23.507812 26.246094 28.75 26.246094 L 47.039062 26.246094 C 47.308594 26.246094 47.554688 26.40625 47.667969 26.652344 C 47.78125 26.902344 47.730469 27.191406 47.546875 27.394531 L 43.539062 31.75 C 42.839844 32.507812 41.859375 32.945312 40.828125 32.945312 L 28.800781 32.945312 C 27.226562 32.945312 25.949219 34.222656 25.949219 35.796875 L 25.949219 42.980469 C 25.949219 43.90625 25.199219 44.652344 24.273438 44.652344 L 20.929688 44.652344 C 20.003906 44.652344 19.257812 43.90625 19.257812 42.980469 L 19.257812 35.746094 Z M 19.25 35.746094 "/>
</g>
</g>
<g clip-path="url(#clip4)" clip-rule="nonzero">
<g clip-path="url(#clip5)" clip-rule="nonzero">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 76.152344 60.253906 C 76.152344 65.5 71.894531 69.753906 66.648438 69.753906 L 58.878906 69.753906 C 53.632812 69.753906 49.375 65.5 49.375 60.253906 L 49.375 46.652344 C 49.375 45.726562 49.722656 44.835938 50.351562 44.152344 L 54.882812 39.234375 C 55.074219 39.027344 55.371094 38.957031 55.636719 39.054688 C 55.898438 39.164062 56.074219 39.410156 56.074219 39.691406 L 56.074219 60.210938 C 56.074219 61.785156 57.351562 63.0625 58.925781 63.0625 L 66.605469 63.0625 C 68.179688 63.0625 69.457031 61.785156 69.457031 60.210938 L 69.457031 35.796875 C 69.457031 34.222656 68.179688 32.945312 66.605469 32.945312 L 57.675781 32.945312 C 56.652344 32.945312 55.679688 33.375 54.980469 34.121094 L 28.347656 63.0625 L 44.351562 63.0625 C 45.273438 63.0625 46.023438 63.8125 46.023438 64.738281 L 46.023438 68.082031 C 46.023438 69.007812 45.273438 69.753906 44.351562 69.753906 L 22.785156 69.753906 C 20.832031 69.753906 19.242188 68.167969 19.242188 66.210938 L 19.242188 64.441406 C 19.242188 63.550781 19.574219 62.695312 20.179688 62.039062 L 50.039062 29.605469 C 52.015625 27.457031 54.789062 26.246094 57.707031 26.246094 L 66.640625 26.246094 C 71.886719 26.246094 76.144531 30.5 76.144531 35.746094 L 76.144531 60.253906 Z M 76.152344 60.253906 "/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -35,6 +35,7 @@ import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-ke
import { SettingsDevelopers } from '~/pages/settings/developers/SettingsDevelopers';
import { SettingsDevelopersWebhooksDetail } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail';
import { SettingsDevelopersWebhooksNew } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew';
import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations';
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
@ -153,6 +154,10 @@ export const App = () => {
</Routes>
}
/>
<Route
path={SettingsPath.Integrations}
element={<SettingsIntegrations />}
/>
<Route
path={SettingsPath.ObjectNewFieldStep1}
element={<SettingsObjectNewFieldStep1 />}

View File

@ -4,6 +4,7 @@ import { useMatch, useNavigate, useResolvedPath } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { AppPath } from '@/types/AppPath';
import {
IconApps,
IconAt,
IconCalendarEvent,
IconColorSwatch,
@ -31,6 +32,12 @@ export const SettingsNavigationDrawerItems = () => {
}, [signOut, navigate]);
const isMessagingEnabled = useIsFeatureEnabled('IS_MESSAGING_ENABLED');
const isIntegrationsEnabled = useIsFeatureEnabled('IS_INTEGRATIONS_ENABLED');
const isIntegrationsItemActive = !!useMatch({
path: useResolvedPath('/settings/integrations').pathname,
end: true,
});
const isAccountsItemActive = !!useMatch({
path: useResolvedPath('/settings/accounts').pathname,
end: true,
@ -137,6 +144,14 @@ export const SettingsNavigationDrawerItems = () => {
})
}
/>
{isIntegrationsEnabled && (
<NavigationDrawerItem
label="Integrations"
to="/settings/integrations"
Icon={IconApps}
active={isIntegrationsItemActive}
/>
)}
</NavigationDrawerSection>
<NavigationDrawerSection>

View File

@ -17,6 +17,7 @@ export enum SettingsPath {
Developers = '',
DevelopersNewApiKey = 'api-keys/new',
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
Integrations = 'integrations',
DevelopersNewWebhook = 'webhooks/new',
DevelopersNewWebhookDetail = 'webhooks/:webhookId',
}

View File

@ -16,6 +16,7 @@ export {
IconAt,
IconBaselineDensitySmall,
IconBell,
IconBolt,
IconBox,
IconBrandGithub,
IconBrandGoogle,

View File

@ -1,4 +1,5 @@
export type FeatureFlagKey =
| 'IS_MESSAGING_ENABLED'
| 'IS_INTEGRATIONS_ENABLED'
| 'IS_QUICK_ACTIONS_ENABLED'
| 'IS_NEW_RECORD_BOARD_ENABLED';

View File

@ -0,0 +1,104 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { SoonPill } from 'tsup.ui.index';
import { IconArrowUpRight, IconBolt } from '@/ui/display/icon';
import { Integration, IntegrationType } from './constants/IntegrationTypes';
interface SettingsIntegrationComponentProps {
integration: Integration;
}
const StyledContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
font-size: ${({ theme }) => theme.font.size.md};
display: flex;
flex-direction: row;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(3)};
`;
const StyledSection = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(3)};
`;
const StyledIntegrationLogo = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
color: ${({ theme }) => theme.border.color.strong};
`;
const StyledButton = styled.button`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
cursor: pointer;
`;
const StyledSoonPill = styled(SoonPill)`
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
`;
const StyledLogo = styled.img`
height: 24px;
width: 24px;
`;
export const SettingsIntegrationComponent = ({
integration,
}: SettingsIntegrationComponentProps) => {
const theme = useTheme();
const openLinkInTab = (link: string) => {
window.open(link);
};
return (
<StyledContainer>
<StyledSection>
<StyledIntegrationLogo>
<StyledLogo src={integration.from.image} alt={integration.from.key} />
{integration.to ? (
<>
<div></div>
<StyledLogo src={integration.to.image} alt={integration.to.key} />
</>
) : (
<></>
)}
</StyledIntegrationLogo>
{integration.text}
</StyledSection>
{integration.type === IntegrationType.Soon ? (
<StyledSoonPill />
) : (
<StyledButton onClick={() => openLinkInTab(integration.link)}>
{integration.type === IntegrationType.Use ? (
<IconBolt size={theme.icon.size.md} />
) : (
<IconArrowUpRight size={theme.icon.size.md} />
)}
{integration.type === IntegrationType.Goto ? (
<div>{integration.linkText}</div>
) : (
<div>Use</div>
)}
</StyledButton>
)}
</StyledContainer>
);
};

View File

@ -0,0 +1,61 @@
import styled from '@emotion/styled';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Section } from '@/ui/layout/section/components/Section';
import { SettingsIntegrationComponent } from '~/pages/settings/integrations/SettingsIntegrationComponent';
import { IntegrationCategory } from './constants/IntegrationTypes';
interface SettingsIntegrationGroupProps {
integrationGroup: IntegrationCategory;
}
const StyledIntegrationGroupHeader = styled.div`
align-items: start;
display: flex;
flex-direction: row;
justify-content: space-between;
`;
const StyledGroupLink = styled.div`
align-items: start;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
cursor: pointer;
`;
const StyledIntegrationsSection = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsIntegrationGroup = ({
integrationGroup,
}: SettingsIntegrationGroupProps) => {
const openLinkInTab = (link: string) => {
window.open(link);
};
return (
<Section>
<StyledIntegrationGroupHeader>
<H2Title title={integrationGroup.title} />
{integrationGroup.hyperlink && (
<StyledGroupLink
onClick={() => openLinkInTab(integrationGroup.hyperlink ?? '')}
>
<div>{integrationGroup.hyperlinkText}</div>
<div></div>
</StyledGroupLink>
)}
</StyledIntegrationGroupHeader>
<StyledIntegrationsSection>
{integrationGroup.integrations.map((integration) => {
return <SettingsIntegrationComponent integration={integration} />;
})}
</StyledIntegrationsSection>
</Section>
);
};

View File

@ -0,0 +1,20 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { IconSettings } from '@/ui/display/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { SettingsIntegrationGroup } from '~/pages/settings/integrations/SettingsIntegrationGroup';
import integrationCategories from './constants/Integrations';
export const SettingsIntegrations = () => {
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<Breadcrumb links={[{ children: 'Integrations' }]} />
{integrationCategories.map((group) => {
return <SettingsIntegrationGroup integrationGroup={group} />;
})}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,22 @@
export enum IntegrationType {
Use = 'Use',
Goto = 'Goto',
Soon = 'Soon',
}
export interface Integration {
from: { key: string; image: string };
to: { key: string; image: string } | null;
type: IntegrationType;
linkText?: string;
link: string;
text: string;
}
export interface IntegrationCategory {
key: string;
title: string;
hyperlinkText?: string;
hyperlink: string | null;
integrations: Integration[];
}

View File

@ -0,0 +1,12 @@
import { IntegrationCategory } from './IntegrationTypes';
import requestIntegrations from './Request';
import windmillIntegrations from './Windmill';
import zapierIntegrations from './Zapier';
const integrationCategories: IntegrationCategory[] = [
zapierIntegrations,
windmillIntegrations,
requestIntegrations,
];
export default integrationCategories;

View File

@ -0,0 +1,19 @@
import { IntegrationCategory, IntegrationType } from './IntegrationTypes';
const requestIntegrations: IntegrationCategory = {
key: 'request',
title: 'Request an integration',
hyperlink: null,
integrations: [
{
from: { key: 'github', image: '/images/integrations/Github.png' },
to: null,
type: IntegrationType.Goto,
text: 'Request an integration on Github conversations',
link: 'https://github.com/twentyhq/twenty/issues/new/choose',
linkText: 'Go to GitHub',
},
],
};
export default requestIntegrations;

View File

@ -0,0 +1,19 @@
import { IntegrationCategory, IntegrationType } from './IntegrationTypes';
const windmillIntegrations: IntegrationCategory = {
key: 'windmill',
title: 'With Windmill',
hyperlink: null,
integrations: [
{
from: { key: 'windmill', image: '/images/integrations/Windmill.png' },
to: null,
type: IntegrationType.Goto,
text: 'Create a workflow with Windmill',
link: 'https://www.windmill.dev',
linkText: 'Go to Windmill',
},
],
};
export default windmillIntegrations;

View File

@ -0,0 +1,40 @@
import { IntegrationCategory, IntegrationType } from './IntegrationTypes';
const zapierIntegrations: IntegrationCategory = {
key: 'zapier',
title: 'With Zapier',
hyperlinkText: 'See all zaps',
hyperlink: 'https://zapier.com/apps/twenty/integrations',
integrations: [
{
from: { key: 'twenty', image: '/images/integrations/Twenty.svg' },
to: { key: 'slack', image: '/images/integrations/Slack.png' },
type: IntegrationType.Use,
text: 'Post to Slack when a company is updated',
link: 'https://zapier.com/apps/twenty/integrations/slack',
},
{
from: { key: 'cal', image: '/images/integrations/Cal.png' },
to: { key: 'twenty', image: '/images/integrations/Twenty.svg' },
type: IntegrationType.Use,
text: 'Create a person when Cal.com event is created',
link: 'https://zapier.com/apps/twenty/integrations/calcom',
},
{
from: { key: 'mailchimp', image: '/images/integrations/MailChimp.png' },
to: { key: 'twenty', image: '/images/integrations/Twenty.svg' },
type: IntegrationType.Use,
text: 'Create a person when a MailChimp sub is created',
link: 'https://zapier.com/apps/twenty/integrations/mailchimp',
},
{
from: { key: 'tally', image: '/images/integrations/Tally.png' },
to: { key: 'twenty', image: '/images/integrations/Twenty.svg' },
type: IntegrationType.Use,
text: 'Create a company when a Tally form is sent',
link: 'https://zapier.com/apps/twenty/integrations/tally',
},
],
};
export default zapierIntegrations;

View File

@ -14,6 +14,7 @@ import { IDField } from '@ptc-org/nestjs-query-graphql';
import { Workspace } from 'src/core/workspace/workspace.entity';
export enum FeatureFlagKeys {
IsIntegrationsEnabled = 'IS_INTEGRATIONS_ENABLED',
IsMessagingEnabled = 'IS_MESSAGING_ENABLED',
IsWorkspaceCleanable = 'IS_WORKSPACE_CLEANABLE',
IsNewRecordBoardEnabled = 'IS_NEW_RECORD_BOARD_ENABLED',