feat(invitation): Improve invitation flow - Milestone 2 (#6804)
From PR: #6626 Resolves #6763 Resolves #6055 Resolves #6782 ## GTK I retain the 'Invite by link' feature to prevent any breaking changes. We could make the invitation by link optional through an admin setting, allowing users to rely solely on personal invitations. ## Todo - [x] Add an expiration date to an invitation - [x] Allow to renew an invitation to postpone the expiration date - [x] Refresh the UI - [x] Add the new personal token in the link sent to new user - [x] Display an error if a user tries to use an expired invitation - [x] Display an error if a user uses another mail than the one in the invitation --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -340,6 +340,7 @@ export type Mutation = {
|
||||
activateWorkflowVersion: Scalars['Boolean'];
|
||||
activateWorkspace: Workspace;
|
||||
addUserToWorkspace: User;
|
||||
addUserToWorkspaceByInviteToken: User;
|
||||
authorizeApp: AuthorizeApp;
|
||||
challenge: LoginToken;
|
||||
checkoutSession: SessionEntity;
|
||||
@ -352,6 +353,7 @@ export type Mutation = {
|
||||
deleteOneObject: Object;
|
||||
deleteOneServerlessFunction: ServerlessFunction;
|
||||
deleteUser: User;
|
||||
deleteWorkspaceInvitation: Scalars['String'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
emailPasswordResetLink: EmailPasswordResetLink;
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
@ -363,8 +365,9 @@ export type Mutation = {
|
||||
impersonate: Verify;
|
||||
publishServerlessFunction: ServerlessFunction;
|
||||
renewToken: AuthTokens;
|
||||
resendWorkspaceInvitation: SendInvitationsOutput;
|
||||
runWorkflowVersion: WorkflowRun;
|
||||
sendInviteLink: SendInviteLink;
|
||||
sendInvitations: SendInvitationsOutput;
|
||||
signUp: LoginToken;
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||
track: Analytics;
|
||||
@ -396,6 +399,11 @@ export type MutationAddUserToWorkspaceArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationAddUserToWorkspaceByInviteTokenArgs = {
|
||||
inviteToken: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationAuthorizeAppArgs = {
|
||||
clientId: Scalars['String'];
|
||||
codeChallenge?: InputMaybe<Scalars['String']>;
|
||||
@ -442,6 +450,11 @@ export type MutationDeleteOneServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkspaceInvitationArgs = {
|
||||
appTokenId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationEmailPasswordResetLinkArgs = {
|
||||
email: Scalars['String'];
|
||||
};
|
||||
@ -485,12 +498,17 @@ export type MutationRenewTokenArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationResendWorkspaceInvitationArgs = {
|
||||
appTokenId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationRunWorkflowVersionArgs = {
|
||||
input: RunWorkflowVersionInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSendInviteLinkArgs = {
|
||||
export type MutationSendInvitationsArgs = {
|
||||
emails: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
@ -500,6 +518,7 @@ export type MutationSignUpArgs = {
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||
workspacePersonalInviteToken?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
@ -636,6 +655,7 @@ export type Query = {
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||
getAISQLQuery: AisqlQueryResult;
|
||||
getAvailablePackages: Scalars['JSON'];
|
||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||
@ -790,8 +810,10 @@ export type RunWorkflowVersionInput = {
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type SendInviteLink = {
|
||||
__typename?: 'SendInviteLink';
|
||||
export type SendInvitationsOutput = {
|
||||
__typename?: 'SendInvitationsOutput';
|
||||
errors: Array<Scalars['String']>;
|
||||
result: Array<WorkspaceInvitation>;
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
@ -1147,6 +1169,13 @@ export type WorkspaceEdge = {
|
||||
node: Workspace;
|
||||
};
|
||||
|
||||
export type WorkspaceInvitation = {
|
||||
__typename?: 'WorkspaceInvitation';
|
||||
email: Scalars['String'];
|
||||
expiresAt: Scalars['DateTime'];
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type WorkspaceInviteHashValid = {
|
||||
__typename?: 'WorkspaceInviteHashValid';
|
||||
isValid: Scalars['Boolean'];
|
||||
@ -1415,6 +1444,7 @@ export type SignUpMutationVariables = Exact<{
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||
workspacePersonalInviteToken?: InputMaybe<Scalars['String']>;
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
@ -1514,6 +1544,32 @@ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, 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, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
||||
|
||||
export type DeleteWorkspaceInvitationMutationVariables = Exact<{
|
||||
appTokenId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteWorkspaceInvitationMutation = { __typename?: 'Mutation', deleteWorkspaceInvitation: string };
|
||||
|
||||
export type ResendWorkspaceInvitationMutationVariables = Exact<{
|
||||
appTokenId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ResendWorkspaceInvitationMutation = { __typename?: 'Mutation', resendWorkspaceInvitation: { __typename?: 'SendInvitationsOutput', success: boolean, errors: Array<string>, result: Array<{ __typename?: 'WorkspaceInvitation', id: any, email: string, expiresAt: string }> } };
|
||||
|
||||
export type SendInvitationsMutationVariables = Exact<{
|
||||
emails: Array<Scalars['String']> | Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SendInvitationsMutation = { __typename?: 'Mutation', sendInvitations: { __typename?: 'SendInvitationsOutput', success: boolean, errors: Array<string>, result: Array<{ __typename?: 'WorkspaceInvitation', id: any, email: string, expiresAt: string }> } };
|
||||
|
||||
export type GetWorkspaceInvitationsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetWorkspaceInvitationsQuery = { __typename?: 'Query', findWorkspaceInvitations: Array<{ __typename?: 'WorkspaceInvitation', id: any, email: string, expiresAt: string }> };
|
||||
|
||||
export type WorkspaceMemberQueryFragmentFragment = { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } };
|
||||
|
||||
export type AddUserToWorkspaceMutationVariables = Exact<{
|
||||
@ -1523,6 +1579,13 @@ export type AddUserToWorkspaceMutationVariables = Exact<{
|
||||
|
||||
export type AddUserToWorkspaceMutation = { __typename?: 'Mutation', addUserToWorkspace: { __typename?: 'User', id: any } };
|
||||
|
||||
export type AddUserToWorkspaceByInviteTokenMutationVariables = Exact<{
|
||||
inviteToken: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AddUserToWorkspaceByInviteTokenMutation = { __typename?: 'Mutation', addUserToWorkspaceByInviteToken: { __typename?: 'User', id: any } };
|
||||
|
||||
export type ActivateWorkspaceMutationVariables = Exact<{
|
||||
input: ActivateWorkspaceInput;
|
||||
}>;
|
||||
@ -1535,13 +1598,6 @@ export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: nev
|
||||
|
||||
export type DeleteCurrentWorkspaceMutation = { __typename?: 'Mutation', deleteCurrentWorkspace: { __typename?: 'Workspace', id: any } };
|
||||
|
||||
export type SendInviteLinkMutationVariables = Exact<{
|
||||
emails: Array<Scalars['String']> | Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SendInviteLinkMutation = { __typename?: 'Mutation', sendInviteLink: { __typename?: 'SendInviteLink', success: boolean } };
|
||||
|
||||
export type UpdateWorkspaceMutationVariables = Exact<{
|
||||
input: UpdateWorkspaceInput;
|
||||
}>;
|
||||
@ -2262,11 +2318,12 @@ export type RenewTokenMutationHookResult = ReturnType<typeof useRenewTokenMutati
|
||||
export type RenewTokenMutationResult = Apollo.MutationResult<RenewTokenMutation>;
|
||||
export type RenewTokenMutationOptions = Apollo.BaseMutationOptions<RenewTokenMutation, RenewTokenMutationVariables>;
|
||||
export const SignUpDocument = gql`
|
||||
mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $captchaToken: String) {
|
||||
mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String) {
|
||||
signUp(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
) {
|
||||
loginToken {
|
||||
@ -2293,6 +2350,7 @@ export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMut
|
||||
* email: // value for 'email'
|
||||
* password: // value for 'password'
|
||||
* workspaceInviteHash: // value for 'workspaceInviteHash'
|
||||
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
|
||||
* captchaToken: // value for 'captchaToken'
|
||||
* },
|
||||
* });
|
||||
@ -2828,6 +2886,155 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
|
||||
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
|
||||
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
|
||||
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
|
||||
export const DeleteWorkspaceInvitationDocument = gql`
|
||||
mutation DeleteWorkspaceInvitation($appTokenId: String!) {
|
||||
deleteWorkspaceInvitation(appTokenId: $appTokenId)
|
||||
}
|
||||
`;
|
||||
export type DeleteWorkspaceInvitationMutationFn = Apollo.MutationFunction<DeleteWorkspaceInvitationMutation, DeleteWorkspaceInvitationMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteWorkspaceInvitationMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteWorkspaceInvitationMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteWorkspaceInvitationMutation` 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 [deleteWorkspaceInvitationMutation, { data, loading, error }] = useDeleteWorkspaceInvitationMutation({
|
||||
* variables: {
|
||||
* appTokenId: // value for 'appTokenId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteWorkspaceInvitationMutation(baseOptions?: Apollo.MutationHookOptions<DeleteWorkspaceInvitationMutation, DeleteWorkspaceInvitationMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteWorkspaceInvitationMutation, DeleteWorkspaceInvitationMutationVariables>(DeleteWorkspaceInvitationDocument, options);
|
||||
}
|
||||
export type DeleteWorkspaceInvitationMutationHookResult = ReturnType<typeof useDeleteWorkspaceInvitationMutation>;
|
||||
export type DeleteWorkspaceInvitationMutationResult = Apollo.MutationResult<DeleteWorkspaceInvitationMutation>;
|
||||
export type DeleteWorkspaceInvitationMutationOptions = Apollo.BaseMutationOptions<DeleteWorkspaceInvitationMutation, DeleteWorkspaceInvitationMutationVariables>;
|
||||
export const ResendWorkspaceInvitationDocument = gql`
|
||||
mutation ResendWorkspaceInvitation($appTokenId: String!) {
|
||||
resendWorkspaceInvitation(appTokenId: $appTokenId) {
|
||||
success
|
||||
errors
|
||||
result {
|
||||
... on WorkspaceInvitation {
|
||||
id
|
||||
email
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ResendWorkspaceInvitationMutationFn = Apollo.MutationFunction<ResendWorkspaceInvitationMutation, ResendWorkspaceInvitationMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useResendWorkspaceInvitationMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useResendWorkspaceInvitationMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useResendWorkspaceInvitationMutation` 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 [resendWorkspaceInvitationMutation, { data, loading, error }] = useResendWorkspaceInvitationMutation({
|
||||
* variables: {
|
||||
* appTokenId: // value for 'appTokenId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useResendWorkspaceInvitationMutation(baseOptions?: Apollo.MutationHookOptions<ResendWorkspaceInvitationMutation, ResendWorkspaceInvitationMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ResendWorkspaceInvitationMutation, ResendWorkspaceInvitationMutationVariables>(ResendWorkspaceInvitationDocument, options);
|
||||
}
|
||||
export type ResendWorkspaceInvitationMutationHookResult = ReturnType<typeof useResendWorkspaceInvitationMutation>;
|
||||
export type ResendWorkspaceInvitationMutationResult = Apollo.MutationResult<ResendWorkspaceInvitationMutation>;
|
||||
export type ResendWorkspaceInvitationMutationOptions = Apollo.BaseMutationOptions<ResendWorkspaceInvitationMutation, ResendWorkspaceInvitationMutationVariables>;
|
||||
export const SendInvitationsDocument = gql`
|
||||
mutation SendInvitations($emails: [String!]!) {
|
||||
sendInvitations(emails: $emails) {
|
||||
success
|
||||
errors
|
||||
result {
|
||||
... on WorkspaceInvitation {
|
||||
id
|
||||
email
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SendInvitationsMutationFn = Apollo.MutationFunction<SendInvitationsMutation, SendInvitationsMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSendInvitationsMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSendInvitationsMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSendInvitationsMutation` 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 [sendInvitationsMutation, { data, loading, error }] = useSendInvitationsMutation({
|
||||
* variables: {
|
||||
* emails: // value for 'emails'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSendInvitationsMutation(baseOptions?: Apollo.MutationHookOptions<SendInvitationsMutation, SendInvitationsMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SendInvitationsMutation, SendInvitationsMutationVariables>(SendInvitationsDocument, options);
|
||||
}
|
||||
export type SendInvitationsMutationHookResult = ReturnType<typeof useSendInvitationsMutation>;
|
||||
export type SendInvitationsMutationResult = Apollo.MutationResult<SendInvitationsMutation>;
|
||||
export type SendInvitationsMutationOptions = Apollo.BaseMutationOptions<SendInvitationsMutation, SendInvitationsMutationVariables>;
|
||||
export const GetWorkspaceInvitationsDocument = gql`
|
||||
query GetWorkspaceInvitations {
|
||||
findWorkspaceInvitations {
|
||||
id
|
||||
email
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetWorkspaceInvitationsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetWorkspaceInvitationsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetWorkspaceInvitationsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetWorkspaceInvitationsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetWorkspaceInvitationsQuery(baseOptions?: Apollo.QueryHookOptions<GetWorkspaceInvitationsQuery, GetWorkspaceInvitationsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetWorkspaceInvitationsQuery, GetWorkspaceInvitationsQueryVariables>(GetWorkspaceInvitationsDocument, options);
|
||||
}
|
||||
export function useGetWorkspaceInvitationsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetWorkspaceInvitationsQuery, GetWorkspaceInvitationsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetWorkspaceInvitationsQuery, GetWorkspaceInvitationsQueryVariables>(GetWorkspaceInvitationsDocument, options);
|
||||
}
|
||||
export type GetWorkspaceInvitationsQueryHookResult = ReturnType<typeof useGetWorkspaceInvitationsQuery>;
|
||||
export type GetWorkspaceInvitationsLazyQueryHookResult = ReturnType<typeof useGetWorkspaceInvitationsLazyQuery>;
|
||||
export type GetWorkspaceInvitationsQueryResult = Apollo.QueryResult<GetWorkspaceInvitationsQuery, GetWorkspaceInvitationsQueryVariables>;
|
||||
export const AddUserToWorkspaceDocument = gql`
|
||||
mutation AddUserToWorkspace($inviteHash: String!) {
|
||||
addUserToWorkspace(inviteHash: $inviteHash) {
|
||||
@ -2861,6 +3068,39 @@ export function useAddUserToWorkspaceMutation(baseOptions?: Apollo.MutationHookO
|
||||
export type AddUserToWorkspaceMutationHookResult = ReturnType<typeof useAddUserToWorkspaceMutation>;
|
||||
export type AddUserToWorkspaceMutationResult = Apollo.MutationResult<AddUserToWorkspaceMutation>;
|
||||
export type AddUserToWorkspaceMutationOptions = Apollo.BaseMutationOptions<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>;
|
||||
export const AddUserToWorkspaceByInviteTokenDocument = gql`
|
||||
mutation AddUserToWorkspaceByInviteToken($inviteToken: String!) {
|
||||
addUserToWorkspaceByInviteToken(inviteToken: $inviteToken) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type AddUserToWorkspaceByInviteTokenMutationFn = Apollo.MutationFunction<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useAddUserToWorkspaceByInviteTokenMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useAddUserToWorkspaceByInviteTokenMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useAddUserToWorkspaceByInviteTokenMutation` 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 [addUserToWorkspaceByInviteTokenMutation, { data, loading, error }] = useAddUserToWorkspaceByInviteTokenMutation({
|
||||
* variables: {
|
||||
* inviteToken: // value for 'inviteToken'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAddUserToWorkspaceByInviteTokenMutation(baseOptions?: Apollo.MutationHookOptions<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>(AddUserToWorkspaceByInviteTokenDocument, options);
|
||||
}
|
||||
export type AddUserToWorkspaceByInviteTokenMutationHookResult = ReturnType<typeof useAddUserToWorkspaceByInviteTokenMutation>;
|
||||
export type AddUserToWorkspaceByInviteTokenMutationResult = Apollo.MutationResult<AddUserToWorkspaceByInviteTokenMutation>;
|
||||
export type AddUserToWorkspaceByInviteTokenMutationOptions = Apollo.BaseMutationOptions<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>;
|
||||
export const ActivateWorkspaceDocument = gql`
|
||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||
activateWorkspace(data: $input) {
|
||||
@ -2926,39 +3166,6 @@ export function useDeleteCurrentWorkspaceMutation(baseOptions?: Apollo.MutationH
|
||||
export type DeleteCurrentWorkspaceMutationHookResult = ReturnType<typeof useDeleteCurrentWorkspaceMutation>;
|
||||
export type DeleteCurrentWorkspaceMutationResult = Apollo.MutationResult<DeleteCurrentWorkspaceMutation>;
|
||||
export type DeleteCurrentWorkspaceMutationOptions = Apollo.BaseMutationOptions<DeleteCurrentWorkspaceMutation, DeleteCurrentWorkspaceMutationVariables>;
|
||||
export const SendInviteLinkDocument = gql`
|
||||
mutation SendInviteLink($emails: [String!]!) {
|
||||
sendInviteLink(emails: $emails) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SendInviteLinkMutationFn = Apollo.MutationFunction<SendInviteLinkMutation, SendInviteLinkMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSendInviteLinkMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSendInviteLinkMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSendInviteLinkMutation` 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 [sendInviteLinkMutation, { data, loading, error }] = useSendInviteLinkMutation({
|
||||
* variables: {
|
||||
* emails: // value for 'emails'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSendInviteLinkMutation(baseOptions?: Apollo.MutationHookOptions<SendInviteLinkMutation, SendInviteLinkMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SendInviteLinkMutation, SendInviteLinkMutationVariables>(SendInviteLinkDocument, options);
|
||||
}
|
||||
export type SendInviteLinkMutationHookResult = ReturnType<typeof useSendInviteLinkMutation>;
|
||||
export type SendInviteLinkMutationResult = Apollo.MutationResult<SendInviteLinkMutation>;
|
||||
export type SendInviteLinkMutationOptions = Apollo.BaseMutationOptions<SendInviteLinkMutation, SendInviteLinkMutationVariables>;
|
||||
export const UpdateWorkspaceDocument = gql`
|
||||
mutation UpdateWorkspace($input: UpdateWorkspaceInput!) {
|
||||
updateWorkspace(data: $input) {
|
||||
|
||||
@ -5,12 +5,14 @@ export const SIGN_UP = gql`
|
||||
$email: String!
|
||||
$password: String!
|
||||
$workspaceInviteHash: String
|
||||
$workspacePersonalInviteToken: String = null
|
||||
$captchaToken: String
|
||||
) {
|
||||
signUp(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
) {
|
||||
loginToken {
|
||||
|
||||
@ -264,6 +264,7 @@ export const useAuth = () => {
|
||||
email: string,
|
||||
password: string,
|
||||
workspaceInviteHash?: string,
|
||||
workspacePersonalInviteToken?: string,
|
||||
captchaToken?: string,
|
||||
) => {
|
||||
setIsVerifyPendingState(true);
|
||||
@ -273,6 +274,7 @@ export const useAuth = () => {
|
||||
email,
|
||||
password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
captchaToken,
|
||||
},
|
||||
});
|
||||
@ -296,21 +298,43 @@ export const useAuth = () => {
|
||||
[setIsVerifyPendingState, signUp, handleVerify],
|
||||
);
|
||||
|
||||
const handleGoogleLogin = useCallback((workspaceInviteHash?: string) => {
|
||||
const buildRedirectUrl = (
|
||||
path: string,
|
||||
params: {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
},
|
||||
) => {
|
||||
const authServerUrl = REACT_APP_SERVER_BASE_URL;
|
||||
window.location.href =
|
||||
`${authServerUrl}/auth/google/${
|
||||
workspaceInviteHash ? '?inviteHash=' + workspaceInviteHash : ''
|
||||
}` || '';
|
||||
}, []);
|
||||
const url = new URL(`${authServerUrl}${path}`);
|
||||
if (isDefined(params.workspaceInviteHash)) {
|
||||
url.searchParams.set('inviteHash', params.workspaceInviteHash);
|
||||
}
|
||||
if (isDefined(params.workspacePersonalInviteToken)) {
|
||||
url.searchParams.set('inviteToken', params.workspacePersonalInviteToken);
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const handleMicrosoftLogin = useCallback((workspaceInviteHash?: string) => {
|
||||
const authServerUrl = REACT_APP_SERVER_BASE_URL;
|
||||
window.location.href =
|
||||
`${authServerUrl}/auth/microsoft/${
|
||||
workspaceInviteHash ? '?inviteHash=' + workspaceInviteHash : ''
|
||||
}` || '';
|
||||
}, []);
|
||||
const handleGoogleLogin = useCallback(
|
||||
(params: {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
}) => {
|
||||
window.location.href = buildRedirectUrl('/auth/google', params);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMicrosoftLogin = useCallback(
|
||||
(params: {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
}) => {
|
||||
window.location.href = buildRedirectUrl('/auth/microsoft', params);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
challenge: handleChallenge,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { SubmitHandler, UseFormReturn } from 'react-hook-form';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
@ -29,6 +29,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [searchParams] = useSearchParams();
|
||||
const workspacePersonalInviteToken =
|
||||
searchParams.get('inviteToken') ?? undefined;
|
||||
|
||||
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
||||
|
||||
@ -112,6 +115,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
data.email.toLowerCase().trim(),
|
||||
data.password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
token,
|
||||
);
|
||||
} catch (err: any) {
|
||||
@ -128,6 +132,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
signInWithCredentials,
|
||||
signUpWithCredentials,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
enqueueSnackBar,
|
||||
requestFreshCaptchaToken,
|
||||
],
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
|
||||
export const useSignInWithGoogle = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [searchParams] = useSearchParams();
|
||||
const workspacePersonalInviteToken =
|
||||
searchParams.get('inviteToken') ?? undefined;
|
||||
const { signInWithGoogle } = useAuth();
|
||||
return { signInWithGoogle: () => signInWithGoogle(workspaceInviteHash) };
|
||||
return {
|
||||
signInWithGoogle: () =>
|
||||
signInWithGoogle({ workspaceInviteHash, workspacePersonalInviteToken }),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
|
||||
export const useSignInWithMicrosoft = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [searchParams] = useSearchParams();
|
||||
const workspacePersonalInviteToken =
|
||||
searchParams.get('inviteToken') ?? undefined;
|
||||
const { signInWithMicrosoft } = useAuth();
|
||||
return {
|
||||
signInWithMicrosoft: () => signInWithMicrosoft(workspaceInviteHash),
|
||||
signInWithMicrosoft: () =>
|
||||
signInWithMicrosoft({
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
|
||||
|
||||
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ export const useObjectMetadataItem = ({
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
// Todo: deprecate this logic as mocked objectMetadataItems are laod in ObjectMetadataItemsLoadEffect anyway
|
||||
// Todo: deprecate this logic as mocked objectMetadataItems are load in ObjectMetadataItemsLoadEffect anyway
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
let objectMetadataItem = useRecoilValue(
|
||||
|
||||
@ -9,12 +9,13 @@ const StyledTableRow = styled('div', {
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
to?: string;
|
||||
gridAutoColumns?: string;
|
||||
}>`
|
||||
background-color: ${({ isSelected, theme }) =>
|
||||
isSelected ? theme.accent.quaternary : 'transparent'};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-columns: ${({ gridAutoColumns }) => gridAutoColumns ?? '1fr'};
|
||||
grid-auto-flow: column;
|
||||
transition: background-color
|
||||
${({ theme }) => theme.animation.duration.normal}s;
|
||||
@ -33,6 +34,7 @@ type TableRowProps = {
|
||||
onClick?: () => void;
|
||||
to?: string;
|
||||
className?: string;
|
||||
gridAutoColumns?: string;
|
||||
};
|
||||
|
||||
export const TableRow = ({
|
||||
@ -41,10 +43,12 @@ export const TableRow = ({
|
||||
to,
|
||||
className,
|
||||
children,
|
||||
gridAutoColumns,
|
||||
}: React.PropsWithChildren<TableRowProps>) => (
|
||||
<StyledTableRow
|
||||
isSelected={isSelected}
|
||||
onClick={onClick}
|
||||
gridAutoColumns={gridAutoColumns}
|
||||
className={className}
|
||||
to={to}
|
||||
as={to ? Link : 'div'}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_WORKSPACE_INVITATION = gql`
|
||||
mutation DeleteWorkspaceInvitation($appTokenId: String!) {
|
||||
deleteWorkspaceInvitation(appTokenId: $appTokenId)
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,17 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const RESEND_WORKSPACE_INVITATION = gql`
|
||||
mutation ResendWorkspaceInvitation($appTokenId: String!) {
|
||||
resendWorkspaceInvitation(appTokenId: $appTokenId) {
|
||||
success
|
||||
errors
|
||||
result {
|
||||
... on WorkspaceInvitation {
|
||||
id
|
||||
email
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,17 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SEND_INVITATIONS = gql`
|
||||
mutation SendInvitations($emails: [String!]!) {
|
||||
sendInvitations(emails: $emails) {
|
||||
success
|
||||
errors
|
||||
result {
|
||||
... on WorkspaceInvitation {
|
||||
id
|
||||
email
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_WORKSPACE_INVITATIONS = gql`
|
||||
query GetWorkspaceInvitations {
|
||||
findWorkspaceInvitations {
|
||||
id
|
||||
email
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,26 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useSendInvitationsMutation } from '~/generated/graphql';
|
||||
import { SendInvitationsMutationVariables } from '../../../generated/graphql';
|
||||
import { workspaceInvitationsState } from '../states/workspaceInvitationsStates';
|
||||
|
||||
export const useCreateWorkspaceInvitation = () => {
|
||||
const [sendInvitationsMutation] = useSendInvitationsMutation();
|
||||
|
||||
const setWorkspaceInvitations = useSetRecoilState(workspaceInvitationsState);
|
||||
|
||||
const sendInvitation = async (emails: SendInvitationsMutationVariables) => {
|
||||
return await sendInvitationsMutation({
|
||||
variables: emails,
|
||||
onCompleted: (data) => {
|
||||
setWorkspaceInvitations((workspaceInvitations) => [
|
||||
...workspaceInvitations,
|
||||
...data.sendInvitations.result,
|
||||
]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
sendInvitation,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
DeleteWorkspaceInvitationMutationVariables,
|
||||
useDeleteWorkspaceInvitationMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { workspaceInvitationsState } from '../states/workspaceInvitationsStates';
|
||||
|
||||
export const useDeleteWorkspaceInvitation = () => {
|
||||
const [deleteWorkspaceInvitationMutation] =
|
||||
useDeleteWorkspaceInvitationMutation();
|
||||
|
||||
const setWorkspaceInvitations = useSetRecoilState(workspaceInvitationsState);
|
||||
|
||||
const deleteWorkspaceInvitation = async ({
|
||||
appTokenId,
|
||||
}: DeleteWorkspaceInvitationMutationVariables) => {
|
||||
return await deleteWorkspaceInvitationMutation({
|
||||
variables: {
|
||||
appTokenId,
|
||||
},
|
||||
onCompleted: () => {
|
||||
setWorkspaceInvitations((workspaceInvitations) =>
|
||||
workspaceInvitations.filter(
|
||||
(workspaceInvitation) => workspaceInvitation.id !== appTokenId,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
deleteWorkspaceInvitation,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
ResendWorkspaceInvitationMutationVariables,
|
||||
useResendWorkspaceInvitationMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { workspaceInvitationsState } from '../states/workspaceInvitationsStates';
|
||||
|
||||
export const useResendWorkspaceInvitation = () => {
|
||||
const [resendWorkspaceInvitationMutation] =
|
||||
useResendWorkspaceInvitationMutation();
|
||||
|
||||
const setWorkspaceInvitations = useSetRecoilState(workspaceInvitationsState);
|
||||
|
||||
const resendInvitation = async ({
|
||||
appTokenId,
|
||||
}: ResendWorkspaceInvitationMutationVariables) => {
|
||||
return await resendWorkspaceInvitationMutation({
|
||||
variables: {
|
||||
appTokenId,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
setWorkspaceInvitations((workspaceInvitations) => [
|
||||
...data.resendWorkspaceInvitation.result,
|
||||
...workspaceInvitations.filter(
|
||||
(workspaceInvitation) => workspaceInvitation.id !== appTokenId,
|
||||
),
|
||||
]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
resendInvitation,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
import { WorkspaceInvitation } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export const workspaceInvitationsState = createState<
|
||||
Omit<WorkspaceInvitation, '__typename'>[]
|
||||
>({
|
||||
key: 'workspaceInvitationsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const ADD_USER_TO_WORKSPACE_BY_INVITE_TOKEN = gql`
|
||||
mutation AddUserToWorkspaceByInviteToken($inviteToken: String!) {
|
||||
addUserToWorkspaceByInviteToken(inviteToken: $inviteToken) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -24,3 +24,10 @@ export type WorkspaceMember = {
|
||||
dateFormat?: WorkspaceMemberDateFormatEnum | null;
|
||||
timeFormat?: WorkspaceMemberTimeFormatEnum | null;
|
||||
};
|
||||
|
||||
export type WorkspaceInvitation = {
|
||||
__typename: 'WorkspaceInvitation';
|
||||
id: string;
|
||||
email: string;
|
||||
expiresAt: string;
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { IconMail, IconSend } from 'twenty-ui';
|
||||
import { IconSend } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
@ -11,12 +11,13 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList';
|
||||
import { useSendInviteLinkMutation } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useCreateWorkspaceInvitation } from '../../workspace-invitation/hooks/useCreateWorkspaceInvitation';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
@ -69,7 +70,7 @@ type FormInput = {
|
||||
|
||||
export const WorkspaceInviteTeam = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const [sendInviteLink] = useSendInviteLinkMutation();
|
||||
const { sendInvitation } = useCreateWorkspaceInvitation();
|
||||
|
||||
const { reset, handleSubmit, control, formState } = useForm<FormInput>({
|
||||
mode: 'onSubmit',
|
||||
@ -79,16 +80,27 @@ export const WorkspaceInviteTeam = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const submit = handleSubmit(async (data) => {
|
||||
const emailsList = sanitizeEmailList(data.emails.split(','));
|
||||
const result = await sendInviteLink({ variables: { emails: emailsList } });
|
||||
if (isDefined(result.errors)) {
|
||||
throw result.errors;
|
||||
const submit = handleSubmit(async ({ emails }) => {
|
||||
const emailsList = sanitizeEmailList(emails.split(','));
|
||||
const { data } = await sendInvitation({ emails: emailsList });
|
||||
if (isDefined(data) && data.sendInvitations.result.length > 0) {
|
||||
enqueueSnackBar(
|
||||
`${data.sendInvitations.result.length} invitations sent`,
|
||||
{
|
||||
variant: SnackBarVariant.Success,
|
||||
duration: 2000,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isDefined(data) && !data.sendInvitations.success) {
|
||||
data.sendInvitations.errors.forEach((error) => {
|
||||
enqueueSnackBar(error, {
|
||||
variant: SnackBarVariant.Error,
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
enqueueSnackBar('Invite link sent to email addresses', {
|
||||
variant: SnackBarVariant.Success,
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@ -116,7 +128,6 @@ export const WorkspaceInviteTeam = () => {
|
||||
return (
|
||||
<TextInput
|
||||
placeholder="tim@apple.com, jony.ive@apple.dev"
|
||||
LeftIcon={IconMail}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error?.message}
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.spacing(2)};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(0)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-left: ${({ theme }) => theme.spacing(3)};
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const StyledEmailText = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
type WorkspaceMemberCardProps = {
|
||||
workspaceMember: WorkspaceMember;
|
||||
accessory?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const WorkspaceMemberCard = ({
|
||||
workspaceMember,
|
||||
accessory,
|
||||
}: WorkspaceMemberCardProps) => (
|
||||
<StyledContainer>
|
||||
<Avatar
|
||||
avatarUrl={workspaceMember.avatarUrl}
|
||||
placeholderColorSeed={workspaceMember.id}
|
||||
placeholder={workspaceMember.name.firstName || ''}
|
||||
type="squared"
|
||||
size="xl"
|
||||
/>
|
||||
<StyledContent>
|
||||
<OverflowingTextWithTooltip
|
||||
text={
|
||||
workspaceMember.name.firstName + ' ' + workspaceMember.name.lastName
|
||||
}
|
||||
/>
|
||||
<StyledEmailText>{workspaceMember.userEmail}</StyledEmailText>
|
||||
</StyledContent>
|
||||
{accessory}
|
||||
</StyledContainer>
|
||||
);
|
||||
@ -1,9 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SEND_INVITE_LINK = gql`
|
||||
mutation SendInviteLink($emails: [String!]!) {
|
||||
sendInviteLink(emails: $emails) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -13,8 +13,12 @@ import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
|
||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||
import { useAddUserToWorkspaceMutation } from '~/generated/graphql';
|
||||
import {
|
||||
useAddUserToWorkspaceMutation,
|
||||
useAddUserToWorkspaceByInviteTokenMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
@ -24,26 +28,40 @@ const StyledContentContainer = styled.div`
|
||||
export const Invite = () => {
|
||||
const { workspace: workspaceFromInviteHash, workspaceInviteHash } =
|
||||
useWorkspaceFromInviteHash();
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
|
||||
const [addUserToWorkspaceByInviteToken] =
|
||||
useAddUserToWorkspaceByInviteTokenMutation();
|
||||
const { switchWorkspace } = useWorkspaceSwitching();
|
||||
const [searchParams] = useSearchParams();
|
||||
const workspaceInviteToken = searchParams.get('inviteToken');
|
||||
|
||||
const title = useMemo(() => {
|
||||
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
|
||||
}, [workspaceFromInviteHash?.displayName]);
|
||||
|
||||
const handleUserJoinWorkspace = async () => {
|
||||
if (
|
||||
!(isDefined(workspaceInviteHash) && isDefined(workspaceFromInviteHash))
|
||||
if (isDefined(workspaceInviteToken) && isDefined(workspaceFromInviteHash)) {
|
||||
await addUserToWorkspaceByInviteToken({
|
||||
variables: {
|
||||
inviteToken: workspaceInviteToken,
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
isDefined(workspaceInviteHash) &&
|
||||
isDefined(workspaceFromInviteHash)
|
||||
) {
|
||||
await addUserToWorkspace({
|
||||
variables: {
|
||||
inviteHash: workspaceInviteHash,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await addUserToWorkspace({
|
||||
variables: {
|
||||
inviteHash: workspaceInviteHash,
|
||||
},
|
||||
});
|
||||
|
||||
await switchWorkspace(workspaceFromInviteHash.id);
|
||||
};
|
||||
|
||||
|
||||
@ -27,11 +27,9 @@ import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { AnimatedTranslation } from '@/ui/utilities/animation/components/AnimatedTranslation';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import {
|
||||
OnboardingStatus,
|
||||
useSendInviteLinkMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useCreateWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useCreateWorkspaceInvitation';
|
||||
|
||||
const StyledAnimatedContainer = styled.div`
|
||||
display: flex;
|
||||
@ -65,7 +63,8 @@ type FormInput = z.infer<typeof validationSchema>;
|
||||
export const InviteTeam = () => {
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const [sendInviteLink] = useSendInviteLinkMutation();
|
||||
const { sendInvitation } = useCreateWorkspaceInvitation();
|
||||
|
||||
const setNextOnboardingStatus = useSetNextOnboardingStatus();
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
@ -134,7 +133,7 @@ export const InviteTeam = () => {
|
||||
.filter((email) => email.length > 0),
|
||||
),
|
||||
);
|
||||
const result = await sendInviteLink({ variables: { emails } });
|
||||
const result = await sendInvitation({ emails });
|
||||
|
||||
setNextOnboardingStatus();
|
||||
|
||||
@ -148,7 +147,7 @@ export const InviteTeam = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[enqueueSnackBar, sendInviteLink, setNextOnboardingStatus],
|
||||
[enqueueSnackBar, sendInvitation, setNextOnboardingStatus],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { H2Title, IconTrash, IconUsers } from 'twenty-ui';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
H2Title,
|
||||
IconTrash,
|
||||
IconUsers,
|
||||
IconReload,
|
||||
IconMail,
|
||||
StyledText,
|
||||
Avatar,
|
||||
} from 'twenty-ui';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
@ -18,7 +28,19 @@ import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { WorkspaceInviteLink } from '@/workspace/components/WorkspaceInviteLink';
|
||||
import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam';
|
||||
import { WorkspaceMemberCard } from '@/workspace/components/WorkspaceMemberCard';
|
||||
import { useGetWorkspaceInvitationsQuery } from '~/generated/graphql';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { workspaceInvitationsState } from '../../modules/workspace-invitation/states/workspaceInvitationsStates';
|
||||
import { TableRow } from '../../modules/ui/layout/table/components/TableRow';
|
||||
import { TableCell } from '../../modules/ui/layout/table/components/TableCell';
|
||||
import { Status } from '../../modules/ui/display/status/components/Status';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useResendWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useResendWorkspaceInvitation';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useDeleteWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useDeleteWorkspaceInvitation';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -27,7 +49,17 @@ const StyledButtonContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
margin-top: ${({ theme }) => theme.spacing(0.5)};
|
||||
`;
|
||||
|
||||
const StyledTableHeaderRow = styled(Table)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
|
||||
`;
|
||||
|
||||
export const SettingsWorkspaceMembers = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const theme = useTheme();
|
||||
const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
|
||||
const [workspaceMemberToDelete, setWorkspaceMemberToDelete] = useState<
|
||||
string | undefined
|
||||
@ -39,6 +71,10 @@ export const SettingsWorkspaceMembers = () => {
|
||||
const { deleteOneRecord: deleteOneWorkspaceMember } = useDeleteOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
});
|
||||
|
||||
const { resendInvitation } = useResendWorkspaceInvitation();
|
||||
const { deleteWorkspaceInvitation } = useDeleteWorkspaceInvitation();
|
||||
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
@ -47,6 +83,47 @@ export const SettingsWorkspaceMembers = () => {
|
||||
setIsConfirmationModalOpen(false);
|
||||
};
|
||||
|
||||
const workspaceInvitations = useRecoilValue(workspaceInvitationsState);
|
||||
const setWorkspaceInvitations = useSetRecoilState(workspaceInvitationsState);
|
||||
|
||||
useGetWorkspaceInvitationsQuery({
|
||||
onError: (error: Error) => {
|
||||
enqueueSnackBar(error.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
setWorkspaceInvitations(data?.findWorkspaceInvitations ?? []);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRemoveWorkspaceInvitation = async (appTokenId: string) => {
|
||||
const result = await deleteWorkspaceInvitation({ appTokenId });
|
||||
if (isDefined(result.errors)) {
|
||||
enqueueSnackBar('Error deleting invitation', {
|
||||
variant: SnackBarVariant.Error,
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendWorkspaceInvitation = async (appTokenId: string) => {
|
||||
const result = await resendInvitation({ appTokenId });
|
||||
if (isDefined(result.errors)) {
|
||||
enqueueSnackBar('Error resending invitation', {
|
||||
variant: SnackBarVariant.Error,
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getExpiresAtText = (expiresAt: string) => {
|
||||
const expiresAtDate = new Date(expiresAt);
|
||||
return expiresAtDate < new Date()
|
||||
? 'Expired'
|
||||
: formatDistanceToNow(new Date(expiresAt));
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
Icon={IconUsers}
|
||||
@ -60,18 +137,11 @@ export const SettingsWorkspaceMembers = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Invite by email"
|
||||
description="Send an invite email to your team"
|
||||
/>
|
||||
<WorkspaceInviteTeam />
|
||||
</Section>
|
||||
{currentWorkspace?.inviteHash && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Or send an invite link"
|
||||
description="Copy and send an invite link directly"
|
||||
title="Invite by link"
|
||||
description="Share this link to invite users to join your workspace"
|
||||
/>
|
||||
<WorkspaceInviteLink
|
||||
inviteLink={`${window.location.origin}/invite/${currentWorkspace?.inviteHash}`}
|
||||
@ -83,27 +153,125 @@ export const SettingsWorkspaceMembers = () => {
|
||||
title="Members"
|
||||
description="Manage the members of your space here"
|
||||
/>
|
||||
{workspaceMembers?.map((member) => (
|
||||
<WorkspaceMemberCard
|
||||
key={member.id}
|
||||
workspaceMember={member as WorkspaceMember}
|
||||
accessory={
|
||||
currentWorkspaceMember?.id !== member.id && (
|
||||
<StyledButtonContainer>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setIsConfirmationModalOpen(true);
|
||||
setWorkspaceMemberToDelete(member.id);
|
||||
}}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
<Table>
|
||||
<StyledTableHeaderRow>
|
||||
<TableRow>
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Email</TableHeader>
|
||||
<TableHeader align={'right'}></TableHeader>
|
||||
</TableRow>
|
||||
</StyledTableHeaderRow>
|
||||
{workspaceMembers?.map((workspaceMember) => (
|
||||
<StyledTable key={workspaceMember.id}>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<StyledText
|
||||
PrefixComponent={
|
||||
<Avatar
|
||||
avatarUrl={workspaceMember.avatarUrl}
|
||||
placeholderColorSeed={workspaceMember.id}
|
||||
placeholder={workspaceMember.name.firstName ?? ''}
|
||||
type="rounded"
|
||||
size="sm"
|
||||
/>
|
||||
}
|
||||
text={
|
||||
workspaceMember.name.firstName +
|
||||
' ' +
|
||||
workspaceMember.name.lastName
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StyledText
|
||||
text={workspaceMember.userEmail}
|
||||
color={theme.font.color.secondary}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align={'right'}>
|
||||
{currentWorkspaceMember?.id !== workspaceMember.id && (
|
||||
<StyledButtonContainer>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setIsConfirmationModalOpen(true);
|
||||
setWorkspaceMemberToDelete(workspaceMember.id);
|
||||
}}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</StyledTable>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Invite by email"
|
||||
description="Send an invite email to your team"
|
||||
/>
|
||||
<WorkspaceInviteTeam />
|
||||
{isNonEmptyArray(workspaceInvitations) && (
|
||||
<Table>
|
||||
<StyledTableHeaderRow>
|
||||
<TableRow gridAutoColumns={`1fr 1fr ${theme.spacing(22)}`}>
|
||||
<TableHeader>Email</TableHeader>
|
||||
<TableHeader align={'right'}>Expires in</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
</StyledTableHeaderRow>
|
||||
{workspaceInvitations?.map((workspaceInvitation) => (
|
||||
<StyledTable key={workspaceInvitation.id}>
|
||||
<TableRow gridAutoColumns={`1fr 1fr ${theme.spacing(22)}`}>
|
||||
<TableCell>
|
||||
<StyledText
|
||||
PrefixComponent={
|
||||
<IconMail
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
text={workspaceInvitation.email}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align={'right'}>
|
||||
<Status
|
||||
color={'gray'}
|
||||
text={getExpiresAtText(workspaceInvitation.expiresAt)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align={'right'}>
|
||||
<StyledButtonContainer>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleResendWorkspaceInvitation(
|
||||
workspaceInvitation.id,
|
||||
);
|
||||
}}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
Icon={IconReload}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleRemoveWorkspaceInvitation(
|
||||
workspaceInvitation.id,
|
||||
);
|
||||
}}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</StyledTable>
|
||||
))}
|
||||
</Table>
|
||||
)}
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
<ConfirmationModal
|
||||
|
||||
Reference in New Issue
Block a user