Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1813,6 +1813,7 @@ export type Mutation = {
deleteSSOIdentityProvider: DeleteSsoOutput;
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
deleteUser: User;
deleteUserFromWorkspace: UserWorkspace;
deleteWebhook: Scalars['Boolean'];
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
Expand Down Expand Up @@ -2295,6 +2296,11 @@ export type MutationDeleteTwoFactorAuthenticationMethodArgs = {
};


export type MutationDeleteUserFromWorkspaceArgs = {
workspaceMemberIdToDelete: Scalars['String'];
};


export type MutationDeleteWebhookArgs = {
input: DeleteWebhookInput;
};
Expand Down Expand Up @@ -5917,6 +5923,13 @@ export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }

export type DeleteUserAccountMutation = { __typename?: 'Mutation', deleteUser: { __typename?: 'User', id: string } };

export type DeleteUserWorkspaceMutationVariables = Exact<{
workspaceMemberIdToDelete: Scalars['String'];
}>;


export type DeleteUserWorkspaceMutation = { __typename?: 'Mutation', deleteUserFromWorkspace: { __typename?: 'UserWorkspace', id: string } };

export type UploadProfilePictureMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
Expand Down Expand Up @@ -12389,6 +12402,39 @@ export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOp
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>;
export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const DeleteUserWorkspaceDocument = gql`
mutation DeleteUserWorkspace($workspaceMemberIdToDelete: String!) {
deleteUserFromWorkspace(workspaceMemberIdToDelete: $workspaceMemberIdToDelete) {
id
}
}
`;
export type DeleteUserWorkspaceMutationFn = Apollo.MutationFunction<DeleteUserWorkspaceMutation, DeleteUserWorkspaceMutationVariables>;

/**
* __useDeleteUserWorkspaceMutation__
*
* To run a mutation, you first call `useDeleteUserWorkspaceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteUserWorkspaceMutation` 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 [deleteUserWorkspaceMutation, { data, loading, error }] = useDeleteUserWorkspaceMutation({
* variables: {
* workspaceMemberIdToDelete: // value for 'workspaceMemberIdToDelete'
* },
* });
*/
export function useDeleteUserWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<DeleteUserWorkspaceMutation, DeleteUserWorkspaceMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteUserWorkspaceMutation, DeleteUserWorkspaceMutationVariables>(DeleteUserWorkspaceDocument, options);
}
export type DeleteUserWorkspaceMutationHookResult = ReturnType<typeof useDeleteUserWorkspaceMutation>;
export type DeleteUserWorkspaceMutationResult = Apollo.MutationResult<DeleteUserWorkspaceMutation>;
export type DeleteUserWorkspaceMutationOptions = Apollo.BaseMutationOptions<DeleteUserWorkspaceMutation, DeleteUserWorkspaceMutationVariables>;
export const UploadProfilePictureDocument = gql`
mutation UploadProfilePicture($file: Upload!) {
uploadProfilePicture(file: $file) {
Expand Down
6 changes: 6 additions & 0 deletions packages/twenty-front/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1768,6 +1768,7 @@ export type Mutation = {
deleteSSOIdentityProvider: DeleteSsoOutput;
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
deleteUser: User;
deleteUserFromWorkspace: UserWorkspace;
deleteWebhook: Scalars['Boolean'];
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
Expand Down Expand Up @@ -2226,6 +2227,11 @@ export type MutationDeleteTwoFactorAuthenticationMethodArgs = {
};


export type MutationDeleteUserFromWorkspaceArgs = {
workspaceMemberIdToDelete: Scalars['String'];
};


export type MutationDeleteWebhookArgs = {
input: DeleteWebhookInput;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({

const hasReadPermission = objectPermissions.canReadObjectRecords;

const { data, loading, error, fetchMore } =
const { data, loading, error, fetchMore, refetch } =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about exposing the refetch for useFindMany for this new use case only 🤔 Could we use optimistic update / apollo cache mutation instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not? by fear it would be use in the wrong context?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

@lucasbordeau lucasbordeau Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's dedicated tooling for fetchMore and doing a refetch like that has not been QAed, so I would not expose this method.

useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem || !hasReadPermission,
variables: {
Expand Down Expand Up @@ -126,5 +126,6 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
queryIdentifier,
hasNextPage,
pageInfo,
refetch,
};
};
Original file line number Diff line number Diff line change
@@ -1,52 +1,122 @@
import { useRecoilValue } from 'recoil';

import { useAuth } from '@/auth/hooks/useAuth';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { useDeleteUserAccountMutation } from '~/generated-metadata/graphql';
import {
useDeleteUserAccountMutation,
useDeleteUserWorkspaceMutation,
} from '~/generated-metadata/graphql';

const DELETE_ACCOUNT_MODAL_ID = 'delete-account-modal';
const LEAVE_WORKSPACE_MODAL_ID = 'leave-workspace-modal';

const StyledDiv = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;

export const DeleteAccount = () => {
const { t } = useLingui();
const { openModal } = useModal();
const { enqueueErrorSnackBar } = useSnackBar();

const [deleteUserAccount] = useDeleteUserAccountMutation();
const [deleteUserFromWorkspace] = useDeleteUserWorkspaceMutation();
const currentUser = useRecoilValue(currentUserState);
const userEmail = currentUser?.email;
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
const { signOut } = useAuth();
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const availableWorkspacesCount =
countAvailableWorkspaces(availableWorkspaces);

const userHasMultipleWorkspaces = availableWorkspacesCount > 1;

const deleteAccount = async () => {
await deleteUserAccount();
await signOut();
};

const leaveWorkspace = async () => {
if (!isDefined(currentWorkspaceMemberId)) {
enqueueErrorSnackBar({
message: t`Current workspace member not found.`,
});
return;
}

await deleteUserFromWorkspace?.({
variables: {
workspaceMemberIdToDelete: currentWorkspaceMemberId,
},
});
await signOut();
};
Comment on lines +51 to +65
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: missing required argument workspaceMemberIdToDelete for mutation - backend expects this parameter but frontend doesn't provide it

Check backend mutation signature:

deleteUserFromWorkspace(
  @Args('workspaceMemberIdToDelete') workspaceMemberIdToDelete: string,
  ...
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx
Line: 45:48

Comment:
**logic:** missing required argument `workspaceMemberIdToDelete` for mutation - backend expects this parameter but frontend doesn't provide it

Check backend mutation signature:
```typescript
deleteUserFromWorkspace(
  @Args('workspaceMemberIdToDelete') workspaceMemberIdToDelete: string,
  ...
)
```

How can I resolve this? If you propose a fix, please make it concise.


return (
<>
<H2Title
title={t`Danger zone`}
description={t`Delete account and all the associated data`}
description={
userHasMultipleWorkspaces
? t`Delete account and all the associated data or leave workspace`
: t`Delete account and all the associated data`
}
/>
{userHasMultipleWorkspaces && (
<StyledDiv>
<Button
accent="danger"
onClick={() => openModal(LEAVE_WORKSPACE_MODAL_ID)}
variant="secondary"
title={t`Leave workspace`}
/>

<ConfirmationModal
confirmationValue={userEmail}
confirmationPlaceholder={userEmail ?? ''}
modalId={LEAVE_WORKSPACE_MODAL_ID}
title={t`Leave workspace`}
subtitle={
<>
{t`This action cannot be undone. This will permanently remove your membership from this workspace.`}
<br />
{t`Please type in your email to confirm.`}
</>
}
onConfirmClick={leaveWorkspace}
confirmButtonText={t`Leave workspace`}
/>
</StyledDiv>
)}
<Button
accent="danger"
onClick={() => openModal(DELETE_ACCOUNT_MODAL_ID)}
variant="secondary"
title={t`Delete account`}
/>

<ConfirmationModal
confirmationValue={userEmail}
confirmationPlaceholder={userEmail ?? ''}
modalId={DELETE_ACCOUNT_MODAL_ID}
title={t`Account Deletion`}
subtitle={
<>
This action cannot be undone. This will permanently delete your
entire account. <br /> Please type in your email to confirm.
{t`This action cannot be undone. This will permanently delete your
entire account.`}
<br />
{t`Please type in your email to confirm.`}
</>
}
onConfirmClick={deleteAccount}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';

export const DELETE_USER_FROM_WORKSPACE = gql`
mutation DeleteUserWorkspace($workspaceMemberIdToDelete: String!) {
deleteUserFromWorkspace(
workspaceMemberIdToDelete: $workspaceMemberIdToDelete
) {
id
}
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useDebounce } from 'use-debounce';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
Expand Down Expand Up @@ -47,6 +46,7 @@ import {
import { IconButton } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import {
useDeleteUserWorkspaceMutation,
useGetWorkspaceInvitationsQuery,
useImpersonateMutation,
} from '~/generated-metadata/graphql';
Expand Down Expand Up @@ -142,23 +142,27 @@ export const SettingsWorkspaceMembers = () => {
fetchMoreRecords,
hasNextPage,
loading,
refetch: refetchWorkspaceMembers,
} = useFindManyRecords<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
filter: searchServerFilter,
});
const { deleteOneRecord: deleteOneWorkspaceMember } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});

