Compare commits

...
Author SHA1 Message Date
Félix Malfait 20b621ebb3 fix(settings): use symmetric padding so title, tabs, body share one center
Last asymmetry remaining: SettingsPageHeader's padding was 16px left
and 12px right, which shifts the centered title in the 1fr-auto-1fr
grid by +2px relative to the column midpoint. The secondary bar and
body content had no such offset, so the title was 2px right of the
tabs and body.

Replace the header's asymmetric padding with symmetric 16px on both
sides, and apply matching 16px symmetric padding to the secondary bar.
Title center, tab center, and body content center now all sit at the
exact same x-coordinate (column midpoint).
2026-06-02 07:14:28 +02:00
Félix Malfait cce466d9bd fix(settings): put header + tabs + body in the same column
ROOT CAUSE of the alignment drift:

- SettingsPageHeader rendered in StyledContainer at full settings width
- MainContainerLayoutWithSidePanel wraps everything below in PageBody
  whose StyledMainContainer has padding-right: 12px and sits next to
  SidePanelForDesktop in a flex row
- So PagePanel is narrower than the header by 12px + SidePanelWidth
- Header centers to one x-coordinate, anything centered inside PagePanel
  (tabs, body) centers to a different x-coordinate
- No CSS inside SettingsSecondaryBar can fix this — they were in
  different coordinate spaces

Fix: replace the MainContainerLayoutWithSidePanel wrapper with a custom
StyledRoot that puts the page header AND the rounded panel into the
same StyledMainColumn, with SidePanelForDesktop as a sibling column.
All three (header, secondary bar, body content) now share the exact
same width, so the centered title above, the centered tabs in the
middle, and the centered 760px body content below all sit on the same
vertical axis.

Strip SettingsSecondaryBar back down to plain flex + justify-content:
center now that it lives at the same width as the header — no grid
needed, no asymmetric padding to chase.
2026-06-02 07:06:55 +02:00
Félix Malfait 1681326a1c fix(settings): move tab bar inside PagePanel so it shares the rounded card
The settings rounded white card comes from PagePanel inside
MainContainerLayoutWithSidePanel. SettingsSecondaryBar was rendered
*outside* that wrapper, so it sat in the gray noisy area between the
top bar and the panel rather than appearing as the top section of the
rounded card.

Move SettingsSecondaryBar inside MainContainerLayoutWithSidePanel,
above the body wrapper, so it becomes the first row of the rounded
panel — its top edge is the panel's rounded edge, no separate strip.

Style follows: drop the bar's own background and border-top (inherits
the panel's white + rounded top), keep only border-bottom to draw the
single line between tabs and body content. flex-shrink: 0 keeps the
48px row from collapsing in the panel's column flex.
2026-06-02 06:56:38 +02:00
Félix Malfait e2fa51b9b6 fix(settings): align secondary bar grid + lighten background
The page header uses display:grid with 1fr-auto-1fr columns and
asymmetric padding (16px left, 12px right). The secondary bar was
using display:flex with justify-content:center and symmetric 16px
padding — different math meant the title above and the tabs below
centered to slightly different x-positions, so the tabs read as
off-center vs the title.

Mirror the page header's grid + asymmetric padding on the secondary
bar so the centered slot lands at the exact same x-position as the
title. Drop the now-redundant width:100% + justify-content:center
on StyledTabRow — grid positioning handles it.

Also switch the secondary bar background from background.noisy to
background.primary (lighter) for clearer contrast with the top bar
above. The two border lines + lighter fill give the tab row its own
visual band rather than a thin strip stamped on the same gray as
the header.
2026-06-02 06:48:28 +02:00
Félix Malfait 45189eb96d fix(settings): give secondary bar a top border so it reads as its own row
The target design has two clear horizontal lines bracketing the tabs:
one between the top bar and the tab bar, one between the tab bar and
the body. SettingsSecondaryBar only had border-bottom, so the bar
visually bled into the noisy top bar above. Add border-top and bump
the bar height from 40px to 48px so the tabs sit in a proper row with
breathing room rather than appearing to float under the title.
2026-06-02 06:33:32 +02:00
Félix Malfait fde53e01b1 feat(settings): tab data model + accounts, fix tab centering
Three changes:

1. Fix tab alignment. SettingsTabBar's StyledTabRow was a flex item
   with default content sizing, sitting inside a justify-content:center
   parent. Real DOM layout had it left-anchored, not centered. Switch
   the row itself to width:100% + justify-content:center so the tabs
   are guaranteed centered inside the secondary bar regardless of
   parent flex behavior.

2. Migrate SettingsObjectDetailPage to SettingsTabBar. Tabs (Fields /
   Settings / Layout) move from the body into the secondary bar.
   "See records" rightComponent slot on TabList doesn't exist on
   SettingsTabBar — folded it into the page's actionButton alongside
   the conditional "New Field" so both stay reachable from the top bar.

3. Consolidate Accounts / Emails / Calendars into one tabbed page.
   - SettingsAccounts now has General / Emails / Calendars tabs.
   - Delete SettingsAccountsEmails + SettingsAccountsCalendars pages
     and their stories.
   - Drop the standalone /accounts/emails and /accounts/calendars
     routes from SettingsRoutes.
   - Repoint SettingsPath.AccountsEmails → 'accounts#emails' and
     SettingsPath.AccountsCalendars → 'accounts#calendars' so every
     existing getSettingsPath() call site (sidebar nav, row dropdown,
     settings section card) deep-links into the tab automatically via
     TabListFromUrlOptionalEffect.
2026-06-01 23:59:25 +02:00
Félix Malfait aaadcd9d78 refactor(settings): drop dead props in tab bar and page layout
- SettingsTabBar: drop unused `behaveAsLinks` prop (always behaves
  as links) and collapse the inner SettingsTabBarContent split.
  Pass componentInstanceId directly to useAtomComponentState so a
  single component is enough; Context.Provider still wraps the JSX
  for TabListFromUrlOptionalEffect's implicit context read.
- SettingsPageLayout: drop unused `className` prop.
2026-06-01 23:46:28 +02:00
Félix Malfait 31928548a8 feat(settings): centered header + secondary bar with tabs/wizard slot
Settings pages now have a distinct chrome from the rest of the app —
the shared PageHeader and TabList were drifting toward something
specific to settings, so this PR introduces a settings-owned layout
family instead of bending the shared primitives.

New components (all in @/settings/components/layout):
- SettingsPageHeader: 3-zone bar — breadcrumb left, title centered,
  actions right. 3-column grid with a 1fr/auto/1fr template so the
  title stays visually centered even when the breadcrumb is long
  (left zone ellipsizes).
- SettingsSecondaryBar: full-width bar below the header with a bottom
  border and a single centered slot.
- SettingsTabBar: centered horizontal tabs. Reuses TabButton from
  twenty-ui plus activeTabIdComponentState + TabListFromUrlOptionalEffect
  so the existing Jotai instance state and URL hash sync work unchanged.
  Drops the overflow dropdown — settings pages have ≤5 tabs.
- SettingsWizardStepBar: back arrow (optional) + "N. Label" centered
  + trailing slot (optional). State lives in the page's routing layer.
- SettingsPageLayout: composes the header + optional secondary bar +
  body (MainContainerLayoutWithSidePanel + 760px max-width). Drop-in
  replacement for SubMenuTopBarContainer.

Migration:
- 80 settings pages: SubMenuTopBarContainer → SettingsPageLayout.
  Mechanical rename; no behavior change.
- 5 tab pages (SettingsAI / SettingsApiWebhooks / SettingsApplications /
  SettingsWorkspaceMembers / SettingsRole): TabList pulled out of body
  content into secondaryBar={<SettingsTabBar ... />}.
- 2 wizard pages (SettingsRoleAddObjectLevel +
  SettingsRolePermissionsObjectLevelObjectForm): the "1. Select an
  object" / "2. Set X permissions" strings move from the page title
  into SettingsWizardStepBar.label; the top-bar title becomes the
  role (or agent) name. Finish button moves from actionButton to
  the wizard's trailing slot. Step 2 wires onBack to step 1's route.
- Drop reserveTitleSpace prop (the new header always reserves space)
  and tag prop (one usage in SettingsApplicationRegistrationDetails
  inlined the Tag into the title node instead).

