diff --git a/packages/twenty-front/public/images/integrations/Cal.png b/packages/twenty-front/public/images/integrations/Cal.png new file mode 100644 index 000000000..7941af38e Binary files /dev/null and b/packages/twenty-front/public/images/integrations/Cal.png differ diff --git a/packages/twenty-front/public/images/integrations/Github.png b/packages/twenty-front/public/images/integrations/Github.png new file mode 100644 index 000000000..a8aff42a6 Binary files /dev/null and b/packages/twenty-front/public/images/integrations/Github.png differ diff --git a/packages/twenty-front/public/images/integrations/MailChimp.png b/packages/twenty-front/public/images/integrations/MailChimp.png new file mode 100644 index 000000000..67018e75e Binary files /dev/null and b/packages/twenty-front/public/images/integrations/MailChimp.png differ diff --git a/packages/twenty-front/public/images/integrations/Slack.png b/packages/twenty-front/public/images/integrations/Slack.png new file mode 100644 index 000000000..8fe767eda Binary files /dev/null and b/packages/twenty-front/public/images/integrations/Slack.png differ diff --git a/packages/twenty-front/public/images/integrations/Tally.png b/packages/twenty-front/public/images/integrations/Tally.png new file mode 100644 index 000000000..46d6cc6c8 Binary files /dev/null and b/packages/twenty-front/public/images/integrations/Tally.png differ diff --git a/packages/twenty-front/public/images/integrations/Twenty.svg b/packages/twenty-front/public/images/integrations/Twenty.svg new file mode 100644 index 000000000..bb5819bc2 --- /dev/null +++ b/packages/twenty-front/public/images/integrations/Twenty.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-front/public/images/integrations/Windmill.png b/packages/twenty-front/public/images/integrations/Windmill.png new file mode 100644 index 000000000..4f10ae59f Binary files /dev/null and b/packages/twenty-front/public/images/integrations/Windmill.png differ diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index 801fd04c5..bdf99f5bd 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -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 = () => { } /> + } + /> } diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx index 99db28f34..d5ae7b3a7 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx @@ -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 && ( + + )} diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts index f2ffab8ab..24b7446d5 100644 --- a/packages/twenty-front/src/modules/types/SettingsPath.ts +++ b/packages/twenty-front/src/modules/types/SettingsPath.ts @@ -17,6 +17,7 @@ export enum SettingsPath { Developers = '', DevelopersNewApiKey = 'api-keys/new', DevelopersApiKeyDetail = 'api-keys/:apiKeyId', + Integrations = 'integrations', DevelopersNewWebhook = 'webhooks/new', DevelopersNewWebhookDetail = 'webhooks/:webhookId', } diff --git a/packages/twenty-front/src/modules/ui/display/icon/index.ts b/packages/twenty-front/src/modules/ui/display/icon/index.ts index 3508b027c..96f35f00e 100644 --- a/packages/twenty-front/src/modules/ui/display/icon/index.ts +++ b/packages/twenty-front/src/modules/ui/display/icon/index.ts @@ -16,6 +16,7 @@ export { IconAt, IconBaselineDensitySmall, IconBell, + IconBolt, IconBox, IconBrandGithub, IconBrandGoogle, diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index 1316f6e35..ea63653e7 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -1,4 +1,5 @@ export type FeatureFlagKey = | 'IS_MESSAGING_ENABLED' + | 'IS_INTEGRATIONS_ENABLED' | 'IS_QUICK_ACTIONS_ENABLED' | 'IS_NEW_RECORD_BOARD_ENABLED'; diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationComponent.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationComponent.tsx new file mode 100644 index 000000000..19345b702 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationComponent.tsx @@ -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 ( + + + + + {integration.to ? ( + <> + → + + > + ) : ( + <>> + )} + + {integration.text} + + {integration.type === IntegrationType.Soon ? ( + + ) : ( + openLinkInTab(integration.link)}> + {integration.type === IntegrationType.Use ? ( + + ) : ( + + )} + {integration.type === IntegrationType.Goto ? ( + {integration.linkText} + ) : ( + Use + )} + + )} + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationGroup.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationGroup.tsx new file mode 100644 index 000000000..f742d7d69 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationGroup.tsx @@ -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 ( + + + + {integrationGroup.hyperlink && ( + openLinkInTab(integrationGroup.hyperlink ?? '')} + > + {integrationGroup.hyperlinkText} + → + + )} + + + {integrationGroup.integrations.map((integration) => { + return ; + })} + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx new file mode 100644 index 000000000..c488bf179 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx @@ -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 ( + + + + {integrationCategories.map((group) => { + return ; + })} + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/integrations/constants/IntegrationTypes.ts b/packages/twenty-front/src/pages/settings/integrations/constants/IntegrationTypes.ts new file mode 100644 index 000000000..37eca610d --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/constants/IntegrationTypes.ts @@ -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[]; +} diff --git a/packages/twenty-front/src/pages/settings/integrations/constants/Integrations.ts b/packages/twenty-front/src/pages/settings/integrations/constants/Integrations.ts new file mode 100644 index 000000000..cf1f1ae78 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/constants/Integrations.ts @@ -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; diff --git a/packages/twenty-front/src/pages/settings/integrations/constants/Request.ts b/packages/twenty-front/src/pages/settings/integrations/constants/Request.ts new file mode 100644 index 000000000..92a15fef4 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/constants/Request.ts @@ -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; diff --git a/packages/twenty-front/src/pages/settings/integrations/constants/Windmill.ts b/packages/twenty-front/src/pages/settings/integrations/constants/Windmill.ts new file mode 100644 index 000000000..4711837db --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/constants/Windmill.ts @@ -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; diff --git a/packages/twenty-front/src/pages/settings/integrations/constants/Zapier.ts b/packages/twenty-front/src/pages/settings/integrations/constants/Zapier.ts new file mode 100644 index 000000000..75b0a5064 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/integrations/constants/Zapier.ts @@ -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; diff --git a/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts b/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts index 5e579a452..ad226e829 100644 --- a/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts +++ b/packages/twenty-server/src/core/feature-flag/feature-flag.entity.ts @@ -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',