feat(twenty-front/workspace-menu): improve workspace menu (#10642)

New workspace menu
This commit is contained in:
Antoine Moreaux
2025-03-17 16:31:31 +01:00
committed by GitHub
parent 78b3b7edab
commit bda835b9f8
28 changed files with 706 additions and 265 deletions

View File

@ -0,0 +1,18 @@
import { gql } from '@apollo/client';
export const SIGN_UP_IN_NEW_WORKSPACE = gql`
mutation SignUpInNewWorkspace {
signUpInNewWorkspace {
loginToken {
...AuthTokenFragment
}
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
}
}
}
}
`;

View File

@ -4,8 +4,8 @@
import { useDebouncedCallback } from 'use-debounce';
export const useRedirect = () => {
const redirect = useDebouncedCallback((url: string) => {
window.location.href = url;
const redirect = useDebouncedCallback((url: string, target?: string) => {
window.open(url, target ?? '_self');
}, 1);
return {

View File

@ -12,9 +12,10 @@ export const useRedirectToWorkspaceDomain = () => {
baseUrl: string,
pathname?: string,
searchParams?: Record<string, string | boolean>,
target?: string,
) => {
if (!isMultiWorkspaceEnabled) return;
redirect(buildWorkspaceUrl(baseUrl, pathname, searchParams));
redirect(buildWorkspaceUrl(baseUrl, pathname, searchParams), target);
};
return {

View File

@ -42,10 +42,8 @@ export const AppNavigationDrawer = ({
label={t`Advanced:`}
/>
),
logo: '',
}
: {
logo: currentWorkspace?.logo ?? '',
title: currentWorkspace?.displayName ?? '',
children: <MainNavigationDrawerItems />,
footer: <SupportDropdown />,
@ -54,7 +52,6 @@ export const AppNavigationDrawer = ({
return (
<NavigationDrawer
className={className}
logo={drawerProps.logo}
title={drawerProps.title}
footer={drawerProps.footer}
>

View File

@ -5,7 +5,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata
import { SettingsPath } from '@/types/SettingsPath';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
@ -70,7 +69,6 @@ export const SignInAppNavigationDrawerMock = ({
<NavigationDrawer
className={className}
footer={footer}
logo={DEFAULT_WORKSPACE_LOGO}
title={DEFAULT_WORKSPACE_NAME}
>
{children}

View File

@ -19,9 +19,8 @@ const StyledHeader = styled.li`
font-weight: ${({ theme }) => theme.font.weight.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
padding: ${({ theme }) => theme.spacing(1)};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
user-select: none;

View File

@ -80,12 +80,12 @@ export const useDropdown = (dropdownId?: string) => {
}
},
[
dropdownId,
isDropdownOpen,
setIsDropdownOpen,
setActiveDropdownFocusIdAndMemorizePrevious,
dropdownId,
scopeId,
setHotkeyScopeAndMemorizePreviousScope,
setActiveDropdownFocusIdAndMemorizePrevious,
setIsDropdownOpen,
],
);

View File

@ -0,0 +1,49 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId';
import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { useRecoilState } from 'recoil';
import { useMemo } from 'react';
import { MultiWorkspaceDropdownClickableComponent } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownClickableComponent';
import { MultiWorkspaceDropdownDefaultComponents } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents';
import { MultiWorkspaceDropdownThemesComponents } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownThemesComponents';
import { MultiWorkspaceDropdownWorkspacesListComponents } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownWorkspacesListComponents';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
export const MultiWorkspaceDropdownButton = () => {
const [multiWorkspaceDropdown, setMultiWorkspaceDropdown] = useRecoilState(
multiWorkspaceDropdownState,
);
const DropdownComponents = useMemo(() => {
switch (multiWorkspaceDropdown) {
case 'themes':
return MultiWorkspaceDropdownThemesComponents;
case 'workspaces-list':
return MultiWorkspaceDropdownWorkspacesListComponents;
default:
return MultiWorkspaceDropdownDefaultComponents;
}
}, [multiWorkspaceDropdown]);
const { isDropdownOpen } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
return (
<Dropdown
dropdownId={MULTI_WORKSPACE_DROPDOWN_ID}
dropdownHotkeyScope={{
scope: NavigationDrawerHotKeyScope.MultiWorkspaceDropdownButton,
}}
dropdownOffset={{ y: 0, x: 0 }}
clickableComponent={
<MultiWorkspaceDropdownClickableComponent
isDropdownOpen={isDropdownOpen}
/>
}
dropdownComponents={<DropdownComponents />}
onClose={() => {
setMultiWorkspaceDropdown('default');
}}
/>
);
};

View File

@ -0,0 +1,46 @@
import { Avatar } from 'twenty-ui';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import {
StyledContainer,
StyledIconChevronDown,
StyledLabel,
} from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspacesDropdownStyles';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValue } from 'recoil';
import { useTheme } from '@emotion/react';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
export const MultiWorkspaceDropdownClickableComponent = ({
isDropdownOpen,
}: {
isDropdownOpen: boolean;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const theme = useTheme();
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
return (
<StyledContainer
data-testid="workspace-dropdown"
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
>
<Avatar
placeholder={currentWorkspace?.displayName || ''}
avatarUrl={currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
</NavigationDrawerAnimatedCollapseWrapper>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledIconChevronDown
isDropdownOpen={isDropdownOpen}
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</NavigationDrawerAnimatedCollapseWrapper>
</StyledContainer>
);
};

View File

@ -0,0 +1,182 @@
import {
Avatar,
IconDotsVertical,
IconLogout,
IconPlus,
IconSwitchHorizontal,
IconUserPlus,
LightIconButton,
MenuItem,
MenuItemSelectAvatar,
UndecoratedLink,
} from 'twenty-ui';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useLingui } from '@lingui/react/macro';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsPath } from '@/types/SettingsPath';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId';
import { useAuth } from '@/auth/hooks/useAuth';
import { AppPath } from '@/types/AppPath';
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import styled from '@emotion/styled';
const StyledDescription = styled.div`
color: ${({ theme }) => theme.font.color.light};
padding-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledDropdownMenuItemsContainer = styled.div`
margin: ${({ theme }) => theme.spacing(1)} 0;
padding: 0 ${({ theme }) => theme.spacing(1)};
`;
export const MultiWorkspaceDropdownDefaultComponents = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { t } = useLingui();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const workspaces = useRecoilValue(workspacesState);
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
const { signOut } = useAuth();
const { enqueueSnackBar } = useSnackBar();
const { colorScheme, colorSchemeList } = useColorScheme();
const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation();
const setMultiWorkspaceDropdownState = useSetRecoilState(
multiWorkspaceDropdownState,
);
const handleChange = async (workspace: Workspaces[0]) => {
redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
};
const createWorkspace = () => {
signUpInNewWorkspaceMutation({
onCompleted: (data) => {
return redirectToWorkspaceDomain(
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
AppPath.Verify,
{
loginToken: data.signUpInNewWorkspace.loginToken.token,
},
'_blank',
);
},
onError: (error: Error) => {
enqueueSnackBar(error.message, {
variant: SnackBarVariant.Error,
});
},
});
};
return (
<>
<DropdownMenuHeader
StartAvatar={
<Avatar
placeholder={currentWorkspace?.displayName || ''}
avatarUrl={currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
}
DropdownOnEndIcon={
<Dropdown
clickableComponent={
<LightIconButton
Icon={IconDotsVertical}
size="small"
accent="tertiary"
/>
}
dropdownId={'multi-workspace-dropdown-context-menu'}
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconPlus}
text={t`Create Workspace`}
onClick={createWorkspace}
/>
</DropdownMenuItemsContainer>
}
/>
}
>
{currentWorkspace?.displayName}
</DropdownMenuHeader>
<StyledDropdownMenuItemsContainer>
{workspaces
.filter(({ id }) => id !== currentWorkspace?.id)
.slice(0, 3)
.map((workspace) => (
<UndecoratedLink
key={workspace.id}
to={buildWorkspaceUrl(getWorkspaceUrl(workspace.workspaceUrls))}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace);
}}
>
<MenuItemSelectAvatar
text={workspace.displayName ?? '(No name)'}
avatar={
<Avatar
placeholder={workspace.displayName || ''}
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
}
selected={false}
/>
</UndecoratedLink>
))}
{workspaces.length > 4 && (
<MenuItem
LeftIcon={IconSwitchHorizontal}
text={t`Other workspaces`}
onClick={() => setMultiWorkspaceDropdownState('workspaces-list')}
hasSubMenu={true}
/>
)}
</StyledDropdownMenuItemsContainer>
{workspaces.length > 1 && <DropdownMenuSeparator />}
<StyledDropdownMenuItemsContainer>
<MenuItem
LeftIcon={colorSchemeList.find(({ id }) => id === colorScheme)?.icon}
text={
<>
{t`Theme `}
<StyledDescription>{` · ${colorScheme}`}</StyledDescription>
</>
}
hasSubMenu={true}
onClick={() => setMultiWorkspaceDropdownState('themes')}
/>
<UndecoratedLink
to={getSettingsPath(SettingsPath.WorkspaceMembersPage)}
onClick={closeDropdown}
>
<MenuItem LeftIcon={IconUserPlus} text={t`Invite user`} />
</UndecoratedLink>
<MenuItem LeftIcon={IconLogout} text={t`Log out`} onClick={signOut} />
</StyledDropdownMenuItemsContainer>
</>
);
};

View File

@ -0,0 +1,37 @@
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { IconCheck, IconChevronLeft, MenuItem } from 'twenty-ui';
import { useLingui } from '@lingui/react/macro';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { useSetRecoilState } from 'recoil';
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
export const MultiWorkspaceDropdownThemesComponents = () => {
const { t } = useLingui();
const { setColorScheme, colorScheme, colorSchemeList } = useColorScheme();
const setMultiWorkspaceDropdownState = useSetRecoilState(
multiWorkspaceDropdownState,
);
return (
<DropdownMenuItemsContainer>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onStartIconClick={() => setMultiWorkspaceDropdownState('default')}
>
{t`Theme`}
</DropdownMenuHeader>
{colorSchemeList.map((theme) => (
<MenuItem
LeftIcon={theme.icon}
/* eslint-disable-next-line lingui/no-expression-in-message */
text={t`${theme.id}`}
onClick={() => setColorScheme(theme.id)}
RightIcon={theme.id === colorScheme ? IconCheck : undefined}
/>
))}
</DropdownMenuItemsContainer>
);
};

View File

@ -0,0 +1,84 @@
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import {
Avatar,
IconChevronLeft,
MenuItemSelectAvatar,
UndecoratedLink,
} from 'twenty-ui';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useLingui } from '@lingui/react/macro';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { useState } from 'react';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const workspaces = useRecoilValue(workspacesState);
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { t } = useLingui();
const handleChange = async (workspace: Workspaces[0]) => {
redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
};
const setMultiWorkspaceDropdownState = useSetRecoilState(
multiWorkspaceDropdownState,
);
const [searchValue, setSearchValue] = useState('');
return (
<DropdownMenuItemsContainer>
<DropdownMenuHeader
StartIcon={IconChevronLeft}
onStartIconClick={() => setMultiWorkspaceDropdownState('default')}
>
{t`Other workspaces`}
</DropdownMenuHeader>
<DropdownMenuSearchInput
placeholder={t`Search`}
autoFocus
onChange={(event) => {
setSearchValue(event.target.value);
}}
/>
<DropdownMenuSeparator />
{workspaces
.filter(
(workspace) =>
workspace.id !== currentWorkspace?.id &&
workspace.displayName
?.toLowerCase()
.includes(searchValue.toLowerCase()),
)
.map((workspace) => (
<UndecoratedLink
key={workspace.id}
to={buildWorkspaceUrl(getWorkspaceUrl(workspace.workspaceUrls))}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace);
}}
>
<MenuItemSelectAvatar
text={workspace.displayName ?? '(No name)'}
avatar={
<Avatar
placeholder={workspace.displayName || ''}
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
}
selected={currentWorkspace?.id === workspace.id}
/>
</UndecoratedLink>
))}
</DropdownMenuItemsContainer>
);
};

View File

@ -0,0 +1,43 @@
import { IconChevronDown } from 'twenty-ui';
import styled from '@emotion/styled';
export const StyledContainer = styled.div<{
isNavigationDrawerExpanded: boolean;
}>`
align-items: center;
cursor: pointer;
color: ${({ theme }) => theme.font.color.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
border: 1px solid transparent;
display: flex;
justify-content: space-between;
height: ${({ theme, isNavigationDrawerExpanded }) =>
isNavigationDrawerExpanded ? theme.spacing(5) : theme.spacing(4)};
padding: calc(${({ theme }) => theme.spacing(1)} - 1px);
width: ${({ isNavigationDrawerExpanded }) =>
isNavigationDrawerExpanded ? '100%' : 'auto'};
gap: ${({ theme, isNavigationDrawerExpanded }) =>
isNavigationDrawerExpanded ? theme.spacing(1) : '0'};
&:hover {
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
}
`;
export const StyledLabel = styled.div`
align-items: center;
display: flex;
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
export const StyledIconChevronDown = styled(IconChevronDown)<{
disabled?: boolean;
isDropdownOpen?: boolean;
}>`
align-items: center;
color: ${({ disabled, theme }) =>
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
display: flex;
rotate: ${({ isDropdownOpen }) => (isDropdownOpen ? '-180deg' : '0deg')};
transition: rotate 0.1s ease-in-out;
`;

View File

@ -1,130 +0,0 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId';
import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
Avatar,
IconChevronDown,
MenuItemSelectAvatar,
UndecoratedLink,
} from 'twenty-ui';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
const StyledContainer = styled.div<{ isNavigationDrawerExpanded: boolean }>`
align-items: center;
cursor: pointer;
color: ${({ theme }) => theme.font.color.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
border: 1px solid transparent;
display: flex;
justify-content: space-between;
height: ${({ theme, isNavigationDrawerExpanded }) =>
isNavigationDrawerExpanded ? theme.spacing(5) : theme.spacing(4)};
padding: calc(${({ theme }) => theme.spacing(1)} - 1px);
width: ${({ isNavigationDrawerExpanded }) =>
isNavigationDrawerExpanded ? '100%' : 'auto'};
gap: ${({ theme, isNavigationDrawerExpanded }) =>
isNavigationDrawerExpanded ? theme.spacing(1) : '0'};
&:hover {
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
}
`;
const StyledLabel = styled.div`
align-items: center;
display: flex;
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
align-items: center;
color: ${({ disabled, theme }) =>
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
display: flex;
`;
type MultiWorkspaceDropdownButtonProps = {
workspaces: Workspaces;
};
export const MultiWorkspaceDropdownButton = ({
workspaces,
}: MultiWorkspaceDropdownButtonProps) => {
const theme = useTheme();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const handleChange = async (workspace: Workspaces[0]) => {
redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
};
const [isNavigationDrawerExpanded] = useRecoilState(
isNavigationDrawerExpandedState,
);
return (
<Dropdown
dropdownId={MULTI_WORKSPACE_DROPDOWN_ID}
dropdownHotkeyScope={{
scope: NavigationDrawerHotKeyScope.MultiWorkspaceDropdownButton,
}}
clickableComponent={
<StyledContainer
data-testid="workspace-dropdown"
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
>
<Avatar
placeholder={currentWorkspace?.displayName || ''}
avatarUrl={currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
</NavigationDrawerAnimatedCollapseWrapper>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledIconChevronDown
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</NavigationDrawerAnimatedCollapseWrapper>
</StyledContainer>
}
dropdownComponents={
<DropdownMenuItemsContainer>
{workspaces.map((workspace) => (
<UndecoratedLink
key={workspace.id}
to={buildWorkspaceUrl(getWorkspaceUrl(workspace.workspaceUrls))}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace);
}}
>
<MenuItemSelectAvatar
text={workspace.displayName ?? '(No name)'}
avatar={
<Avatar
placeholder={workspace.displayName || ''}
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
}
selected={currentWorkspace?.id === workspace.id}
/>
</UndecoratedLink>
))}
</DropdownMenuItemsContainer>
}
/>
);
};

View File

@ -19,7 +19,6 @@ export type NavigationDrawerProps = {
children: ReactNode;
className?: string;
footer?: ReactNode;
logo?: string;
title: string;
};
@ -64,7 +63,6 @@ export const NavigationDrawer = ({
children,
className,
footer,
logo,
title,
}: NavigationDrawerProps) => {
const [isHovered, setIsHovered] = useState(false);
@ -113,11 +111,7 @@ export const NavigationDrawer = ({
{isSettingsDrawer && title ? (
!isMobile && <NavigationDrawerBackButton title={title} />
) : (
<NavigationDrawerHeader
name={title}
logo={logo || ''}
showCollapseButton={isHovered}
/>
<NavigationDrawerHeader showCollapseButton={isHovered} />
)}
<StyledItemsContainer isSettings={isSettingsDrawer}>
{children}

View File

@ -1,14 +1,10 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { workspacesState } from '@/auth/states/workspaces';
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton';
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/MultiWorkspaceDropdownButton';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { Avatar } from 'twenty-ui';
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
const StyledContainer = styled.div`
@ -18,18 +14,6 @@ const StyledContainer = styled.div`
user-select: none;
`;
const StyledSingleWorkspaceContainer = styled(StyledContainer)`
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledName = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-family: 'Inter';
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledNavigationDrawerCollapseButton = styled(
NavigationDrawerCollapseButton,
)<{ show?: boolean }>`
@ -39,38 +23,21 @@ const StyledNavigationDrawerCollapseButton = styled(
`;
type NavigationDrawerHeaderProps = {
name: string;
logo: string;
showCollapseButton: boolean;
};
export const NavigationDrawerHeader = ({
name,
logo,
showCollapseButton,
}: NavigationDrawerHeaderProps) => {
const isMobile = useIsMobile();
const workspaces = useRecoilValue(workspacesState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
const isMultiWorkspace = isMultiWorkspaceEnabled && workspaces.length > 1;
return (
<StyledContainer>
{isMultiWorkspace ? (
<MultiWorkspaceDropdownButton workspaces={workspaces} />
) : (
<StyledSingleWorkspaceContainer>
<Avatar placeholder={name} avatarUrl={logo} />
<NavigationDrawerAnimatedCollapseWrapper>
<StyledName>{name}</StyledName>
</NavigationDrawerAnimatedCollapseWrapper>
</StyledSingleWorkspaceContainer>
)}
<MultiWorkspaceDropdownButton />
{!isMobile && isNavigationDrawerExpanded && (
<StyledNavigationDrawerCollapseButton
direction="left"

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
export const multiWorkspaceDropdownState = atom<
'default' | 'workspaces-list' | 'themes'
>({
key: 'multiWorkspaceDropdownState',
default: 'default',
});

View File

@ -5,6 +5,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { IconComponent, IconMoon, IconSun, IconSunMoon } from 'twenty-ui';
export const useColorScheme = () => {
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
@ -45,8 +46,27 @@ export const useColorScheme = () => {
],
);
const colorSchemeList: Array<{
id: ColorScheme;
icon: IconComponent;
}> = [
{
id: 'System',
icon: IconSunMoon,
},
{
id: 'Dark',
icon: IconMoon,
},
{
id: 'Light',
icon: IconSun,
},
];
return {
colorScheme,
setColorScheme,
colorSchemeList,
};
};