feat(twenty-front/workspace-menu): improve workspace menu (#10642)
New workspace menu
This commit is contained in:
@ -922,6 +922,7 @@ export type Mutation = {
|
|||||||
runWorkflowVersion: WorkflowRun;
|
runWorkflowVersion: WorkflowRun;
|
||||||
sendInvitations: SendInvitationsOutput;
|
sendInvitations: SendInvitationsOutput;
|
||||||
signUp: SignUpOutput;
|
signUp: SignUpOutput;
|
||||||
|
signUpInNewWorkspace: SignUpOutput;
|
||||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||||
submitFormStep: Scalars['Boolean']['output'];
|
submitFormStep: Scalars['Boolean']['output'];
|
||||||
syncRemoteTable: RemoteTable;
|
syncRemoteTable: RemoteTable;
|
||||||
|
|||||||
@ -842,6 +842,7 @@ export type Mutation = {
|
|||||||
runWorkflowVersion: WorkflowRun;
|
runWorkflowVersion: WorkflowRun;
|
||||||
sendInvitations: SendInvitationsOutput;
|
sendInvitations: SendInvitationsOutput;
|
||||||
signUp: SignUpOutput;
|
signUp: SignUpOutput;
|
||||||
|
signUpInNewWorkspace: SignUpOutput;
|
||||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||||
submitFormStep: Scalars['Boolean'];
|
submitFormStep: Scalars['Boolean'];
|
||||||
track: Analytics;
|
track: Analytics;
|
||||||
@ -2349,6 +2350,11 @@ export type SignUpMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
|
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
|
||||||
|
|
||||||
|
export type SignUpInNewWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type SignUpInNewWorkspaceMutation = { __typename?: 'Mutation', signUpInNewWorkspace: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
|
||||||
|
|
||||||
export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
|
export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
|
||||||
token: Scalars['String'];
|
token: Scalars['String'];
|
||||||
newPassword: Scalars['String'];
|
newPassword: Scalars['String'];
|
||||||
@ -3620,6 +3626,47 @@ export function useSignUpMutation(baseOptions?: Apollo.MutationHookOptions<SignU
|
|||||||
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
|
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
|
||||||
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
|
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
|
||||||
export type SignUpMutationOptions = Apollo.BaseMutationOptions<SignUpMutation, SignUpMutationVariables>;
|
export type SignUpMutationOptions = Apollo.BaseMutationOptions<SignUpMutation, SignUpMutationVariables>;
|
||||||
|
export const SignUpInNewWorkspaceDocument = gql`
|
||||||
|
mutation SignUpInNewWorkspace {
|
||||||
|
signUpInNewWorkspace {
|
||||||
|
loginToken {
|
||||||
|
...AuthTokenFragment
|
||||||
|
}
|
||||||
|
workspace {
|
||||||
|
id
|
||||||
|
workspaceUrls {
|
||||||
|
subdomainUrl
|
||||||
|
customUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${AuthTokenFragmentFragmentDoc}`;
|
||||||
|
export type SignUpInNewWorkspaceMutationFn = Apollo.MutationFunction<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useSignUpInNewWorkspaceMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useSignUpInNewWorkspaceMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useSignUpInNewWorkspaceMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [signUpInNewWorkspaceMutation, { data, loading, error }] = useSignUpInNewWorkspaceMutation({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useSignUpInNewWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>(SignUpInNewWorkspaceDocument, options);
|
||||||
|
}
|
||||||
|
export type SignUpInNewWorkspaceMutationHookResult = ReturnType<typeof useSignUpInNewWorkspaceMutation>;
|
||||||
|
export type SignUpInNewWorkspaceMutationResult = Apollo.MutationResult<SignUpInNewWorkspaceMutation>;
|
||||||
|
export type SignUpInNewWorkspaceMutationOptions = Apollo.BaseMutationOptions<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>;
|
||||||
export const UpdatePasswordViaResetTokenDocument = gql`
|
export const UpdatePasswordViaResetTokenDocument = gql`
|
||||||
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
|
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
|
||||||
updatePasswordViaResetToken(
|
updatePasswordViaResetToken(
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -4,8 +4,8 @@
|
|||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
export const useRedirect = () => {
|
export const useRedirect = () => {
|
||||||
const redirect = useDebouncedCallback((url: string) => {
|
const redirect = useDebouncedCallback((url: string, target?: string) => {
|
||||||
window.location.href = url;
|
window.open(url, target ?? '_self');
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -12,9 +12,10 @@ export const useRedirectToWorkspaceDomain = () => {
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
pathname?: string,
|
pathname?: string,
|
||||||
searchParams?: Record<string, string | boolean>,
|
searchParams?: Record<string, string | boolean>,
|
||||||
|
target?: string,
|
||||||
) => {
|
) => {
|
||||||
if (!isMultiWorkspaceEnabled) return;
|
if (!isMultiWorkspaceEnabled) return;
|
||||||
redirect(buildWorkspaceUrl(baseUrl, pathname, searchParams));
|
redirect(buildWorkspaceUrl(baseUrl, pathname, searchParams), target);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -42,10 +42,8 @@ export const AppNavigationDrawer = ({
|
|||||||
label={t`Advanced:`}
|
label={t`Advanced:`}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
logo: '',
|
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
logo: currentWorkspace?.logo ?? '',
|
|
||||||
title: currentWorkspace?.displayName ?? '',
|
title: currentWorkspace?.displayName ?? '',
|
||||||
children: <MainNavigationDrawerItems />,
|
children: <MainNavigationDrawerItems />,
|
||||||
footer: <SupportDropdown />,
|
footer: <SupportDropdown />,
|
||||||
@ -54,7 +52,6 @@ export const AppNavigationDrawer = ({
|
|||||||
return (
|
return (
|
||||||
<NavigationDrawer
|
<NavigationDrawer
|
||||||
className={className}
|
className={className}
|
||||||
logo={drawerProps.logo}
|
|
||||||
title={drawerProps.title}
|
title={drawerProps.title}
|
||||||
footer={drawerProps.footer}
|
footer={drawerProps.footer}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata
|
|||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
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 { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
@ -70,7 +69,6 @@ export const SignInAppNavigationDrawerMock = ({
|
|||||||
<NavigationDrawer
|
<NavigationDrawer
|
||||||
className={className}
|
className={className}
|
||||||
footer={footer}
|
footer={footer}
|
||||||
logo={DEFAULT_WORKSPACE_LOGO}
|
|
||||||
title={DEFAULT_WORKSPACE_NAME}
|
title={DEFAULT_WORKSPACE_NAME}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -19,9 +19,8 @@ const StyledHeader = styled.li`
|
|||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
border-top-right-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)};
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
|||||||
@ -80,12 +80,12 @@ export const useDropdown = (dropdownId?: string) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
dropdownId,
|
|
||||||
isDropdownOpen,
|
isDropdownOpen,
|
||||||
|
setIsDropdownOpen,
|
||||||
|
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||||
|
dropdownId,
|
||||||
scopeId,
|
scopeId,
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
|
||||||
setIsDropdownOpen,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
`;
|
||||||
@ -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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -19,7 +19,6 @@ export type NavigationDrawerProps = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
logo?: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,7 +63,6 @@ export const NavigationDrawer = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
footer,
|
footer,
|
||||||
logo,
|
|
||||||
title,
|
title,
|
||||||
}: NavigationDrawerProps) => {
|
}: NavigationDrawerProps) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@ -113,11 +111,7 @@ export const NavigationDrawer = ({
|
|||||||
{isSettingsDrawer && title ? (
|
{isSettingsDrawer && title ? (
|
||||||
!isMobile && <NavigationDrawerBackButton title={title} />
|
!isMobile && <NavigationDrawerBackButton title={title} />
|
||||||
) : (
|
) : (
|
||||||
<NavigationDrawerHeader
|
<NavigationDrawerHeader showCollapseButton={isHovered} />
|
||||||
name={title}
|
|
||||||
logo={logo || ''}
|
|
||||||
showCollapseButton={isHovered}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<StyledItemsContainer isSettings={isSettingsDrawer}>
|
<StyledItemsContainer isSettings={isSettingsDrawer}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { workspacesState } from '@/auth/states/workspaces';
|
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/MultiWorkspaceDropdownButton';
|
||||||
import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton';
|
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
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 { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
import { Avatar } from 'twenty-ui';
|
|
||||||
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
|
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -18,18 +14,6 @@ const StyledContainer = styled.div`
|
|||||||
user-select: none;
|
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(
|
const StyledNavigationDrawerCollapseButton = styled(
|
||||||
NavigationDrawerCollapseButton,
|
NavigationDrawerCollapseButton,
|
||||||
)<{ show?: boolean }>`
|
)<{ show?: boolean }>`
|
||||||
@ -39,38 +23,21 @@ const StyledNavigationDrawerCollapseButton = styled(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type NavigationDrawerHeaderProps = {
|
type NavigationDrawerHeaderProps = {
|
||||||
name: string;
|
|
||||||
logo: string;
|
|
||||||
showCollapseButton: boolean;
|
showCollapseButton: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NavigationDrawerHeader = ({
|
export const NavigationDrawerHeader = ({
|
||||||
name,
|
|
||||||
logo,
|
|
||||||
showCollapseButton,
|
showCollapseButton,
|
||||||
}: NavigationDrawerHeaderProps) => {
|
}: NavigationDrawerHeaderProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const workspaces = useRecoilValue(workspacesState);
|
|
||||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
|
||||||
|
|
||||||
const isNavigationDrawerExpanded = useRecoilValue(
|
const isNavigationDrawerExpanded = useRecoilValue(
|
||||||
isNavigationDrawerExpandedState,
|
isNavigationDrawerExpandedState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isMultiWorkspace = isMultiWorkspaceEnabled && workspaces.length > 1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{isMultiWorkspace ? (
|
<MultiWorkspaceDropdownButton />
|
||||||
<MultiWorkspaceDropdownButton workspaces={workspaces} />
|
|
||||||
) : (
|
|
||||||
<StyledSingleWorkspaceContainer>
|
|
||||||
<Avatar placeholder={name} avatarUrl={logo} />
|
|
||||||
<NavigationDrawerAnimatedCollapseWrapper>
|
|
||||||
<StyledName>{name}</StyledName>
|
|
||||||
</NavigationDrawerAnimatedCollapseWrapper>
|
|
||||||
</StyledSingleWorkspaceContainer>
|
|
||||||
)}
|
|
||||||
{!isMobile && isNavigationDrawerExpanded && (
|
{!isMobile && isNavigationDrawerExpanded && (
|
||||||
<StyledNavigationDrawerCollapseButton
|
<StyledNavigationDrawerCollapseButton
|
||||||
direction="left"
|
direction="left"
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const multiWorkspaceDropdownState = atom<
|
||||||
|
'default' | 'workspaces-list' | 'themes'
|
||||||
|
>({
|
||||||
|
key: 'multiWorkspaceDropdownState',
|
||||||
|
default: 'default',
|
||||||
|
});
|
||||||
@ -5,6 +5,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
import { IconComponent, IconMoon, IconSun, IconSunMoon } from 'twenty-ui';
|
||||||
|
|
||||||
export const useColorScheme = () => {
|
export const useColorScheme = () => {
|
||||||
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
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 {
|
return {
|
||||||
colorScheme,
|
colorScheme,
|
||||||
setColorScheme,
|
setColorScheme,
|
||||||
|
colorSchemeList,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
|
|||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||||
|
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||||
|
|
||||||
import { AuthResolver } from './auth.resolver';
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
@ -64,6 +65,10 @@ describe('AuthResolver', () => {
|
|||||||
provide: RenewTokenService,
|
provide: RenewTokenService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: SignInUpService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: ApiKeyService,
|
provide: ApiKeyService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
|
|||||||
@ -51,6 +51,7 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
|||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||||
|
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||||
|
|
||||||
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||||
@ -75,6 +76,7 @@ export class AuthResolver {
|
|||||||
private apiKeyService: ApiKeyService,
|
private apiKeyService: ApiKeyService,
|
||||||
private resetPasswordService: ResetPasswordService,
|
private resetPasswordService: ResetPasswordService,
|
||||||
private loginTokenService: LoginTokenService,
|
private loginTokenService: LoginTokenService,
|
||||||
|
private signInUpService: SignInUpService,
|
||||||
private transientTokenService: TransientTokenService,
|
private transientTokenService: TransientTokenService,
|
||||||
private emailVerificationService: EmailVerificationService,
|
private emailVerificationService: EmailVerificationService,
|
||||||
// private oauthService: OAuthService,
|
// private oauthService: OAuthService,
|
||||||
@ -258,6 +260,28 @@ export class AuthResolver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => SignUpOutput)
|
||||||
|
async signUpInNewWorkspace(
|
||||||
|
@AuthUser() currentUser: User,
|
||||||
|
): Promise<SignUpOutput> {
|
||||||
|
const { user, workspace } = await this.signInUpService.signUpOnNewWorkspace(
|
||||||
|
{ type: 'existingUser', existingUser: currentUser },
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
|
user.email,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginToken,
|
||||||
|
workspace: {
|
||||||
|
id: workspace.id,
|
||||||
|
workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// @Mutation(() => ExchangeAuthCode)
|
// @Mutation(() => ExchangeAuthCode)
|
||||||
// async exchangeAuthorizationCode(
|
// async exchangeAuthorizationCode(
|
||||||
// @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
// @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { render } from '@react-email/render';
|
|||||||
import { addMilliseconds } from 'date-fns';
|
import { addMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
||||||
import { APP_LOCALES } from 'twenty-shared';
|
import { APP_LOCALES, isDefined } from 'twenty-shared';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
|
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
|
||||||
@ -174,35 +174,50 @@ export class AuthService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validatePassword(
|
||||||
|
userData: ExistingUserOrNewUser['userData'],
|
||||||
|
authParams: Extract<
|
||||||
|
AuthProviderWithPasswordType['authParams'],
|
||||||
|
{ provider: 'password' }
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
if (userData.type === 'newUser') {
|
||||||
|
userData.newUserPayload.passwordHash =
|
||||||
|
await this.signInUpService.generateHash(authParams.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userData.type === 'existingUser') {
|
||||||
|
await this.signInUpService.validatePassword({
|
||||||
|
password: authParams.password,
|
||||||
|
passwordHash: userData.existingUser.passwordHash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isAuthProviderEnabledOrThrow(
|
||||||
|
userData: ExistingUserOrNewUser['userData'],
|
||||||
|
authParams: AuthProviderWithPasswordType['authParams'],
|
||||||
|
workspace: Workspace | undefined | null,
|
||||||
|
) {
|
||||||
|
if (authParams.provider === 'password') {
|
||||||
|
await this.validatePassword(userData, authParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(workspace)) {
|
||||||
|
workspaceValidator.isAuthEnabledOrThrow(authParams.provider, workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async signInUp(
|
async signInUp(
|
||||||
params: SignInUpBaseParams &
|
params: SignInUpBaseParams &
|
||||||
ExistingUserOrNewUser &
|
ExistingUserOrNewUser &
|
||||||
AuthProviderWithPasswordType,
|
AuthProviderWithPasswordType,
|
||||||
) {
|
) {
|
||||||
if (
|
await this.isAuthProviderEnabledOrThrow(
|
||||||
params.authParams.provider === 'password' &&
|
params.userData,
|
||||||
params.userData.type === 'newUser'
|
params.authParams,
|
||||||
) {
|
params.workspace,
|
||||||
params.userData.newUserPayload.passwordHash =
|
);
|
||||||
await this.signInUpService.generateHash(params.authParams.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
params.authParams.provider === 'password' &&
|
|
||||||
params.userData.type === 'existingUser'
|
|
||||||
) {
|
|
||||||
await this.signInUpService.validatePassword({
|
|
||||||
password: params.authParams.password,
|
|
||||||
passwordHash: params.userData.existingUser.passwordHash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.workspace) {
|
|
||||||
workspaceValidator.isAuthEnabledOrThrow(
|
|
||||||
params.authParams.provider,
|
|
||||||
params.workspace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.userData.type === 'newUser') {
|
if (params.userData.type === 'newUser') {
|
||||||
const partialUserWithPicture =
|
const partialUserWithPicture =
|
||||||
|
|||||||
@ -261,8 +261,11 @@ describe('SignInUpService', () => {
|
|||||||
.mockResolvedValue('a-subdomain');
|
.mockResolvedValue('a-subdomain');
|
||||||
jest
|
jest
|
||||||
.spyOn(UserRepository, 'save')
|
.spyOn(UserRepository, 'save')
|
||||||
|
|
||||||
.mockResolvedValue({ id: 'newUserId' } as User);
|
.mockResolvedValue({ id: 'newUserId' } as User);
|
||||||
jest.spyOn(userWorkspaceService, 'create').mockResolvedValue({} as any);
|
jest
|
||||||
|
.spyOn(userWorkspaceService, 'create')
|
||||||
|
.mockResolvedValue({} as UserWorkspace);
|
||||||
|
|
||||||
const result = await service.signInUp(params);
|
const result = await service.signInUp(params);
|
||||||
|
|
||||||
@ -334,6 +337,35 @@ describe('SignInUpService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle signup for existing user on new workspace', async () => {
|
||||||
|
const params: SignInUpBaseParams &
|
||||||
|
ExistingUserOrPartialUserWithPicture &
|
||||||
|
AuthProviderWithPasswordType = {
|
||||||
|
workspace: null,
|
||||||
|
authParams: { provider: 'password', password: 'validPassword' },
|
||||||
|
userData: {
|
||||||
|
type: 'existingUser',
|
||||||
|
existingUser: { email: 'existinguser@example.com' } as User,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(environmentService, 'get').mockReturnValue(false);
|
||||||
|
jest.spyOn(WorkspaceRepository, 'count').mockResolvedValue(0);
|
||||||
|
jest.spyOn(WorkspaceRepository, 'create').mockReturnValue({} as Workspace);
|
||||||
|
jest.spyOn(WorkspaceRepository, 'save').mockResolvedValue({
|
||||||
|
id: 'newWorkspaceId',
|
||||||
|
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||||
|
} as Workspace);
|
||||||
|
jest.spyOn(userWorkspaceService, 'create').mockResolvedValue({} as any);
|
||||||
|
|
||||||
|
const result = await service.signInUp(params);
|
||||||
|
|
||||||
|
expect(result.workspace).toBeDefined();
|
||||||
|
expect(result.user).toBeDefined();
|
||||||
|
expect(WorkspaceRepository.create).toHaveBeenCalled();
|
||||||
|
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should assign default role when permissions are enabled', async () => {
|
it('should assign default role when permissions are enabled', async () => {
|
||||||
const params: SignInUpBaseParams &
|
const params: SignInUpBaseParams &
|
||||||
ExistingUserOrPartialUserWithPicture &
|
ExistingUserOrPartialUserWithPicture &
|
||||||
|
|||||||
@ -90,7 +90,6 @@ export class SignInUpService {
|
|||||||
ExistingUserOrPartialUserWithPicture &
|
ExistingUserOrPartialUserWithPicture &
|
||||||
AuthProviderWithPasswordType,
|
AuthProviderWithPasswordType,
|
||||||
) {
|
) {
|
||||||
// with personal invitation flow
|
|
||||||
if (params.workspace && params.invitation) {
|
if (params.workspace && params.invitation) {
|
||||||
return {
|
return {
|
||||||
workspace: params.workspace,
|
workspace: params.workspace,
|
||||||
@ -110,14 +109,7 @@ export class SignInUpService {
|
|||||||
return { user: updatedUser, workspace: params.workspace };
|
return { user: updatedUser, workspace: params.workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.userData.type === 'newUserWithPicture') {
|
return await this.signUpOnNewWorkspace(params.userData);
|
||||||
return await this.signUpOnNewWorkspace(
|
|
||||||
params.userData.newUserWithPicture,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// should never happen.
|
|
||||||
throw new Error('Invalid sign in up params');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateHash(password: string) {
|
async generateHash(password: string) {
|
||||||
@ -200,24 +192,6 @@ export class SignInUpService {
|
|||||||
return updatedUser;
|
return updatedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async persistNewUser(
|
|
||||||
newUser: PartialUserWithPicture,
|
|
||||||
workspace: Workspace,
|
|
||||||
) {
|
|
||||||
const imagePath = await this.uploadPicture(newUser.picture, workspace.id);
|
|
||||||
|
|
||||||
delete newUser.picture;
|
|
||||||
|
|
||||||
const userToCreate = this.userRepository.create({
|
|
||||||
...newUser,
|
|
||||||
defaultAvatarUrl: imagePath,
|
|
||||||
canAccessFullAdminPanel: false,
|
|
||||||
canImpersonate: false,
|
|
||||||
} as Partial<User>);
|
|
||||||
|
|
||||||
return await this.userRepository.save(userToCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async throwIfWorkspaceIsNotReadyForSignInUp(
|
private async throwIfWorkspaceIsNotReadyForSignInUp(
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
user: ExistingUserOrPartialUserWithPicture,
|
user: ExistingUserOrPartialUserWithPicture,
|
||||||
@ -254,9 +228,10 @@ export class SignInUpService {
|
|||||||
|
|
||||||
const currentUser =
|
const currentUser =
|
||||||
params.userData.type === 'newUserWithPicture'
|
params.userData.type === 'newUserWithPicture'
|
||||||
? await this.persistNewUser(
|
? await this.saveNewUser(
|
||||||
params.userData.newUserWithPicture,
|
params.userData.newUserWithPicture,
|
||||||
params.workspace,
|
params.workspace.id,
|
||||||
|
{ canAccessFullAdminPanel: false, canImpersonate: false },
|
||||||
)
|
)
|
||||||
: params.userData.existingUser;
|
: params.userData.existingUser;
|
||||||
|
|
||||||
@ -299,14 +274,42 @@ export class SignInUpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signUpOnNewWorkspace(partialUserWithPicture: PartialUserWithPicture) {
|
private async saveNewUser(
|
||||||
const user: PartialUserWithPicture = {
|
newUserWithPicture: PartialUserWithPicture,
|
||||||
...partialUserWithPicture,
|
workspaceId: string,
|
||||||
canImpersonate: false,
|
{
|
||||||
canAccessFullAdminPanel: false,
|
canImpersonate,
|
||||||
};
|
canAccessFullAdminPanel,
|
||||||
|
}: {
|
||||||
|
canImpersonate: boolean;
|
||||||
|
canAccessFullAdminPanel: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const defaultAvatarUrl = await this.uploadPicture(
|
||||||
|
newUserWithPicture.picture,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
const userCreated = this.userRepository.create({
|
||||||
|
...newUserWithPicture,
|
||||||
|
defaultAvatarUrl,
|
||||||
|
canImpersonate,
|
||||||
|
canAccessFullAdminPanel,
|
||||||
|
});
|
||||||
|
|
||||||
if (!user.email) {
|
return await this.userRepository.save(userCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signUpOnNewWorkspace(
|
||||||
|
userData: ExistingUserOrPartialUserWithPicture['userData'],
|
||||||
|
) {
|
||||||
|
let canImpersonate = false;
|
||||||
|
let canAccessFullAdminPanel = false;
|
||||||
|
const email =
|
||||||
|
userData.type === 'newUserWithPicture'
|
||||||
|
? userData.newUserWithPicture.email
|
||||||
|
: userData.existingUser.email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
throw new AuthException(
|
throw new AuthException(
|
||||||
'Email is required',
|
'Email is required',
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
@ -317,8 +320,8 @@ export class SignInUpService {
|
|||||||
const workspacesCount = await this.workspaceRepository.count();
|
const workspacesCount = await this.workspaceRepository.count();
|
||||||
|
|
||||||
// if the workspace doesn't exist it means it's the first user of the workspace
|
// if the workspace doesn't exist it means it's the first user of the workspace
|
||||||
user.canImpersonate = true;
|
canImpersonate = true;
|
||||||
user.canAccessFullAdminPanel = true;
|
canAccessFullAdminPanel = true;
|
||||||
|
|
||||||
// let the creation of the first workspace
|
// let the creation of the first workspace
|
||||||
if (workspacesCount > 0) {
|
if (workspacesCount > 0) {
|
||||||
@ -329,7 +332,7 @@ export class SignInUpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(user.email)}`;
|
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
|
||||||
const isLogoUrlValid = async () => {
|
const isLogoUrlValid = async () => {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
@ -342,7 +345,7 @@ export class SignInUpService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const logo =
|
const logo =
|
||||||
isWorkEmail(user.email) && (await isLogoUrlValid()) ? logoUrl : undefined;
|
isWorkEmail(email) && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||||
|
|
||||||
const workspaceToCreate = this.workspaceRepository.create({
|
const workspaceToCreate = this.workspaceRepository.create({
|
||||||
subdomain: await this.domainManagerService.generateSubdomain(),
|
subdomain: await this.domainManagerService.generateSubdomain(),
|
||||||
@ -354,25 +357,24 @@ export class SignInUpService {
|
|||||||
|
|
||||||
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
||||||
|
|
||||||
user.defaultAvatarUrl = await this.uploadPicture(
|
const user =
|
||||||
partialUserWithPicture.picture,
|
userData.type === 'existingUser'
|
||||||
workspace.id,
|
? userData.existingUser
|
||||||
);
|
: await this.saveNewUser(userData.newUserWithPicture, workspace.id, {
|
||||||
|
canImpersonate,
|
||||||
|
canAccessFullAdminPanel,
|
||||||
|
});
|
||||||
|
|
||||||
const userCreated = this.userRepository.create(user);
|
await this.userWorkspaceService.create(user.id, workspace.id);
|
||||||
|
|
||||||
const newUser = await this.userRepository.save(userCreated);
|
await this.activateOnboardingForUser(user, workspace);
|
||||||
|
|
||||||
await this.userWorkspaceService.create(newUser.id, workspace.id);
|
|
||||||
|
|
||||||
await this.activateOnboardingForUser(newUser, workspace);
|
|
||||||
|
|
||||||
await this.onboardingService.setOnboardingInviteTeamPending({
|
await this.onboardingService.setOnboardingInviteTeamPending({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
value: true,
|
value: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { user: newUser, workspace };
|
return { user, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadPicture(
|
async uploadPicture(
|
||||||
|
|||||||
@ -203,12 +203,10 @@ export class WorkspaceResolver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = await this.roleService.getRoleById(
|
return await this.roleService.getRoleById(
|
||||||
workspace.defaultRoleId,
|
workspace.defaultRoleId,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => BillingSubscription, { nullable: true })
|
@ResolveField(() => BillingSubscription, { nullable: true })
|
||||||
|
|||||||
@ -281,9 +281,13 @@ export {
|
|||||||
IconUpload,
|
IconUpload,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconUserCircle,
|
IconUserCircle,
|
||||||
|
IconSwitchHorizontal,
|
||||||
IconUserCog,
|
IconUserCog,
|
||||||
IconUserPin,
|
IconUserPin,
|
||||||
IconUserPlus,
|
IconUserPlus,
|
||||||
|
IconSunMoon,
|
||||||
|
IconMoon,
|
||||||
|
IconSun,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
IconVariable,
|
IconVariable,
|
||||||
IconVariablePlus,
|
IconVariablePlus,
|
||||||
|
|||||||
Reference in New Issue
Block a user