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

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

View File

@ -143,6 +143,14 @@ export type AvailableWorkspaceOutput = {
workspaceUrls: WorkspaceUrls;
};
export type AvailableWorkspacesToJoin = {
__typename?: 'AvailableWorkspacesToJoin';
displayName?: Maybe<Scalars['String']['output']>;
id: Scalars['String']['output'];
logo?: Maybe<Scalars['String']['output']>;
workspaceUrl: Scalars['String']['output'];
};
export type Billing = {
__typename?: 'Billing';
billingUrl?: Maybe<Scalars['String']['output']>;
@ -298,6 +306,13 @@ export enum CaptchaDriverType {
TURNSTILE = 'TURNSTILE'
}
export type CheckUserExistOutput = {
__typename?: 'CheckUserExistOutput';
availableWorkspaces: Array<AvailableWorkspaceOutput>;
exists: Scalars['Boolean']['output'];
isEmailVerified: Scalars['Boolean']['output'];
};
export type ClientConfig = {
__typename?: 'ClientConfig';
analyticsEnabled: Scalars['Boolean']['output'];
@ -942,6 +957,7 @@ export type Mutation = {
createOneRole: Role;
createOneServerlessFunction: ServerlessFunction;
createSAMLIdentityProvider: SetupSsoOutput;
createUserAndWorkspace: SignUpOutput;
createWorkflowVersionStep: WorkflowAction;
deactivateWorkflowVersion: Scalars['Boolean']['output'];
deleteApprovedAccessDomain: Scalars['Boolean']['output'];
@ -1103,6 +1119,16 @@ export type MutationCreateSamlIdentityProviderArgs = {
};
export type MutationCreateUserAndWorkspaceArgs = {
captchaToken?: InputMaybe<Scalars['String']['input']>;
email: Scalars['String']['input'];
firstName?: InputMaybe<Scalars['String']['input']>;
lastName?: InputMaybe<Scalars['String']['input']>;
locale?: InputMaybe<Scalars['String']['input']>;
picture?: InputMaybe<Scalars['String']['input']>;
};
export type MutationCreateWorkflowVersionStepArgs = {
input: CreateWorkflowVersionStepInput;
};
@ -1607,7 +1633,7 @@ export type PublishServerlessFunctionInput = {
export type Query = {
__typename?: 'Query';
billingPortalSession: BillingSessionOutput;
checkUserExists: UserExistsOutput;
checkUserExists: CheckUserExistOutput;
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
clientConfig: ClientConfig;
currentUser: User;
@ -1640,6 +1666,7 @@ export type Query = {
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
index: Index;
indexMetadatas: IndexConnection;
listAvailableWorkspaces: Array<AvailableWorkspaceOutput>;
object: Object;
objects: ObjectConnection;
plans: Array<BillingPlanOutput>;
@ -1771,6 +1798,12 @@ export type QueryIndexMetadatasArgs = {
};
export type QueryListAvailableWorkspacesArgs = {
captchaToken?: InputMaybe<Scalars['String']['input']>;
email: Scalars['String']['input'];
};
export type QueryObjectArgs = {
id: Scalars['UUID']['input'];
};
@ -2353,6 +2386,7 @@ export type UpsertSettingPermissionsInput = {
export type User = {
__typename?: 'User';
availableWorkspaces: Array<AvailableWorkspacesToJoin>;
canAccessFullAdminPanel: Scalars['Boolean']['output'];
canImpersonate: Scalars['Boolean']['output'];
createdAt: Scalars['DateTime']['output'];
@ -2386,15 +2420,6 @@ export type UserEdge = {
node: User;
};
export type UserExists = {
__typename?: 'UserExists';
availableWorkspaces: Array<AvailableWorkspaceOutput>;
exists: Scalars['Boolean']['output'];
isEmailVerified: Scalars['Boolean']['output'];
};
export type UserExistsOutput = UserExists | UserNotExists;
export type UserInfo = {
__typename?: 'UserInfo';
email: Scalars['String']['output'];
@ -2424,11 +2449,6 @@ export type UserMappingOptionsUser = {
user?: Maybe<Scalars['String']['output']>;
};
export type UserNotExists = {
__typename?: 'UserNotExists';
exists: Scalars['Boolean']['output'];
};
export type UserWorkspace = {
__typename?: 'UserWorkspace';
createdAt: Scalars['DateTime']['output'];

View File

@ -126,15 +126,30 @@ export type AuthorizeApp = {
redirectUrl: Scalars['String'];
};
export type AvailableWorkspaceOutput = {
__typename?: 'AvailableWorkspaceOutput';
export type AvailableWorkspace = {
__typename?: 'AvailableWorkspace';
displayName?: Maybe<Scalars['String']>;
id: Scalars['String'];
inviteHash?: Maybe<Scalars['String']>;
loginToken?: Maybe<Scalars['String']>;
logo?: Maybe<Scalars['String']>;
personalInviteToken?: Maybe<Scalars['String']>;
sso: Array<SsoConnection>;
workspaceUrls: WorkspaceUrls;
};
export type AvailableWorkspaces = {
__typename?: 'AvailableWorkspaces';
availableWorkspacesForSignIn: Array<AvailableWorkspace>;
availableWorkspacesForSignUp: Array<AvailableWorkspace>;
};
export type AvailableWorkspacesAndAccessTokensOutput = {
__typename?: 'AvailableWorkspacesAndAccessTokensOutput';
availableWorkspaces: AvailableWorkspaces;
tokens: AuthTokenPair;
};
export type Billing = {
__typename?: 'Billing';
billingUrl?: Maybe<Scalars['String']>;
@ -290,6 +305,13 @@ export enum CaptchaDriverType {
TURNSTILE = 'TURNSTILE'
}
export type CheckUserExistOutput = {
__typename?: 'CheckUserExistOutput';
availableWorkspacesCount: Scalars['Float'];
exists: Scalars['Boolean'];
isEmailVerified: Scalars['Boolean'];
};
export type ClientConfig = {
__typename?: 'ClientConfig';
analyticsEnabled: Scalars['Boolean'];
@ -922,8 +944,10 @@ export type Mutation = {
resendWorkspaceInvitation: SendInvitationsOutput;
runWorkflowVersion: WorkflowRun;
sendInvitations: SendInvitationsOutput;
signUp: SignUpOutput;
signIn: AvailableWorkspacesAndAccessTokensOutput;
signUp: AvailableWorkspacesAndAccessTokensOutput;
signUpInNewWorkspace: SignUpOutput;
signUpInWorkspace: SignUpOutput;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
submitFormStep: Scalars['Boolean'];
switchToEnterprisePlan: BillingUpdateOutput;
@ -1172,7 +1196,21 @@ export type MutationSendInvitationsArgs = {
};
export type MutationSignInArgs = {
captchaToken?: InputMaybe<Scalars['String']>;
email: Scalars['String'];
password: Scalars['String'];
};
export type MutationSignUpArgs = {
captchaToken?: InputMaybe<Scalars['String']>;
email: Scalars['String'];
password: Scalars['String'];
};
export type MutationSignUpInWorkspaceArgs = {
captchaToken?: InputMaybe<Scalars['String']>;
email: Scalars['String'];
locale?: InputMaybe<Scalars['String']>;
@ -1510,7 +1548,7 @@ export type PublishServerlessFunctionInput = {
export type Query = {
__typename?: 'Query';
billingPortalSession: BillingSessionOutput;
checkUserExists: UserExistsOutput;
checkUserExists: CheckUserExistOutput;
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
clientConfig: ClientConfig;
currentUser: User;
@ -2183,6 +2221,7 @@ export type UpsertSettingPermissionsInput = {
export type User = {
__typename?: 'User';
availableWorkspaces: AvailableWorkspaces;
canAccessFullAdminPanel: Scalars['Boolean'];
canImpersonate: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
@ -2202,7 +2241,7 @@ export type User = {
passwordHash?: Maybe<Scalars['String']>;
supportUserHash?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
userVars: Scalars['JSONObject'];
userVars?: Maybe<Scalars['JSONObject']>;
workspaceMember?: Maybe<WorkspaceMember>;
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
workspaces: Array<UserWorkspace>;
@ -2216,15 +2255,6 @@ export type UserEdge = {
node: User;
};
export type UserExists = {
__typename?: 'UserExists';
availableWorkspaces: Array<AvailableWorkspaceOutput>;
exists: Scalars['Boolean'];
isEmailVerified: Scalars['Boolean'];
};
export type UserExistsOutput = UserExists | UserNotExists;
export type UserInfo = {
__typename?: 'UserInfo';
email: Scalars['String'];
@ -2244,11 +2274,6 @@ export type UserMappingOptionsUser = {
user?: Maybe<Scalars['String']>;
};
export type UserNotExists = {
__typename?: 'UserNotExists';
exists: Scalars['Boolean'];
};
export type UserWorkspace = {
__typename?: 'UserWorkspace';
createdAt: Scalars['DateTime'];
@ -2510,6 +2535,10 @@ export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: strin
export type AuthTokensFragmentFragment = { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } };
export type AvailableWorkspaceFragmentFragment = { __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> };
export type AvailableWorkspacesFragmentFragment = { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> };
export type AvailableSsoIdentityProvidersFragmentFragment = { __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } };
export type AuthorizeAppMutationVariables = Exact<{
@ -2600,7 +2629,30 @@ export type ResendEmailVerificationTokenMutationVariables = Exact<{
export type ResendEmailVerificationTokenMutation = { __typename?: 'Mutation', resendEmailVerificationToken: { __typename?: 'ResendEmailVerificationTokenOutput', success: boolean } };
export type SignInMutationVariables = Exact<{
email: Scalars['String'];
password: Scalars['String'];
captchaToken?: InputMaybe<Scalars['String']>;
}>;
export type SignInMutation = { __typename?: 'Mutation', signIn: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type SignUpMutationVariables = Exact<{
email: Scalars['String'];
password: Scalars['String'];
captchaToken?: InputMaybe<Scalars['String']>;
}>;
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type SignUpInNewWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
export type SignUpInNewWorkspaceMutation = { __typename?: 'Mutation', signUpInNewWorkspace: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
export type SignUpInWorkspaceMutationVariables = Exact<{
email: Scalars['String'];
password: Scalars['String'];
workspaceInviteHash?: InputMaybe<Scalars['String']>;
@ -2612,12 +2664,7 @@ export type SignUpMutationVariables = Exact<{
}>;
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
export type SignUpInNewWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
export type SignUpInNewWorkspaceMutation = { __typename?: 'Mutation', signUpInNewWorkspace: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
export type SignUpInWorkspaceMutation = { __typename?: 'Mutation', signUpInWorkspace: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } } };
export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
token: Scalars['String'];
@ -2633,7 +2680,7 @@ export type CheckUserExistsQueryVariables = Exact<{
}>;
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, isEmailVerified: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } };
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename?: 'CheckUserExistOutput', exists: boolean, availableWorkspacesCount: number, isEmailVerified: boolean } };
export type GetPublicWorkspaceDataByDomainQueryVariables = Exact<{
origin: Scalars['String'];
@ -2909,7 +2956,9 @@ export type OnDbEventSubscriptionVariables = Exact<{
export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array<string> | null, record: any } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -2926,7 +2975,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
@ -3208,6 +3257,12 @@ export const ObjectPermissionFragmentFragmentDoc = gql`
canDestroyObjectRecords
}
`;
export const WorkspaceUrlsFragmentFragmentDoc = gql`
fragment WorkspaceUrlsFragment on WorkspaceUrls {
subdomainUrl
customUrl
}
`;
export const RoleFragmentFragmentDoc = gql`
fragment RoleFragment on Role {
id
@ -3222,6 +3277,37 @@ export const RoleFragmentFragmentDoc = gql`
canDestroyAllObjectRecords
}
`;
export const AvailableWorkspaceFragmentFragmentDoc = gql`
fragment AvailableWorkspaceFragment on AvailableWorkspace {
id
displayName
loginToken
inviteHash
personalInviteToken
workspaceUrls {
subdomainUrl
customUrl
}
logo
sso {
type
id
issuer
name
status
}
}
`;
export const AvailableWorkspacesFragmentFragmentDoc = gql`
fragment AvailableWorkspacesFragment on AvailableWorkspaces {
availableWorkspacesForSignIn {
...AvailableWorkspaceFragment
}
availableWorkspacesForSignUp {
...AvailableWorkspaceFragment
}
}
${AvailableWorkspaceFragmentFragmentDoc}`;
export const UserQueryFragmentFragmentDoc = gql`
fragment UserQueryFragment on User {
id
@ -3264,8 +3350,7 @@ export const UserQueryFragmentFragmentDoc = gql`
customDomain
isCustomDomainEnabled
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
featureFlags {
key
@ -3302,25 +3387,17 @@ export const UserQueryFragmentFragmentDoc = gql`
...RoleFragment
}
}
workspaces {
workspace {
id
logo
displayName
subdomain
customDomain
workspaceUrls {
subdomainUrl
customUrl
}
}
availableWorkspaces {
...AvailableWorkspacesFragment
}
userVars
}
${WorkspaceMemberQueryFragmentFragmentDoc}
${DeletedWorkspaceMemberQueryFragmentFragmentDoc}
${ObjectPermissionFragmentFragmentDoc}
${RoleFragmentFragmentDoc}`;
${WorkspaceUrlsFragmentFragmentDoc}
${RoleFragmentFragmentDoc}
${AvailableWorkspacesFragmentFragmentDoc}`;
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) {
getTimelineCalendarEventsFromCompanyId(
@ -3858,12 +3935,12 @@ export const GetLoginTokenFromEmailVerificationTokenDocument = gql`
...AuthTokenFragment
}
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
}
}
${AuthTokenFragmentFragmentDoc}`;
${AuthTokenFragmentFragmentDoc}
${WorkspaceUrlsFragmentFragmentDoc}`;
export type GetLoginTokenFromEmailVerificationTokenMutationFn = Apollo.MutationFunction<GetLoginTokenFromEmailVerificationTokenMutation, GetLoginTokenFromEmailVerificationTokenMutationVariables>;
/**
@ -3898,8 +3975,7 @@ export const ImpersonateDocument = gql`
impersonate(userId: $userId, workspaceId: $workspaceId) {
workspace {
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
id
}
@ -3908,7 +3984,8 @@ export const ImpersonateDocument = gql`
}
}
}
${AuthTokenFragmentFragmentDoc}`;
${WorkspaceUrlsFragmentFragmentDoc}
${AuthTokenFragmentFragmentDoc}`;
export type ImpersonateMutationFn = Apollo.MutationFunction<ImpersonateMutation, ImpersonateMutationVariables>;
/**
@ -4005,31 +4082,60 @@ export function useResendEmailVerificationTokenMutation(baseOptions?: Apollo.Mut
export type ResendEmailVerificationTokenMutationHookResult = ReturnType<typeof useResendEmailVerificationTokenMutation>;
export type ResendEmailVerificationTokenMutationResult = Apollo.MutationResult<ResendEmailVerificationTokenMutation>;
export type ResendEmailVerificationTokenMutationOptions = Apollo.BaseMutationOptions<ResendEmailVerificationTokenMutation, ResendEmailVerificationTokenMutationVariables>;
export const SignUpDocument = gql`
mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String, $verifyEmailNextPath: String) {
signUp(
email: $email
password: $password
workspaceInviteHash: $workspaceInviteHash
workspacePersonalInviteToken: $workspacePersonalInviteToken
captchaToken: $captchaToken
workspaceId: $workspaceId
locale: $locale
verifyEmailNextPath: $verifyEmailNextPath
) {
loginToken {
...AuthTokenFragment
export const SignInDocument = gql`
mutation SignIn($email: String!, $password: String!, $captchaToken: String) {
signIn(email: $email, password: $password, captchaToken: $captchaToken) {
availableWorkspaces {
...AvailableWorkspacesFragment
}
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
}
tokens {
...AuthTokensFragment
}
}
}
${AuthTokenFragmentFragmentDoc}`;
${AvailableWorkspacesFragmentFragmentDoc}
${AuthTokensFragmentFragmentDoc}`;
export type SignInMutationFn = Apollo.MutationFunction<SignInMutation, SignInMutationVariables>;
/**
* __useSignInMutation__
*
* To run a mutation, you first call `useSignInMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSignInMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [signInMutation, { data, loading, error }] = useSignInMutation({
* variables: {
* email: // value for 'email'
* password: // value for 'password'
* captchaToken: // value for 'captchaToken'
* },
* });
*/
export function useSignInMutation(baseOptions?: Apollo.MutationHookOptions<SignInMutation, SignInMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<SignInMutation, SignInMutationVariables>(SignInDocument, options);
}
export type SignInMutationHookResult = ReturnType<typeof useSignInMutation>;
export type SignInMutationResult = Apollo.MutationResult<SignInMutation>;
export type SignInMutationOptions = Apollo.BaseMutationOptions<SignInMutation, SignInMutationVariables>;
export const SignUpDocument = gql`
mutation SignUp($email: String!, $password: String!, $captchaToken: String) {
signUp(email: $email, password: $password, captchaToken: $captchaToken) {
availableWorkspaces {
...AvailableWorkspacesFragment
}
tokens {
...AuthTokensFragment
}
}
}
${AvailableWorkspacesFragmentFragmentDoc}
${AuthTokensFragmentFragmentDoc}`;
export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMutationVariables>;
/**
@ -4047,12 +4153,7 @@ export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMut
* variables: {
* email: // value for 'email'
* password: // value for 'password'
* workspaceInviteHash: // value for 'workspaceInviteHash'
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
* captchaToken: // value for 'captchaToken'
* workspaceId: // value for 'workspaceId'
* locale: // value for 'locale'
* verifyEmailNextPath: // value for 'verifyEmailNextPath'
* },
* });
*/
@ -4072,13 +4173,13 @@ export const SignUpInNewWorkspaceDocument = gql`
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
}
}
}
${AuthTokenFragmentFragmentDoc}`;
${AuthTokenFragmentFragmentDoc}
${WorkspaceUrlsFragmentFragmentDoc}`;
export type SignUpInNewWorkspaceMutationFn = Apollo.MutationFunction<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>;
/**
@ -4104,6 +4205,64 @@ export function useSignUpInNewWorkspaceMutation(baseOptions?: Apollo.MutationHoo
export type SignUpInNewWorkspaceMutationHookResult = ReturnType<typeof useSignUpInNewWorkspaceMutation>;
export type SignUpInNewWorkspaceMutationResult = Apollo.MutationResult<SignUpInNewWorkspaceMutation>;
export type SignUpInNewWorkspaceMutationOptions = Apollo.BaseMutationOptions<SignUpInNewWorkspaceMutation, SignUpInNewWorkspaceMutationVariables>;
export const SignUpInWorkspaceDocument = gql`
mutation SignUpInWorkspace($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String, $verifyEmailNextPath: String) {
signUpInWorkspace(
email: $email
password: $password
workspaceInviteHash: $workspaceInviteHash
workspacePersonalInviteToken: $workspacePersonalInviteToken
captchaToken: $captchaToken
workspaceId: $workspaceId
locale: $locale
verifyEmailNextPath: $verifyEmailNextPath
) {
loginToken {
...AuthTokenFragment
}
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
}
}
}
}
${AuthTokenFragmentFragmentDoc}`;
export type SignUpInWorkspaceMutationFn = Apollo.MutationFunction<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>;
/**
* __useSignUpInWorkspaceMutation__
*
* To run a mutation, you first call `useSignUpInWorkspaceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSignUpInWorkspaceMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [signUpInWorkspaceMutation, { data, loading, error }] = useSignUpInWorkspaceMutation({
* variables: {
* email: // value for 'email'
* password: // value for 'password'
* workspaceInviteHash: // value for 'workspaceInviteHash'
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
* captchaToken: // value for 'captchaToken'
* workspaceId: // value for 'workspaceId'
* locale: // value for 'locale'
* verifyEmailNextPath: // value for 'verifyEmailNextPath'
* },
* });
*/
export function useSignUpInWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>(SignUpInWorkspaceDocument, options);
}
export type SignUpInWorkspaceMutationHookResult = ReturnType<typeof useSignUpInWorkspaceMutation>;
export type SignUpInWorkspaceMutationResult = Apollo.MutationResult<SignUpInWorkspaceMutation>;
export type SignUpInWorkspaceMutationOptions = Apollo.BaseMutationOptions<SignUpInWorkspaceMutation, SignUpInWorkspaceMutationVariables>;
export const UpdatePasswordViaResetTokenDocument = gql`
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
updatePasswordViaResetToken(
@ -4144,30 +4303,9 @@ export type UpdatePasswordViaResetTokenMutationOptions = Apollo.BaseMutationOpti
export const CheckUserExistsDocument = gql`
query CheckUserExists($email: String!, $captchaToken: String) {
checkUserExists(email: $email, captchaToken: $captchaToken) {
__typename
... on UserExists {
exists
availableWorkspaces {
id
displayName
workspaceUrls {
subdomainUrl
customUrl
}
logo
sso {
type
id
issuer
name
status
}
}
isEmailVerified
}
... on UserNotExists {
exists
}
exists
availableWorkspacesCount
isEmailVerified
}
}
`;
@ -4207,8 +4345,7 @@ export const GetPublicWorkspaceDataByDomainDocument = gql`
logo
displayName
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
authProviders {
sso {
@ -4225,7 +4362,7 @@ export const GetPublicWorkspaceDataByDomainDocument = gql`
}
}
}
`;
${WorkspaceUrlsFragmentFragmentDoc}`;
/**
* __useGetPublicWorkspaceDataByDomainQuery__

View File

@ -12,6 +12,7 @@ import { OnboardingStatus } from '~/generated/graphql';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
jest.mock('@/onboarding/hooks/useOnboardingStatus');
const setupMockOnboardingStatus = (
@ -50,6 +51,11 @@ jest.mocked(useDefaultHomePagePath).mockReturnValue({
defaultHomePagePath,
});
jest.mock('@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace');
jest.mocked(useIsCurrentLocationOnAWorkspace).mockReturnValue({
isOnAWorkspace: true,
});
jest.mock('react-router-dom');
const setupMockUseParams = (objectNamePlural?: string) => {
jest

View File

@ -12,9 +12,11 @@ import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { OnboardingStatus } from '~/generated/graphql';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
export const usePageChangeEffectNavigateLocation = () => {
const isLoggedIn = useIsLogged();
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const onboardingStatus = useOnboardingStatus();
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
WorkspaceActivationStatus.SUSPENDED,
@ -41,13 +43,13 @@ export const usePageChangeEffectNavigateLocation = () => {
const objectNamePlural = useParams().objectNamePlural ?? '';
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
const objectMetadataItem = objectMetadataItems?.find(
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
);
const verifyEmailNextPath = useRecoilValue(verifyEmailNextPathState);
if (
!isLoggedIn &&
(!isLoggedIn || (isLoggedIn && !isOnAWorkspace)) &&
!someMatchingLocationOf([
...onGoingUserCreationPaths,
AppPath.ResetPassword,

View File

@ -8,7 +8,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { previousUrlState } from '@/auth/states/previousUrlState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { workspacesState } from '@/auth/states/workspaces';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
@ -33,8 +32,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setWorkspaces = useSetRecoilState(workspacesState);
const [, setPreviousUrl] = useRecoilState(previousUrlState);
const setPreviousUrl = useSetRecoilState(previousUrlState);
const location = useLocation();
const apolloClient = useMemo(() => {
@ -65,7 +63,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
setCurrentWorkspaceMember(null);
setCurrentWorkspace(null);
setCurrentUserWorkspace(null);
setWorkspaces([]);
if (
!isMatchingLocation(location, AppPath.Verify) &&
!isMatchingLocation(location, AppPath.SignInUp) &&
@ -89,7 +86,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
setCurrentUser,
setCurrentWorkspaceMember,
setCurrentWorkspace,
setWorkspaces,
setPreviousUrl,
]);

View File

@ -11,6 +11,7 @@ type LogoProps = {
primaryLogo?: string | null;
secondaryLogo?: string | null;
placeholder?: string | null;
onClick?: () => void;
};
const StyledContainer = styled.div`
@ -53,6 +54,7 @@ export const Logo = ({
primaryLogo,
secondaryLogo,
placeholder,
onClick,
}: LogoProps) => {
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
const defaultPrimaryLogoUrl = `${window.location.origin}/images/icons/android/android-launchericon-192-192.png`;
@ -72,7 +74,7 @@ export const Logo = ({
const isUsingDefaultLogo = !isDefined(primaryLogo);
return (
<StyledContainer>
<StyledContainer onClick={() => onClick?.()}>
{isUsingDefaultLogo ? (
<UndecoratedLink
to={AppPath.SignInUp}

View File

@ -17,3 +17,36 @@ export const AUTH_TOKENS = gql`
}
}
`;
export const AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT = gql`
fragment AvailableWorkspaceFragment on AvailableWorkspace {
id
displayName
loginToken
inviteHash
personalInviteToken
workspaceUrls {
subdomainUrl
customUrl
}
logo
sso {
type
id
issuer
name
status
}
}
`;
export const AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT = gql`
fragment AvailableWorkspacesFragment on AvailableWorkspaces {
availableWorkspacesForSignIn {
...AvailableWorkspaceFragment
}
availableWorkspacesForSignUp {
...AvailableWorkspaceFragment
}
}
`;

View File

@ -17,8 +17,7 @@ export const GET_LOGIN_TOKEN_FROM_EMAIL_VERIFICATION_TOKEN = gql`
...AuthTokenFragment
}
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
}
}

View File

@ -6,8 +6,7 @@ export const IMPERSONATE = gql`
impersonate(userId: $userId, workspaceId: $workspaceId) {
workspace {
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
id
}

View File

@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const SIGN_IN = gql`
mutation SignIn($email: String!, $password: String!, $captchaToken: String) {
signIn(email: $email, password: $password, captchaToken: $captchaToken) {
availableWorkspaces {
...AvailableWorkspacesFragment
}
tokens {
...AuthTokensFragment
}
}
}
`;

View File

@ -1,35 +1,13 @@
import { gql } from '@apollo/client';
export const SIGN_UP = gql`
mutation SignUp(
$email: String!
$password: String!
$workspaceInviteHash: String
$workspacePersonalInviteToken: String = null
$captchaToken: String
$workspaceId: String
$locale: String
$verifyEmailNextPath: String
) {
signUp(
email: $email
password: $password
workspaceInviteHash: $workspaceInviteHash
workspacePersonalInviteToken: $workspacePersonalInviteToken
captchaToken: $captchaToken
workspaceId: $workspaceId
locale: $locale
verifyEmailNextPath: $verifyEmailNextPath
) {
loginToken {
...AuthTokenFragment
mutation SignUp($email: String!, $password: String!, $captchaToken: String) {
signUp(email: $email, password: $password, captchaToken: $captchaToken) {
availableWorkspaces {
...AvailableWorkspacesFragment
}
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
}
tokens {
...AuthTokensFragment
}
}
}

View File

@ -9,8 +9,7 @@ export const SIGN_UP_IN_NEW_WORKSPACE = gql`
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
}
}

View File

@ -0,0 +1,36 @@
import { gql } from '@apollo/client';
export const SIGN_UP_IN_WORKSPACE = gql`
mutation SignUpInWorkspace(
$email: String!
$password: String!
$workspaceInviteHash: String
$workspacePersonalInviteToken: String = null
$captchaToken: String
$workspaceId: String
$locale: String
$verifyEmailNextPath: String
) {
signUpInWorkspace(
email: $email
password: $password
workspaceInviteHash: $workspaceInviteHash
workspacePersonalInviteToken: $workspacePersonalInviteToken
captchaToken: $captchaToken
workspaceId: $workspaceId
locale: $locale
verifyEmailNextPath: $verifyEmailNextPath
) {
loginToken {
...AuthTokenFragment
}
workspace {
id
workspaceUrls {
subdomainUrl
customUrl
}
}
}
}
`;

View File

@ -3,30 +3,9 @@ import { gql } from '@apollo/client';
export const CHECK_USER_EXISTS = gql`
query CheckUserExists($email: String!, $captchaToken: String) {
checkUserExists(email: $email, captchaToken: $captchaToken) {
__typename
... on UserExists {
exists
availableWorkspaces {
id
displayName
workspaceUrls {
subdomainUrl
customUrl
}
logo
sso {
type
id
issuer
name
status
}
}
isEmailVerified
}
... on UserNotExists {
exists
}
exists
availableWorkspacesCount
isEmailVerified
}
}
`;

View File

@ -1,4 +1,5 @@
import { gql } from '@apollo/client';
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
query GetPublicWorkspaceDataByDomain($origin: String!) {
@ -7,8 +8,7 @@ export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
logo
displayName
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
authProviders {
sso {
@ -25,4 +25,5 @@ export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql`
}
}
}
${WORKSPACE_URLS_FRAGMENT}
`;

View File

@ -3,6 +3,7 @@ import {
GetCurrentUserDocument,
GetLoginTokenFromCredentialsDocument,
SignUpDocument,
SignUpInWorkspaceDocument,
} from '~/generated/graphql';
export const queries = {
@ -10,6 +11,7 @@ export const queries = {
getAuthTokensFromLoginToken: GetAuthTokensFromLoginTokenDocument,
signup: SignUpDocument,
getCurrentUser: GetCurrentUserDocument,
signUpInWorkspace: SignUpInWorkspaceDocument,
};
export const email = 'test@test.com';
@ -29,7 +31,13 @@ export const variables = {
email,
password,
workspacePersonalInviteToken: null,
locale: "",
locale: '',
},
signUpInWorkspace: {
email,
password,
workspacePersonalInviteToken: null,
locale: '',
},
getCurrentUser: {},
};
@ -48,6 +56,16 @@ export const results = {
},
},
signUp: { loginToken: { token, expiresAt: 'expiresAt' } },
signUpInWorkspace: {
loginToken: { token, expiresAt: 'expiresAt' },
workspace: {
id: 'workspace-id',
workspaceUrls: {
subdomainUrl: 'https://subdomain.twenty.com',
customUrl: 'https://custom.twenty.com',
},
},
},
getCurrentUser: {
currentUser: {
id: 'id',
@ -67,6 +85,7 @@ export const results = {
avatarUrl: 'avatarUrl',
locale: 'locale',
},
availableWorkspaces: [],
currentWorkspace: {
id: 'id',
displayName: 'displayName',
@ -74,6 +93,11 @@ export const results = {
inviteHash: 'inviteHash',
allowImpersonation: true,
subscriptionStatus: 'subscriptionStatus',
customDomain: null,
workspaceUrls: {
customUrl: undefined,
subdomainUrl: 'https://twenty.com',
},
featureFlags: {
id: 'id',
key: 'key',
@ -85,8 +109,8 @@ export const results = {
},
};
export const mocks = [
{
export const mocks = {
getLoginTokenFromCredentials: {
request: {
query: queries.getLoginTokenFromCredentials,
variables: variables.getLoginTokenFromCredentials,
@ -97,7 +121,7 @@ export const mocks = [
},
})),
},
{
getAuthTokensFromLoginToken: {
request: {
query: queries.getAuthTokensFromLoginToken,
variables: variables.getAuthTokensFromLoginToken,
@ -108,7 +132,7 @@ export const mocks = [
},
})),
},
{
signup: {
request: {
query: queries.signup,
variables: variables.signup,
@ -119,7 +143,7 @@ export const mocks = [
},
})),
},
{
getCurrentUser: {
request: {
query: queries.getCurrentUser,
variables: variables.getCurrentUser,
@ -128,4 +152,15 @@ export const mocks = [
data: results.getCurrentUser,
})),
},
];
signUpInWorkspace: {
request: {
query: queries.signUpInWorkspace,
variables: variables.signUpInWorkspace,
},
result: jest.fn(() => ({
data: {
signUpInWorkspace: results.signUpInWorkspace,
},
})),
},
};

View File

@ -2,6 +2,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { billingState } from '@/client-config/states/billingState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
@ -30,9 +31,13 @@ jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItem', () => ({
}));
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
<RecoilRoot>
<MemoryRouter>{children}</MemoryRouter>
<MemoryRouter>
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
{children}
</SnackBarProviderScope>
</MemoryRouter>
</RecoilRoot>
</MockedProvider>
);
@ -63,7 +68,7 @@ describe('useAuth', () => {
).toStrictEqual(results.getLoginTokenFromCredentials);
});
expect(mocks[0].result).toHaveBeenCalled();
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
});
it('should verify user', async () => {
@ -73,19 +78,19 @@ describe('useAuth', () => {
await result.current.getAuthTokensFromLoginToken(token);
});
expect(mocks[1].result).toHaveBeenCalled();
expect(mocks[3].result).toHaveBeenCalled();
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
expect(mocks.getCurrentUser.result).toHaveBeenCalled();
});
it('should handle credential sign-in', async () => {
const { result } = renderHooks();
await act(async () => {
await result.current.signInWithCredentials(email, password);
await result.current.signInWithCredentialsInWorkspace(email, password);
});
expect(mocks[0].result).toHaveBeenCalled();
expect(mocks[1].result).toHaveBeenCalled();
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
});
it('should handle google sign-in', async () => {
@ -94,6 +99,7 @@ describe('useAuth', () => {
await act(async () => {
await result.current.signInWithGoogle({
workspaceInviteHash: 'workspaceInviteHash',
action: 'join-workspace',
});
});
@ -163,9 +169,12 @@ describe('useAuth', () => {
const { result } = renderHooks();
await act(async () => {
await result.current.signUpWithCredentials({ email, password });
await result.current.signUpWithCredentialsInWorkspace({
email,
password,
});
});
expect(mocks[2].result).toHaveBeenCalled();
expect(mocks.signUpInWorkspace.result).toHaveBeenCalled();
});
});

View File

@ -11,19 +11,21 @@ import {
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { workspacesState } from '@/auth/states/workspaces';
import { billingState } from '@/client-config/states/billingState';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
AuthTokenPair,
useCheckUserExistsLazyQuery,
useGetAuthTokensFromLoginTokenMutation,
useGetCurrentUserLazyQuery,
useGetLoginTokenFromCredentialsMutation,
useGetLoginTokenFromEmailVerificationTokenMutation,
useSignUpMutation,
useSignInMutation,
useSignUpInWorkspaceMutation,
} from '~/generated/graphql';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
@ -68,10 +70,17 @@ import { iconsState } from 'twenty-ui/display';
import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
import {
countAvailableWorkspaces,
getFirstAvailableWorkspaces,
} from '@/auth/utils/availableWorkspacesUtils';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
const setCurrentUser = useSetRecoilState(currentUserState);
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
@ -87,16 +96,18 @@ export const useAuth = () => {
);
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { createWorkspace } = useSignUpInNewWorkspace();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setWorkspaces = useSetRecoilState(workspacesState);
const { redirect } = useRedirect();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const [getLoginTokenFromCredentials] =
useGetLoginTokenFromCredentialsMutation();
const [signIn] = useSignInMutation();
const [signUp] = useSignUpMutation();
const [signUpInWorkspace] = useSignUpInWorkspaceMutation();
const [getAuthTokensFromLoginToken] =
useGetAuthTokensFromLoginTokenMutation();
const [getLoginTokenFromEmailVerificationToken] =
@ -280,6 +291,10 @@ export const useAuth = () => {
setCurrentWorkspaceMembers(workspaceMembers);
}
if (isDefined(user.availableWorkspaces)) {
setAvailableWorkspaces(user.availableWorkspaces);
}
if (isDefined(user.currentUserWorkspace)) {
setCurrentUserWorkspace(user.currentUserWorkspace);
}
@ -326,17 +341,6 @@ export const useAuth = () => {
});
}
if (isDefined(user.workspaces)) {
const validWorkspaces = user.workspaces
.filter(
({ workspace }) => workspace !== null && workspace !== undefined,
)
.map((validWorkspace) => validWorkspace.workspace)
.filter(isDefined);
setWorkspaces(validWorkspaces);
}
return {
user,
workspaceMember,
@ -352,9 +356,17 @@ export const useAuth = () => {
setCurrentWorkspaceMembers,
setDateTimeFormat,
setLastAuthenticateWorkspaceDomain,
setWorkspaces,
setAvailableWorkspaces,
]);
const handleSetAuthTokens = useCallback(
(tokens: AuthTokenPair) => {
setTokenPair(tokens);
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
},
[setTokenPair],
);
const handleGetAuthTokensFromLoginToken = useCallback(
async (loginToken: string) => {
const getAuthTokensResult = await getAuthTokensFromLoginToken({
@ -372,14 +384,8 @@ export const useAuth = () => {
throw new Error('No getAuthTokensFromLoginToken result');
}
setTokenPair(
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
);
cookieStorage.setItem(
'tokenPair',
JSON.stringify(
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
),
handleSetAuthTokens(
getAuthTokensResult.data.getAuthTokensFromLoginToken.tokens,
);
// TODO: We can't parallelize this yet because when loadCurrentUSer is loaded
@ -390,14 +396,97 @@ export const useAuth = () => {
},
[
getAuthTokensFromLoginToken,
setTokenPair,
loadCurrentUser,
origin,
handleSetAuthTokens,
refreshObjectMetadataItems,
],
);
const handleCredentialsSignIn = useCallback(
async (email: string, password: string, captchaToken?: string) => {
signIn({
variables: { email, password, captchaToken },
onCompleted: async (data) => {
handleSetAuthTokens(data.signIn.tokens);
const { user } = await loadCurrentUser();
const availableWorkspacesCount = countAvailableWorkspaces(
user.availableWorkspaces,
);
if (availableWorkspacesCount === 0) {
return createWorkspace();
}
if (availableWorkspacesCount === 1) {
const targetWorkspace = getFirstAvailableWorkspaces(
user.availableWorkspaces,
);
return await redirectToWorkspaceDomain(
getWorkspaceUrl(targetWorkspace.workspaceUrls),
targetWorkspace.loginToken ? AppPath.Verify : AppPath.SignInUp,
{
...(targetWorkspace.loginToken && {
loginToken: targetWorkspace.loginToken,
}),
email: user.email,
},
);
}
setSignInUpStep(SignInUpStep.WorkspaceSelection);
},
onError: (error) => {
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
throw error;
}
throw error;
},
});
},
[
handleSetAuthTokens,
redirectToWorkspaceDomain,
signIn,
loadCurrentUser,
setSearchParams,
setSignInUpStep,
createWorkspace,
],
);
const handleCredentialsSignUp = useCallback(
async (email: string, password: string, captchaToken?: string) => {
signUp({
variables: { email, password, captchaToken },
onCompleted: async (data) => {
handleSetAuthTokens(data.signUp.tokens);
const { user } = await loadCurrentUser();
if (countAvailableWorkspaces(user.availableWorkspaces) === 0) {
return createWorkspace();
}
setSignInUpStep(SignInUpStep.WorkspaceSelection);
},
});
},
[
handleSetAuthTokens,
signUp,
loadCurrentUser,
setSignInUpStep,
createWorkspace,
],
);
const handleCredentialsSignInInWorkspace = useCallback(
async (email: string, password: string, captchaToken?: string) => {
const { loginToken } = await handleGetLoginTokenFromCredentials(
email,
@ -413,7 +502,7 @@ export const useAuth = () => {
await clearSession();
}, [clearSession]);
const handleCredentialsSignUp = useCallback(
const handleCredentialsSignUpInWorkspace = useCallback(
async ({
email,
password,
@ -429,7 +518,7 @@ export const useAuth = () => {
captchaToken?: string;
verifyEmailNextPath?: string;
}) => {
const signUpResult = await signUp({
const signUpInWorkspaceResult = await signUpInWorkspace({
variables: {
email,
password,
@ -444,11 +533,11 @@ export const useAuth = () => {
},
});
if (isDefined(signUpResult.errors)) {
throw signUpResult.errors;
if (isDefined(signUpInWorkspaceResult.errors)) {
throw signUpInWorkspaceResult.errors;
}
if (!signUpResult.data?.signUp) {
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
throw new Error('No login token');
}
@ -460,11 +549,15 @@ export const useAuth = () => {
if (isMultiWorkspaceEnabled) {
return await redirectToWorkspaceDomain(
getWorkspaceUrl(signUpResult.data.signUp.workspace.workspaceUrls),
getWorkspaceUrl(
signUpInWorkspaceResult.data.signUpInWorkspace.workspace
.workspaceUrls,
),
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
{
...(!isEmailVerificationRequired && {
loginToken: signUpResult.data.signUp.loginToken.token,
loginToken:
signUpInWorkspaceResult.data.signUpInWorkspace.loginToken.token,
}),
email,
},
@ -472,11 +565,11 @@ export const useAuth = () => {
}
await handleGetAuthTokensFromLoginToken(
signUpResult.data?.signUp.loginToken.token,
signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token,
);
},
[
signUp,
signUpInWorkspace,
workspacePublicData,
isMultiWorkspaceEnabled,
handleGetAuthTokensFromLoginToken,
@ -494,6 +587,7 @@ export const useAuth = () => {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
billingCheckoutSession?: BillingCheckoutSession;
action?: string;
},
) => {
const url = new URL(`${REACT_APP_SERVER_BASE_URL}${path}`);
@ -513,6 +607,10 @@ export const useAuth = () => {
);
}
if (isDefined(params.action)) {
url.searchParams.set('action', params.action);
}
if (isDefined(workspacePublicData)) {
url.searchParams.set('workspaceId', workspacePublicData.id);
}
@ -527,6 +625,7 @@ export const useAuth = () => {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
billingCheckoutSession?: BillingCheckoutSession;
action: string;
}) => {
redirect(buildRedirectUrl('/auth/google', params));
},
@ -538,6 +637,7 @@ export const useAuth = () => {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
billingCheckoutSession?: BillingCheckoutSession;
action: string;
}) => {
redirect(buildRedirectUrl('/auth/microsoft', params));
},
@ -556,8 +656,11 @@ export const useAuth = () => {
clearSession,
signOut: handleSignOut,
signUpWithCredentials: handleCredentialsSignUp,
signUpWithCredentialsInWorkspace: handleCredentialsSignUpInWorkspace,
signInWithCredentialsInWorkspace: handleCredentialsSignInInWorkspace,
signInWithCredentials: handleCredentialsSignIn,
signInWithGoogle: handleGoogleLogin,
signInWithMicrosoft: handleMicrosoftLogin,
setAuthTokens: handleSetAuthTokens,
};
};

View File

@ -2,14 +2,17 @@ import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { FormProvider } from 'react-hook-form';
import { useLocation } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { useAuth } from '@/auth/hooks/useAuth';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { signInUpModeState } from '@/auth/states/signInUpModeState';
@ -17,19 +20,23 @@ import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator } from 'twenty-ui/display';
import {
HorizontalSeparator,
IconChevronRight,
IconPlus,
Avatar,
} from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
import { AvailableWorkspace } from '~/generated/graphql';
const StyledContentContainer = styled(motion.div)`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@ -44,29 +51,101 @@ const StyledForm = styled.form`
width: 100%;
`;
const StyledWorkspaceContainer = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex-direction: column;
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
overflow: hidden;
width: 100%;
`;
const StyledWorkspaceItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
height: ${({ theme }) => theme.spacing(15)};
padding: 0;
overflow: hidden;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
cursor: pointer;
justify-content: space-between;
&:hover {
background-color: ${({ theme }) => theme.background.transparent.light};
}
&:last-child {
border-bottom: none;
}
`;
const StyledWorkspaceContent = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
width: 100%;
padding: 0 ${({ theme }) => theme.spacing(4)};
`;
const StyledWorkspaceTextContainer = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
`;
const StyledWorkspaceLogo = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm};
height: ${({ theme }) => theme.spacing(6)};
width: ${({ theme }) => theme.spacing(6)};
background-color: ${({ theme }) => theme.background.transparent.light};
display: flex;
justify-content: center;
align-items: center;
`;
const StyledWorkspaceName = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding-bottom: ${({ theme }) => theme.spacing(1)};
`;
const StyledWorkspaceUrl = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.xs};
`;
const StyledChevronIcon = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
`;
export const SignInUpGlobalScopeForm = () => {
const authProviders = useRecoilValue(authProvidersState);
const signInUpStep = useRecoilValue(signInUpStepState);
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { checkUserExists } = useAuth();
const { readCaptchaToken } = useReadCaptchaToken();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { createWorkspace } = useSignUpInNewWorkspace();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
const [signInUpMode] = useRecoilState(signInUpModeState);
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const theme = useTheme();
const { t } = useLingui();
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
const { enqueueSnackBar } = useSnackBar();
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const [showErrors, setShowErrors] = useState(false);
const { form } = useSignInUpForm();
const { pathname } = useLocation();
const { submitCredentials } = useSignInUp(form);
const { submitCredentials, continueWithCredentials } = useSignInUp(form);
const handleSubmit = async () => {
if (isDefined(form?.formState?.errors?.email)) {
@ -79,38 +158,7 @@ export const SignInUpGlobalScopeForm = () => {
return;
}
const token = await readCaptchaToken();
await checkUserExists.checkUserExistsQuery({
variables: {
email: form.getValues('email').toLowerCase().trim(),
captchaToken: token,
},
onError: (error) => {
enqueueSnackBar(`${error.message}`, {
variant: SnackBarVariant.Error,
});
},
onCompleted: async (data) => {
requestFreshCaptchaToken();
const response = data.checkUserExists;
if (response.__typename === 'UserExists') {
if (response.availableWorkspaces.length >= 1) {
const workspace = response.availableWorkspaces[0];
return await redirectToWorkspaceDomain(
getWorkspaceUrl(workspace.workspaceUrls),
pathname,
{
email: form.getValues('email'),
},
);
}
}
if (response.__typename === 'UserNotExists') {
setSignInUpMode(SignInUpMode.SignUp);
setSignInUpStep(SignInUpStep.Password);
}
},
});
continueWithCredentials();
};
const onEmailChange = (email: string) => {
@ -119,42 +167,118 @@ export const SignInUpGlobalScopeForm = () => {
}
};
const getAvailableWorkspaceUrl = (availableWorkspace: AvailableWorkspace) => {
const { pathname, searchParams } = getAvailableWorkspacePathAndSearchParams(
availableWorkspace,
{ email: form.getValues('email') },
);
return buildWorkspaceUrl(
getWorkspaceUrl(availableWorkspace.workspaceUrls),
pathname,
searchParams,
);
};
return (
<>
<StyledContentContainer>
{authProviders.google && <SignInUpWithGoogle />}
{authProviders.microsoft && <SignInUpWithMicrosoft />}
{(authProviders.google || authProviders.microsoft) && (
<HorizontalSeparator />
)}
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...form}>
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
<SignInUpEmailField
showErrors={showErrors}
onInputChange={onEmailChange}
/>
{signInUpStep === SignInUpStep.Password && (
<SignInUpPasswordField
{signInUpStep === SignInUpStep.WorkspaceSelection && (
<StyledWorkspaceContainer>
{[
...availableWorkspaces.availableWorkspacesForSignIn,
...availableWorkspaces.availableWorkspacesForSignUp,
].map((availableWorkspace) => (
<UndecoratedLink
key={availableWorkspace.id}
to={getAvailableWorkspaceUrl(availableWorkspace)}
>
<StyledWorkspaceItem>
<StyledWorkspaceContent>
<Avatar
placeholder={availableWorkspace.displayName || ''}
avatarUrl={
availableWorkspace.logo ?? DEFAULT_WORKSPACE_LOGO
}
size="lg"
/>
<StyledWorkspaceTextContainer>
<StyledWorkspaceName>
{availableWorkspace.displayName || availableWorkspace.id}
</StyledWorkspaceName>
<StyledWorkspaceUrl>
{
new URL(
getWorkspaceUrl(availableWorkspace.workspaceUrls),
).hostname
}
</StyledWorkspaceUrl>
</StyledWorkspaceTextContainer>
<StyledChevronIcon>
<IconChevronRight size={theme.icon.size.md} />
</StyledChevronIcon>
</StyledWorkspaceContent>
</StyledWorkspaceItem>
</UndecoratedLink>
))}
<StyledWorkspaceItem onClick={() => createWorkspace()}>
<StyledWorkspaceContent>
<StyledWorkspaceLogo>
<IconPlus size={theme.icon.size.lg} />
</StyledWorkspaceLogo>
<StyledWorkspaceTextContainer>
<StyledWorkspaceName>{t`Create a workspace`}</StyledWorkspaceName>
</StyledWorkspaceTextContainer>
<StyledChevronIcon>
<IconChevronRight size={theme.icon.size.md} />
</StyledChevronIcon>
</StyledWorkspaceContent>
</StyledWorkspaceItem>
</StyledWorkspaceContainer>
)}
{signInUpStep !== SignInUpStep.WorkspaceSelection && (
<StyledContentContainer>
{authProviders.google && (
<SignInUpWithGoogle action="list-available-workspaces" />
)}
{authProviders.microsoft && (
<SignInUpWithMicrosoft action="list-available-workspaces" />
)}
{(authProviders.google || authProviders.microsoft) && (
<HorizontalSeparator />
)}
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...form}>
<StyledForm onSubmit={form.handleSubmit(handleSubmit)}>
<SignInUpEmailField
showErrors={showErrors}
signInUpMode={signInUpMode}
onInputChange={onEmailChange}
/>
)}
<MainButton
disabled={isRequestingCaptchaToken}
title={
signInUpStep === SignInUpStep.Password ? 'Sign Up' : 'Continue'
}
type="submit"
variant={
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
}
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
fullWidth
/>
</StyledForm>
</FormProvider>
</StyledContentContainer>
{signInUpStep === SignInUpStep.Password && (
<SignInUpPasswordField
showErrors={showErrors}
signInUpMode={signInUpMode}
/>
)}
<MainButton
disabled={isRequestingCaptchaToken}
title={
signInUpStep === SignInUpStep.Password
? signInUpMode === SignInUpMode.SignIn
? t`Sign In`
: t`Sign Up`
: t`Continue`
}
type="submit"
variant={
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
}
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
fullWidth
/>
</StyledForm>
</FormProvider>
</StyledContentContainer>
)}
</>
);
};

View File

@ -1,7 +1,7 @@
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/SignInUpWithSSO';
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 { SignInUpWithSSO } from '@/auth/sign-in-up/components/internal/SignInUpWithSSO';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
@ -36,9 +36,13 @@ export const SignInUpWorkspaceScopeForm = () => {
return (
<>
<StyledContentContainer>
{workspaceAuthProviders.google && <SignInUpWithGoogle />}
{workspaceAuthProviders.google && (
<SignInUpWithGoogle action="join-workspace" />
)}
{workspaceAuthProviders.microsoft && <SignInUpWithMicrosoft />}
{workspaceAuthProviders.microsoft && (
<SignInUpWithMicrosoft action="join-workspace" />
)}
{workspaceAuthProviders.sso.length > 0 && <SignInUpWithSSO />}

View File

@ -0,0 +1,34 @@
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useAuth } from '@/auth/hooks/useAuth';
import { useSearchParams } from 'react-router-dom';
export const SignInUpGlobalScopeFormEffect = () => {
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const [searchParams, setSearchParams] = useSearchParams();
const { setAuthTokens, loadCurrentUser } = useAuth();
useEffect(() => {
const tokenPair = searchParams.get('tokenPair');
if (isDefined(tokenPair)) {
setAuthTokens(JSON.parse(tokenPair));
searchParams.delete('tokenPair');
setSearchParams(searchParams);
loadCurrentUser();
setSignInUpStep(SignInUpStep.WorkspaceSelection);
}
}, [
searchParams,
setSearchParams,
setSignInUpStep,
loadCurrentUser,
setAuthTokens,
]);
return <></>;
};

View File

@ -5,8 +5,8 @@ import {
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/internal/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/internal/SignInUpPasswordField';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { captchaState } from '@/client-config/states/captchaState';

View File

@ -9,23 +9,27 @@ import { memo } from 'react';
import { useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconGoogle } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
const GoogleIcon = memo(() => {
const theme = useTheme();
return <IconGoogle size={theme.icon.size.md} />;
});
export const SignInUpWithGoogle = () => {
export const SignInUpWithGoogle = ({
action,
}: {
action: SocialSSOSignInUpActionType;
}) => {
const { t } = useLingui();
const signInUpStep = useRecoilValue(signInUpStepState);
const { signInWithGoogle } = useSignInWithGoogle();
return (
<>
<MainButton
Icon={GoogleIcon}
title={t`Continue with Google`}
onClick={signInWithGoogle}
onClick={() => signInWithGoogle({ action })}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>

View File

@ -8,8 +8,13 @@ import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconMicrosoft } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
export const SignInUpWithMicrosoft = () => {
export const SignInUpWithMicrosoft = ({
action,
}: {
action: SocialSSOSignInUpActionType;
}) => {
const theme = useTheme();
const { t } = useLingui();
@ -21,7 +26,7 @@ export const SignInUpWithMicrosoft = () => {
<MainButton
Icon={() => <IconMicrosoft size={theme.icon.size.md} />}
title={t`Continue with Microsoft`}
onClick={signInWithMicrosoft}
onClick={() => signInWithMicrosoft({ action })}
variant={signInUpStep === SignInUpStep.Init ? undefined : 'secondary'}
fullWidth
/>

View File

@ -43,9 +43,12 @@ describe('useSignInWithGoogle', () => {
const { result } = renderHook(() => useSignInWithGoogle(), {
wrapper: Wrapper,
});
result.current.signInWithGoogle();
result.current.signInWithGoogle({
action: 'join-workspace',
});
expect(signInWithGoogleMock).toHaveBeenCalledWith({
action: 'join-workspace',
workspaceInviteHash: 'testHash',
workspacePersonalInviteToken: 'testToken',
billingCheckoutSession: mockBillingCheckoutSession,
@ -66,9 +69,12 @@ describe('useSignInWithGoogle', () => {
const { result } = renderHook(() => useSignInWithGoogle(), {
wrapper: Wrapper,
});
result.current.signInWithGoogle();
result.current.signInWithGoogle({
action: 'join-workspace',
});
expect(signInWithGoogleMock).toHaveBeenCalledWith({
action: 'join-workspace',
workspaceInviteHash: 'testHash',
workspacePersonalInviteToken: undefined,
billingCheckoutSession: mockBillingCheckoutSession,

View File

@ -43,9 +43,12 @@ describe('useSignInWithMicrosoft', () => {
const { result } = renderHook(() => useSignInWithMicrosoft(), {
wrapper: Wrapper,
});
result.current.signInWithMicrosoft();
result.current.signInWithMicrosoft({
action: 'join-workspace',
});
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
action: 'join-workspace',
workspaceInviteHash: workspaceInviteHashMock,
workspacePersonalInviteToken: inviteTokenMock,
billingCheckoutSession: mockBillingCheckoutSession,
@ -67,9 +70,12 @@ describe('useSignInWithMicrosoft', () => {
const { result } = renderHook(() => useSignInWithMicrosoft(), {
wrapper: Wrapper,
});
result.current.signInWithMicrosoft();
result.current.signInWithMicrosoft({
action: 'join-workspace',
});
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
action: 'join-workspace',
billingCheckoutSession: mockBillingCheckoutSession,
workspaceInviteHash: workspaceInviteHashMock,
workspacePersonalInviteToken: undefined,

View File

@ -19,12 +19,14 @@ import { useRecoilState } from 'recoil';
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useAuth } from '../../hooks/useAuth';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
export const useSignInUp = (form: UseFormReturn<Form>) => {
const { enqueueSnackBar } = useSnackBar();
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const location = useLocation();
@ -38,7 +40,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
);
const {
signInWithCredentialsInWorkspace,
signInWithCredentials,
signUpWithCredentialsInWorkspace,
signUpWithCredentials,
checkUserExists: { checkUserExistsQuery },
} = useAuth();
@ -71,11 +75,11 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
},
onCompleted: (data) => {
requestFreshCaptchaToken();
if (data?.checkUserExists.exists) {
setSignInUpMode(SignInUpMode.SignIn);
} else {
setSignInUpMode(SignInUpMode.SignUp);
}
setSignInUpMode(
data?.checkUserExists.exists
? SignInUpMode.SignIn
: SignInUpMode.SignUp,
);
setSignInUpStep(SignInUpStep.Password);
},
});
@ -97,31 +101,60 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
throw new Error('Email and password are required');
}
if (signInUpMode === SignInUpMode.SignIn && !isInviteMode) {
await signInWithCredentials(
if (
!isInviteMode &&
signInUpMode === SignInUpMode.SignIn &&
isOnAWorkspace
) {
return await signInWithCredentialsInWorkspace(
data.email.toLowerCase().trim(),
data.password,
token,
);
} else {
const verifyEmailNextPath = buildAppPathWithQueryParams(
AppPath.PlanRequired,
await buildSearchParamsFromUrlSyncedStates(),
);
await signUpWithCredentials({
email: data.email.toLowerCase().trim(),
password: data.password,
workspaceInviteHash,
workspacePersonalInviteToken,
captchaToken: token,
verifyEmailNextPath,
});
}
if (
!isInviteMode &&
signInUpMode === SignInUpMode.SignIn &&
!isOnAWorkspace
) {
return await signInWithCredentials(
data.email.toLowerCase().trim(),
data.password,
token,
);
}
if (
!isInviteMode &&
signInUpMode === SignInUpMode.SignUp &&
!isOnAWorkspace
) {
return await signUpWithCredentials(
data.email.toLowerCase().trim(),
data.password,
token,
);
}
const verifyEmailNextPath = buildAppPathWithQueryParams(
AppPath.PlanRequired,
await buildSearchParamsFromUrlSyncedStates(),
);
await signUpWithCredentialsInWorkspace({
email: data.email.toLowerCase().trim(),
password: data.password,
workspaceInviteHash,
workspacePersonalInviteToken,
captchaToken: token,
verifyEmailNextPath,
});
} catch (err: any) {
enqueueSnackBar(err?.message, {
variant: SnackBarVariant.Error,
});
} finally {
requestFreshCaptchaToken();
}
},
@ -129,13 +162,16 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
readCaptchaToken,
signInUpMode,
isInviteMode,
signInWithCredentialsInWorkspace,
signInWithCredentials,
signUpWithCredentials,
signUpWithCredentialsInWorkspace,
workspaceInviteHash,
workspacePersonalInviteToken,
enqueueSnackBar,
requestFreshCaptchaToken,
buildSearchParamsFromUrlSyncedStates,
isOnAWorkspace,
],
);

View File

@ -1,7 +1,7 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useLocation, useSearchParams } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { z } from 'zod';
@ -30,7 +30,6 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
export type Form = z.infer<ReturnType<typeof makeValidationSchema>>;
export const useSignInUpForm = () => {
const location = useLocation();
const signInUpStep = useRecoilValue(signInUpStepState);
const validationSchema = makeValidationSchema(signInUpStep); // Create schema based on the current step
@ -61,11 +60,6 @@ export const useSignInUpForm = () => {
form.setValue('email', prefilledEmail ?? 'tim@apple.dev');
form.setValue('password', 'tim@apple.dev');
}
}, [
form,
isDeveloperDefaultSignInPrefilled,
prefilledEmail,
location.search,
]);
}, [form, isDeveloperDefaultSignInPrefilled, prefilledEmail]);
return { form: form };
};

View File

@ -2,6 +2,7 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
export const useSignInWithGoogle = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
@ -15,12 +16,14 @@ export const useSignInWithGoogle = () => {
} as BillingCheckoutSession;
const { signInWithGoogle } = useAuth();
return {
signInWithGoogle: () =>
signInWithGoogle: ({ action }: { action: SocialSSOSignInUpActionType }) =>
signInWithGoogle({
workspaceInviteHash,
workspacePersonalInviteToken,
billingCheckoutSession,
action,
}),
};
};

View File

@ -3,6 +3,7 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { useRecoilValue } from 'recoil';
import { SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
export const useSignInWithMicrosoft = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
@ -13,11 +14,16 @@ export const useSignInWithMicrosoft = () => {
const { signInWithMicrosoft } = useAuth();
return {
signInWithMicrosoft: () =>
signInWithMicrosoft: ({
action,
}: {
action: SocialSSOSignInUpActionType;
}) =>
signInWithMicrosoft({
workspaceInviteHash,
workspacePersonalInviteToken,
billingCheckoutSession,
action,
}),
};
};

View File

@ -0,0 +1,37 @@
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { AppPath } from '@/types/AppPath';
import { useSignUpInNewWorkspaceMutation } from '~/generated/graphql';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
export const useSignUpInNewWorkspace = () => {
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { enqueueSnackBar } = useSnackBar();
const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation();
const createWorkspace = () => {
signUpInNewWorkspaceMutation({
onCompleted: async (data) => {
return await redirectToWorkspaceDomain(
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
AppPath.Verify,
{
loginToken: data.signUpInNewWorkspace.loginToken.token,
},
'_blank',
);
},
onError: (error: Error) => {
enqueueSnackBar(error.message, {
variant: SnackBarVariant.Error,
});
},
});
};
return {
createWorkspace,
};
};

View File

@ -1,9 +0,0 @@
import { UserExists } from '~/generated/graphql';
import { createState } from 'twenty-ui/utilities';
export const availableSSOIdentityProvidersForAuthState = createState<
NonNullable<UserExists['availableWorkspaces']>[0]['sso']
>({
key: 'availableSSOIdentityProvidersForAuth',
defaultValue: [],
});

View File

@ -0,0 +1,10 @@
import { createState } from 'twenty-ui/utilities';
import { AvailableWorkspaces } from '~/generated/graphql';
export const availableWorkspacesState = createState<AvailableWorkspaces>({
key: 'availableWorkspacesState',
defaultValue: {
availableWorkspacesForSignIn: [],
availableWorkspacesForSignUp: [],
},
});

View File

@ -1,12 +0,0 @@
import { Workspace } from '~/generated/graphql';
import { createState } from 'twenty-ui/utilities';
export type Workspaces = Pick<
Workspace,
'id' | 'logo' | 'displayName' | 'workspaceUrls'
>[];
export const workspacesState = createState<Workspaces>({
key: 'workspacesState',
defaultValue: [],
});

View File

@ -0,0 +1,4 @@
export type SocialSSOSignInUpActionType =
| 'create-new-workspace'
| 'list-available-workspaces'
| 'join-workspace';

View File

@ -0,0 +1,70 @@
import { AvailableWorkspaces, AvailableWorkspace } from '~/generated/graphql';
import { AppPath } from '@/types/AppPath';
import { isDefined } from 'twenty-shared/utils';
import { generatePath } from 'react-router-dom';
export const countAvailableWorkspaces = ({
availableWorkspacesForSignIn,
availableWorkspacesForSignUp,
}: AvailableWorkspaces): number => {
return (
availableWorkspacesForSignIn.length + availableWorkspacesForSignUp.length
);
};
export const getFirstAvailableWorkspaces = ({
availableWorkspacesForSignIn,
availableWorkspacesForSignUp,
}: AvailableWorkspaces): AvailableWorkspace => {
return availableWorkspacesForSignIn[0] ?? availableWorkspacesForSignUp[0];
};
const getAvailableWorkspacePathname = (
availableWorkspace: AvailableWorkspace,
) => {
if (isDefined(availableWorkspace.loginToken)) {
return AppPath.Verify;
}
if (
isDefined(availableWorkspace.personalInviteToken) &&
isDefined(availableWorkspace.inviteHash)
) {
return generatePath(AppPath.Invite, {
workspaceInviteHash: availableWorkspace.inviteHash,
});
}
return AppPath.SignInUp;
};
const getAvailableWorkspaceSearchParams = (
availableWorkspace: AvailableWorkspace,
defaultSearchParams: Record<string, string> = {},
) => {
const searchParams: Record<string, string> = defaultSearchParams;
if (isDefined(availableWorkspace.loginToken)) {
searchParams.loginToken = availableWorkspace.loginToken;
return searchParams;
}
if (isDefined(availableWorkspace.personalInviteToken)) {
searchParams.inviteToken = availableWorkspace.personalInviteToken;
}
return searchParams;
};
export const getAvailableWorkspacePathAndSearchParams = (
availableWorkspace: AvailableWorkspace,
defaultSearchParams: Record<string, string> = {},
): { pathname: string; searchParams: Record<string, string> } => {
return {
pathname: getAvailableWorkspacePathname(availableWorkspace),
searchParams: getAvailableWorkspaceSearchParams(
availableWorkspace,
defaultSearchParams,
),
};
};

View File

@ -0,0 +1,43 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
describe('useReadCaptchaToken', () => {
it('should return undefined when no token exists', async () => {
const { result } = renderHook(() => useReadCaptchaToken(), {
wrapper: RecoilRoot,
});
await act(async () => {
const token = await result.current.readCaptchaToken();
expect(token).toBeUndefined();
});
});
it('should return the token when it exists', async () => {
const { result } = renderHook(
() => {
const hook = useReadCaptchaToken();
return hook;
},
{
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(captchaTokenState, 'test-token');
}}
>
{children}
</RecoilRoot>
),
},
);
await act(async () => {
const token = await result.current.readCaptchaToken();
expect(token).toBe('test-token');
});
});
});

View File

@ -0,0 +1,66 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import * as ReactRouterDom from 'react-router-dom';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
describe('useRequestFreshCaptchaToken', () => {
beforeEach(() => {
// Mock useLocation to return a path that requires captcha
(ReactRouterDom.useLocation as jest.Mock).mockReturnValue({
pathname: '/sign-in',
});
// Mock window.grecaptcha
window.grecaptcha = {
execute: jest.fn().mockImplementation(() => {
return Promise.resolve('google-recaptcha-token');
}),
};
// Mock window.turnstile
window.turnstile = {
render: jest.fn().mockReturnValue('turnstile-widget-id'),
execute: jest.fn().mockImplementation((widgetId, options) => {
return options?.callback('turnstile-token');
}),
};
});
afterEach(() => {
jest.clearAllMocks();
delete window.grecaptcha;
delete window.turnstile;
});
it('should not request a token if captcha is not required for the path', async () => {
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: RecoilRoot,
});
await act(async () => {
await result.current.requestFreshCaptchaToken();
});
expect(window.grecaptcha.execute).not.toHaveBeenCalled();
expect(window.turnstile.execute).not.toHaveBeenCalled();
});
it('should not request a token if captcha provider is not defined', async () => {
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: RecoilRoot,
});
await act(async () => {
await result.current.requestFreshCaptchaToken();
});
expect(window.grecaptcha.execute).not.toHaveBeenCalled();
expect(window.turnstile.execute).not.toHaveBeenCalled();
});
});

View File

@ -18,8 +18,9 @@ export const useIsCurrentLocationOnAWorkspace = () => {
throw new Error('frontDomain and defaultSubdomain are required');
}
const isOnAWorkspace =
isMultiWorkspaceEnabled && window.location.hostname !== defaultDomain;
const isOnAWorkspace = !isMultiWorkspaceEnabled
? true
: window.location.hostname !== defaultDomain;
return {
isOnAWorkspace,

View File

@ -150,8 +150,7 @@ export const queries = {
hasValidEnterpriseKey
customDomain
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
featureFlags {
id
@ -179,8 +178,7 @@ export const queries = {
subdomain
customDomain
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
}
}

View File

@ -14,6 +14,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
const mockCurrentUser = {
id: 'fake-user-id',
@ -41,7 +42,11 @@ const initializeState = ({ set }: MutableSnapshot) => {
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider>
<RecoilRoot initializeState={initializeState}>
<MemoryRouter>{children}</MemoryRouter>
<MemoryRouter>
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
{children}
</SnackBarProviderScope>
</MemoryRouter>
</RecoilRoot>
</MockedProvider>
);

View File

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

View File

@ -1,32 +1,23 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces, workspacesState } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Avatar, IconChevronLeft } from 'twenty-ui/display';
import { MenuItemSelectAvatar, UndecoratedLink } from 'twenty-ui/navigation';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { IconChevronLeft } from 'twenty-ui/display';
import { WorkspacesForSignIn } from './components/WorkspacesForSignIn';
import { WorkspacesForSignUp } from './components/WorkspacesForSignUp';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const workspaces = useRecoilValue(workspacesState);
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { t } = useLingui();
const handleChange = async (workspace: Workspaces[0]) => {
await redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls));
};
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const setMultiWorkspaceDropdownState = useSetRecoilState(
multiWorkspaceDropdownState,
);
@ -52,37 +43,10 @@ export const MultiWorkspaceDropdownWorkspacesListComponents = () => {
}}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
{workspaces
.filter(
(workspace) =>
workspace.id !== currentWorkspace?.id &&
workspace.displayName
?.toLowerCase()
.includes(searchValue.toLowerCase()),
)
.map((workspace) => (
<UndecoratedLink
key={workspace.id}
to={buildWorkspaceUrl(getWorkspaceUrl(workspace.workspaceUrls))}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace);
}}
>
<MenuItemSelectAvatar
text={workspace.displayName ?? '(No name)'}
avatar={
<Avatar
placeholder={workspace.displayName || ''}
avatarUrl={workspace.logo ?? DEFAULT_WORKSPACE_LOGO}
/>
}
selected={currentWorkspace?.id === workspace.id}
/>
</UndecoratedLink>
))}
</DropdownMenuItemsContainer>
<WorkspacesForSignIn searchValue={searchValue} />
{availableWorkspaces.availableWorkspacesForSignUp.length > 0 && (
<WorkspacesForSignUp searchValue={searchValue} />
)}
</DropdownContent>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { workspacesState } from '@/auth/states/workspaces';
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
@ -30,6 +29,7 @@ import { useGetCurrentUserQuery } from '~/generated/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
export const UserProviderEffect = () => {
const location = useLocation();
@ -40,7 +40,7 @@ export const UserProviderEffect = () => {
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setWorkspaces = useSetRecoilState(workspacesState);
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
const isLoggedIn = useIsLogged();
@ -102,7 +102,7 @@ export const UserProviderEffect = () => {
workspaceMember,
workspaceMembers,
deletedWorkspaceMembers,
workspaces: userWorkspaces,
availableWorkspaces,
} = queryData.currentUser;
const affectDefaultValuesOnEmptyWorkspaceMemberFields = (
@ -153,12 +153,8 @@ export const UserProviderEffect = () => {
setCurrentWorkspaceMembersWithDeleted(deletedWorkspaceMembers);
}
if (isDefined(userWorkspaces)) {
const workspaces = userWorkspaces
.map(({ workspace }) => workspace)
.filter(isDefined);
setWorkspaces(workspaces);
if (isDefined(availableWorkspaces)) {
setAvailableWorkspaces(availableWorkspaces);
}
}, [
queryLoading,
@ -166,9 +162,9 @@ export const UserProviderEffect = () => {
setCurrentUser,
setCurrentUserWorkspace,
setCurrentWorkspaceMembers,
setAvailableWorkspaces,
setCurrentWorkspace,
setCurrentWorkspaceMember,
setWorkspaces,
setIsCurrentUserLoaded,
setDateTimeFormat,
setCurrentWorkspaceMembersWithDeleted,

View File

@ -3,6 +3,7 @@ import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
import { gql } from '@apollo/client';
import { AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT } from '@/auth/graphql/fragments/authFragments';
export const USER_QUERY_FRAGMENT = gql`
${ROLE_FRAGMENT}
@ -48,8 +49,7 @@ export const USER_QUERY_FRAGMENT = gql`
customDomain
isCustomDomainEnabled
workspaceUrls {
subdomainUrl
customUrl
...WorkspaceUrlsFragment
}
featureFlags {
key
@ -86,22 +86,13 @@ export const USER_QUERY_FRAGMENT = gql`
...RoleFragment
}
}
workspaces {
workspace {
id
logo
displayName
subdomain
customDomain
workspaceUrls {
subdomainUrl
customUrl
}
}
availableWorkspaces {
...AvailableWorkspacesFragment
}
userVars
}
${AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT}
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
${DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT}
`;

View File

@ -0,0 +1,8 @@
import { gql } from '@apollo/client';
export const WORKSPACE_URLS_FRAGMENT = gql`
fragment WorkspaceUrlsFragment on WorkspaceUrls {
subdomainUrl
customUrl
}
`;

View File

@ -124,7 +124,7 @@ export const PasswordReset = () => {
const [updatePasswordViaToken, { loading: isUpdatingPassword }] =
useUpdatePasswordViaResetTokenMutation();
const { signInWithCredentials } = useAuth();
const { signInWithCredentialsInWorkspace } = useAuth();
const { readCaptchaToken } = useReadCaptchaToken();
const onSubmit = async (formData: Form) => {
@ -153,7 +153,11 @@ export const PasswordReset = () => {
const token = await readCaptchaToken();
await signInWithCredentials(email || '', formData.newPassword, token);
await signInWithCredentialsInWorkspace(
email || '',
formData.newPassword,
token,
);
navigate(AppPath.Index);
} catch (err) {
logError(err);

View File

@ -1,17 +1,20 @@
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/internal/SignInUpSSOIdentityProviderSelection';
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceScopeFormEffect';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
@ -26,17 +29,20 @@ import { useSearchParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { PublicWorkspaceDataOutput } from '~/generated/graphql';
import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect';
const StandardContent = ({
workspacePublicData,
signInUpForm,
signInUpStep,
title,
onClickOnLogo,
}: {
workspacePublicData: PublicWorkspaceDataOutput | null;
signInUpForm: JSX.Element | null;
signInUpStep: SignInUpStep;
title: string;
onClickOnLogo: () => void;
}) => {
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
@ -44,6 +50,7 @@ const StandardContent = ({
<Logo
secondaryLogo={workspacePublicData?.logo}
placeholder={workspacePublicData?.displayName}
onClick={onClickOnLogo}
/>
</AnimatedEaseIn>
<Title animate>{title}</Title>
@ -55,6 +62,7 @@ const StandardContent = ({
export const SignInUp = () => {
const { t } = useLingui();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { form } = useSignInUpForm();
const { signInUpStep } = useSignInUp(form);
@ -67,10 +75,20 @@ export const SignInUp = () => {
useWorkspaceFromInviteHash();
const [searchParams] = useSearchParams();
const onClickOnLogo = () => {
setSignInUpStep(SignInUpStep.Init);
};
const title = useMemo(() => {
if (isDefined(workspaceInviteHash)) {
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
}
if (signInUpStep === SignInUpStep.WorkspaceSelection) {
return t`Choose a Workspace`;
}
const workspaceName = !isDefined(workspacePublicData?.displayName)
? DEFAULT_WORKSPACE_NAME
: workspacePublicData?.displayName === ''
@ -79,31 +97,32 @@ export const SignInUp = () => {
return t`Welcome to ${workspaceName}`;
}, [
workspaceFromInviteHash?.displayName,
workspaceInviteHash,
signInUpStep,
workspacePublicData?.displayName,
t,
workspaceFromInviteHash?.displayName,
]);
const signInUpForm = useMemo(() => {
if (loading) return null;
if (isDefaultDomain && isMultiWorkspaceEnabled) {
return <SignInUpGlobalScopeForm />;
return (
<>
<SignInUpGlobalScopeFormEffect />
<SignInUpGlobalScopeForm />
</>
);
}
if (
(!isMultiWorkspaceEnabled ||
(isMultiWorkspaceEnabled && isOnAWorkspace)) &&
isOnAWorkspace &&
signInUpStep === SignInUpStep.SSOIdentityProviderSelection
) {
return <SignInUpSSOIdentityProviderSelection />;
}
if (
isDefined(workspacePublicData) &&
(!isMultiWorkspaceEnabled || isOnAWorkspace)
) {
if (isDefined(workspacePublicData) && isOnAWorkspace) {
return (
<>
<SignInUpWorkspaceScopeFormEffect />
@ -112,7 +131,12 @@ export const SignInUp = () => {
);
}
return <SignInUpGlobalScopeForm />;
return (
<>
<SignInUpGlobalScopeFormEffect />
<SignInUpGlobalScopeForm />
</>
);
}, [
isDefaultDomain,
isMultiWorkspaceEnabled,
@ -136,6 +160,7 @@ export const SignInUp = () => {
signInUpForm={signInUpForm}
signInUpStep={signInUpStep}
title={title}
onClickOnLogo={onClickOnLogo}
/>
);
};

View File

@ -1,19 +1,19 @@
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPath } from '@/types/SettingsPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm } from 'react-hook-form';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { Trans, useLingui } from '@lingui/react/macro';
import { z } from 'zod';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
import { Controller, useForm } from 'react-hook-form';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { TextInput } from '@/ui/input/components/TextInput';
import { z } from 'zod';
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsSecurityApprovedAccessDomain = () => {
const navigate = useNavigateSettings();
@ -62,7 +62,7 @@ export const SettingsSecurityApprovedAccessDomain = () => {
},
},
onCompleted: () => {
enqueueSnackBar(t`Domain added successfully.`, {
enqueueSnackBar(t`Please check your email for a verification link.`, {
variant: SnackBarVariant.Success,
});
navigate(SettingsPath.Security);

View File

@ -19,6 +19,7 @@ import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
import { mockedUserData } from '~/testing/mock-data/users';
import { mockedViewsData } from '~/testing/mock-data/views';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
import { mockedPublicWorkspaceDataBySubdomain } from '~/testing/mock-data/publicWorkspaceDataBySubdomain';
import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain';
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
@ -90,22 +91,8 @@ export const graphqlMocks = {
() => {
return HttpResponse.json({
data: {
getPublicWorkspaceDataByDomain: {
id: 'id',
logo: 'logo',
displayName: 'displayName',
workspaceUrls: {
customUrl: undefined,
subdomainUrl: 'https://twenty.com',
},
authProviders: {
google: true,
microsoft: false,
password: true,
magicLink: false,
sso: [],
},
},
getPublicWorkspaceDataByDomain:
mockedPublicWorkspaceDataBySubdomain,
},
});
},

View File

@ -7,6 +7,7 @@ export const mockedPublicWorkspaceDataBySubdomain: GetPublicWorkspaceDataByDomai
logo: 'workspace-logo/original/c88deb49-7636-4560-918d-08c3265ffb20.49?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3b3Jrc3BhY2VJZCI6Ijk4NzAzMjNlLTIyYzMtNGQxNC05YjdmLTViZGM4NGY3ZDZlZSIsImlhdCI6MTczNjU0MDU0MywiZXhwIjoxNzM2NjI2OTQzfQ.C8cnHu09VGseRbQAMM4nhiO6z4TLG03ntFTvxm53-xg',
displayName: 'Twenty Eng',
workspaceUrls: {
__typename: 'WorkspaceUrls',
customUrl: 'https://twenty-eng.com',
subdomainUrl: 'https://custom.twenty.com',
},

View File

@ -25,6 +25,7 @@ type MockedUser = Pick<
| 'supportUserHash'
| 'onboardingStatus'
| 'userVars'
| 'availableWorkspaces'
> & {
workspaceMember: WorkspaceMember | null;
locale: string;
@ -132,6 +133,10 @@ export const mockedUserData: MockedUser = {
workspaces: [{ workspace: mockCurrentWorkspace }],
workspaceMembers: [mockedWorkspaceMemberData],
onboardingStatus: OnboardingStatus.COMPLETED,
availableWorkspaces: {
availableWorkspacesForSignIn: [],
availableWorkspacesForSignUp: [],
},
userVars: {},
};