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:
@ -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 />}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -17,6 +17,7 @@ export enum SettingsPath {
|
||||
Developers = '',
|
||||
DevelopersNewApiKey = 'api-keys/new',
|
||||
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
|
||||
Integrations = 'integrations',
|
||||
DevelopersNewWebhook = 'webhooks/new',
|
||||
DevelopersNewWebhookDetail = 'webhooks/:webhookId',
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ export {
|
||||
IconAt,
|
||||
IconBaselineDensitySmall,
|
||||
IconBell,
|
||||
IconBolt,
|
||||
IconBox,
|
||||
IconBrandGithub,
|
||||
IconBrandGoogle,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export type FeatureFlagKey =
|
||||
| 'IS_MESSAGING_ENABLED'
|
||||
| 'IS_INTEGRATIONS_ENABLED'
|
||||
| 'IS_QUICK_ACTIONS_ENABLED'
|
||||
| 'IS_NEW_RECORD_BOARD_ENABLED';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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[];
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user