mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
## Summary - AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from `FeatureFlagKey`, the public flag catalog, and the dev seeder. - Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent, agent chat, chat subscription, role-to-agent assignment, workflow AI step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard` wiring in the AI and workflow modules. - Removes frontend gating from settings nav, role permissions/assignment/applicability, command menu hotkeys, side panel, mobile/drawer nav, and the agent chat provider so AI UI is always on. Tests and generated GraphQL/SDK schemas updated accordingly. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx nx lint:diff-with-main twenty-server` - [x] `npx nx lint:diff-with-main twenty-front` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs feature-flag` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs workspace-entity-manager` - [ ] Manual smoke test: AI features still accessible without any flag row in `featureFlag` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
259 lines
8.6 KiB
TypeScript
259 lines
8.6 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { t } from '@lingui/core/macro';
|
|
import {
|
|
IconComment,
|
|
IconHome,
|
|
IconMessageCirclePlus,
|
|
OverflowingTextWithTooltip,
|
|
} from 'twenty-ui/display';
|
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|
import { useIsMobile } from 'twenty-ui/utilities';
|
|
|
|
import { useContext } from 'react';
|
|
|
|
import { useSwitchToNewAiChat } from '@/ai/hooks/useSwitchToNewAiChat';
|
|
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
|
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
|
import { navigationDrawerActiveTabState } from '@/ui/navigation/states/navigationDrawerActiveTabState';
|
|
import {
|
|
type NavigationDrawerActiveTab,
|
|
NAVIGATION_DRAWER_TABS,
|
|
} from '@/ui/navigation/states/navigationDrawerTabs';
|
|
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
|
|
|
const StyledRow = styled.div<{ isExpanded: boolean }>`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${({ isExpanded }) => (isExpanded ? themeCssVariables.spacing[2] : 0)};
|
|
justify-content: ${({ isExpanded }) =>
|
|
isExpanded ? 'space-between' : 'center'};
|
|
transition: gap calc(${themeCssVariables.animation.duration.normal} * 1s) ease;
|
|
width: ${({ isExpanded }) => (isExpanded ? '100%' : 'max-content')};
|
|
`;
|
|
|
|
const StyledTabsPill = styled.div`
|
|
align-items: center;
|
|
background: ${themeCssVariables.background.secondary};
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: ${themeCssVariables.border.radius.pill};
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
gap: ${themeCssVariables.spacing[0.5]};
|
|
height: ${themeCssVariables.spacing[7]};
|
|
padding: 3px;
|
|
width: ${themeCssVariables.spacing[18]};
|
|
`;
|
|
|
|
const StyledTabWrapper = styled.div<{ isActive: boolean }>`
|
|
align-items: center;
|
|
background: ${({ isActive }) =>
|
|
isActive ? themeCssVariables.background.transparent.light : 'transparent'};
|
|
border-radius: ${themeCssVariables.border.radius.pill};
|
|
color: ${({ isActive }) =>
|
|
isActive
|
|
? themeCssVariables.font.color.primary
|
|
: themeCssVariables.font.color.tertiary};
|
|
cursor: pointer;
|
|
display: flex;
|
|
flex: 1;
|
|
height: 100%;
|
|
justify-content: center;
|
|
|
|
&:hover {
|
|
background: ${({ isActive }) =>
|
|
isActive
|
|
? themeCssVariables.background.transparent.light
|
|
: themeCssVariables.background.transparent.lighter};
|
|
}
|
|
`;
|
|
|
|
const StyledTabIcon = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
height: ${themeCssVariables.spacing[5]};
|
|
justify-content: center;
|
|
width: ${themeCssVariables.spacing[5]};
|
|
`;
|
|
|
|
const StyledNewChatIcon = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
flex-grow: 0;
|
|
flex-shrink: 0;
|
|
justify-content: center;
|
|
`;
|
|
|
|
const StyledNewChatButtonWrapper = styled.div<{ isExpanded: boolean }>`
|
|
align-items: center;
|
|
background: ${themeCssVariables.background.secondary};
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: ${themeCssVariables.border.radius.pill};
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
height: ${({ isExpanded }) =>
|
|
isExpanded ? themeCssVariables.spacing[7] : themeCssVariables.spacing[6]};
|
|
justify-content: center;
|
|
padding: ${({ isExpanded }) =>
|
|
isExpanded ? '3px' : themeCssVariables.spacing[0.5]};
|
|
transition:
|
|
height calc(${themeCssVariables.animation.duration.normal} * 1s) ease,
|
|
padding calc(${themeCssVariables.animation.duration.normal} * 1s) ease;
|
|
width: ${({ isExpanded }) =>
|
|
isExpanded ? '103px' : themeCssVariables.spacing[6]};
|
|
`;
|
|
|
|
const StyledNewChatButton = styled.div`
|
|
align-items: center;
|
|
border-radius: inherit;
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
cursor: pointer;
|
|
display: flex;
|
|
font-size: ${themeCssVariables.font.size.sm};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
height: 100%;
|
|
justify-content: center;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
padding-inline: ${themeCssVariables.spacing[1]};
|
|
transition:
|
|
background calc(${themeCssVariables.animation.duration.fast} * 1s) ease,
|
|
color calc(${themeCssVariables.animation.duration.fast} * 1s) ease;
|
|
width: 100%;
|
|
|
|
&:hover {
|
|
background: ${themeCssVariables.background.transparent.light};
|
|
color: ${themeCssVariables.font.color.primary};
|
|
}
|
|
`;
|
|
|
|
export const MainNavigationDrawerTabsRow = () => {
|
|
const { theme } = useContext(ThemeContext);
|
|
const isMobile = useIsMobile();
|
|
const isNavigationDrawerExpanded = useAtomStateValue(
|
|
isNavigationDrawerExpandedState,
|
|
);
|
|
const [navigationDrawerActiveTab, setNavigationDrawerActiveTab] =
|
|
useAtomState(navigationDrawerActiveTabState);
|
|
const { switchToNewChat } = useSwitchToNewAiChat();
|
|
const setIsNavigationDrawerExpanded = useSetAtomState(
|
|
isNavigationDrawerExpandedState,
|
|
);
|
|
|
|
const isExpanded = isNavigationDrawerExpanded || isMobile;
|
|
|
|
const handleTabClick = (tab: NavigationDrawerActiveTab) => () => {
|
|
setNavigationDrawerActiveTab(tab);
|
|
};
|
|
|
|
const handleTabKeyDown =
|
|
(tab: NavigationDrawerActiveTab) => (event: React.KeyboardEvent) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
setNavigationDrawerActiveTab(tab);
|
|
}
|
|
};
|
|
|
|
const handleNewChatClick = () => {
|
|
if (isMobile) {
|
|
setIsNavigationDrawerExpanded(false);
|
|
}
|
|
switchToNewChat();
|
|
};
|
|
|
|
const handleNewChatKeyDown = (event: React.KeyboardEvent) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
handleNewChatClick();
|
|
}
|
|
};
|
|
|
|
const getTabIconColor = (isActive: boolean) =>
|
|
isActive ? theme.font.color.primary : theme.font.color.tertiary;
|
|
|
|
return (
|
|
<StyledRow isExpanded={isExpanded}>
|
|
<NavigationDrawerAnimatedCollapseWrapper>
|
|
<StyledTabsPill role="tablist" aria-label={t`Navigation tabs`}>
|
|
<StyledTabWrapper
|
|
isActive={
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
|
|
}
|
|
role="tab"
|
|
aria-selected={
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
|
|
}
|
|
aria-label={t`Home`}
|
|
tabIndex={
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU
|
|
? 0
|
|
: -1
|
|
}
|
|
onClick={handleTabClick(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU)}
|
|
onKeyDown={handleTabKeyDown(NAVIGATION_DRAWER_TABS.NAVIGATION_MENU)}
|
|
>
|
|
<StyledTabIcon>
|
|
<IconHome
|
|
size={theme.icon.size.md}
|
|
color={getTabIconColor(
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.NAVIGATION_MENU,
|
|
)}
|
|
/>
|
|
</StyledTabIcon>
|
|
</StyledTabWrapper>
|
|
<StyledTabWrapper
|
|
isActive={
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
|
|
}
|
|
role="tab"
|
|
aria-selected={
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
|
|
}
|
|
aria-label={t`Chat`}
|
|
tabIndex={
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY
|
|
? 0
|
|
: -1
|
|
}
|
|
onClick={handleTabClick(NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY)}
|
|
onKeyDown={handleTabKeyDown(NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY)}
|
|
>
|
|
<StyledTabIcon>
|
|
<IconComment
|
|
size={theme.icon.size.md}
|
|
color={getTabIconColor(
|
|
navigationDrawerActiveTab ===
|
|
NAVIGATION_DRAWER_TABS.AI_CHAT_HISTORY,
|
|
)}
|
|
/>
|
|
</StyledTabIcon>
|
|
</StyledTabWrapper>
|
|
</StyledTabsPill>
|
|
</NavigationDrawerAnimatedCollapseWrapper>
|
|
<StyledNewChatButtonWrapper isExpanded={isExpanded}>
|
|
<StyledNewChatButton
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label={t`New chat`}
|
|
onClick={handleNewChatClick}
|
|
onKeyDown={handleNewChatKeyDown}
|
|
>
|
|
<StyledNewChatIcon>
|
|
<IconMessageCirclePlus size={theme.icon.size.md} />
|
|
</StyledNewChatIcon>
|
|
{isExpanded && <OverflowingTextWithTooltip text={t`New chat`} />}
|
|
</StyledNewChatButton>
|
|
</StyledNewChatButtonWrapper>
|
|
</StyledRow>
|
|
);
|
|
};
|