SubMenuTopBarContainer is deleted — no non-settings consumers existed.
2026-06-01 23:33:16 +02:00
Félix Malfait d6c3b4c277 feat(settings): gate discovery play button on feature flag
Add IS_SETTINGS_DISCOVERY_HERO_ENABLED feature flag (default off,
registered as a public flag so it shows in /settings/labs).

In SettingsDiscoveryHeroCard the cover illustration always renders;
the play button overlay and SettingsCustomizeVideoModal only render
when the flag is on. This lets us ship the hero artwork now and turn
the video walkthroughs on later when they're ready.
2026-06-01 23:01:51 +02:00
Félix Malfait 0e6fa33815 chore(settings): address review comments from PR 21072
Round through bosiraphael's 31 review threads from the merged
discovery-hero + playground-token PR.

Comment cleanup:
- Drop narrative comments that restate the code or describe the PR diff
- Tighten security/CSS/API doc comments to one or two factual lines
- Keep security-critical rationale on RequireAccessTokenGuard call sites

Structure / extraction:
- Move WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS to its own constants file
- Split SettingsAgentToolsTab into queries/, hooks/useSettingsAgentToolsTable,
  types/SettingsAgentToolItem|Application|MarketplaceApp,
  utils/getToolApplicationId|getToolLink
- Extract SettingsAiModelsTab mutations into hooks/useSettingsAiModelsActions
- Extract SettingsAI handleCreateTool into hooks/useCreateTool
- Drop unnecessary useMemo wrappers on heroTabs arrays
  (SettingsObjects, SettingsLayout)
- Simplify MenuItemToggle handler to onToggleChange={setShowDeactivated}

Hero assets:
- Replace placeholder customize-illustration with per-page exports
  (layout, data-model, applications, ai, mcp, members)
- Rename layout/customize-illustration-* to layout/cover-*
- Add cover-light/dark.png for applications and members (previously
  reused the layout placeholder)
2026-06-01 22:40:27 +02:00
142 changed files with 1303 additions and 1227 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 70 KiB

