diff --git a/package.json b/package.json index da21618c97..f7e14be3a0 100644 --- a/package.json +++ b/package.json @@ -230,6 +230,7 @@ "@types/js-cookie": "^3.0.3", "@types/lodash.camelcase": "^4.3.7", "@types/lodash.debounce": "^4.0.7", + "@types/lodash.groupby": "^4.6.9", "@types/lodash.isempty": "^4.4.7", "@types/lodash.isequal": "^4.5.7", "@types/lodash.isobject": "^3.0.7", diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 43192e2bc9..1c89af101e 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -254,6 +254,7 @@ export type Mutation = { generateJWT: AuthTokens; generateTransientToken: TransientToken; impersonate: Verify; + removeWorkspaceMember: Scalars['String']; renewToken: AuthTokens; signUp: LoginToken; track: Analytics; @@ -311,6 +312,11 @@ export type MutationImpersonateArgs = { }; +export type MutationRemoveWorkspaceMemberArgs = { + memberId: Scalars['String']; +}; + + export type MutationRenewTokenArgs = { refreshToken: Scalars['String']; }; @@ -953,6 +959,13 @@ export type GenerateApiKeyTokenMutationVariables = Exact<{ export type GenerateApiKeyTokenMutation = { __typename?: 'Mutation', generateApiKeyToken: { __typename?: 'ApiKeyToken', token: string } }; +export type GenerateJwtMutationVariables = Exact<{ + workspaceId: Scalars['String']; +}>; + + +export type GenerateJwtMutation = { __typename?: 'Mutation', generateJWT: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; + export type GenerateTransientTokenMutationVariables = Exact<{ [key: string]: never; }>; @@ -963,7 +976,7 @@ export type ImpersonateMutationVariables = Exact<{ }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ refreshToken: Scalars['String']; @@ -994,7 +1007,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -1053,7 +1066,7 @@ export type UploadImageMutationVariables = Exact<{ export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -1072,6 +1085,13 @@ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', status: string } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } }; +export type RemoveWorkspaceMemberMutationVariables = Exact<{ + memberId: Scalars['String']; +}>; + + +export type RemoveWorkspaceMemberMutation = { __typename?: 'Mutation', removeWorkspaceMember: string }; + export type ActivateWorkspaceMutationVariables = Exact<{ input: ActivateWorkspaceInput; }>; @@ -1232,6 +1252,14 @@ export const UserQueryFragmentFragmentDoc = gql` workspaceId } } + workspaces { + workspace { + id + logo + displayName + domainName + } + } } `; export const GetTimelineCalendarEventsFromCompanyIdDocument = gql` @@ -1535,6 +1563,41 @@ export function useGenerateApiKeyTokenMutation(baseOptions?: Apollo.MutationHook export type GenerateApiKeyTokenMutationHookResult = ReturnType; export type GenerateApiKeyTokenMutationResult = Apollo.MutationResult; export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions; +export const GenerateJwtDocument = gql` + mutation GenerateJWT($workspaceId: String!) { + generateJWT(workspaceId: $workspaceId) { + tokens { + ...AuthTokensFragment + } + } +} + ${AuthTokensFragmentFragmentDoc}`; +export type GenerateJwtMutationFn = Apollo.MutationFunction; + +/** + * __useGenerateJwtMutation__ + * + * To run a mutation, you first call `useGenerateJwtMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGenerateJwtMutation` 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 [generateJwtMutation, { data, loading, error }] = useGenerateJwtMutation({ + * variables: { + * workspaceId: // value for 'workspaceId' + * }, + * }); + */ +export function useGenerateJwtMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GenerateJwtDocument, options); + } +export type GenerateJwtMutationHookResult = ReturnType; +export type GenerateJwtMutationResult = Apollo.MutationResult; +export type GenerateJwtMutationOptions = Apollo.BaseMutationOptions; export const GenerateTransientTokenDocument = gql` mutation generateTransientToken { generateTransientToken { @@ -2202,6 +2265,37 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt export type GetCurrentUserQueryHookResult = ReturnType; export type GetCurrentUserLazyQueryHookResult = ReturnType; export type GetCurrentUserQueryResult = Apollo.QueryResult; +export const RemoveWorkspaceMemberDocument = gql` + mutation RemoveWorkspaceMember($memberId: String!) { + removeWorkspaceMember(memberId: $memberId) +} + `; +export type RemoveWorkspaceMemberMutationFn = Apollo.MutationFunction; + +/** + * __useRemoveWorkspaceMemberMutation__ + * + * To run a mutation, you first call `useRemoveWorkspaceMemberMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemoveWorkspaceMemberMutation` 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 [removeWorkspaceMemberMutation, { data, loading, error }] = useRemoveWorkspaceMemberMutation({ + * variables: { + * memberId: // value for 'memberId' + * }, + * }); + */ +export function useRemoveWorkspaceMemberMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RemoveWorkspaceMemberDocument, options); + } +export type RemoveWorkspaceMemberMutationHookResult = ReturnType; +export type RemoveWorkspaceMemberMutationResult = Apollo.MutationResult; +export type RemoveWorkspaceMemberMutationOptions = Apollo.BaseMutationOptions; export const ActivateWorkspaceDocument = gql` mutation ActivateWorkspace($input: ActivateWorkspaceInput!) { activateWorkspace(data: $input) { diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts new file mode 100644 index 0000000000..7f7d19ae71 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const GENERATE_JWT = gql` + mutation GenerateJWT($workspaceId: String!) { + generateJWT(workspaceId: $workspaceId) { + tokens { + ...AuthTokensFragment + } + } + } +`; diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index a39d49bcd1..5f6e97ea77 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -11,6 +11,7 @@ import { import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState'; +import { workspacesState } from '@/auth/states/workspaces'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; @@ -40,6 +41,7 @@ export const useAuth = () => { const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState); + const setWorkspaces = useSetRecoilState(workspacesState); const [challenge] = useChallengeMutation(); const [signUp] = useSignUpMutation(); @@ -101,6 +103,15 @@ export const useAuth = () => { } const workspace = user.defaultWorkspace ?? null; setCurrentWorkspace(workspace); + if (isDefined(verifyResult.data?.verify.user.workspaces)) { + const validWorkspaces = verifyResult.data?.verify.user.workspaces + .filter( + ({ workspace }) => workspace !== null && workspace !== undefined, + ) + .map((validWorkspace) => validWorkspace.workspace!); + + setWorkspaces(validWorkspaces); + } return { user, workspaceMember, @@ -114,6 +125,7 @@ export const useAuth = () => { setCurrentUser, setCurrentWorkspaceMember, setCurrentWorkspace, + setWorkspaces, ], ); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx index bc1997f2d4..6b2c5f2362 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx @@ -54,6 +54,7 @@ export const SignInUpForm = () => { const { form } = useSignInUpForm(); const { + isInviteMode, signInUpStep, signInUpMode, continueWithCredentials, @@ -89,14 +90,14 @@ export const SignInUpForm = () => { }, [signInUpMode, signInUpStep]); const title = useMemo(() => { - if (signInUpMode === SignInUpMode.Invite) { + if (isInviteMode) { return `Join ${workspace?.displayName ?? ''} team`; } return signInUpMode === SignInUpMode.SignIn ? 'Sign in to Twenty' : 'Sign up to Twenty'; - }, [signInUpMode, workspace?.displayName]); + }, [signInUpMode, workspace?.displayName, isInviteMode]); const theme = useTheme(); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx index cbc4d5192b..5433e8f0d2 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx @@ -15,7 +15,6 @@ import { useAuth } from '../../hooks/useAuth'; export enum SignInUpMode { SignIn = 'sign-in', SignUp = 'sign-up', - Invite = 'invite', } export enum SignInUpStep { @@ -33,15 +32,13 @@ export const useSignInUp = (form: UseFormReturn
) => { const { navigateAfterSignInUp } = useNavigateAfterSignInUp(); + const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite)); + const [signInUpStep, setSignInUpStep] = useState( SignInUpStep.Init, ); const [signInUpMode, setSignInUpMode] = useState(() => { - if (isMatchingLocation(AppPath.Invite)) { - return SignInUpMode.Invite; - } - return isMatchingLocation(AppPath.SignIn) ? SignInUpMode.SignIn : SignInUpMode.SignUp; @@ -72,24 +69,14 @@ export const useSignInUp = (form: UseFormReturn) => { }, onCompleted: (data) => { if (data?.checkUserExists.exists) { - isMatchingLocation(AppPath.Invite) - ? setSignInUpMode(SignInUpMode.Invite) - : setSignInUpMode(SignInUpMode.SignIn); + setSignInUpMode(SignInUpMode.SignIn); } else { - isMatchingLocation(AppPath.Invite) - ? setSignInUpMode(SignInUpMode.Invite) - : setSignInUpMode(SignInUpMode.SignUp); + setSignInUpMode(SignInUpMode.SignUp); } setSignInUpStep(SignInUpStep.Password); }, }); - }, [ - isMatchingLocation, - setSignInUpStep, - checkUserExistsQuery, - form, - setSignInUpMode, - ]); + }, [setSignInUpStep, checkUserExistsQuery, form, setSignInUpMode]); const submitCredentials: SubmitHandler = useCallback( async (data) => { @@ -102,7 +89,7 @@ export const useSignInUp = (form: UseFormReturn) => { workspace: currentWorkspace, workspaceMember: currentWorkspaceMember, } = - signInUpMode === SignInUpMode.SignIn + signInUpMode === SignInUpMode.SignIn && !isInviteMode ? await signInWithCredentials( data.email.toLowerCase().trim(), data.password, @@ -122,6 +109,7 @@ export const useSignInUp = (form: UseFormReturn) => { }, [ signInUpMode, + isInviteMode, signInWithCredentials, signUpWithCredentials, workspaceInviteHash, @@ -156,6 +144,7 @@ export const useSignInUp = (form: UseFormReturn) => { ); return { + isInviteMode, signInUpStep, signInUpMode, continueWithCredentials, diff --git a/packages/twenty-front/src/modules/auth/states/workspaces.ts b/packages/twenty-front/src/modules/auth/states/workspaces.ts new file mode 100644 index 0000000000..dc0be614a7 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/states/workspaces.ts @@ -0,0 +1,9 @@ +import { createState } from '@/ui/utilities/state/utils/createState'; +import { Workspace } from '~/generated/graphql'; + +export type Workspaces = Pick; + +export const workspacesState = createState({ + key: 'workspacesState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx new file mode 100644 index 0000000000..04e3217db7 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx @@ -0,0 +1,124 @@ +import { useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { Workspaces } from '@/auth/states/workspaces'; +import { IconChevronDown } from '@/ui/display/icon'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar'; +import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; +import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId'; +import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; +import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope'; + +const StyledLogo = styled.div<{ logo: string }>` + background: url(${({ logo }) => logo}); + background-position: center; + background-size: cover; + border-radius: ${({ theme }) => theme.border.radius.xs}; + height: 16px; + width: 16px; +`; + +const StyledContainer = styled.div` + 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 }) => theme.spacing(7)}; + padding: 0 ${({ theme }) => theme.spacing(2)}; + width: 100%; + + &: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; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` + color: ${({ disabled, theme }) => + disabled ? theme.font.color.extraLight : theme.font.color.tertiary}; +`; + +type MultiWorkspaceDropdownButtonProps = { + workspaces: Workspaces[]; +}; + +export const MultiWorkspaceDropdownButton = ({ + workspaces, +}: MultiWorkspaceDropdownButtonProps) => { + const theme = useTheme(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const [isMultiWorkspaceDropdownOpen, setToggleMultiWorkspaceDropdown] = + useState(false); + + const { switchWorkspace } = useWorkspaceSwitching(); + + const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID); + + const handleChange = async (workspaceId: string) => { + setToggleMultiWorkspaceDropdown(!isMultiWorkspaceDropdownOpen); + closeDropdown(); + await switchWorkspace(workspaceId); + }; + + return ( + + + {currentWorkspace?.displayName ?? ''} + + + } + dropdownComponents={ + + {workspaces.map((workspace) => ( + + } + selected={currentWorkspace?.id === workspace.id} + onClick={() => handleChange(workspace.id)} + /> + ))} + + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx index 568fbb4b6b..eae27e8c73 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx @@ -1,5 +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 { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; +import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton'; @@ -44,16 +49,24 @@ type NavigationDrawerHeaderProps = { }; export const NavigationDrawerHeader = ({ - name = 'Twenty', - logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=', + name = DEFAULT_WORKSPACE_NAME, + logo = DEFAULT_WORKSPACE_LOGO, showCollapseButton, }: NavigationDrawerHeaderProps) => { const isMobile = useIsMobile(); + const workspaces = useRecoilValue(workspacesState); return ( - - {name} + {workspaces !== null && workspaces.length > 1 ? ( + + ) : ( + <> + + {name} + + )} + {!isMobile && ( { + const navigate = useNavigate(); + const setTokenPair = useSetRecoilState(tokenPairState); + const [generateJWT] = useGenerateJwtMutation(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const switchWorkspace = async (workspaceId: string) => { + if (currentWorkspace?.id === workspaceId) return; + const jwt = await generateJWT({ + variables: { + workspaceId, + }, + }); + + if (isDefined(jwt.errors)) { + throw jwt.errors; + } + + if (!isDefined(jwt.data?.generateJWT)) { + throw new Error('could not create token'); + } + + const { tokens } = jwt.data.generateJWT; + setTokenPair(tokens); + navigate(`/objects/companies`); + window.location.reload(); + }; + + return { switchWorkspace }; +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope.ts new file mode 100644 index 0000000000..834b5d50ea --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope.ts @@ -0,0 +1,3 @@ +export enum NavigationDrawerHotKeyScope { + MultiWorkspaceDropdownButton = 'multi-workspace-dropdown', +} diff --git a/packages/twenty-front/src/modules/users/components/UserProvider.tsx b/packages/twenty-front/src/modules/users/components/UserProvider.tsx index aa0af98a25..43e5f04e61 100644 --- a/packages/twenty-front/src/modules/users/components/UserProvider.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProvider.tsx @@ -1,10 +1,11 @@ -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useQuery } from '@apollo/client'; import { useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { Workspaces, workspacesState } from '@/auth/states/workspaces'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { isDefined } from '~/utils/isDefined'; @@ -14,6 +15,7 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => { const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); + const setWorkspaces = useSetRecoilState(workspacesState); const setCurrentWorkspaceMember = useSetRecoilState( currentWorkspaceMemberState, @@ -36,12 +38,25 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => { colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light', }); } + if (isDefined(queryData?.currentUser?.workspaces)) { + const validWorkspaces = queryData.currentUser.workspaces.filter( + (obj: any) => obj.workspace !== null && obj.workspace !== undefined, + ); + const workspaces: Workspaces[] = []; + validWorkspaces.forEach((validWorkspace: any) => { + const workspace = validWorkspace.workspace! as Workspaces; + workspaces.push(workspace); + }); + + setWorkspaces(workspaces); + } }, [ setCurrentUser, isLoading, queryLoading, setCurrentWorkspace, setCurrentWorkspaceMember, + setWorkspaces, queryData?.currentUser, ]); diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index a19b5ff18f..bd02380fae 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -34,5 +34,13 @@ export const USER_QUERY_FRAGMENT = gql` workspaceId } } + workspaces { + workspace { + id + logo + displayName + domainName + } + } } `; diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index b038227318..97a012d18c 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -15,6 +15,10 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; import { seedCalendarEvents } from 'src/database/typeorm-seeds/workspace/calendar-events'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { + SeedAppleWorkspaceId, + SeedTwentyWorkspaceId, +} from 'src/database/typeorm-seeds/core/workspaces'; // TODO: implement dry-run @Command({ @@ -23,7 +27,7 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm 'Seed workspace with initial data. This command is intended for development only.', }) export class DataSeedWorkspaceCommand extends CommandRunner { - workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; + workspaceIds = [SeedAppleWorkspaceId, SeedTwentyWorkspaceId]; constructor( private readonly environmentService: EnvironmentService, @@ -45,79 +49,88 @@ export class DataSeedWorkspaceCommand extends CommandRunner { schema: 'core', }); - await dataSource.initialize(); + for (const workspaceId of this.workspaceIds) { + await dataSource.initialize(); - await seedCoreSchema(dataSource, this.workspaceId); + await seedCoreSchema(dataSource, workspaceId); - await dataSource.destroy(); + await dataSource.destroy(); - const schemaName = - await this.workspaceDataSourceService.createWorkspaceDBSchema( - this.workspaceId, - ); + const schemaName = + await this.workspaceDataSourceService.createWorkspaceDBSchema( + workspaceId, + ); - const dataSourceMetadata = - await this.dataSourceService.createDataSourceMetadata( - this.workspaceId, - schemaName, - ); + const dataSourceMetadata = + await this.dataSourceService.createDataSourceMetadata( + workspaceId, + schemaName, + ); - await this.workspaceSyncMetadataService.synchronize({ - workspaceId: this.workspaceId, - dataSourceId: dataSourceMetadata.id, - }); + await this.workspaceSyncMetadataService.synchronize({ + workspaceId: workspaceId, + dataSourceId: dataSourceMetadata.id, + }); + } } catch (error) { console.error(error); return; } - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - this.workspaceId, - ); - - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - if (!workspaceDataSource) { - throw new Error('Could not connect to workspace data source'); - } - - try { - const objectMetadata = - await this.objectMetadataService.findManyWithinWorkspace( - this.workspaceId, + for (const workspaceId of this.workspaceIds) { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + workspaceId, ); - const objectMetadataMap = objectMetadata.reduce((acc, object) => { - acc[object.nameSingular] = { - id: object.id, - fields: object.fields.reduce((acc, field) => { - acc[field.name] = field.id; - return acc; - }, {}), - }; + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); - return acc; - }, {}); + if (!workspaceDataSource) { + throw new Error('Could not connect to workspace data source'); + } - await seedCompanies(workspaceDataSource, dataSourceMetadata.schema); - await seedPeople(workspaceDataSource, dataSourceMetadata.schema); - await seedPipelineStep(workspaceDataSource, dataSourceMetadata.schema); - await seedOpportunity(workspaceDataSource, dataSourceMetadata.schema); - await seedCalendarEvents(workspaceDataSource, dataSourceMetadata.schema); + try { + const objectMetadata = + await this.objectMetadataService.findManyWithinWorkspace(workspaceId); + const objectMetadataMap = objectMetadata.reduce((acc, object) => { + acc[object.nameSingular] = { + id: object.id, + fields: object.fields.reduce((acc, field) => { + acc[field.name] = field.id; - await seedViews( - workspaceDataSource, - dataSourceMetadata.schema, - objectMetadataMap, - ); - await seedWorkspaceMember(workspaceDataSource, dataSourceMetadata.schema); - } catch (error) { - console.error(error); + return acc; + }, {}), + }; + + return acc; + }, {}); + + await seedCompanies(workspaceDataSource, dataSourceMetadata.schema); + await seedPeople(workspaceDataSource, dataSourceMetadata.schema); + await seedPipelineStep(workspaceDataSource, dataSourceMetadata.schema); + await seedOpportunity(workspaceDataSource, dataSourceMetadata.schema); + await seedCalendarEvents( + workspaceDataSource, + dataSourceMetadata.schema, + ); + + await seedViews( + workspaceDataSource, + dataSourceMetadata.schema, + objectMetadataMap, + ); + await seedWorkspaceMember( + workspaceDataSource, + dataSourceMetadata.schema, + workspaceId, + ); + } catch (error) { + console.error(error); + } + + await this.typeORMService.disconnectFromDataSource(dataSourceMetadata.id); } - - await this.typeORMService.disconnectFromDataSource(dataSourceMetadata.id); } } diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts index 39b2771825..f3902d95c5 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts @@ -1,5 +1,11 @@ import { DataSource } from 'typeorm'; +import { + SeedAppleWorkspaceId, + SeedTwentyWorkspaceId, +} from 'src/database/typeorm-seeds/core/workspaces'; +import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; + // import { SeedWorkspaceId } from 'src/database/typeorm-seeds/core/workspaces'; const tableName = 'userWorkspace'; @@ -15,12 +21,10 @@ export const seedUserWorkspaces = async ( schemaName: string, workspaceId: string, ) => { - await workspaceDataSource - .createQueryBuilder() - .insert() - .into(`${schemaName}.${tableName}`, ['userId', 'workspaceId']) - .orIgnore() - .values([ + let userWorkspaces: Pick[] = []; + + if (workspaceId === SeedAppleWorkspaceId) { + userWorkspaces = [ { userId: SeedUserIds.Tim, workspaceId, @@ -33,7 +37,23 @@ export const seedUserWorkspaces = async ( userId: SeedUserIds.Phil, workspaceId, }, - ]) + ]; + } + + if (workspaceId === SeedTwentyWorkspaceId) { + userWorkspaces = [ + { + userId: SeedUserIds.Tim, + workspaceId, + }, + ]; + } + await workspaceDataSource + .createQueryBuilder() + .insert() + .into(`${schemaName}.${tableName}`, ['userId', 'workspaceId']) + .orIgnore() + .values(userWorkspaces) .execute(); }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts index 0cef733301..75513e5ccb 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts @@ -1,14 +1,46 @@ import { DataSource } from 'typeorm'; +import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; + const tableName = 'workspace'; -export const SeedWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; +export const SeedAppleWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; +export const SeedTwentyWorkspaceId = '3b8e6458-5fc1-4e63-8563-008ccddaa6db'; export const seedWorkspaces = async ( workspaceDataSource: DataSource, schemaName: string, workspaceId: string, ) => { + const workspaces: { + [key: string]: Pick< + Workspace, + | 'id' + | 'displayName' + | 'domainName' + | 'inviteHash' + | 'logo' + | 'subscriptionStatus' + >; + } = { + [SeedAppleWorkspaceId]: { + id: workspaceId, + displayName: 'Apple', + domainName: 'apple.dev', + inviteHash: 'apple.dev-invite-hash', + logo: 'data:image/png;base64,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', + subscriptionStatus: 'incomplete', + }, + [SeedTwentyWorkspaceId]: { + id: workspaceId, + displayName: 'Twenty', + domainName: 'twenty.dev', + inviteHash: 'twenty.dev-invite-hash', + logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=', + subscriptionStatus: 'incomplete', + }, + }; + await workspaceDataSource .createQueryBuilder() .insert() @@ -21,16 +53,7 @@ export const seedWorkspaces = async ( 'subscriptionStatus', ]) .orIgnore() - .values([ - { - id: workspaceId, - displayName: 'Apple', - domainName: 'apple.dev', - inviteHash: 'apple.dev-invite-hash', - logo: 'data:image/png;base64,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', - subscriptionStatus: 'incomplete', - }, - ]) + .values(workspaces[workspaceId]) .execute(); }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/workspaceMember.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/workspaceMember.ts index 2f0054e87a..6432388736 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/workspaceMember.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/workspaceMember.ts @@ -1,6 +1,11 @@ import { DataSource } from 'typeorm'; import { SeedUserIds } from 'src/database/typeorm-seeds/core/users'; +import { + SeedAppleWorkspaceId, + SeedTwentyWorkspaceId, +} from 'src/database/typeorm-seeds/core/workspaces'; +import { WorkspaceMember } from 'src/engine/modules/user/dtos/workspace-member.dto'; const tableName = 'workspaceMember'; @@ -10,24 +15,25 @@ const WorkspaceMemberIds = { Phil: '20202020-1553-45c6-a028-5a9064cce07f', }; +type WorkspaceMembers = Pick< + WorkspaceMember, + 'id' | 'locale' | 'colorScheme' +> & { + nameFirstName: string; + nameLastName: string; + userEmail: string; + userId: string; +}; + export const seedWorkspaceMember = async ( workspaceDataSource: DataSource, schemaName: string, + workspaceId: string, ) => { - await workspaceDataSource - .createQueryBuilder() - .insert() - .into(`${schemaName}.${tableName}`, [ - 'id', - 'nameFirstName', - 'nameLastName', - 'locale', - 'colorScheme', - 'userEmail', - 'userId', - ]) - .orIgnore() - .values([ + let workspaceMembers: WorkspaceMembers[] = []; + + if (workspaceId === SeedAppleWorkspaceId) { + workspaceMembers = [ { id: WorkspaceMemberIds.Tim, nameFirstName: 'Tim', @@ -55,6 +61,35 @@ export const seedWorkspaceMember = async ( userEmail: 'phil.schiler@apple.dev', userId: SeedUserIds.Phil, }, + ]; + } + + if (workspaceId === SeedTwentyWorkspaceId) { + workspaceMembers = [ + { + id: WorkspaceMemberIds.Tim, + nameFirstName: 'Tim', + nameLastName: 'Apple', + locale: 'en', + colorScheme: 'Light', + userEmail: 'tim@apple.dev', + userId: SeedUserIds.Tim, + }, + ]; + } + await workspaceDataSource + .createQueryBuilder() + .insert() + .into(`${schemaName}.${tableName}`, [ + 'id', + 'nameFirstName', + 'nameLastName', + 'locale', + 'colorScheme', + 'userEmail', + 'userId', ]) + .orIgnore() + .values(workspaceMembers) .execute(); }; diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1707778127558-addUserWorkspaces.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1707778127558-addUserWorkspaces.ts index e0e0b2013b..b509cbca57 100644 --- a/packages/twenty-server/src/database/typeorm/core/migrations/1707778127558-addUserWorkspaces.ts +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1707778127558-addUserWorkspaces.ts @@ -14,10 +14,6 @@ export class AddUserWorkspaces1707778127558 implements MigrationInterface { "deletedAt" TIMESTAMP )`, ); - - await queryRunner.query( - `ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`, - ); } public async down(): Promise {} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1709680520888-updateUserWorkspaceColumnConstraints.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1709680520888-updateUserWorkspaceColumnConstraints.ts new file mode 100644 index 0000000000..abaab00343 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1709680520888-updateUserWorkspaceColumnConstraints.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class updateUserWorkspaceColumnConstraints1709680520888 + implements MigrationInterface +{ + name = 'updateUserWorkspaceColumnConstraints1709680520888'; + + public async up(queryRunner: QueryRunner): Promise { + // ----------------- WARNING ------------------------ + // Dropping constraints and adding them back is NOT a recommended and should be AVOIDED, + // since it can affect data integrity and cause downtime and unintentional data loss. + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "userWorkspace_userId_fkey"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "userWorkspace_workspaceId_fkey"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_cb488f32c6a0827b938edadf221"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6"`, + ); + + await queryRunner.query(` + ALTER TABLE "core"."userWorkspace" + ADD CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6" + FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") + ON DELETE CASCADE ON UPDATE NO ACTION + `); + + await queryRunner.query(` + ALTER TABLE "core"."userWorkspace" + ADD CONSTRAINT "FK_cb488f32c6a0827b938edadf221" + FOREIGN KEY ("userId") REFERENCES "core"."user"("id") + ON DELETE CASCADE ON UPDATE NO ACTION + `); + } + + public async down(): Promise {} +} diff --git a/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/__tests__/fields.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/__tests__/fields.utils.spec.ts index 474a1a203b..5408accb6c 100644 --- a/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/__tests__/fields.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/api-rest-query-builder/utils/__tests__/fields.utils.spec.ts @@ -20,7 +20,9 @@ describe('FieldUtils', () => { checkFields(objectMetadataItemMock, ['fieldNumber']), ).not.toThrow(); - expect(() => checkFields(objectMetadataItemMock, ['wrongField'])).toThrow(); + expect(() => + checkFields(objectMetadataItemMock, ['wrongField']), + ).toThrow(); expect(() => checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']), diff --git a/packages/twenty-server/src/engine/modules/user/services/user.service.spec.ts b/packages/twenty-server/src/engine/modules/user/services/user.service.spec.ts index 010a7c6992..9ad7b8ef41 100644 --- a/packages/twenty-server/src/engine/modules/user/services/user.service.spec.ts +++ b/packages/twenty-server/src/engine/modules/user/services/user.service.spec.ts @@ -4,6 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from 'src/engine/modules/user/user.entity'; import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; import { UserService } from './user.service'; @@ -18,6 +19,10 @@ describe('UserService', () => { provide: getRepositoryToken(User, 'core'), useValue: {}, }, + { + provide: getRepositoryToken(UserWorkspace, 'core'), + useValue: {}, + }, { provide: DataSourceService, useValue: {}, diff --git a/packages/twenty-server/src/engine/modules/user/services/user.service.ts b/packages/twenty-server/src/engine/modules/user/services/user.service.ts index f4c76ff842..59f1398753 100644 --- a/packages/twenty-server/src/engine/modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/modules/user/services/user.service.ts @@ -8,12 +8,15 @@ import { User } from 'src/engine/modules/user/user.entity'; import { WorkspaceMember } from 'src/engine/modules/user/dtos/workspace-member.dto'; import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; import { DataSourceEntity } from 'src/engine-metadata/data-source/data-source.entity'; export class UserService extends TypeOrmQueryService { constructor( @InjectRepository(User, 'core') private readonly userRepository: Repository, + @InjectRepository(UserWorkspace, 'core') + private readonly userWorkspaceRepository: Repository, private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, ) { @@ -104,6 +107,8 @@ export class UserService extends TypeOrmQueryService { `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, ); + await this.userWorkspaceRepository.delete({ userId }); + await this.userRepository.delete(user.id); return user; diff --git a/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.spec.ts index 1f6961f6c8..af996f78e2 100644 --- a/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.spec.ts @@ -2,11 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; -import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; import { User } from 'src/engine/modules/user/user.entity'; -import { BillingService } from 'src/engine/modules/billing/billing.service'; +import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service'; +import { BillingService } from 'src/engine/modules/billing/billing.service'; import { WorkspaceService } from './workspace.service'; diff --git a/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.ts index 2e32aa5fe7..a87bdf26a7 100644 --- a/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/modules/workspace/services/workspace.service.ts @@ -6,13 +6,13 @@ import assert from 'assert'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; -import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; -import { User } from 'src/engine/modules/user/user.entity'; -import { ActivateWorkspaceInput } from 'src/engine/modules/workspace/dtos/activate-workspace-input'; import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; +import { User } from 'src/engine/modules/user/user.entity'; +import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { UserWorkspaceService } from 'src/engine/modules/user-workspace/user-workspace.service'; import { BillingService } from 'src/engine/modules/billing/billing.service'; +import { ActivateWorkspaceInput } from 'src/engine/modules/workspace/dtos/activate-workspace-input'; export class WorkspaceService extends TypeOrmQueryService { constructor( @@ -70,4 +70,118 @@ export class WorkspaceService extends TypeOrmQueryService { .find() .then((workspaces) => workspaces.map((workspace) => workspace.id)); } + + private async reassignDefaultWorkspace( + currentWorkspaceId: string, + user: User, + worskpaces: UserWorkspace[], + ) { + // We'll filter all user workspaces without the one which its getting removed from + const filteredUserWorkspaces = worskpaces.filter( + (workspace) => workspace.workspaceId !== currentWorkspaceId, + ); + + // Loop over each workspace in the filteredUserWorkspaces array and check if it currently exists in + // the database + for (let index = 0; index < filteredUserWorkspaces.length; index++) { + const userWorkspace = filteredUserWorkspaces[index]; + + const nextWorkspace = await this.workspaceRepository.findOneBy({ + id: userWorkspace.workspaceId, + }); + + if (nextWorkspace) { + await this.userRepository.save({ + id: user.id, + defaultWorkspace: nextWorkspace, + updatedAt: new Date().toISOString(), + }); + break; + } + + // if no workspaces are valid then we delete the user + if (index === filteredUserWorkspaces.length - 1) { + await this.userRepository.delete({ id: user.id }); + } + } + } + + /* + async removeWorkspaceMember(workspaceId: string, memberId: string) { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + workspaceId, + ); + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + // using "SELECT *" here because we will need the corresponding members userId later + const [workspaceMember] = await workspaceDataSource?.query( + `SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`, + ); + + if (!workspaceMember) { + throw new NotFoundException('Member not found.'); + } + + await workspaceDataSource?.query( + `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`, + ); + + const workspaceMemberUser = await this.userRepository.findOne({ + where: { + id: workspaceMember.userId, + }, + relations: ['defaultWorkspace'], + }); + + if (!workspaceMemberUser) { + throw new NotFoundException('User not found'); + } + + const userWorkspaces = await this.userWorkspaceRepository.find({ + where: { userId: workspaceMemberUser.id }, + relations: ['workspace'], + }); + + // We want to check if we the user has signed up to more than one workspace + if (userWorkspaces.length > 1) { + // We neeed to check if the workspace that its getting removed from is its default workspace, if it is then + // change the default workspace to point to the next workspace available. + if (workspaceMemberUser.defaultWorkspace.id === workspaceId) { + await this.reassignDefaultWorkspace( + workspaceId, + workspaceMemberUser, + userWorkspaces, + ); + } + // if its not the default workspace then simply delete the user-workspace mapping + await this.userWorkspaceRepository.delete({ + userId: workspaceMemberUser.id, + workspaceId, + }); + } else { + await this.userWorkspaceRepository.delete({ + userId: workspaceMemberUser.id, + }); + + // After deleting the user-workspace mapping, we have a condition where we have the users default workspace points to a + // workspace which it doesnt have access to. So we delete the user. + await this.userRepository.delete({ id: workspaceMemberUser.id }); + } + + const payload = + new ObjectRecordDeleteEvent(); + + payload.workspaceId = workspaceId; + payload.details = { + before: workspaceMember, + }; + + this.eventEmitter.emit('workspaceMember.deleted', payload); + + return memberId; + } + */ } diff --git a/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts index a2fa95f231..e636e051cc 100644 --- a/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/modules/workspace/workspace.module.ts @@ -6,15 +6,16 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; import { WorkspaceResolver } from 'src/engine/modules/workspace/workspace.resolver'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity'; -import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; -import { User } from 'src/engine/modules/user/user.entity'; -import { UserWorkspaceModule } from 'src/engine/modules/user-workspace/user-workspace.module'; import { BillingModule } from 'src/engine/modules/billing/billing.module'; +import { UserWorkspace } from 'src/engine/modules/user-workspace/user-workspace.entity'; +import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity'; +import { UserWorkspaceModule } from 'src/engine/modules/user-workspace/user-workspace.module'; +import { User } from 'src/engine/modules/user/user.entity'; +import { DataSourceModule } from 'src/engine-metadata/data-source/data-source.module'; import { FileUploadModule } from 'src/engine/modules/file/file-upload/file-upload.module'; -import { Workspace } from './workspace.entity'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; +import { Workspace } from './workspace.entity'; import { WorkspaceService } from './services/workspace.service'; @@ -31,6 +32,8 @@ import { WorkspaceService } from './services/workspace.service'; ), UserWorkspaceModule, WorkspaceManagerModule, + DataSourceModule, + TypeORMModule, ], services: [WorkspaceService], resolvers: workspaceAutoResolverOpts, diff --git a/yarn.lock b/yarn.lock index bfb3bdf8a6..9bc8d86202 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16560,6 +16560,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash.groupby@npm:^4.6.9": + version: 4.6.9 + resolution: "@types/lodash.groupby@npm:4.6.9" + dependencies: + "@types/lodash": "npm:*" + checksum: 1302531f76da99cc8f1bbd486a8c7048a833352b12c39eb5b2ded01173dd5fff76f2c7ace04f08b51c55840271170f1cfbffe4e454dde8597c3ee996e70d4e11 + languageName: node + linkType: hard + "@types/lodash.isempty@npm:^4.4.7": version: 4.4.9 resolution: "@types/lodash.isempty@npm:4.4.9" @@ -45916,6 +45925,7 @@ __metadata: "@types/js-cookie": "npm:^3.0.3" "@types/lodash.camelcase": "npm:^4.3.7" "@types/lodash.debounce": "npm:^4.0.7" + "@types/lodash.groupby": "npm:^4.6.9" "@types/lodash.isempty": "npm:^4.4.7" "@types/lodash.isequal": "npm:^4.5.7" "@types/lodash.isobject": "npm:^3.0.7"