feat: move support links to workspace switcher (#16653)

## Summary
Move the Support and Documentation links from the bottom left navigation
drawer to the workspace switcher dropdown menu.

## Changes
- **Workspace Switcher**: Added Support (conditional) and Documentation
links before Log out
- **Settings Navigation**: Added Support and Documentation links in the
Other section
- **Support visibility**: Support link only appears when FrontChat is
configured (not waiting for script to load)
- **FrontChat loading**: Created `SupportChatEffect` component to ensure
FrontChat script loads at app startup for popup notifications
- **Bug fix**: Fixed settings navigation items without a path appearing
highlighted
- **Cleanup**: Removed unused `SupportDropdown`, `SupportButton`, and
`SupportButtonSkeletonLoader` components
- **Hidden**: Temporarily commented out Integrations page

## Screenshots
The Support and Documentation links now appear in:
1. Workspace switcher dropdown (top left)
2. Settings navigation (Other section)
This commit is contained in:
Félix Malfait
2025-12-18 10:09:10 +01:00
committed by GitHub
parent 4fe8e3d3b6
commit 12544da527
12 changed files with 100 additions and 195 deletions
@@ -1,41 +0,0 @@
import styled from '@emotion/styled';
import { SupportButtonSkeletonLoader } from '@/support/components/SupportButtonSkeletonLoader';
import { useSupportChat } from '@/support/hooks/useSupportChat';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { IconHelpCircle } from 'twenty-ui/display';
import { Button, LightIconButton } from 'twenty-ui/input';
const StyledButtonContainer = styled.div`
display: flex;
`;
export const SupportButton = () => {
const { loading, isFrontChatLoaded } = useSupportChat();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
if (loading) {
return <SupportButtonSkeletonLoader />;
}
if (!isFrontChatLoaded) {
return;
}
return isNavigationDrawerExpanded ? (
<StyledButtonContainer>
<Button
variant="tertiary"
size="small"
title={t`Support`}
Icon={IconHelpCircle}
/>
</StyledButtonContainer>
) : (
<LightIconButton Icon={IconHelpCircle} />
);
};
@@ -1,16 +0,0 @@
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { useTheme } from '@emotion/react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
export const SupportButtonSkeletonLoader = () => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton width={84} height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} />
</SkeletonTheme>
);
};
@@ -0,0 +1,7 @@
import { useSupportChat } from '@/support/hooks/useSupportChat';
export const SupportChatEffect = () => {
useSupportChat();
return null;
};
@@ -1,50 +0,0 @@
import { SupportButton } from '@/support/components/SupportButton';
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { t } from '@lingui/core/macro';
import { IconHelpCircle, IconMessage } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
export const SupportDropdown = () => {
const dropdownId = `support-field-active-action-dropdown`;
const { closeDropdown } = useCloseDropdown();
const handleTalkToUs = () => {
window.FrontChat?.('show');
closeDropdown(dropdownId);
};
const handleUserGuide = () => {
window.open('https://docs.twenty.com/user-guide/introduction', '_blank');
closeDropdown(dropdownId);
};
return (
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="top-start"
dropdownOffset={{ x: 0, y: -28 }}
clickableComponent={<SupportButton />}
dropdownComponents={
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<MenuItem
text={t`Talk to us`}
LeftIcon={IconMessage}
onClick={handleTalkToUs}
/>
<MenuItem
text={t`Documentation`}
LeftIcon={IconHelpCircle}
onClick={handleUserGuide}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
);
};
@@ -1,67 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { graphqlMocks } from '~/testing/graphqlMocks';
import {
mockCurrentWorkspace,
mockedUserData,
mockedWorkspaceMemberData,
} from '~/testing/mock-data/users';
import { SupportDropdown } from '@/support/components/SupportDropdown';
import { SupportDriver } from '~/generated-metadata/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { PrefetchLoadedDecorator } from '~/testing/decorators/PrefetchLoadedDecorator';
const meta: Meta<typeof SupportDropdown> = {
title: 'Modules/Support/SupportDropdown',
component: SupportDropdown,
decorators: [
I18nFrontDecorator,
(Story) => {
const setCurrentUser = useSetRecoilState(currentUserState);
const setSupportChat = useSetRecoilState(supportChatState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
setCurrentWorkspace(mockCurrentWorkspace);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
setCurrentUser(mockedUserData);
setSupportChat({
supportDriver: SupportDriver.FRONT,
supportFrontChatId: '1234',
});
return <Story />;
},
PrefetchLoadedDecorator,
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof SupportDropdown>;
export const Default: Story = {
play: async () => {
const canvas = within(document.body);
expect(await canvas.findByText('Support')).toBeInTheDocument();
await userEvent.click(canvas.getByText('Support'));
// Add delay to account for the timeout in useSupportChat
await new Promise((resolve) => setTimeout(resolve, 600));
expect(await canvas.findByText('Documentation')).toBeInTheDocument();
expect(await canvas.findByText('Talk to us')).toBeInTheDocument();
},
};