@@ -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'
}
@@ -24,20 +24,6 @@ const SettingsRestPlayground = lazy(() =>
),
);
const SettingsAccountsCalendars = lazy(() =>
import('~/pages/settings/accounts/SettingsAccountsCalendars').then(
(module) => ({
default: module.SettingsAccountsCalendars,
}),
),
);
const SettingsAccountsEmails = lazy(() =>
import('~/pages/settings/accounts/SettingsAccountsEmails').then((module) => ({
default: module.SettingsAccountsEmails,
})),
);
const SettingsAccountsConfiguration = lazy(() =>
import('~/pages/settings/accounts/SettingsAccountsConfiguration').then(
(module) => ({
@@ -631,14 +617,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
path={SettingsPath.AccountsConfiguration}
element={<SettingsAccountsConfiguration />}
/>
<Route
path={SettingsPath.AccountsCalendars}
element={<SettingsAccountsCalendars />}
/>
<Route
path={SettingsPath.AccountsEmails}
element={<SettingsAccountsEmails />}
/>
<Route
path={SettingsPath.NewImapSmtpCaldavConnection}
element={<SettingsNewImapSmtpCaldavConnection />}
@@ -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,
@@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsPath } from 'twenty-shared/types';
import { Loader } from 'twenty-ui/feedback';
@@ -64,7 +64,7 @@ export const SettingsAccountsEditImapSmtpCaldavConnection = () => {
const renderForm = () => (
// oxlint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formMethods}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Edit Account`}
links={[
{
@@ -94,7 +94,7 @@ export const SettingsAccountsEditImapSmtpCaldavConnection = () => {
existingProtocols={existingProtocols}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</FormProvider>
);
@@ -12,7 +12,7 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const SettingsAccountsNewEmailGroupChannel = () => {
@@ -45,7 +45,7 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
}, [createEmailGroupChannel, handle, navigate, enqueueErrorSnackBar, t]);
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`New Email Handle`}
links={[
{
@@ -84,6 +84,6 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -3,7 +3,7 @@ import { FormProvider } from 'react-hook-form';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -30,7 +30,7 @@ export const SettingsAccountsNewImapSmtpCaldavConnection = () => {
return (
// oxlint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formMethods}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`New Account`}
links={[
{
@@ -56,7 +56,7 @@ export const SettingsAccountsNewImapSmtpCaldavConnection = () => {
<SettingsPageContainer>
<SettingsAccountsConnectionForm control={control} isEditing={false} />
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</FormProvider>
);
};
@@ -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 = ({
<Card rounded>
<StyledCoverContainer>
<StyledImage src={src} alt="" aria-hidden />
<StyledOverlay>
<HeroPlayButton
onClick={() => openModal(modalInstanceId)}
ariaLabel={playButtonAriaLabel ?? t`Watch demo`}
/>
</StyledOverlay>
{isDiscoveryVideoEnabled && (
<StyledOverlay>
<HeroPlayButton
onClick={() => openModal(modalInstanceId)}
ariaLabel={playButtonAriaLabel ?? t`Watch demo`}
/>
</StyledOverlay>
)}
</StyledCoverContainer>
</Card>
<SettingsCustomizeVideoModal
modalInstanceId={modalInstanceId}
tabsInstanceId={tabsInstanceId}
tabs={tabs}
/>
{isDiscoveryVideoEnabled && (
<SettingsCustomizeVideoModal
modalInstanceId={modalInstanceId}
tabsInstanceId={tabsInstanceId}
tabs={tabs}
/>
)}
</>
);
};
@@ -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[][];
};
@@ -0,0 +1,88 @@
import {
Breadcrumb,
type BreadcrumbProps,
} from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
import { useNavigationDrawerExpanded } from '@/navigation/hooks/useNavigationDrawerExpanded';
import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsPageHeaderProps = {
links: BreadcrumbProps['links'];
title?: ReactNode;
actions?: ReactNode;
};
const StyledBar = styled.div`
align-items: center;
background: ${themeCssVariables.background.noisy};
box-sizing: border-box;
color: ${themeCssVariables.font.color.primary};
display: grid;
gap: ${themeCssVariables.spacing[2]};
grid-template-columns: 1fr auto 1fr;
min-height: ${PAGE_BAR_MIN_HEIGHT}px;
padding: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[4]};
@media (max-width: ${MOBILE_VIEWPORT}px) {
grid-template-columns: auto 1fr auto;
}
`;
const StyledLeft = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[1]};
min-width: 0;
overflow: hidden;
`;
const StyledCenter = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.primary};
display: flex;
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.semiBold};
justify-content: center;
min-width: 0;
`;
const StyledRight = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
justify-content: flex-end;
min-width: 0;
`;
export const SettingsPageHeader = ({
links,
title,
actions,
}: SettingsPageHeaderProps) => {
const isMobile = useIsMobile();
const isNavigationDrawerExpanded = useNavigationDrawerExpanded();
return (
<StyledBar>
<StyledLeft>
{!isNavigationDrawerExpanded && (
<NavigationDrawerCollapseButton direction="right" />
)}
{!isMobile && <Breadcrumb links={links} />}
</StyledLeft>
<StyledCenter>
{typeof title === 'string' ? (
<OverflowingTextWithTooltip text={title} />
) : (
title
)}
</StyledCenter>
<StyledRight>{actions}</StyledRight>
</StyledBar>
);
};
@@ -0,0 +1,108 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { SettingsPageHeader } from '@/settings/components/layout/SettingsPageHeader';
import { SettingsSecondaryBar } from '@/settings/components/layout/SettingsSecondaryBar';
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsPageLayoutProps = {
links: BreadcrumbProps['links'];
title?: ReactNode;
actionButton?: ReactNode;
secondaryBar?: ReactNode;
children: ReactNode;
};
const SETTINGS_CONTENT_MAX_WIDTH = 760;
// Header + rounded panel both sit in StyledMainColumn — same width, same
// centering. SidePanelForDesktop is alongside so opening it doesn't shift
// the page header or the tab bar relative to each other.
const StyledRoot = styled.div`
background: ${themeCssVariables.background.noisy};
display: flex;
flex: 1;
flex-direction: row;
gap: ${themeCssVariables.spacing[2]};
min-height: 0;
padding-bottom: ${themeCssVariables.spacing[3]};
padding-right: ${themeCssVariables.spacing[3]};
width: 100%;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${themeCssVariables.spacing[3]};
}
`;
const StyledMainColumn = styled.div`
display: flex;
flex: 1 1 0;
flex-direction: column;
min-width: 0;
width: 0;
`;
const StyledPanel = styled.div`
background: ${themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
`;
// flex: 1 + min-height: 0 keep the overflow chain intact — child must
// participate in the flex height calc rather than collapse to content.
const StyledBodyContentWrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
margin: 0 auto;
max-width: ${SETTINGS_CONTENT_MAX_WIDTH}px;
min-height: 0;
overflow-y: auto;
padding-top: ${themeCssVariables.spacing[6]};
width: 100%;
`;
export const SettingsPageLayout = ({
links,
title,
actionButton,
secondaryBar,
children,
}: SettingsPageLayoutProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
return (
<StyledRoot>
<StyledMainColumn>
<SettingsPageHeader
links={links}
title={title}
actions={actionButton}
/>
<StyledPanel>
{isDefined(secondaryBar) && (
<SettingsSecondaryBar>{secondaryBar}</SettingsSecondaryBar>
)}
<StyledBodyContentWrapper>
<InformationBannerWrapper />
{children}
</StyledBodyContentWrapper>
</StyledPanel>
</StyledMainColumn>
{isMobile ? <CommandMenuForMobile /> : <SidePanelForDesktop />}
</StyledRoot>
);
};
@@ -0,0 +1,30 @@
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsSecondaryBarProps = {
children: ReactNode;
};
const SECONDARY_BAR_HEIGHT = 48;
// Lives at the top of StyledPanel (the rounded white card). The panel's
// rounded top edge serves as the bar's top boundary, so no top border or
// background of its own — just a bottom border separating tabs from the
// body content underneath.
const StyledBar = styled.div`
align-items: center;
border-bottom: 1px solid ${themeCssVariables.border.color.light};
display: flex;
flex-shrink: 0;
height: ${SECONDARY_BAR_HEIGHT}px;
justify-content: center;
padding: 0 ${themeCssVariables.spacing[4]};
width: 100%;
`;
// SettingsSecondaryBar is a generic centered slot. Pass SettingsTabBar for
// tabbed pages or SettingsWizardStepBar for step-based pages.
export const SettingsSecondaryBar = ({
children,
}: SettingsSecondaryBarProps) => <StyledBar>{children}</StyledBar>;
@@ -0,0 +1,77 @@
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/TabListFromUrlOptionalEffect';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { styled } from '@linaria/react';
import { useEffect } from 'react';
import { TabButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsTabBarProps<TabId extends string = string> = {
tabs: SingleTabProps<TabId>[];
componentInstanceId: string;
loading?: boolean;
onChangeTab?: (tabId: TabId) => void;
};
const StyledTabRow = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[4]};
height: 100%;
`;
export const SettingsTabBar = <TabId extends string = string>({
tabs,
componentInstanceId,
loading,
onChangeTab,
}: SettingsTabBarProps<TabId>) => {
const visibleTabs = tabs.filter((tab) => !tab.hide);
const [activeTabId, setActiveTabId] = useAtomComponentState(
activeTabIdComponentState,
componentInstanceId,
);
const activeTabExists = visibleTabs.some((tab) => tab.id === activeTabId);
const initialActiveTabId = activeTabExists ? activeTabId : visibleTabs[0]?.id;
useEffect(() => {
setActiveTabId(initialActiveTabId);
onChangeTab?.((initialActiveTabId as TabId | null) ?? ('' as TabId));
}, [initialActiveTabId, setActiveTabId, onChangeTab]);
if (visibleTabs.length === 0) {
return null;
}
return (
<TabListComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<TabListFromUrlOptionalEffect
isInSidePanel={false}
tabListIds={tabs.map((tab) => tab.id)}
/>
<StyledTabRow>
{visibleTabs.map((tab) => (
<TabButton
key={tab.id}
id={tab.id}
title={tab.title}
LeftIcon={tab.Icon}
logo={tab.logo}
active={tab.id === activeTabId}
disabled={tab.disabled ?? loading}
pill={tab.pill}
to={`#${tab.id}`}
tooltipContent={tab.tooltipContent}
onClick={() => onChangeTab?.(tab.id)}
/>
))}
</StyledTabRow>
</TabListComponentInstanceContext.Provider>
);
};
@@ -0,0 +1,67 @@
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { IconChevronLeft } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsWizardStepBarProps = {
stepNumber: number;
label: string;
onBack?: () => void;
trailing?: ReactNode;
};
const StyledRow = styled.div`
align-items: center;
display: grid;
grid-template-columns: 1fr auto 1fr;
height: 100%;
width: 100%;
`;
const StyledLeft = styled.div`
align-items: center;
display: flex;
justify-content: flex-start;
`;
const StyledCenter = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.primary};
display: flex;
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[1]};
justify-content: center;
`;
const StyledRight = styled.div`
align-items: center;
display: flex;
justify-content: flex-end;
`;
export const SettingsWizardStepBar = ({
stepNumber,
label,
onBack,
trailing,
}: SettingsWizardStepBarProps) => (
<StyledRow>
<StyledLeft>
{onBack && (
<LightIconButton
Icon={IconChevronLeft}
size="small"
accent="tertiary"
onClick={onBack}
/>
)}
</StyledLeft>
<StyledCenter>
<span>{stepNumber}.</span>
<span>{label}</span>
</StyledCenter>
<StyledRight>{trailing}</StyledRight>
</StyledRow>
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 40 KiB

@@ -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';
@@ -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;
}
@@ -9,7 +9,7 @@ import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { Trans, useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import {
@@ -72,9 +72,8 @@ export const SettingsDevelopersWebhookForm = ({
return (
// oxlint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={getTitle()}
reserveTitleSpace
links={[
{
children: t`Workspace`,
@@ -194,7 +193,7 @@ export const SettingsDevelopersWebhookForm = ({
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
{!isCreationMode && (
<ConfirmationModal
confirmationPlaceholder={t`yes`}
@@ -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};
@@ -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';
@@ -0,0 +1 @@
export const WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS = '1fr 28px';
@@ -3,7 +3,7 @@ import { H2Title, IconReload, IconTrash } from 'twenty-ui/display';
import { Trans, useLingui } from '@lingui/react/macro';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
@@ -194,7 +194,7 @@ export const SettingPublicDomain = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Public domain`}
links={[
{
@@ -286,6 +286,6 @@ export const SettingPublicDomain = () => {
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -7,7 +7,7 @@ import { SettingsDomainRecords } from '@/settings/domains/components/SettingsDom
import { useSettingsCustomDomain } from '@/settings/domains/hooks/useSettingsCustomDomain';
import { customDomainRecordsState } from '@/settings/domains/states/customDomainRecordsState';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Trans, useLingui } from '@lingui/react/macro';
import { styled } from '@linaria/react';
@@ -62,7 +62,7 @@ export const SettingsCustomDomain = () => {
} = useSettingsCustomDomain();
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Custom Domain`}
links={[
{
@@ -133,6 +133,6 @@ export const SettingsCustomDomain = () => {
)}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -8,7 +8,7 @@ import {
} from '@/settings/domains/hooks/useSettingsSubdomain';
import { TextInput } from '@/ui/input/components/TextInput';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Trans, useLingui } from '@lingui/react/macro';
import { styled } from '@linaria/react';
@@ -41,7 +41,7 @@ export const SettingsSubdomain = () => {
return (
<>
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Subdomain`}
links={[
{
@@ -86,7 +86,7 @@ export const SettingsSubdomain = () => {
</StyledDomainFormWrapper>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
<ConfirmationModal
modalInstanceId={SUBDOMAIN_CHANGE_CONFIRMATION_MODAL_ID}
title={t`Change subdomain?`}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 111 KiB

@@ -19,9 +19,6 @@ const playgroundSetupFormSchema = z.object({
type PlaygroundSetupFormValues = z.infer<typeof playgroundSetupFormSchema>;
// 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;
@@ -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<AuthToken | null>({
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,
@@ -6,10 +6,11 @@ import { SettingsRolePermissionsObjectLevelObjectFieldPermissionTable } from '@/
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel';
import { SettingsRolePermissionsObjectLevelRecordLevelSection } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelSection';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsWizardStepBar } from '@/settings/components/layout/SettingsWizardStepBar';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { t } from '@lingui/core/macro';
import { useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath } from 'twenty-shared/types';
import {
@@ -36,6 +37,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
}: SettingsRolePermissionsObjectLevelObjectFormProps) => {
const [searchParams] = useSearchParams();
const fromAgentId = searchParams.get('fromAgent');
const navigate = useNavigate();
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
@@ -119,6 +121,11 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
? getSettingsPath(SettingsPath.AiAgentDetail, { agentId: agent.id })
: getSettingsPath(SettingsPath.RoleDetail, { roleId });
const backToObjectPickerPath =
fromAgentId && isDefined(agent)
? `${getSettingsPath(SettingsPath.RoleAddObjectLevel, { roleId })}?fromAgent=${agent.id}`
: getSettingsPath(SettingsPath.RoleAddObjectLevel, { roleId });
const objectPredicates =
settingsDraftRole.rowLevelPermissionPredicates?.filter(
(predicate) => predicate.objectMetadataId === objectMetadataItem.id,
@@ -138,17 +145,28 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
const isFinishDisabled = hasInvalidPredicate;
return (
<SubMenuTopBarContainer
title={t`2. Set ${objectLabelPlural} permissions`}
<SettingsPageLayout
title={
fromAgentId && isDefined(agent)
? agent.label
: (settingsDraftRole.label ?? '')
}
links={breadcrumbLinks}
actionButton={
<Button
title={t`Finish`}
variant="secondary"
size="small"
accent="blue"
to={isFinishDisabled ? undefined : finishButtonPath}
disabled={isFinishDisabled}
secondaryBar={
<SettingsWizardStepBar
stepNumber={2}
label={t`Set ${objectLabelPlural} permissions`}
onBack={() => navigate(backToObjectPickerPath)}
trailing={
<Button
title={t`Finish`}
variant="secondary"
size="small"
accent="blue"
to={isFinishDisabled ? undefined : finishButtonPath}
disabled={isFinishDisabled}
/>
}
/>
}
>
@@ -167,6 +185,6 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
hasOrganizationPlan={isRLSBillingEntitlementEnabled}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -10,8 +10,8 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
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 { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
@@ -134,7 +134,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={<SettingsRoleLabelContainer roleId={roleId} />}
links={[
{
@@ -163,15 +163,16 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
/>
)
}
>
<SettingsPageContainer>
<TabList
secondaryBar={
<SettingsTabBar
tabs={tabs}
className="tab-list"
componentInstanceId={
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId
}
/>
}
>
<SettingsPageContainer>
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT && (
<SettingsRoleAssignment roleId={roleId} isCreateMode={isCreateMode} />
)}
@@ -189,6 +190,6 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
/>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -1,95 +0,0 @@
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import {
Breadcrumb,
type BreadcrumbProps,
} from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { isDefined } from 'twenty-shared/utils';
import { styled } from '@linaria/react';
import { type JSX, type ReactNode } from 'react';
import { PageHeader } from './PageHeader';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SubMenuTopBarContainerProps = {
children: JSX.Element | JSX.Element[];
title?: string | JSX.Element;
reserveTitleSpace?: boolean;
actionButton?: ReactNode;
className?: string;
links: BreadcrumbProps['links'];
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`
display: flex;
flex-direction: column;
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.
const StyledBodyContentWrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
margin: 0 auto;
max-width: ${SETTINGS_CONTENT_MAX_WIDTH}px;
min-height: 0;
width: 100%;
`;
const StyledTitle = styled.span<{ reserveTitleSpace?: boolean }>`
color: ${themeCssVariables.font.color.primary};
display: flex;
font-size: ${themeCssVariables.font.size.lg};
font-weight: ${themeCssVariables.font.weight.semiBold};
gap: ${themeCssVariables.spacing[2]};
line-height: 1.2;
margin: ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]}
${themeCssVariables.spacing[2]};
min-height: ${({ reserveTitleSpace }) =>
reserveTitleSpace ? themeCssVariables.spacing[5] : 'none'};
`;
export const SubMenuTopBarContainer = ({
children,
title,
tag,
reserveTitleSpace,
actionButton,
className,
links,
}: SubMenuTopBarContainerProps) => {
return (
<StyledContainer className={className}>
<PageHeader title={<Breadcrumb links={links} />}>
{actionButton}
</PageHeader>
{/*
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.
*/}
<MainContainerLayoutWithSidePanel>
<StyledBodyContentWrapper>
<InformationBannerWrapper />
{(isDefined(title) || reserveTitleSpace === true) && (
<StyledTitle reserveTitleSpace={reserveTitleSpace}>
{title}
{tag}
</StyledTitle>
)}
{children}
</StyledBodyContentWrapper>
</MainContainerLayoutWithSidePanel>
</StyledContainer>
);
};
@@ -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,
@@ -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'};
@@ -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};
@@ -1,7 +1,7 @@
import { Trans, useLingui } from '@lingui/react/macro';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsBillingContent } from '@/settings/billing/components/SettingsBillingContent';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
@@ -15,7 +15,7 @@ export const SettingsBilling = () => {
const { isPlansLoaded } = usePlans();
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Billing`}
links={[
{
@@ -26,6 +26,6 @@ export const SettingsBilling = () => {
]}
>
{currentWorkspace && isPlansLoaded ? <SettingsBillingContent /> : <></>}
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -8,7 +8,7 @@ import { NameFields } from '@/settings/profile/components/NameFields';
import { WorkspaceMemberPictureUploader } from '@/settings/workspace-member/components/WorkspaceMemberPictureUploader';
import { useCanChangePassword } from '@/settings/profile/hooks/useCanChangePassword';
import { useCurrentUserWorkspaceTwoFactorAuthentication } from '@/settings/two-factor-authentication/hooks/useCurrentUserWorkspaceTwoFactorAuthentication';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { Trans, useLingui } from '@lingui/react/macro';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath } from 'twenty-shared/types';
@@ -35,7 +35,7 @@ export const SettingsProfile = () => {
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Profile`}
links={[
{
@@ -99,6 +99,6 @@ export const SettingsProfile = () => {
<DeleteAccount />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -12,7 +12,7 @@ import { TwoFactorAuthenticationVerificationForSettings } from '@/settings/two-f
import { useCurrentUserWorkspaceTwoFactorAuthentication } from '@/settings/two-factor-authentication/hooks/useCurrentUserWorkspaceTwoFactorAuthentication';
import { useTwoFactorVerificationForSettings } from '@/settings/two-factor-authentication/hooks/useTwoFactorVerificationForSettings';
import { extractSecretFromOtpUri } from '@/settings/two-factor-authentication/utils/extractSecretFromOtpUri';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
@@ -110,7 +110,7 @@ export const SettingsTwoFactorAuthenticationMethod = () => {
return (
// oxlint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...verificationForm.formConfig}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Two Factor Authentication`}
links={[
{
@@ -183,7 +183,7 @@ export const SettingsTwoFactorAuthenticationMethod = () => {
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</FormProvider>
);
};
@@ -1,7 +1,7 @@
import { Trans, useLingui } from '@lingui/react/macro';
import { SettingsUsageAnalyticsSection } from '@/settings/usage/components/SettingsUsageAnalyticsSection';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
@@ -9,7 +9,7 @@ export const SettingsUsage = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Usage`}
links={[
{
@@ -26,6 +26,6 @@ export const SettingsUsage = () => {
<SettingsPageContainer>
<SettingsUsageAnalyticsSection />
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -6,7 +6,7 @@ import { UsageDailyChartSection } from '@/settings/usage/components/UsageDailyCh
import { UsageSectionSkeleton } from '@/settings/usage/components/UsageSectionSkeleton';
import { useUsageAnalyticsData } from '@/settings/usage/hooks/useUsageAnalyticsData';
import { useUsageValueFormatter } from '@/settings/usage/hooks/useUsageValueFormatter';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -89,10 +89,7 @@ export const SettingsUsageUserDetail = () => {
if (isInitialLoading) {
return (
<SubMenuTopBarContainer
title={tLingui`User Usage`}
links={breadcrumbLinks}
>
<SettingsPageLayout title={tLingui`User Usage`} links={breadcrumbLinks}>
<SettingsPageContainer>
<SkeletonTheme
baseColor={theme.background.tertiary}
@@ -110,12 +107,12 @@ export const SettingsUsageUserDetail = () => {
<UsageSectionSkeleton />
</SkeletonTheme>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
}
return (
<SubMenuTopBarContainer title={tLingui`User Usage`} links={breadcrumbLinks}>
<SettingsPageLayout title={tLingui`User Usage`} links={breadcrumbLinks}>
<SettingsPageContainer>
<StyledUserHeader>
<Avatar
@@ -159,6 +156,6 @@ export const SettingsUsageUserDetail = () => {
sectionId="user-type"
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -5,7 +5,7 @@ import { SettingsWorkspaceDomainCard } from '@/settings/domains/components/Setti
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
import { NameField } from '@/settings/workspace/components/NameField';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath } from 'twenty-shared/types';
@@ -21,7 +21,7 @@ export const SettingsWorkspace = () => {
);
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`General`}
links={[
{
@@ -53,6 +53,6 @@ export const SettingsWorkspace = () => {
<DeleteWorkspace />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -4,7 +4,7 @@ import { isEmailGroupEnabledState } from '@/client-config/states/isEmailGroupEna
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsWorkspaceEmailGroupSection } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSection';
import { SettingsWorkspaceEmailingDomainsSection } from '@/settings/workspace/components/SettingsWorkspaceEmailingDomainsSection';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
@@ -25,7 +25,7 @@ export const SettingsWorkspaceEmail = () => {
const showEmailGroupSection = isEmailGroupEnabled;
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Email`}
links={[
{
@@ -39,6 +39,6 @@ export const SettingsWorkspaceEmail = () => {
{showEmailGroupSection && <SettingsWorkspaceEmailGroupSection />}
<SettingsWorkspaceEmailingDomainsSection />
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -1,23 +1,57 @@
import { SettingsPath } from 'twenty-shared/types';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { SettingsAccountsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsBlocklistSection';
import { SettingsAccountsCalendarChannelsContainer } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsContainer';
import { SettingsAccountsConnectedAccountsListCard } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsListCard';
import { SettingsAccountsMessageChannelsContainer } from '@/settings/accounts/components/SettingsAccountsMessageChannelsContainer';
import { SettingsAccountsSettingsSection } from '@/settings/accounts/components/SettingsAccountsSettingsSection';
import { useMyConnectedAccounts } from '@/settings/accounts/hooks/useMyConnectedAccounts';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import {
H2Title,
IconCalendarEvent,
IconMail,
IconSettings,
} from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { SETTINGS_ACCOUNTS_TABS } from '~/pages/settings/accounts/constants/SettingsAccountsTabs';
export const SettingsAccounts = () => {
const { t } = useLingui();
const { accounts: allAccounts, loading } = useMyConnectedAccounts();
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
SETTINGS_ACCOUNTS_TABS.COMPONENT_INSTANCE_ID,
);
const activeTab = activeTabId ?? SETTINGS_ACCOUNTS_TABS.TABS_IDS.GENERAL;
const tabs = [
{
id: SETTINGS_ACCOUNTS_TABS.TABS_IDS.GENERAL,
title: t`General`,
Icon: IconSettings,
},
{
id: SETTINGS_ACCOUNTS_TABS.TABS_IDS.EMAILS,
title: t`Emails`,
Icon: IconMail,
},
{
id: SETTINGS_ACCOUNTS_TABS.TABS_IDS.CALENDARS,
title: t`Calendars`,
Icon: IconCalendarEvent,
},
];
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Account`}
links={[
{
@@ -26,26 +60,46 @@ export const SettingsAccounts = () => {
},
{ children: t`Account` },
]}
secondaryBar={
<SettingsTabBar
tabs={tabs}
componentInstanceId={SETTINGS_ACCOUNTS_TABS.COMPONENT_INSTANCE_ID}
/>
}
>
<SettingsPageContainer>
{loading ? (
<SettingsSectionSkeletonLoader />
) : (
<>
<Section>
<H2Title
title={t`Connected accounts`}
description={t`Manage your internet accounts.`}
/>
<SettingsAccountsConnectedAccountsListCard
accounts={allAccounts}
/>
</Section>
<SettingsAccountsBlocklistSection />
<SettingsAccountsSettingsSection />
{activeTab === SETTINGS_ACCOUNTS_TABS.TABS_IDS.GENERAL && (
<>
<Section>
<H2Title
title={t`Connected accounts`}
description={t`Manage your internet accounts.`}
/>
<SettingsAccountsConnectedAccountsListCard
accounts={allAccounts}
/>
</Section>
<SettingsAccountsBlocklistSection />
<SettingsAccountsSettingsSection />
</>
)}
{activeTab === SETTINGS_ACCOUNTS_TABS.TABS_IDS.EMAILS && (
<Section>
<SettingsAccountsMessageChannelsContainer />
</Section>
)}
{activeTab === SETTINGS_ACCOUNTS_TABS.TABS_IDS.CALENDARS && (
<Section>
<SettingsAccountsCalendarChannelsContainer />
</Section>
)}
</>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -1,34 +0,0 @@
import { SettingsAccountsCalendarChannelsContainer } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Trans, useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { Section } from 'twenty-ui/layout';
export const SettingsAccountsCalendars = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`Calendars`}
links={[
{
children: <Trans>User</Trans>,
href: getSettingsPath(SettingsPath.ProfilePage),
},
{
children: <Trans>Accounts</Trans>,
href: getSettingsPath(SettingsPath.Accounts),
},
{ children: <Trans>Calendars</Trans> },
]}
>
<SettingsPageContainer>
<Section>
<SettingsAccountsCalendarChannelsContainer />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -4,7 +4,7 @@ import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { IconDeviceFloppy } from 'twenty-ui/display';
@@ -29,7 +29,7 @@ export const SettingsAccountsConfigurationStepCalendar = ({
const stepTitle = t`${stepNumber}. Calendar`;
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={stepTitle}
links={[
{
@@ -61,6 +61,6 @@ export const SettingsAccountsConfigurationStepCalendar = ({
calendarChannel={calendarChannel}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -3,7 +3,7 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconChevronRight, IconPlus } from 'twenty-ui/display';
@@ -27,7 +27,7 @@ export const SettingsAccountsConfigurationStepEmail = ({
const { t } = useLingui();
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`1. Email`}
links={[
{
@@ -71,6 +71,6 @@ export const SettingsAccountsConfigurationStepEmail = ({
messageChannel={messageChannel}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -1,34 +0,0 @@
import { SettingsAccountsMessageChannelsContainer } from '@/settings/accounts/components/SettingsAccountsMessageChannelsContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { Section } from 'twenty-ui/layout';
export const SettingsAccountsEmails = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`Emails`}
links={[
{
children: t`User`,
href: getSettingsPath(SettingsPath.ProfilePage),
},
{
children: t`Accounts`,
href: getSettingsPath(SettingsPath.Accounts),
},
{ children: t`Emails` },
]}
>
<SettingsPageContainer>
<Section>
<SettingsAccountsMessageChannelsContainer />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -1,13 +1,13 @@
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
export const SettingsNewAccount = () => {
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`New Account`}
links={[
{
@@ -24,6 +24,6 @@ export const SettingsNewAccount = () => {
<SettingsPageContainer>
<SettingsNewAccountSection />
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -1,104 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { HttpResponse, graphql } from 'msw';
import { SettingsAccountsCalendars } from '~/pages/settings/accounts/SettingsAccountsCalendars';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Accounts/SettingsAccountsCalendars',
component: SettingsAccountsCalendars,
decorators: [PageDecorator],
args: {
routePath: '/settings/accounts/calendars',
},
parameters: {
layout: 'fullscreen',
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsAccountsCalendars>;
export const NoConnectedAccount: Story = {};
export const TwoConnectedAccounts: Story = {
parameters: {
msw: {
handlers: [
...graphqlMocks.handlers,
graphql.query('MyConnectedAccounts', () => {
return HttpResponse.json({
data: {
myConnectedAccounts: [
{
id: '20202020-954c-4d76-9a87-e5f072d4b7ef',
handle: 'test.test@gmail.com',
provider: 'google',
authFailedAt: null,
scopes: ['calendar'],
handleAliases: '',
lastSignedInAt: null,
userWorkspaceId: '20202020-03f2-4d83-b0d5-2ec2bcee72d4',
connectionProviderId: null,
name: 'Test User',
visibility: 'SHARE_EVERYTHING',
lastCredentialsRefreshedAt: null,
connectionParameters: null,
createdAt: '2024-07-03T20:03:35.064Z',
updatedAt: '2024-07-03T20:03:35.064Z',
},
],
},
});
}),
graphql.query('MyCalendarChannels', () => {
return HttpResponse.json({
data: {
myCalendarChannels: [
{
id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6f',
handle: 'test.test@gmail.com',
connectedAccountId: '20202020-954c-4d76-9a87-e5f072d4b7ef',
isSyncEnabled: true,
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
syncStatus: 'COMPLETED',
visibility: 'SHARE_EVERYTHING',
contactAutoCreationPolicy: 'SENT',
isContactAutoCreationEnabled: true,
createdAt: '2024-07-03T20:03:11.903Z',
updatedAt: '2024-07-03T20:03:11.903Z',
},
{
id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6a',
handle: 'test.test2@gmail.com',
connectedAccountId: '20202020-954c-4d76-9a87-e5f072d4b7ef',
isSyncEnabled: true,
syncStage: 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING',
syncStatus: 'COMPLETED',
visibility: 'SHARE_EVERYTHING',
contactAutoCreationPolicy: 'SENT',
isContactAutoCreationEnabled: true,
createdAt: '2024-07-03T20:03:11.903Z',
updatedAt: '2024-07-03T20:03:11.903Z',
},
],
},
});
}),
graphql.query('MyMessageChannels', () => {
return HttpResponse.json({
data: {
myMessageChannels: [],
},
});
}),
],
},
},
};
@@ -1,111 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { HttpResponse, graphql } from 'msw';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccountsEmails';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Accounts/SettingsAccountsEmails',
component: SettingsAccountsEmails,
decorators: [PageDecorator],
args: {
routePath: '/settings/accounts/emails',
},
parameters: {
layout: 'fullscreen',
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsAccountsEmails>;
export const NoConnectedAccount: Story = {};
export const TwoConnectedAccounts: Story = {
parameters: {
msw: {
handlers: [
...graphqlMocks.handlers,
graphql.query('MyConnectedAccounts', () => {
return HttpResponse.json({
data: {
myConnectedAccounts: [
{
id: '20202020-954c-4d76-9a87-e5f072d4b7ef',
handle: 'test.test@gmail.com',
provider: 'google',
authFailedAt: null,
scopes: ['email'],
handleAliases: '',
lastSignedInAt: null,
userWorkspaceId: '20202020-03f2-4d83-b0d5-2ec2bcee72d4',
connectionProviderId: null,
name: 'Test User',
visibility: 'SHARE_EVERYTHING',
lastCredentialsRefreshedAt: null,
connectionParameters: null,
createdAt: '2024-07-03T20:03:35.064Z',
updatedAt: '2024-07-03T20:03:35.064Z',
},
],
},
});
}),
graphql.query('MyMessageChannels', () => {
return HttpResponse.json({
data: {
myMessageChannels: [
{
id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6f',
handle: 'test.test@gmail.com',
connectedAccountId: '20202020-954c-4d76-9a87-e5f072d4b7ef',
type: 'email',
isSyncEnabled: true,
syncStage: 'MESSAGE_LIST_FETCH_PENDING',
syncStatus: 'COMPLETED',
visibility: 'SHARE_EVERYTHING',
contactAutoCreationPolicy: 'SENT',
isContactAutoCreationEnabled: true,
excludeNonProfessionalEmails: true,
excludeGroupEmails: true,
createdAt: '2024-07-03T20:03:11.903Z',
updatedAt: '2024-07-03T20:03:11.903Z',
},
{
id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6a',
handle: 'test.test2@gmail.com',
connectedAccountId: '20202020-954c-4d76-9a87-e5f072d4b7ef',
type: 'email',
isSyncEnabled: true,
syncStage: 'MESSAGE_LIST_FETCH_PENDING',
syncStatus: 'COMPLETED',
visibility: 'SHARE_EVERYTHING',
contactAutoCreationPolicy: 'SENT',
isContactAutoCreationEnabled: true,
excludeNonProfessionalEmails: true,
excludeGroupEmails: true,
createdAt: '2024-07-03T20:03:11.903Z',
updatedAt: '2024-07-03T20:03:11.903Z',
},
],
},
});
}),
graphql.query('MyCalendarChannels', () => {
return HttpResponse.json({
data: {
myCalendarChannels: [],
},
});
}),
],
},
},
};
@@ -0,0 +1,8 @@
export const SETTINGS_ACCOUNTS_TABS = {
COMPONENT_INSTANCE_ID: 'settings-accounts-tabs',
TABS_IDS: {
GENERAL: 'general',
EMAILS: 'emails',
CALENDARS: 'calendars',
},
} as const;
@@ -1,6 +1,6 @@
import { SettingsAdminContent } from '@/settings/admin-panel/components/SettingsAdminContent';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -9,7 +9,7 @@ export const SettingsAdmin = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Admin Panel`}
links={[
{
@@ -22,6 +22,6 @@ export const SettingsAdmin = () => {
<SettingsPageContainer>
<SettingsAdminContent />
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -40,7 +40,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import {
type AdminAiModelConfig,
SetAdminAiModelEnabledDocument,
@@ -303,7 +303,7 @@ export const SettingsAdminAiProviderDetail = () => {
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -439,6 +439,6 @@ export const SettingsAdminAiProviderDetail = () => {
confirmButtonText={t`Remove`}
confirmButtonAccent="danger"
/>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -8,7 +8,7 @@ import {
} from '~/generated-admin/graphql';
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
import { APPLICATION_REGISTRATION_ADMIN_PATH } from '@/settings/admin-panel/apps/constants/ApplicationRegistrationAdminPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
@@ -73,7 +73,7 @@ export const SettingsAdminApplicationRegistrationConfigVariableDetail = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -101,6 +101,6 @@ export const SettingsAdminApplicationRegistrationConfigVariableDetail = () => {
variable={variable}
onUpdateVariable={onUpdateVariable}
/>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -4,7 +4,7 @@ import { FindOneAdminApplicationRegistrationDocument } from '~/generated-admin/g
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { useLingui } from '@lingui/react/macro';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
import { APPLICATION_REGISTRATION_ADMIN_PATH } from '@/settings/admin-panel/apps/constants/ApplicationRegistrationAdminPath';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@@ -102,7 +102,7 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={
<StyledTitleContainer>
<Avatar
@@ -134,6 +134,6 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
/>
{renderActiveTabContent()}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -9,7 +9,7 @@ import { ConfigVariableValueInput } from '@/settings/admin-panel/config-variable
import { useConfigVariableActions } from '@/settings/admin-panel/config-variables/hooks/useConfigVariableActions';
import { ConfigVariableEdit } from '@/settings/config-variables/components/ConfigVariableEdit';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath, type ConfigVariableValue } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
@@ -97,7 +97,7 @@ export const SettingsAdminConfigVariableDetails = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -144,6 +144,6 @@ export const SettingsAdminConfigVariableDetails = () => {
/>
}
/>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -4,7 +4,7 @@ import { SettingsAdminIndicatorHealthStatusContent } from '@/settings/admin-pane
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
@@ -47,7 +47,7 @@ export const SettingsAdminIndicatorHealthStatus = () => {
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -99,6 +99,6 @@ export const SettingsAdminIndicatorHealthStatus = () => {
</Section>
</SettingsAdminIndicatorHealthContext.Provider>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -2,7 +2,7 @@ import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApo
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useMutation, useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
@@ -63,7 +63,7 @@ export const SettingsAdminInferredVersion = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -104,6 +104,6 @@ export const SettingsAdminInferredVersion = () => {
</StyledRefreshButtonContainer>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -3,7 +3,7 @@ import { getUpgradeHealthStatusBadge } from '@/settings/admin-panel/utils/getUpg
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { UserContext } from '@/users/contexts/UserContext';
import { useMutation, useQuery } from '@apollo/client/react';
@@ -94,7 +94,7 @@ export const SettingsAdminInstanceStatus = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -177,6 +177,6 @@ export const SettingsAdminInstanceStatus = () => {
</StyledRefreshButtonContainer>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -23,7 +23,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { Checkbox, Toggle } from 'twenty-ui/input';
const StyledComboInputContainer = styled.div`
@@ -278,7 +278,7 @@ export const SettingsAdminNewAiModel = () => {
return (
<form onSubmit={form.handleSubmit(handleSave)}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`New Model`}
links={[
{
@@ -539,7 +539,7 @@ export const SettingsAdminNewAiModel = () => {
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</form>
);
};
@@ -25,7 +25,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
type ModelsDevProvider = { id: string; modelCount: number; npm: AiSdkPackage };
@@ -229,7 +229,7 @@ export const SettingsAdminNewAiProvider = () => {
return (
<form onSubmit={form.handleSubmit(handleSave)}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`New AI Provider`}
links={[
{
@@ -444,7 +444,7 @@ export const SettingsAdminNewAiProvider = () => {
</>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</form>
);
};
@@ -1,6 +1,6 @@
import { SettingsAdminQueueJobsTable } from '@/settings/admin-panel/health-status/components/SettingsAdminQueueJobsTable';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { plural, t } from '@lingui/core/macro';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -52,7 +52,7 @@ export const SettingsAdminQueueDetail = () => {
: t`Loading retention configuration...`;
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Queue: ${queueName}`}
links={[
{
@@ -83,6 +83,6 @@ export const SettingsAdminQueueDetail = () => {
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -19,7 +19,7 @@ import { useHandleImpersonate } from '@/settings/admin-panel/hooks/useHandleImpe
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
@@ -123,7 +123,7 @@ export const SettingsAdminUserDetail = () => {
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -191,6 +191,6 @@ export const SettingsAdminUserDetail = () => {
</>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -11,7 +11,7 @@ import { SettingsAdminChatThreadMessageList } from '@/settings/admin-panel/compo
import { GET_ADMIN_CHAT_THREAD_MESSAGES } from '@/settings/admin-panel/graphql/queries/getAdminChatThreadMessages';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { type GetAdminChatThreadMessagesQuery } from '~/generated-admin/graphql';
@@ -40,7 +40,7 @@ export const SettingsAdminWorkspaceChatThread = () => {
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -67,6 +67,6 @@ export const SettingsAdminWorkspaceChatThread = () => {
<SettingsAdminChatThreadMessageList messages={messages} />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -21,7 +21,7 @@ import { useHandleImpersonate } from '@/settings/admin-panel/hooks/useHandleImpe
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { Table } from '@/ui/layout/table/components/Table';
@@ -201,7 +201,7 @@ export const SettingsAdminWorkspaceDetail = () => {
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -411,6 +411,6 @@ export const SettingsAdminWorkspaceDetail = () => {
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -3,7 +3,7 @@ import { SettingsAdminWorkspacesByHealthAccordion } from '@/settings/admin-panel
import { SettingsAdminWorkspacesStatusSummaryCard } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkspacesStatusSummaryCard';
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 { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useMutation, useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { plural, t } from '@lingui/core/macro';
@@ -70,7 +70,7 @@ export const SettingsAdminWorkspacesStatus = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Other`,
@@ -131,6 +131,6 @@ export const SettingsAdminWorkspacesStatus = () => {
</StyledAccordionCardsContainer>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -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 { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar';
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,
@@ -122,7 +76,7 @@ export const SettingsAI = () => {
const isUsageTab = resolvedTabId === SETTINGS_AI_TABS.TABS_IDS.USAGE;
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`AI`}
actionButton={
isSkillsTab ? (
@@ -152,6 +106,12 @@ export const SettingsAI = () => {
},
{ children: t`AI` },
]}
secondaryBar={
<SettingsTabBar
tabs={tabs}
componentInstanceId={SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID}
/>
}
>
<SettingsPageContainer>
<Section>
@@ -182,16 +142,12 @@ export const SettingsAI = () => {
playButtonAriaLabel={t`Watch AI demo`}
/>
</Section>
<TabList
tabs={tabs}
componentInstanceId={SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID}
/>
{isOverviewTab && <SettingsAiOverviewTab />}
{isModelsTab && <SettingsAiModelsTab />}
{isSkillsTab && <SettingsAgentSkillsTab />}
{isToolsTab && <SettingsAgentToolsTab />}
{isUsageTab && <SettingsAiUsageTab />}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -10,7 +10,7 @@ import { useSaveDraftRoleToDB } from '@/settings/roles/role/hooks/useSaveDraftRo
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
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';
@@ -401,7 +401,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
return (
<>
<SettingsRolesQueryEffect />
<SubMenuTopBarContainer
<SettingsPageLayout
title={title}
actionButton={
isCreateMode ? (
@@ -470,7 +470,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
)}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</>
);
};
@@ -1,7 +1,7 @@
import { AiChatAssistantMessageRenderer } from '@/ai/components/AiChatAssistantMessageRenderer';
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
@@ -74,7 +74,7 @@ export const SettingsAgentTurnDetail = () => {
if (loading) {
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Turn Details`}
links={[
{
@@ -95,13 +95,13 @@ export const SettingsAgentTurnDetail = () => {
<SettingsPageContainer>
<Skeleton height={200} />
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
}
if (!turn) {
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Turn Not Found`}
links={[
{
@@ -115,12 +115,12 @@ export const SettingsAgentTurnDetail = () => {
<SettingsPageContainer>
<div>{t`Turn not found`}</div>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
}
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Turn Details`}
links={[
{
@@ -241,6 +241,6 @@ export const SettingsAgentTurnDetail = () => {
)}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -3,7 +3,7 @@ import { styled } from '@linaria/react';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useQuery } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
@@ -71,7 +71,7 @@ export const SettingsAiPrompts = () => {
: '';
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Workspace`,
@@ -156,6 +156,6 @@ export const SettingsAiPrompts = () => {
</StyledFormContainer>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -6,7 +6,7 @@ import { UsageDailyChartSection } from '@/settings/usage/components/UsageDailyCh
import { UsageSectionSkeleton } from '@/settings/usage/components/UsageSectionSkeleton';
import { AI_OPERATION_TYPES } from '@/settings/usage/constants/AiOperationTypes';
import { useUsageAnalyticsData } from '@/settings/usage/hooks/useUsageAnalyticsData';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { t } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
@@ -49,7 +49,7 @@ export const SettingsAiUsageUserDetail = () => {
if (isInitialLoading) {
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={tLingui`AI User Usage`}
links={breadcrumbLinks}
>
@@ -57,12 +57,12 @@ export const SettingsAiUsageUserDetail = () => {
<UsageSectionSkeleton />
<UsageSectionSkeleton />
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
}
return (
<SubMenuTopBarContainer title={displayName} links={breadcrumbLinks}>
<SettingsPageLayout title={displayName} links={breadcrumbLinks}>
<SettingsPageContainer>
{!hasAnyData && (
<Section>
@@ -102,6 +102,6 @@ export const SettingsAiUsageUserDetail = () => {
sectionId="ai-user-model"
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -13,7 +13,7 @@ import { IconPicker } from '@/ui/input/components/IconPicker';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import { TitleInput } from '@/ui/input/components/TitleInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { t } from '@lingui/core/macro';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
@@ -422,7 +422,7 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
);
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={title}
actionButton={
isCreateMode ? (
@@ -602,6 +602,6 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
confirmButtonText={t`Delete`}
loading={isSubmitting}
/>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -14,7 +14,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextArea } from '@/ui/input/components/TextArea';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined, isValidUuid } from 'twenty-shared/utils';
@@ -169,7 +169,7 @@ export const SettingsToolDetail = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={
isCustomTool ? (
<SettingsLogicFunctionLabelContainer
@@ -263,6 +263,6 @@ export const SettingsToolDetail = () => {
confirmButtonText={t`Delete`}
loading={isDeleting}
/>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -104,7 +104,7 @@ export const SettingsAgentSkillsTab = () => {
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconArchive}
onToggleChange={() => setShowDeactivated(!showDeactivated)}
onToggleChange={setShowDeactivated}
toggled={showDeactivated}
text={t`Deactivated`}
toggleSize="small"
@@ -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 (
<Section>
<H2Title
@@ -173,27 +90,21 @@ export const SettingsAgentToolsTab = () => {
<DropdownMenuItemsContainer>
<MenuItemToggle
LeftIcon={IconTool}
onToggleChange={() =>
setShowCustomTools(!showCustomTools)
}
onToggleChange={setShowCustomTools}
toggled={showCustomTools}
text={t`Custom`}
toggleSize="small"
/>
<MenuItemToggle
LeftIcon={IconLock}
onToggleChange={() =>
setShowManagedTools(!showManagedTools)
}
onToggleChange={setShowManagedTools}
toggled={showManagedTools}
text={t`Managed`}
toggleSize="small"
/>
<MenuItemToggle
LeftIcon={IconPuzzle}
onToggleChange={() =>
setShowStandardTools(!showStandardTools)
}
onToggleChange={setShowStandardTools}
toggled={showStandardTools}
text={t`Standard`}
toggleSize="small"
@@ -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<string, Application>;
marketplaceAppByUniversalIdentifier: Map<string, MarketplaceApp>;
applicationById: Map<string, SettingsAgentToolApplication>;
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,
@@ -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"
/>
</StyledCustomModelsContainer>
@@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
export const FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE = gql`
query FindManyApplicationsForToolTable {
findManyApplications {
id
name
universalIdentifier
logo
}
}
`;
@@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
export const FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE = gql`
query FindManyMarketplaceAppsForToolTable {
findManyMarketplaceApps {
id
universalIdentifier
icon
logo
}
}
`;
@@ -0,0 +1,57 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
export const useCreateTool = () => {
const navigate = useNavigate();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = 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: {} },
},
},
});
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);
}
};
return { handleCreateTool, isCreatingTool };
};
@@ -0,0 +1,79 @@
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,
]),
);
const marketplaceAppByUniversalIdentifier = new Map(
(marketplaceAppsData?.findManyMarketplaceApps ?? []).map(
(marketplaceApp) => [marketplaceApp.universalIdentifier, marketplaceApp],
),
);
const isLoading = toolIndexLoading && !toolIndexError;
return {
allTools,
applicationById,
marketplaceAppByUniversalIdentifier,
currentWorkspace,
isLoading,
};
};
@@ -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<string>,
) => {
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,
};
};
@@ -0,0 +1,6 @@
export type SettingsAgentToolApplication = {
id: string;
name: string;
universalIdentifier: string;
logo?: string | null;
};
@@ -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;
};
@@ -0,0 +1,6 @@
export type SettingsAgentToolMarketplaceApp = {
id: string;
universalIdentifier: string;
icon: string;
logo?: string | null;
};
@@ -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 ?? ''
);
};
@@ -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,
});
@@ -1,6 +1,6 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useQuery } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { useParams } from 'react-router-dom';
@@ -52,7 +52,7 @@ export const SettingsApplicationCommandMenuItemDetail = () => {
];
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={commandMenuItem?.label ?? t`Command menu item`}
links={breadcrumbLinks}
>
@@ -76,6 +76,6 @@ export const SettingsApplicationCommandMenuItemDetail = () => {
/>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -23,7 +23,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
@@ -283,7 +283,7 @@ export const SettingsApplicationConnectionDetail = () => {
: [];
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={connectionLabel}
links={[
{
@@ -405,6 +405,6 @@ export const SettingsApplicationConnectionDetail = () => {
</>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -8,7 +8,7 @@ import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMeta
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import type { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
@@ -325,7 +325,7 @@ export const SettingsApplicationDetails = () => {
return (
<CurrentApplicationContext.Provider value={application?.id ?? null}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={
<SettingsApplicationDetailTitle
displayName={displayName}
@@ -349,7 +349,7 @@ export const SettingsApplicationDetails = () => {
<TabList tabs={tabs} componentInstanceId={APPLICATION_DETAIL_ID} />
{renderActiveTabContent()}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</CurrentApplicationContext.Provider>
);
};
@@ -1,6 +1,6 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
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';
@@ -97,7 +97,7 @@ export const SettingsApplicationFrontComponentDetail = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={frontComponent?.name ?? t`Front component`}
links={breadcrumbLinks}
>
@@ -105,6 +105,6 @@ export const SettingsApplicationFrontComponentDetail = () => {
<TabList tabs={tabs} componentInstanceId={instanceId} />
{loading ? <SettingsSectionSkeletonLoader /> : renderActiveTabContent()}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -1,4 +1,4 @@
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useQuery } from '@apollo/client/react';
import { useParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
@@ -94,7 +94,7 @@ export const SettingsApplicationRegistrationDetails = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={
<StyledTitleContainer>
<Avatar
@@ -105,9 +105,9 @@ export const SettingsApplicationRegistrationDetails = () => {
placeholderColorSeed={registration.name}
/>
{registration.name}
<Tag text={t`Owner`} color={'gray'} />
</StyledTitleContainer>
}
tag={<Tag text={t`Owner`} color={'gray'} />}
links={[
{
children: t`Workspace`,
@@ -132,6 +132,6 @@ export const SettingsApplicationRegistrationDetails = () => {
/>
{renderActiveTabContent()}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -1,8 +1,8 @@
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -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,
@@ -69,7 +68,7 @@ export const SettingsApplications = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`Applications`}
links={[
{
@@ -78,12 +77,18 @@ export const SettingsApplications = () => {
},
{ children: t`Applications` },
]}
secondaryBar={
<SettingsTabBar
tabs={tabs}
componentInstanceId={APPLICATIONS_TAB_LIST_ID}
/>
}
>
<SettingsPageContainer>
<Section>
<SettingsDiscoveryHeroCard
lightSrc={placeholderHeroLight}
darkSrc={placeholderHeroDark}
lightSrc={coverLight}
darkSrc={coverDark}
instanceIdPrefix={APPLICATIONS_HERO_INSTANCE_ID_PREFIX}
tabs={[
{
@@ -108,9 +113,8 @@ export const SettingsApplications = () => {
playButtonAriaLabel={t`Watch apps demo`}
/>
</Section>
<TabList tabs={tabs} componentInstanceId={APPLICATIONS_TAB_LIST_ID} />
{renderActiveTabContent()}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -4,7 +4,7 @@ import { useInstallMarketplaceAppWithPermissionValidation } from '@/marketplace/
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
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';
@@ -271,7 +271,7 @@ export const SettingsAvailableApplicationDetails = () => {
return (
<CurrentApplicationContext.Provider value={application?.id ?? null}>
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Workspace`,
@@ -299,7 +299,7 @@ export const SettingsAvailableApplicationDetails = () => {
/>
{renderActiveTabContent()}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
<SettingsApplicationInstallPermissionValidationModal
modalInstanceId={modalInstanceId}
appDisplayName={displayName}
@@ -65,8 +65,8 @@ jest.mock('@/settings/components/SettingsPageContainer', () => ({
),
}));
jest.mock('@/ui/layout/page/components/SubMenuTopBarContainer', () => ({
SubMenuTopBarContainer: ({ children }: { children: ReactNode }) => (
jest.mock('@/settings/components/layout/SettingsPageLayout', () => ({
SettingsPageLayout: ({ children }: { children: ReactNode }) => (
<>{children}</>
),
}));
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

@@ -6,7 +6,7 @@ import {
UpdateApplicationRegistrationVariableDocument,
} from '~/generated-metadata/graphql';
import { useMutation, useQuery } from '@apollo/client/react';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
@@ -67,7 +67,7 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
};
return (
<SubMenuTopBarContainer
<SettingsPageLayout
links={[
{
children: t`Workspace`,
@@ -100,6 +100,6 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
variable={variable}
onUpdateVariable={onUpdateVariable}
/>
</SubMenuTopBarContainer>
</SettingsPageLayout>
);
};
@@ -10,7 +10,7 @@ import {
settingsDataModelObjectAboutFormSchema,
} from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema';
import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@@ -77,7 +77,7 @@ export const SettingsNewObject = () => {
return (
// oxlint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
<SubMenuTopBarContainer
<SettingsPageLayout
title={t`New Object`}
links={[
{
@@ -114,7 +114,7 @@ export const SettingsNewObject = () => {
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</SettingsPageLayout>
</FormProvider>
);
};

Some files were not shown because too many files have changed in this diff Show More