const { resendInvitation } = useResendWorkspaceInvitation();
const { deleteWorkspaceInvitation } = useDeleteWorkspaceInvitation();
const [deleteUserFromWorkspace] = useDeleteUserWorkspaceMutation();

const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);

const handleRemoveWorkspaceMember = async (workspaceMemberId: string) => {
await deleteOneWorkspaceMember?.(workspaceMemberId);
await deleteUserFromWorkspace?.({
variables: {
workspaceMemberIdToDelete: workspaceMemberId,
},
});
setWorkspaceMemberToDelete(undefined);
refetchWorkspaceMembers();
};

const handleImpersonate = async (targetWorkspaceMember: WorkspaceMember) => {
Expand Down Expand Up @@ -485,18 +489,19 @@ export const SettingsWorkspaceMembers = () => {
</SettingsPageContainer>
<ConfirmationModal
modalId={WORKSPACE_MEMBER_DELETION_MODAL_ID}
title={t`Account Deletion`}
title={t`Remove member from workspace`}
subtitle={
<Trans>
This action cannot be undone. This will permanently delete this user
and remove them from all their assignments.
This action cannot be undone. This will permanently remove this
member from this workspace and remove them from all their
assignments.
</Trans>
}
onConfirmClick={() =>
workspaceMemberToDelete &&
handleRemoveWorkspaceMember(workspaceMemberToDelete)
}
confirmButtonText={t`Delete account`}
confirmButtonText={t`Remove member`}
/>
</SubMenuTopBarContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { TrashCleanupModule } from 'src/engine/trash-cleanup/trash-cleanup.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-seeder.module';
import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module';
import { TrashCleanupModule } from 'src/engine/trash-cleanup/trash-cleanup.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { CalendarEventImportManagerModule } from 'src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
Expand Down
Loading
Loading