diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
index 6a4d98291e..4b95d1ae5e 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
@@ -1779,6 +1779,7 @@ enum FeatureFlagKey {
IS_EMAIL_GROUP_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
+ IS_SETTINGS_DISCOVERY_HERO_ENABLED
}
type WorkspaceUrls {
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
index dc602df02b..446464d971 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
@@ -1411,7 +1411,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
-export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT'
+export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED'
export interface WorkspaceUrls {
customUrl?: Scalars['String']
@@ -8732,7 +8732,8 @@ export const enumFeatureFlagKey = {
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
IS_EMAIL_GROUP_ENABLED: 'IS_EMAIL_GROUP_ENABLED' as const,
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
- IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const
+ IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const,
+ IS_SETTINGS_DISCOVERY_HERO_ENABLED: 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' as const
}
export const enumIdentityProviderType = {
diff --git a/packages/twenty-front/public/images/ai/ai-tools-cover-dark.png b/packages/twenty-front/public/images/ai/ai-tools-cover-dark.png
index 6ab502be13..24b3ae918a 100644
Binary files a/packages/twenty-front/public/images/ai/ai-tools-cover-dark.png and b/packages/twenty-front/public/images/ai/ai-tools-cover-dark.png differ
diff --git a/packages/twenty-front/public/images/ai/ai-tools-cover-light.png b/packages/twenty-front/public/images/ai/ai-tools-cover-light.png
index b29ed89a50..dc68ef81af 100644
Binary files a/packages/twenty-front/public/images/ai/ai-tools-cover-light.png and b/packages/twenty-front/public/images/ai/ai-tools-cover-light.png differ
diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts
index 8926f0f4d0..e7f04a0980 100644
--- a/packages/twenty-front/src/generated-admin/graphql.ts
+++ b/packages/twenty-front/src/generated-admin/graphql.ts
@@ -289,6 +289,7 @@ export enum FeatureFlagKey {
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
+ IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
}
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index fb584c997b..ce5eea32df 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1641,6 +1641,7 @@ export enum FeatureFlagKey {
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
+ IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
}
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
index 6e0c2892e3..fe71592a34 100644
--- a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
@@ -24,9 +24,6 @@ export const useEnterLayoutCustomizationMode = () => {
const { navigateSidePanel } = useNavigateSidePanel();
const { enqueueWarningSnackBar } = useSnackBar();
- // Returns whether customization mode is active afterward, so callers that
- // navigate on entry can skip navigation when entry was blocked (e.g. a
- // dashboard is mid-edit).
const enterLayoutCustomizationMode = useCallback((): boolean => {
const isLayoutCustomizationModeAlreadyEnabled = store.get(
isLayoutCustomizationModeEnabledState.atom,
diff --git a/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx
index c6958ad81e..15196d35b4 100644
--- a/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx
+++ b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx
@@ -4,11 +4,13 @@ import {
} from '@/settings/components/SettingsCustomizeVideoModal';
import { HeroPlayButton } from '@/ui/layout/hero/components/HeroPlayButton';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { Card } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
const COVER_HEIGHT = 150;
@@ -56,6 +58,9 @@ export const SettingsDiscoveryHeroCard = ({
const { t } = useLingui();
const { colorScheme } = useContext(ThemeContext);
const { openModal } = useModal();
+ const isDiscoveryVideoEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_SETTINGS_DISCOVERY_HERO_ENABLED,
+ );
const modalInstanceId = `${instanceIdPrefix}-modal`;
const tabsInstanceId = `${instanceIdPrefix}-tabs`;
@@ -67,19 +72,23 @@ export const SettingsDiscoveryHeroCard = ({
-
- openModal(modalInstanceId)}
- ariaLabel={playButtonAriaLabel ?? t`Watch demo`}
- />
-
+ {isDiscoveryVideoEnabled && (
+
+ openModal(modalInstanceId)}
+ ariaLabel={playButtonAriaLabel ?? t`Watch demo`}
+ />
+
+ )}
-
+ {isDiscoveryVideoEnabled && (
+
+ )}
>
);
};
diff --git a/packages/twenty-front/src/modules/settings/components/SettingsStatsGrid.tsx b/packages/twenty-front/src/modules/settings/components/SettingsStatsGrid.tsx
index d00667107a..9332be1d03 100644
--- a/packages/twenty-front/src/modules/settings/components/SettingsStatsGrid.tsx
+++ b/packages/twenty-front/src/modules/settings/components/SettingsStatsGrid.tsx
@@ -6,15 +6,10 @@ import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
export type SettingsStatRow = {
Icon: IconComponent;
label: string;
- // String so callers can render a placeholder (e.g. "—") while async counts
- // are still loading. Layout stats just pass `count.toString()`.
value: string;
};
type SettingsStatsGridProps = {
- // Each inner array is one column rendered top-to-bottom; columns are
- // separated by a vertical divider. Pass [[a, b], [c, d]] for a 2x2 layout
- // or [[a, b, c]] for a single column.
columns: SettingsStatRow[][];
};
diff --git a/packages/twenty-front/src/modules/settings/data-model/assets/cover-dark.png b/packages/twenty-front/src/modules/settings/data-model/assets/cover-dark.png
index 5da09efbdf..dcaf412186 100644
Binary files a/packages/twenty-front/src/modules/settings/data-model/assets/cover-dark.png and b/packages/twenty-front/src/modules/settings/data-model/assets/cover-dark.png differ
diff --git a/packages/twenty-front/src/modules/settings/data-model/assets/cover-light.png b/packages/twenty-front/src/modules/settings/data-model/assets/cover-light.png
index 6d8aebd11b..f5bfd36760 100644
Binary files a/packages/twenty-front/src/modules/settings/data-model/assets/cover-light.png and b/packages/twenty-front/src/modules/settings/data-model/assets/cover-light.png differ
diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents.tsx
index 777fa6671b..32adfb3215 100644
--- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents.tsx
@@ -3,12 +3,8 @@ import { styled } from '@linaria/react';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
import React from 'react';
-// Column width used by the Applications data tables (Instances column).
export const SETTINGS_OBJECT_TABLE_COLUMN_WIDTH = '98.7px';
-// Relative grid: Name takes all remaining space (with a floor); App / Fields /
-// Instances get fixed minimums so short text columns don't collapse; trailing
-// 36 px holds the chevron / action cell.
export const SETTINGS_OBJECT_TABLE_ROW_GRID_TEMPLATE_COLUMNS = `minmax(180px, 1fr) 140px 80px 100px 36px`;
export const SETTINGS_OBJECT_TABLE_ROW_MOBILE_MIN_WIDTH = '520px';
diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx
index 6f6eb284c3..b9b9bb09d6 100644
--- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx
@@ -58,7 +58,6 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
return;
}
- // Skip navigation when entry was blocked (e.g. a dashboard is mid-edit).
if (!enterLayoutCustomizationMode()) {
return;
}
diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx
index fae608161a..e8c4316b02 100644
--- a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx
@@ -1,5 +1,6 @@
import { styled } from '@linaria/react';
+import { WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS } from '@/settings/developers/constants/WebhookTableRowGridTemplateColumns';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { getUrlHostnameOrThrow, isValidUrl } from 'twenty-shared/utils';
@@ -11,8 +12,6 @@ import { useContext } from 'react';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { type Webhook } from '~/generated-metadata/graphql';
-export const WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS = '1fr 28px';
-
const StyledIconChevronRightContainer = styled.span`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsWebhooksTable.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsWebhooksTable.tsx
index f2e4ab8640..7bf816fd85 100644
--- a/packages/twenty-front/src/modules/settings/developers/components/SettingsWebhooksTable.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsWebhooksTable.tsx
@@ -1,9 +1,7 @@
import { styled } from '@linaria/react';
-import {
- SettingsDevelopersWebhookTableRow,
- WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
-} from '@/settings/developers/components/SettingsDevelopersWebhookTableRow';
+import { SettingsDevelopersWebhookTableRow } from '@/settings/developers/components/SettingsDevelopersWebhookTableRow';
+import { WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS } from '@/settings/developers/constants/WebhookTableRowGridTemplateColumns';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
diff --git a/packages/twenty-front/src/modules/settings/developers/constants/WebhookTableRowGridTemplateColumns.ts b/packages/twenty-front/src/modules/settings/developers/constants/WebhookTableRowGridTemplateColumns.ts
new file mode 100644
index 0000000000..1e4abffdcf
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/constants/WebhookTableRowGridTemplateColumns.ts
@@ -0,0 +1 @@
+export const WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS = '1fr 28px';
diff --git a/packages/twenty-front/src/modules/settings/playground/assets/cover-dark.png b/packages/twenty-front/src/modules/settings/playground/assets/cover-dark.png
index 3e58c9f5fc..bebc3c2387 100644
Binary files a/packages/twenty-front/src/modules/settings/playground/assets/cover-dark.png and b/packages/twenty-front/src/modules/settings/playground/assets/cover-dark.png differ
diff --git a/packages/twenty-front/src/modules/settings/playground/assets/cover-light.png b/packages/twenty-front/src/modules/settings/playground/assets/cover-light.png
index b03a8d23d0..8698450c1e 100644
Binary files a/packages/twenty-front/src/modules/settings/playground/assets/cover-light.png and b/packages/twenty-front/src/modules/settings/playground/assets/cover-light.png differ
diff --git a/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx b/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx
index 57d55ef38a..2706c0d120 100644
--- a/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx
+++ b/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx
@@ -19,9 +19,6 @@ const playgroundSetupFormSchema = z.object({
type PlaygroundSetupFormValues = z.infer;
-// Last column shrinks to the Launch button's content width so its right
-// edge sits at the form's right edge. The two select columns share the
-// remaining space equally.
const StyledForm = styled.form`
align-items: end;
display: grid;
diff --git a/packages/twenty-front/src/modules/settings/playground/components/__stories__/PlaygroundSetupForm.stories.tsx b/packages/twenty-front/src/modules/settings/playground/components/__stories__/PlaygroundSetupForm.stories.tsx
index 48eaaa48fe..aacfbd9d99 100644
--- a/packages/twenty-front/src/modules/settings/playground/components/__stories__/PlaygroundSetupForm.stories.tsx
+++ b/packages/twenty-front/src/modules/settings/playground/components/__stories__/PlaygroundSetupForm.stories.tsx
@@ -1,11 +1,12 @@
import { PlaygroundSetupForm } from '@/settings/playground/components/PlaygroundSetupForm';
+import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
const meta: Meta = {
title: 'Modules/Settings/Playground/PlaygroundSetupForm',
component: PlaygroundSetupForm,
- decorators: [ComponentDecorator, RouterDecorator],
+ decorators: [ComponentDecorator, RouterDecorator, SnackBarDecorator],
parameters: {
docs: {
description: {
diff --git a/packages/twenty-front/src/modules/settings/playground/states/playgroundApiKeyState.ts b/packages/twenty-front/src/modules/settings/playground/states/playgroundApiKeyState.ts
index 75e9478a37..9483a46b01 100644
--- a/packages/twenty-front/src/modules/settings/playground/states/playgroundApiKeyState.ts
+++ b/packages/twenty-front/src/modules/settings/playground/states/playgroundApiKeyState.ts
@@ -2,17 +2,13 @@ import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomStat
import { isDefined } from 'twenty-shared/utils';
import { type AuthToken } from '~/generated-metadata/graphql';
-// In-memory only: a short-lived, full-permission bearer token. Keeping it out of
-// localStorage bounds the exfiltration window to the current tab and leaves no
-// usable credential at rest after the tab closes.
+// In-memory only — never persist this bearer token to localStorage.
export const playgroundApiKeyState = createAtomState({
key: 'playgroundApiKeyState',
defaultValue: null,
});
-// Usable only while it stays valid for at least `bufferMs` longer. Consumers pass
-// no buffer (reject the moment it expires); the launcher passes a buffer so it
-// re-mints before a near-expired token can fail mid-session.
+// Returns true when the token is still valid `bufferMs` from now.
export const isPlaygroundApiKeyFresh = (
token: AuthToken | null,
bufferMs = 0,
diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx
index ca4e8bc530..1a68e76e93 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx
@@ -20,9 +20,6 @@ type SubMenuTopBarContainerProps = {
tag?: JSX.Element;
};
-// Cards, forms, and tables inside the white panel are centered in a fixed
-// max-width column so they don't sprawl on large displays. The white panel
-// itself spans edge-to-edge; only the content is constrained.
const SETTINGS_CONTENT_MAX_WIDTH = 760;
const StyledContainer = styled.div`
@@ -31,10 +28,8 @@ const StyledContainer = styled.div`
width: 100%;
`;
-// flex: 1 + min-height: 0 keep the vertical-scroll chain intact: PagePanel's
-// own overflow handling sits one level up and depends on its children
-// participating in the flex height calculation rather than collapsing to
-// content height.
+// flex: 1 + min-height: 0 are required for PagePanel's overflow chain — the
+// child must participate in the flex height calc rather than collapse to content.
const StyledBodyContentWrapper = styled.div`
display: flex;
flex: 1;
@@ -72,12 +67,6 @@ export const SubMenuTopBarContainer = ({
}>
{actionButton}
- {/*
- MainContainerLayoutWithSidePanel is the same wrapper the App's record
- pages use: it renders the page body on the left and SidePanelForDesktop
- on the right. Hosting it here lets the AI chat side panel (and any
- other side-panel page) open in settings exactly as it does in the App.
- */}
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/CollapsibleNavigationDrawerSection.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/CollapsibleNavigationDrawerSection.tsx
index 3d89770794..9006063354 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/CollapsibleNavigationDrawerSection.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/CollapsibleNavigationDrawerSection.tsx
@@ -5,24 +5,13 @@ import { type ReactNode } from 'react';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
type CollapsibleNavigationDrawerSectionProps = {
- // Unique id used to persist the open/closed state in localStorage. Pass
- // a namespaced value (e.g. 'settings/User') so unrelated sections in
- // different drawers don't share state.
+ // Namespaced id (e.g. 'settings/User') used to persist open/closed state.
sectionId: string;
label: string;
children: ReactNode;
- // Optional wrapper around the section title (e.g. AdvancedSettingsWrapper
- // for advanced-mode-only sections). Receives the title node and returns
- // the wrapped node.
wrapTitle?: (titleNode: ReactNode) => ReactNode;
};
-// One-stop section component for any drawer that wants the main-app's
-// collapsible section behavior: click the title to collapse / expand,
-// animated height transition, persisted open state, chevron-on-hover.
-// Use this instead of stitching together NavigationDrawerSection +
-// NavigationDrawerSectionTitle + AnimatedExpandableContainer by hand at
-// every call site.
export const CollapsibleNavigationDrawerSection = ({
sectionId,
label,
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent.tsx
index 4496174a02..1cce1a9375 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent.tsx
@@ -5,8 +5,6 @@ import { styled } from '@linaria/react';
import { useIsMobile } from 'twenty-ui/utilities';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-// Mobile keeps the touch-friendly horizontal padding; on desktop the container
-// is edge-to-edge and the child supplies its own padding.
const StyledFixedContainer = styled.div<{ isMobile?: boolean }>`
padding-left: ${({ isMobile }) =>
isMobile ? themeCssVariables.spacing[5] : '0'};
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx
index ef6cae878d..114b09d63e 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx
@@ -52,9 +52,6 @@ export type NavigationDrawerItemProps = {
onClick?: () => void;
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
iconColor?: string | null;
- // Wrap the plain icon in a soft grey tile (no border) — used by the
- // settings drawer so its icons read as a uniform group without picking
- // up TintedIconTile's bordered colored treatment.
withIconBackground?: boolean;
active?: boolean;
modifier?: NavigationDrawerItemModifier;
@@ -206,10 +203,6 @@ const StyledIcon = styled.div`
margin-right: ${themeCssVariables.spacing[2]};
`;
-// Soft grey background-only tile (no border) used by the settings drawer.
-// Sized one step larger than the icon so the icon sits with a couple of
-// pixels of breathing room on every side. radius.md matches the rest of
-// the App's small-card / tile language; radius.sm read as sharp squares.
const StyledIconBackgroundTile = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.tertiary};
diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx
index 395f2e8129..e971ece7b5 100644
--- a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx
@@ -1,16 +1,11 @@
-import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
-import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath, isDefined } from 'twenty-shared/utils';
+import { getSettingsPath } from 'twenty-shared/utils';
import { t } from '@lingui/core/macro';
import {
@@ -30,6 +25,7 @@ import { SettingsAiModelsTab } from '~/pages/settings/ai/components/SettingsAiMo
import { SettingsAiOverviewTab } from '~/pages/settings/ai/components/SettingsAiOverviewTab';
import { SettingsAiUsageTab } from '~/pages/settings/ai/components/SettingsAiUsageTab';
import { SETTINGS_AI_TABS } from '~/pages/settings/ai/constants/SettingsAiTabs';
+import { useCreateTool } from '~/pages/settings/ai/hooks/useCreateTool';
const AI_HERO_LIGHT = '/images/ai/ai-tools-cover-light.png';
const AI_HERO_DARK = '/images/ai/ai-tools-cover-dark.png';
@@ -37,55 +33,13 @@ const AI_HERO_DARK = '/images/ai/ai-tools-cover-dark.png';
const SETTINGS_AI_HERO_INSTANCE_ID_PREFIX = 'settings-ai-hero';
export const SettingsAI = () => {
- const navigate = useNavigate();
- const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
- const { createLogicFunction } = usePersistLogicFunction();
- const [isCreatingTool, setIsCreatingTool] = useState(false);
+ const { handleCreateTool, isCreatingTool } = useCreateTool();
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID,
);
- const handleCreateTool = async () => {
- setIsCreatingTool(true);
- try {
- const result = await createLogicFunction({
- input: {
- name: 'new-tool',
- toolTriggerSettings: {
- inputSchema: { type: 'object', properties: {} },
- },
- },
- });
-
- if (result.status === 'successful' && isDefined(result.response?.data)) {
- const newLogicFunction = result.response.data.createOneLogicFunction;
- enqueueSuccessSnackBar({ message: t`Tool created` });
-
- const applicationId = newLogicFunction.applicationId;
- if (isDefined(applicationId)) {
- navigate(
- getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
- applicationId,
- logicFunctionId: newLogicFunction.id,
- }),
- );
- } else {
- navigate(
- getSettingsPath(SettingsPath.LogicFunctionDetail, {
- logicFunctionId: newLogicFunction.id,
- }),
- );
- }
- } else {
- enqueueErrorSnackBar({ message: t`Failed to create tool` });
- }
- } finally {
- setIsCreatingTool(false);
- }
- };
-
const tabs = [
{
id: SETTINGS_AI_TABS.TABS_IDS.OVERVIEW,
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkillsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkillsTab.tsx
index df8ecbc7b0..11aab8c375 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkillsTab.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkillsTab.tsx
@@ -104,7 +104,7 @@ export const SettingsAgentSkillsTab = () => {
setShowDeactivated(!showDeactivated)}
+ onToggleChange={setShowDeactivated}
toggled={showDeactivated}
text={t`Deactivated`}
toggleSize="small"
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTab.tsx
index 01e7e7e086..6182874fd6 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTab.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTab.tsx
@@ -1,80 +1,35 @@
-import { gql } from '@apollo/client';
-import { useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
-import { type ReactNode, useMemo, useState } from 'react';
+import { type ReactNode, useState } from 'react';
-import { useGetToolIndex } from '@/ai/hooks/useGetToolIndex';
-import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
-import { logicFunctionsSelector } from '@/logic-functions/states/logicFunctionsSelector';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { ToolCategory } from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
import { H2Title, IconLock, IconPuzzle, IconTool } from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { MenuItemToggle } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-import {
- type SettingsAgentToolItem,
- SettingsAgentToolsTable,
-} from '~/pages/settings/ai/components/SettingsAgentToolsTable';
+import { SettingsAgentToolsTable } from '~/pages/settings/ai/components/SettingsAgentToolsTable';
+import { useSettingsAgentToolsTable } from '~/pages/settings/ai/hooks/useSettingsAgentToolsTable';
+import { type SettingsAgentToolItem } from '~/pages/settings/ai/types/SettingsAgentToolItem';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
-const FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE = gql`
- query FindManyApplicationsForToolTable {
- findManyApplications {
- id
- name
- universalIdentifier
- logo
- }
- }
-`;
-
-const FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE = gql`
- query FindManyMarketplaceAppsForToolTable {
- findManyMarketplaceApps {
- id
- universalIdentifier
- icon
- logo
- }
- }
-`;
-
const StyledSearchContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
`;
export const SettingsAgentToolsTab = () => {
- const logicFunctions = useAtomStateValue(logicFunctionsSelector);
- const currentWorkspace = useAtomStateValue(currentWorkspaceState);
- const {
- toolIndex,
- loading: toolIndexLoading,
- error: toolIndexError,
- } = useGetToolIndex();
- const { data: applicationsData } = useQuery<{
- findManyApplications: Array<{
- id: string;
- name: string;
- universalIdentifier: string;
- logo?: string | null;
- }>;
- }>(FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE);
- const { data: marketplaceAppsData } = useQuery<{
- findManyMarketplaceApps: Array<{
- id: string;
- universalIdentifier: string;
- icon: string;
- logo?: string | null;
- }>;
- }>(FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE);
const { t } = useLingui();
+ const {
+ allTools,
+ applicationById,
+ marketplaceAppByUniversalIdentifier,
+ currentWorkspace,
+ isLoading,
+ } = useSettingsAgentToolsTable();
+
const [searchTerm, setSearchTerm] = useState('');
const [showCustomTools, setShowCustomTools] = useState(true);
const [showManagedTools, setShowManagedTools] = useState(true);
@@ -89,42 +44,6 @@ export const SettingsAgentToolsTab = () => {
const isCustom = (tool: SettingsAgentToolItem) =>
isDefined(tool.applicationId);
- const allTools: SettingsAgentToolItem[] = useMemo(
- () => [
- ...logicFunctions
- .filter((fn) => isDefined(fn.toolTriggerSettings))
- .map((fn) => ({
- identifier: fn.id,
- name: fn.name,
- description: fn.description,
- applicationId: fn.applicationId,
- })),
- ...toolIndex
- .filter((tool) => tool.category !== ToolCategory.LOGIC_FUNCTION)
- .map((tool) => ({
- identifier: tool.name,
- name: tool.name,
- description: tool.description,
- category: tool.category,
- objectName: tool.objectName,
- icon: tool.icon,
- })),
- ],
- [logicFunctions, toolIndex],
- );
-
- const applicationById = new Map(
- (applicationsData?.findManyApplications ?? []).map((application) => [
- application.id,
- application,
- ]),
- );
- const marketplaceAppByUniversalIdentifier = new Map(
- (marketplaceAppsData?.findManyMarketplaceApps ?? []).map(
- (marketplaceApp) => [marketplaceApp.universalIdentifier, marketplaceApp],
- ),
- );
-
const filteredTools = allTools
.filter((tool) => {
const searchNormalized = normalizeSearchText(searchTerm);
@@ -149,8 +68,6 @@ export const SettingsAgentToolsTab = () => {
})
.sort((a, b) => a.name.localeCompare(b.name));
- const isLoading = toolIndexLoading && !toolIndexError;
-
return (
{
- setShowCustomTools(!showCustomTools)
- }
+ onToggleChange={setShowCustomTools}
toggled={showCustomTools}
text={t`Custom`}
toggleSize="small"
/>
- setShowManagedTools(!showManagedTools)
- }
+ onToggleChange={setShowManagedTools}
toggled={showManagedTools}
text={t`Managed`}
toggleSize="small"
/>
- setShowStandardTools(!showStandardTools)
- }
+ onToggleChange={setShowStandardTools}
toggled={showStandardTools}
text={t`Standard`}
toggleSize="small"
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTable.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTable.tsx
index 6d1f7b7007..1ccdd81f10 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTable.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentToolsTable.tsx
@@ -1,53 +1,34 @@
import { useContext } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { type CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
-import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
-import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath, isDefined } from 'twenty-shared/utils';
+import { isDefined } from 'twenty-shared/utils';
import { IconChevronRight } from 'twenty-ui/display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
-
-import { type CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { SettingsToolIcon } from '~/pages/settings/ai/components/SettingsToolIcon';
import {
SettingsToolTableRow,
TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
} from '~/pages/settings/ai/components/SettingsToolTableRow';
-
-export type SettingsAgentToolItem = {
- identifier: string;
- name: string;
- description?: string | null;
- category?: string;
- objectName?: string | null;
- icon?: string | null;
- applicationId?: string | null;
-};
-
-type Application = {
- id: string;
- name: string;
- universalIdentifier: string;
- logo?: string | null;
-};
-
-type MarketplaceApp = {
- id: string;
- universalIdentifier: string;
- icon: string;
- logo?: string | null;
-};
+import { type SettingsAgentToolApplication } from '~/pages/settings/ai/types/SettingsAgentToolApplication';
+import { type SettingsAgentToolItem } from '~/pages/settings/ai/types/SettingsAgentToolItem';
+import { type SettingsAgentToolMarketplaceApp } from '~/pages/settings/ai/types/SettingsAgentToolMarketplaceApp';
+import { getToolApplicationId } from '~/pages/settings/ai/utils/getToolApplicationId';
+import { getToolLink } from '~/pages/settings/ai/utils/getToolLink';
type SettingsAgentToolsTableProps = {
tools: SettingsAgentToolItem[];
isLoading: boolean;
- applicationById: Map;
- marketplaceAppByUniversalIdentifier: Map;
+ applicationById: Map;
+ marketplaceAppByUniversalIdentifier: Map<
+ string,
+ SettingsAgentToolMarketplaceApp
+ >;
currentWorkspace: CurrentWorkspace | null;
};
@@ -55,28 +36,6 @@ const StyledTableHeaderRowContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[2]};
`;
-const getToolApplicationId = (
- tool: SettingsAgentToolItem,
- currentWorkspace: CurrentWorkspace | null,
-): string => {
- if (isDefined(tool.applicationId)) {
- return tool.applicationId;
- }
-
- return (
- currentWorkspace?.installedApplications?.find(
- (app) =>
- app.universalIdentifier ===
- TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
- )?.id ?? ''
- );
-};
-
-const getToolLink = (tool: SettingsAgentToolItem): string =>
- getSettingsPath(SettingsPath.AiToolDetail, {
- toolIdentifier: tool.identifier,
- });
-
export const SettingsAgentToolsTable = ({
tools,
isLoading,
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx
index a0c2f4a937..5a11851fd2 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx
@@ -10,17 +10,17 @@ import { getModelIcon } from '@/settings/ai/utils/getModelIcon';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
-import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
-import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { useMutation, useQuery } from '@apollo/client/react';
+import { useQuery } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import {
AUTO_SELECT_FAST_MODEL_ID,
AUTO_SELECT_SMART_MODEL_ID,
} from 'twenty-shared/constants';
+import { SettingsPath } from 'twenty-shared/types';
+import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import {
H2Title,
IconBolt,
@@ -32,12 +32,8 @@ import { SearchInput } from 'twenty-ui/input';
import { Card, Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
-import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath, isDefined } from 'twenty-shared/utils';
-import {
- GetAiSystemPromptPreviewDocument,
- UpdateWorkspaceDocument,
-} from '~/generated-metadata/graphql';
+import { GetAiSystemPromptPreviewDocument } from '~/generated-metadata/graphql';
+import { useSettingsAiModelsActions } from '~/pages/settings/ai/hooks/useSettingsAiModelsActions';
import { formatNumber } from '~/utils/format/formatNumber';
const StyledCustomModelsContainer = styled.div`
@@ -49,14 +45,19 @@ const StyledCustomModelsContainer = styled.div`
export const SettingsAiModelsTab = () => {
const { theme } = useContext(ThemeContext);
- const { enqueueErrorSnackBar } = useSnackBar();
- const [currentWorkspace, setCurrentWorkspace] = useAtomState(
- currentWorkspaceState,
- );
- const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
+ const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [searchQuery, setSearchQuery] = useState('');
+
const { data: previewData } = useQuery(GetAiSystemPromptPreviewDocument);
const aiModels = useAtomStateValue(aiModelsState);
+ const { useRecommendedModels, realModels, enabledModels } =
+ useWorkspaceAiModelAvailability();
+ const {
+ handleModelFieldChange,
+ handleUseRecommendedToggle,
+ handleModelToggle,
+ handleToggleAllVisibleModels,
+ } = useSettingsAiModelsActions();
const systemPromptTokenCount =
previewData?.getAiSystemPromptPreview.estimatedTokenCount;
@@ -67,9 +68,6 @@ export const SettingsAiModelsTab = () => {
)} tokens)`
: t`Read the system prompts to understand how the AI works`;
- const { useRecommendedModels, realModels, enabledModels } =
- useWorkspaceAiModelAvailability();
-
const currentSmartModel = currentWorkspace?.smartModel;
const currentFastModel = currentWorkspace?.fastModel;
@@ -103,110 +101,11 @@ export const SettingsAiModelsTab = () => {
};
});
- const handleModelFieldChange = async (
- field: 'smartModel' | 'fastModel',
- value: string,
- ) => {
- if (!currentWorkspace?.id) return;
- const previousValue = currentWorkspace[field];
- try {
- setCurrentWorkspace({ ...currentWorkspace, [field]: value });
- await updateWorkspace({ variables: { input: { [field]: value } } });
- } catch {
- setCurrentWorkspace({ ...currentWorkspace, [field]: previousValue });
- enqueueErrorSnackBar({ message: t`Failed to update model` });
- }
- };
-
const enabledModelIdSet = new Set(currentWorkspace?.enabledAiModelIds ?? []);
- const handleUseRecommendedToggle = async (checked: boolean) => {
- if (!currentWorkspace?.id) {
- return;
- }
-
- const previousValue = currentWorkspace.useRecommendedModels;
-
- let newEnabledIds = currentWorkspace.enabledAiModelIds ?? [];
-
- if (!checked && previousValue) {
- const recommendedModelIds = realModels
- .filter((model) => model.isRecommended)
- .map((model) => model.modelId);
-
- newEnabledIds = recommendedModelIds;
- }
-
- try {
- setCurrentWorkspace({
- ...currentWorkspace,
- useRecommendedModels: checked,
- enabledAiModelIds: newEnabledIds,
- });
-
- await updateWorkspace({
- variables: {
- input: {
- useRecommendedModels: checked,
- enabledAiModelIds: newEnabledIds,
- },
- },
- });
- } catch {
- setCurrentWorkspace({
- ...currentWorkspace,
- useRecommendedModels: previousValue,
- });
-
- enqueueErrorSnackBar({
- message: t`Failed to update model selection mode`,
- });
- }
- };
-
- const handleModelToggle = async (
- modelId: string,
- isCurrentlyEnabled: boolean,
- ) => {
- if (!currentWorkspace?.id) {
- return;
- }
-
- const previousEnabled = currentWorkspace.enabledAiModelIds ?? [];
-
- const newEnabledIds = isCurrentlyEnabled
- ? previousEnabled.filter((id) => id !== modelId)
- : [...previousEnabled, modelId];
-
- try {
- setCurrentWorkspace({
- ...currentWorkspace,
- enabledAiModelIds: newEnabledIds,
- });
-
- await updateWorkspace({
- variables: {
- input: {
- enabledAiModelIds: newEnabledIds,
- },
- },
- });
- } catch {
- setCurrentWorkspace({
- ...currentWorkspace,
- enabledAiModelIds: previousEnabled,
- });
-
- enqueueErrorSnackBar({
- message: t`Failed to update model availability`,
- });
- }
- };
-
const filteredModels = searchQuery.trim()
? realModels.filter((model) => {
const query = searchQuery.toLowerCase();
-
return (
model.label.toLowerCase().includes(query) ||
(model.modelFamily?.toLowerCase().includes(query) ?? false) ||
@@ -284,34 +183,12 @@ export const SettingsAiModelsTab = () => {
models={filteredModels}
isChecked={(model) => enabledModelIdSet.has(model.modelId)}
onToggle={handleModelToggle}
- onToggleAll={async (shouldCheckAll) => {
- const previousIds = currentWorkspace?.enabledAiModelIds ?? [];
- const visibleModelIds = new Set(
- filteredModels.map((m) => m.modelId),
- );
-
- const newEnabledIds = shouldCheckAll
- ? [...new Set([...previousIds, ...visibleModelIds])]
- : previousIds.filter((id) => !visibleModelIds.has(id));
-
- try {
- setCurrentWorkspace({
- ...currentWorkspace!,
- enabledAiModelIds: newEnabledIds,
- });
- await updateWorkspace({
- variables: { input: { enabledAiModelIds: newEnabledIds } },
- });
- } catch {
- setCurrentWorkspace({
- ...currentWorkspace!,
- enabledAiModelIds: previousIds,
- });
- enqueueErrorSnackBar({
- message: t`Failed to update model availability`,
- });
- }
- }}
+ onToggleAll={(shouldCheckAll) =>
+ handleToggleAllVisibleModels(
+ shouldCheckAll,
+ new Set(filteredModels.map((m) => m.modelId)),
+ )
+ }
anchorPrefix="workspace-model-row"
/>
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsToolIcon.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsToolIcon.tsx
index 37d898b78b..251d2a029f 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsToolIcon.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsToolIcon.tsx
@@ -22,7 +22,6 @@ type ApplicationInfo = {
};
type MarketplaceAppInfo = {
- icon: string;
logo?: string | null;
};
@@ -109,11 +108,6 @@ export const SettingsToolIcon = ({
);
}
- if (isDefined(marketplaceApp)) {
- const MarketplaceIcon = getIcon(marketplaceApp.icon);
- return ;
- }
-
if (isDefined(application)) {
return (
{
+ const navigate = useNavigate();
+ const { enqueueSuccessSnackBar } = useSnackBar();
+ const { createLogicFunction } = usePersistLogicFunction();
+ const [isCreatingTool, setIsCreatingTool] = useState(false);
+
+ const handleCreateTool = async () => {
+ setIsCreatingTool(true);
+ try {
+ const result = await createLogicFunction({
+ input: {
+ name: 'new-tool',
+ toolTriggerSettings: {
+ inputSchema: { type: 'object', properties: {} },
+ },
+ },
+ });
+
+ // Failure path already surfaces its own snackbar from usePersistLogicFunction.
+ if (result.status !== 'successful' || !isDefined(result.response?.data)) {
+ return;
+ }
+
+ const newLogicFunction = result.response.data.createOneLogicFunction;
+ enqueueSuccessSnackBar({ message: t`Tool created` });
+
+ const applicationId = newLogicFunction.applicationId;
+ if (isDefined(applicationId)) {
+ navigate(
+ getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
+ applicationId,
+ logicFunctionId: newLogicFunction.id,
+ }),
+ );
+ } else {
+ navigate(
+ getSettingsPath(SettingsPath.LogicFunctionDetail, {
+ logicFunctionId: newLogicFunction.id,
+ }),
+ );
+ }
+ } finally {
+ setIsCreatingTool(false);
+ }
+ };
+
+ return { handleCreateTool, isCreatingTool };
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/hooks/useSettingsAgentToolsTable.ts b/packages/twenty-front/src/pages/settings/ai/hooks/useSettingsAgentToolsTable.ts
new file mode 100644
index 0000000000..cb9bf3b04b
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/hooks/useSettingsAgentToolsTable.ts
@@ -0,0 +1,81 @@
+import { useQuery } from '@apollo/client/react';
+import { useMemo } from 'react';
+
+import { useGetToolIndex } from '@/ai/hooks/useGetToolIndex';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { logicFunctionsSelector } from '@/logic-functions/states/logicFunctionsSelector';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { ToolCategory } from 'twenty-shared/ai';
+import { isDefined } from 'twenty-shared/utils';
+import { FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE } from '~/pages/settings/ai/graphql/queries/findManyApplicationsForToolTable';
+import { FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE } from '~/pages/settings/ai/graphql/queries/findManyMarketplaceAppsForToolTable';
+import { type SettingsAgentToolApplication } from '~/pages/settings/ai/types/SettingsAgentToolApplication';
+import { type SettingsAgentToolItem } from '~/pages/settings/ai/types/SettingsAgentToolItem';
+import { type SettingsAgentToolMarketplaceApp } from '~/pages/settings/ai/types/SettingsAgentToolMarketplaceApp';
+
+export const useSettingsAgentToolsTable = () => {
+ const logicFunctions = useAtomStateValue(logicFunctionsSelector);
+ const currentWorkspace = useAtomStateValue(currentWorkspaceState);
+ const {
+ toolIndex,
+ loading: toolIndexLoading,
+ error: toolIndexError,
+ } = useGetToolIndex();
+
+ const { data: applicationsData } = useQuery<{
+ findManyApplications: SettingsAgentToolApplication[];
+ }>(FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE);
+
+ const { data: marketplaceAppsData } = useQuery<{
+ findManyMarketplaceApps: SettingsAgentToolMarketplaceApp[];
+ }>(FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE);
+
+ const allTools: SettingsAgentToolItem[] = useMemo(
+ () => [
+ ...logicFunctions
+ .filter((fn) => isDefined(fn.toolTriggerSettings))
+ .map((fn) => ({
+ identifier: fn.id,
+ name: fn.name,
+ description: fn.description,
+ applicationId: fn.applicationId,
+ })),
+ ...toolIndex
+ .filter((tool) => tool.category !== ToolCategory.LOGIC_FUNCTION)
+ .map((tool) => ({
+ identifier: tool.name,
+ name: tool.name,
+ description: tool.description,
+ category: tool.category,
+ objectName: tool.objectName,
+ icon: tool.icon,
+ })),
+ ],
+ [logicFunctions, toolIndex],
+ );
+
+ const applicationById = new Map(
+ (applicationsData?.findManyApplications ?? []).map((application) => [
+ application.id,
+ application,
+ ]),
+ );
+
+ // MarketplaceApp.id IS the universal identifier — see
+ // marketplace-query.service.ts where `id: registration.universalIdentifier`.
+ const marketplaceAppByUniversalIdentifier = new Map(
+ (marketplaceAppsData?.findManyMarketplaceApps ?? []).map(
+ (marketplaceApp) => [marketplaceApp.id, marketplaceApp],
+ ),
+ );
+
+ const isLoading = toolIndexLoading && !toolIndexError;
+
+ return {
+ allTools,
+ applicationById,
+ marketplaceAppByUniversalIdentifier,
+ currentWorkspace,
+ isLoading,
+ };
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/hooks/useSettingsAiModelsActions.ts b/packages/twenty-front/src/pages/settings/ai/hooks/useSettingsAiModelsActions.ts
new file mode 100644
index 0000000000..5ebedd5333
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/hooks/useSettingsAiModelsActions.ts
@@ -0,0 +1,137 @@
+import { useMutation } from '@apollo/client/react';
+import { t } from '@lingui/core/macro';
+
+import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
+import { UpdateWorkspaceDocument } from '~/generated-metadata/graphql';
+
+export const useSettingsAiModelsActions = () => {
+ const { enqueueErrorSnackBar } = useSnackBar();
+ const [currentWorkspace, setCurrentWorkspace] = useAtomState(
+ currentWorkspaceState,
+ );
+ const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
+ const { realModels } = useWorkspaceAiModelAvailability();
+
+ const handleModelFieldChange = async (
+ field: 'smartModel' | 'fastModel',
+ value: string,
+ ) => {
+ if (!currentWorkspace?.id) return;
+
+ const previousValue = currentWorkspace[field];
+ try {
+ setCurrentWorkspace({ ...currentWorkspace, [field]: value });
+ await updateWorkspace({ variables: { input: { [field]: value } } });
+ } catch {
+ setCurrentWorkspace({ ...currentWorkspace, [field]: previousValue });
+ enqueueErrorSnackBar({ message: t`Failed to update model` });
+ }
+ };
+
+ const handleUseRecommendedToggle = async (checked: boolean) => {
+ if (!currentWorkspace?.id) return;
+
+ const previousValue = currentWorkspace.useRecommendedModels;
+ let newEnabledIds = currentWorkspace.enabledAiModelIds ?? [];
+
+ if (!checked && previousValue) {
+ newEnabledIds = realModels
+ .filter((model) => model.isRecommended)
+ .map((model) => model.modelId);
+ }
+
+ try {
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ useRecommendedModels: checked,
+ enabledAiModelIds: newEnabledIds,
+ });
+ await updateWorkspace({
+ variables: {
+ input: {
+ useRecommendedModels: checked,
+ enabledAiModelIds: newEnabledIds,
+ },
+ },
+ });
+ } catch {
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ useRecommendedModels: previousValue,
+ });
+ enqueueErrorSnackBar({
+ message: t`Failed to update model selection mode`,
+ });
+ }
+ };
+
+ const handleModelToggle = async (
+ modelId: string,
+ isCurrentlyEnabled: boolean,
+ ) => {
+ if (!currentWorkspace?.id) return;
+
+ const previousEnabled = currentWorkspace.enabledAiModelIds ?? [];
+ const newEnabledIds = isCurrentlyEnabled
+ ? previousEnabled.filter((id) => id !== modelId)
+ : [...previousEnabled, modelId];
+
+ try {
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ enabledAiModelIds: newEnabledIds,
+ });
+ await updateWorkspace({
+ variables: { input: { enabledAiModelIds: newEnabledIds } },
+ });
+ } catch {
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ enabledAiModelIds: previousEnabled,
+ });
+ enqueueErrorSnackBar({
+ message: t`Failed to update model availability`,
+ });
+ }
+ };
+
+ const handleToggleAllVisibleModels = async (
+ shouldCheckAll: boolean,
+ visibleModelIds: Set,
+ ) => {
+ if (!currentWorkspace?.id) return;
+
+ const previousIds = currentWorkspace.enabledAiModelIds ?? [];
+ const newEnabledIds = shouldCheckAll
+ ? [...new Set([...previousIds, ...visibleModelIds])]
+ : previousIds.filter((id) => !visibleModelIds.has(id));
+
+ try {
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ enabledAiModelIds: newEnabledIds,
+ });
+ await updateWorkspace({
+ variables: { input: { enabledAiModelIds: newEnabledIds } },
+ });
+ } catch {
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ enabledAiModelIds: previousIds,
+ });
+ enqueueErrorSnackBar({
+ message: t`Failed to update model availability`,
+ });
+ }
+ };
+
+ return {
+ handleModelFieldChange,
+ handleUseRecommendedToggle,
+ handleModelToggle,
+ handleToggleAllVisibleModels,
+ };
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolApplication.ts b/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolApplication.ts
new file mode 100644
index 0000000000..48f8b55147
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolApplication.ts
@@ -0,0 +1,6 @@
+export type SettingsAgentToolApplication = {
+ id: string;
+ name: string;
+ universalIdentifier: string;
+ logo?: string | null;
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolItem.ts b/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolItem.ts
new file mode 100644
index 0000000000..e77b3a3191
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolItem.ts
@@ -0,0 +1,9 @@
+export type SettingsAgentToolItem = {
+ identifier: string;
+ name: string;
+ description?: string | null;
+ category?: string;
+ objectName?: string | null;
+ icon?: string | null;
+ applicationId?: string | null;
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolMarketplaceApp.ts b/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolMarketplaceApp.ts
new file mode 100644
index 0000000000..d17930accc
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/types/SettingsAgentToolMarketplaceApp.ts
@@ -0,0 +1,4 @@
+export type SettingsAgentToolMarketplaceApp = {
+ id: string;
+ logo?: string | null;
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/utils/getToolApplicationId.ts b/packages/twenty-front/src/pages/settings/ai/utils/getToolApplicationId.ts
new file mode 100644
index 0000000000..a65ce590bb
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/utils/getToolApplicationId.ts
@@ -0,0 +1,21 @@
+import { type CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
+import { type SettingsAgentToolItem } from '~/pages/settings/ai/types/SettingsAgentToolItem';
+import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
+import { isDefined } from 'twenty-shared/utils';
+
+export const getToolApplicationId = (
+ tool: SettingsAgentToolItem,
+ currentWorkspace: CurrentWorkspace | null,
+): string => {
+ if (isDefined(tool.applicationId)) {
+ return tool.applicationId;
+ }
+
+ return (
+ currentWorkspace?.installedApplications?.find(
+ (app) =>
+ app.universalIdentifier ===
+ TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
+ )?.id ?? ''
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/utils/getToolLink.ts b/packages/twenty-front/src/pages/settings/ai/utils/getToolLink.ts
new file mode 100644
index 0000000000..5cc904d2a5
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/utils/getToolLink.ts
@@ -0,0 +1,8 @@
+import { type SettingsAgentToolItem } from '~/pages/settings/ai/types/SettingsAgentToolItem';
+import { SettingsPath } from 'twenty-shared/types';
+import { getSettingsPath } from 'twenty-shared/utils';
+
+export const getToolLink = (tool: SettingsAgentToolItem): string =>
+ getSettingsPath(SettingsPath.AiToolDetail, {
+ toolIdentifier: tool.identifier,
+ });
diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx
index 4f71a894d4..d788ae11c2 100644
--- a/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx
@@ -11,9 +11,8 @@ import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconApps, IconCode, IconDownload, IconPlug } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
-// TODO: replace with apps-specific illustrations + recordings when designed.
-import placeholderHeroDark from '~/pages/settings/layout/assets/customize-illustration-dark.png';
-import placeholderHeroLight from '~/pages/settings/layout/assets/customize-illustration-light.png';
+import coverDark from '~/pages/settings/applications/assets/cover-dark.png';
+import coverLight from '~/pages/settings/applications/assets/cover-light.png';
import {
FeatureFlagKey,
PermissionFlagType,
@@ -82,8 +81,8 @@ export const SettingsApplications = () => {
{
const { objectMetadataItems } = useFilteredObjectMetadataItems();
const isDDLLocked = useAtomStateValue(isDDLLockedState);
- const heroTabs = useMemo(
- () => [
- {
- id: 'objects',
- title: t`Objects`,
- Icon: IconHierarchy2,
- vimeoId: '926288174',
- },
- {
- id: 'fields',
- title: t`Fields`,
- Icon: IconList,
- vimeoId: '927628219',
- },
- {
- id: 'relations',
- title: t`Relations`,
- Icon: IconLink,
- vimeoId: '1185511827',
- },
- ],
- [t],
- );
+ const heroTabs = [
+ {
+ id: 'objects',
+ title: t`Objects`,
+ Icon: IconHierarchy2,
+ vimeoId: '926288174',
+ },
+ {
+ id: 'fields',
+ title: t`Fields`,
+ Icon: IconList,
+ vimeoId: '927628219',
+ },
+ {
+ id: 'relations',
+ title: t`Relations`,
+ Icon: IconLink,
+ vimeoId: '1185511827',
+ },
+ ];
return (
{
const navigate = useNavigate();
const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
- // Match the in-app customization entry points: arm customization mode before
- // landing in the app, otherwise the user just drops into the app normally.
- // Skip navigation when entry was blocked (e.g. a dashboard is mid-edit) so we
- // don't yank the user away after the warning.
const handleCustomize = () => {
if (enterLayoutCustomizationMode()) {
navigate(AppPath.Index);
}
};
- // TODO: replace placeholder demo videos per tab
- const heroTabs = useMemo(
- () => [
- {
- id: 'sidebar',
- title: t`Sidebar`,
- Icon: IconLayoutSidebarLeftExpand,
- vimeoId: '1185511790',
- },
- {
- id: 'record-page',
- title: t`Record page`,
- Icon: IconAppWindow,
- vimeoId: '1185511790',
- },
- {
- id: 'command-menu',
- title: t`Command menu`,
- Icon: IconCommand,
- vimeoId: '1185416775',
- },
- {
- id: 'views',
- title: t`Views`,
- Icon: IconTable,
- vimeoId: '1145648745',
- },
- {
- id: 'dashboards',
- title: t`Dashboards`,
- Icon: IconLayoutDashboard,
- vimeoId: '1185511768',
- },
- ],
- [t],
- );
+ const heroTabs = [
+ {
+ id: 'sidebar',
+ title: t`Sidebar`,
+ Icon: IconLayoutSidebarLeftExpand,
+ vimeoId: '1185511790',
+ },
+ {
+ id: 'record-page',
+ title: t`Record page`,
+ Icon: IconAppWindow,
+ vimeoId: '1185511790',
+ },
+ {
+ id: 'command-menu',
+ title: t`Command menu`,
+ Icon: IconCommand,
+ vimeoId: '1185416775',
+ },
+ {
+ id: 'views',
+ title: t`Views`,
+ Icon: IconTable,
+ vimeoId: '1145648745',
+ },
+ {
+ id: 'dashboards',
+ title: t`Dashboards`,
+ Icon: IconLayoutDashboard,
+ vimeoId: '1185511768',
+ },
+ ];
return (
{
{
ApiKeyEntity)
async createApiKey(
diff --git a/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts
index 0473835450..07ce27b6bf 100644
--- a/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts
+++ b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts
@@ -55,8 +55,8 @@ export class ApiKeyController {
return this.apiKeyService.findById(id, workspace.id);
}
- // Minting/altering a long-lived API key requires a first-person session
- // (ACCESS); a derived PLAYGROUND token or an API key cannot escalate here.
+ // Minting an API key requires an ACCESS token — derived PLAYGROUND tokens
+ // and API keys must not escalate into a long-lived credential.
@UseGuards(RequireAccessTokenGuard)
@Post()
async create(
diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts
index c39d57a6ba..3c99e87555 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts
@@ -944,11 +944,7 @@ describe('JwtAuthStrategy', () => {
});
describe('PLAYGROUND token validation', () => {
- // A PLAYGROUND token is access-shaped but must never impersonate. This is the
- // same payload that, as an ACCESS token with isImpersonating set but without
- // impersonation ids, throws 'Invalid or missing user workspace ID in
- // impersonation token' above; as a PLAYGROUND token it must skip the
- // impersonation path entirely and resolve to the first-person context.
+ // PLAYGROUND tokens are access-shaped but must never impersonate.
it('ignores isImpersonating and resolves first-person', async () => {
const validUserId = 'valid-user-id';
const validUserWorkspaceId = randomUUID();
diff --git a/packages/twenty-server/src/engine/core-modules/auth/types/app-oauth-state-jwt-payload.type.ts b/packages/twenty-server/src/engine/core-modules/auth/types/app-oauth-state-jwt-payload.type.ts
index ca03331ea2..f1f16a06af 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/types/app-oauth-state-jwt-payload.type.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/types/app-oauth-state-jwt-payload.type.ts
@@ -7,9 +7,8 @@ export type AppOAuthStateJwtPayload = CommonPropertiesJwtPayload & {
connectionProviderId: string;
userId: string;
userWorkspaceId: string;
- // 'user' = the resulting credential is private to userWorkspaceId.
- // 'workspace' = visible to anyone in the workspace.
- // Named `visibility` to disambiguate from OAuth `scopes` on the row.
+ // 'user' scopes the credential to userWorkspaceId; 'workspace' makes it
+ // visible to anyone in the workspace.
visibility: 'user' | 'workspace';
// If set, the callback updates this existing connectedAccount row instead
// of creating a new one (used by the UI's "Reconnect" action).
diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts
index 15855ae52d..72a1f4380f 100644
--- a/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts
+++ b/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts
@@ -20,6 +20,14 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
'Enable many-to-many relations through junction tables configuration',
},
},
+ {
+ key: FeatureFlagKey.IS_SETTINGS_DISCOVERY_HERO_ENABLED,
+ metadata: {
+ label: 'Settings Discovery Hero',
+ description:
+ 'Show the per-page hero illustration + video walkthrough modal on settings pages',
+ },
+ },
...(process.env.CLOUDFLARE_API_KEY
? [
// {
diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
index 848dfc3ce8..1e5e293499 100644
--- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
@@ -237,6 +237,7 @@ describe('WorkspaceEntityManager', () => {
IS_EMAIL_GROUP_ENABLED: false,
IS_JUNCTION_RELATIONS_ENABLED: false,
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: false,
+ IS_SETTINGS_DISCOVERY_HERO_ENABLED: false,
},
userWorkspaceRoleMap: {},
eventEmitterService: {
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
index eed5ea196c..aba4d5ec6c 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
@@ -45,6 +45,11 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
+ {
+ key: FeatureFlagKey.IS_SETTINGS_DISCOVERY_HERO_ENABLED,
+ workspaceId: workspaceId,
+ value: false,
+ },
])
.execute();
};
diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/api-key-webhooks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/api-key-webhooks.integration-spec.ts
index e2e3cd854f..690c4a6a0c 100644
--- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/api-key-webhooks.integration-spec.ts
+++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/api-key-webhooks.integration-spec.ts
@@ -33,9 +33,8 @@ describe('api key and webhooks permissions', () => {
});
});
- // A non-session token (here API_KEY; PLAYGROUND shares the same access path)
- // must not mint a durable API key, otherwise a short-lived token could
- // escalate into a long-lived one. RequireAccessTokenGuard enforces this.
+ // Non-ACCESS tokens (API_KEY here, PLAYGROUND same path) must never mint
+ // an API key — enforced by RequireAccessTokenGuard.
it('should reject a non-ACCESS token even with API key permission', async () => {
const queryData = {
query: `
diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts
index b73e6182cc..6ff204ea02 100644
--- a/packages/twenty-shared/src/types/FeatureFlagKey.ts
+++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts
@@ -6,4 +6,5 @@ export enum FeatureFlagKey {
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
+ IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
}