refactor(auth): add workspaces selection (#12098)

This commit is contained in:
Antoine Moreaux
2025-06-13 16:17:35 +02:00
committed by GitHub
parent 836e2f792c
commit b1af98f93d
162 changed files with 3542 additions and 1340 deletions

View File

@ -2,7 +2,6 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta
import { useAuth } from '@/auth/hooks/useAuth';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { AppPath } from '@/types/AppPath';
@ -37,9 +36,14 @@ import {
MenuItemSelectAvatar,
UndecoratedLink,
} from 'twenty-ui/navigation';
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
import {
useSignUpInNewWorkspaceMutation,
AvailableWorkspace,
} from '~/generated/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
const StyledDescription = styled.div`
color: ${({ theme }) => theme.font.color.light};
@ -50,7 +54,9 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { t } = useLingui();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const workspaces = useRecoilValue(workspacesState);
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const availableWorkspacesCount =
countAvailableWorkspaces(availableWorkspaces);
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
const { signOut } = useAuth();
@ -63,8 +69,10 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
multiWorkspaceDropdownState,
);
const handleChange = async (workspace: Workspaces[0]) => {
redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
const handleChange = async (availableWorkspace: AvailableWorkspace) => {
redirectToWorkspaceDomain(
getWorkspaceUrl(availableWorkspace.workspaceUrls),
);
};
const createWorkspace = () => {
@ -127,36 +135,41 @@ export const MultiWorkspaceDropdownDefaultComponents = () => {
>
{currentWorkspace?.displayName}
</DropdownMenuHeader>
{workspaces.length > 1 && (
{availableWorkspacesCount > 1 && (
<>
<DropdownMenuItemsContainer>
{workspaces
{[
...availableWorkspaces.availableWorkspacesForSignIn,
...availableWorkspaces.availableWorkspacesForSignUp,
]
.filter(({ id }) => id !== currentWorkspace?.id)
.slice(0, 3)
.map((workspace) => (
.map((availableWorkspace) => (
<UndecoratedLink
key={workspace.id}
key={availableWorkspace.id}
to={buildWorkspaceUrl(
getWorkspaceUrl(workspace.workspaceUrls),
getWorkspaceUrl(availableWorkspace.workspaceUrls),
)}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace);
handleChange(availableWorkspace);
}}
>
<MenuItemSelectAvatar
text={workspace.displayName ?? '(No name)'}
text={availableWorkspace.displayName ?? '(No name)'}
avatar={
<Avatar
placeholder={workspace.displayName || ''}
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
placeholder={availableWorkspace.displayName || ''}
avatarUrl={
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
}
/>
}
selected={false}
/>
</UndecoratedLink>
))}
{workspaces.length > 4 && (
{availableWorkspacesCount > 4 && (
<MenuItem
LeftIcon={IconSwitchHorizontal}
text={t`Other workspaces`}

View File

@ -1,32 +1,23 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Avatar, IconChevronLeft } from 'twenty-ui/display';
import { MenuItemSelectAvatar, UndecoratedLink } from 'twenty-ui/navigation';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { IconChevronLeft } from 'twenty-ui/display';
import { WorkspacesForSignIn } from './components/WorkspacesForSignIn';
import { WorkspacesForSignUp } from './components/WorkspacesForSignUp';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
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]) => {
await redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
};
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const setMultiWorkspaceDropdownState = useSetRecoilState(
multiWorkspaceDropdownState,
);
@ -52,37 +43,10 @@ export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
}}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
{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>
<WorkspacesForSignIn searchValue={searchValue} />
{availableWorkspaces.availableWorkspacesForSignUp.length > 0 && (
<WorkspacesForSignUp searchValue={searchValue} />
)}
</DropdownContent>
);
};

View File

@ -0,0 +1,58 @@
import { Avatar } from 'twenty-ui/display';
import { MenuItemSelectAvatar, UndecoratedLink } from 'twenty-ui/navigation';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { AvailableWorkspace } from '~/generated/graphql';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
import React from 'react';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
export const AvailableWorkspaceItem = ({
availableWorkspace,
isSelected,
}: {
availableWorkspace: AvailableWorkspace;
isSelected: boolean;
}) => {
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { pathname, searchParams } =
getAvailableWorkspacePathAndSearchParams(availableWorkspace);
const handleChange = async () => {
await redirectToWorkspaceDomain(
getWorkspaceUrl(availableWorkspace.workspaceUrls),
pathname,
searchParams,
);
};
return (
<UndecoratedLink
key={availableWorkspace.id}
to={buildWorkspaceUrl(
getWorkspaceUrl(availableWorkspace.workspaceUrls),
pathname,
searchParams,
)}
onClick={(event) => {
event.preventDefault();
handleChange();
}}
>
<MenuItemSelectAvatar
text={availableWorkspace.displayName ?? '(No name)'}
avatar={
<Avatar
placeholder={availableWorkspace.displayName || ''}
avatarUrl={availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
}
selected={isSelected}
/>
</UndecoratedLink>
);
};

View File

@ -0,0 +1,39 @@
import { useLingui } from '@lingui/react/macro';
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useFilteredAvailableWorkspaces } from '@/ui/navigation/navigation-drawer/hooks/useFilteredAvailableWorkspaces';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValue } from 'recoil';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { AvailableWorkspaceItem } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/components/AvailableWorkspaceItem';
export const WorkspacesForSignIn = ({
searchValue,
}: {
searchValue: string;
}) => {
const { t } = useLingui();
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { searchAvailableWorkspaces } = useFilteredAvailableWorkspaces();
return (
<>
<StyledDropdownMenuSubheader>{t`Member of`}</StyledDropdownMenuSubheader>
<DropdownMenuItemsContainer>
{searchAvailableWorkspaces(
searchValue,
availableWorkspaces.availableWorkspacesForSignIn,
).map((availableWorkspace) => (
<AvailableWorkspaceItem
key={availableWorkspace.id}
availableWorkspace={availableWorkspace}
isSelected={currentWorkspace?.id === availableWorkspace.id}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -0,0 +1,39 @@
import { useLingui } from '@lingui/react/macro';
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useFilteredAvailableWorkspaces } from '@/ui/navigation/navigation-drawer/hooks/useFilteredAvailableWorkspaces';
import { AvailableWorkspaceItem } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/components/AvailableWorkspaceItem';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValue } from 'recoil';
export const WorkspacesForSignUp = ({
searchValue,
}: {
searchValue: string;
}) => {
const { t } = useLingui();
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { searchAvailableWorkspaces } = useFilteredAvailableWorkspaces();
return (
<>
<StyledDropdownMenuSubheader>{t`Invitations`}</StyledDropdownMenuSubheader>
<DropdownMenuItemsContainer scrollable={false}>
{searchAvailableWorkspaces(
searchValue,
availableWorkspaces.availableWorkspacesForSignUp,
).map((availableWorkspace) => (
<AvailableWorkspaceItem
key={availableWorkspace.id}
availableWorkspace={availableWorkspace}
isSelected={currentWorkspace?.id === availableWorkspace.id}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -0,0 +1,25 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValue } from 'recoil';
import { AvailableWorkspace } from '~/generated/graphql';
export const useFilteredAvailableWorkspaces = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const searchAvailableWorkspaces = (
searchValue: string,
availableWorkspaces: Array<AvailableWorkspace>,
) => {
return availableWorkspaces.filter(
(availableWorkspace) =>
currentWorkspace?.id &&
availableWorkspace.id !== currentWorkspace.id &&
availableWorkspace.displayName
?.toLowerCase()
.includes(searchValue.toLowerCase()),
);
};
return {
searchAvailableWorkspaces,
};
};