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:
Antoine Moreaux
2024-09-18 23:27:31 +02:00
committed by GitHub
parent ad18c44f25
commit 89c97993e3
81 changed files with 1726 additions and 363 deletions

View File

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_WORKSPACE_INVITATION = gql`
mutation DeleteWorkspaceInvitation($appTokenId: String!) {
deleteWorkspaceInvitation(appTokenId: $appTokenId)
}
`;

View File

@ -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
}
}
}
}
`;

View File

@ -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
}
}
}
}
`;

View File

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const GET_WORKSPACE_INVITATIONS = gql`
query GetWorkspaceInvitations {
findWorkspaceInvitations {
id
email
expiresAt
}
}
`;

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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: [],
});