Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20b621ebb3 | ||
|
|
cce466d9bd | ||
|
|
1681326a1c | ||
|
|
e2fa51b9b6 | ||
|
|
45189eb96d | ||
|
|
fde53e01b1 | ||
|
|
aaadcd9d78 | ||
|
|
31928548a8 | ||
|
|
d6c3b4c277 | ||
|
|
0e6fa33815 |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 64 KiB |
|
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>
|
||||
);
|
||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 38 KiB |
|
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?`}
|
||||
|
||||
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 112 KiB |
|
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}</>
|
||||
),
|
||||
}));
|
||||
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
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>
|
||||
);
|
||||
};
|
||||
|
||||