diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index b47f88af34..e9e6e6d97a 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3395,6 +3395,7 @@ type Mutation { signUp(email: String!, password: String!, captchaToken: String, locale: String, verifyEmailRedirectPath: String): AvailableWorkspacesAndAccessTokens! signUpInWorkspace(email: String!, password: String!, workspaceId: UUID, workspaceInviteHash: String, workspacePersonalInviteToken: String, captchaToken: String, locale: String, verifyEmailRedirectPath: String): SignUp! signUpInNewWorkspace(input: SignUpInNewWorkspaceInput): SignUp! + uploadNewWorkspaceLogo(workspaceId: String!, file: Upload!): FileWithSignedUrl! generateTransientToken: TransientToken! getAuthTokensFromLoginToken(loginToken: String!, origin: String!): AuthTokens! authorizeApp(clientId: String!, codeChallenge: String, redirectUrl: String!, state: String, scope: String): AuthorizeApp! @@ -4475,6 +4476,9 @@ input UpdateWorkspaceMemberSettingsInput { } input ActivateWorkspaceInput { + """ + Deprecated: the workspace name is set at creation (signUpInNewWorkspace) and this field is ignored during activation. Kept for backward compatibility. + """ displayName: String } diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index bd42fca367..c3d6f51152 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2916,6 +2916,7 @@ export interface Mutation { signUp: AvailableWorkspacesAndAccessTokens signUpInWorkspace: SignUp signUpInNewWorkspace: SignUp + uploadNewWorkspaceLogo: FileWithSignedUrl generateTransientToken: TransientToken getAuthTokensFromLoginToken: AuthTokens authorizeApp: AuthorizeApp @@ -6094,6 +6095,7 @@ export interface MutationGenqlSelection{ signUp?: (AvailableWorkspacesAndAccessTokensGenqlSelection & { __args: {email: Scalars['String'], password: Scalars['String'], captchaToken?: (Scalars['String'] | null), locale?: (Scalars['String'] | null), verifyEmailRedirectPath?: (Scalars['String'] | null)} }) signUpInWorkspace?: (SignUpGenqlSelection & { __args: {email: Scalars['String'], password: Scalars['String'], workspaceId?: (Scalars['UUID'] | null), workspaceInviteHash?: (Scalars['String'] | null), workspacePersonalInviteToken?: (Scalars['String'] | null), captchaToken?: (Scalars['String'] | null), locale?: (Scalars['String'] | null), verifyEmailRedirectPath?: (Scalars['String'] | null)} }) signUpInNewWorkspace?: (SignUpGenqlSelection & { __args?: {input?: (SignUpInNewWorkspaceInput | null)} }) + uploadNewWorkspaceLogo?: (FileWithSignedUrlGenqlSelection & { __args: {workspaceId: Scalars['String'], file: Scalars['Upload']} }) generateTransientToken?: TransientTokenGenqlSelection getAuthTokensFromLoginToken?: (AuthTokensGenqlSelection & { __args: {loginToken: Scalars['String'], origin: Scalars['String']} }) authorizeApp?: (AuthorizeAppGenqlSelection & { __args: {clientId: Scalars['String'], codeChallenge?: (Scalars['String'] | null), redirectUrl: Scalars['String'], state?: (Scalars['String'] | null), scope?: (Scalars['String'] | null)} }) @@ -6507,7 +6509,9 @@ export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars[ export interface UpdateWorkspaceMemberSettingsInput {workspaceMemberId: Scalars['UUID'],update: Scalars['JSON']} -export interface ActivateWorkspaceInput {displayName?: (Scalars['String'] | null)} +export interface ActivateWorkspaceInput { +/** Deprecated: the workspace name is set at creation (signUpInNewWorkspace) and this field is ignored during activation. Kept for backward compatibility. */ +displayName?: (Scalars['String'] | null)} export interface UpdateWorkspaceInput {subdomain?: (Scalars['String'] | null),customDomain?: (Scalars['String'] | null),displayName?: (Scalars['String'] | null),logo?: (Scalars['String'] | null),inviteHash?: (Scalars['String'] | null),isPublicInviteLinkEnabled?: (Scalars['Boolean'] | null),allowImpersonation?: (Scalars['Boolean'] | null),isGoogleAuthEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthEnabled?: (Scalars['Boolean'] | null),isPasswordAuthEnabled?: (Scalars['Boolean'] | null),isGoogleAuthBypassEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthBypassEnabled?: (Scalars['Boolean'] | null),isPasswordAuthBypassEnabled?: (Scalars['Boolean'] | null),defaultRoleId?: (Scalars['UUID'] | null),isTwoFactorAuthenticationEnforced?: (Scalars['Boolean'] | null),trashRetentionDays?: (Scalars['Float'] | null),eventLogRetentionDays?: (Scalars['Float'] | null),fastModel?: (Scalars['String'] | null),smartModel?: (Scalars['String'] | null),aiAdditionalInstructions?: (Scalars['String'] | null),editableProfileFields?: (Scalars['String'][] | null),enabledAiModelIds?: (Scalars['String'][] | null),useRecommendedModels?: (Scalars['Boolean'] | null),isInternalMessagesImportEnabled?: (Scalars['Boolean'] | null)} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 043f8d5c09..2773f92bfb 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -8454,6 +8454,19 @@ export default { ] } ], + "uploadNewWorkspaceLogo": [ + 131, + { + "workspaceId": [ + 1, + "String!" + ], + "file": [ + 357, + "Upload!" + ] + } + ], "generateTransientToken": [ 250 ], diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 742105ab97..45fe1344c9 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -22,6 +22,7 @@ export type Scalars = { }; export type ActivateWorkspaceInput = { + /** Deprecated: the workspace name is set at creation (signUpInNewWorkspace) and this field is ignored during activation. Kept for backward compatibility. */ displayName?: InputMaybe; }; @@ -2646,6 +2647,7 @@ export type Mutation = { uploadEmailAttachmentFile: FileWithSignedUrl; uploadFilesFieldFile: FileWithSignedUrl; uploadFilesFieldFileByUniversalIdentifier: FileWithSignedUrl; + uploadNewWorkspaceLogo: FileWithSignedUrl; uploadWorkflowFile: FileWithSignedUrl; uploadWorkspaceLogo: FileWithSignedUrl; uploadWorkspaceMemberProfilePicture: FileWithSignedUrl; @@ -3677,6 +3679,12 @@ export type MutationUploadFilesFieldFileByUniversalIdentifierArgs = { }; +export type MutationUploadNewWorkspaceLogoArgs = { + file: Scalars['Upload']['input']; + workspaceId: Scalars['String']['input']; +}; + + export type MutationUploadWorkflowFileArgs = { file: Scalars['Upload']['input']; }; @@ -6758,6 +6766,14 @@ export type UpdatePasswordViaResetTokenMutationVariables = Exact<{ export type UpdatePasswordViaResetTokenMutation = { __typename?: 'Mutation', updatePasswordViaResetToken: { __typename?: 'InvalidatePassword', success: boolean } }; +export type UploadNewWorkspaceLogoMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + file: Scalars['Upload']['input']; +}>; + + +export type UploadNewWorkspaceLogoMutation = { __typename?: 'Mutation', uploadNewWorkspaceLogo: { __typename?: 'FileWithSignedUrl', url: string } }; + export type VerifyEmailAndGetLoginTokenMutationVariables = Exact<{ emailVerificationToken: Scalars['String']['input']; email: Scalars['String']['input']; @@ -8699,6 +8715,7 @@ export const SignUpDocument = {"kind":"Document","definitions":[{"kind":"Operati export const SignUpInNewWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignUpInNewWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SignUpInNewWorkspaceInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"signUpInNewWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"loginToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthTokenFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceUrlsFragment"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthTokenFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AuthToken"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceUrlsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUrls"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}},{"kind":"Field","name":{"kind":"Name","value":"customUrl"}}]}}]} as unknown as DocumentNode; export const SignUpInWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignUpInWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceInviteHash"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspacePersonalInviteToken"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"signUpInWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceInviteHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceInviteHash"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspacePersonalInviteToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspacePersonalInviteToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"captchaToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"locale"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}},{"kind":"Argument","name":{"kind":"Name","value":"verifyEmailRedirectPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"loginToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthTokenFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}},{"kind":"Field","name":{"kind":"Name","value":"customUrl"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthTokenFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AuthToken"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]} as unknown as DocumentNode; export const UpdatePasswordViaResetTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdatePasswordViaResetToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updatePasswordViaResetToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"passwordResetToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}},{"kind":"Argument","name":{"kind":"Name","value":"newPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode; +export const UploadNewWorkspaceLogoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadNewWorkspaceLogo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadNewWorkspaceLogo"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode; export const VerifyEmailAndGetLoginTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"VerifyEmailAndGetLoginToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"emailVerificationToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"origin"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"verifyEmailAndGetLoginToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"emailVerificationToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"emailVerificationToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"captchaToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"origin"},"value":{"kind":"Variable","name":{"kind":"Name","value":"origin"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"loginToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthTokenFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceUrlsFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthTokenFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AuthToken"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceUrlsFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUrls"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}},{"kind":"Field","name":{"kind":"Name","value":"customUrl"}}]}}]} as unknown as DocumentNode; export const VerifyEmailAndGetWorkspaceAgnosticTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"VerifyEmailAndGetWorkspaceAgnosticToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"emailVerificationToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"verifyEmailAndGetWorkspaceAgnosticToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"emailVerificationToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"emailVerificationToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"captchaToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"availableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AvailableWorkspacesFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tokens"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthTokenPairFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AvailableWorkspaceFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AvailableWorkspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"loginToken"}},{"kind":"Field","name":{"kind":"Name","value":"inviteHash"}},{"kind":"Field","name":{"kind":"Name","value":"personalInviteToken"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}},{"kind":"Field","name":{"kind":"Name","value":"customUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"sso"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"issuer"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthTokenFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AuthToken"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AvailableWorkspacesFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AvailableWorkspaces"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"availableWorkspacesForSignIn"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AvailableWorkspaceFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"availableWorkspacesForSignUp"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AvailableWorkspaceFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AuthTokenPairFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AuthTokenPair"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"accessOrWorkspaceAgnosticToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthTokenFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"refreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AuthTokenFragment"}}]}}]}}]} as unknown as DocumentNode; export const CheckUserExistsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CheckUserExists"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkUserExists"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"captchaToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"captchaToken"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exists"}},{"kind":"Field","name":{"kind":"Name","value":"availableWorkspacesCount"}},{"kind":"Field","name":{"kind":"Name","value":"isEmailVerified"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index 32960e914e..f7115fbd8a 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -84,9 +84,11 @@ const setupMockState = ( verifyEmailRedirectPath?: string, calendarBookingPageId?: string | null, returnToPath?: string, + currentWorkspace: object | null = { id: 'mock-workspace-id' }, ) => { jest .mocked(useAtomStateValue) + .mockReturnValueOnce(currentWorkspace) .mockReturnValueOnce(calendarBookingPageId ?? 'mock-calendar-id') .mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }]) .mockReturnValueOnce(verifyEmailRedirectPath) @@ -111,7 +113,7 @@ const testCases: { { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.Verify, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.Verify, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -121,7 +123,7 @@ const testCases: { { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.SignInUp, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.SignInUp, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -131,7 +133,7 @@ const testCases: { { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.Invite, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' }, + { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/workspace-activation' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' }, { loc: AppPath.Invite, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' }, @@ -141,7 +143,7 @@ const testCases: { { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: '/plan-required' }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.ResetPassword, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/create/workspace' }, + { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: '/workspace-activation' }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: '/create/profile' }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: '/sync/emails' }, { loc: AppPath.ResetPassword, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: '/invite-team' }, @@ -153,27 +155,27 @@ const testCases: { { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, verifyEmailRedirectPath: '/nextPath?key=value', res: undefined }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, - { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, { loc: AppPath.VerifyEmail, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, - { loc: AppPath.CreateWorkspace, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.BOOK_ONBOARDING, res: AppPath.BookCallDecision }, + { loc: AppPath.WorkspaceActivation, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: defaultHomePagePath }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.CreateProfile, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: undefined }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.CreateProfile, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -183,7 +185,7 @@ const testCases: { { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.SyncEmails, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: undefined }, { loc: AppPath.SyncEmails, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -193,7 +195,7 @@ const testCases: { { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.InviteTeam, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.InviteTeam, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: undefined }, @@ -223,7 +225,7 @@ const testCases: { { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined }, { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.PlanRequired, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.PlanRequired, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -233,7 +235,7 @@ const testCases: { { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.PlanRequiredSuccess, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -243,7 +245,7 @@ const testCases: { { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.Index, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.Index, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -253,7 +255,7 @@ const testCases: { { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.TasksPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.TasksPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -263,7 +265,7 @@ const testCases: { { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.OpportunitiesPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -273,7 +275,7 @@ const testCases: { { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.RecordIndexPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -285,7 +287,7 @@ const testCases: { { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.RecordShowPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -295,7 +297,7 @@ const testCases: { { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.PageLayoutPage, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -309,7 +311,7 @@ const testCases: { { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.SettingsCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -319,7 +321,7 @@ const testCases: { { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.DevelopersCatchAll, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -329,7 +331,7 @@ const testCases: { { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.Authorize, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.Authorize, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -339,7 +341,7 @@ const testCases: { { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.NotFoundWildcard, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -349,7 +351,7 @@ const testCases: { { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: getSettingsPath(SettingsPath.Billing) }, { loc: AppPath.NotFound, hasAccessTokenPair: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, + { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.WorkspaceActivation }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails }, { loc: AppPath.NotFound, hasAccessTokenPair: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam }, @@ -424,3 +426,21 @@ describe('usePageChangeEffectNavigateLocation', () => { }); }); }); + +describe('usePageChangeEffectNavigateLocation — authenticated with no current workspace', () => { + it.each([AppPath.Index, AppPath.TasksPage, AppPath.SettingsCatchAll])( + 'redirects to SignInUp from %s when authenticated with no current workspace', + (loc) => { + setupMockIsMatchingLocation(loc); + setupMockOnboardingStatus(undefined); + setupMockIsWorkspaceActivationStatusEqualsTo(false); + setupMockHasAccessTokenPair(true); + setupMockIsOnAWorkspace(true); + setupMockUseQuery(); + setupMockUseParams(); + setupMockState(undefined, undefined, undefined, undefined, null); + + expect(usePageChangeEffectNavigateLocation()).toEqual(AppPath.SignInUp); + }, + ); +}); diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts index 7100ef81f0..5ab89f153c 100644 --- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts +++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts @@ -2,6 +2,7 @@ import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPa import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths'; import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths'; import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { returnToPathState } from '@/auth/states/returnToPathState'; import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState'; import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; @@ -32,6 +33,7 @@ const readReturnToPathFromUrlSearchParams = (): string | null => { export const usePageChangeEffectNavigateLocation = () => { const hasAccessTokenPair = useHasAccessTokenPair(); + const currentWorkspace = useAtomStateValue(currentWorkspaceState); const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); const onboardingStatus = useOnboardingStatus(); const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo( @@ -75,7 +77,7 @@ export const usePageChangeEffectNavigateLocation = () => { : readReturnToPathFromUrlSearchParams(); if ( - (!hasAccessTokenPair || (hasAccessTokenPair && !isOnAWorkspace)) && + (!hasAccessTokenPair || !isOnAWorkspace || !isDefined(currentWorkspace)) && !someMatchingLocationOf([ ...ONGOING_USER_CREATION_PATHS, AppPath.ResetPassword, @@ -115,12 +117,12 @@ export const usePageChangeEffectNavigateLocation = () => { if ( onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION && !someMatchingLocationOf([ - AppPath.CreateWorkspace, + AppPath.WorkspaceActivation, AppPath.BookCallDecision, AppPath.BookCall, ]) ) { - return AppPath.CreateWorkspace; + return AppPath.WorkspaceActivation; } if ( diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 2ba2ab3822..90e43dca6d 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -301,13 +301,13 @@ export const PageChangeEffect = () => { }); break; } - case isMatchingLocation(location, AppPath.CreateWorkspace): { + case isMatchingLocation(location, AppPath.WorkspaceActivation): { resetFocusStackToFocusItem({ focusStackItem: { - focusId: PageFocusId.CreateWorkspace, + focusId: PageFocusId.WorkspaceActivation, componentInstance: { componentType: FocusComponentType.PAGE, - componentInstanceId: PageFocusId.CreateWorkspace, + componentInstanceId: PageFocusId.WorkspaceActivation, }, globalHotkeysConfig: { enableGlobalHotkeysWithModifiers: false, diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx index c626b4c2a5..083ea7d9f6 100644 --- a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx +++ b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx @@ -48,9 +48,9 @@ const Authorize = lazy(() => })), ); -const CreateWorkspace = lazy(() => - import('~/pages/onboarding/CreateWorkspace').then((module) => ({ - default: module.CreateWorkspace, +const WorkspaceActivation = lazy(() => + import('~/pages/onboarding/WorkspaceActivation').then((module) => ({ + default: module.WorkspaceActivation, })), ); @@ -148,10 +148,10 @@ export const useCreateAppRouter = ( } /> - + } /> diff --git a/packages/twenty-front/src/modules/auth/components/Logo.tsx b/packages/twenty-front/src/modules/auth/components/Logo.tsx index 20e7c9755f..7e21eccf3a 100644 --- a/packages/twenty-front/src/modules/auth/components/Logo.tsx +++ b/packages/twenty-front/src/modules/auth/components/Logo.tsx @@ -78,7 +78,7 @@ export const Logo = ({ {isUsingDefaultLogo ? ( redirectToDefaultDomain()} > { const { loadCurrentUser } = useLoadCurrentUser(); const apolloClient = useApolloClient(); - const { createWorkspace } = useSignUpInNewWorkspace(); - const setSignInUpStep = useSetAtomState(signInUpStepState); const { redirect } = useRedirect(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); @@ -131,16 +128,18 @@ export const useAuth = () => { async ( availableWorkspaces: Parameters[0], email: string, - { newTab = true }: { newTab?: boolean } = {}, ) => { const availableWorkspacesCount = countAvailableWorkspaces(availableWorkspaces); - if (availableWorkspacesCount === 0) { - if (!isMultiWorkspaceEnabled) { - return await createWorkspace({ newTab }); - } + // The in-app "Create Workspace" entry point redirects here with this + // signal so an existing user with workspaces lands on the creation form + // instead of the workspace selection step. + const wantsToCreateNewWorkspace = + new URLSearchParams(window.location.search).get('action') === + 'create-new-workspace'; + if (availableWorkspacesCount === 0 || wantsToCreateNewWorkspace) { await apolloClient.query({ query: GetWorkspaceCreationDefaultsDocument, }); @@ -166,13 +165,7 @@ export const useAuth = () => { setSignInUpStep(SignInUpStep.WorkspaceSelection); }, - [ - apolloClient, - createWorkspace, - isMultiWorkspaceEnabled, - redirectToWorkspaceDomain, - setSignInUpStep, - ], + [apolloClient, redirectToWorkspaceDomain, setSignInUpStep], ); const handleGetLoginTokenFromCredentials = useCallback( @@ -264,7 +257,6 @@ export const useAuth = () => { await navigateAfterMultiWorkspaceSignInUp( user.availableWorkspaces, user.email, - { newTab: false }, ); }, [ @@ -363,7 +355,6 @@ export const useAuth = () => { await navigateAfterMultiWorkspaceSignInUp( user.availableWorkspaces, user.email, - { newTab: false }, ); }, onError: (error) => { @@ -418,7 +409,6 @@ export const useAuth = () => { await navigateAfterMultiWorkspaceSignInUp( user.availableWorkspaces, user.email, - { newTab: false }, ); }, [ diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx index 91e00f086d..c3211fcfbb 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx @@ -11,7 +11,6 @@ import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboar import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials'; import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle'; import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft'; -import { SignInUpWorkspaceCreationForm } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm'; import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword'; import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { @@ -218,42 +217,38 @@ export const SignInUpGlobalScopeForm = () => { )} - {signInUpStep === SignInUpStep.WorkspaceCreation && ( - + {signInUpStep !== SignInUpStep.WorkspaceSelection && ( + + {authProviders.google && ( + + )} + {authProviders.microsoft && ( + + )} + {(authProviders.google || authProviders.microsoft) && ( + + )} + {/* oxlint-disable-next-line react/jsx-props-no-spreading */} + + + + {signInUpStep === SignInUpStep.Password && ( + + + Forgot your password? + + + )} + )} - {signInUpStep !== SignInUpStep.WorkspaceSelection && - signInUpStep !== SignInUpStep.WorkspaceCreation && ( - - {authProviders.google && ( - - )} - {authProviders.microsoft && ( - - )} - {(authProviders.google || authProviders.microsoft) && ( - - )} - {/* oxlint-disable-next-line react/jsx-props-no-spreading */} - - - - {signInUpStep === SignInUpStep.Password && ( - - - Forgot your password? - - - )} - - )} ); }; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx index 1b26d64031..0dd7d33c73 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx @@ -23,7 +23,6 @@ export const SignInUpGlobalScopeFormEffect = () => { await navigateAfterMultiWorkspaceSignInUp( user.availableWorkspaces, user.email, - { newTab: false }, ); }; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx index 7ad8cf9e5e..1133622fa4 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx @@ -2,14 +2,17 @@ import { SubTitle } from '@/auth/components/SubTitle'; import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer'; import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace'; import { useWorkspaceSubdomainField } from '@/auth/sign-in-up/hooks/useWorkspaceSubdomainField'; +import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; +import { ImageInput } from '@/ui/input/components/ImageInput'; import { InputHint } from '@/ui/input/components/InputHint'; +import { InputLabel } from '@/ui/input/components/InputLabel'; import { TextInput } from '@/ui/input/components/TextInput'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useLingui } from '@lingui/react/macro'; import { styled } from '@linaria/react'; import { isNonEmptyString } from '@sniptt/guards'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils'; import { Loader } from 'twenty-ui/feedback'; @@ -47,8 +50,15 @@ export const SignInUpWorkspaceCreationForm = () => { const { t } = useLingui(); const { createWorkspace } = useSignUpInNewWorkspace(); const { frontDomain } = useAtomStateValue(domainConfigurationState); + const isMultiWorkspaceEnabled = useAtomStateValue( + isMultiWorkspaceEnabledState, + ); const [isSubmitting, setIsSubmitting] = useState(false); + const [logo, setLogo] = useState(undefined); + const [logoPreviewUrl, setLogoPreviewUrl] = useState( + undefined, + ); const { workspaceName, @@ -60,10 +70,37 @@ export const SignInUpWorkspaceCreationForm = () => { handleWorkspaceNameChange, handleSubdomainChange, applySuggestion, - } = useWorkspaceSubdomainField(); + } = useWorkspaceSubdomainField({ + isSubdomainEnabled: isMultiWorkspaceEnabled, + }); const isContinueDisabled = - workspaceName.trim() === '' || !isAvailable || isSubmitting; + workspaceName.trim() === '' || + isSubmitting || + (isMultiWorkspaceEnabled && !isAvailable); + + const handleLogoUpload = (file: File) => { + if (!isDefined(file)) { + return; + } + setLogo(file); + setLogoPreviewUrl(URL.createObjectURL(file)); + }; + + const handleLogoRemove = () => { + setLogo(undefined); + setLogoPreviewUrl(undefined); + }; + + useEffect(() => { + if (!isDefined(logoPreviewUrl)) { + return; + } + + return () => { + URL.revokeObjectURL(logoPreviewUrl); + }; + }, [logoPreviewUrl]); const handleSubmit = async () => { if (isContinueDisabled) { @@ -74,8 +111,8 @@ export const SignInUpWorkspaceCreationForm = () => { try { await createWorkspace({ displayName: workspaceName.trim(), - subdomain, - newTab: false, + ...(isMultiWorkspaceEnabled ? { subdomain } : {}), + logo, }); } finally { setIsSubmitting(false); @@ -104,8 +141,18 @@ export const SignInUpWorkspaceCreationForm = () => { return ( - {t`Pick a name and a web address for your new workspace.`} + {isMultiWorkspaceEnabled + ? t`Pick a name and a web address for your new workspace.` + : t`Pick a name and a logo for your new workspace.`} + + {t`Workspace logo`} + + { fullWidth /> - - - {status === 'checking' && {t`Checking…`}} - {status === 'available' && ( - - {t`This address is available`} - - )} - {status === 'unavailable' && ( - - {subdomainError} - {isDefined(suggestion) && ( - - {t`Use ${suggestion} instead`} - - )} - - )} - + {isMultiWorkspaceEnabled && ( + + + {status === 'checking' && {t`Checking…`}} + {status === 'available' && ( + + {t`This address is available`} + + )} + {status === 'unavailable' && ( + + {subdomainError} + {isDefined(suggestion) && ( + + {t`Use ${suggestion} instead`} + + )} + + )} + + )} ({ useWorkspaceSubdomainField: () => useWorkspaceSubdomainFieldMock(), })); +global.URL.createObjectURL = jest.fn(() => 'blob:logo-preview'); +global.URL.revokeObjectURL = jest.fn(); + dynamicActivate(SOURCE_LOCALE); +const setMultiWorkspaceEnabled = (isEnabled: boolean) => { + jotaiStore.set(isMultiWorkspaceEnabledState.atom, isEnabled); +}; + const renderForm = () => render( @@ -56,73 +64,155 @@ describe('SignInUpWorkspaceCreationForm', () => { }); }); - it('keeps Continue disabled until a workspace name is entered', () => { - useWorkspaceSubdomainFieldMock.mockReturnValue({ - workspaceName: '', - subdomain: '', - status: 'idle', - errorMessage: undefined, - suggestion: undefined, - isAvailable: false, - handleWorkspaceNameChange: handleWorkspaceNameChangeMock, - handleSubdomainChange: handleSubdomainChangeMock, - applySuggestion: applySuggestionMock, + describe('multi-workspace', () => { + beforeEach(() => { + setMultiWorkspaceEnabled(true); }); - renderForm(); + it('keeps Continue disabled until a workspace name is entered', () => { + useWorkspaceSubdomainFieldMock.mockReturnValue({ + workspaceName: '', + subdomain: '', + status: 'idle', + errorMessage: undefined, + suggestion: undefined, + isAvailable: false, + handleWorkspaceNameChange: handleWorkspaceNameChangeMock, + handleSubdomainChange: handleSubdomainChangeMock, + applySuggestion: applySuggestionMock, + }); - expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled(); - }); + renderForm(); - it('creates the workspace with the chosen name and address in the same tab', async () => { - createWorkspaceMock.mockResolvedValue(undefined); - - renderForm(); - - const continueButton = screen.getByRole('button', { name: 'Continue' }); - - expect(continueButton).toBeEnabled(); - - await act(async () => { - fireEvent.click(continueButton); + expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled(); }); - expect(createWorkspaceMock).toHaveBeenCalledWith({ - displayName: 'Apple', - subdomain: 'apple', - newTab: false, + it('creates the workspace with the chosen name and address in the same tab', async () => { + createWorkspaceMock.mockResolvedValue(undefined); + + renderForm(); + + const continueButton = screen.getByRole('button', { name: 'Continue' }); + + expect(continueButton).toBeEnabled(); + + await act(async () => { + fireEvent.click(continueButton); + }); + + expect(createWorkspaceMock).toHaveBeenCalledWith({ + displayName: 'Apple', + subdomain: 'apple', + logo: undefined, + }); + }); + + it('passes the picked logo file when creating the workspace', async () => { + createWorkspaceMock.mockResolvedValue(undefined); + + const { container } = renderForm(); + + const fileInput = container.querySelector( + 'input[type="file"]', + ) as HTMLInputElement; + const logoFile = new File(['logo'], 'logo.png', { type: 'image/png' }); + + await act(async () => { + fireEvent.change(fileInput, { target: { files: [logoFile] } }); + }); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + expect(createWorkspaceMock).toHaveBeenCalledWith({ + displayName: 'Apple', + subdomain: 'apple', + logo: logoFile, + }); + }); + + it('routes name edits back through the field hook', () => { + renderForm(); + + fireEvent.change(screen.getByLabelText('Workspace name'), { + target: { value: 'Acme' }, + }); + + expect(handleWorkspaceNameChangeMock).toHaveBeenCalledWith('Acme'); + }); + + it('offers a one-click suggestion when the address is taken', () => { + useWorkspaceSubdomainFieldMock.mockReturnValue({ + workspaceName: 'Apple', + subdomain: 'apple', + status: 'unavailable', + errorMessage: undefined, + suggestion: 'apple-2', + isAvailable: false, + handleWorkspaceNameChange: handleWorkspaceNameChangeMock, + handleSubdomainChange: handleSubdomainChangeMock, + applySuggestion: applySuggestionMock, + }); + + renderForm(); + + expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled(); + + fireEvent.click(screen.getByText('Use apple-2 instead')); + + expect(applySuggestionMock).toHaveBeenCalledTimes(1); }); }); - it('routes name edits back through the field hook', () => { - renderForm(); - - fireEvent.change(screen.getByLabelText('Workspace name'), { - target: { value: 'Acme' }, + describe('single-workspace', () => { + beforeEach(() => { + setMultiWorkspaceEnabled(false); }); - expect(handleWorkspaceNameChangeMock).toHaveBeenCalledWith('Acme'); - }); + it('hides the workspace address field', () => { + renderForm(); - it('offers a one-click suggestion when the address is taken', () => { - useWorkspaceSubdomainFieldMock.mockReturnValue({ - workspaceName: 'Apple', - subdomain: 'apple', - status: 'unavailable', - errorMessage: undefined, - suggestion: 'apple-2', - isAvailable: false, - handleWorkspaceNameChange: handleWorkspaceNameChangeMock, - handleSubdomainChange: handleSubdomainChangeMock, - applySuggestion: applySuggestionMock, + expect(screen.getByLabelText('Workspace name')).toBeInTheDocument(); + expect( + screen.queryByLabelText('Workspace address'), + ).not.toBeInTheDocument(); }); - renderForm(); + it('enables Continue based on the name only and creates without a subdomain', async () => { + createWorkspaceMock.mockResolvedValue(undefined); - expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled(); + // An unavailable subdomain status must not block submission when the + // address field is hidden. + useWorkspaceSubdomainFieldMock.mockReturnValue({ + workspaceName: 'Apple', + subdomain: 'apple', + status: 'unavailable', + errorMessage: undefined, + suggestion: 'apple-2', + isAvailable: false, + handleWorkspaceNameChange: handleWorkspaceNameChangeMock, + handleSubdomainChange: handleSubdomainChangeMock, + applySuggestion: applySuggestionMock, + }); - fireEvent.click(screen.getByText('Use apple-2 instead')); + renderForm(); - expect(applySuggestionMock).toHaveBeenCalledTimes(1); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + + expect(continueButton).toBeEnabled(); + + await act(async () => { + fireEvent.click(continueButton); + }); + + expect(createWorkspaceMock).toHaveBeenCalledWith({ + displayName: 'Apple', + logo: undefined, + }); + expect(createWorkspaceMock.mock.calls[0][0]).not.toHaveProperty( + 'subdomain', + ); + }); }); }); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx index 1a45804a65..2e43bdd47c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx @@ -94,6 +94,22 @@ describe('useWorkspaceSubdomainField', () => { expect(result.current.isAvailable).toBe(true); }); + it('skips availability checks when the subdomain field is disabled', () => { + const { result } = renderHook( + () => useWorkspaceSubdomainField({ isSubdomainEnabled: false }), + { wrapper: createWrapper([]) }, + ); + + act(() => { + result.current.handleWorkspaceNameChange('Apple'); + }); + + // The name is still tracked, but no subdomain is derived and no lookup runs. + expect(result.current.workspaceName).toBe('Apple'); + expect(result.current.subdomain).toBe(''); + expect(result.current.status).toBe('idle'); + }); + it('reports an unavailable manual address and offers a suggestion', async () => { const { result } = renderHook(() => useWorkspaceSubdomainField(), { wrapper: createWrapper([ diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts index e8b2caa420..cfd89e5938 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts @@ -13,6 +13,7 @@ import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum'; import { SignInUpMode } from '@/auth/types/signInUpMode'; import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; import { useCaptcha } from '@/client-config/hooks/useCaptcha'; +import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates'; import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -24,6 +25,7 @@ import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams import { isMatchingLocation } from '~/utils/isMatchingLocation'; import { useAuth } from '@/auth/hooks/useAuth'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; export const useSignInUp = (form: UseFormReturn
) => { @@ -33,6 +35,9 @@ export const useSignInUp = (form: UseFormReturn) => { const [signInUpStep, setSignInUpStep] = useAtomState(signInUpStepState); const [signInUpMode, setSignInUpMode] = useAtomState(signInUpModeState); const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); + const isMultiWorkspaceEnabled = useAtomStateValue( + isMultiWorkspaceEnabledState, + ); const { isCaptchaReady } = useCaptcha(); const setLastAuthenticatedMethod = useSetAtomState( lastAuthenticatedMethodState, @@ -156,7 +161,7 @@ export const useSignInUp = (form: UseFormReturn) => { if ( !isInviteMode && signInUpMode === SignInUpMode.SignUp && - !isOnAWorkspace + (!isOnAWorkspace || !isMultiWorkspaceEnabled) ) { return await signUpWithCredentials( data.email.toLowerCase().trim(), @@ -198,6 +203,7 @@ export const useSignInUp = (form: UseFormReturn) => { enqueueErrorSnackBar, buildSearchParamsFromUrlSyncedStates, isOnAWorkspace, + isMultiWorkspaceEnabled, setLastAuthenticatedMethod, t, ], diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts index 26c6763be2..2269b6fae9 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts @@ -1,43 +1,82 @@ +import { useAuth } from '@/auth/hooks/useAuth'; +import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { CombinedGraphQLErrors } from '@apollo/client/errors'; import { AppPath } from 'twenty-shared/types'; import { useMutation } from '@apollo/client/react'; -import { SignUpInNewWorkspaceDocument } from '~/generated-metadata/graphql'; +import { + SignUpInNewWorkspaceDocument, + UploadNewWorkspaceLogoDocument, +} from '~/generated-metadata/graphql'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; -import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { useLingui } from '@lingui/react/macro'; export const useSignUpInNewWorkspace = () => { const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); + const { getAuthTokensFromLoginToken } = useAuth(); + const isMultiWorkspaceEnabled = useAtomStateValue( + isMultiWorkspaceEnabledState, + ); const { enqueueErrorSnackBar } = useSnackBar(); const { t } = useLingui(); const [signUpInNewWorkspaceMutation] = useMutation( SignUpInNewWorkspaceDocument, ); + const [uploadNewWorkspaceLogoMutation] = useMutation( + UploadNewWorkspaceLogoDocument, + ); const createWorkspace = async ({ displayName, subdomain, - newTab = true, + logo, }: { displayName?: string; subdomain?: string; - newTab?: boolean; + logo?: File; } = {}) => { try { const { data } = await signUpInNewWorkspaceMutation({ variables: { input: { displayName, subdomain } }, }); assertIsDefinedOrThrow(data?.signUpInNewWorkspace); + + const workspaceId = data.signUpInNewWorkspace.workspace.id; + + if (isDefined(logo)) { + try { + await uploadNewWorkspaceLogoMutation({ + variables: { workspaceId, file: logo }, + }); + } catch (logoUploadError) { + enqueueErrorSnackBar( + CombinedGraphQLErrors.is(logoUploadError) + ? { apolloError: logoUploadError } + : { + message: + logoUploadError instanceof Error + ? logoUploadError.message + : t`Workspace logo upload failed`, + }, + ); + } + } + + const loginToken = data.signUpInNewWorkspace.loginToken.token; + + if (!isMultiWorkspaceEnabled) { + return await getAuthTokensFromLoginToken(loginToken); + } + return await redirectToWorkspaceDomain( getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls), AppPath.Verify, - { - loginToken: data.signUpInNewWorkspace.loginToken.token, - }, - newTab ? '_blank' : '_self', + { loginToken }, + '_self', ); } catch (error) { enqueueErrorSnackBar( diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts index 889c20e58f..e15bf2f9e7 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts @@ -21,7 +21,9 @@ export type SubdomainFieldStatus = const AVAILABILITY_CHECK_DEBOUNCE_MS = 400; -export const useWorkspaceSubdomainField = () => { +export const useWorkspaceSubdomainField = ({ + isSubdomainEnabled = true, +}: { isSubdomainEnabled?: boolean } = {}) => { const apolloClient = useApolloClient(); const subdomainSchema = useMemo(() => getSubdomainValidationSchema(), []); @@ -118,7 +120,7 @@ export const useWorkspaceSubdomainField = () => { const handleWorkspaceNameChange = (name: string) => { setWorkspaceName(name); - if (isManuallyEdited) { + if (!isSubdomainEnabled || isManuallyEdited) { return; } diff --git a/packages/twenty-front/src/modules/auth/utils/__tests__/isValidReturnToPath.test.ts b/packages/twenty-front/src/modules/auth/utils/__tests__/isValidReturnToPath.test.ts index fbc9f8e39b..701c24fa98 100644 --- a/packages/twenty-front/src/modules/auth/utils/__tests__/isValidReturnToPath.test.ts +++ b/packages/twenty-front/src/modules/auth/utils/__tests__/isValidReturnToPath.test.ts @@ -24,7 +24,7 @@ describe('isValidReturnToPath', () => { }); it('should return false for onboarding paths', () => { - expect(isValidReturnToPath('/create/workspace')).toBe(false); + expect(isValidReturnToPath('/workspace-activation')).toBe(false); expect(isValidReturnToPath('/create/profile')).toBe(false); }); diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts index 0ba43e130b..a92f466517 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts @@ -12,7 +12,10 @@ export const useRedirectToDefaultDomain = () => { const store = useStore(); const { redirect } = useRedirect(); - const redirectToDefaultDomain = () => { + const redirectToDefaultDomain = (options?: { + pathname?: string; + searchParams?: Record; + }) => { const url = new URL(window.location.href); if (url.hostname !== defaultDomain) { setLastAuthenticateWorkspaceDomain(null); @@ -25,6 +28,14 @@ export const useRedirectToDefaultDomain = () => { url.searchParams.set('returnToPath', returnToPath); } + if (isNonEmptyString(options?.pathname)) { + url.pathname = options.pathname; + } + + Object.entries(options?.searchParams ?? {}).forEach(([key, value]) => { + url.searchParams.set(key, value); + }); + url.hostname = defaultDomain; redirect(url.toString()); } diff --git a/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx b/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx index c570d1942e..393769027c 100644 --- a/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx +++ b/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx @@ -21,7 +21,7 @@ export const MinimalMetadataGater = ({ children }: React.PropsWithChildren) => { isMatchingLocation(location, AppPath.SignInUp) || isMatchingLocation(location, AppPath.Invite) || isMatchingLocation(location, AppPath.ResetPassword) || - isMatchingLocation(location, AppPath.CreateWorkspace) || + isMatchingLocation(location, AppPath.WorkspaceActivation) || isMatchingLocation(location, AppPath.PlanRequired) || isMatchingLocation(location, AppPath.PlanRequiredSuccess) || isMatchingLocation(location, AppPath.Authorize); diff --git a/packages/twenty-front/src/modules/types/PageFocusId.ts b/packages/twenty-front/src/modules/types/PageFocusId.ts index 966b36b80d..c2e739fefd 100644 --- a/packages/twenty-front/src/modules/types/PageFocusId.ts +++ b/packages/twenty-front/src/modules/types/PageFocusId.ts @@ -1,6 +1,6 @@ export enum PageFocusId { Settings = 'settings', - CreateWorkspace = 'create-workspace', + WorkspaceActivation = 'workspace-activation', SignInUp = 'sign-in-up', CreateProfile = 'create-profile', InviteTeam = 'invite-team', diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx index bf0c66ca3e..8c8306b46f 100644 --- a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx +++ b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx @@ -32,7 +32,7 @@ const testCases = [ { loc: AppPath.SignInUp, res: true }, { loc: AppPath.Invite, res: true }, { loc: AppPath.ResetPassword, res: true }, - { loc: AppPath.CreateWorkspace, res: true }, + { loc: AppPath.WorkspaceActivation, res: true }, { loc: AppPath.SyncEmails, res: true }, { loc: AppPath.InviteTeam, res: true }, { loc: AppPath.PlanRequired, res: true }, diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts b/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts index ebfc27ffac..4e678cb62f 100644 --- a/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts +++ b/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts @@ -17,7 +17,7 @@ export const useShowAuthModal = () => { isMatchingLocation(location, AppPath.VerifyEmail) || isMatchingLocation(location, AppPath.Verify) || isMatchingLocation(location, AppPath.SignInUp) || - isMatchingLocation(location, AppPath.CreateWorkspace) || + isMatchingLocation(location, AppPath.WorkspaceActivation) || isMatchingLocation(location, AppPath.PlanRequired) || isMatchingLocation(location, AppPath.PlanRequiredSuccess) || isMatchingLocation(location, AppPath.BookCallDecision) || diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx index ecfef131e2..f7f27d9b89 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx @@ -6,8 +6,8 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils'; import { supportChatState } from '@/client-config/states/supportChatState'; import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl'; +import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; @@ -21,7 +21,6 @@ import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/s import { useColorScheme } from '@/ui/theme/hooks/useColorScheme'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { CombinedGraphQLErrors } from '@apollo/client/errors'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { themeCssVariables } from 'twenty-ui/theme-constants'; @@ -44,11 +43,7 @@ import { MenuItemSelectAvatar, UndecoratedLink, } from 'twenty-ui/navigation'; -import { useMutation } from '@apollo/client/react'; -import { - type AvailableWorkspace, - SignUpInNewWorkspaceDocument, -} from '~/generated-metadata/graphql'; +import { type AvailableWorkspace } from '~/generated-metadata/graphql'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; const StyledDescription = styled.div` @@ -64,19 +59,15 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { const availableWorkspacesCount = countAvailableWorkspaces(availableWorkspaces); const { buildWorkspaceUrl } = useBuildWorkspaceUrl(); + const { redirectToDefaultDomain } = useRedirectToDefaultDomain(); const { closeDropdown } = useCloseDropdown(); const { signOut } = useAuth(); - const { enqueueErrorSnackBar } = useSnackBar(); const { colorScheme, colorSchemeList } = useColorScheme(); const supportChat = useAtomStateValue(supportChatState); const isSupportChatConfigured = supportChat?.supportDriver === 'FRONT' && isNonEmptyString(supportChat.supportFrontChatId); - const [signUpInNewWorkspaceMutation] = useMutation( - SignUpInNewWorkspaceDocument, - ); - const setMultiWorkspaceDropdown = useSetAtomState( multiWorkspaceDropdownState, ); @@ -94,23 +85,14 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { ); }; + // The workspace name (and logo + subdomain) are collected by the shared + // creation form on the root domain, so we send the user there on the + // WorkspaceCreation step instead of creating a nameless workspace on the fly. const createWorkspace = () => { - signUpInNewWorkspaceMutation({ - onCompleted: async (data) => { - return await redirectToWorkspaceDomain( - getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls), - AppPath.Verify, - { - loginToken: data.signUpInNewWorkspace.loginToken.token, - }, - '_blank', - ); - }, - onError: (error) => { - enqueueErrorSnackBar({ - ...(CombinedGraphQLErrors.is(error) ? { apolloError: error } : {}), - }); - }, + closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); + redirectToDefaultDomain({ + pathname: AppPath.SignInUp, + searchParams: { action: 'create-new-workspace' }, }); }; diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx index de4d88e5e2..1a023a33d9 100644 --- a/packages/twenty-front/src/pages/auth/SignInUp.tsx +++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx @@ -18,6 +18,7 @@ import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGl import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm'; import { WorkspaceSelectionFooter } from '@/auth/sign-in-up/components/WorkspaceSelectionFooter'; import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/internal/SignInUpSSOIdentityProviderSelection'; +import { SignInUpWorkspaceCreationForm } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm'; import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceScopeFormEffect'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain'; @@ -163,6 +164,13 @@ export const SignInUp = () => { ); } + // The workspace creation form is shared by both multi-workspace and + // single-workspace self-host, so it must render regardless of domain or + // workspace scope. + if (signInUpStep === SignInUpStep.WorkspaceCreation) { + return ; + } + if (isDefaultDomain && isMultiWorkspaceEnabled) { return ( <> diff --git a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx deleted file mode 100644 index 9a1e76c6a7..0000000000 --- a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { styled } from '@linaria/react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useCallback, useState } from 'react'; -import { Controller, type SubmitHandler, useForm } from 'react-hook-form'; -import { Key } from 'ts-key-enum'; -import { z } from 'zod'; - -import { Logo } from '@/auth/components/Logo'; -import { SubTitle } from '@/auth/components/SubTitle'; -import { Title } from '@/auth/components/Title'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; -import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { TextInput } from '@/ui/input/components/TextInput'; -import { ModalContent } from 'twenty-ui/surfaces'; -import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; -import { CombinedGraphQLErrors } from '@apollo/client/errors'; -import { Trans, useLingui } from '@lingui/react/macro'; -import { isNonEmptyString } from '@sniptt/guards'; - -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { isDefined } from 'twenty-shared/utils'; -import { H2Title } from 'twenty-ui/typography'; -import { Loader } from 'twenty-ui/feedback'; -import { MainButton } from 'twenty-ui/input'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { useMutation } from '@apollo/client/react'; -import { ActivateWorkspaceDocument } from '~/generated-metadata/graphql'; - -const StyledContentContainer = styled.div` - width: 100%; -`; - -const StyledSectionContainer = styled.div` - margin-top: ${themeCssVariables.spacing[8]}; -`; - -const StyledButtonContainer = styled.div` - margin-top: ${themeCssVariables.spacing[8]}; - width: 200px; -`; - -const StyledLoaderContainer = styled.div` - align-items: center; - display: flex; - justify-content: center; - margin-bottom: ${themeCssVariables.spacing[8]}; - margin-top: ${themeCssVariables.spacing[8]}; - width: 100%; -`; - -enum PendingCreationLoaderStep { - None = 'none', - Step1 = 'step-1', - Step2 = 'step-2', - Step3 = 'step-3', -} - -const StyledPendingCreationLoader = styled.div` - align-items: center; - display: flex; - justify-content: center; - width: 100%; -`; - -export const CreateWorkspace = () => { - const { t } = useLingui(); - const { enqueueErrorSnackBar } = useSnackBar(); - const setNextOnboardingStatus = useSetNextOnboardingStatus(); - const { loadCurrentUser } = useLoadCurrentUser(); - const [activateWorkspace] = useMutation(ActivateWorkspaceDocument); - const [pendingCreationLoaderStep, setPendingCreationLoaderStep] = useState( - PendingCreationLoaderStep.None, - ); - const currentWorkspace = useAtomStateValue(currentWorkspaceState); - - const validationSchema = z - .object({ - name: z.string().min(1, { message: t`Name can not be empty` }), - }) - .required(); - - type Form = z.infer; - - // Form - const { - control, - handleSubmit, - formState: { isValid, isSubmitting }, - } = useForm({ - mode: 'onChange', - defaultValues: { - name: currentWorkspace?.displayName ?? '', - }, - resolver: zodResolver(validationSchema), - }); - - const onSubmit: SubmitHandler = useCallback( - async (data) => { - try { - setTimeout(() => { - setPendingCreationLoaderStep(PendingCreationLoaderStep.Step1); - }, 500); - setTimeout(() => { - setPendingCreationLoaderStep(PendingCreationLoaderStep.Step2); - }, 2000); - setTimeout(() => { - setPendingCreationLoaderStep(PendingCreationLoaderStep.Step3); - }, 5000); - - const result = await activateWorkspace({ - variables: { - input: { - displayName: data.name, - }, - }, - }); - - if (isDefined(result.error)) { - throw result.error ?? new Error(t`Unknown error`); - } - - await loadCurrentUser(); - setNextOnboardingStatus(); - } catch (error: any) { - setPendingCreationLoaderStep(PendingCreationLoaderStep.None); - - enqueueErrorSnackBar({ - apolloError: CombinedGraphQLErrors.is(error) ? error : undefined, - }); - } - }, - [ - activateWorkspace, - enqueueErrorSnackBar, - loadCurrentUser, - setNextOnboardingStatus, - t, - ], - ); - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.nativeEvent.isComposing || event.keyCode === 229) { - return; - } - if (event.key === Key.Enter) { - event.preventDefault(); - handleSubmit(onSubmit)(); - } - }; - - return ( - - {pendingCreationLoaderStep !== PendingCreationLoaderStep.None && ( - <> - - - <Trans>Creating your workspace</Trans> - - - {pendingCreationLoaderStep === PendingCreationLoaderStep.Step1 && ( - - Setting up your database... - - )} - {pendingCreationLoaderStep === PendingCreationLoaderStep.Step2 && ( - - Creating your data model... - - )} - {pendingCreationLoaderStep === PendingCreationLoaderStep.Step3 && ( - - Prefilling your workspace data... - - )} - - - - - - )} - {pendingCreationLoaderStep === PendingCreationLoaderStep.None && ( - <> - - <Trans>Create your workspace</Trans> - - - - A shared environment where you will be able to manage your - customer relations with your team. - - - - - - - - - - - ( - - )} - /> - - - - isSubmitting && } - fullWidth - /> - - - )} - - ); -}; diff --git a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx index 365371795d..3f3a370673 100644 --- a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx +++ b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx @@ -40,7 +40,7 @@ export const PaymentSuccess = () => { try { if (isDefined(subscriptionStatus)) { - navigate(AppPath.CreateWorkspace); + navigate(AppPath.WorkspaceActivation); return; } @@ -51,7 +51,7 @@ export const PaymentSuccess = () => { if (isDefined(currentUser) && isDefined(refreshedSubscriptionStatus)) { setCurrentUser(currentUser); - navigate(AppPath.CreateWorkspace); + navigate(AppPath.WorkspaceActivation); return; } diff --git a/packages/twenty-front/src/pages/onboarding/WorkspaceActivation.tsx b/packages/twenty-front/src/pages/onboarding/WorkspaceActivation.tsx new file mode 100644 index 0000000000..d1a8e1cb46 --- /dev/null +++ b/packages/twenty-front/src/pages/onboarding/WorkspaceActivation.tsx @@ -0,0 +1,180 @@ +import { styled } from '@linaria/react'; +import { useCallback, useEffect, useState } from 'react'; + +import { Logo } from '@/auth/components/Logo'; +import { SubTitle } from '@/auth/components/SubTitle'; +import { Title } from '@/auth/components/Title'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ModalContent } from 'twenty-ui/surfaces'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; +import { CombinedGraphQLErrors } from '@apollo/client/errors'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { isNonEmptyString } from '@sniptt/guards'; + +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { isDefined } from 'twenty-shared/utils'; +import { Loader } from 'twenty-ui/feedback'; +import { MainButton } from 'twenty-ui/input'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { useMutation } from '@apollo/client/react'; +import { ActivateWorkspaceDocument } from '~/generated-metadata/graphql'; + +const StyledButtonContainer = styled.div` + margin-top: ${themeCssVariables.spacing[8]}; + width: 200px; +`; + +const StyledLoaderContainer = styled.div` + align-items: center; + display: flex; + justify-content: center; + margin-bottom: ${themeCssVariables.spacing[8]}; + margin-top: ${themeCssVariables.spacing[8]}; + width: 100%; +`; + +type ActivationStep = 'pending' | 'database' | 'data-model' | 'prefill'; + +const StyledActivationStep = styled.div` + align-items: center; + display: flex; + justify-content: center; + width: 100%; +`; + +export const WorkspaceActivation = () => { + const { t } = useLingui(); + const { enqueueErrorSnackBar } = useSnackBar(); + const setNextOnboardingStatus = useSetNextOnboardingStatus(); + const { loadCurrentUser } = useLoadCurrentUser(); + const [activateWorkspace, { loading: isActivating }] = useMutation( + ActivateWorkspaceDocument, + ); + const [activationStep, setActivationStep] = + useState('pending'); + const [hasFailed, setHasFailed] = useState(false); + const currentWorkspace = useAtomStateValue(currentWorkspaceState); + + const activate = useCallback(async () => { + setHasFailed(false); + + const databaseTimeout = setTimeout(() => { + setActivationStep('database'); + }, 500); + const dataModelTimeout = setTimeout(() => { + setActivationStep('data-model'); + }, 2000); + const prefillTimeout = setTimeout(() => { + setActivationStep('prefill'); + }, 5000); + + const clearStepTimeouts = () => { + clearTimeout(databaseTimeout); + clearTimeout(dataModelTimeout); + clearTimeout(prefillTimeout); + }; + + try { + const result = await activateWorkspace({ + variables: { + input: {}, + }, + }); + + if (isDefined(result.error)) { + throw result.error; + } + + await loadCurrentUser(); + setNextOnboardingStatus(); + } catch (error) { + setActivationStep('pending'); + setHasFailed(true); + + enqueueErrorSnackBar({ + apolloError: CombinedGraphQLErrors.is(error) ? error : undefined, + }); + } finally { + clearStepTimeouts(); + } + }, [ + activateWorkspace, + enqueueErrorSnackBar, + loadCurrentUser, + setNextOnboardingStatus, + ]); + + const [hasTriggered, setHasTriggered] = useState(false); + useEffect(() => { + if (hasTriggered || !isDefined(currentWorkspace)) { + return; + } + + setHasTriggered(true); + void activate(); + }, [activate, hasTriggered, currentWorkspace]); + + return ( + + + + {hasFailed ? ( + <Trans>Workspace creation failed</Trans> + ) : ( + <Trans>Creating your workspace</Trans> + )} + + {hasFailed ? ( + <> + + + Something went wrong while creating your workspace. Please try + again. + + + + { + void activate(); + }} + disabled={isActivating} + fullWidth + /> + + + ) : ( + <> + + {activationStep === 'database' && ( + + Setting up your database... + + )} + {activationStep === 'data-model' && ( + + Creating your data model... + + )} + {activationStep === 'prefill' && ( + + Prefilling your workspace data... + + )} + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/pages/onboarding/__stories__/CreateWorkspace.stories.tsx b/packages/twenty-front/src/pages/onboarding/__stories__/WorkspaceActivation.stories.tsx similarity index 79% rename from packages/twenty-front/src/pages/onboarding/__stories__/CreateWorkspace.stories.tsx rename to packages/twenty-front/src/pages/onboarding/__stories__/WorkspaceActivation.stories.tsx index 65b228becf..3d4273cbf1 100644 --- a/packages/twenty-front/src/pages/onboarding/__stories__/CreateWorkspace.stories.tsx +++ b/packages/twenty-front/src/pages/onboarding/__stories__/WorkspaceActivation.stories.tsx @@ -6,7 +6,7 @@ import { within } from 'storybook/test'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { AppPath } from 'twenty-shared/types'; import { OnboardingStatus } from '~/generated-metadata/graphql'; -import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace'; +import { WorkspaceActivation } from '~/pages/onboarding/WorkspaceActivation'; import { PageDecorator, type PageDecoratorArgs, @@ -15,10 +15,10 @@ import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedOnboardingUserData } from '~/testing/mock-data/users'; const meta: Meta = { - title: 'Pages/Onboarding/CreateWorkspace', - component: CreateWorkspace, + title: 'Pages/Onboarding/WorkspaceActivation', + component: WorkspaceActivation, decorators: [PageDecorator], - args: { routePath: AppPath.CreateWorkspace }, + args: { routePath: AppPath.WorkspaceActivation }, parameters: { msw: { handlers: [ @@ -39,12 +39,12 @@ const meta: Meta = { export default meta; -export type Story = StoryObj; +export type Story = StoryObj; export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement.ownerDocument.body); - await canvas.findByText('Create your workspace', undefined, { + await canvas.findByText('Creating your workspace', undefined, { timeout: 3000, }); }, diff --git a/packages/twenty-front/src/utils/__tests__/title-utils.test.ts b/packages/twenty-front/src/utils/__tests__/title-utils.test.ts index 9d30ab2241..16c7db7ce5 100644 --- a/packages/twenty-front/src/utils/__tests__/title-utils.test.ts +++ b/packages/twenty-front/src/utils/__tests__/title-utils.test.ts @@ -12,7 +12,9 @@ describe('title-utils', () => { 'Sign in or Create an account', ); expect(getPageTitleFromPath('/invite/:workspaceInviteHash')).toBe('Invite'); - expect(getPageTitleFromPath('/create/workspace')).toBe('Create Workspace'); + expect(getPageTitleFromPath('/workspace-activation')).toBe( + 'Create Workspace', + ); expect(getPageTitleFromPath('/create/profile')).toBe('Create Profile'); expect(getPageTitleFromPath('/settings/objects/opportunities')).toBe( 'Data model - Settings', diff --git a/packages/twenty-front/src/utils/title-utils.ts b/packages/twenty-front/src/utils/title-utils.ts index 618816949c..0d54fb9a66 100644 --- a/packages/twenty-front/src/utils/title-utils.ts +++ b/packages/twenty-front/src/utils/title-utils.ts @@ -32,7 +32,7 @@ export const getPageTitleFromPath = (pathname: string): string => { return t`Sign in or Create an account`; case AppPath.Invite: return t`Invite`; - case AppPath.CreateWorkspace: + case AppPath.WorkspaceActivation: return t`Create Workspace`; case AppPath.CreateProfile: return t`Create Profile`; diff --git a/packages/twenty-server/jest.config.mjs b/packages/twenty-server/jest.config.mjs index 8ffcb033b2..1a5079a060 100644 --- a/packages/twenty-server/jest.config.mjs +++ b/packages/twenty-server/jest.config.mjs @@ -20,7 +20,7 @@ const jestConfig = { // jsdom 29 pulls ESM-only transitive deps (parse5, entities, tough-cookie, // @exodus/bytes via html-encoding-sniffer, @csstools/@asamuzakjp css engine). // jest's CJS runtime can't load their `export` syntax, so let swc transform them. - '/node_modules/(?!(file-type|@file-type|strtok3|token-types|@borewit|@tokenizer|uint8array-extras|read-next-line|digest-fetch|md5|js-sha256|js-sha512|base-64|charenc|crypt|email-reply-parser|jsdom|html-encoding-sniffer|whatwg-encoding|@exodus|parse5|entities|tough-cookie|@csstools|@asamuzakjp)/)', + '/node_modules/(?!(file-type|@file-type|strtok3|token-types|@borewit|@tokenizer|uint8array-extras|read-next-line|digest-fetch|md5|js-sha256|js-sha512|base-64|charenc|crypt|email-reply-parser|jsdom|html-encoding-sniffer|whatwg-encoding|@exodus|parse5|entities|tough-cookie|@csstools|@asamuzakjp|graphql-upload|fs-capacitor)/)', ], testRegex: '.*\\.spec\\.ts$', transform: { diff --git a/packages/twenty-server/src/engine/core-modules/application/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/application.service.ts index f8d98a0001..2a9f43f83f 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.service.ts @@ -320,6 +320,15 @@ export class ApplicationService { }, queryRunner?: QueryRunner, ) { + const existingApplication = await this.findByUniversalIdentifier({ + universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier, + workspaceId, + }); + + if (isDefined(existingApplication)) { + return existingApplication; + } + const defaultPackageFields = await getDefaultApplicationPackageFields(); const twentyStandardApplication = await this.create( diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts index 154b7c2873..8b5b650be0 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts @@ -15,6 +15,7 @@ import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomai import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { TwoFactorAuthenticationService } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.service'; @@ -77,6 +78,10 @@ describe('AuthResolver', () => { provide: SubdomainManagerService, useValue: {}, }, + { + provide: FileCorePictureService, + useValue: {}, + }, { provide: UserWorkspaceService, useValue: {}, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 665b48e9a9..ce3083bcc9 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -2,6 +2,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Context, Mutation, Query } from '@nestjs/graphql'; import { InjectRepository } from '@nestjs/typeorm'; +import bytes from 'bytes'; +import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import omit from 'lodash.omit'; import { PermissionFlagType } from 'twenty-shared/constants'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; @@ -9,7 +11,10 @@ import { TwoFactorAuthenticationStrategy } from 'twenty-shared/types'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; +import type { FileUpload } from 'graphql-upload/processRequest.mjs'; + import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; +import { settings } from 'src/engine/constants/settings'; import { ApiKeyService } from 'src/engine/core-modules/api-key/services/api-key.service'; import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity'; import { EventLogEmitterService } from 'src/engine/core-modules/event-logs/emit/event-log-emitter.service'; @@ -57,6 +62,8 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac import { EmailVerificationExceptionFilter } from 'src/engine/core-modules/email-verification/email-verification-exception-filter.util'; import { EmailVerificationTrigger } from 'src/engine/core-modules/email-verification/email-verification.constants'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; +import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto'; +import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; @@ -84,6 +91,7 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; +import { streamToBuffer } from 'src/utils/stream-to-buffer'; import { ApiKeyToken } from './dto/api-key-token.dto'; import { AuthToken } from './dto/auth-token.dto'; @@ -136,6 +144,7 @@ export class AuthResolver { private readonly eventLogEmitterService: EventLogEmitterService, private readonly impersonationAuthorizationService: ImpersonationAuthorizationService, private readonly subdomainManagerService: SubdomainManagerService, + private readonly fileCorePictureService: FileCorePictureService, ) {} @UseGuards(CaptchaGuard, PublicEndpointGuard, NoPermissionGuard) @@ -557,6 +566,34 @@ export class AuthResolver { }; } + @Mutation(() => FileWithSignedUrlDTO) + @UseGuards(UserAuthGuard, NoPermissionGuard) + async uploadNewWorkspaceLogo( + @AuthUser() currentUser: AuthContextUser, + @Args('workspaceId') workspaceId: string, + @Args({ name: 'file', type: () => GraphQLUpload }) + { createReadStream, filename }: FileUpload, + ): Promise { + const workspace = + await this.fileCorePictureService.getPendingWorkspaceForLogoUploadOrThrow( + { + userId: currentUser.id, + workspaceId, + }, + ); + + const buffer = await streamToBuffer( + createReadStream(), + bytes(settings.storage.maxFileSize) ?? undefined, + ); + + return this.fileCorePictureService.uploadWorkspacePicture({ + file: buffer, + filename, + workspace, + }); + } + @Mutation(() => TransientTokenDTO) @UseGuards(UserAuthGuard, NoPermissionGuard) async generateTransientToken( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index fde5b5d08b..85032cee24 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -951,7 +951,6 @@ export class AuthService { workspaceInviteHash, workspaceId, billingCheckoutSessionState, - action, locale, returnToPath, }: MicrosoftRequest['user'] | GoogleRequest['user'], @@ -964,11 +963,7 @@ export class AuthService { // Route SSO sign-ins through the same create-or-select flow as credentials // instead of landing straight on a workspace subdomain. - if ( - !workspaceId && - !workspaceInviteHash && - action === 'list-available-workspaces' - ) { + if (!workspaceId && !workspaceInviteHash) { const user = existingUser ?? (await this.signInUpService.signUpWithoutWorkspace( @@ -1010,15 +1005,12 @@ export class AuthService { return url.toString(); } - const currentWorkspace = - action === 'create-new-workspace' - ? undefined - : await this.findWorkspaceForSignInUp({ - workspaceId, - workspaceInviteHash, - email, - authProvider, - }); + const currentWorkspace = await this.findWorkspaceForSignInUp({ + workspaceId, + workspaceInviteHash, + email, + authProvider, + }); try { const invitation = diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 7ca8478463..6be7380327 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -516,6 +516,18 @@ export class SignInUpService { await this.assertWorkspaceCreationAllowed(userData); + const displayName = options?.displayName?.trim(); + + if (!displayName) { + throw new AuthException( + 'Workspace name is required', + AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: msg`Workspace name is required`, + }, + ); + } + const requestedSubdomain = options?.subdomain; if (isDefined(requestedSubdomain)) { @@ -544,7 +556,7 @@ export class SignInUpService { isWorkEmailFound ? { userEmail: email } : {}, ), workspaceCustomApplicationId, - displayName: options?.displayName ?? '', + displayName, inviteHash: v4(), activationStatus: WorkspaceActivationStatus.PENDING_CREATION, }); diff --git a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/file-core-picture.module.ts b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/file-core-picture.module.ts index a03fd2e58b..af0994e777 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/file-core-picture.module.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/file-core-picture.module.ts @@ -8,13 +8,18 @@ import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-p import { FileUrlModule } from 'src/engine/core-modules/file/file-url/file-url.module'; import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module'; +import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository'; @Module({ imports: [ JwtModule, - TypeOrmModule.forFeature([FileEntity, WorkspaceEntity]), + TypeOrmModule.forFeature([ + FileEntity, + WorkspaceEntity, + UserWorkspaceEntity, + ]), PermissionsModule, FileStorageModule, FileUrlModule, diff --git a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/resolvers/file-core-picture.resolver.ts b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/resolvers/file-core-picture.resolver.ts index 66733c574c..8551ebbf5e 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/resolvers/file-core-picture.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/resolvers/file-core-picture.resolver.ts @@ -1,12 +1,14 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation } from '@nestjs/graphql'; +import bytes from 'bytes'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { PermissionFlagType } from 'twenty-shared/constants'; import type { FileUpload } from 'graphql-upload/processRequest.mjs'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; +import { settings } from 'src/engine/constants/settings'; import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto'; import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; @@ -41,7 +43,10 @@ export class FileCorePictureResolver { @Args({ name: 'file', type: () => GraphQLUpload }) { createReadStream, filename }: FileUpload, ): Promise { - const buffer = await streamToBuffer(createReadStream()); + const buffer = await streamToBuffer( + createReadStream(), + bytes(settings.storage.maxFileSize) ?? undefined, + ); return await this.fileCorePictureService.uploadWorkspacePicture({ file: buffer, @@ -57,7 +62,10 @@ export class FileCorePictureResolver { @Args({ name: 'file', type: () => GraphQLUpload }) { createReadStream, filename }: FileUpload, ): Promise { - const buffer = await streamToBuffer(createReadStream()); + const buffer = await streamToBuffer( + createReadStream(), + bytes(settings.storage.maxFileSize) ?? undefined, + ); return await this.fileCorePictureService.uploadWorkspaceMemberProfilePicture( { diff --git a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts index b152397246..575e901c65 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts @@ -8,6 +8,7 @@ import { FileTypeParser } from 'file-type'; import { detectPdf } from '@file-type/pdf'; import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { Like, type QueryRunner, Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -15,6 +16,10 @@ import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; @@ -22,6 +27,7 @@ import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.s import { extractFileInfoOrThrow } from 'src/engine/core-modules/file/utils/extract-file-info-or-throw.utils'; import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils'; import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; +import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; @@ -35,6 +41,8 @@ export class FileCorePictureService { private readonly fileStorageService: FileStorageService, @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, + @InjectRepository(UserWorkspaceEntity) + private readonly userWorkspaceRepository: Repository, @InjectWorkspaceScopedRepository(FileEntity) private readonly fileRepository: WorkspaceScopedRepository, private readonly fileUrlService: FileUrlService, @@ -137,6 +145,35 @@ export class FileCorePictureService { }; } + async getPendingWorkspaceForLogoUploadOrThrow({ + userId, + workspaceId, + }: { + userId: string; + workspaceId: string; + }): Promise { + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + }); + + const userWorkspace = await this.userWorkspaceRepository.findOne({ + where: { userId, workspaceId }, + }); + + if ( + !isDefined(workspace) || + !isDefined(userWorkspace) || + workspace.activationStatus !== WorkspaceActivationStatus.PENDING_CREATION + ) { + throw new AuthException( + 'Cannot set a logo for this workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + return workspace; + } + async uploadWorkspaceMemberProfilePicture({ file, filename, diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts index d5f415869b..0b270aaa8a 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts @@ -39,7 +39,9 @@ export class OnboardingService { private isWorkspaceActivationPending(workspace: WorkspaceEntity) { return ( - workspace.activationStatus === WorkspaceActivationStatus.PENDING_CREATION + workspace.activationStatus === + WorkspaceActivationStatus.PENDING_CREATION || + workspace.activationStatus === WorkspaceActivationStatus.ONGOING_CREATION ); } diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts index 2f282059a9..697b66ea9b 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-migration.service.ts @@ -166,6 +166,14 @@ export class UpgradeMigrationService { ? queryRunner.manager.getRepository(UpgradeMigrationEntity) : this.upgradeMigrationRepository; + const existingInitialMigration = await repository.findOne({ + where: { name, attempt: 1, workspaceId, isInitial: true }, + }); + + if (isDefined(existingInitialMigration)) { + return; + } + await repository.save({ name, status, diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index d36b2be9f3..019348bda0 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -275,7 +275,10 @@ describe('UserWorkspaceService', () => { ]; const workspaceMemberRepository = { insert: jest.fn(), - find: jest.fn().mockResolvedValue(workspaceMember), + find: jest + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValue(workspaceMember), }; jest @@ -304,6 +307,31 @@ describe('UserWorkspaceService', () => { avatarUrl: 'userWorkspace-avatar-url', }); }); + + it('should not create a workspace member when one already exists', async () => { + const workspaceId = 'workspace-id'; + const user = { + id: 'user-id', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + locale: 'en', + } as unknown as AuthContextUser; + const workspaceMemberRepository = { + insert: jest.fn(), + find: jest + .fn() + .mockResolvedValue([{ id: 'existing-member-id', userId: 'user-id' }]), + }; + + jest + .spyOn(globalWorkspaceOrmManager, 'getRepository') + .mockResolvedValue(workspaceMemberRepository as any); + + await service.createWorkspaceMember(workspaceId, user); + + expect(workspaceMemberRepository.insert).not.toHaveBeenCalled(); + }); }); describe('addUserToWorkspaceIfUserNotInWorkspace', () => { diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 85b03b82e0..8a39fbe4e7 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -149,6 +149,14 @@ export class UserWorkspaceService extends TypeOrmQueryService 0) { + return; + } + const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({ where: { userId: user.id, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/dtos/activate-workspace-input.ts b/packages/twenty-server/src/engine/core-modules/workspace/dtos/activate-workspace-input.ts index 563203c9f2..63816f393b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/dtos/activate-workspace-input.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/dtos/activate-workspace-input.ts @@ -4,7 +4,13 @@ import { IsOptional, IsString } from 'class-validator'; @InputType() export class ActivateWorkspaceInput { - @Field({ nullable: true }) + // The workspace name is set at creation (signUpInNewWorkspace). This field is + // ignored during activation and kept only for backward compatibility. + @Field({ + nullable: true, + description: + 'Deprecated: the workspace name is set at creation (signUpInNewWorkspace) and this field is ignored during activation. Kept for backward compatibility.', + }) @IsString() @IsOptional() displayName?: string; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 6001108bf5..9b930d8173 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import assert from 'assert'; @@ -8,7 +8,7 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; -import { DataSource, QueryRunner, Repository } from 'typeorm'; +import { DataSource, LessThan, QueryRunner, Repository } from 'typeorm'; import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; @@ -42,7 +42,6 @@ import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/se import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserEntity } from 'src/engine/core-modules/user/user.entity'; -import { type ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceException, @@ -76,6 +75,12 @@ import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace- import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +// A workspace stuck in ONGOING_CREATION for longer than this is treated as a +// crashed activation (the process died before the catch block could reset it to +// PENDING_CREATION) and may be retried. It is far longer than a real activation +// takes, so a genuinely in-progress activation is never reclaimed. +const WORKSPACE_ACTIVATION_STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1000; + @Injectable() // oxlint-disable-next-line twenty/inject-workspace-repository export class WorkspaceService extends TypeOrmQueryService { @@ -324,57 +329,90 @@ export class WorkspaceService extends TypeOrmQueryService { return updatedWorkspace; } - async activateWorkspace( - user: AuthContextUser, - workspace: WorkspaceEntity, - data: ActivateWorkspaceInput, - ) { - if (!data.displayName || !data.displayName.length) { - throw new BadRequestException("'displayName' not provided"); + async activateWorkspace(user: AuthContextUser, workspace: WorkspaceEntity) { + // Acquire the activation lock by atomically moving the workspace to + // ONGOING_CREATION. First try the normal case (PENDING_CREATION). If nothing + // matches, the workspace may be stuck in ONGOING_CREATION from a prior + // attempt that was killed before the catch block could reset it — reclaim it, + // but only once the lock is stale, so a genuinely concurrent activation is + // never interrupted. Postgres row locking serializes concurrent reclaims, and + // repository.update bumps updatedAt, so a reclaimed lock is immediately fresh. + let activationLockResult = await this.workspaceRepository.update( + { + id: workspace.id, + activationStatus: WorkspaceActivationStatus.PENDING_CREATION, + }, + { activationStatus: WorkspaceActivationStatus.ONGOING_CREATION }, + ); + + if ((activationLockResult.affected ?? 0) === 0) { + activationLockResult = await this.workspaceRepository.update( + { + id: workspace.id, + activationStatus: WorkspaceActivationStatus.ONGOING_CREATION, + updatedAt: LessThan( + new Date(Date.now() - WORKSPACE_ACTIVATION_STALE_LOCK_TIMEOUT_MS), + ), + }, + { activationStatus: WorkspaceActivationStatus.ONGOING_CREATION }, + ); } - if ( - workspace.activationStatus === WorkspaceActivationStatus.ONGOING_CREATION - ) { + if ((activationLockResult.affected ?? 0) === 0) { + // Activation is idempotent for the terminal state: if a prior attempt + // already completed (e.g. the client lost the response and retried), + // return the active workspace instead of failing. Otherwise another + // activation is genuinely in progress and must not be interrupted. + const existingWorkspace = await this.workspaceRepository.findOneBy({ + id: workspace.id, + }); + + if ( + existingWorkspace?.activationStatus === WorkspaceActivationStatus.ACTIVE + ) { + return existingWorkspace; + } + throw new Error('Workspace is already being created'); } - if ( - workspace.activationStatus !== WorkspaceActivationStatus.PENDING_CREATION - ) { - throw new Error('Workspace is not pending creation'); - } - - await this.workspaceRepository.update(workspace.id, { - activationStatus: WorkspaceActivationStatus.ONGOING_CREATION, - }); - await this.coreEntityCacheService.invalidate( 'workspaceEntity', workspace.id, ); - await this.workspaceManagerService.init({ - workspace, - userId: user.id, - }); + try { + await this.workspaceManagerService.init({ + workspace, + userId: user.id, + }); - await this.featureFlagService.enableFeatureFlags( - DEFAULT_FEATURE_FLAGS, - workspace.id, - ); + await this.featureFlagService.enableFeatureFlags( + DEFAULT_FEATURE_FLAGS, + workspace.id, + ); - await this.userWorkspaceService.createWorkspaceMember(workspace.id, user); + await this.userWorkspaceService.createWorkspaceMember(workspace.id, user); - await this.prefillCreatedWorkspaceRecords({ - workspaceId: workspace.id, - schemaName: getWorkspaceSchemaName(workspace.id), - }); + await this.prefillCreatedWorkspaceRecords({ + workspaceId: workspace.id, + schemaName: getWorkspaceSchemaName(workspace.id), + }); - await this.activateAndInitializeUpgradeState({ - workspaceId: workspace.id, - displayName: data.displayName, - }); + await this.activateAndInitializeUpgradeState({ + workspaceId: workspace.id, + }); + } catch (error) { + await this.workspaceRepository.update(workspace.id, { + activationStatus: WorkspaceActivationStatus.PENDING_CREATION, + }); + await this.coreEntityCacheService.invalidate( + 'workspaceEntity', + workspace.id, + ); + + throw error; + } try { await this.sdkClientGenerationService.enqueueSdkClientGenerationForWorkspace( @@ -399,11 +437,9 @@ export class WorkspaceService extends TypeOrmQueryService { } private async activateAndInitializeUpgradeState({ - displayName, workspaceId, }: { workspaceId: string; - displayName: string; }): Promise { const lastAttemptedInstanceCommand = await this.upgradeMigrationService.getLastAttemptedInstanceCommandOrThrow(); @@ -423,7 +459,6 @@ export class WorkspaceService extends TypeOrmQueryService { try { await queryRunner.manager.update(WorkspaceEntity, workspaceId, { - displayName, activationStatus: WorkspaceActivationStatus.ACTIVE, }); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 45f5d2eab1..121e663f97 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -115,11 +115,14 @@ export class WorkspaceResolver { @Mutation(() => WorkspaceEntity) @UseGuards(UserAuthGuard, WorkspaceAuthGuard, NoPermissionGuard) async activateWorkspace( - @Args('data') data: ActivateWorkspaceInput, + // Deprecated: the workspace name is set at creation. This argument is kept + // for backward compatibility (removing it would be a breaking schema change) + // but is ignored. + @Args('data') _data: ActivateWorkspaceInput, @AuthUser() user: AuthContextUser, @AuthWorkspace() workspace: WorkspaceEntity, ) { - return await this.workspaceService.activateWorkspace(user, workspace, data); + return await this.workspaceService.activateWorkspace(user, workspace); } @Mutation(() => WorkspaceEntity) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 63ffcbf168..8df71cfcdc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -10,6 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants'; import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; @@ -117,11 +118,17 @@ export class WorkspaceManagerService { }); } - const memberRole = await this.roleService.createMemberRole({ - workspaceId, - ownerFlatApplication: workspaceCustomFlatApplication, + const existingMemberRole = await this.roleRepository.findOne(workspaceId, { + where: { label: MEMBER_ROLE_LABEL }, }); + const memberRole = + existingMemberRole ?? + (await this.roleService.createMemberRole({ + workspaceId, + ownerFlatApplication: workspaceCustomFlatApplication, + })); + await this.workspaceRepository.update(workspaceId, { defaultRoleId: memberRole.id, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts index a37f12cf3b..22b7547afd 100644 --- a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts @@ -72,7 +72,6 @@ describe('Successful user and workspace creation', () => { data: { activateWorkspace: activateWorkspaceData }, } = await activateWorkspace({ accessToken: newWorkspaceAccessToken, - displayName: '42 answer', expectToFail: false, }); @@ -131,6 +130,180 @@ describe('Successful user and workspace creation', () => { ); }); + it('should reclaim a stale ONGOING_CREATION workspace and activate it', async () => { + const uniqueEmail = `test-reclaim-${randomUUID()}@example.com`; + + const { data } = await signUp({ + input: { + email: uniqueEmail, + password: 'Test123!@#', + }, + expectToFail: false, + }); + + createdUserAccessToken = + data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + + await testDataSource.query( + 'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1', + [uniqueEmail], + ); + + const { + data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, + } = await signUpInNewWorkspace({ + accessToken: createdUserAccessToken, + expectToFail: false, + }); + + const workspaceId = signUpInNewWorkspaceData.workspace.id; + + const { + data: { getAuthTokensFromLoginToken: authTokensData }, + } = await getAuthTokensFromLoginToken({ + origin: signUpInNewWorkspaceData.workspace.workspaceUrls.subdomainUrl, + loginToken: signUpInNewWorkspaceData.loginToken.token, + expectToFail: false, + }); + + const newWorkspaceAccessToken = + authTokensData.tokens.accessOrWorkspaceAgnosticToken.token; + + // Simulate an activation attempt that was killed mid-flight: the workspace + // is stuck in ONGOING_CREATION with a stale updatedAt. A retry should + // reclaim the stale lock and finish activation. + await testDataSource.query( + `UPDATE core.workspace SET "activationStatus" = 'ONGOING_CREATION', "updatedAt" = now() - interval '10 minutes' WHERE id = $1`, + [workspaceId], + ); + + const { + data: { activateWorkspace: activateWorkspaceData }, + } = await activateWorkspace({ + accessToken: newWorkspaceAccessToken, + expectToFail: false, + }); + + expect(activateWorkspaceData.activationStatus).toBe( + WorkspaceActivationStatus.ACTIVE, + ); + }); + + it('should not reclaim a workspace whose activation is genuinely in progress', async () => { + const uniqueEmail = `test-fresh-ongoing-${randomUUID()}@example.com`; + + const { data } = await signUp({ + input: { + email: uniqueEmail, + password: 'Test123!@#', + }, + expectToFail: false, + }); + + createdUserAccessToken = + data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + + await testDataSource.query( + 'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1', + [uniqueEmail], + ); + + const { + data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, + } = await signUpInNewWorkspace({ + accessToken: createdUserAccessToken, + expectToFail: false, + }); + + const workspaceId = signUpInNewWorkspaceData.workspace.id; + + const { + data: { getAuthTokensFromLoginToken: authTokensData }, + } = await getAuthTokensFromLoginToken({ + origin: signUpInNewWorkspaceData.workspace.workspaceUrls.subdomainUrl, + loginToken: signUpInNewWorkspaceData.loginToken.token, + expectToFail: false, + }); + + const newWorkspaceAccessToken = + authTokensData.tokens.accessOrWorkspaceAgnosticToken.token; + + // A fresh ONGOING_CREATION lock means another activation is genuinely in + // progress, so a concurrent attempt must be rejected rather than reclaim it. + await testDataSource.query( + `UPDATE core.workspace SET "activationStatus" = 'ONGOING_CREATION', "updatedAt" = now() WHERE id = $1`, + [workspaceId], + ); + + const { errors } = await activateWorkspace({ + accessToken: newWorkspaceAccessToken, + expectToFail: true, + }); + + expect(errors).toBeDefined(); + + // This workspace was never actually activated (no tenant schema/metadata), + // so the standard deleteUser teardown cannot operate on it. Skip the + // afterEach cleanup; the integration test database is reset per run. + createdUserAccessToken = undefined; + }); + + it('should be idempotent when re-activating an already-active workspace', async () => { + const uniqueEmail = `test-idempotent-${randomUUID()}@example.com`; + + const { data } = await signUp({ + input: { + email: uniqueEmail, + password: 'Test123!@#', + }, + expectToFail: false, + }); + + createdUserAccessToken = + data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + + await testDataSource.query( + 'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1', + [uniqueEmail], + ); + + const { + data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, + } = await signUpInNewWorkspace({ + accessToken: createdUserAccessToken, + expectToFail: false, + }); + + const { + data: { getAuthTokensFromLoginToken: authTokensData }, + } = await getAuthTokensFromLoginToken({ + origin: signUpInNewWorkspaceData.workspace.workspaceUrls.subdomainUrl, + loginToken: signUpInNewWorkspaceData.loginToken.token, + expectToFail: false, + }); + + const newWorkspaceAccessToken = + authTokensData.tokens.accessOrWorkspaceAgnosticToken.token; + + await activateWorkspace({ + accessToken: newWorkspaceAccessToken, + expectToFail: false, + }); + + // A second activation (e.g. the client lost the first response and retried) + // must succeed idempotently instead of throwing "already being created". + const { + data: { activateWorkspace: secondActivation }, + } = await activateWorkspace({ + accessToken: newWorkspaceAccessToken, + expectToFail: false, + }); + + expect(secondActivation.activationStatus).toBe( + WorkspaceActivationStatus.ACTIVE, + ); + }); + it('should suspend and soft-delete workspace when last user is deleted', async () => { const uniqueEmail = `test-delete-${randomUUID()}@example.com`; @@ -173,7 +346,6 @@ describe('Successful user and workspace creation', () => { await activateWorkspace({ accessToken: newWorkspaceAccessToken, - displayName: 'Test Workspace for Deletion', expectToFail: false, }); diff --git a/packages/twenty-server/test/integration/graphql/utils/activate-workspace.util.ts b/packages/twenty-server/test/integration/graphql/utils/activate-workspace.util.ts index 4f0e2ce7a2..bf0a697f5f 100644 --- a/packages/twenty-server/test/integration/graphql/utils/activate-workspace.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/activate-workspace.util.ts @@ -8,13 +8,11 @@ import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspac type ActivateWorkspaceUtilArgs = { accessToken: string; - displayName: string; expectToFail?: boolean; }; export const activateWorkspace = async ({ accessToken, - displayName, expectToFail, }: ActivateWorkspaceUtilArgs): CommonResponseBody<{ activateWorkspace: WorkspaceEntity; @@ -36,9 +34,7 @@ export const activateWorkspace = async ({ { query: mutation, variables: { - input: { - displayName, - }, + input: {}, }, }, accessToken, diff --git a/packages/twenty-server/test/integration/graphql/utils/sign-up-in-new-workspace.util.ts b/packages/twenty-server/test/integration/graphql/utils/sign-up-in-new-workspace.util.ts index 26d4072b3a..3806157c35 100644 --- a/packages/twenty-server/test/integration/graphql/utils/sign-up-in-new-workspace.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/sign-up-in-new-workspace.util.ts @@ -8,18 +8,20 @@ import { type SignUpDTO } from 'src/engine/core-modules/auth/dto/sign-up.dto'; type SignUpOnNewWorkspaceUtilArgs = { accessToken: string; + displayName?: string; expectToFail?: boolean; }; export const signUpInNewWorkspace = async ({ accessToken, + displayName = 'Test Workspace', expectToFail, }: SignUpOnNewWorkspaceUtilArgs): CommonResponseBody<{ signUpInNewWorkspace: SignUpDTO; }> => { const mutation = gql` - mutation SignUpInNewWorkspace { - signUpInNewWorkspace { + mutation SignUpInNewWorkspace($input: SignUpInNewWorkspaceInput) { + signUpInNewWorkspace(input: $input) { loginToken { token expiresAt @@ -38,7 +40,7 @@ export const signUpInNewWorkspace = async ({ const response = await makeMetadataAPIRequest( { query: mutation, - variables: {}, + variables: { input: { displayName } }, }, accessToken, ); diff --git a/packages/twenty-shared/src/types/AppPath.ts b/packages/twenty-shared/src/types/AppPath.ts index 83366d2e12..8d9a12a273 100644 --- a/packages/twenty-shared/src/types/AppPath.ts +++ b/packages/twenty-shared/src/types/AppPath.ts @@ -7,7 +7,7 @@ export enum AppPath { ResetPassword = '/reset-password/:passwordResetToken', // Onboarding - CreateWorkspace = '/create/workspace', + WorkspaceActivation = '/workspace-activation', CreateProfile = '/create/profile', SyncEmails = '/sync/emails', InviteTeam = '/invite-team',