Revamp settings navigation (#14174)

Menu was getting too long so I'm removing the developer section and
merging API+Webhook together (in the past we had made a split partially
because it was in the advanced mode), I'm also merging Lab and Releases.
I'm moving Approved Domains and Workspace Domain to a new dedicated
page, which will also hold email domains by end of year, because it
makes sense to do all those at once (involve DNS changes).
This commit is contained in:
Félix Malfait
2025-08-30 16:43:04 +02:00
committed by GitHub
parent d570ab05e6
commit 50521dec34
35 changed files with 403 additions and 221 deletions
@@ -6,14 +6,6 @@ import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLo
import { SettingsPath } from '@/types/SettingsPath';
import { PermissionFlagType } from '~/generated/graphql';
const SettingsApiKeys = lazy(() =>
import('~/pages/settings/developers/api-keys/SettingsApiKeys').then(
(module) => ({
default: module.SettingsApiKeys,
}),
),
);
const SettingsGraphQLPlayground = lazy(() =>
import(
'~/pages/settings/developers/playground/SettingsGraphQLPlayground'
@@ -30,14 +22,6 @@ const SettingsRestPlayground = lazy(() =>
),
);
const SettingsWebhooks = lazy(() =>
import(
'~/pages/settings/developers/webhooks/components/SettingsWebhooks'
).then((module) => ({
default: module.SettingsWebhooks,
})),
);
const SettingsAccountsCalendars = lazy(() =>
import('~/pages/settings/accounts/SettingsAccountsCalendars').then(
(module) => ({
@@ -112,12 +96,6 @@ const SettingsDevelopersApiKeysNew = lazy(() =>
})),
);
const Releases = lazy(() =>
import('~/pages/settings/Releases').then((module) => ({
default: module.Releases,
})),
);
const SettingsServerlessFunctions = lazy(() =>
import(
'~/pages/settings/serverless-functions/SettingsServerlessFunctions'
@@ -146,6 +124,18 @@ const SettingsWorkspace = lazy(() =>
})),
);
const SettingsDomains = lazy(() =>
import('~/pages/settings/domains').then((module) => ({
default: module.SettingsDomains,
})),
);
const SettingsApiWebhooks = lazy(() =>
import('~/pages/settings/workspace/SettingsApiWebhooks').then((module) => ({
default: module.SettingsApiWebhooks,
})),
);
const SettingsAI = lazy(() =>
import('~/pages/settings/ai/SettingsAI').then((module) => ({
default: module.SettingsAI,
@@ -347,9 +337,9 @@ const SettingsAdminConfigVariableDetails = lazy(() =>
})),
);
const SettingsLab = lazy(() =>
import('~/pages/settings/lab/SettingsLab').then((module) => ({
default: module.SettingsLab,
const SettingsReleases = lazy(() =>
import('~/pages/settings/releases/SettingsReleases').then((module) => ({
default: module.SettingsReleases,
})),
);
@@ -428,6 +418,11 @@ export const SettingsRoutes = ({
}
>
<Route path={SettingsPath.Workspace} element={<SettingsWorkspace />} />
<Route path={SettingsPath.Domains} element={<SettingsDomains />} />
<Route
path={SettingsPath.ApiWebhooks}
element={<SettingsApiWebhooks />}
/>
<Route path={SettingsPath.AI} element={<SettingsAI />} />
<Route
path={SettingsPath.AINewAgent}
@@ -511,8 +506,6 @@ export const SettingsRoutes = ({
/>
}
>
<Route path={SettingsPath.APIs} element={<SettingsApiKeys />} />
<Route path={SettingsPath.Webhooks} element={<SettingsWebhooks />} />
<Route
path={`${SettingsPath.GraphQLPlayground}`}
element={<SettingsGraphQLPlayground />}
@@ -578,7 +571,7 @@ export const SettingsRoutes = ({
/>
</>
)}
<Route path={SettingsPath.Releases} element={<Releases />} />
<Route
element={
<SettingsProtectedRouteWrapper
@@ -622,7 +615,7 @@ export const SettingsRoutes = ({
/>
}
>
<Route path={SettingsPath.Lab} element={<SettingsLab />} />
<Route path={SettingsPath.Releases} element={<SettingsReleases />} />
</Route>
</Routes>
</Suspense>
@@ -131,8 +131,8 @@ export const SettingsDevelopersWebhookForm = ({
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`Webhooks`,
href: getSettingsPath(SettingsPath.Webhooks),
children: t`APIs & Webhooks`,
href: getSettingsPath(SettingsPath.ApiWebhooks),
},
{ children: isCreationMode ? t`New` : getTitle() },
]}
@@ -140,7 +140,7 @@ export const SettingsDevelopersWebhookForm = ({
<SaveAndCancelButtons
isSaveDisabled={!canSave}
isCancelDisabled={formConfig.formState.isSubmitting}
onCancel={() => navigate(SettingsPath.Webhooks)}
onCancel={() => navigate(SettingsPath.ApiWebhooks)}
onSave={formConfig.handleSubmit(handleSave)}
/>
}
@@ -100,7 +100,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
});
navigate(
createdWebhook ? SettingsPath.WebhookDetail : SettingsPath.Webhooks,
createdWebhook ? SettingsPath.WebhookDetail : SettingsPath.ApiWebhooks,
createdWebhook ? { webhookId: createdWebhook.id } : undefined,
);
} catch (error) {
@@ -188,7 +188,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
message: t`Webhook deleted successfully`,
});
navigate(SettingsPath.Webhooks);
navigate(SettingsPath.ApiWebhooks);
} catch (error) {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
@@ -3,7 +3,6 @@ import { SettingsPath } from '@/types/SettingsPath';
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { type NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -18,7 +17,6 @@ import {
type IconComponent,
IconCurrencyDollar,
IconDoorEnter,
IconFlask,
IconFunction,
IconHierarchy2,
IconKey,
@@ -31,7 +29,7 @@ import {
IconSparkles,
IconUserCircle,
IconUsers,
IconWebhook,
IconWorld,
} from 'twenty-ui/display';
import { FeatureFlagKey, PermissionFlagType } from '~/generated/graphql';
@@ -65,7 +63,6 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const isAdminEnabled =
(currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ??
false;
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const isPageLayoutEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED,
@@ -116,6 +113,12 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconSettings,
isHidden: !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Data model`,
path: SettingsPath.Objects,
Icon: IconHierarchy2,
isHidden: !permissionMap[PermissionFlagType.DATA_MODEL],
},
{
label: t`Members`,
path: SettingsPath.WorkspaceMembersPage,
@@ -128,6 +131,12 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconLock,
isHidden: !permissionMap[PermissionFlagType.ROLES],
},
{
label: t`Domains`,
path: SettingsPath.Domains,
Icon: IconWorld,
isHidden: !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Billing`,
path: SettingsPath.Billing,
@@ -136,13 +145,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
!isBillingEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Data model`,
path: SettingsPath.Objects,
Icon: IconHierarchy2,
isHidden: !permissionMap[PermissionFlagType.DATA_MODEL],
},
{
label: t`Page Layout`,
label: t`Layout`,
path: SettingsPath.PageLayoutEdition,
Icon: IconLayout,
isHidden:
@@ -150,6 +153,12 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
!permissionMap[PermissionFlagType.WORKSPACE],
isNew: true,
},
{
label: t`APIs & Webhooks`,
path: SettingsPath.ApiWebhooks,
Icon: IconApi,
isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
},
{
label: t`Integrations`,
path: SettingsPath.Integrations,
@@ -164,33 +173,6 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
!isAIEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
isNew: true,
},
{
label: t`Security`,
path: SettingsPath.Security,
Icon: IconKey,
isAdvanced: true,
isHidden: !permissionMap[PermissionFlagType.SECURITY],
},
],
},
{
label: t`Developers`,
isAdvanced: false,
items: [
{
label: t`APIs`,
path: SettingsPath.APIs,
Icon: IconApi,
isAdvanced: false,
isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
},
{
label: t`Webhooks`,
path: SettingsPath.Webhooks,
Icon: IconWebhook,
isAdvanced: false,
isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
},
{
label: t`Functions`,
path: SettingsPath.ServerlessFunctions,
@@ -198,6 +180,13 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
isHidden: !isFunctionSettingsEnabled,
isAdvanced: true,
},
{
label: t`Security`,
path: SettingsPath.Security,
Icon: IconKey,
isAdvanced: true,
isHidden: !permissionMap[PermissionFlagType.SECURITY],
},
],
},
{
@@ -209,18 +198,11 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconServer,
isHidden: !isAdminEnabled,
},
{
label: t`Lab`,
path: SettingsPath.Lab,
Icon: IconFlask,
isHidden:
!labPublicFeatureFlags.length ||
!permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Releases`,
path: SettingsPath.Releases,
Icon: IconRocket,
isHidden: !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Logout`,
@@ -22,14 +22,18 @@ export enum SettingsPath {
WorkspaceMembersPage = 'members',
Workspace = 'general',
Domain = 'general/domain',
Domains = 'domains',
Releases = 'releases',
AI = 'ai',
AINewAgent = 'ai/new-agent',
AIAgentDetail = 'ai/agents/:agentId',
APIs = 'apis',
ApiWebhooks = 'api-webhooks',
RestPlayground = 'playground/rest/:schema',
GraphQLPlayground = 'playground/graphql/:schema',
NewApiKey = 'apis/new',
ApiKeyDetail = 'apis/:apiKeyId',
NewApiKey = 'api-webhooks/apis/new',
ApiKeyDetail = 'api-webhooks/apis/:apiKeyId',
NewWebhook = 'api-webhooks/webhooks/new',
WebhookDetail = 'api-webhooks/webhooks/:webhookId',
Integrations = 'integrations',
IntegrationMCP = 'integrations/mcp',
IntegrationDatabase = 'integrations/:databaseKey',
@@ -39,15 +43,12 @@ export enum SettingsPath {
Security = 'security',
NewSSOIdentityProvider = 'security/sso/new',
NewApprovedAccessDomain = 'security/approved-access-domain/new',
Webhooks = 'webhooks',
NewWebhook = 'webhooks/new',
WebhookDetail = 'webhooks/:webhookId',
Releases = 'releases',
AdminPanel = 'admin-panel',
AdminPanelHealthStatus = 'admin-panel#health-status',
AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
AdminPanelConfigVariableDetails = 'admin-panel/config-variables/:variableName',
Lab = 'lab',
Roles = 'roles',
RoleCreate = 'roles/create',
RoleDetail = 'roles/:roleId',
@@ -1,24 +1,17 @@
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
import { NameField } from '@/settings/workspace/components/NameField';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { H2Title, IconWorld, Status } from 'twenty-ui/display';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsWorkspace = () => {
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { t } = useLingui();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
return (
<SubMenuTopBarContainer
title={t`General`}
@@ -39,30 +32,7 @@ export const SettingsWorkspace = () => {
<H2Title title={t`Name`} description={t`Name of your workspace`} />
<NameField />
</Section>
{isMultiWorkspaceEnabled && (
<>
<Section>
<H2Title
title={t`Domain`}
description={t`Edit your subdomain name or set a custom domain.`}
/>
<UndecoratedLink to={getSettingsPath(SettingsPath.Domain)}>
<SettingsCard
title={t`Customize Domain`}
Icon={<IconWorld />}
Status={
currentWorkspace?.customDomain &&
currentWorkspace?.isCustomDomainEnabled ? (
<Status text={'Active'} color={'turquoise'} />
) : currentWorkspace?.customDomain ? (
<Status text={'Inactive'} color={'orange'} />
) : undefined
}
/>
</UndecoratedLink>
</Section>
</>
)}
<Section>
<DeleteWorkspace />
</Section>
@@ -143,7 +143,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
},
});
if (redirect) {
navigate(SettingsPath.APIs);
navigate(SettingsPath.ApiWebhooks);
}
} catch {
enqueueErrorSnackBar({ message: t`Error deleting api key.` });
@@ -242,8 +242,8 @@ export const SettingsDevelopersApiKeyDetail = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`APIs`,
href: getSettingsPath(SettingsPath.APIs),
children: t`APIs & Webhooks`,
href: getSettingsPath(SettingsPath.ApiWebhooks),
},
{ children: apiKey?.name },
]}
@@ -139,8 +139,8 @@ export const SettingsDevelopersApiKeysNew = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`APIs`,
href: getSettingsPath(SettingsPath.APIs),
children: t`APIs & Webhooks`,
href: getSettingsPath(SettingsPath.ApiWebhooks),
},
{ children: t`New Key` },
]}
@@ -148,7 +148,7 @@ export const SettingsDevelopersApiKeysNew = () => {
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => {
navigateSettings(SettingsPath.APIs);
navigateSettings(SettingsPath.ApiWebhooks);
}}
onSave={handleSave}
/>
@@ -19,7 +19,7 @@ export const SettingsGraphQLPlayground = () => {
: PlaygroundSchemas.CORE;
const handleExitFullScreen = () => {
navigateSettings(SettingsPath.APIs);
navigateSettings(SettingsPath.ApiWebhooks);
};
const handleOnError = () => {
@@ -35,8 +35,8 @@ export const SettingsGraphQLPlayground = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>APIs</Trans>,
href: getSettingsPath(SettingsPath.APIs),
children: <Trans>APIs & Webhooks</Trans>,
href: getSettingsPath(SettingsPath.ApiWebhooks),
},
{ children: <Trans>GraphQL API Playground</Trans> },
]}
@@ -14,7 +14,7 @@ export const SettingsRestPlayground = () => {
}>();
const handleExitFullScreen = () => {
navigateSettings(SettingsPath.APIs);
navigateSettings(SettingsPath.ApiWebhooks);
};
return (
@@ -26,15 +26,15 @@ export const SettingsRestPlayground = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>APIs</Trans>,
href: getSettingsPath(SettingsPath.APIs),
children: <Trans>APIs & Webhooks</Trans>,
href: getSettingsPath(SettingsPath.ApiWebhooks),
},
{ children: <Trans>REST</Trans> },
]}
>
<RestPlayground
schema={schema}
onError={() => navigateSettings(SettingsPath.APIs)}
onError={() => navigateSettings(SettingsPath.ApiWebhooks)}
/>
</FullScreenContainer>
);
@@ -0,0 +1,58 @@
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsWorkspaceDomainCard } from './components';
const StyledMainContent = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(10)};
min-height: 200px;
`;
const StyledSection = styled(Section)`
flex-shrink: 0;
`;
export const SettingsDomains = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`Domains`}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>Domains</Trans> },
]}
>
<SettingsPageContainer>
<StyledMainContent>
<StyledSection>
<H2Title
title={t`Workspace Domain`}
description={t`Edit your subdomain name or set a custom domain.`}
/>
<SettingsWorkspaceDomainCard />
</StyledSection>
<StyledSection>
<H2Title
title={t`Approved Domains`}
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
/>
<SettingsApprovedAccessDomainsListCard />
</StyledSection>
</StyledMainContent>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -0,0 +1,36 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsPath } from '@/types/SettingsPath';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { IconWorld, Status } from 'twenty-ui/display';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsWorkspaceDomainCard = () => {
const { t } = useLingui();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
if (!isMultiWorkspaceEnabled) {
return null;
}
return (
<UndecoratedLink to={getSettingsPath(SettingsPath.Domain)}>
<SettingsCard
title={t`Customize Domain`}
Icon={<IconWorld />}
Status={
currentWorkspace?.customDomain &&
currentWorkspace?.isCustomDomainEnabled ? (
<Status text={t`Active`} color={'turquoise'} />
) : currentWorkspace?.customDomain ? (
<Status text={t`Inactive`} color={'orange'} />
) : undefined
}
/>
</UndecoratedLink>
);
};
@@ -0,0 +1 @@
export { SettingsWorkspaceDomainCard } from './SettingsWorkspaceDomainCard';
@@ -0,0 +1 @@
export { SettingsDomains } from './SettingsDomains';
@@ -1,25 +0,0 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsLabContent } from '@/settings/lab/components/SettingsLabContent';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { t } from '@lingui/core/macro';
export const SettingsLab = () => {
return (
<SubMenuTopBarContainer
title={t`Lab`}
links={[
{
children: t`Other`,
href: getSettingsPath(SettingsPath.Lab),
},
{ children: t`Lab` },
]}
>
<SettingsPageContainer>
<SettingsLabContent />
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -0,0 +1,46 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { t } from '@lingui/core/macro';
import { IconFlask, IconRocket } from 'twenty-ui/display';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsReleasesTabContent } from './components';
import { SETTINGS_RELEASES_TABS, SETTINGS_RELEASES_TABS_ID } from './constants';
export const SettingsReleases = () => {
const tabs = [
{
id: SETTINGS_RELEASES_TABS.CHANGELOG,
title: t`Changelog`,
Icon: IconRocket,
},
{
id: SETTINGS_RELEASES_TABS.LAB,
title: t`Lab`,
Icon: IconFlask,
},
];
return (
<SubMenuTopBarContainer
title={t`Releases`}
links={[
{
children: t`Other`,
href: getSettingsPath(SettingsPath.Releases),
},
{ children: t`Releases` },
]}
>
<SettingsPageContainer>
<TabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={SETTINGS_RELEASES_TABS_ID}
/>
<SettingsReleasesTabContent />
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import React, { useEffect, useState } from 'react';
import rehypeStringify from 'rehype-stringify';
import remarkParse from 'remark-parse';
@@ -7,11 +6,7 @@ import remarkRehype from 'remark-rehype';
import { type PluggableList, unified } from 'unified';
import { visit } from 'unist-util-visit';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type ReleaseNote = {
slug: string;
@@ -79,8 +74,7 @@ const StyledReleaseDate = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const Releases = () => {
const { t } = useLingui();
export const SettingsReleasesChangelogContent = () => {
const [releases, setReleases] = useState<ReleaseNote[]>([]);
useEffect(() => {
@@ -105,29 +99,16 @@ export const Releases = () => {
}, []);
return (
<SubMenuTopBarContainer
title={t`Releases`}
links={[
{
children: <Trans>Other</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>Releases</Trans> },
]}
>
<SettingsPageContainer>
<ScrollWrapper componentInstanceId="scroll-wrapper-releases">
<StyledReleaseContainer>
{releases.map((release) => (
<React.Fragment key={release.slug}>
<StyledReleaseHeader>{release.release}</StyledReleaseHeader>
<StyledReleaseDate>{release.date}</StyledReleaseDate>
<div dangerouslySetInnerHTML={{ __html: release.html }}></div>
</React.Fragment>
))}
</StyledReleaseContainer>
</ScrollWrapper>
</SettingsPageContainer>
</SubMenuTopBarContainer>
<ScrollWrapper componentInstanceId="scroll-wrapper-releases">
<StyledReleaseContainer>
{releases.map((release) => (
<React.Fragment key={release.slug}>
<StyledReleaseHeader>{release.release}</StyledReleaseHeader>
<StyledReleaseDate>{release.date}</StyledReleaseDate>
<div dangerouslySetInnerHTML={{ __html: release.html }}></div>
</React.Fragment>
))}
</StyledReleaseContainer>
</ScrollWrapper>
);
};
@@ -0,0 +1,24 @@
import { SettingsLabContent } from '@/settings/lab/components/SettingsLabContent';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import {
SETTINGS_RELEASES_TABS,
SETTINGS_RELEASES_TABS_ID,
} from '../constants';
import { SettingsReleasesChangelogContent } from './SettingsReleasesChangelogContent';
export const SettingsReleasesTabContent = () => {
const activeTabId = useRecoilComponentValue(
activeTabIdComponentState,
SETTINGS_RELEASES_TABS_ID,
);
switch (activeTabId) {
case SETTINGS_RELEASES_TABS.CHANGELOG:
return <SettingsReleasesChangelogContent />;
case SETTINGS_RELEASES_TABS.LAB:
return <SettingsLabContent />;
default:
return <SettingsReleasesChangelogContent />;
}
};
@@ -0,0 +1,2 @@
export { SettingsReleasesChangelogContent } from './SettingsReleasesChangelogContent';
export { SettingsReleasesTabContent } from './SettingsReleasesTabContent';
@@ -0,0 +1,4 @@
export const SETTINGS_RELEASES_TABS = {
CHANGELOG: 'changelog',
LAB: 'lab',
};
@@ -0,0 +1 @@
export const SETTINGS_RELEASES_TABS_ID = 'settings-releases-tabs';
@@ -0,0 +1,2 @@
export { SETTINGS_RELEASES_TABS } from './SettingsReleasesTabs';
export { SETTINGS_RELEASES_TABS_ID } from './SettingsReleasesTabsId';
@@ -0,0 +1 @@
export { SettingsReleases } from './SettingsReleases';
@@ -5,7 +5,7 @@ import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWork
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard';
import { SettingsSecurityAuthProvidersOptionsList } from '@/settings/security/components/SettingsSecurityAuthProvidersOptionsList';
import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard';
import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
@@ -63,13 +63,7 @@ export const SettingsSecurity = () => {
/>
<SettingsSSOIdentitiesProvidersListCard />
</StyledSection>
<StyledSection>
<H2Title
title={t`Approved Domains`}
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
/>
<SettingsApprovedAccessDomainsListCard />
</StyledSection>
<Section>
<StyledContainer>
<H2Title
@@ -65,7 +65,7 @@ export const SettingsSecurityApprovedAccessDomain = () => {
enqueueSuccessSnackBar({
message: t`Please check your email for a verification link.`,
});
navigate(SettingsPath.Security);
navigate(SettingsPath.Domains);
},
onError: (error) => {
enqueueErrorSnackBar({
@@ -86,7 +86,7 @@ export const SettingsSecurityApprovedAccessDomain = () => {
title="New Approved Access Domain"
actionButton={
<SaveAndCancelButtons
onCancel={() => navigate(SettingsPath.Security)}
onCancel={() => navigate(SettingsPath.Domains)}
isSaveDisabled={form.formState.isSubmitting}
/>
}
@@ -96,8 +96,8 @@ export const SettingsSecurityApprovedAccessDomain = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>Security</Trans>,
href: getSettingsPath(SettingsPath.Security),
children: <Trans>Domains</Trans>,
href: getSettingsPath(SettingsPath.Domains),
},
{ children: <Trans>New Approved Access Domain</Trans> },
]}
@@ -0,0 +1,113 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable';
import { SettingsWebhooksTable } from '@/settings/developers/components/SettingsWebhooksTable';
import { PlaygroundSetupForm } from '@/settings/playground/components/PlaygroundSetupForm';
import { StyledSettingsApiPlaygroundCoverImage } from '@/settings/playground/components/SettingsPlaygroundCoverImage';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
padding-top: ${({ theme }) => theme.spacing(2)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-top: ${({ theme }) => theme.spacing(5)};
}
`;
const StyledMainContent = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(10)};
min-height: 200px;
`;
const StyledSection = styled(Section)`
flex-shrink: 0;
`;
const StyledContainer = styled.div<{ isMobile?: boolean }>`
display: flex;
flex-direction: column;
overflow: ${({ isMobile }) => (isMobile ? 'hidden' : 'visible')};
gap: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsApiWebhooks = () => {
const isMobile = useIsMobile();
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`APIs & Webhooks`}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>APIs & Webhooks</Trans> },
]}
>
<SettingsPageContainer>
<StyledMainContent>
<StyledSection>
<H2Title
title={t`Documentation`}
description={t`Try our REST or GraphQL API playgrounds.`}
/>
<StyledContainer>
<StyledSettingsApiPlaygroundCoverImage />
<PlaygroundSetupForm />
</StyledContainer>
</StyledSection>
<StyledSection>
<H2Title
title={t`API Keys`}
description={t`Active API keys created by you or your team.`}
/>
<StyledContainer isMobile={isMobile}>
<SettingsApiKeysTable />
<StyledButtonContainer>
<Button
Icon={IconPlus}
title={t`Create API key`}
size="small"
variant="secondary"
to={getSettingsPath(SettingsPath.NewApiKey)}
/>
</StyledButtonContainer>
</StyledContainer>
</StyledSection>
<StyledSection>
<H2Title
title={t`Webhooks`}
description={t`Establish Webhook endpoints for notifications on asynchronous events.`}
/>
<StyledContainer isMobile={isMobile}>
<SettingsWebhooksTable />
<StyledButtonContainer>
<Button
Icon={IconPlus}
title={t`Create webhook`}
size="small"
variant="secondary"
to={getSettingsPath(SettingsPath.NewWebhook)}
/>
</StyledButtonContainer>
</StyledContainer>
</StyledSection>
</StyledMainContent>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -215,14 +215,14 @@ export const SettingsDomain = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>General</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
children: <Trans>Domains</Trans>,
href: getSettingsPath(SettingsPath.Domains),
},
{ children: <Trans>Domain</Trans> },
]}
actionButton={
<SaveAndCancelButtons
onCancel={() => navigate(SettingsPath.Workspace)}
onCancel={() => navigate(SettingsPath.Domains)}
isSaveDisabled={form.formState.isSubmitting}
/>
}
@@ -23,8 +23,7 @@ enum SettingsPathPrefixes {
Profile = `${AppBasePath.Settings}/${SettingsPath.ProfilePage}`,
Objects = `${AppBasePath.Settings}/${SettingsPath.Objects}`,
Members = `${AppBasePath.Settings}/${SettingsPath.WorkspaceMembersPage}`,
ApiKeys = `${AppBasePath.Settings}/${SettingsPath.APIs}`,
Webhooks = `${AppBasePath.Settings}/${SettingsPath.Webhooks}`,
ApiWebhooks = `${AppBasePath.Settings}/${SettingsPath.ApiWebhooks}`,
ServerlessFunctions = `${AppBasePath.Settings}/${SettingsPath.ServerlessFunctions}`,
Integration = `${AppBasePath.Settings}/${SettingsPath.Integrations}`,
General = `${AppBasePath.Settings}/${SettingsPath.Workspace}`,
@@ -62,10 +61,8 @@ export const getPageTitleFromPath = (pathname: string): string => {
return SettingsPageTitles.Members;
case SettingsPathPrefixes.Objects:
return SettingsPageTitles.Objects;
case SettingsPathPrefixes.ApiKeys:
case SettingsPathPrefixes.ApiWebhooks:
return SettingsPageTitles.Apis;
case SettingsPathPrefixes.Webhooks:
return SettingsPageTitles.Webhooks;
case SettingsPathPrefixes.ServerlessFunctions:
return SettingsPageTitles.ServerlessFunctions;
case SettingsPathPrefixes.Integration:
@@ -8,7 +8,7 @@ info: Discover how to use our APIs.
## Overview
The Twenty API allows developers to interact programmatically with the Twenty CRM platform. Using the API, you can integrate Twenty with other systems, automate data synchronization, and build custom solutions around your customer data. The API provides endpoints to **create, read, update, and delete** core CRM objects (such as people and companies) as well as access metadata configuration.
**API Playground:** You can now access the API Playground within the app's settings. To try out API calls in real-time, log in to your Twenty workspace and navigate to **Settings > API & Webhooks** (under **Developers**). This will open the in-app API Playground and settings for API keys. _(If you dont see the API & Webhooks section in Settings, enable “Advanced mode” in your app settings to reveal developer options.)_
**API Playground:** You can now access the API Playground within the app's settings. To try out API calls in real-time, log in to your Twenty workspace and navigate to **Settings APIs & Webhooks**. This opens the in-app API Playground and the settings for API keys.
**[Go to API Settings](https://app.twenty.com/settings)**
## Authentication
@@ -11,9 +11,9 @@ Webhooks in Twenty complement the API by enabling **real-time notifications** to
With webhooks, Twenty will send an HTTP POST request to a URL you specify, containing details about the event. You can then handle that data in your application (e.g., to update your external database, trigger workflows, or send alerts).
## Setting Up a Webhook
To create a webhook in Twenty, use the **API & Webhooks** settings in your Twenty app (Developer settings):
To create a webhook in Twenty, use the **APIs & Webhooks** settings in your Twenty app:
1. **Navigate to Settings:** In your Twenty application, go to **Settings** (ensure Advanced mode is enabled to see developer options).
1. **Navigate to Settings:** In your Twenty application, go to **Settings → APIs & Webhooks**.
2. **Create a Webhook:** Under **Webhooks** click on **+ Create webhook**.
3. **Enter URL:** Provide the endpoint URL on your server where you want Twenty to send webhook requests. This should be a publicly accessible URL that can handle POST requests.
4. **Save:** Click **Save** to create the webhook. The new webhook will be active immediately.
@@ -37,7 +37,7 @@ From the `packages/twenty-zapier` folder, run:
```bash
cp .env.example .env
```
Run the application locally, go to [http://localhost:3000/settings/developers](http://localhost:3000/settings/developers/api-keys), and generate an API key.
Run the application locally, go to [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks), and generate an API key.
Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated.
@@ -25,7 +25,7 @@ You control which contacts are imported in Twenty and what gets shared with the
You can configure the visibility and the contact creation preferences under Settings → Email / Calendar.
### Using another provider?
This is currently under beta: you can activate this feature under Settings → Lab.
This is currently under beta: you can activate this feature under Settings → Releases → Lab tab.
You can add email accounts from any provider that supports IMAP and send emails with SMTP. Synchronizing calendars with CalDAV is coming soon!
@@ -54,7 +54,7 @@ Open your Settings on the top left to
- Invite members
- Edit your profile
- Customize your workspace: billing, user permissions
- Discover the latest releases and the upcoming features (under the Lab)
- Discover the latest releases and the upcoming features (under Releases → Lab tab)
If you do not see all those sections under Settings, reach out to your workspace administrator - some of them have a restricted access.
@@ -24,7 +24,7 @@ This means you will have a dedicated endpoint for each of your custom objects an
## API Documentation
**Twenty API documentation is accessible under your workspace Settings -> API.**
**Twenty API documentation is accessible under your workspace Settings APIs & Webhooks.**
As unique APIs are generated to perfectly reflect your data model, Twenty creates customized API documentation.
This is why you need to input an API key to access it.
You will also find a playground where you can test the APIs and the data returned.
@@ -39,7 +39,7 @@ For example, you can use them to retrieve details of a specific `Person` or `Com
### Generate an API key
1. Go to Settings in the sidebar on the left.
2. In the bottom left, activate "Advanced mode." A new section for API & Webhooks will appear.
2. Open **APIs & Webhooks**.
3. To generate a new key, click on `+ Create key` at the top right.
4. Give your API key a name, an expiration date, and a logo.
5. Hit save to see your API key.
@@ -80,7 +80,7 @@ For instance, a webhook can alert your system in real-time when someone creates
### Creating Webhooks
1. From Settings -> Developers, click `+` Create webhook.
1. From Settings → APIs & Webhooks → Webhooks, click `+` Create webhook.
2. Enter your URL.
3. Click Save.
@@ -105,7 +105,7 @@ For instance, a webhook can alert your system in real-time when someone creates
### Deleting Webhooks
1. In Developers, find your webhook.
1. In Settings → APIs & Webhooks → Webhooks, find your webhook.
2. Click on your webhook and press **Delete** to remove it. A confirmation popup will appear to confirm.
<ArticleEditContent></ArticleEditContent>
@@ -96,7 +96,7 @@ This action runs custom JavaScript within your workflow. You can use this action
Any variables you create and return here can be reused in later steps of the workflow. You can also test your code directly in this step and view the output to confirm it works as expected.
Note: You can manage keys and access your API Playground [here](https://app.twenty.com/settings) or navigate to Settings > API & Webhooks (under Developers).
Note: You can manage keys and access your API Playground [here](https://app.twenty.com/settings) or navigate to Settings APIs & Webhooks.
### Form